@laminapdf/viewer 0.2.0-alpha.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +48 -6
  2. package/dist/client.d.ts +60 -4
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +219 -22
  5. package/dist/client.js.map +1 -1
  6. package/dist/edit/session.d.ts +113 -0
  7. package/dist/edit/session.d.ts.map +1 -0
  8. package/dist/edit/session.js +232 -0
  9. package/dist/edit/session.js.map +1 -0
  10. package/dist/edit-log.d.ts +37 -0
  11. package/dist/edit-log.d.ts.map +1 -0
  12. package/dist/edit-log.js +98 -0
  13. package/dist/edit-log.js.map +1 -0
  14. package/dist/find/controller.d.ts +6 -0
  15. package/dist/find/controller.d.ts.map +1 -1
  16. package/dist/find/controller.js +69 -2
  17. package/dist/find/controller.js.map +1 -1
  18. package/dist/history/history.d.ts +9 -0
  19. package/dist/history/history.d.ts.map +1 -1
  20. package/dist/history/history.js.map +1 -1
  21. package/dist/index.d.ts +9 -6
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/layers/link-layer.d.ts +33 -0
  26. package/dist/layers/link-layer.d.ts.map +1 -1
  27. package/dist/layers/link-layer.js +251 -3
  28. package/dist/layers/link-layer.js.map +1 -1
  29. package/dist/layers/link-preview.d.ts +77 -0
  30. package/dist/layers/link-preview.d.ts.map +1 -0
  31. package/dist/layers/link-preview.js +202 -0
  32. package/dist/layers/link-preview.js.map +1 -0
  33. package/dist/layers/text-layer.d.ts +7 -3
  34. package/dist/layers/text-layer.d.ts.map +1 -1
  35. package/dist/layers/text-layer.js +3 -5
  36. package/dist/layers/text-layer.js.map +1 -1
  37. package/dist/layout/page-layout.d.ts +7 -0
  38. package/dist/layout/page-layout.d.ts.map +1 -1
  39. package/dist/layout/page-layout.js +15 -0
  40. package/dist/layout/page-layout.js.map +1 -1
  41. package/dist/protocol.d.ts +145 -1
  42. package/dist/protocol.d.ts.map +1 -1
  43. package/dist/protocol.js +17 -2
  44. package/dist/protocol.js.map +1 -1
  45. package/dist/thumbnails/strip.d.ts.map +1 -1
  46. package/dist/thumbnails/strip.js +34 -1
  47. package/dist/thumbnails/strip.js.map +1 -1
  48. package/dist/view/previews.d.ts +47 -0
  49. package/dist/view/previews.d.ts.map +1 -0
  50. package/dist/view/previews.js +97 -0
  51. package/dist/view/previews.js.map +1 -0
  52. package/dist/view/viewer.d.ts +85 -7
  53. package/dist/view/viewer.d.ts.map +1 -1
  54. package/dist/view/viewer.js +347 -12
  55. package/dist/view/viewer.js.map +1 -1
  56. package/dist/worker/document.d.ts +56 -5
  57. package/dist/worker/document.d.ts.map +1 -1
  58. package/dist/worker/document.js +73 -2
  59. package/dist/worker/document.js.map +1 -1
  60. package/dist/worker/edit.d.ts +60 -0
  61. package/dist/worker/edit.d.ts.map +1 -0
  62. package/dist/worker/edit.js +383 -0
  63. package/dist/worker/edit.js.map +1 -0
  64. package/dist/worker/find.d.ts +2 -2
  65. package/dist/worker/find.d.ts.map +1 -1
  66. package/dist/worker/find.js +3 -3
  67. package/dist/worker/find.js.map +1 -1
  68. package/dist/worker/fingerprint.d.ts +10 -0
  69. package/dist/worker/fingerprint.d.ts.map +1 -0
  70. package/dist/worker/fingerprint.js +42 -0
  71. package/dist/worker/fingerprint.js.map +1 -0
  72. package/dist/worker/methods.d.ts.map +1 -1
  73. package/dist/worker/methods.js +34 -0
  74. package/dist/worker/methods.js.map +1 -1
  75. package/dist/worker/navigation.d.ts +2 -2
  76. package/dist/worker/navigation.d.ts.map +1 -1
  77. package/dist/worker/navigation.js +3 -3
  78. package/dist/worker/navigation.js.map +1 -1
  79. package/dist/worker/recent-pages.d.ts +2 -0
  80. package/dist/worker/recent-pages.d.ts.map +1 -1
  81. package/dist/worker/recent-pages.js +15 -0
  82. package/dist/worker/recent-pages.js.map +1 -1
  83. package/dist/worker/session.d.ts +61 -0
  84. package/dist/worker/session.d.ts.map +1 -0
  85. package/dist/worker/session.js +159 -0
  86. package/dist/worker/session.js.map +1 -0
  87. package/dist/worker/text.d.ts +2 -2
  88. package/dist/worker/text.d.ts.map +1 -1
  89. package/dist/worker/text.js +5 -4
  90. package/dist/worker/text.js.map +1 -1
  91. package/package.json +16 -10
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  - **The view** handles layout, virtualisation, canvases and zoom.
6
6
  - **The document Web Worker**, one per open document, opens and renders the file away from the page's main thread.
7
7
 
8
- The package is private and ESM-only.
8
+ The package is ESM-only.
9
9
 
10
10
  ```ts
11
11
  import pdfiumWasm from "@embedpdf/pdfium/pdfium.wasm?url";
@@ -48,10 +48,12 @@ The view keeps only the pages near the viewport in the DOM and paints each in tw
48
48
  | `jumpToPage(index)`, `jumpToDestination(target)` | Like `goToPage` and `goToDestination`, but recorded as history jumps, so Back returns to where the view was. |
49
49
  | `history` | Back and forward through the jumps (below). |
50
50
  | `getState()`, `subscribe(listener)` | The view state (`currentPage`, `pageCount`, `zoom`, `percent`, `rotation`, `scrollTop`, `visibleRange`), for `useSyncExternalStore`. |
51
- | `on("pagechange" \| "zoomchange" \| "renderstats", …)`, `stats()` | Events and counters. `renderstats` counts renders, for measuring performance. |
51
+ | `on("pagechange" \| "zoomchange" \| "renderstats" \| "invalidate", …)`, `stats()` | Events and counters. `renderstats` counts renders, for measuring performance. `invalidate` lists the pages `invalidate` redrew, each with its size in points now. |
52
52
  | `addLayer({ name, onMount, onUnmount, onZoom })` | Mounts a layer into each page. |
53
53
  | `keepMounted(pages)` | Keeps these pages mounted wherever the view scrolls, replacing the previous set; `[]` releases them. The text layer pins the pages a selection touches. |
54
54
  | `pointAt(x, y)`, `positionOf(point)` | Converts between viewport positions and document points. |
55
+ | `pageSizes()` | Every page's displayed size in points, as the view lays it out, edits included. A thumbnail strip lays its pages out from it. |
56
+ | `applyEdit(ops)`, `invalidate(pages, sizes?)` | Edits the document and redraws the pages the edit touched (below). |
55
57
  | `MIN_ZOOM`, `MAX_ZOOM` | The bounds of a percentage zoom, 10 and 800, for a zoom control's range. |
56
58
 
57
59
  **Layers.** Each page shell stacks a canvas and three slots above it: `div.lamina-layer-text`, `div.lamina-layer-annot` and `div.lamina-layer-highlight`. The text layer fills the first, links the second and find highlights the third, through `addLayer`. A layer with another name gets its own slot on top. Every slot above the text lets the pointer through (`pointer-events: none`), so text stays selectable under it; an element a layer wants clicked, such as a link's anchor, sets `pointer-events: auto` on itself.
@@ -92,6 +94,13 @@ A left the file leaves out keeps the current horizontal scroll, and a top it lea
92
94
 
93
95
  Anchors follow page order, so Tab reaches a page's links in order. Each is named by its `/Contents` or by what it does ("Link to page 3"); `links.words` gives those names in another language. Only the anchors take pointer events, so text beside a link still selects. Style them as `a.lamina-link`; the viewer adds no style sheet.
94
96
 
97
+ **Link previews.** The viewer asks to preview a link inside the document after 300 ms of mouse or pen hover or of visible keyboard focus, or on a 500 ms long-press with a finger that doesn't move. It cancels a long-press's click, and the long-press selects no text. Web links never preview.
98
+
99
+ The viewer emits `linkpreview` with the link, its anchor, how it was asked for (`fine`, `keyboard` or `touch`) and what it shows: a destination, or the sentence of a page action or another file's link. `linkpreviewend` follows 300 ms after the pointer leaves both the link and the preview, or when focus leaves the link. The app draws the preview with these:
100
+ - `viewer.preview(target, { signal })` draws where the link goes, in one render by the document's Web Worker. A destination with a point gets a crop from above and left of the point across its text column, at the reader's zoom clamped to 100–150 % and at most 480 × 180 CSS px. Any other destination gets the top of its page.
101
+ - It returns the bitmap, its CSS size, the destination page and the text of the lines it shows. The viewer keeps the last 8 bitmaps and closes them on a zoom or rotation change and when the document closes, so draw the bitmap at once and never close it. `stats().previewBitmaps` counts them.
102
+ - `viewer.holdLinkPreview(true | false)` keeps the preview while the pointer is on it, `viewer.dismissLinkPreview()` hides it on Escape or Close, and `viewer.followLinkPreview()` follows the previewed link, else the focused one.
103
+
95
104
  **History.** `viewer.history` records jumps: links, `jumpToPage`, `jumpToDestination`, find's moves and thumbnail clicks. Scrolling alone records nothing. Each jump saves the view it leaves, both scroll position and zoom, so Back returns there, not only to that page.
96
105
  - `back()`, `forward()` and `goTo(id)` move the view; `getState()` and `subscribe()` say where Back and Forward go (`back`, `forward`, `current`, each an entry with a page point and a zoom).
97
106
  - A jump that lands where the view already is adds nothing, and a new jump drops what Forward held.
@@ -104,6 +113,32 @@ Anchors follow page order, so Tab reaches a page's links in order. Each is named
104
113
  - **Keyboard and pointer.** A click, or Enter or Space, goes to that page. The arrow keys, Home and End move the focus, and only one thumbnail is in the Tab order.
105
114
  - **Names.** Each thumbnail is named by its page label and number ("Page iv (4 of 120)") and captioned with its label. For another language, pass `label` to replace both and `name` to replace the list's own name.
106
115
 
116
+ **Edits.** `viewer.applyEdit(ops)` edits the open document and redraws the pages the edit touched, in one call. The ops are `@laminapdf/sdk`'s, such as `{ op: "rotatePage", args: { page, rotate: 90 } }`, and apply as one transaction. The call resolves with those pages as they are after the edit: index, ID, rotation and displayed size in points, so a quarter turn swaps width and height.
117
+ - **How an edit reaches the screen.** PDFium draws from the bytes it opened, so the Web Worker writes the edit into memory as an incremental update and opens the render document again from the result. The old render document keeps drawing until the new one is open. The Web Worker runs one call at a time, so every render answered before the edit drew the old file, and every render after it draws the new one. A render the view asked for before an edit and that answers after it is dropped, never cached, and asked for again.
118
+ - **What the viewer redraws.** Only the touched pages: their bitmaps go, here and in a thumbnail strip, and their layers read their text and links again. Every other page keeps what it drew. A page in view at its old size keeps its pixels until sharp tiles cover them, so it never flashes. A page whose size changed paints its quick pass first, and the page at the top of the viewport stays there.
119
+ - **What changes nothing.** An edit of the document's metadata touches no page: the Web Worker opens nothing again, and the viewer redraws nothing. A failed edit, such as one naming a page that doesn't exist, rejects and leaves the document and the pages as they were. So does an edit whose new file the backend can't open: the Web Worker undoes it. An edit that adds, removes or moves pages is refused for now, since the view finds pages by index.
120
+ - **When an edit can't be undone.** If the undo after a failed reopen fails too, the call rejects with `worker-failed` saying to open the document again, and every later edit does the same.
121
+ - **Driving it yourself.** `ViewerClient.applyEdit(ops)` makes the edit without the redraw; pass its pages to `viewer.invalidate(indices, sizes)`, which redraws any pages you name.
122
+ - **The zoom follows.** While the zoom fits the width or the page, a page that turned is fitted again, so a page lying on its side never runs past the window.
123
+
124
+ **The edit session.** `viewer.edits` keeps the open document's history and is the store an editor reads (`getState()`, `subscribe()`):
125
+
126
+ ```ts
127
+ await viewer.edits.apply([{ op: "rotatePage", args: { page, rotate: 90 } }], "Rotate page 5");
128
+ await viewer.edits.undo(); // the page stands again
129
+ await viewer.edits.redo();
130
+ await viewer.edits.goTo(0); // back to the file as opened, in one call
131
+ const { history, busy, blocked, notice } = viewer.edits.getState();
132
+ ```
133
+
134
+ - **One step per edit.** Each edit is a step with its label, its pages and its time. `history.steps` lists them oldest first; the first `history.position` are done and the rest are undone, ready to redo. A new edit drops the undone ones. An edit that changed nothing (setting a value the document already had) makes no step.
135
+ - **Undo and redo redraw too.** They draw only the pages the step touched, as an edit does, and `goTo` draws once however many steps it moves. Calls run one after another in the order they were made, so three quick presses of Undo undo three steps.
136
+ - **When an undo can't restore everything.** A later change can replace part of what a step changed. Then the undo restores the rest and the step is marked `partial`; `notice` holds it until the next edit, and `onReverted(listener)` hears each undo and redo with how it went. An undo that could restore nothing takes the step out of the history.
137
+ - **Unsaved changes.** `history.unsaved` counts the steps between the document now and the file as opened, so undoing every step leaves it clean again. The history keeps 500 steps and then drops the oldest (`history.dropped`); the steps it drops still count.
138
+ - **What blocks editing.** `blocked` says why the document can't be edited: none is open, or it is encrypted, or an edit failed and couldn't be undone, so it must be opened again. An encrypted file's edits can't be written yet, and `OpenedDocument.encrypted` says so when it opens.
139
+ - **A restart keeps the edits.** If the Web Worker crashes, the client opens the file again, replays every step, then undoes the ones that were undone, so the history and the pages are as they were (`restarted`'s `edits` is `restored`). A replay can't recreate an undo or redo that restored only part of a step, or none of it, so after one of those, or when the replay fails, the file opens as it was and `edits` is `lost`.
140
+ - **Without the viewer.** `ViewerClient` has the same calls (`applyEdit(ops, { label })`, `undoEdit()`, `redoEdit()`, `goToStep(n)`, `editHistory()`), each resolving with the pages to redraw and the history, and fires `edit` after each.
141
+
107
142
  **Tuning.** The margin, pool size, pixel cap, cache budget and settle delay are measured by the perf harness ([Performance](#performance)); the first full run kept them.
108
143
 
109
144
  ## The document Web Worker
@@ -144,8 +179,9 @@ The client starts the Web Worker and talks to it; you call `ViewerClient`'s meth
144
179
  - Defaults are `soft` 10 s (`render-slow`) and `hard` 120 s.
145
180
  - At the hard cap the client calls `stop("hard-timeout")`. For a document that needed a password, it emits `render-stuck` instead and waits for the user's `stop()`.
146
181
  - **Crashes:** a crash is the Web Worker's `error` or `messageerror`, or a WebAssembly trap it reports, never silence.
147
- - Pending calls reject with `worker-crashed`.
148
- - A new Web Worker reopens the document from the same source, but nothing that was pending is re-sent. The UI offers Retry, so a page that crashes the engine can't loop.
182
+ - Pending calls reject with `worker-crashed`, and a new Web Worker reopens the document from the same source.
183
+ - `render()` sends again, once, in the new Web Worker, each render that was waiting when the old one was lost. The render that was drawing then isn't re-sent: its page shows an error the UI offers Retry for, so a page that crashes the engine can't loop.
184
+ - When the restart can't reopen the document (an encrypted one waits for its password), a waiting render rejects with an `AbortError`.
149
185
  - `stop()` takes the same path with `reason: "cancelled"`.
150
186
  - **Secrets:** the password crosses to the Web Worker once, when the document opens, and is kept only there. The page keeps the source and the other options.
151
187
  - After a crash of an encrypted document, `password-required` fires and the caller opens again.
@@ -191,6 +227,11 @@ There is one protocol, not two: the viewer's worker is `@laminapdf/sdk/worker`'s
191
227
  | `currentDocument` | — | `{ document, ticket }` of the open document, or `undefined`: `open()` asks after a cancelled open | document |
192
228
  | `closeDocument` | — | — | document |
193
229
  | `pageInfo` | `index` | `PageInfo` (displayed size in points, rotation, ID, label) | document |
230
+ | `applyEdit` | `ops`, the document's `id`, `label?` | `EditResult`: the touched pages' `PageInfo`, after the edit (empty for a metadata edit), and the edit session's `history` | document. `client.applyEdit(ops)`. Saves into memory and opens the render document again when the edit touched pages; records a step unless nothing changed |
231
+ | `undoEdit`, `redoEdit` | the document's `id` | `EditResult` with `reverted`: the step and how it went | document. Redraws as an edit does |
232
+ | `goToStep` | `position`, the document's `id` | `EditResult` with every step undone or redone | document. One redraw; a failure half way takes every move back |
233
+ | `editHistory` | the document's `id` | `EditHistory` | document |
234
+ | `restoreEdits` | `steps` (ops, label, time), `undone`, the document's `id` | `EditResult` | document. A restarted worker's replay |
194
235
  | `find` | `query`, `FindOptions`, `pages` | `FindPageResult[]`: each page's matches as quads, and whether it has no text | `background` |
195
236
  | `render` | ADR 0004 `RenderRequest` | `{ bitmap: ImageBitmap, width, height }` (transferred) | `visible` > `prefetch` > `background` |
196
237
  | `memory` | — | `WorkerMemory`: the bytes of the backend's and Lamina PDF's WebAssembly memories | document. `client.memory()`; the perf harness samples it. |
@@ -232,7 +273,7 @@ The browser suites run in Chromium, Firefox and WebKit over the built package: r
232
273
  - page labels in the names;
233
274
  - the outline and a named destination of the links-and-outlines reference PDF landing on their pages.
234
275
  - Unit tests sit next to their modules (`src/**/*.test.ts`) and run in Node. They cover the layout's cost, counted in steps, and the fit modes for portrait, landscape and rotated pages. They also cover zoom anchoring, tiles, cache eviction and the pool's caps, and where each kind of destination puts the view at each rotation. `test/dependencies.test.ts` checks that `src/` imports no UI framework, and `test/types.test.ts` checks the RPC types.
235
- - Two demo pages are manual checks: run `pnpm --filter @laminapdf/viewer exec vite examples`. `examples/viewer-demo.html` is the viewer, with zoom buttons; `examples/rpc-demo.html` draws page 1 through the RPC alone. The perf harness has its own page, `perf/page/` ([Performance](#performance)).
276
+ - Two demo pages are manual checks: run `pnpm --filter @laminapdf/viewer exec vite demo`. `demo/viewer-demo.html` is the viewer, with zoom buttons; `demo/rpc-demo.html` draws page 1 through the RPC alone. `examples/` holds the examples the docs site shows, under [the examples' rules](../../docs/guidelines/examples.md); `test/browser/show-a-pdf.browser.test.ts` runs `examples/show-a-pdf.ts`. The perf harness has its own page, `perf/page/` ([Performance](#performance)).
236
277
 
237
278
  ### Performance
238
279
 
@@ -246,7 +287,7 @@ cargo xtask viewer-perf [chromium|firefox|webkit] # the same, plus the job summ
246
287
 
247
288
  It writes `target/viewer-perf/<browser>.json` and `summary.md`, and never fails on a missed target: no pull request lane asserts a duration ([ADR 0018](../../docs/decisions/0018-test-strategy.md#2-ci-lanes)).
248
289
 
249
- **What it does.** It opens three seeded files, built with `@laminapdf/sdk` and cached in `perf/.fixtures/`: 10 pages of text with an image each (about 1 MB), 1,000 pages of text, and 300 pages of full-page images. A Vite build of `perf/page/` shows them, and `perf/serve.ts` serves that page gzipped, as Cloudflare serves the app. Playwright drives real input, so every run scrolls the same way:
290
+ **What it does.** It opens four seeded files, built with `@laminapdf/sdk` and cached in `perf/.fixtures/`: 10 pages of text with an image each (about 1 MB), 100 and 1,000 pages of text, and 300 pages of full-page images. A Vite build of `perf/page/` shows them, and `perf/serve.ts` serves that page gzipped, as Cloudflare serves the app. Playwright drives real input, so every run scrolls the same way:
250
291
 
251
292
  | Scenario | Measures | Target |
252
293
  | --- | --- | --- |
@@ -256,6 +297,7 @@ It writes `target/viewer-perf/<browser>.json` and `summary.md`, and never fails
256
297
  | Scroll the 1,000-page file end to end, twice, with wheel steps every 20 ms; stop at a quarter, a half and three quarters | the longest main-thread task, time to sharp after each stop, peak memory, memory after each pass, cancelled renders that painted | no task > 50 ms; sharp ≤ 300 ms; ≤ 600 MB; second pass ≤ first + 5 %; 0 |
257
298
  | Fling across the 300 image pages, then stop | time to sharp | ≤ 300 ms |
258
299
  | Three ctrl+wheel bursts in and out on the 10-page file | the longest frame, time to sharp after each burst | reported |
300
+ | Turn the page in view a quarter (`viewer.applyEdit`), five times, in the 10-, 100- and 1,000-page files; on the 100-page file, go on to 20 turns in a row | the median time from the call to the page's first new pixels (its quick pass), and to sharp; the twentieth turn's first new pixels | ≤ 150 ms at 100 pages ([E1 plan §6](../../docs/plans/editor/e1-editing-foundation.md#6-redrawing-after-an-edit)) |
259
301
 
260
302
  "Time to sharp" runs from the last scroll or zoom event to the moment every visible page turned sharp. Memory is the page's JS heap (Chromium only), the document Web Worker's WebAssembly memories (`client.memory()`), the bitmap cache and the canvases' pixels. Firefox and WebKit have no Long Tasks API, so their longest task is the longest gap between frames, which is an upper bound.
261
303
 
package/dist/client.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { RenderRequest } from "@laminapdf/render";
2
+ import { type DocumentMetadata, type Op, type PlannedSave, type SaveOptions } from "@laminapdf/sdk";
2
3
  import { type WorkerInitOptions, type WorkerWasmSource } from "@laminapdf/sdk/worker";
3
4
  import type { FindOptions } from "./find/fold.js";
4
- import { type BackendInit, type LoadProgress, type MethodName, type NetworkRecord, type OpenedDocument, type Params, type PolicyViolation, type PrintOptions, type Priority, type RenderedBitmap, type Result, type SourceInit, type ViewerOpenOptions, type WorkerNetworkLog } from "./protocol.js";
5
+ import { type BackendInit, type EditHistory, type EditResult, type LoadProgress, type MethodName, type NetworkRecord, type OpenedDocument, type Params, type PolicyViolation, type PrintOptions, type Priority, type RenderedBitmap, type Result, type SavedCopy, type SourceInit, type ViewerOpenOptions, type WorkerNetworkLog } from "./protocol.js";
5
6
  /** `ViewerClient.create`'s options: the backend, Lamina PDF's WebAssembly, the timeouts, the worker and a signal to stop starting. */
6
7
  export interface ViewerOptions {
7
8
  /** The render engine; PDFium needs its `.wasm` URL from your bundler (`pdfium.wasm?url`). */
@@ -69,11 +70,21 @@ export interface ViewerEvents {
69
70
  crashed: {
70
71
  readonly reason: string;
71
72
  };
72
- /** A new worker serves again (and has reopened the document, unless it needed a password). */
73
+ /**
74
+ * A new worker serves again (and has reopened the document, unless it needed a password).
75
+ * `edits` says what became of the edit session's steps: `restored` (applied again, the same
76
+ * history), `lost` (the file is as it was opened), or absent when there were none.
77
+ */
73
78
  restarted: {
74
79
  readonly reason: RestartReason;
75
80
  readonly reopened: boolean;
81
+ readonly edits?: "restored" | "lost";
76
82
  };
83
+ /**
84
+ * An edit, undo or redo was made (the document's semantic dirty state is `history.unsaved`):
85
+ * the pages it touched, as they are now, and the edit session's history.
86
+ */
87
+ edit: EditResult;
77
88
  /** The restarted worker can't reopen the document without its password: open it again. */
78
89
  "password-required": Record<string, never>;
79
90
  /** A restart failed (the backend couldn't load again, …): the client serves nothing until `stop()` works. */
@@ -90,6 +101,13 @@ export interface ViewerCallOptions {
90
101
  /** Default `visible` for renders; document calls run above every render. */
91
102
  readonly priority?: Priority;
92
103
  }
104
+ /**
105
+ * What a `Viewer` needs of the engine it draws from: the open document, its events, renders,
106
+ * and the text, link, find, edit and save calls of the layers and the edit session it builds. A
107
+ * `ViewerClient` over its document worker is one; the desktop app's client over its engine
108
+ * process is another, so the same viewer runs on both.
109
+ */
110
+ export type ViewerEngine = Pick<ViewerClient, "document" | "on" | "render" | "pageText" | "readingOrder" | "structure" | "pageLinks" | "find" | "applyEdit" | "undoEdit" | "redoEdit" | "goToStep" | "editHistory" | "planSave" | "copyEdits" | "adoptEdits">;
93
111
  /** One document worker and the typed RPC to it. */
94
112
  export declare class ViewerClient {
95
113
  #private;
@@ -115,6 +133,44 @@ export declare class ViewerClient {
115
133
  get document(): OpenedDocument | undefined;
116
134
  /** Closes the document in the worker (the worker stays, ready for another `open`). */
117
135
  close(): Promise<void>;
136
+ /**
137
+ * Applies `ops` to the open document as one transaction (`@laminapdf/sdk`'s ops, such as
138
+ * `rotatePage`), and draws the pages it touched from the edited file from then on. The edit
139
+ * session records it as one step named `label` ("Rotate page 5"), which undo takes back; an
140
+ * edit that changed nothing records none. Resolves with those pages as they now are and the
141
+ * history; `document` gives their new sizes too. Pass the pages to `viewer.invalidate`, or call
142
+ * `viewer.applyEdit`, which does both. A failed edit changes nothing. A restarted worker opens
143
+ * the file and applies the session's steps again (`restarted`'s `edits`).
144
+ */
145
+ applyEdit(ops: readonly Op[], options?: ViewerCallOptions & {
146
+ readonly label?: string;
147
+ }): Promise<EditResult>;
148
+ /** Undoes the newest step of the edit session; the pages it touched are drawn as they were. */
149
+ undoEdit(options?: ViewerCallOptions): Promise<EditResult>;
150
+ /** Redoes the oldest undone step. */
151
+ redoEdit(options?: ViewerCallOptions): Promise<EditResult>;
152
+ /** Undoes or redoes steps until `position` of the history's steps are done, drawing once. */
153
+ goToStep(position: number, options?: ViewerCallOptions): Promise<EditResult>;
154
+ /** The edit session's history, read from the worker (after a failed call, which answers none). */
155
+ editHistory(options?: ViewerCallOptions): Promise<EditHistory>;
156
+ /** The open document's metadata as its edits leave it: Info and XMP read together. */
157
+ metadata(call?: ViewerCallOptions): Promise<DocumentMetadata>;
158
+ /** How saving the open document with `options` would go: its constraint, signatures, mode and warnings. */
159
+ planSave(options: SaveOptions, call?: ViewerCallOptions): Promise<PlannedSave>;
160
+ /**
161
+ * Writes the open document as a save with `options` would, for the caller to write where the
162
+ * file lives; the document stays unsaved until `adoptEdits(copy.token)`. Save a copy uses it
163
+ * and never adopts.
164
+ */
165
+ copyEdits(options: SaveOptions, call?: ViewerCallOptions): Promise<SavedCopy>;
166
+ /**
167
+ * Makes the copy `token` names the document's saved file, once it is written where the file
168
+ * lives: the worker rebases onto it, the history marks the point saved, and a restarted worker
169
+ * reopens that file. Emits `edit` with the history.
170
+ *
171
+ * @throws when the document changed since the copy (it stays unsaved).
172
+ */
173
+ adoptEdits(token: number, call?: ViewerCallOptions): Promise<EditHistory>;
118
174
  /**
119
175
  * Every request the document worker has made, and had blocked, since it started: what its
120
176
  * `network` and `csp-violation` events reported, including those sent before anyone listened.
@@ -177,8 +233,8 @@ export declare class ViewerClient {
177
233
  render(request: RenderRequest, options?: ViewerCallOptions): Promise<RenderedBitmap>;
178
234
  /**
179
235
  * Stops whatever the worker is doing by terminating it (the user's Stop, or the hard cap), then
180
- * restarts it as after a crash: pending calls reject, the running one isn't re-sent, and an
181
- * encrypted document asks for its password again.
236
+ * restarts it as after a crash: pending calls reject, renders that were waiting are sent again,
237
+ * the running one isn't, and an encrypted document asks for its password again.
182
238
  */
183
239
  stop(reason?: "cancelled" | "hard-timeout"): Promise<void>;
184
240
  /** Ends the worker for good. */
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js CHANGED
@@ -11,11 +11,13 @@
11
11
  // (`hard-timeout`) or, for an encrypted document, `render-stuck` and a restart only on `stop()`;
12
12
  // - restart after a crash (the worker's error, messageerror or a reported trap; never silence):
13
13
  // pending calls reject with `worker-crashed`, a new worker reopens the document from the same
14
- // source, but the call that was running is never re-sent (the UI offers Retry). An encrypted
14
+ // source, and renders that were waiting are sent again once, there; the render that was running
15
+ // is never re-sent, so a page that crashes the engine can't loop (the UI offers Retry). An encrypted
15
16
  // document isn't reopened: the password lived only in the dead worker, so `password-required`
16
17
  // fires and the caller opens again with the password.
17
18
  import { isLaminaPdfError } from "@laminapdf/sdk";
18
19
  import { connect, Emitter, } from "@laminapdf/sdk/worker";
20
+ import { EditLog } from "./edit-log.js";
19
21
  import { FirstDraw } from "./first-draw.js";
20
22
  import { PRIORITY, renderKey, } from "./protocol.js";
21
23
  /** Default render timeouts (ADR 0009 §3.7), in ms. */
@@ -47,6 +49,10 @@ export class ViewerClient {
47
49
  /** What to reopen after a restart: the source and the options without the password. */
48
50
  #reopen;
49
51
  #document;
52
+ /** The open document's edit session, kept so a restarted worker takes it back. */
53
+ #edits = new EditLog();
54
+ /** The last copy a save wrote (`copyEdits`), until it is adopted or another replaces it. */
55
+ #copy;
50
56
  /** Opens asked for so far: each open's ticket. */
51
57
  #opens = 0;
52
58
  #recovering;
@@ -57,6 +63,8 @@ export class ViewerClient {
57
63
  #timers = new Map();
58
64
  /** The key the worker is drawing now: a render that joins it starts its timers at once. */
59
65
  #drawing;
66
+ /** The render that was drawing when the worker crashed or was stopped: it isn't sent again. */
67
+ #lost;
60
68
  constructor(pdf, options) {
61
69
  this.#pdf = pdf;
62
70
  this.#backend = options.backend;
@@ -178,6 +186,7 @@ export class ViewerClient {
178
186
  }
179
187
  this.#reopen = { source, options: rest };
180
188
  this.#document = opened;
189
+ this.#edits.reset();
181
190
  // Only once it opened: a failed open leaves the shown document's gate as it was.
182
191
  this.#firstDraw = new FirstDraw(opened.id);
183
192
  return opened;
@@ -195,8 +204,121 @@ export class ViewerClient {
195
204
  async close() {
196
205
  this.#reopen = undefined;
197
206
  this.#document = undefined;
207
+ this.#edits.reset();
198
208
  await this.call("closeDocument", []);
199
209
  }
210
+ /**
211
+ * Applies `ops` to the open document as one transaction (`@laminapdf/sdk`'s ops, such as
212
+ * `rotatePage`), and draws the pages it touched from the edited file from then on. The edit
213
+ * session records it as one step named `label` ("Rotate page 5"), which undo takes back; an
214
+ * edit that changed nothing records none. Resolves with those pages as they now are and the
215
+ * history; `document` gives their new sizes too. Pass the pages to `viewer.invalidate`, or call
216
+ * `viewer.applyEdit`, which does both. A failed edit changes nothing. A restarted worker opens
217
+ * the file and applies the session's steps again (`restarted`'s `edits`).
218
+ */
219
+ applyEdit(ops, options) {
220
+ const label = options?.label;
221
+ return this.#edit((id) => this.call("applyEdit", label === undefined ? [ops, id] : [ops, id, label], options), {
222
+ ops,
223
+ label: label ?? "",
224
+ });
225
+ }
226
+ /** Undoes the newest step of the edit session; the pages it touched are drawn as they were. */
227
+ undoEdit(options) {
228
+ return this.#edit((id) => this.call("undoEdit", [id], options));
229
+ }
230
+ /** Redoes the oldest undone step. */
231
+ redoEdit(options) {
232
+ return this.#edit((id) => this.call("redoEdit", [id], options));
233
+ }
234
+ /** Undoes or redoes steps until `position` of the history's steps are done, drawing once. */
235
+ goToStep(position, options) {
236
+ return this.#edit((id) => this.call("goToStep", [position, id], options));
237
+ }
238
+ /** The edit session's history, read from the worker (after a failed call, which answers none). */
239
+ async editHistory(options) {
240
+ if (this.#recovering !== undefined)
241
+ await this.#recovering.catch(() => { });
242
+ const opened = this.#document;
243
+ const history = await this.call("editHistory", [opened?.id ?? 0], options);
244
+ // A failed edit clears the steps undone: what a restart would replay follows.
245
+ if (opened !== undefined && this.#document === opened)
246
+ this.#edits.take({ pages: [], history });
247
+ return history;
248
+ }
249
+ /** The open document's metadata as its edits leave it: Info and XMP read together. */
250
+ async metadata(call) {
251
+ if (this.#recovering !== undefined)
252
+ await this.#recovering.catch(() => { });
253
+ return this.call("metadata", [this.#document?.id ?? 0], call);
254
+ }
255
+ /** How saving the open document with `options` would go: its constraint, signatures, mode and warnings. */
256
+ async planSave(options, call) {
257
+ if (this.#recovering !== undefined)
258
+ await this.#recovering.catch(() => { });
259
+ return this.call("planSave", [options, this.#document?.id ?? 0], call);
260
+ }
261
+ /**
262
+ * Writes the open document as a save with `options` would, for the caller to write where the
263
+ * file lives; the document stays unsaved until `adoptEdits(copy.token)`. Save a copy uses it
264
+ * and never adopts.
265
+ */
266
+ async copyEdits(options, call) {
267
+ if (this.#recovering !== undefined)
268
+ await this.#recovering.catch(() => { });
269
+ const copy = await this.call("copyEdits", [options, this.#document?.id ?? 0], call);
270
+ this.#copy = { token: copy.token, bytes: copy.bytes };
271
+ return copy;
272
+ }
273
+ /**
274
+ * Makes the copy `token` names the document's saved file, once it is written where the file
275
+ * lives: the worker rebases onto it, the history marks the point saved, and a restarted worker
276
+ * reopens that file. Emits `edit` with the history.
277
+ *
278
+ * @throws when the document changed since the copy (it stays unsaved).
279
+ */
280
+ async adoptEdits(token, call) {
281
+ if (this.#recovering !== undefined)
282
+ await this.#recovering.catch(() => { });
283
+ const opened = this.#document;
284
+ const history = await this.call("adoptEdits", [token, opened?.id ?? 0], call);
285
+ const copy = this.#copy;
286
+ if (opened === undefined || this.#document !== opened)
287
+ return history;
288
+ this.#copy = undefined;
289
+ if (copy?.token === token && this.#reopen !== undefined)
290
+ this.#reopen = { ...this.#reopen, source: copy.bytes };
291
+ this.#edits.saved(history);
292
+ this.#events.emit("edit", { pages: [], history });
293
+ return history;
294
+ }
295
+ /**
296
+ * Runs one edit session call on the open document, then keeps what it changed: the pages'
297
+ * sizes in `document`, the steps for a restart, and an `edit` event.
298
+ */
299
+ async #edit(call, edit) {
300
+ if (this.#recovering !== undefined)
301
+ await this.#recovering.catch(() => { });
302
+ const opened = this.#document;
303
+ // 0 names no document: the worker answers `disposed`.
304
+ const result = await call(opened?.id ?? 0);
305
+ // Another document opened meanwhile: this answer is the old one's.
306
+ if (opened === undefined || this.#document !== opened)
307
+ return result;
308
+ this.#takePages(opened, result);
309
+ this.#edits.take(result, edit);
310
+ this.#events.emit("edit", result);
311
+ return result;
312
+ }
313
+ /** The pages keep their sizes in `document`, for whoever lays them out. */
314
+ #takePages(opened, result) {
315
+ if (result.pages.length === 0)
316
+ return;
317
+ const pages = [...opened.pages];
318
+ for (const page of result.pages)
319
+ pages[page.index] = page;
320
+ this.#document = { ...opened, pages };
321
+ }
200
322
  /**
201
323
  * Every request the document worker has made, and had blocked, since it started: what its
202
324
  * `network` and `csp-violation` events reported, including those sent before anyone listened.
@@ -293,6 +415,24 @@ export class ViewerClient {
293
415
  * drawn once. An aborted render rejects with `AbortError` and never paints.
294
416
  */
295
417
  async render(request, options = {}) {
418
+ const attempt = { interrupted: false };
419
+ try {
420
+ return await this.#render(request, options, attempt);
421
+ }
422
+ catch (e) {
423
+ if (!attempt.interrupted)
424
+ throw e;
425
+ }
426
+ // The worker was lost while this render waited: it is sent again, once, in the restarted
427
+ // worker (ADR 0009 §3.6). A restart that didn't reopen the document (an encrypted one waits
428
+ // for its password) leaves it nothing to draw from: it ends as cancelled, as for a new open.
429
+ await this.#recovering?.catch(() => { });
430
+ if (this.#document === undefined)
431
+ throw new DOMException("the document closed in a restart", "AbortError");
432
+ return this.#render(request, options, { interrupted: false });
433
+ }
434
+ /** One render; `attempt.interrupted` says a failure was only the worker's loss (see `render`). */
435
+ async #render(request, options, attempt) {
296
436
  const id = this.#nextRender++;
297
437
  // A restarted worker numbers its documents again: read the ID once it serves.
298
438
  if (this.#recovering !== undefined)
@@ -334,6 +474,13 @@ export class ViewerClient {
334
474
  this.#firstDraw?.drawn(documentId);
335
475
  return drawn;
336
476
  }
477
+ catch (e) {
478
+ // Lost with its worker while it waited: a restart is under way, and it wasn't the render
479
+ // drawing then, which isn't re-sent so a page that crashes the engine can't loop.
480
+ attempt.interrupted =
481
+ isLaminaPdfError(e, "worker-crashed") && this.#recovering !== undefined && key !== this.#lost;
482
+ throw e;
483
+ }
337
484
  finally {
338
485
  const own = this.#timers.get(key);
339
486
  own?.delete(timers);
@@ -347,8 +494,8 @@ export class ViewerClient {
347
494
  }
348
495
  /**
349
496
  * Stops whatever the worker is doing by terminating it (the user's Stop, or the hard cap), then
350
- * restarts it as after a crash: pending calls reject, the running one isn't re-sent, and an
351
- * encrypted document asks for its password again.
497
+ * restarts it as after a crash: pending calls reject, renders that were waiting are sent again,
498
+ * the running one isn't, and an encrypted document asks for its password again.
352
499
  */
353
500
  stop(reason = "cancelled") {
354
501
  // A restart already under way serves: a second would end the worker it is starting.
@@ -361,33 +508,83 @@ export class ViewerClient {
361
508
  terminate() {
362
509
  this.#reopen = undefined;
363
510
  this.#document = undefined;
511
+ this.#edits.reset();
364
512
  this.#pdf.terminate();
365
513
  }
514
+ /**
515
+ * Applies the edit session's steps to the document a new worker reopened, which is the file as
516
+ * first opened. When they can't all be applied again, the session starts over.
517
+ */
518
+ /**
519
+ * `restored`; `lost` when the steps couldn't be replayed, and nothing was sent; `failed` when the
520
+ * replay failed half way, leaving edits in the model that aren't drawn.
521
+ */
522
+ async #restoreEdits(opened) {
523
+ if (this.#edits.empty)
524
+ return undefined;
525
+ const replay = this.#edits.replay();
526
+ // A replay that can't reproduce the document would show something else as restored.
527
+ if (replay === undefined) {
528
+ this.#edits.reset();
529
+ return "lost";
530
+ }
531
+ const { steps, undone } = replay;
532
+ try {
533
+ const result = (await this.#pdf.request("restoreEdits", [steps, undone, opened.id], {
534
+ priority: PRIORITY.document,
535
+ }));
536
+ this.#takePages(opened, result);
537
+ this.#edits.replayed(result);
538
+ this.#events.emit("edit", result);
539
+ return "restored";
540
+ }
541
+ catch {
542
+ this.#edits.reset();
543
+ return "failed";
544
+ }
545
+ }
546
+ /**
547
+ * Opens the document again in a restarted worker, with its edits: `reopened` when it did,
548
+ * `edits` what became of the session's steps. An encrypted document waits for its password.
549
+ */
550
+ async #reopenAfterRestart() {
551
+ const reopen = this.#reopen;
552
+ if (reopen === undefined)
553
+ return { reopened: false, edits: undefined };
554
+ if (this.#document?.needsPassword === true) {
555
+ // The password died with the worker; it is never kept on the page.
556
+ this.#reopen = undefined;
557
+ this.#document = undefined;
558
+ const edits = this.#edits.empty ? undefined : "lost";
559
+ this.#edits.reset();
560
+ this.#events.emit("password-required", {});
561
+ return { reopened: false, edits };
562
+ }
563
+ const open = async () => {
564
+ this.#document = (await this.#pdf.request("openDocument", [reopen.source, reopen.options], {
565
+ priority: PRIORITY.document,
566
+ }));
567
+ // The restarted worker has no extract module yet: its first page goes first again.
568
+ this.#firstDraw = new FirstDraw(this.#document.id);
569
+ return this.#document;
570
+ };
571
+ const restored = await this.#restoreEdits(await open());
572
+ // A replay that failed half way left edits in the model that aren't drawn: open the file once
573
+ // more, as it was. One that never started left the file as opened, so it isn't opened again.
574
+ if (restored === "failed")
575
+ await open();
576
+ return { reopened: true, edits: restored === "failed" ? "lost" : restored };
577
+ }
366
578
  #recover(reason) {
367
579
  if (this.#recovering !== undefined)
368
580
  return this.#recovering;
581
+ // Read before the lost calls' rejections run, which clear what was drawing.
582
+ this.#lost = this.#drawing;
369
583
  const recovering = (async () => {
370
584
  await this.#pdf.restart();
371
585
  await this.#pdf.request("initBackend", [this.#backend], { priority: PRIORITY.document });
372
- let reopened = false;
373
- const reopen = this.#reopen;
374
- if (reopen !== undefined) {
375
- if (this.#document?.needsPassword === true) {
376
- // The password died with the worker; it is never kept on the page.
377
- this.#reopen = undefined;
378
- this.#document = undefined;
379
- this.#events.emit("password-required", {});
380
- }
381
- else {
382
- this.#document = (await this.#pdf.request("openDocument", [reopen.source, reopen.options], {
383
- priority: PRIORITY.document,
384
- }));
385
- // The restarted worker has no extract module yet: its first page goes first again.
386
- this.#firstDraw = new FirstDraw(this.#document.id);
387
- reopened = true;
388
- }
389
- }
390
- this.#events.emit("restarted", { reason, reopened });
586
+ const { reopened, edits } = await this.#reopenAfterRestart();
587
+ this.#events.emit("restarted", { reason, reopened, ...(edits === undefined ? {} : { edits }) });
391
588
  })();
392
589
  const tracked = recovering.finally(() => {
393
590
  if (this.#recovering === tracked)