[web] Cleanup PDFjs instances in visual editor (#33022)

GitOrigin-RevId: 2aa9ab01f88196fb56dc41749977ca33295c964f
This commit is contained in:
Mathias Jakobsen
2026-04-27 08:05:38 +00:00
committed by Copybot
parent 5c7db11d9b
commit 5e675664c6
3 changed files with 234 additions and 2 deletions
@@ -0,0 +1,26 @@
/**
* Coordinates PDF.js document destroys across GraphicsWidget instances.
*
* PDF.js uses a single shared worker (GlobalWorkerOptions.workerPort) and
* rejects getDocument while any previous PDFDocumentLoadingTask.destroy()
* are in flight.
*/
export class PdfDestroyLock {
private readonly pendingDestroys = new Set<Promise<unknown>>()
/** Resolves once no pdf.destroy() is in flight. */
async waitForPending(): Promise<void> {
// Loop in case a new destroy is scheduled while we await existing ones.
while (this.pendingDestroys.size > 0) {
await Promise.all(this.pendingDestroys)
}
}
/** Registers an in-flight destroy so waitForPending() will wait for it. */
schedule<T>(destroyFn: () => Promise<T>): Promise<T> {
const task = destroyFn()
this.pendingDestroys.add(task)
task.finally(() => this.pendingDestroys.delete(task))
return task
}
}
@@ -4,10 +4,24 @@ import { isEqual } from 'lodash'
import { FigureData } from '../../figure-modal'
import { debugConsole } from '@/utils/debugging'
import { PreviewPath } from '../../../../../../../types/preview-path'
import type { PDFDocumentProxy } from 'pdfjs-dist/types/src/display/api'
import { PdfDestroyLock } from '../utils/pdf-destroy-lock'
// Module level to synchronize across all GraphicsWidgets
const pdfDestroyLock = new PdfDestroyLock()
function schedulePdfDestroy(pdf: PDFDocumentProxy) {
pdfDestroyLock.schedule(() =>
pdf.destroy().catch(() => {
debugConsole.warn('Failed to destroy PDFjs instance')
})
)
}
export class GraphicsWidget extends WidgetType {
destroyed = false
height = 300 // for estimatedHeight, updated when the image is loaded
pdfInstance: PDFDocumentProxy | null = null
constructor(
public filePath: string,
@@ -54,6 +68,10 @@ export class GraphicsWidget extends WidgetType {
) {
return true
}
if (this.pdfInstance) {
schedulePdfDestroy(this.pdfInstance)
this.pdfInstance = null
}
this.renderGraphic(element, view)
view.requestMeasure()
return true
@@ -72,6 +90,10 @@ export class GraphicsWidget extends WidgetType {
destroy() {
this.destroyed = true
if (this.pdfInstance) {
schedulePdfDestroy(this.pdfInstance)
this.pdfInstance = null
}
}
coordsAt(element: HTMLElement) {
@@ -104,7 +126,11 @@ export class GraphicsWidget extends WidgetType {
{
const canvas = document.createElement('canvas')
canvas.classList.add('ol-cm-graphics')
this.renderPDF(view, canvas, preview.url).catch(debugConsole.error)
this.renderPDF(view, canvas, preview.url).catch(err => {
if (!this.destroyed) {
debugConsole.error('Failed to render PDF graphics widget', err)
}
})
element.append(canvas)
}
break
@@ -249,10 +275,23 @@ export class GraphicsWidget extends WidgetType {
return
}
await pdfDestroyLock.waitForPending()
if (this.destroyed) {
return
}
const pdf = await loadPdfDocumentFromUrl(url).promise
const page = await pdf.getPage(1)
this.pdfInstance = pdf
// bail out if loading the PDF took too long
if (this.destroyed) {
schedulePdfDestroy(pdf)
this.pdfInstance = null
return
}
const page = await pdf.getPage(1)
if (this.destroyed) {
return
}