@gmickel/gno 1.39.2 → 1.40.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.
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.39.2.zip → gno-browser-clipper-v1.40.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.40.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/src/core/network-boundary-inventory.ts +10 -0
- package/src/serve/CLAUDE.md +22 -2
- package/src/serve/clipper-security.ts +3 -17
- package/src/serve/public/components/pdf/PdfViewer.tsx +97 -19
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/globals.css +4 -0
- package/src/serve/public/hooks/use-pdf-document.ts +101 -37
- package/src/serve/public/hooks/use-pdf-pages.ts +324 -92
- package/src/serve/public/lib/pdf-transport.ts +13 -0
- package/src/serve/public/lib/pdf.ts +77 -10
- package/src/serve/public/lib/server-capabilities.ts +19 -0
- package/src/serve/public/pages/Ask.tsx +8 -9
- package/src/serve/public/pages/DocView.tsx +80 -18
- package/src/serve/public/pages/Search.tsx +8 -9
- package/src/serve/request-locality.ts +84 -0
- package/src/serve/routes/api.ts +88 -3
- package/src/serve/server.ts +163 -29
- package/browser-extension/artifacts/gno-browser-clipper-v1.39.2.zip.sha256 +0 -1
|
@@ -460,6 +460,10 @@ mark {
|
|
|
460
460
|
|
|
461
461
|
.gno-pdf-page-column {
|
|
462
462
|
scrollbar-gutter: stable;
|
|
463
|
+
/* The page hook is the sole scroll compensator for geometry corrections
|
|
464
|
+
(use-pdf-pages.ts anchored correction); browser scroll anchoring on top
|
|
465
|
+
of it would move the page in view by the delta a second time. */
|
|
466
|
+
overflow-anchor: none;
|
|
463
467
|
}
|
|
464
468
|
|
|
465
469
|
.gno-pdf-page {
|
|
@@ -4,9 +4,11 @@ import {
|
|
|
4
4
|
classifyPdfError as defaultClassifyPdfError,
|
|
5
5
|
getDocument as defaultGetDocument,
|
|
6
6
|
getPdfMetrics as defaultGetPdfMetrics,
|
|
7
|
+
transportHintForContentLength,
|
|
7
8
|
type GnoDocumentLoadingTask,
|
|
8
9
|
type PdfFallbackReason,
|
|
9
10
|
type PDFDocumentProxy,
|
|
11
|
+
type PdfTransportHint,
|
|
10
12
|
} from "../lib/pdf";
|
|
11
13
|
|
|
12
14
|
export type PdfDocumentStatus = "loading" | "ready" | "error";
|
|
@@ -30,12 +32,18 @@ export type UsePdfDocumentDeps = {
|
|
|
30
32
|
getDocument?: typeof defaultGetDocument;
|
|
31
33
|
classifyPdfError?: typeof defaultClassifyPdfError;
|
|
32
34
|
getPdfMetrics?: typeof defaultGetPdfMetrics;
|
|
35
|
+
/** HEAD-probe transport; the JSON API helper cannot carry a HEAD. */
|
|
36
|
+
fetch?: FetchLike;
|
|
33
37
|
};
|
|
34
38
|
|
|
39
|
+
/** Minimal fetch shape the HEAD probe needs (same-origin string URL only). */
|
|
40
|
+
export type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
|
|
41
|
+
|
|
35
42
|
type LoadOwnership = {
|
|
36
|
-
/** Minted opaque id for this load attempt. */
|
|
37
|
-
docId: string;
|
|
38
|
-
|
|
43
|
+
/** Minted opaque id for this load attempt; null until the probe settles. */
|
|
44
|
+
docId: string | null;
|
|
45
|
+
/** Null while the HEAD probe is in flight; never created once torn down. */
|
|
46
|
+
task: GnoDocumentLoadingTask | null;
|
|
39
47
|
/** Set only after promise resolves into viewer ownership. */
|
|
40
48
|
viewerDoc: PDFDocumentProxy | null;
|
|
41
49
|
/** True once teardown for this load has run (idempotent). */
|
|
@@ -44,6 +52,33 @@ type LoadOwnership = {
|
|
|
44
52
|
destroyMetricEmitted: boolean;
|
|
45
53
|
};
|
|
46
54
|
|
|
55
|
+
/** Module-level so the effect dependency stays referentially stable. */
|
|
56
|
+
const defaultFetch: FetchLike = (url, init) => fetch(url, init);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Probe the asset size with one same-origin HEAD (fn-136 R1). Network failure,
|
|
60
|
+
* a non-2xx status, or a missing/invalid Content-Length all fall back to the
|
|
61
|
+
* ranged tier; the probe never rejects and never fails the document.
|
|
62
|
+
*/
|
|
63
|
+
async function probeTransportHint(
|
|
64
|
+
url: string,
|
|
65
|
+
fetchImpl: FetchLike
|
|
66
|
+
): Promise<PdfTransportHint> {
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetchImpl(url, { method: "HEAD" });
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
return "ranged";
|
|
71
|
+
}
|
|
72
|
+
const header = response.headers.get("content-length");
|
|
73
|
+
if (header === null || !/^\d+$/u.test(header.trim())) {
|
|
74
|
+
return "ranged";
|
|
75
|
+
}
|
|
76
|
+
return transportHintForContentLength(Number(header));
|
|
77
|
+
} catch {
|
|
78
|
+
return "ranged";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
47
82
|
/**
|
|
48
83
|
* Load a PDF document from a same-origin asset URL.
|
|
49
84
|
*
|
|
@@ -59,6 +94,7 @@ export function usePdfDocument(
|
|
|
59
94
|
const getDocument = deps.getDocument ?? defaultGetDocument;
|
|
60
95
|
const classifyPdfError = deps.classifyPdfError ?? defaultClassifyPdfError;
|
|
61
96
|
const getPdfMetrics = deps.getPdfMetrics ?? defaultGetPdfMetrics;
|
|
97
|
+
const fetchImpl = deps.fetch ?? defaultFetch;
|
|
62
98
|
|
|
63
99
|
const [status, setStatus] = useState<PdfDocumentStatus>("loading");
|
|
64
100
|
const [doc, setDoc] = useState<PDFDocumentProxy | null>(null);
|
|
@@ -99,13 +135,11 @@ export function usePdfDocument(
|
|
|
99
135
|
setError(null);
|
|
100
136
|
setErrorMessage(null);
|
|
101
137
|
|
|
102
|
-
|
|
103
|
-
const instanceDocId = loadingTask.gnoDocId;
|
|
104
|
-
setDocId(instanceDocId);
|
|
138
|
+
setDocId(null);
|
|
105
139
|
|
|
106
140
|
const ownership: LoadOwnership = {
|
|
107
|
-
docId:
|
|
108
|
-
task:
|
|
141
|
+
docId: null,
|
|
142
|
+
task: null,
|
|
109
143
|
viewerDoc: null,
|
|
110
144
|
tornDown: false,
|
|
111
145
|
destroyMetricEmitted: false,
|
|
@@ -119,12 +153,15 @@ export function usePdfDocument(
|
|
|
119
153
|
if (ownership.tornDown) {
|
|
120
154
|
return true;
|
|
121
155
|
}
|
|
122
|
-
return Boolean(
|
|
156
|
+
return Boolean(
|
|
157
|
+
(ownership.task as { destroyed?: boolean } | null)?.destroyed
|
|
158
|
+
);
|
|
123
159
|
};
|
|
124
160
|
|
|
125
161
|
/**
|
|
126
162
|
* Idempotent teardown for this load.
|
|
127
|
-
* -
|
|
163
|
+
* - Destroy the loading task exactly once when one exists; a load torn
|
|
164
|
+
* down during the HEAD probe never creates a task.
|
|
128
165
|
* - Emit documentDestroy only for a viewer-owned success.
|
|
129
166
|
* - A stale late resolution needs no separate proxy cleanup: the loading
|
|
130
167
|
* task already owns and destroys its transport.
|
|
@@ -138,45 +175,65 @@ export function usePdfDocument(
|
|
|
138
175
|
const viewerDoc = ownership.viewerDoc;
|
|
139
176
|
ownership.viewerDoc = null;
|
|
140
177
|
|
|
141
|
-
if (viewerDoc) {
|
|
178
|
+
if (viewerDoc && ownership.docId !== null) {
|
|
142
179
|
if (!ownership.destroyMetricEmitted) {
|
|
143
180
|
ownership.destroyMetricEmitted = true;
|
|
144
181
|
metrics.recordDocumentDestroy({ docId: ownership.docId });
|
|
145
182
|
}
|
|
146
183
|
}
|
|
147
184
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
185
|
+
const task = ownership.task;
|
|
186
|
+
if (task) {
|
|
187
|
+
try {
|
|
188
|
+
void task.destroy().catch(() => undefined);
|
|
189
|
+
} catch {
|
|
190
|
+
// ignore
|
|
191
|
+
}
|
|
152
192
|
}
|
|
153
193
|
};
|
|
154
194
|
|
|
155
|
-
|
|
156
|
-
|
|
195
|
+
const failLoad = (err: unknown): void => {
|
|
196
|
+
if (isStale()) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const reason = classifyPdfError(err);
|
|
200
|
+
setStatus("error");
|
|
201
|
+
setError(reason);
|
|
202
|
+
setErrorMessage(err instanceof Error ? err.message : String(err));
|
|
203
|
+
setDoc(null);
|
|
204
|
+
setNumPages(0);
|
|
205
|
+
setFirstPageReady(false);
|
|
206
|
+
teardown();
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// One HEAD per document load picks the transport tier; the load itself
|
|
210
|
+
// starts only if this generation is still live once the probe settles.
|
|
211
|
+
// A synchronous getDocument failure routes through the same error path.
|
|
212
|
+
void probeTransportHint(url, fetchImpl)
|
|
213
|
+
.then((transport) => {
|
|
157
214
|
if (isStale()) {
|
|
158
|
-
teardown();
|
|
159
215
|
return;
|
|
160
216
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
217
|
+
const loadingTask = getDocument({ url, transport });
|
|
218
|
+
ownership.task = loadingTask;
|
|
219
|
+
ownership.docId = loadingTask.gnoDocId;
|
|
220
|
+
setDocId(loadingTask.gnoDocId);
|
|
221
|
+
|
|
222
|
+
loadingTask.promise
|
|
223
|
+
.then(async (pdf) => {
|
|
224
|
+
if (isStale()) {
|
|
225
|
+
teardown();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
ownership.viewerDoc = pdf;
|
|
229
|
+
setDoc(pdf);
|
|
230
|
+
setNumPages(pdf.numPages);
|
|
231
|
+
setStatus("ready");
|
|
232
|
+
setFirstPageReady(pdf.numPages > 0);
|
|
233
|
+
})
|
|
234
|
+
.catch(failLoad);
|
|
166
235
|
})
|
|
167
|
-
.catch(
|
|
168
|
-
if (isStale()) {
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
const reason = classifyPdfError(err);
|
|
172
|
-
setStatus("error");
|
|
173
|
-
setError(reason);
|
|
174
|
-
setErrorMessage(err instanceof Error ? err.message : String(err));
|
|
175
|
-
setDoc(null);
|
|
176
|
-
setNumPages(0);
|
|
177
|
-
setFirstPageReady(false);
|
|
178
|
-
teardown();
|
|
179
|
-
});
|
|
236
|
+
.catch(failLoad);
|
|
180
237
|
|
|
181
238
|
return () => {
|
|
182
239
|
if (ownershipRef.current === ownership) {
|
|
@@ -184,7 +241,14 @@ export function usePdfDocument(
|
|
|
184
241
|
}
|
|
185
242
|
teardown();
|
|
186
243
|
};
|
|
187
|
-
}, [
|
|
244
|
+
}, [
|
|
245
|
+
url,
|
|
246
|
+
retryToken,
|
|
247
|
+
getDocument,
|
|
248
|
+
classifyPdfError,
|
|
249
|
+
getPdfMetrics,
|
|
250
|
+
fetchImpl,
|
|
251
|
+
]);
|
|
188
252
|
|
|
189
253
|
return {
|
|
190
254
|
status,
|