@rightkit/document-core 0.1.1 → 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 (136) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +31 -7
  3. package/dist/app/chrome.js +1 -1
  4. package/dist/app/documentPrinting.d.ts +0 -6
  5. package/dist/app/documentPrinting.js +0 -18
  6. package/dist/app/shell/nativeMenu.d.ts +3 -2
  7. package/dist/app/shell/nativeMenu.js +8 -5
  8. package/dist/documents/perfEventCounters.d.ts +0 -8
  9. package/dist/documents/perfEventCounters.js +2 -1
  10. package/dist/documents/preview/previewFrame.d.ts +2 -1
  11. package/dist/documents/preview/previewFrame.js +9 -7
  12. package/dist/documents/searchUtils.js +1 -1
  13. package/dist/host.d.ts +36 -0
  14. package/dist/host.js +54 -0
  15. package/dist/index.d.ts +1 -50
  16. package/dist/index.js +1 -50
  17. package/dist/settings/themes.js +1 -1
  18. package/dist/state/bridgeEvents.d.ts +1 -1
  19. package/dist/state/bridgeEvents.js +7 -4
  20. package/dist/state/recentUrls.js +4 -3
  21. package/dist/state/tauriBridge.d.ts +3 -61
  22. package/dist/state/tauriBridge.js +8 -297
  23. package/dist/styles.css +2 -2
  24. package/package.json +25 -98
  25. package/dist/app/shortcutChords.d.ts +0 -6
  26. package/dist/app/shortcutChords.js +0 -14
  27. package/dist/app/useCloseFindOnSurfaceChange.d.ts +0 -14
  28. package/dist/app/useCloseFindOnSurfaceChange.js +0 -23
  29. package/dist/app/useEditText.d.ts +0 -6
  30. package/dist/app/useEditText.js +0 -10
  31. package/dist/appName.d.ts +0 -2
  32. package/dist/appName.js +0 -8
  33. package/dist/documents/FullscreenIcon.d.ts +0 -4
  34. package/dist/documents/FullscreenIcon.js +0 -7
  35. package/dist/documents/mdx/mdxRuntimeScript.d.ts +0 -2
  36. package/dist/documents/mdx/mdxRuntimeScript.js +0 -3
  37. package/dist/documents/mdx/mdxRuntimeScript.txt +0 -584
  38. package/dist/documents/pdf/MacPdfReadPane.d.ts +0 -23
  39. package/dist/documents/pdf/MacPdfReadPane.js +0 -708
  40. package/dist/documents/pdf/NativePdfDocument.d.ts +0 -9
  41. package/dist/documents/pdf/NativePdfDocument.js +0 -41
  42. package/dist/documents/pdf/PdfInlineTextEditor.d.ts +0 -53
  43. package/dist/documents/pdf/PdfInlineTextEditor.js +0 -225
  44. package/dist/documents/pdf/PdfOcrLayer.d.ts +0 -4
  45. package/dist/documents/pdf/PdfOcrLayer.js +0 -30
  46. package/dist/documents/pdf/PdfOutline.d.ts +0 -6
  47. package/dist/documents/pdf/PdfOutline.js +0 -12
  48. package/dist/documents/pdf/PdfPasswordPrompt.d.ts +0 -5
  49. package/dist/documents/pdf/PdfPasswordPrompt.js +0 -24
  50. package/dist/documents/pdf/PdfThumbnails.d.ts +0 -8
  51. package/dist/documents/pdf/PdfThumbnails.js +0 -201
  52. package/dist/documents/pdf/PdfToolbar.d.ts +0 -47
  53. package/dist/documents/pdf/PdfToolbar.js +0 -76
  54. package/dist/documents/pdf/PdfViewer.d.ts +0 -36
  55. package/dist/documents/pdf/PdfViewer.js +0 -1522
  56. package/dist/documents/pdf/PdfViewerBody.d.ts +0 -22
  57. package/dist/documents/pdf/PdfViewerBody.js +0 -42
  58. package/dist/documents/pdf/macPdfNativeBridge.d.ts +0 -118
  59. package/dist/documents/pdf/macPdfNativeBridge.js +0 -134
  60. package/dist/documents/pdf/nearestVisiblePdfPage.d.ts +0 -2
  61. package/dist/documents/pdf/nearestVisiblePdfPage.js +0 -22
  62. package/dist/documents/pdf/pdfBench.d.ts +0 -88
  63. package/dist/documents/pdf/pdfBench.js +0 -535
  64. package/dist/documents/pdf/pdfBenchAutopilot.d.ts +0 -48
  65. package/dist/documents/pdf/pdfBenchAutopilot.js +0 -154
  66. package/dist/documents/pdf/pdfBenchTransport.d.ts +0 -21
  67. package/dist/documents/pdf/pdfBenchTransport.js +0 -233
  68. package/dist/documents/pdf/pdfEditModel.d.ts +0 -294
  69. package/dist/documents/pdf/pdfEditModel.js +0 -105
  70. package/dist/documents/pdf/pdfEditor.d.ts +0 -100
  71. package/dist/documents/pdf/pdfEditor.js +0 -68
  72. package/dist/documents/pdf/pdfOcr.d.ts +0 -36
  73. package/dist/documents/pdf/pdfOcr.js +0 -88
  74. package/dist/documents/pdf/pdfOcrEdit.d.ts +0 -32
  75. package/dist/documents/pdf/pdfOcrEdit.js +0 -138
  76. package/dist/documents/pdf/pdfPrinting.d.ts +0 -28
  77. package/dist/documents/pdf/pdfPrinting.js +0 -110
  78. package/dist/documents/pdf/pdfSave.d.ts +0 -87
  79. package/dist/documents/pdf/pdfSave.js +0 -250
  80. package/dist/documents/pdf/pdfShared.d.ts +0 -16
  81. package/dist/documents/pdf/pdfShared.js +0 -20
  82. package/dist/documents/pdf/pdfToMarkdown.d.ts +0 -11
  83. package/dist/documents/pdf/pdfToMarkdown.js +0 -67
  84. package/dist/documents/pdf/pdfjsLibGlobal.d.ts +0 -1
  85. package/dist/documents/pdf/pdfjsLibGlobal.js +0 -12
  86. package/dist/documents/pdf/toolbarIcons.d.ts +0 -18
  87. package/dist/documents/pdf/toolbarIcons.js +0 -31
  88. package/dist/documents/pdf/usePdfDocumentLoader.d.ts +0 -40
  89. package/dist/documents/pdf/usePdfDocumentLoader.js +0 -243
  90. package/dist/documents/pdf/usePdfEditSession.d.ts +0 -88
  91. package/dist/documents/pdf/usePdfEditSession.js +0 -341
  92. package/dist/documents/pdf/usePdfEditor.d.ts +0 -59
  93. package/dist/documents/pdf/usePdfEditor.js +0 -317
  94. package/dist/documents/pdf/usePdfFit.d.ts +0 -19
  95. package/dist/documents/pdf/usePdfFit.js +0 -55
  96. package/dist/documents/pdf/usePdfOcr.d.ts +0 -38
  97. package/dist/documents/pdf/usePdfOcr.js +0 -337
  98. package/dist/documents/pdf/usePdfPreviewRecompose.d.ts +0 -28
  99. package/dist/documents/pdf/usePdfPreviewRecompose.js +0 -256
  100. package/dist/documents/pdf/usePdfSavePlan.d.ts +0 -61
  101. package/dist/documents/pdf/usePdfSavePlan.js +0 -303
  102. package/dist/documents/pdf/usePdfSearch.d.ts +0 -31
  103. package/dist/documents/pdf/usePdfSearch.js +0 -367
  104. package/dist/documents/pdf/usePdfViewControls.d.ts +0 -72
  105. package/dist/documents/pdf/usePdfViewControls.js +0 -152
  106. package/dist/documents/scopedSelectAll.d.ts +0 -5
  107. package/dist/documents/scopedSelectAll.js +0 -77
  108. package/dist/documents/txt/cmScroll.d.ts +0 -11
  109. package/dist/documents/txt/cmScroll.js +0 -52
  110. package/dist/documents/useMirroredEditText.d.ts +0 -15
  111. package/dist/documents/useMirroredEditText.js +0 -29
  112. package/dist/documents/useSplitResize.d.ts +0 -7
  113. package/dist/documents/useSplitResize.js +0 -37
  114. package/dist/settings/rk-settings.css +0 -38
  115. package/dist/settings/sections/EditorSection.d.ts +0 -1
  116. package/dist/settings/sections/EditorSection.js +0 -18
  117. package/dist/settings/sections/FileAssociationsSection.d.ts +0 -1
  118. package/dist/settings/sections/FileAssociationsSection.js +0 -5
  119. package/dist/settings/sections/MacFileAssociationsBlock.d.ts +0 -33
  120. package/dist/settings/sections/MacFileAssociationsBlock.js +0 -151
  121. package/dist/settings/sections/ThemeSection.d.ts +0 -1
  122. package/dist/settings/sections/ThemeSection.js +0 -31
  123. package/dist/settings/sections/TypographySection.d.ts +0 -1
  124. package/dist/settings/sections/TypographySection.js +0 -19
  125. package/dist/settings/sections/onOff.d.ts +0 -11
  126. package/dist/settings/sections/onOff.js +0 -8
  127. package/dist/state/browserQa.d.ts +0 -21
  128. package/dist/state/browserQa.js +0 -303
  129. package/dist/state/browserQaDocx.d.ts +0 -3
  130. package/dist/state/browserQaDocx.js +0 -35
  131. package/dist/state/browserQaLicense.d.ts +0 -8
  132. package/dist/state/browserQaLicense.js +0 -7
  133. package/dist/state/browserQaPdf.d.ts +0 -60
  134. package/dist/state/browserQaPdf.js +0 -302
  135. package/dist/state/browserQaText.d.ts +0 -6
  136. package/dist/state/browserQaText.js +0 -48
@@ -1,337 +0,0 @@
1
- import { useCallback, useEffect, useRef, useState } from "react";
2
- import { isBackendUnavailable, isProbablyScannedPage, isTextlessPage, ocrBackendMissing, ocrPage, renderPageToPng, } from "./pdfOcr.js";
3
- import { viewportRotation } from "./pdfShared.js";
4
- import { logError, logPerf } from "../../state/diagnostics.js";
5
- function errorMessage(error) {
6
- if (error instanceof Error && error.message)
7
- return error.message;
8
- return typeof error === "string" ? error : String(error);
9
- }
10
- export function usePdfOcr({ documentId, pdfDoc, pdfRef, current, rotation, setPageText, invalidatePageText, rehighlight, }) {
11
- const [scannedPages, setScannedPages] = useState(new Map());
12
- const [pageStates, setPageStates] = useState(new Map());
13
- const [linesByPage, setLinesByPage] = useState(new Map());
14
- const [currentPageScanned, setCurrentPageScanned] = useState(false);
15
- const [documentState, setDocumentState] = useState({
16
- status: "idle",
17
- completed: 0,
18
- total: 0,
19
- scanned: 0,
20
- skipped: 0,
21
- failed: 0,
22
- });
23
- const generationRef = useRef(0);
24
- const scanRequestRef = useRef(0);
25
- const pageRunRef = useRef(new Map());
26
- const documentRunRef = useRef(null);
27
- const previousRotationRef = useRef(rotation);
28
- const linesByPageRef = useRef(linesByPage);
29
- const scannedPagesRef = useRef(scannedPages);
30
- const pageStatesRef = useRef(pageStates);
31
- const documentStateRef = useRef(documentState);
32
- linesByPageRef.current = linesByPage;
33
- scannedPagesRef.current = scannedPages;
34
- pageStatesRef.current = pageStates;
35
- documentStateRef.current = documentState;
36
- // documentId is the lifecycle boundary; reset OCR state when it changes.
37
- // biome-ignore lint/correctness/useExhaustiveDependencies: documentId intentionally marks document lifecycle.
38
- useEffect(() => {
39
- generationRef.current += 1;
40
- scanRequestRef.current += 1;
41
- pageRunRef.current.clear();
42
- documentRunRef.current = null;
43
- setScannedPages(new Map());
44
- setPageStates(new Map());
45
- setLinesByPage(new Map());
46
- setCurrentPageScanned(false);
47
- setDocumentState({
48
- status: "idle",
49
- completed: 0,
50
- total: 0,
51
- scanned: 0,
52
- skipped: 0,
53
- failed: 0,
54
- });
55
- return () => {
56
- generationRef.current += 1;
57
- scanRequestRef.current += 1;
58
- pageRunRef.current.clear();
59
- documentRunRef.current = null;
60
- };
61
- }, [documentId]);
62
- // Geometry from a previous rotation is no longer valid. Do not transform it: the backend's
63
- // normalized coordinates describe the raster orientation, so recognition must run again.
64
- useEffect(() => {
65
- if (previousRotationRef.current === rotation)
66
- return;
67
- previousRotationRef.current = rotation;
68
- generationRef.current += 1;
69
- scanRequestRef.current += 1;
70
- pageRunRef.current.clear();
71
- documentRunRef.current = null;
72
- const recognizedPages = [...linesByPageRef.current.keys()];
73
- for (const page of recognizedPages)
74
- invalidatePageText(page);
75
- if (recognizedPages.length > 0) {
76
- setLinesByPage((previous) => {
77
- const next = new Map(previous);
78
- for (const page of recognizedPages)
79
- next.delete(page);
80
- return next;
81
- });
82
- }
83
- setPageStates((previous) => {
84
- const next = new Map(previous);
85
- for (const [page, state] of next) {
86
- if (state.status === "done" || state.status === "running") {
87
- next.set(page, { status: "idle" });
88
- }
89
- }
90
- return next;
91
- });
92
- setDocumentState({ status: "idle", completed: 0, total: 0, scanned: 0, skipped: 0, failed: 0 });
93
- }, [invalidatePageText, rotation]);
94
- // Browsing detection reads only streamed text. Image/operator inspection is deferred until an
95
- // explicit OCR request, so opening/changing pages never decodes a page just to show a button.
96
- useEffect(() => {
97
- if (!pdfDoc || current < 1 || current > pdfDoc.numPages) {
98
- setCurrentPageScanned(false);
99
- return;
100
- }
101
- const known = scannedPagesRef.current.get(current);
102
- if (known !== undefined) {
103
- setCurrentPageScanned(known);
104
- return;
105
- }
106
- const request = ++scanRequestRef.current;
107
- let cancelled = false;
108
- void pdfDoc
109
- .getPage(current)
110
- .then((page) => isTextlessPage(page))
111
- .then((textless) => {
112
- if (cancelled || request !== scanRequestRef.current)
113
- return;
114
- setScannedPages((previous) => {
115
- const next = new Map(previous);
116
- next.set(current, textless);
117
- return next;
118
- });
119
- setCurrentPageScanned(textless);
120
- })
121
- .catch(() => {
122
- if (!cancelled && request === scanRequestRef.current)
123
- setCurrentPageScanned(false);
124
- });
125
- return () => {
126
- cancelled = true;
127
- };
128
- }, [current, pdfDoc]);
129
- const setState = useCallback((page, state) => {
130
- setPageStates((previous) => {
131
- const next = new Map(previous);
132
- next.set(page, state);
133
- return next;
134
- });
135
- }, []);
136
- const isCurrentGeneration = useCallback((generation, pdf) => generation === generationRef.current && pdfRef.current === pdf, [pdfRef]);
137
- const recognizePage = useCallback(async (pageNumber, pdf, generation, pageOverride) => {
138
- const startedAt = performance.now();
139
- setState(pageNumber, { status: "running" });
140
- try {
141
- const page = pageOverride ?? (await pdf.getPage(pageNumber));
142
- if (!isCurrentGeneration(generation, pdf))
143
- return "cancelled";
144
- const png = await renderPageToPng(page, 300, viewportRotation(page.rotate, rotation));
145
- if (!isCurrentGeneration(generation, pdf))
146
- return "cancelled";
147
- const lines = await ocrPage(new Uint8Array(await png.arrayBuffer()));
148
- if (!isCurrentGeneration(generation, pdf))
149
- return "cancelled";
150
- setLinesByPage((previous) => new Map(previous).set(pageNumber, lines));
151
- setState(pageNumber, { status: "done" });
152
- setPageText(pageNumber, lines.map((line) => line.text).join(" "));
153
- logPerf("pdf:ocr-page", startedAt);
154
- rehighlight();
155
- return "done";
156
- }
157
- catch (error) {
158
- if (!isCurrentGeneration(generation, pdf))
159
- return "cancelled";
160
- setState(pageNumber, { status: "error", error: errorMessage(error) });
161
- logError("pdf:ocr-page-failed");
162
- return "failed";
163
- }
164
- }, [isCurrentGeneration, rehighlight, rotation, setPageText, setState]);
165
- const recognizeCurrentPage = useCallback(async (inspectEligibility = false) => {
166
- const pageNumber = current;
167
- const pdf = pdfRef.current ?? pdfDoc;
168
- if (!pdf ||
169
- (!currentPageScanned && !inspectEligibility) ||
170
- pageStatesRef.current.get(pageNumber)?.status === "running" ||
171
- pageRunRef.current.size > 0 ||
172
- documentRunRef.current !== null) {
173
- return;
174
- }
175
- const generation = generationRef.current;
176
- pageRunRef.current.set(pageNumber, generation);
177
- setState(pageNumber, { status: "running" });
178
- try {
179
- const page = await pdf.getPage(pageNumber);
180
- if (!isCurrentGeneration(generation, pdf))
181
- return;
182
- const scanned = await isProbablyScannedPage(page);
183
- if (!isCurrentGeneration(generation, pdf))
184
- return;
185
- setScannedPages((previous) => new Map(previous).set(pageNumber, scanned));
186
- setCurrentPageScanned(scanned);
187
- if (!scanned) {
188
- setState(pageNumber, { status: "idle" });
189
- return;
190
- }
191
- await recognizePage(pageNumber, pdf, generation, page);
192
- }
193
- catch {
194
- if (!isCurrentGeneration(generation, pdf))
195
- return;
196
- setState(pageNumber, { status: "error", error: "Unable to inspect page for OCR." });
197
- logError("pdf:ocr-page-failed");
198
- }
199
- finally {
200
- if (pageRunRef.current.get(pageNumber) === generation)
201
- pageRunRef.current.delete(pageNumber);
202
- }
203
- }, [current, currentPageScanned, isCurrentGeneration, pdfDoc, pdfRef, recognizePage, setState]);
204
- const recognizeDocument = useCallback(async () => {
205
- const pdf = pdfRef.current ?? pdfDoc;
206
- if (!pdf ||
207
- documentStateRef.current.status === "running" ||
208
- documentRunRef.current !== null ||
209
- pageRunRef.current.size > 0) {
210
- return;
211
- }
212
- const generation = generationRef.current;
213
- const total = pdf.numPages;
214
- documentRunRef.current = generation;
215
- setDocumentState({
216
- status: "running",
217
- completed: 0,
218
- total,
219
- scanned: 0,
220
- skipped: 0,
221
- failed: 0,
222
- currentPage: 1,
223
- });
224
- const startedAt = performance.now();
225
- let failedCount = 0;
226
- try {
227
- for (let pageNumber = 1; pageNumber <= total; pageNumber += 1) {
228
- if (!isCurrentGeneration(generation, pdf))
229
- return;
230
- const previous = pageStatesRef.current.get(pageNumber);
231
- try {
232
- const page = await pdf.getPage(pageNumber);
233
- const scanned = await isProbablyScannedPage(page);
234
- if (!isCurrentGeneration(generation, pdf))
235
- return;
236
- setScannedPages((pages) => new Map(pages).set(pageNumber, scanned));
237
- if (scanned) {
238
- setDocumentState((state) => ({
239
- ...state,
240
- scanned: state.scanned + 1,
241
- currentPage: pageNumber,
242
- }));
243
- }
244
- if (!scanned || previous?.status === "done") {
245
- setDocumentState((state) => ({
246
- ...state,
247
- completed: state.completed + 1,
248
- skipped: state.skipped + 1,
249
- currentPage: pageNumber,
250
- }));
251
- continue;
252
- }
253
- const outcome = await recognizePage(pageNumber, pdf, generation, page);
254
- if (outcome === "cancelled")
255
- return;
256
- if (outcome === "failed")
257
- failedCount += 1;
258
- setDocumentState((state) => ({
259
- ...state,
260
- completed: state.completed + 1,
261
- failed: state.failed + (outcome === "failed" ? 1 : 0),
262
- currentPage: pageNumber,
263
- }));
264
- }
265
- catch {
266
- if (!isCurrentGeneration(generation, pdf))
267
- return;
268
- failedCount += 1;
269
- setState(pageNumber, { status: "error", error: "Unable to inspect page for OCR." });
270
- logError("pdf:ocr-page-failed");
271
- setDocumentState((state) => ({
272
- ...state,
273
- completed: state.completed + 1,
274
- failed: state.failed + 1,
275
- currentPage: pageNumber,
276
- }));
277
- }
278
- }
279
- if (!isCurrentGeneration(generation, pdf))
280
- return;
281
- setDocumentState((state) => ({ ...state, status: failedCount > 0 ? "error" : "done" }));
282
- logPerf("pdf:ocr-document", startedAt);
283
- }
284
- catch {
285
- if (isCurrentGeneration(generation, pdf)) {
286
- setDocumentState((state) => ({ ...state, status: "error" }));
287
- setState(current, {
288
- status: "error",
289
- error: "Text recognition is unavailable. Try again.",
290
- });
291
- }
292
- }
293
- finally {
294
- if (documentRunRef.current === generation)
295
- documentRunRef.current = null;
296
- }
297
- }, [current, isCurrentGeneration, pdfDoc, pdfRef, recognizePage, setState]);
298
- // State updates commit the OCR layer after recognition returns. Re-run once the lines are
299
- // mounted so a query that was already committed can mark the newly-rendered spans.
300
- useEffect(() => {
301
- if (linesByPage.size > 0)
302
- rehighlight();
303
- }, [linesByPage, rehighlight]);
304
- // Ask the backend only once a scanned page is in view, which is the first moment OCR would be
305
- // offered: on macOS the probe runs a recognition canary that is costly on a cold model cache.
306
- // Until it answers, and whenever it cannot answer, OCR stays offered.
307
- const [ocrMissing, setOcrMissing] = useState(false);
308
- useEffect(() => {
309
- if (!currentPageScanned)
310
- return;
311
- let cancelled = false;
312
- void ocrBackendMissing().then((missing) => {
313
- if (!cancelled && missing)
314
- setOcrMissing(true);
315
- });
316
- return () => {
317
- cancelled = true;
318
- };
319
- }, [currentPageScanned]);
320
- const currentPageState = pageStates.get(current) ?? { status: "idle" };
321
- const currentError = currentPageState.status === "error" ? currentPageState.error : undefined;
322
- return {
323
- // The backend probe decides whether OCR is offered at all; PdfToolbar hides the controls when
324
- // it is false. Recognition failures on the requested page are still reported from that page.
325
- ocrAvailable: !ocrMissing,
326
- availabilityResolved: true,
327
- currentPageScanned,
328
- currentPageState,
329
- currentError,
330
- currentErrorIsBackendUnavailable: currentError ? isBackendUnavailable(currentError) : false,
331
- linesByPage,
332
- pageStates,
333
- recognizeCurrentPage,
334
- documentOcrState: documentState,
335
- recognizeDocument,
336
- };
337
- }
@@ -1,28 +0,0 @@
1
- import { type MutableRefObject } from "react";
2
- import * as pdfjs from "pdfjs-dist";
3
- import type { PDFDocumentProxy } from "pdfjs-dist";
4
- import type { usePdfEditSession } from "./usePdfEditSession.js";
5
- interface PdfPreviewRecomposeDeps {
6
- documentPath: string;
7
- editSession: ReturnType<typeof usePdfEditSession>;
8
- /** Best-known document_sha256 (§5.1) — populated from every inspected page graph. */
9
- sourceShaRef: MutableRefObject<string | null>;
10
- /** Guards against concurrent publishes racing: a superseded call discards its own task. */
11
- previewSeqRef: MutableRefObject<number>;
12
- previewLoadingTaskRef: MutableRefObject<ReturnType<typeof pdfjs.getDocument> | null>;
13
- previewDocRef: MutableRefObject<PDFDocumentProxy | null>;
14
- previewPagesRef: MutableRefObject<Set<number>>;
15
- renderedRef: MutableRefObject<Map<number, () => void>>;
16
- /** Indirection to the per-page renderer so this path never grows renderPage's dep list. */
17
- renderPageRef: MutableRefObject<(n: number) => Promise<void>>;
18
- }
19
- /**
20
- * §9.4 in-memory candidate preview: recompose the candidate document from the committed-but-unsaved
21
- * source commands and re-render each affected page on every source-history mutation (commit, undo,
22
- * redo — anything that calls `editSession.publish()`). Extracted from PdfViewer verbatim — no
23
- * behavior change.
24
- */
25
- export declare function usePdfPreviewRecompose({ documentPath, editSession, sourceShaRef, previewSeqRef, previewLoadingTaskRef, previewDocRef, previewPagesRef, renderedRef, renderPageRef, }: PdfPreviewRecomposeDeps): {
26
- invalidatePreview: () => void;
27
- };
28
- export {};
@@ -1,256 +0,0 @@
1
- import { useCallback, useEffect, useRef } from "react";
2
- import * as pdfjs from "pdfjs-dist";
3
- import { pdfDocumentInit, pdfWasmUrl } from "@rightkit/pdf-ui";
4
- import { recomposePdfPreview } from "./pdfSave.js";
5
- import { composePdfPreview } from "../../state/tauriBridge.js";
6
- import { logError } from "../../state/diagnostics.js";
7
- import { acquireResource } from "../../app/resourceCensus.js";
8
- /**
9
- * §9.4 in-memory candidate preview: recompose the candidate document from the committed-but-unsaved
10
- * source commands and re-render each affected page on every source-history mutation (commit, undo,
11
- * redo — anything that calls `editSession.publish()`). Extracted from PdfViewer verbatim — no
12
- * behavior change.
13
- */
14
- export function usePdfPreviewRecompose({ documentPath, editSession, sourceShaRef, previewSeqRef, previewLoadingTaskRef, previewDocRef, previewPagesRef, renderedRef, renderPageRef, }) {
15
- const onPublish = editSession.onPublish;
16
- // Mirrors editSession.session.sourceCommands on every render. `publish()` (from commitDraft, or
17
- // from the addCommands `post` above on undo/redo) fires SYNCHRONOUSLY inside the same call that
18
- // just invoked `setSession`, i.e. before React has re-rendered — so a listener reading
19
- // `editSession.session` directly would see the state from BEFORE this mutation. Deferring the
20
- // read to a microtask (below) lets React's synchronous end-of-event-handler flush happen first,
21
- // by which point this ref (updated every render, during the render itself) is current.
22
- const sourceCommandsRef = useRef(editSession.session.sourceCommands);
23
- sourceCommandsRef.current = editSession.session.sourceCommands;
24
- // A publish can leave work queued behind React's render, while pdf.js can keep a loading task
25
- // alive long after its caller has gone away. Keep hook lifetime separate from the shared preview
26
- // sequence so document switches & unmounts invalidate both queued work & in-flight loads.
27
- const mountedRef = useRef(false);
28
- const lifecycleRef = useRef(0);
29
- const queueEpochRef = useRef(0);
30
- const publishEpochRef = useRef(0);
31
- const pendingLoadingTaskRef = useRef(null);
32
- const retirementBarrierRef = useRef(null);
33
- const deferredResumeRef = useRef(null);
34
- const resumeWatcherRef = useRef(null);
35
- const handleRecomposePreviewRef = useRef(null);
36
- const destroyedLoadingTasksRef = useRef(new WeakSet());
37
- const taskDestroyPromisesRef = useRef(new WeakMap());
38
- const taskOwnershipRef = useRef(new WeakMap());
39
- const ensureDestroyLoadingTask = useCallback((task) => {
40
- const existing = taskDestroyPromisesRef.current.get(task);
41
- if (existing)
42
- return existing;
43
- const ownership = taskOwnershipRef.current.get(task);
44
- const destroyPromise = (async () => {
45
- try {
46
- if (!task.destroyed)
47
- await task.destroy();
48
- }
49
- catch {
50
- // Destruction is best-effort during unmount/path replacement.
51
- }
52
- finally {
53
- ownership?.document?.();
54
- ownership?.worker();
55
- destroyedLoadingTasksRef.current.add(task);
56
- }
57
- })();
58
- taskDestroyPromisesRef.current.set(task, destroyPromise);
59
- return destroyPromise;
60
- }, []);
61
- const waitForDestroy = useCallback(async (task) => {
62
- if (!task)
63
- return true;
64
- const destroyPromise = ensureDestroyLoadingTask(task);
65
- let timeout;
66
- const completed = await Promise.race([
67
- destroyPromise.then(() => true),
68
- new Promise((resolve) => {
69
- timeout = window.setTimeout(() => resolve(false), 500);
70
- }),
71
- ]);
72
- if (timeout !== undefined)
73
- window.clearTimeout(timeout);
74
- return completed;
75
- }, [ensureDestroyLoadingTask]);
76
- const beginRetirement = useCallback((task) => {
77
- const promise = ensureDestroyLoadingTask(task);
78
- retirementBarrierRef.current = { task, promise };
79
- void promise.then(() => {
80
- if (retirementBarrierRef.current?.task === task)
81
- retirementBarrierRef.current = null;
82
- });
83
- return promise;
84
- }, [ensureDestroyLoadingTask]);
85
- const deferUntilRetirement = useCallback((lifecycle, publishEpoch, queueEpoch) => {
86
- const barrier = retirementBarrierRef.current;
87
- if (!barrier)
88
- return;
89
- deferredResumeRef.current = { lifecycle, publishEpoch, queueEpoch };
90
- if (resumeWatcherRef.current === barrier.promise)
91
- return;
92
- const watcher = barrier.promise.then(() => {
93
- if (resumeWatcherRef.current === watcher)
94
- resumeWatcherRef.current = null;
95
- const deferred = deferredResumeRef.current;
96
- deferredResumeRef.current = null;
97
- if (deferred &&
98
- mountedRef.current &&
99
- lifecycleRef.current === deferred.lifecycle &&
100
- publishEpochRef.current === deferred.publishEpoch &&
101
- queueEpochRef.current === deferred.queueEpoch)
102
- void handleRecomposePreviewRef.current?.(deferred.lifecycle, deferred.publishEpoch);
103
- });
104
- resumeWatcherRef.current = watcher;
105
- }, []);
106
- // biome-ignore lint/correctness/useExhaustiveDependencies: refs arrive as stable hook params
107
- const clearPreview = useCallback(() => {
108
- previewSeqRef.current += 1;
109
- const pending = pendingLoadingTaskRef.current;
110
- pendingLoadingTaskRef.current = null;
111
- const published = previewLoadingTaskRef.current;
112
- previewLoadingTaskRef.current = null;
113
- previewDocRef.current = null;
114
- previewPagesRef.current = new Set();
115
- if (pending) {
116
- const pendingRetirement = beginRetirement(pending);
117
- if (published && published !== pending)
118
- void pendingRetirement.then(() => beginRetirement(published));
119
- }
120
- else if (published)
121
- beginRetirement(published);
122
- }, [beginRetirement]);
123
- // biome-ignore lint/correctness/useExhaustiveDependencies: refs arrive as stable hook params
124
- const handleRecomposePreview = useCallback(async (lifecycle, publishEpoch = publishEpochRef.current) => {
125
- const isCurrent = () => mountedRef.current &&
126
- lifecycleRef.current === lifecycle &&
127
- publishEpochRef.current === publishEpoch;
128
- if (!isCurrent())
129
- return;
130
- const sha = sourceShaRef.current;
131
- if (!sha)
132
- return; // nothing inspected yet — no possible edits either
133
- const seq = ++previewSeqRef.current;
134
- const queueEpoch = queueEpochRef.current;
135
- const barrier = retirementBarrierRef.current;
136
- if (barrier) {
137
- const reaped = await waitForDestroy(barrier.task);
138
- if (!reaped) {
139
- deferUntilRetirement(lifecycle, publishEpoch, queueEpoch);
140
- return;
141
- }
142
- }
143
- const previousPending = pendingLoadingTaskRef.current;
144
- pendingLoadingTaskRef.current = null;
145
- if (previousPending) {
146
- beginRetirement(previousPending);
147
- const reaped = await waitForDestroy(previousPending);
148
- if (!reaped) {
149
- deferUntilRetirement(lifecycle, publishEpoch, queueEpoch);
150
- return;
151
- }
152
- retirementBarrierRef.current = null;
153
- if (!isCurrent() || previewSeqRef.current !== seq)
154
- return;
155
- }
156
- let task = null;
157
- try {
158
- const composed = await recomposePdfPreview({ composePdfPreview }, documentPath, sha, sourceCommandsRef.current);
159
- if (!isCurrent() || previewSeqRef.current !== seq)
160
- return; // superseded while composing
161
- task = pdfjs.getDocument(pdfDocumentInit(composed.candidateBytes.slice(), { wasmUrl: pdfWasmUrl() }));
162
- taskOwnershipRef.current.set(task, {
163
- worker: acquireResource({
164
- kind: "pdf-preview-worker",
165
- documentPath,
166
- format: "pdf",
167
- }),
168
- document: null,
169
- });
170
- pendingLoadingTaskRef.current = task;
171
- const doc = await task.promise;
172
- if (!isCurrent() || previewSeqRef.current !== seq) {
173
- if (pendingLoadingTaskRef.current === task)
174
- pendingLoadingTaskRef.current = null;
175
- beginRetirement(task);
176
- const reaped = await waitForDestroy(task); // superseded while loading — this call's own task
177
- if (!reaped)
178
- deferUntilRetirement(lifecycle, publishEpoch, queueEpoch);
179
- return;
180
- }
181
- if (pendingLoadingTaskRef.current === task)
182
- pendingLoadingTaskRef.current = null;
183
- const ownership = taskOwnershipRef.current.get(task);
184
- if (ownership && !ownership.document) {
185
- ownership.document = acquireResource({
186
- kind: "pdf-preview-document",
187
- documentPath,
188
- format: "pdf",
189
- });
190
- }
191
- const previousPublished = previewLoadingTaskRef.current;
192
- // An undo can remove the last command, making the new candidate's affected-pages list
193
- // empty. Re-render pages from the previous candidate too, so those slots switch back to
194
- // the on-disk document instead of retaining stale edited pixels.
195
- const pagesToRender = new Set([...previewPagesRef.current, ...composed.affectedPages]);
196
- if (!isCurrent() || previewSeqRef.current !== seq) {
197
- beginRetirement(task);
198
- return;
199
- }
200
- previewLoadingTaskRef.current = task;
201
- previewDocRef.current = doc;
202
- previewPagesRef.current = new Set(composed.affectedPages);
203
- if (previousPublished && previousPublished !== task)
204
- beginRetirement(previousPublished);
205
- for (const n of pagesToRender) {
206
- renderedRef.current.get(n)?.();
207
- void renderPageRef.current(n);
208
- }
209
- }
210
- catch {
211
- if (pendingLoadingTaskRef.current === task)
212
- pendingLoadingTaskRef.current = null;
213
- if (task)
214
- beginRetirement(task);
215
- if (isCurrent() && previewSeqRef.current === seq) {
216
- logError("pdf-editor:preview");
217
- }
218
- }
219
- }, [beginRetirement, deferUntilRetirement, documentPath, waitForDestroy]);
220
- handleRecomposePreviewRef.current = handleRecomposePreview;
221
- useEffect(() => {
222
- mountedRef.current = true;
223
- const lifecycle = ++lifecycleRef.current;
224
- const unsubscribe = onPublish(() => {
225
- const queueEpoch = queueEpochRef.current;
226
- const publishEpoch = ++publishEpochRef.current;
227
- queueMicrotask(() => {
228
- if (mountedRef.current &&
229
- lifecycleRef.current === lifecycle &&
230
- queueEpochRef.current === queueEpoch &&
231
- publishEpochRef.current === publishEpoch) {
232
- void handleRecomposePreview(lifecycle, publishEpoch);
233
- }
234
- });
235
- });
236
- return () => {
237
- unsubscribe();
238
- mountedRef.current = false;
239
- lifecycleRef.current += 1;
240
- clearPreview();
241
- };
242
- }, [clearPreview, handleRecomposePreview, onPublish]);
243
- // Save commits invalidate queued/in-flight candidate work without destroying the currently
244
- // published preview. If reload fails, that preview remains visible as last-known-good evidence;
245
- // a successful reload clears it after the fresh document is installed. Later source publishes
246
- // remain valid because lifecycleRef is deliberately left untouched.
247
- const invalidatePreview = useCallback(() => {
248
- queueEpochRef.current += 1;
249
- previewSeqRef.current += 1;
250
- const pending = pendingLoadingTaskRef.current;
251
- pendingLoadingTaskRef.current = null;
252
- if (pending)
253
- beginRetirement(pending);
254
- }, [beginRetirement, previewSeqRef]);
255
- return { invalidatePreview };
256
- }