From f6f94e579d527a1ee7abd787bf9b4f9d8446b4db Mon Sep 17 00:00:00 2001 From: Daniele Date: Mon, 10 Aug 2026 17:53:00 +0100 Subject: [PATCH] fix(file-viewer): keep rendering a PDF after a file-watcher reload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An open PDF went blank the moment the watcher reported the file had changed, and stayed blank for the rest of the session — every later version of the file too. ._teardown() released the previous document with PDFDocumentProxy.destroy(), a method pdf.js no longer has: a document is torn down through its loading task. The absent method threw a TypeError, and _teardown() is the *first* statement of _open(): the page list had already been emptied, so nothing after the throw ran — no new document was loaded, and the emptied .pdfv-pages had nothing to refill it. Since _doc was never cleared either, every subsequent src hit the same throw, which is why the viewer never recovered. _open() is async and its caller (updated()) does not await it, so the TypeError surfaced only as an unhandled rejection. Tear the document down through doc.loadingTask.destroy() instead, and swallow its failure: releasing the previous document must never be able to stop the next one from loading. The same call in _open()'s stale-document path had the identical bug. Verified in headless Chromium against the real component: swapping the blob URL the way FileViewerBase._load does now reloads the document (5 pages -> 7 -> 5, correct text layer, no exceptions), including three reloads fired back-to-back so the stale-document path is exercised. --- web/components/shared/pdf-view.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/web/components/shared/pdf-view.js b/web/components/shared/pdf-view.js index 9933b16..28a1e17 100644 --- a/web/components/shared/pdf-view.js +++ b/web/components/shared/pdf-view.js @@ -62,6 +62,23 @@ const MAX_DPR = 2; let _pdfjsPromise = null; +/** + * Tear a document down. pdf.js dropped `PDFDocumentProxy.destroy()` — the + * document is released through its loading task — and calling the absent method + * threw a TypeError out of `_teardown()`, which is called *first* in `_open()`: + * the page list had already been emptied, the rest of `_open()` never ran, and + * the viewer stayed blank for the rest of its life (every later src hit the same + * throw). That is what a file-watcher reload of an open PDF looked like. Hence + * also the swallow: releasing the previous document must never be able to stop + * the next one from loading. + */ +function destroyDoc(doc) { + try { + const p = doc?.loadingTask?.destroy(); + if (p?.catch) p.catch(() => { /* already gone */ }); + } catch { /* already gone */ } +} + /** Import pdf.js once per page load and point it at the vendored worker. */ function loadPdfjs() { if (!_pdfjsPromise) { @@ -131,7 +148,7 @@ export class PdfView extends LightElement { standardFontDataUrl: STD_FONTS_URL, }).promise; // A newer src landed while we were loading — drop this one on the floor. - if (seq !== this._loadSeq) { doc.destroy(); return; } + if (seq !== this._loadSeq) { destroyDoc(doc); return; } this._pdfjs = pdfjs; this._doc = doc; this._total = doc.numPages; @@ -186,8 +203,9 @@ export class PdfView extends LightElement { for (const slot of this._slots) this._release(slot); this._slots = []; this.querySelector('.pdfv-pages')?.replaceChildren(); - this._doc?.destroy(); + const doc = this._doc; this._doc = null; + destroyDoc(doc); this._lastWidth = 0; }