@rangojs/router 0.17.0 → 0.18.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/README.md +3 -2
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/history-state.d.ts +16 -0
- package/dist/types/cache/cache-key-utils.d.ts +17 -2
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +98 -14
- package/dist/types/cache/cache-tag.d.ts +92 -7
- package/dist/types/cache/cf/cf-base64.d.ts +6 -0
- package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
- package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
- package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
- package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
- package/dist/types/cache/handle-snapshot.d.ts +4 -3
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +110 -21
- package/dist/types/cache/tag-invalidation.d.ts +30 -3
- package/dist/types/cache/types.d.ts +207 -43
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/middleware-types.d.ts +6 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +40 -11
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +21 -10
- package/dist/types/router/segment-resolution/mask-nested.d.ts +65 -24
- package/dist/types/rsc/full-payload.d.ts +14 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +53 -6
- package/dist/types/rsc/shell-capture.d.ts +146 -15
- package/dist/types/rsc/shell-serve.d.ts +140 -17
- package/dist/types/rsc/types.d.ts +7 -0
- package/dist/types/server/context.d.ts +109 -15
- package/dist/types/server/cookie-store.d.ts +21 -0
- package/dist/types/server/request-context.d.ts +159 -33
- package/dist/types/ssr/index.d.ts +9 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/testing/flight.entry.d.ts +7 -0
- package/dist/types/testing/run-loader.d.ts +18 -3
- package/dist/types/testing/serve-shell-request.d.ts +132 -0
- package/dist/types/testing/shell-status.d.ts +23 -10
- package/dist/types/types/handler-context.d.ts +14 -1
- package/dist/types/urls/pattern-types.d.ts +22 -18
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +46 -9
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +17 -7
- package/skills/cache-guide/SKILL.md +123 -51
- package/skills/caching/SKILL.md +253 -23
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +11 -6
- package/skills/cloudflare/SKILL.md +15 -0
- package/skills/comparison/references/framework-comparison.md +44 -32
- package/skills/defer-hydration/SKILL.md +50 -30
- package/skills/deployment-caching/SKILL.md +5 -2
- package/skills/document-cache/SKILL.md +8 -2
- package/skills/handler-use/SKILL.md +2 -0
- package/skills/hooks/handle-and-actions.md +3 -1
- package/skills/hooks/state.md +3 -1
- package/skills/loader/SKILL.md +161 -39
- package/skills/middleware/SKILL.md +16 -9
- package/skills/migrate-nextjs/SKILL.md +67 -36
- package/skills/migrate-react-router/SKILL.md +2 -1
- package/skills/migrate-react-router/component-migration.md +3 -1
- package/skills/migrate-react-router/data-and-actions.md +3 -1
- package/skills/migrate-react-router/route-mapping.md +7 -4
- package/skills/observability/SKILL.md +64 -0
- package/skills/parallel/SKILL.md +13 -8
- package/skills/ppr/SKILL.md +655 -363
- package/skills/prerender/SKILL.md +27 -14
- package/skills/rango/SKILL.md +52 -33
- package/skills/route/SKILL.md +12 -7
- package/skills/router-setup/SKILL.md +4 -2
- package/skills/scripts/SKILL.md +8 -1
- package/skills/server-actions/SKILL.md +11 -0
- package/skills/shell-manifest/SKILL.md +21 -12
- package/skills/testing/SKILL.md +25 -21
- package/skills/testing/cache-prerender.md +15 -14
- package/skills/testing/loader.md +18 -1
- package/skills/testing/serve-shell-request.md +188 -0
- package/skills/theme/SKILL.md +52 -4
- package/skills/typesafety/env-and-bindings.md +10 -2
- package/skills/use-cache/SKILL.md +39 -16
- package/skills/vercel/SKILL.md +14 -2
- package/skills/view-transitions/SKILL.md +1 -1
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/history-state.ts +23 -0
- package/src/browser/rsc-router.tsx +5 -0
- package/src/cache/cache-key-utils.ts +22 -2
- package/src/cache/cache-policy.ts +22 -0
- package/src/cache/cache-runtime.ts +34 -7
- package/src/cache/cache-scope.ts +345 -33
- package/src/cache/cache-tag.ts +375 -11
- package/src/cache/cf/cf-base64.ts +36 -4
- package/src/cache/cf/cf-cache-constants.ts +12 -0
- package/src/cache/cf/cf-cache-store.ts +990 -348
- package/src/cache/cf/cf-cache-types.ts +32 -4
- package/src/cache/cf/cf-shell-frame.ts +258 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
- package/src/cache/handle-snapshot.ts +4 -3
- package/src/cache/index.ts +2 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +217 -27
- package/src/cache/tag-invalidation.ts +104 -19
- package/src/cache/types.ts +211 -43
- package/src/cache/vercel/vercel-cache-store.ts +545 -61
- package/src/prerender/build-shell-capture.ts +65 -11
- package/src/route-definition/dsl-helpers.ts +4 -3
- package/src/router/handler-context.ts +38 -4
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +19 -7
- package/src/router/loader-resolution.ts +63 -32
- package/src/router/match-api.ts +28 -9
- package/src/router/match-handlers.ts +5 -0
- package/src/router/match-middleware/cache-lookup.ts +17 -0
- package/src/router/match-middleware/cache-store.ts +31 -11
- package/src/router/middleware-types.ts +6 -1
- package/src/router/middleware.ts +23 -4
- package/src/router/prerender-match.ts +6 -0
- package/src/router/segment-resolution/fresh.ts +18 -9
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +227 -56
- package/src/router/segment-resolution/loader-snapshot.ts +203 -51
- package/src/router/segment-resolution/mask-nested.ts +136 -42
- package/src/router/segment-resolution/revalidation.ts +27 -10
- package/src/rsc/full-payload.ts +25 -1
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/response-cache-serve.ts +9 -6
- package/src/rsc/rsc-rendering.ts +601 -181
- package/src/rsc/shell-build-manifest.ts +27 -9
- package/src/rsc/shell-capture-constants.ts +60 -6
- package/src/rsc/shell-capture.ts +1001 -250
- package/src/rsc/shell-serve.ts +316 -34
- package/src/rsc/types.ts +7 -0
- package/src/server/context.ts +219 -28
- package/src/server/cookie-store.ts +87 -62
- package/src/server/request-context.ts +263 -38
- package/src/ssr/index.tsx +96 -37
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/run-loader.ts +63 -5
- package/src/testing/serve-shell-request.ts +378 -0
- package/src/testing/shell-status.ts +34 -12
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/types/handler-context.ts +14 -1
- package/src/urls/pattern-types.ts +22 -18
- package/src/vite/discovery/shell-prerender-phase.ts +9 -0
- package/src/vite/router-discovery.ts +14 -4
package/src/rsc/shell-serve.ts
CHANGED
|
@@ -20,9 +20,15 @@ import React from "react";
|
|
|
20
20
|
import { isPprEntry, type EntryData } from "../server/context.js";
|
|
21
21
|
import { sortedSearchString } from "../cache/cache-key-utils.js";
|
|
22
22
|
import type { SearchParamsFilter } from "../cache/search-params-filter.js";
|
|
23
|
+
import { base64ToBytes } from "../cache/cf/cf-base64.js";
|
|
24
|
+
import { getNonceAttribute } from "../theme/theme-script.js";
|
|
23
25
|
import type {
|
|
24
26
|
DocumentShellCacheEntry,
|
|
25
27
|
ShellCacheEntry,
|
|
28
|
+
ShellDocumentRead,
|
|
29
|
+
ShellReadStats,
|
|
30
|
+
ShellSnapshotFailure,
|
|
31
|
+
ShellSnapshotRecord,
|
|
26
32
|
SegmentCacheStore,
|
|
27
33
|
} from "../cache/types.js";
|
|
28
34
|
import { SHELL_CAPTURE_MAX_WAIT_MS } from "./shell-capture-constants.js";
|
|
@@ -132,6 +138,66 @@ export function buildShellKey(url: URL, filter?: SearchParamsFilter): string {
|
|
|
132
138
|
return `${url.host}${url.pathname}${shellSearchSeed(url, filter)}:shell`;
|
|
133
139
|
}
|
|
134
140
|
|
|
141
|
+
export {
|
|
142
|
+
navigationShellKey,
|
|
143
|
+
partitionShellKey,
|
|
144
|
+
} from "./shell-capture-constants.js";
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The forced-MISS query marker. A document HIT that cannot finish its tail
|
|
148
|
+
* reloads the page with it (shellReloadScript); the serve gate renders a
|
|
149
|
+
* request carrying it on axis 1, with no shell read and no capture, so the
|
|
150
|
+
* reload can never be a HIT that degrades again. The handler drops it from
|
|
151
|
+
* the request on entry (withoutShellMissMarker) and keeps only the flag
|
|
152
|
+
* (RequestContext._shellForcedMiss), so nothing else sees it.
|
|
153
|
+
*/
|
|
154
|
+
export const SHELL_MISS_PARAM: string = "_rsc_shell";
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The request without the forced-MISS marker, or undefined when it carries
|
|
158
|
+
* none. The handler calls it before anything reads the request: left in, the
|
|
159
|
+
* marker reached `ctx.request.url`, `originalUrl`, middleware, and the SSR
|
|
160
|
+
* search seed (so `useSearchParams` rendered it), and a `"use cache"` key
|
|
161
|
+
* built from the request URL was never read again. GET and HEAD only: the
|
|
162
|
+
* reload is a navigation, and a request with a body cannot be re-created
|
|
163
|
+
* without re-streaming it. The substring test keeps the URL parse off every
|
|
164
|
+
* other request.
|
|
165
|
+
*/
|
|
166
|
+
export function withoutShellMissMarker(request: Request): Request | undefined {
|
|
167
|
+
if (!request.url.includes(SHELL_MISS_PARAM)) return undefined;
|
|
168
|
+
if (request.method !== "GET" && request.method !== "HEAD") return undefined;
|
|
169
|
+
const url = new URL(request.url);
|
|
170
|
+
if (!url.searchParams.has(SHELL_MISS_PARAM)) return undefined;
|
|
171
|
+
url.searchParams.delete(SHELL_MISS_PARAM);
|
|
172
|
+
return new Request(url, request);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The inline script a degraded document HIT ends with: reload once into a
|
|
177
|
+
* forced MISS. The marker in the URL is the loop bound — a request carrying
|
|
178
|
+
* it is never a HIT, so this script never runs on the reload, and if it ever
|
|
179
|
+
* did (a server that ignores the marker) it would return without touching
|
|
180
|
+
* the page. window.stop() runs only when it reloads: it keeps the half-sent
|
|
181
|
+
* Flight stream from being closed by DOMContentLoaded, which would throw
|
|
182
|
+
* React's "Connection closed" (#412) before the reload lands. `nonce` is the
|
|
183
|
+
* request's CSP nonce; a ppr route with an active nonce is axis 1
|
|
184
|
+
* (rsc-rendering.ts shellServePlan), so on a HIT it is normally absent.
|
|
185
|
+
*/
|
|
186
|
+
export function shellReloadScript(nonce?: string): string {
|
|
187
|
+
return inlineShellScript(
|
|
188
|
+
"(function(){var u=new URL(location.href);" +
|
|
189
|
+
`if(u.searchParams.has(${JSON.stringify(SHELL_MISS_PARAM)}))return;` +
|
|
190
|
+
`u.searchParams.set(${JSON.stringify(SHELL_MISS_PARAM)},"miss");` +
|
|
191
|
+
"try{window.stop()}catch(e){}location.replace(u.href)})()",
|
|
192
|
+
nonce,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** An inline script a shell HIT's tail appends, carrying the CSP nonce if any. */
|
|
197
|
+
export function inlineShellScript(body: string, nonce?: string): string {
|
|
198
|
+
return `<script${getNonceAttribute(nonce)}>${body}</script>`;
|
|
199
|
+
}
|
|
200
|
+
|
|
135
201
|
/**
|
|
136
202
|
* The shell key's search portion (`?`-prefixed sorted search with the
|
|
137
203
|
* cache.searchParams filter applied, or "") — ALSO the string the capture and
|
|
@@ -166,17 +232,25 @@ export function isValidShellHit(
|
|
|
166
232
|
);
|
|
167
233
|
}
|
|
168
234
|
|
|
235
|
+
/** The postponed blob is stored and parses (null is the DATA variant). */
|
|
236
|
+
function hasParseablePostponed(
|
|
237
|
+
entry: ShellCacheEntry,
|
|
238
|
+
): entry is ShellCacheEntry & { postponed: string | null } {
|
|
239
|
+
if (entry.postponed === undefined) return false;
|
|
240
|
+
try {
|
|
241
|
+
if (entry.postponed !== null) JSON.parse(entry.postponed);
|
|
242
|
+
return true;
|
|
243
|
+
} catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
169
248
|
/**
|
|
170
|
-
* DOCUMENT-half
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* page that never hydrates, re-served on every request until the entry ages
|
|
176
|
-
* out (no eviction path exists; failure schedules no recapture by itself).
|
|
177
|
-
* Checking here turns a corrupt entry (store-layer fault) into a plain MISS
|
|
178
|
-
* the recapture overwrites. Cost: one duplicate decode/parse per HIT, sub-ms
|
|
179
|
-
* against a prelude flush that dominates the path.
|
|
249
|
+
* DOCUMENT-half structural gate and type narrowing, without decoding the
|
|
250
|
+
* prelude: the prelude is a string and the postponed blob parses. Partial
|
|
251
|
+
* replay (which never serves the prelude) and the build-manifest read-through
|
|
252
|
+
* use it as-is; the document HIT path runs it inside
|
|
253
|
+
* {@link openShellDocument}, whose single decode is the prelude's check.
|
|
180
254
|
*
|
|
181
255
|
* navigationOnly entries store no document half (prelude/postponed absent) and
|
|
182
256
|
* therefore never pass. The partial-replay path skips this gate for them
|
|
@@ -187,25 +261,85 @@ export function isValidShellHit(
|
|
|
187
261
|
export function hasIntactShellPayload(
|
|
188
262
|
entry: ShellCacheEntry,
|
|
189
263
|
): entry is DocumentShellCacheEntry {
|
|
190
|
-
|
|
191
|
-
|
|
264
|
+
return typeof entry.prelude === "string" && hasParseablePostponed(entry);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** A document shell ready to serve: its prelude decoded exactly once. */
|
|
268
|
+
export interface ShellDocument {
|
|
269
|
+
entry: ShellCacheEntry;
|
|
270
|
+
prelude: Uint8Array;
|
|
271
|
+
/** React's postponed state, parse-checked (null = DATA variant). */
|
|
272
|
+
postponed: string | null;
|
|
273
|
+
/**
|
|
274
|
+
* The capture snapshot: on the entry, or still arriving on its own promise
|
|
275
|
+
* after a prelude-first read (SegmentCacheStore.readShellDocument), which
|
|
276
|
+
* only the tail awaits.
|
|
277
|
+
*/
|
|
278
|
+
snapshot:
|
|
279
|
+
| ShellSnapshotRecord[]
|
|
280
|
+
| undefined
|
|
281
|
+
| Promise<ShellSnapshotRecord[] | undefined>;
|
|
282
|
+
/** Why a prelude-first read's snapshot is missing (ShellDocumentRead). */
|
|
283
|
+
snapshotFailure?: Promise<ShellSnapshotFailure | undefined>;
|
|
284
|
+
/** The prelude-first read's stats, when perf metrics are on. */
|
|
285
|
+
stats?: ShellReadStats;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Pre-commit integrity gate for a document HIT, and its only prelude decode.
|
|
290
|
+
* A stored entry whose prelude is not decodable base64 or whose postponed blob
|
|
291
|
+
* is not parseable JSON would otherwise throw AFTER the 200 + full static
|
|
292
|
+
* prelude flushed (`resumeShellHTML` parses in the tail) — the client gets a
|
|
293
|
+
* visually complete page that never hydrates, re-served on every request
|
|
294
|
+
* until the entry ages out. Returning null here turns a corrupt entry
|
|
295
|
+
* (store-layer fault) into a plain MISS the recapture overwrites. The decoded
|
|
296
|
+
* bytes are what serveShellHit enqueues, so the check costs no second decode.
|
|
297
|
+
* A prelude-first `read` (SegmentCacheStore.readShellDocument) already carries
|
|
298
|
+
* the raw bytes and a pending snapshot, so only its postponed blob is checked.
|
|
299
|
+
*/
|
|
300
|
+
export function openShellDocument(
|
|
301
|
+
entry: ShellCacheEntry,
|
|
302
|
+
read?: Pick<
|
|
303
|
+
ShellDocumentRead,
|
|
304
|
+
"prelude" | "snapshot" | "snapshotFailure" | "stats"
|
|
305
|
+
>,
|
|
306
|
+
): ShellDocument | null {
|
|
307
|
+
if (read) {
|
|
308
|
+
if (!hasParseablePostponed(entry)) return null;
|
|
309
|
+
return {
|
|
310
|
+
entry,
|
|
311
|
+
postponed: entry.postponed,
|
|
312
|
+
prelude: read.prelude,
|
|
313
|
+
snapshot: read.snapshot,
|
|
314
|
+
...(read.snapshotFailure && { snapshotFailure: read.snapshotFailure }),
|
|
315
|
+
...(read.stats && { stats: read.stats }),
|
|
316
|
+
};
|
|
192
317
|
}
|
|
318
|
+
if (!hasIntactShellPayload(entry)) return null;
|
|
319
|
+
let prelude: Uint8Array;
|
|
193
320
|
try {
|
|
194
|
-
base64ToBytes(entry.prelude);
|
|
195
|
-
if (entry.postponed !== null) JSON.parse(entry.postponed);
|
|
196
|
-
return true;
|
|
321
|
+
prelude = base64ToBytes(entry.prelude);
|
|
197
322
|
} catch {
|
|
198
|
-
return
|
|
323
|
+
return null;
|
|
199
324
|
}
|
|
325
|
+
return {
|
|
326
|
+
entry,
|
|
327
|
+
postponed: entry.postponed,
|
|
328
|
+
prelude,
|
|
329
|
+
snapshot: entry.snapshot,
|
|
330
|
+
};
|
|
200
331
|
}
|
|
201
332
|
|
|
202
|
-
/**
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
333
|
+
/**
|
|
334
|
+
* Prelude enqueue granularity. A streaming compressor in front of the worker
|
|
335
|
+
* emits its first byte only after compressing the whole write it was handed:
|
|
336
|
+
* one 629 KB enqueue delayed the first compressed byte by the full prelude's
|
|
337
|
+
* compression time (brotli-6 5.1 ms, gzip-6 1.1 ms in Node zlib with a flush
|
|
338
|
+
* per write; workerd's CompressionStream emits nothing until the whole write is
|
|
339
|
+
* compressed), while 32 KB chunks emit within 0.1 ms and cost slightly more
|
|
340
|
+
* in total compression (0.55-0.70 ms) than one write (issue #941).
|
|
341
|
+
*/
|
|
342
|
+
export const SHELL_PRELUDE_CHUNK_BYTES: number = 32 * 1024;
|
|
209
343
|
|
|
210
344
|
/** True when the store implements the shell entry family. */
|
|
211
345
|
export function hasShellFamily(
|
|
@@ -227,8 +361,22 @@ export function hasShellFamily(
|
|
|
227
361
|
export interface ShellTailTiming {
|
|
228
362
|
key: string;
|
|
229
363
|
outcome: "complete" | "redirect" | "error";
|
|
364
|
+
/** The capture snapshot available to the tail (read and parsed). */
|
|
365
|
+
snapshotMs?: number;
|
|
366
|
+
/** Snapshot bytes read after the commit (prelude-first stores). */
|
|
367
|
+
snapshotBytes?: number;
|
|
368
|
+
/** Snapshot bytes read after the commit, excluding the parse. */
|
|
369
|
+
snapshotReadMs?: number;
|
|
370
|
+
/** Snapshot JSON.parse CPU (reads 0 on a deployed worker: see bytes). */
|
|
371
|
+
snapshotParseMs?: number;
|
|
372
|
+
/** Snapshot records by family, e.g. `segment:1/item:5` (no commas: it rides a Server-Timing desc). */
|
|
373
|
+
snapshotRecords?: string;
|
|
374
|
+
/** Records the capture pruned from the snapshot, same format (ShellCacheEntry.prunedRecords). */
|
|
375
|
+
snapshotPruned?: string;
|
|
230
376
|
/** Loader-family seed decode (only when the entry carried a snapshot). */
|
|
231
377
|
seedMs?: number;
|
|
378
|
+
/** The seed decode's own duration: Flight deserialization, CPU only. */
|
|
379
|
+
seedCpuMs?: number;
|
|
232
380
|
/** Tail router.match() settled. */
|
|
233
381
|
matchMs?: number;
|
|
234
382
|
/** Tail stream (resume output) handed to the response stream. */
|
|
@@ -244,13 +392,35 @@ export interface ShellTailTiming {
|
|
|
244
392
|
}
|
|
245
393
|
|
|
246
394
|
/**
|
|
247
|
-
* Compact single-line form for the console log and the
|
|
248
|
-
*
|
|
249
|
-
* escaping needed.
|
|
395
|
+
* Compact single-line form for the console log and the Server-Timing mirror's
|
|
396
|
+
* `desc`: alphanumerics and `=`, `-`, `:`, `/` only (records=segment:1 pruned=item:5),
|
|
397
|
+
* so no quoted-string escaping is needed. Offsets are from the commit; the
|
|
398
|
+
* `-cpu` fields are CPU-only durations.
|
|
250
399
|
*/
|
|
251
400
|
export function describeShellTailTiming(timing: ShellTailTiming): string {
|
|
252
401
|
const parts: string[] = [timing.outcome];
|
|
402
|
+
if (timing.snapshotMs !== undefined) {
|
|
403
|
+
parts.push(`snapshot=${timing.snapshotMs}ms`);
|
|
404
|
+
}
|
|
405
|
+
if (timing.snapshotReadMs !== undefined) {
|
|
406
|
+
parts.push(`snapshot-read=${timing.snapshotReadMs}ms`);
|
|
407
|
+
}
|
|
408
|
+
if (timing.snapshotBytes !== undefined) {
|
|
409
|
+
parts.push(`snapshot-bytes=${timing.snapshotBytes}b`);
|
|
410
|
+
}
|
|
411
|
+
if (timing.snapshotParseMs !== undefined) {
|
|
412
|
+
parts.push(`snapshot-parse-cpu=${timing.snapshotParseMs}ms`);
|
|
413
|
+
}
|
|
414
|
+
if (timing.snapshotRecords !== undefined) {
|
|
415
|
+
parts.push(`records=${timing.snapshotRecords}`);
|
|
416
|
+
}
|
|
417
|
+
if (timing.snapshotPruned !== undefined) {
|
|
418
|
+
parts.push(`pruned=${timing.snapshotPruned}`);
|
|
419
|
+
}
|
|
253
420
|
if (timing.seedMs !== undefined) parts.push(`seed=${timing.seedMs}ms`);
|
|
421
|
+
if (timing.seedCpuMs !== undefined) {
|
|
422
|
+
parts.push(`seed-cpu=${timing.seedCpuMs}ms`);
|
|
423
|
+
}
|
|
254
424
|
if (timing.matchMs !== undefined) parts.push(`match=${timing.matchMs}ms`);
|
|
255
425
|
if (timing.handoverMs !== undefined) {
|
|
256
426
|
parts.push(`handover=${timing.handoverMs}ms`);
|
|
@@ -269,19 +439,19 @@ export function describeShellTailTiming(timing: ShellTailTiming): string {
|
|
|
269
439
|
}
|
|
270
440
|
|
|
271
441
|
/**
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
442
|
+
* Last-tail-per-key buffer backing the `ppr:tail` Server-Timing mirror: a
|
|
443
|
+
* HIT's tail finishes after its own headers are long gone, so its per-stage
|
|
444
|
+
* numbers ride the NEXT ppr GET for the key when the metrics surface is
|
|
445
|
+
* active (debugPerformance). Same shape and FIFO cap as the capture mirror
|
|
446
|
+
* (shell-capture.ts lastCaptureEventsForTiming). serveShellHit collects and
|
|
447
|
+
* publishes a timing only in dev or when the HIT itself collected metrics, so
|
|
448
|
+
* a production isolate without debugPerformance never grows the map.
|
|
278
449
|
*/
|
|
279
450
|
const lastTailTimingsForServerTiming = new Map<string, ShellTailTiming>();
|
|
280
451
|
const MAX_TAIL_TIMING_KEYS = 100;
|
|
281
452
|
|
|
282
|
-
/** Buffer one terminal tail timing for the
|
|
453
|
+
/** Buffer one terminal tail timing for the Server-Timing mirror. */
|
|
283
454
|
export function publishShellTailTiming(timing: ShellTailTiming): void {
|
|
284
|
-
if (process.env.NODE_ENV === "production") return;
|
|
285
455
|
lastTailTimingsForServerTiming.delete(timing.key);
|
|
286
456
|
if (lastTailTimingsForServerTiming.size >= MAX_TAIL_TIMING_KEYS) {
|
|
287
457
|
const oldest = lastTailTimingsForServerTiming.keys().next().value;
|
|
@@ -348,3 +518,115 @@ export function warnPprNonceActiveOnce(key: string): void {
|
|
|
348
518
|
"ppr option on this route, or stop setting a per-request nonce for it.",
|
|
349
519
|
);
|
|
350
520
|
}
|
|
521
|
+
|
|
522
|
+
/** Routes already warned about a ppr window the route cache() caps. */
|
|
523
|
+
const warnedPprWindowCapped = new Set<string>();
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Dev only (the caller gates on NODE_ENV): warn once per route that the
|
|
527
|
+
* route's cache() entry reduces an explicit `ppr.ttl` or `ppr.swr`. `shell`
|
|
528
|
+
* is what the capture stores for a record written just now
|
|
529
|
+
* (shell-capture.ts capShellWindow), so the message states the values the
|
|
530
|
+
* shell actually gets. A ppr value the cap leaves whole (or raises: the
|
|
531
|
+
* cap can move a record's stale time into the shell's swr) is silent, and
|
|
532
|
+
* so is a ppr config that sets no ttl/swr.
|
|
533
|
+
*/
|
|
534
|
+
export function warnPprWindowCappedOnce(
|
|
535
|
+
routeName: string,
|
|
536
|
+
ppr: { ttl?: number; swr?: number },
|
|
537
|
+
shell: { ttl: number; swr: number },
|
|
538
|
+
cache: { ttl: number; swr: number },
|
|
539
|
+
): void {
|
|
540
|
+
if (warnedPprWindowCapped.has(routeName)) return;
|
|
541
|
+
const reduced: string[] = [];
|
|
542
|
+
for (const field of ["ttl", "swr"] as const) {
|
|
543
|
+
const own = ppr[field];
|
|
544
|
+
if (own !== undefined && shell[field] < own) {
|
|
545
|
+
reduced.push(`ppr.${field} ${own}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (reduced.length === 0) return;
|
|
549
|
+
warnedPprWindowCapped.add(routeName);
|
|
550
|
+
console.warn(
|
|
551
|
+
`[rango] Route "${routeName}": its shell is stored with ttl ${shell.ttl} ` +
|
|
552
|
+
`and swr ${shell.swr}, below its ${reduced.join(" and ")}. A shell ` +
|
|
553
|
+
`never outlives the route cache() entry (ttl ${cache.ttl}, swr ` +
|
|
554
|
+
`${cache.swr}) it was captured from, so a document request and a ` +
|
|
555
|
+
"client navigation show the same handler output. Keep ppr.ttl within " +
|
|
556
|
+
"the cache() ttl, and ppr.ttl + ppr.swr within the cache() ttl + swr, " +
|
|
557
|
+
"to silence this.",
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** Routes (or paths) already warned that a partition skips their build shell. */
|
|
562
|
+
const partitionBuildShellWarned = new Set<string>();
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Paths a partitioned request found without a build shell. Build shells are
|
|
566
|
+
* per path (a param route prerenders some of its paths), so a negative is
|
|
567
|
+
* kept per path, not per route; cleared when full, so it stays bounded.
|
|
568
|
+
*/
|
|
569
|
+
const pathsWithoutBuildShell = new Set<string>();
|
|
570
|
+
const PATHS_WITHOUT_BUILD_SHELL_MAX = 1_000;
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Whether a partitioned request needs no build-shell probe: its route was
|
|
574
|
+
* already warned, or its path was already found without one.
|
|
575
|
+
*/
|
|
576
|
+
export function partitionBuildShellCheckDone(
|
|
577
|
+
pathname: string,
|
|
578
|
+
routeName: string | undefined,
|
|
579
|
+
): boolean {
|
|
580
|
+
return (
|
|
581
|
+
partitionBuildShellWarned.has(routeName ?? pathname) ||
|
|
582
|
+
pathsWithoutBuildShell.has(pathname)
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Record a partitioned request's build-shell probe, and warn once per route
|
|
588
|
+
* when it found one: the route has a build-time shell its requests cannot
|
|
589
|
+
* read. The route's request partition (a `cache({ key })` enclosing it, at
|
|
590
|
+
* any depth (#970), or the store's keyGenerator) keys the shell per
|
|
591
|
+
* partition, and the build captured only
|
|
592
|
+
* the default one, so each partition captures at runtime. Same
|
|
593
|
+
* declared-intent-cannot-be-honored doctrine as the warnings above; a path
|
|
594
|
+
* with no build shell stays silent.
|
|
595
|
+
*/
|
|
596
|
+
export function notePartitionBuildShellCheck(
|
|
597
|
+
pathname: string,
|
|
598
|
+
routeName: string | undefined,
|
|
599
|
+
found: boolean,
|
|
600
|
+
): void {
|
|
601
|
+
if (!found) {
|
|
602
|
+
if (pathsWithoutBuildShell.size >= PATHS_WITHOUT_BUILD_SHELL_MAX) {
|
|
603
|
+
pathsWithoutBuildShell.clear();
|
|
604
|
+
}
|
|
605
|
+
pathsWithoutBuildShell.add(pathname);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
// Concurrent first requests can both probe; one of them warns.
|
|
609
|
+
const route = routeName ?? pathname;
|
|
610
|
+
if (partitionBuildShellWarned.has(route)) return;
|
|
611
|
+
partitionBuildShellWarned.add(route);
|
|
612
|
+
console.warn(
|
|
613
|
+
`[rango] Route ${routeName ? `"${routeName}" ` : ""}("${pathname}") has a ` +
|
|
614
|
+
"build-time shell, but its request partition (a cache({ key }) enclosing the route, " +
|
|
615
|
+
"or the store's keyGenerator) keys its shell per partition, so the build " +
|
|
616
|
+
"shell is not served and each partition captures its own at runtime. A " +
|
|
617
|
+
"keyGenerator that returns the default key unchanged keeps the build shell.",
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* @internal Reset the serve path's once-per-key warnings and buffered tail
|
|
623
|
+
* timings. Tests only (testing/serve-shell-request.ts resetShellTestState).
|
|
624
|
+
*/
|
|
625
|
+
export function resetShellServeStateForTests(): void {
|
|
626
|
+
warnedMissingStore.clear();
|
|
627
|
+
warnedNonceActive.clear();
|
|
628
|
+
partitionBuildShellWarned.clear();
|
|
629
|
+
pathsWithoutBuildShell.clear();
|
|
630
|
+
warnedPprWindowCapped.clear();
|
|
631
|
+
lastTailTimingsForServerTiming.clear();
|
|
632
|
+
}
|
package/src/rsc/types.ts
CHANGED
|
@@ -228,6 +228,13 @@ export interface SSRModule {
|
|
|
228
228
|
origin?: string;
|
|
229
229
|
/** Each component error the prerender reports (never the capture's own abort). */
|
|
230
230
|
onError?: (error: unknown) => void;
|
|
231
|
+
/**
|
|
232
|
+
* React's errorInfo for each task still pending at the capture's abort
|
|
233
|
+
* (its componentStack is computed when read).
|
|
234
|
+
*/
|
|
235
|
+
onAbortedTask?: (
|
|
236
|
+
errorInfo: { componentStack?: string } | undefined,
|
|
237
|
+
) => void;
|
|
231
238
|
},
|
|
232
239
|
) => Promise<{ prelude: Uint8Array; postponed: string | null } | null>;
|
|
233
240
|
|