@rangojs/router 0.15.1 → 0.17.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 +254 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/navigation-transaction.d.ts +6 -0
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
- package/dist/types/browser/rsc-router.d.ts +1 -1
- package/dist/types/browser/scroll-restoration.d.ts +7 -0
- package/dist/types/cache/cache-exec-scope.d.ts +18 -1
- package/dist/types/cache/cache-scope.d.ts +34 -5
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +53 -13
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +15 -3
- package/dist/types/client.d.ts +4 -4
- package/dist/types/client.rsc.d.ts +4 -5
- package/dist/types/components/DefaultDocument.d.ts +5 -2
- package/dist/types/context-var.d.ts +6 -5
- package/dist/types/escape-script.d.ts +1 -1
- package/dist/types/handles/MetaTags.d.ts +5 -4
- package/dist/types/handles/Scripts.d.ts +8 -7
- package/dist/types/handles/meta.d.ts +2 -2
- package/dist/types/handles/script.d.ts +17 -16
- package/dist/types/href-client.d.ts +1 -1
- package/dist/types/html.d.ts +12 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +3 -2
- package/dist/types/loader-store.d.ts +52 -0
- package/dist/types/reverse.d.ts +36 -4
- package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
- package/dist/types/route-definition/helpers-types.d.ts +36 -10
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +3 -1
- package/dist/types/router/debug-manifest.d.ts +3 -2
- package/dist/types/router/error-handling.d.ts +20 -0
- package/dist/types/router/handler-context.d.ts +6 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +3 -2
- package/dist/types/router/router-context.d.ts +3 -1
- package/dist/types/router/router-interfaces.d.ts +24 -2
- package/dist/types/router/router-options.d.ts +8 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
- package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
- package/dist/types/router/segment-wrappers.d.ts +1 -1
- package/dist/types/rsc/response-route-handler.d.ts +1 -1
- package/dist/types/rsc/rsc-rendering.d.ts +1 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
- package/dist/types/rsc/shell-capture.d.ts +5 -4
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +10 -0
- package/dist/types/server/context.d.ts +33 -3
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +24 -21
- package/dist/types/ssr/index.d.ts +15 -0
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/generated-routes.d.ts +2 -2
- package/dist/types/testing/internal/context.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +32 -6
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
- package/dist/types/testing/vitest.d.ts +35 -1
- package/dist/types/theme/ThemeScript.d.ts +3 -3
- package/dist/types/thenable-status.d.ts +9 -0
- package/dist/types/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/response-types.d.ts +3 -12
- package/dist/types/use-loader.d.ts +12 -3
- package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
- package/dist/types/vite/rango.d.ts +1 -1
- package/dist/vite/index.js +884 -768
- package/package.json +4 -4
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +65 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +207 -97
- package/skills/caching/SKILL.md +295 -93
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +53 -31
- package/skills/cloudflare/SKILL.md +72 -20
- package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
- package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
- package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
- package/skills/comparison/SKILL.md +5 -0
- package/skills/comparison/references/framework-comparison.md +39 -33
- package/skills/composability/SKILL.md +38 -29
- package/skills/css/SKILL.md +10 -13
- package/skills/debug-manifest/SKILL.md +74 -31
- package/skills/defer-hydration/SKILL.md +2 -1
- package/skills/deployment-caching/SKILL.md +6 -1
- package/skills/document-cache/SKILL.md +92 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +53 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +43 -1
- package/skills/hooks/handle-and-actions.md +31 -16
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +37 -15
- package/skills/hooks/urls.md +7 -5
- package/skills/host-router/SKILL.md +64 -12
- package/skills/i18n/SKILL.md +60 -34
- package/skills/intercept/SKILL.md +161 -26
- package/skills/layout/SKILL.md +106 -50
- package/skills/links/SKILL.md +51 -19
- package/skills/loader/SKILL.md +242 -106
- package/skills/middleware/SKILL.md +119 -23
- package/skills/migrate-nextjs/SKILL.md +100 -45
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +49 -38
- package/skills/migrate-react-router/component-migration.md +25 -6
- package/skills/migrate-react-router/data-and-actions.md +18 -5
- package/skills/migrate-react-router/route-mapping.md +12 -6
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +28 -14
- package/skills/parallel/SKILL.md +75 -39
- package/skills/ppr/SKILL.md +345 -150
- package/skills/prerender/SKILL.md +189 -100
- package/skills/rango/SKILL.md +65 -34
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +165 -60
- package/skills/router-setup/SKILL.md +418 -240
- package/skills/scripts/SKILL.md +31 -20
- package/skills/server-actions/SKILL.md +220 -59
- package/skills/shell-manifest/SKILL.md +12 -7
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +68 -32
- package/skills/testing/cache-prerender.md +55 -29
- package/skills/testing/client-components.md +59 -6
- package/skills/testing/e2e-parity.md +12 -2
- package/skills/testing/flight.md +19 -11
- package/skills/testing/handles.md +8 -5
- package/skills/testing/loader.md +8 -5
- package/skills/testing/middleware.md +8 -3
- package/skills/testing/predicates.md +120 -0
- package/skills/testing/render-handler.md +70 -22
- package/skills/testing/response-routes.md +11 -4
- package/skills/testing/reverse-and-types.md +20 -14
- package/skills/testing/server-actions.md +12 -3
- package/skills/testing/server-tree.md +9 -0
- package/skills/testing/setup.md +9 -5
- package/skills/theme/SKILL.md +106 -29
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +126 -47
- package/skills/typesafety/generated-files-and-cli.md +26 -14
- package/skills/typesafety/params-and-search.md +44 -19
- package/skills/typesafety/route-types.md +50 -14
- package/skills/use-cache/SKILL.md +200 -82
- package/skills/vercel/SKILL.md +21 -11
- package/skills/view-transitions/SKILL.md +41 -20
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/index.ts +2 -2
- package/src/browser/navigation-bridge.ts +3 -4
- package/src/browser/navigation-transaction.ts +22 -2
- package/src/browser/partial-update.ts +75 -80
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +1 -1
- package/src/browser/scroll-restoration.ts +53 -6
- package/src/cache/cache-exec-scope.ts +37 -4
- package/src/cache/cache-key-utils.ts +1 -6
- package/src/cache/cache-runtime.ts +230 -61
- package/src/cache/cache-scope.ts +76 -12
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +132 -28
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/tag-invalidation.ts +4 -3
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +16 -3
- package/src/client.rsc.tsx +10 -9
- package/src/client.tsx +5 -4
- package/src/components/DefaultDocument.tsx +7 -2
- package/src/context-var.ts +6 -5
- package/src/escape-script.ts +1 -1
- package/src/handles/MetaTags.tsx +13 -11
- package/src/handles/Scripts.tsx +11 -10
- package/src/handles/meta.ts +2 -2
- package/src/handles/script.ts +17 -16
- package/src/href-client.ts +1 -1
- package/src/html.ts +12 -0
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +1 -0
- package/src/loader-store.ts +106 -0
- package/src/prerender/build-shell-capture.ts +8 -11
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +134 -42
- package/src/route-definition/helpers-types.ts +36 -10
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +2 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +34 -25
- package/src/router/intercept-resolution.ts +125 -39
- package/src/router/loader-resolution.ts +56 -11
- package/src/router/match-api.ts +12 -17
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +93 -10
- package/src/router/match-middleware/cache-store.ts +17 -90
- package/src/router/middleware-types.ts +3 -5
- package/src/router/middleware.ts +52 -9
- package/src/router/prerender-match.ts +53 -19
- package/src/router/route-snapshot.ts +11 -1
- package/src/router/router-context.ts +2 -0
- package/src/router/router-interfaces.ts +25 -2
- package/src/router/router-options.ts +8 -5
- package/src/router/segment-resolution/fresh.ts +44 -12
- package/src/router/segment-resolution/loader-cache.ts +205 -45
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +6 -6
- package/src/router/segment-resolution/mask-nested.ts +7 -7
- package/src/router/segment-resolution/revalidation.ts +34 -6
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +8 -45
- package/src/rsc/shell-capture-constants.ts +13 -0
- package/src/rsc/shell-capture.ts +111 -90
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +11 -0
- package/src/server/context.ts +88 -11
- package/src/server/cookie-store.ts +5 -4
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +35 -30
- package/src/ssr/index.tsx +40 -7
- package/src/ssr-suspension-warning.ts +2 -13
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/generated-routes.ts +2 -2
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/internal/flight-client-globals.ts +7 -5
- package/src/testing/render-route.tsx +85 -24
- package/src/testing/run-loader.ts +5 -1
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeProvider.tsx +14 -12
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/thenable-status.ts +16 -0
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +8 -6
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +24 -4
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
package/src/cache/cache-scope.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { reportCacheError } from "./cache-error.js";
|
|
|
28
28
|
// is unchanged: both methods are async and already awaited the codec.
|
|
29
29
|
import {
|
|
30
30
|
captureHandles,
|
|
31
|
+
captureHandleOwners,
|
|
31
32
|
restoreHandles,
|
|
32
33
|
encodeHandles,
|
|
33
34
|
decodeHandles,
|
|
@@ -149,6 +150,14 @@ export class CacheScope {
|
|
|
149
150
|
config: PartialCacheOptions | false,
|
|
150
151
|
parent: CacheScope | null = null,
|
|
151
152
|
private readonly defaultKeyPrefix?: "doc",
|
|
153
|
+
/**
|
|
154
|
+
* @internal shortCode of the outermost cache() entry of this scope's
|
|
155
|
+
* enabled chain (createCacheScope). Entries above it are live: their
|
|
156
|
+
* segments are not stored and resolve fresh on a hit (withCacheLookup).
|
|
157
|
+
* Undefined covers the whole matched chain (the implicit shell doc scope,
|
|
158
|
+
* a ppr route).
|
|
159
|
+
*/
|
|
160
|
+
readonly boundary?: string,
|
|
152
161
|
) {
|
|
153
162
|
this.config = config;
|
|
154
163
|
this.parent = parent;
|
|
@@ -156,6 +165,24 @@ export class CacheScope {
|
|
|
156
165
|
this.explicitStore = config !== false ? config.store : undefined;
|
|
157
166
|
}
|
|
158
167
|
|
|
168
|
+
/**
|
|
169
|
+
* @internal Whether a segment belongs to this scope's cache entry. ShortCodes are
|
|
170
|
+
* hierarchical (server/context.ts getShortCode: parent shortCode + include
|
|
171
|
+
* scope + type letter + counter), so the boundary's subtree is every id that
|
|
172
|
+
* extends the boundary shortCode at a non-digit (`C1` must not claim `C10`).
|
|
173
|
+
* Intercept slot segments ride the target route's entry wherever the
|
|
174
|
+
* intercept is declared.
|
|
175
|
+
*/
|
|
176
|
+
covers(id: string, namespace?: string): boolean {
|
|
177
|
+
const boundary = this.boundary;
|
|
178
|
+
if (boundary === undefined) return true;
|
|
179
|
+
if (id.startsWith(boundary)) {
|
|
180
|
+
const next = id.charCodeAt(boundary.length);
|
|
181
|
+
return !(next >= 48 && next <= 57);
|
|
182
|
+
}
|
|
183
|
+
return namespace?.startsWith("intercept:") === true;
|
|
184
|
+
}
|
|
185
|
+
|
|
159
186
|
/**
|
|
160
187
|
* Whether caching is enabled for this scope
|
|
161
188
|
*/
|
|
@@ -316,11 +343,14 @@ export class CacheScope {
|
|
|
316
343
|
* @param pathname - URL pathname for cache key generation
|
|
317
344
|
* @param params - Route params for cache key generation
|
|
318
345
|
* @param isIntercept - Whether this is an intercept navigation (uses different cache key)
|
|
346
|
+
* @param claimLoaderPushes - The handler context's _claimLoaderPushes, for
|
|
347
|
+
* restoreHandles' loader-owned values
|
|
319
348
|
*/
|
|
320
349
|
async lookupRoute(
|
|
321
350
|
pathname: string,
|
|
322
351
|
params: Record<string, string>,
|
|
323
352
|
isIntercept?: boolean,
|
|
353
|
+
claimLoaderPushes?: (loaderId: string) => boolean,
|
|
324
354
|
): Promise<{
|
|
325
355
|
segments: ResolvedSegment[];
|
|
326
356
|
shouldRevalidate: boolean;
|
|
@@ -329,6 +359,7 @@ export class CacheScope {
|
|
|
329
359
|
pathname,
|
|
330
360
|
params,
|
|
331
361
|
isIntercept,
|
|
362
|
+
claimLoaderPushes,
|
|
332
363
|
);
|
|
333
364
|
return outcome.status === "hit" ? outcome.result : null;
|
|
334
365
|
}
|
|
@@ -355,6 +386,7 @@ export class CacheScope {
|
|
|
355
386
|
pathname: string,
|
|
356
387
|
params: Record<string, string>,
|
|
357
388
|
isIntercept?: boolean,
|
|
389
|
+
claimLoaderPushes?: (loaderId: string) => boolean,
|
|
358
390
|
): Promise<CacheRouteLookupOutcome> {
|
|
359
391
|
if (!this.enabled) return { status: "bypass" };
|
|
360
392
|
if (!this.conditionAllows("read")) return { status: "bypass" };
|
|
@@ -424,6 +456,14 @@ export class CacheScope {
|
|
|
424
456
|
return { status: "miss" };
|
|
425
457
|
}
|
|
426
458
|
|
|
459
|
+
// A record from a whole-chain writer (a previous build on an unversioned
|
|
460
|
+
// store) can hold segments above the boundary. withCacheLookup resolves
|
|
461
|
+
// those fresh, so replaying them too would duplicate them and their
|
|
462
|
+
// handle pushes.
|
|
463
|
+
if (this.boundary !== undefined) {
|
|
464
|
+
segments = segments.filter((s) => this.covers(s.id, s.namespace));
|
|
465
|
+
}
|
|
466
|
+
|
|
427
467
|
// A hit serves content that was tagged at write time, so the document
|
|
428
468
|
// tag union must include this entry's tags for updateTag()/revalidateTag()
|
|
429
469
|
// to invalidate any full-page entry built on top of it. The write path
|
|
@@ -438,7 +478,18 @@ export class CacheScope {
|
|
|
438
478
|
if (handleStore && cached.handles) {
|
|
439
479
|
const handlesRecord = await decodeHandles(cached.handles);
|
|
440
480
|
if (handlesRecord) {
|
|
441
|
-
|
|
481
|
+
if (this.boundary !== undefined) {
|
|
482
|
+
const kept = new Set(segments.map((s) => s.id));
|
|
483
|
+
for (const id of Object.keys(handlesRecord)) {
|
|
484
|
+
if (!kept.has(id)) delete handlesRecord[id];
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
restoreHandles(
|
|
488
|
+
handlesRecord,
|
|
489
|
+
handleStore,
|
|
490
|
+
cached.handleOwners,
|
|
491
|
+
claimLoaderPushes,
|
|
492
|
+
);
|
|
442
493
|
}
|
|
443
494
|
}
|
|
444
495
|
|
|
@@ -515,8 +566,11 @@ export class CacheScope {
|
|
|
515
566
|
if (!handleStore || !requestCtx) return;
|
|
516
567
|
|
|
517
568
|
// Exclude loader segments - loaders are always fresh by default
|
|
518
|
-
// Loaders can opt-in to caching with their own cache() config
|
|
519
|
-
|
|
569
|
+
// Loaders can opt-in to caching with their own cache() config.
|
|
570
|
+
// Segments above the boundary are live and never stored.
|
|
571
|
+
const nonLoaderSegments = segments.filter(
|
|
572
|
+
(s) => s.type !== "loader" && this.covers(s.id, s.namespace),
|
|
573
|
+
);
|
|
520
574
|
if (nonLoaderSegments.length === 0) return;
|
|
521
575
|
|
|
522
576
|
const ttl = this.ttl;
|
|
@@ -585,11 +639,8 @@ export class CacheScope {
|
|
|
585
639
|
}
|
|
586
640
|
|
|
587
641
|
// Collect handle data for non-loader segments only
|
|
588
|
-
const handles = captureHandles(
|
|
589
|
-
|
|
590
|
-
handleStore,
|
|
591
|
-
requestCtx._shellCaptureLoaderHandleValues,
|
|
592
|
-
);
|
|
642
|
+
const handles = captureHandles(nonLoaderSegments, handleStore);
|
|
643
|
+
const handleOwners = captureHandleOwners(nonLoaderSegments, handleStore);
|
|
593
644
|
|
|
594
645
|
try {
|
|
595
646
|
if (INTERNAL_RANGO_DEBUG) {
|
|
@@ -602,10 +653,18 @@ export class CacheScope {
|
|
|
602
653
|
// through the codec (not raw into the entry) so Promise/ReactNode handle
|
|
603
654
|
// values survive a JSON-serializing store — see encodeHandles.
|
|
604
655
|
const { serializeSegments } = await import("./segment-codec.js");
|
|
656
|
+
const flightErrors: unknown[] = [];
|
|
657
|
+
const onFlightError = (error: unknown): void => {
|
|
658
|
+
flightErrors.push(error);
|
|
659
|
+
};
|
|
605
660
|
const [serializedSegments, encodedHandles] = await Promise.all([
|
|
606
|
-
serializeSegments(nonLoaderSegments),
|
|
607
|
-
encodeHandles(handles),
|
|
661
|
+
serializeSegments(nonLoaderSegments, onFlightError),
|
|
662
|
+
encodeHandles(handles, onFlightError),
|
|
608
663
|
]);
|
|
664
|
+
// Flight encodes a component that throws (an async server component in
|
|
665
|
+
// the tree) or a rejected handle value as an error row and completes
|
|
666
|
+
// normally; stored, every HIT would render that error until expiry.
|
|
667
|
+
if (flightErrors.length > 0) throw flightErrors[0];
|
|
609
668
|
|
|
610
669
|
const data: CachedEntryData = {
|
|
611
670
|
segments: serializedSegments,
|
|
@@ -613,6 +672,7 @@ export class CacheScope {
|
|
|
613
672
|
expiresAt: Date.now() + ttl * 1000,
|
|
614
673
|
tags,
|
|
615
674
|
};
|
|
675
|
+
if (handleOwners && encodedHandles) data.handleOwners = handleOwners;
|
|
616
676
|
|
|
617
677
|
if (INTERNAL_RANGO_DEBUG) {
|
|
618
678
|
debugCacheLog(`[CacheScope] waitUntil: calling store.set for ${key}`);
|
|
@@ -640,14 +700,18 @@ export class CacheScope {
|
|
|
640
700
|
}
|
|
641
701
|
|
|
642
702
|
/**
|
|
643
|
-
* Create a cache scope from entry's cache config
|
|
703
|
+
* Create a cache scope from entry's cache config. `shortCode` is the cache()
|
|
704
|
+
* entry's: a scope nested in an enabled parent keeps the parent's boundary
|
|
705
|
+
* (one entry covers both), any other opens its own at this entry.
|
|
644
706
|
*/
|
|
645
707
|
export function createCacheScope(
|
|
646
708
|
config: { options: PartialCacheOptions | false } | undefined,
|
|
647
709
|
parent: CacheScope | null = null,
|
|
710
|
+
shortCode?: string,
|
|
648
711
|
): CacheScope | null {
|
|
649
712
|
if (!config) return parent; // No config, inherit parent
|
|
650
|
-
|
|
713
|
+
const boundary = parent?.enabled ? parent.boundary : shortCode;
|
|
714
|
+
return new CacheScope(config.options, parent, undefined, boundary);
|
|
651
715
|
}
|
|
652
716
|
|
|
653
717
|
type ShellImplicitCacheMarker = NonNullable<
|
|
@@ -129,6 +129,19 @@ function shouldCacheResponse(response: Response): CacheDirectives | null {
|
|
|
129
129
|
return parseCacheControl(cacheControl);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Throw the first error the drained render reported through Flight or Fizz
|
|
134
|
+
* onError. A component that throws after the 200 committed does not fail the
|
|
135
|
+
* stream: it completes with an error row or an errored Suspense boundary, and
|
|
136
|
+
* every hit would serve it (issue #915). The caller's catch reports the skipped
|
|
137
|
+
* write as cache-write. Read only after the body is fully drained, when both
|
|
138
|
+
* renders have finished.
|
|
139
|
+
*/
|
|
140
|
+
function throwIfRenderErrored(requestCtx: RequestContext | undefined): void {
|
|
141
|
+
const errors = requestCtx?._renderErrors;
|
|
142
|
+
if (errors && errors.length > 0) throw errors[0];
|
|
143
|
+
}
|
|
144
|
+
|
|
132
145
|
// ============================================================================
|
|
133
146
|
// Response Helpers
|
|
134
147
|
// ============================================================================
|
|
@@ -404,6 +417,7 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
404
417
|
// drain the fresh render fully before snapshotting tags
|
|
405
418
|
// (same render-complete barrier as the miss path).
|
|
406
419
|
const body = await new Response(fresh.body).arrayBuffer();
|
|
420
|
+
throwIfRenderErrored(requestCtx);
|
|
407
421
|
await store.putResponse!(
|
|
408
422
|
cacheKey,
|
|
409
423
|
new Response(body, fresh),
|
|
@@ -468,6 +482,7 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
468
482
|
// unaffected) is the render-complete barrier that keeps the cached
|
|
469
483
|
// body and its tag set consistent.
|
|
470
484
|
const body = await new Response(cacheStream).arrayBuffer();
|
|
485
|
+
throwIfRenderErrored(requestCtx);
|
|
471
486
|
await store.putResponse!(
|
|
472
487
|
cacheKey,
|
|
473
488
|
new Response(body, originalResponse),
|
|
@@ -6,10 +6,82 @@
|
|
|
6
6
|
* pulling in @vitejs/plugin-rsc/rsc dependencies.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { getCurrentLoaderBodyId } from "../server/context.js";
|
|
9
10
|
import type { HandleStore } from "../server/handle-store.js";
|
|
10
11
|
import type { SegmentHandleData } from "./types.js";
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Scoping for one capture of a cached unit (a loader's own cache(),
|
|
15
|
+
* loader-cache.ts; a "use cache" function, cache-runtime.ts) that runs
|
|
16
|
+
* concurrently with live pushes into the same store.
|
|
17
|
+
*/
|
|
18
|
+
export interface HandleCaptureOptions {
|
|
19
|
+
/** Push-time predicate; a rejected push is not recorded by this capture. */
|
|
20
|
+
accept?: () => boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Recorded pushes do not reach the store. A stale hit's background
|
|
23
|
+
* revalidation: the foreground already replayed the entry's pushes, so the
|
|
24
|
+
* fresh body's pushes belong only to the refreshed entry.
|
|
25
|
+
*/
|
|
26
|
+
divert?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Record key for an accepted push, in place of its segment id. A loader's
|
|
29
|
+
* own cache() (loader-cache.ts recordOwnerKey) and "use cache"
|
|
30
|
+
* (useCacheRecordKey) group pushes by the loader body that made them.
|
|
31
|
+
*/
|
|
32
|
+
key?: () => string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The loader whose recorded push a "use cache" HIT is replaying
|
|
36
|
+
// (replayLoaderPush), for an enclosing "use cache" MISS's record key.
|
|
37
|
+
let replayingOwner: string | undefined;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* HandleStore.pushReplayed, attributed to `loaderId` for the record key of an
|
|
41
|
+
* enclosing "use cache" MISS: an outer cached function records an inner HIT's
|
|
42
|
+
* replayed loader pushes under that loader, not as its own.
|
|
43
|
+
*/
|
|
44
|
+
export function replayLoaderPush(
|
|
45
|
+
handleStore: HandleStore,
|
|
46
|
+
handleName: string,
|
|
47
|
+
segmentId: string,
|
|
48
|
+
value: unknown,
|
|
49
|
+
loaderId: string,
|
|
50
|
+
): void {
|
|
51
|
+
const prev = replayingOwner;
|
|
52
|
+
replayingOwner = loaderId;
|
|
53
|
+
try {
|
|
54
|
+
handleStore.pushReplayed(handleName, segmentId, value, loaderId);
|
|
55
|
+
} finally {
|
|
56
|
+
replayingOwner = prev;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Record key for a "use cache" capture: run-length groups `${seq}:${owner}`,
|
|
62
|
+
* so the HIT (handle-snapshot.ts appendHandles) delivers each loader's pushes
|
|
63
|
+
* at most once per request. The owner is the loader whose body, entered
|
|
64
|
+
* inside this execution via ctx.use, made the push; empty for the function's
|
|
65
|
+
* own pushes and those of cached functions it calls. Groups keep push order
|
|
66
|
+
* across owners. Create at call time: the caller's loader body is the
|
|
67
|
+
* function's own scope.
|
|
68
|
+
*/
|
|
69
|
+
export function useCacheRecordKey(): () => string {
|
|
70
|
+
const callerBody = getCurrentLoaderBodyId();
|
|
71
|
+
let seq = 0;
|
|
72
|
+
let last: string | undefined;
|
|
73
|
+
return () => {
|
|
74
|
+
const body = replayingOwner ?? getCurrentLoaderBodyId();
|
|
75
|
+
const owner = body === callerBody ? "" : (body ?? "");
|
|
76
|
+
if (owner !== last) {
|
|
77
|
+
last = owner;
|
|
78
|
+
seq++;
|
|
79
|
+
}
|
|
80
|
+
return `${seq}:${owner}`;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface HandleCapture extends HandleCaptureOptions {
|
|
13
85
|
data: Record<string, SegmentHandleData>;
|
|
14
86
|
}
|
|
15
87
|
|
|
@@ -42,33 +114,41 @@ function ensureInterceptorInstalled(handleStore: HandleStore): void {
|
|
|
42
114
|
segmentId: string,
|
|
43
115
|
value: unknown,
|
|
44
116
|
) => {
|
|
117
|
+
let diverted = false;
|
|
45
118
|
for (const capture of captures) {
|
|
46
|
-
if (!capture.
|
|
47
|
-
|
|
119
|
+
if (capture.accept && !capture.accept()) continue;
|
|
120
|
+
if (capture.divert) diverted = true;
|
|
121
|
+
const key = capture.key ? capture.key() : segmentId;
|
|
122
|
+
if (!capture.data[key]) {
|
|
123
|
+
capture.data[key] = {};
|
|
48
124
|
}
|
|
49
|
-
if (!capture.data[
|
|
50
|
-
capture.data[
|
|
125
|
+
if (!capture.data[key][handleName]) {
|
|
126
|
+
capture.data[key][handleName] = [];
|
|
51
127
|
}
|
|
52
|
-
capture.data[
|
|
128
|
+
capture.data[key][handleName].push(value);
|
|
53
129
|
}
|
|
54
|
-
originalPush(handleName, segmentId, value);
|
|
130
|
+
if (!diverted) originalPush(handleName, segmentId, value);
|
|
55
131
|
};
|
|
56
132
|
}
|
|
57
133
|
|
|
58
134
|
/**
|
|
59
|
-
* Start capturing handle pushes for a cached function
|
|
135
|
+
* Start capturing handle pushes for a cached function or cached loader
|
|
136
|
+
* execution (`options`: see HandleCaptureOptions).
|
|
60
137
|
*
|
|
61
138
|
* Concurrency-safe: multiple overlapping captures on the same
|
|
62
139
|
* HandleStore are independent. Each capture registers a token in a
|
|
63
140
|
* Set; stopping removes it. No ordering requirement (LIFO not needed).
|
|
64
141
|
*/
|
|
65
|
-
export function startHandleCapture(
|
|
142
|
+
export function startHandleCapture(
|
|
143
|
+
handleStore: HandleStore,
|
|
144
|
+
options?: HandleCaptureOptions,
|
|
145
|
+
): {
|
|
66
146
|
capture: HandleCapture;
|
|
67
147
|
stop: () => void;
|
|
68
148
|
} {
|
|
69
149
|
ensureInterceptorInstalled(handleStore);
|
|
70
150
|
|
|
71
|
-
const capture: HandleCapture = { data: {} };
|
|
151
|
+
const capture: HandleCapture = { data: {}, ...options };
|
|
72
152
|
const captures = activeCapturesMap.get(handleStore)!;
|
|
73
153
|
captures.add(capture);
|
|
74
154
|
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* during segment resolution and stored alongside cached segments.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { replayLoaderPush } from "./handle-capture.js";
|
|
9
10
|
import type { ResolvedSegment } from "../types.js";
|
|
10
11
|
import type { HandleStore } from "../server/handle-store.js";
|
|
11
|
-
import type { SegmentHandleData } from "./types.js";
|
|
12
|
+
import type { HandleOwners, SegmentHandleData } from "./types.js";
|
|
12
13
|
// segment-codec eagerly pulls @vitejs/plugin-rsc (a virtual: module unresolvable
|
|
13
14
|
// in plain node/vitest). It is imported LAZILY inside the two async encode/decode
|
|
14
15
|
// helpers below so that modules which import handle-snapshot only for the
|
|
@@ -47,19 +48,32 @@ function withTimeout<T>(p: Promise<T>, ms: number, onTimeout: T): Promise<T> {
|
|
|
47
48
|
]);
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
/**
|
|
52
|
+
* `onError` reaches the Flight encode (see serializeResult). A cache writer
|
|
53
|
+
* passes the collector it gives the value's encode, so a handle value that
|
|
54
|
+
* fails to encode (a rejected pushed promise) refuses the whole entry like a
|
|
55
|
+
* failed value does; prerender (prerender-match.ts) passes one too and
|
|
56
|
+
* hands the error to its prerender.onError policy.
|
|
57
|
+
*/
|
|
58
|
+
export async function encodeHandles(
|
|
59
|
+
handles: HandleRecord,
|
|
60
|
+
onError?: (error: unknown) => void,
|
|
61
|
+
): Promise<string> {
|
|
51
62
|
if (!hasHandleData(handles)) return "";
|
|
52
|
-
return encodeHandleValue(handles);
|
|
63
|
+
return encodeHandleValue(handles, onError);
|
|
53
64
|
}
|
|
54
65
|
|
|
55
66
|
export function decodeHandles(encoded: string): Promise<HandleRecord | null> {
|
|
56
67
|
return decodeHandleValue<HandleRecord>(encoded);
|
|
57
68
|
}
|
|
58
69
|
|
|
59
|
-
export async function encodeHandleValue(
|
|
70
|
+
export async function encodeHandleValue(
|
|
71
|
+
value: unknown,
|
|
72
|
+
onError?: (error: unknown) => void,
|
|
73
|
+
): Promise<string> {
|
|
60
74
|
const { serializeResult } = await import("./segment-codec.js");
|
|
61
75
|
const encoded = await withTimeout(
|
|
62
|
-
serializeResult(value),
|
|
76
|
+
serializeResult(value, onError),
|
|
63
77
|
HANDLE_ENCODE_TIMEOUT_MS,
|
|
64
78
|
null,
|
|
65
79
|
);
|
|
@@ -84,49 +98,139 @@ export async function decodeHandleValue<T>(encoded: string): Promise<T | null> {
|
|
|
84
98
|
* Capture handle data for a set of segments from the handle store.
|
|
85
99
|
* Used when caching segments to preserve their handle data.
|
|
86
100
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
101
|
+
* DSL-loader pushes stay out of the CACHE WRITE: a HIT runs loaders exactly
|
|
102
|
+
* as an uncached render of the same request would (cache-lookup.ts
|
|
103
|
+
* resolveFreshLoadersAndYield), so replaying a recorded copy would duplicate
|
|
104
|
+
* the live push. Handler pushes, including
|
|
105
|
+
* handler-invoked ctx.use(Loader) bodies (skipped with their handler on a
|
|
106
|
+
* HIT), are kept. The store tags loader pushes by array position at push
|
|
107
|
+
* time, so primitive values are covered. A PPR shell capture untags the
|
|
108
|
+
* settled bake-lane pushes its record keeps (shell-capture.ts) and gives
|
|
109
|
+
* them an owner (captureHandleOwners). Only this call site passes
|
|
110
|
+
* excludeLoaderPushes; every other getDataForSegment consumer (the
|
|
111
|
+
* render-barrier snapshot, prerender) sees every push.
|
|
94
112
|
*/
|
|
95
113
|
export function captureHandles(
|
|
96
114
|
segments: ResolvedSegment[],
|
|
97
115
|
handleStore: HandleStore,
|
|
98
|
-
exclude?: WeakSet<object>,
|
|
99
116
|
): Record<string, SegmentHandleData> {
|
|
100
117
|
const handles: Record<string, SegmentHandleData> = {};
|
|
101
118
|
for (const seg of segments) {
|
|
102
|
-
|
|
103
|
-
if (!exclude) {
|
|
104
|
-
handles[seg.id] = data;
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
const filtered: SegmentHandleData = {};
|
|
108
|
-
for (const [handleName, values] of Object.entries(data)) {
|
|
109
|
-
const kept = values.filter(
|
|
110
|
-
(v) => typeof v !== "object" || v === null || !exclude.has(v),
|
|
111
|
-
);
|
|
112
|
-
if (kept.length > 0) filtered[handleName] = kept;
|
|
113
|
-
}
|
|
114
|
-
handles[seg.id] = filtered;
|
|
119
|
+
handles[seg.id] = handleStore.getDataForSegment(seg.id, true);
|
|
115
120
|
}
|
|
116
121
|
return handles;
|
|
117
122
|
}
|
|
118
123
|
|
|
124
|
+
/** The owners of captureHandles' values; undefined when none is owned. */
|
|
125
|
+
export function captureHandleOwners(
|
|
126
|
+
segments: ResolvedSegment[],
|
|
127
|
+
handleStore: HandleStore,
|
|
128
|
+
): HandleOwners | undefined {
|
|
129
|
+
let owners: HandleOwners | undefined;
|
|
130
|
+
for (const seg of segments) {
|
|
131
|
+
const segOwners = handleStore.getRecordOwners(seg.id);
|
|
132
|
+
if (segOwners) (owners ??= {})[seg.id] = segOwners;
|
|
133
|
+
}
|
|
134
|
+
return owners;
|
|
135
|
+
}
|
|
136
|
+
|
|
119
137
|
/**
|
|
120
138
|
* Restore handle data from a cached snapshot into the handle store.
|
|
121
|
-
* Used when serving cached segments to replay their handle data
|
|
139
|
+
* Used when serving cached segments to replay their handle data: a route
|
|
140
|
+
* cache() record owns its segments' arrays, so it REPLACES them.
|
|
141
|
+
*
|
|
142
|
+
* A segment with `owners` is replaced with empty arrays and re-pushed in
|
|
143
|
+
* recorded order, an owned value through pushReplayed: the owning loader
|
|
144
|
+
* re-runs on the HIT (loaders stay live), and its live pushes replace the
|
|
145
|
+
* recorded ones in place instead of appending a second copy. `claim`
|
|
146
|
+
* (setupLoaderAccess _claimLoaderPushes) is asked once per owner, so the
|
|
147
|
+
* loader's own cache() HIT does not replay its pushes a second time
|
|
148
|
+
* (loader-cache.ts replayLoaderHandles); an owner it refuses is skipped. A
|
|
149
|
+
* record without owners restores as a plain replay.
|
|
122
150
|
*/
|
|
123
151
|
export function restoreHandles(
|
|
124
152
|
handles: Record<string, SegmentHandleData>,
|
|
125
153
|
handleStore: HandleStore,
|
|
154
|
+
owners?: HandleOwners,
|
|
155
|
+
claim?: (loaderId: string) => boolean,
|
|
126
156
|
): void {
|
|
157
|
+
let delivers: Map<string, boolean> | undefined;
|
|
127
158
|
for (const [segId, segHandles] of Object.entries(handles)) {
|
|
128
|
-
if (Object.keys(segHandles).length
|
|
159
|
+
if (Object.keys(segHandles).length === 0) continue;
|
|
160
|
+
const segOwners = owners?.[segId];
|
|
161
|
+
if (!segOwners) {
|
|
129
162
|
handleStore.replaySegmentData(segId, segHandles);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const cleared: SegmentHandleData = {};
|
|
166
|
+
for (const handleName in segHandles) cleared[handleName] = [];
|
|
167
|
+
handleStore.replaySegmentData(segId, cleared);
|
|
168
|
+
for (const [handleName, values] of Object.entries(segHandles)) {
|
|
169
|
+
const valueOwners = segOwners[handleName];
|
|
170
|
+
for (let i = 0; i < values.length; i++) {
|
|
171
|
+
const owner = valueOwners?.[i];
|
|
172
|
+
if (!owner) {
|
|
173
|
+
handleStore.push(handleName, segId, values[i]);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
delivers ??= new Map();
|
|
177
|
+
let deliver = delivers.get(owner);
|
|
178
|
+
if (deliver === undefined) {
|
|
179
|
+
deliver = claim ? claim(owner) : true;
|
|
180
|
+
delivers.set(owner, deliver);
|
|
181
|
+
}
|
|
182
|
+
if (deliver) {
|
|
183
|
+
handleStore.pushReplayed(handleName, segId, values[i], owner);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Append recorded handle pushes to the store, in recorded order, via push()
|
|
192
|
+
* so active captures and loader-scope tagging see them like live pushes. For
|
|
193
|
+
* a cached unit that shares its segments with live pushes ("use cache");
|
|
194
|
+
* restoreHandles would wipe those. `segmentId` redirects every value to one
|
|
195
|
+
* segment (the caller's).
|
|
196
|
+
*
|
|
197
|
+
* Owner-keyed groups (useCacheRecordKey, `${seq}:${loaderId}`) are a loader's
|
|
198
|
+
* pushes, which reach the page once per request: `claim` (setupLoaderAccess
|
|
199
|
+
* _claimLoaderPushes) skips a loader that already ran or was replayed in this
|
|
200
|
+
* request, and a claimed loader that runs later replaces its replayed values
|
|
201
|
+
* (HandleStore.pushReplayed). `${seq}:` groups are the function's own pushes.
|
|
202
|
+
* A key without ":" is a segment id (records written before owner keys) and
|
|
203
|
+
* replays unchanged. A loader's own cache() replays through loader-cache.ts
|
|
204
|
+
* replayLoaderHandles.
|
|
205
|
+
*/
|
|
206
|
+
export function appendHandles(
|
|
207
|
+
handles: Record<string, SegmentHandleData>,
|
|
208
|
+
handleStore: HandleStore,
|
|
209
|
+
segmentId?: string,
|
|
210
|
+
claim?: (loaderId: string) => boolean,
|
|
211
|
+
): void {
|
|
212
|
+
let claims: Map<string, boolean> | undefined;
|
|
213
|
+
for (const [key, segHandles] of Object.entries(handles)) {
|
|
214
|
+
const colon = key.indexOf(":");
|
|
215
|
+
const owner = colon < 0 ? "" : key.slice(colon + 1);
|
|
216
|
+
if (owner) {
|
|
217
|
+
claims ??= new Map();
|
|
218
|
+
let deliver = claims.get(owner);
|
|
219
|
+
if (deliver === undefined) {
|
|
220
|
+
deliver = claim ? claim(owner) : true;
|
|
221
|
+
claims.set(owner, deliver);
|
|
222
|
+
}
|
|
223
|
+
if (!deliver) continue;
|
|
224
|
+
}
|
|
225
|
+
const target = segmentId ?? key;
|
|
226
|
+
for (const [handleName, values] of Object.entries(segHandles)) {
|
|
227
|
+
for (const value of values) {
|
|
228
|
+
if (owner) {
|
|
229
|
+
replayLoaderPush(handleStore, handleName, target, value, owner);
|
|
230
|
+
} else {
|
|
231
|
+
handleStore.push(handleName, target, value);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
130
234
|
}
|
|
131
235
|
}
|
|
132
236
|
}
|
|
@@ -69,11 +69,15 @@ export function stringToStream(str: string): ReadableStream<Uint8Array> {
|
|
|
69
69
|
*/
|
|
70
70
|
export async function rscSerialize(
|
|
71
71
|
value: unknown,
|
|
72
|
+
onError?: (error: unknown) => void,
|
|
72
73
|
): Promise<string | undefined> {
|
|
73
74
|
if (value === undefined || value === null) return undefined;
|
|
74
75
|
|
|
75
76
|
const temporaryReferences = createTemporaryReferenceSet();
|
|
76
|
-
const stream = renderToReadableStream(value, {
|
|
77
|
+
const stream = renderToReadableStream(value, {
|
|
78
|
+
temporaryReferences,
|
|
79
|
+
onError,
|
|
80
|
+
});
|
|
77
81
|
return streamToString(stream);
|
|
78
82
|
}
|
|
79
83
|
|
|
@@ -102,10 +106,16 @@ export async function rscDeserialize<T>(
|
|
|
102
106
|
*
|
|
103
107
|
* Returns null only on serialization failure.
|
|
104
108
|
*/
|
|
105
|
-
export async function serializeResult(
|
|
109
|
+
export async function serializeResult(
|
|
110
|
+
value: unknown,
|
|
111
|
+
onError?: (error: unknown) => void,
|
|
112
|
+
): Promise<string | null> {
|
|
106
113
|
try {
|
|
107
114
|
const temporaryReferences = createTemporaryReferenceSet();
|
|
108
|
-
const stream = renderToReadableStream(value, {
|
|
115
|
+
const stream = renderToReadableStream(value, {
|
|
116
|
+
temporaryReferences,
|
|
117
|
+
onError,
|
|
118
|
+
});
|
|
109
119
|
return await streamToString(stream);
|
|
110
120
|
} catch (error) {
|
|
111
121
|
// Returning null silently turns a non-serializable cache value into a
|
|
@@ -145,9 +155,17 @@ export const deserializeComponent: (encoded: string) => Promise<unknown> =
|
|
|
145
155
|
* Serialize segments for storage.
|
|
146
156
|
* Each segment's component, layout, loading, and loaderData are RSC-serialized.
|
|
147
157
|
* Metadata is preserved as-is.
|
|
158
|
+
*
|
|
159
|
+
* `onError` goes to every Flight encode. A component that throws while
|
|
160
|
+
* encoding (e.g. an async server component in the tree) does not reject the
|
|
161
|
+
* encode: Flight reports it through onError and writes an error row
|
|
162
|
+
* (`1:E{...}`) that throws when the decoded tree renders. cacheRoute and the
|
|
163
|
+
* prerender producers (prerender-match.ts) pass it to refuse such an entry;
|
|
164
|
+
* without it the encode is unchanged.
|
|
148
165
|
*/
|
|
149
166
|
export async function serializeSegments(
|
|
150
167
|
segments: ResolvedSegment[],
|
|
168
|
+
onError?: (error: unknown) => void,
|
|
151
169
|
): Promise<SerializedSegmentData[]> {
|
|
152
170
|
return Promise.all(
|
|
153
171
|
segments.map(async (segment): Promise<SerializedSegmentData> => {
|
|
@@ -162,6 +180,7 @@ export async function serializeSegments(
|
|
|
162
180
|
// Serialize the component to RSC stream
|
|
163
181
|
const stream = renderToReadableStream(componentResolved, {
|
|
164
182
|
temporaryReferences,
|
|
183
|
+
onError,
|
|
165
184
|
});
|
|
166
185
|
|
|
167
186
|
// RSC-serialize loading: "null" string distinguishes explicit null from undefined
|
|
@@ -169,7 +188,7 @@ export async function serializeSegments(
|
|
|
169
188
|
segment.loading !== undefined
|
|
170
189
|
? segment.loading === null
|
|
171
190
|
? "null"
|
|
172
|
-
: await rscSerialize(segment.loading)
|
|
191
|
+
: await rscSerialize(segment.loading, onError)
|
|
173
192
|
: undefined;
|
|
174
193
|
|
|
175
194
|
// Await loaderData / loaderDataPromise if they're Promises
|
|
@@ -190,9 +209,9 @@ export async function serializeSegments(
|
|
|
190
209
|
encodedLoaderDataPromise,
|
|
191
210
|
] = await Promise.all([
|
|
192
211
|
streamToString(stream),
|
|
193
|
-
segment.layout ? rscSerialize(segment.layout) : undefined,
|
|
194
|
-
rscSerialize(loaderDataResolved),
|
|
195
|
-
rscSerialize(loaderDataPromiseResolved),
|
|
212
|
+
segment.layout ? rscSerialize(segment.layout, onError) : undefined,
|
|
213
|
+
rscSerialize(loaderDataResolved, onError),
|
|
214
|
+
rscSerialize(loaderDataPromiseResolved, onError),
|
|
196
215
|
]);
|
|
197
216
|
|
|
198
217
|
return {
|
|
@@ -68,8 +68,8 @@ function warnNoTagStore(fn: string, tags: string[]): void {
|
|
|
68
68
|
console.warn(
|
|
69
69
|
`[${fn}] No tag-capable cache store is configured; tags ` +
|
|
70
70
|
`[${tags.join(", ")}] were not invalidated. The configured store must ` +
|
|
71
|
-
`implement invalidateTags() (the built-in MemorySegmentCacheStore
|
|
72
|
-
`CFCacheStore do).`,
|
|
71
|
+
`implement invalidateTags() (the built-in MemorySegmentCacheStore, ` +
|
|
72
|
+
`CFCacheStore, and VercelCacheStore do).`,
|
|
73
73
|
);
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -98,7 +98,8 @@ function warnPartialTagStore(fn: string, incapable: number): void {
|
|
|
98
98
|
console.warn(
|
|
99
99
|
`[${fn}] ${incapable} configured cache store(s) do not implement ` +
|
|
100
100
|
`invalidateTags(); their tagged entries were NOT invalidated. Use a ` +
|
|
101
|
-
`tag-capable store (
|
|
101
|
+
`tag-capable store (MemorySegmentCacheStore / CFCacheStore / ` +
|
|
102
|
+
`VercelCacheStore) for any ` +
|
|
102
103
|
`cache({ store }) boundary whose entries you invalidate by tag.`,
|
|
103
104
|
);
|
|
104
105
|
}
|