diff --git a/src/lib/books/reader/PdfReader.svelte b/src/lib/books/reader/PdfReader.svelte index 2eddf56..76c8608 100644 --- a/src/lib/books/reader/PdfReader.svelte +++ b/src/lib/books/reader/PdfReader.svelte @@ -14,6 +14,7 @@ type Rect } from '#lib/books/shared.js'; import { clampZoom, pdfRows, rowIndexOf, type Spread } from './layout.js'; + import { closePdf } from './pdf.js'; import { type EngineProps, type TocItem } from './types.js'; let { @@ -370,6 +371,7 @@ onMount(() => { let disposed = false; + let loadingTask: ReturnType | undefined; const measure = () => { width = scroller.clientWidth; height = scroller.clientHeight; @@ -450,15 +452,15 @@ pdfjs = await import('pdfjs-dist'); pdfjs.GlobalWorkerOptions.workerSrc = workerUrl; onloading(0); - const task = pdfjs.getDocument({ + const task = (loadingTask = pdfjs.getDocument({ url, withCredentials: true, rangeChunkSize: 1 << 20 - }); + })); task.onProgress = ({ loaded, total }: { loaded: number; total: number }) => onloading(total ? Math.min(1, loaded / total) : null); const doc = await task.promise; - if (disposed) return void doc.destroy(); + if (disposed) return void closePdf(task); pdf = doc; onloading(null); // Use the first page's size for every page up front; real sizes fill in quickly. @@ -499,7 +501,8 @@ resize.disconnect(); observer?.disconnect(); for (const num of [...live.keys()]) unrender(num); - void pdf?.destroy(); + // pdf.js 6 has no PDFDocumentProxy.destroy(); calling it threw on every unmount. + void closePdf(loadingTask); }; }); diff --git a/src/lib/books/reader/pdf.test.ts b/src/lib/books/reader/pdf.test.ts new file mode 100644 index 0000000..879ad1d --- /dev/null +++ b/src/lib/books/reader/pdf.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest'; +import { closePdf } from './pdf.js'; + +// A tiny one-page PDF (pdf.js tolerates the missing xref table). +const PDF = + '%PDF-1.4\n1 0 obj<>endobj\n' + + '2 0 obj<>endobj\n' + + '3 0 obj<>endobj\ntrailer<>\n%%EOF'; + +const open = async () => { + const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs'); + return pdfjs.getDocument({ data: new TextEncoder().encode(PDF) }); +}; + +describe('closePdf', () => { + it('the document itself has no destroy() in pdf.js 6 (the old teardown threw)', async () => { + const task = await open(); + const doc = await task.promise; + expect(doc.numPages).toBe(1); + expect((doc as unknown as { destroy?: unknown }).destroy).toBeUndefined(); + await closePdf(task); + }); + + it('destroys a loaded document via its loading task, idempotently', async () => { + const task = await open(); + await task.promise; + await closePdf(task); + expect(task.destroyed).toBe(true); + await expect(closePdf(task)).resolves.toBeUndefined(); + }); + + it('can close while still loading, or with nothing open', async () => { + const task = await open(); + task.promise.catch(() => {}); // rejected by the destroy + await closePdf(task); + expect(task.destroyed).toBe(true); + await expect(closePdf(undefined)).resolves.toBeUndefined(); + }); +}); diff --git a/src/lib/books/reader/pdf.ts b/src/lib/books/reader/pdf.ts new file mode 100644 index 0000000..0a7a97e --- /dev/null +++ b/src/lib/books/reader/pdf.ts @@ -0,0 +1,18 @@ +// pdf.js lifecycle helpers for the reader. + +type LoadingTask = { destroyed: boolean; destroy(): Promise }; + +/** + * Closes a PDF opened with pdfjs.getDocument(). pdf.js 6 removed PDFDocumentProxy.destroy(); + * the loading task owns the document, its network requests and the worker, so tearing it + * down works both mid-load and after. Never throws: it runs while the reader unmounts + * (e.g. the back button), where an exception would break the navigation. + */ +export async function closePdf(task: LoadingTask | null | undefined): Promise { + if (!task || task.destroyed) return; + try { + await task.destroy(); + } catch { + // already torn down + } +}