@rangojs/router 0.31.0 → 0.33.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/bin/browser-7KIJ5HSV.js +15 -0
- package/dist/bin/chunk-3B7CKBRO.js +463 -0
- package/dist/bin/chunk-524VACCG.js +71 -0
- package/dist/bin/chunk-LD7UBHT4.js +196 -0
- package/dist/bin/chunk-TO6CRWLN.js +1429 -0
- package/dist/bin/chunk-TTXC2U3G.js +2477 -0
- package/dist/bin/client-urls-projection-DQLWPDAT.js +28 -0
- package/dist/bin/forward-user-plugins-URBZKJP4.js +62 -0
- package/dist/bin/rango.js +42 -10143
- package/dist/bin/runtime-EPUDEHO4.js +596 -0
- package/dist/bin/runtime-discovery-CZ2QHT4Q.js +352 -0
- package/dist/bin/state-ACD2MGKJ.js +16 -0
- package/dist/bin/temp-rsc-server-QRR6HD6T.js +3190 -0
- package/dist/types/bin/rango.d.ts +1 -0
- package/dist/types/browser/action-fence.d.ts +20 -0
- package/dist/types/browser/cookie-name.d.ts +51 -10
- package/dist/types/browser/history-state.d.ts +13 -2
- package/dist/types/browser/invalidate-client-cache.d.ts +8 -6
- package/dist/types/browser/navigation-bridge.d.ts +36 -1
- package/dist/types/browser/navigation-client.d.ts +19 -0
- package/dist/types/browser/navigation-store-handle.d.ts +37 -4
- package/dist/types/browser/navigation-store.d.ts +2 -2
- package/dist/types/browser/navigation-transaction.d.ts +0 -1
- package/dist/types/browser/partial-update.d.ts +13 -0
- package/dist/types/browser/prefetch/cache.d.ts +96 -8
- package/dist/types/browser/prefetch/fetch.d.ts +26 -2
- package/dist/types/browser/prefetch/queue.d.ts +3 -2
- package/dist/types/browser/rango-state.d.ts +34 -5
- package/dist/types/browser/react/Link.d.ts +19 -13
- package/dist/types/browser/react/location-state-shared.d.ts +2 -1
- package/dist/types/browser/types.d.ts +25 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/include-resolution.d.ts +9 -1
- package/dist/types/build/route-types/router-processing.d.ts +3 -1
- package/dist/types/cache/cache-key-utils.d.ts +12 -8
- package/dist/types/cache/cache-scope.d.ts +4 -2
- package/dist/types/cache/cf/cf-cache-types.d.ts +9 -5
- package/dist/types/cache/document-cache-policy.d.ts +9 -1
- package/dist/types/cache/memory-segment-store.d.ts +3 -2
- package/dist/types/cache/segment-codec.d.ts +2 -2
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/types.d.ts +12 -7
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -1
- package/dist/types/client-urls/types.d.ts +7 -5
- package/dist/types/prefetch-markers.d.ts +9 -0
- package/dist/types/prefetch-source-scope.d.ts +66 -0
- package/dist/types/route-definition/helpers-types.d.ts +8 -5
- package/dist/types/route-definition/redirect.d.ts +5 -0
- package/dist/types/router/intercept-resolution.d.ts +25 -11
- package/dist/types/router/match-context.d.ts +1 -0
- package/dist/types/router/match-handlers.d.ts +1 -1
- package/dist/types/router/middleware-types.d.ts +6 -0
- package/dist/types/router/prefetch-cache-ttl.d.ts +11 -3
- package/dist/types/router/router-context.d.ts +1 -1
- package/dist/types/router/router-options.d.ts +6 -3
- package/dist/types/router/segment-resolution/loader-cache.d.ts +8 -0
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +23 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +19 -0
- package/dist/types/router/source-reads.d.ts +53 -0
- package/dist/types/router/types.d.ts +1 -1
- package/dist/types/rsc/helpers.d.ts +8 -3
- package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
- package/dist/types/rsc/request-stages.d.ts +4 -0
- package/dist/types/rsc/shell-serve.d.ts +6 -1
- package/dist/types/server/context.d.ts +23 -6
- package/dist/types/server/cookie-store.d.ts +15 -1
- package/dist/types/server/document-location-state.d.ts +59 -0
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +65 -12
- package/dist/types/testing/e2e/server.d.ts +16 -1
- package/dist/types/testing/render-route.d.ts +83 -2
- package/dist/types/testing/serve-shell-request.d.ts +15 -3
- package/dist/types/types/handler-context.d.ts +21 -4
- package/dist/types/types/loader-types.d.ts +7 -4
- package/dist/types/types/segments.d.ts +18 -1
- package/dist/types/urls/path-helper-types.d.ts +7 -5
- package/dist/types/vite/temp-rsc-server.d.ts +2 -2
- package/dist/types/vite/utils/forward-user-plugins.d.ts +32 -2
- package/dist/vite/index.js +451 -191
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +74 -0
- package/skills/app-structure/e-commerce.md +10 -9
- package/skills/app-structure/large-app.md +15 -2
- package/skills/caching/SKILL.md +10 -5
- package/skills/client-urls/SKILL.md +18 -11
- package/skills/comparison/references/framework-comparison.md +16 -10
- package/skills/document-cache/SKILL.md +13 -0
- package/skills/hooks/navigation.md +8 -8
- package/skills/hooks/state.md +132 -12
- package/skills/intercept/SKILL.md +33 -11
- package/skills/layout/SKILL.md +11 -0
- package/skills/links/SKILL.md +101 -4
- package/skills/loader/SKILL.md +49 -22
- package/skills/migrate-nextjs/SKILL.md +8 -2
- package/skills/migrate-react-router/data-and-actions.md +5 -0
- package/skills/ppr/SKILL.md +38 -5
- package/skills/prerender/SKILL.md +4 -1
- package/skills/route/SKILL.md +6 -6
- package/skills/router-setup/SKILL.md +2 -1
- package/skills/server-actions/SKILL.md +49 -8
- package/skills/testing/client-components.md +73 -32
- package/skills/testing/serve-shell-request.md +45 -20
- package/skills/testing/server-actions.md +8 -0
- package/skills/ux-architecture/SKILL.md +67 -15
- package/src/bin/rango.ts +39 -13
- package/src/browser/action-fence.ts +72 -0
- package/src/browser/cookie-name.ts +82 -14
- package/src/browser/history-state.ts +24 -2
- package/src/browser/invalidate-client-cache.ts +9 -7
- package/src/browser/navigation-bridge.ts +164 -32
- package/src/browser/navigation-client.ts +95 -20
- package/src/browser/navigation-store-handle.ts +67 -5
- package/src/browser/navigation-store.ts +86 -17
- package/src/browser/navigation-transaction.ts +0 -6
- package/src/browser/partial-update.ts +31 -12
- package/src/browser/prefetch/cache.ts +188 -11
- package/src/browser/prefetch/fetch.ts +211 -51
- package/src/browser/prefetch/queue.ts +3 -2
- package/src/browser/rango-state.ts +50 -8
- package/src/browser/react/Link.tsx +24 -16
- package/src/browser/react/location-state-shared.ts +8 -1
- package/src/browser/react/use-navigation.ts +1 -1
- package/src/browser/rsc-router.tsx +13 -3
- package/src/browser/server-action-bridge.ts +3 -3
- package/src/browser/types.ts +25 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/include-resolution.ts +40 -3
- package/src/build/route-types/router-processing.ts +170 -25
- package/src/build/route-types/scan-filter.ts +1 -0
- package/src/build/runtime-discovery.ts +46 -8
- package/src/cache/cache-key-utils.ts +12 -8
- package/src/cache/cache-runtime.ts +5 -1
- package/src/cache/cache-scope.ts +28 -7
- package/src/cache/cf/cf-cache-types.ts +9 -5
- package/src/cache/document-cache-policy.ts +37 -1
- package/src/cache/document-cache.ts +12 -0
- package/src/cache/memory-segment-store.ts +3 -2
- package/src/cache/segment-codec.ts +2 -2
- package/src/cache/shell-snapshot.ts +1 -1
- package/src/cache/types.ts +12 -7
- package/src/cache/vercel/vercel-cache-store.ts +2 -1
- package/src/client-urls/types.ts +7 -5
- package/src/prefetch-markers.ts +10 -0
- package/src/prefetch-source-scope.ts +137 -0
- package/src/route-definition/helpers-types.ts +8 -5
- package/src/route-definition/redirect.ts +5 -0
- package/src/router/intercept-resolution.ts +94 -50
- package/src/router/loader-resolution.ts +8 -6
- package/src/router/match-api.ts +33 -29
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +2 -1
- package/src/router/match-middleware/cache-lookup.ts +39 -14
- package/src/router/match-result.ts +45 -85
- package/src/router/middleware-types.ts +6 -0
- package/src/router/navigation-snapshot.ts +18 -3
- package/src/router/prefetch-cache-ttl.ts +15 -3
- package/src/router/revalidation.ts +63 -13
- package/src/router/router-context.ts +2 -1
- package/src/router/router-options.ts +6 -3
- package/src/router/segment-resolution/loader-cache.ts +21 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +38 -31
- package/src/router/segment-resolution/revalidation.ts +40 -12
- package/src/router/source-reads.ts +122 -0
- package/src/router/types.ts +2 -1
- package/src/router.ts +11 -12
- package/src/rsc/helpers.ts +20 -6
- package/src/rsc/prefetch-render-error.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +8 -2
- package/src/rsc/request-stages.ts +87 -1
- package/src/rsc/rsc-rendering.ts +78 -32
- package/src/rsc/server-action.ts +2 -1
- package/src/rsc/shell-capture.ts +4 -5
- package/src/rsc/shell-serve.ts +12 -1
- package/src/segment-system.tsx +1 -1
- package/src/server/context.ts +37 -6
- package/src/server/cookie-store.ts +22 -1
- package/src/server/document-location-state.ts +334 -0
- package/src/server/handle-store.ts +2 -0
- package/src/server/request-context.ts +137 -33
- package/src/testing/dispatch.ts +6 -0
- package/src/testing/e2e/fixture.ts +4 -12
- package/src/testing/e2e/server.ts +91 -4
- package/src/testing/render-route.tsx +309 -10
- package/src/testing/run-revalidate.ts +11 -1
- package/src/testing/serve-shell-request.ts +22 -3
- package/src/types/handler-context.ts +21 -4
- package/src/types/loader-types.ts +7 -4
- package/src/types/segments.ts +18 -1
- package/src/urls/path-helper-types.ts +7 -5
- package/src/vite/temp-rsc-server.ts +2 -2
- package/src/vite/utils/forward-user-plugins.ts +13 -5
package/src/cache/cache-scope.ts
CHANGED
|
@@ -235,6 +235,22 @@ function positionalParts(parts: readonly (string | null)[]): string[] {
|
|
|
235
235
|
: parts.map((part) => part ?? "");
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* A route record key's own parts (CacheScope.resolveKeyFrom): `own`, the
|
|
240
|
+
* scope's default key or its store's keyGenerator result (undefined under a
|
|
241
|
+
* `key()` of its own), then the request `kind` unless `own` starts with
|
|
242
|
+
* `{kind}:`. A default key does, and so does a keyGenerator result built on
|
|
243
|
+
* `defaultKey` (`${defaultKey}|x`), which stays byte for byte. One that
|
|
244
|
+
* ignores `defaultKey` (a constant, the pathname) gave a document, a
|
|
245
|
+
* navigation and each intercept navigation one record, so a document GET
|
|
246
|
+
* rendered an intercept's. The kind is a part, never a `{kind}:` prefix: a
|
|
247
|
+
* prefix would give the results `x` and `doc:x` one record.
|
|
248
|
+
*/
|
|
249
|
+
function ownParts(own: string | undefined, kind: string | undefined): string[] {
|
|
250
|
+
if (own === undefined) return kind === undefined ? [] : [kind];
|
|
251
|
+
return kind === undefined || own.startsWith(`${kind}:`) ? [own] : [own, kind];
|
|
252
|
+
}
|
|
253
|
+
|
|
238
254
|
/**
|
|
239
255
|
* One key resolution per request, resolver (`by`: a `key()` or a store's
|
|
240
256
|
* keyGenerator) and `slot`, held on the request context (`_resolvedCacheKeys`,
|
|
@@ -530,8 +546,10 @@ export class CacheScope {
|
|
|
530
546
|
* replaces the URL part, and its store's keyGenerator does not run. It
|
|
531
547
|
* appends `kind` instead (routeRecordKind): the `key()` decides which
|
|
532
548
|
* requests of one kind share a record, never that a document, a
|
|
533
|
-
* navigation and an intercept navigation do.
|
|
534
|
-
*
|
|
549
|
+
* navigation and an intercept navigation do. A keyGenerator result that
|
|
550
|
+
* does not start with its kind gets `kind` after it too (ownParts). With
|
|
551
|
+
* no partition part it is resolveDefaultKey alone, or that result and
|
|
552
|
+
* `kind` composed. With a `key()` on the chain but no request
|
|
535
553
|
* context it rejects (keyWithoutContextError). `partitionedPrefix`
|
|
536
554
|
* prefixes a key built from partition parts only (the response family's
|
|
537
555
|
* `response:`, which passes no `kind`: a response route answers every
|
|
@@ -556,7 +574,9 @@ export class CacheScope {
|
|
|
556
574
|
if (!requestCtx || !this.mayPartition) {
|
|
557
575
|
return this.keyFns.length > 0
|
|
558
576
|
? Promise.reject(keyWithoutContextError())
|
|
559
|
-
: this.resolveDefaultKey(requestCtx, defaultKey)
|
|
577
|
+
: this.resolveDefaultKey(requestCtx, defaultKey).then((own) =>
|
|
578
|
+
composeCacheKeys([], ownParts(own, kind)),
|
|
579
|
+
);
|
|
560
580
|
}
|
|
561
581
|
return Promise.all([
|
|
562
582
|
this.resolvePartitionParts(requestCtx, defaultKey),
|
|
@@ -565,10 +585,11 @@ export class CacheScope {
|
|
|
565
585
|
: this.resolveDefaultKey(requestCtx, defaultKey),
|
|
566
586
|
]).then(([[keys, storeParts], ownDefault]) => {
|
|
567
587
|
const generated = positionalParts(storeParts);
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
588
|
+
const own = ownParts(ownDefault, kind);
|
|
589
|
+
if (keys.length === 0 && generated.length === 0) {
|
|
590
|
+
return composeCacheKeys([], own);
|
|
591
|
+
}
|
|
592
|
+
return partitionedPrefix + composeCacheKeys(keys, [...generated, ...own]);
|
|
572
593
|
});
|
|
573
594
|
}
|
|
574
595
|
|
|
@@ -419,19 +419,23 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
|
419
419
|
/**
|
|
420
420
|
* Custom key generator applied to all cache operations.
|
|
421
421
|
* Receives the full RequestContext (including env) and the default-generated key.
|
|
422
|
-
*
|
|
422
|
+
* The return value is the cache key (unless route overrides with `key`
|
|
423
|
+
* option), except that a route record keeps its request kind: a result that
|
|
424
|
+
* does not start with `defaultKey`'s prefix (`doc:`, `partial:`,
|
|
425
|
+
* `intercept:<intercept>:`) gets the kind appended (see
|
|
426
|
+
* SegmentCacheStore.keyGenerator).
|
|
423
427
|
*
|
|
424
428
|
* Reserved prefixes: tag-invalidation markers live in the SAME KV namespace as
|
|
425
429
|
* data, keyed `__tag__/<tag>` (and `__tagmarker__/<tag>` for the L1 cache). A
|
|
426
430
|
* returned key must NOT begin with `__tag__/` or `__tagmarker__/`, or it can
|
|
427
431
|
* collide with a tag marker and corrupt invalidation. The documented
|
|
428
|
-
*
|
|
432
|
+
* generators below start with `defaultKey` and are safe.
|
|
429
433
|
*
|
|
430
434
|
* @example Using headers for user segmentation
|
|
431
435
|
* ```typescript
|
|
432
436
|
* keyGenerator: (ctx, defaultKey) => {
|
|
433
437
|
* const segment = ctx.request.headers.get('x-user-segment') || 'default';
|
|
434
|
-
* return `${
|
|
438
|
+
* return `${defaultKey}|${segment}`;
|
|
435
439
|
* }
|
|
436
440
|
* ```
|
|
437
441
|
*
|
|
@@ -439,7 +443,7 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
|
439
443
|
* ```typescript
|
|
440
444
|
* keyGenerator: (ctx, defaultKey) => {
|
|
441
445
|
* const region = ctx.env.REGION || 'us';
|
|
442
|
-
* return `${
|
|
446
|
+
* return `${defaultKey}|${region}`;
|
|
443
447
|
* }
|
|
444
448
|
* ```
|
|
445
449
|
*
|
|
@@ -447,7 +451,7 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
|
447
451
|
* ```typescript
|
|
448
452
|
* keyGenerator: (ctx, defaultKey) => {
|
|
449
453
|
* const locale = cookies().get('locale')?.value || 'en';
|
|
450
|
-
* return `${
|
|
454
|
+
* return `${defaultKey}|${locale}`;
|
|
451
455
|
* }
|
|
452
456
|
* ```
|
|
453
457
|
*/
|
|
@@ -69,7 +69,11 @@ export function shouldCacheResponse(
|
|
|
69
69
|
response: Response,
|
|
70
70
|
requestCtx?: Pick<
|
|
71
71
|
RequestContext,
|
|
72
|
-
|
|
72
|
+
| "_payloadVisitorTheme"
|
|
73
|
+
| "_payloadDeferred"
|
|
74
|
+
| "_payloadSourceScoped"
|
|
75
|
+
| "_payloadVisitorLocation"
|
|
76
|
+
| "_documentLocationState"
|
|
73
77
|
>,
|
|
74
78
|
): CacheDirectives | null {
|
|
75
79
|
// Only cache successful responses
|
|
@@ -100,6 +104,12 @@ export function shouldCacheResponse(
|
|
|
100
104
|
return null;
|
|
101
105
|
}
|
|
102
106
|
|
|
107
|
+
// The payload carries the visitor's location state (#1330,
|
|
108
|
+
// server/document-location-state.ts).
|
|
109
|
+
if (requestCtx?._documentLocationState) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
103
113
|
// prefetch: false. The payload carries deferred units: it answers one
|
|
104
114
|
// page's prefetch and nothing else. parseCacheControl refuses the
|
|
105
115
|
// Cache-Control it is sent with (rsc-rendering.ts NOT_REUSED), but a
|
|
@@ -109,10 +119,36 @@ export function shouldCacheResponse(
|
|
|
109
119
|
return null;
|
|
110
120
|
}
|
|
111
121
|
|
|
122
|
+
// A partial scoped to its source page: this cache keys it by
|
|
123
|
+
// URL and the ids the client holds, so it would answer another page with
|
|
124
|
+
// the same ids. rsc-rendering.ts sends it NOT_REUSED; a middleware can
|
|
125
|
+
// replace that after next().
|
|
126
|
+
if (requestCtx?._payloadSourceScoped) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
112
130
|
const cacheControl = response.headers.get("Cache-Control");
|
|
113
131
|
return parseCacheControl(cacheControl);
|
|
114
132
|
}
|
|
115
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Whether a response carries one client's state, which a stored copy would
|
|
136
|
+
* replay to that client's later requests: a per-client signal header (a state
|
|
137
|
+
* cookie rotation, `x-rango-keep-cache`) or location state. The stored
|
|
138
|
+
* prefetch Cache-Control (rsc/request-stages.ts) is not sent on
|
|
139
|
+
* one.
|
|
140
|
+
*/
|
|
141
|
+
export function carriesClientState(
|
|
142
|
+
response: Response,
|
|
143
|
+
requestCtx: Pick<RequestContext, "_locationState" | "_documentLocationState">,
|
|
144
|
+
): boolean {
|
|
145
|
+
return (
|
|
146
|
+
hasPerClientSignal(response.headers) ||
|
|
147
|
+
(requestCtx._locationState?.length ?? 0) > 0 ||
|
|
148
|
+
requestCtx._documentLocationState === true
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
116
152
|
/**
|
|
117
153
|
* Whether this cache will store the response the current render produces, as
|
|
118
154
|
* far as the render can tell: the middleware runs it (a MISS or a stale
|
|
@@ -276,6 +276,13 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
276
276
|
return next();
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
// Location state is already set for this request (a document redirect's
|
|
280
|
+
// cookie, an earlier middleware): the response is the visitor's, so a
|
|
281
|
+
// stored body must not answer it, and it is not stored.
|
|
282
|
+
if ((requestCtx._locationState?.length ?? 0) > 0) {
|
|
283
|
+
return next();
|
|
284
|
+
}
|
|
285
|
+
|
|
279
286
|
// The write gate's start (#977) and the entry's stamp (#1068): the
|
|
280
287
|
// request's, not this middleware's, also for a stale refresh. A
|
|
281
288
|
// middleware ahead of this one can read tagged data the document bakes,
|
|
@@ -350,6 +357,11 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
350
357
|
if (!cached.shouldRevalidate) {
|
|
351
358
|
// Fresh hit - return immediately
|
|
352
359
|
log(`[DocumentCache] HIT ${typeLabel}: ${url.pathname}`);
|
|
360
|
+
// Nothing in it runs: its loaders' output is replayed, and no
|
|
361
|
+
// failed render is ever stored (throwIfRenderErrored). A prefetch
|
|
362
|
+
// answered with it leaves with the router's prefetch Cache-Control
|
|
363
|
+
// (rsc/request-stages.ts).
|
|
364
|
+
requestCtx._servedFromStorage = true;
|
|
353
365
|
return drainOnResponseCallbacks(
|
|
354
366
|
addCacheStatusHeader(cached.response, "HIT"),
|
|
355
367
|
requestCtx,
|
|
@@ -120,13 +120,14 @@ export interface MemorySegmentCacheStoreOptions<TEnv = unknown> {
|
|
|
120
120
|
|
|
121
121
|
/**
|
|
122
122
|
* Custom key generator applied to all cache operations.
|
|
123
|
-
* Receives the full RequestContext and the default-generated key.
|
|
123
|
+
* Receives the full RequestContext and the default-generated key. A route
|
|
124
|
+
* record keeps its request kind (see SegmentCacheStore.keyGenerator).
|
|
124
125
|
*
|
|
125
126
|
* @example
|
|
126
127
|
* ```typescript
|
|
127
128
|
* keyGenerator: (ctx, defaultKey) => {
|
|
128
129
|
* const locale = cookies().get('locale')?.value || 'en';
|
|
129
|
-
* return `${
|
|
130
|
+
* return `${defaultKey}|${locale}`;
|
|
130
131
|
* }
|
|
131
132
|
* ```
|
|
132
133
|
*/
|
|
@@ -309,8 +309,8 @@ export async function serializeSegments(
|
|
|
309
309
|
* cached with the route record).
|
|
310
310
|
*
|
|
311
311
|
* The loading "null" sentinel decodes here (not on the consumer): loading:null
|
|
312
|
-
* must survive as null, and shipping the raw sentinel would
|
|
313
|
-
*
|
|
312
|
+
* must survive as null, and shipping the raw sentinel would hand renderSegments
|
|
313
|
+
* a truthy `loading`, the fallback tree shape (docs/tree-structure.md).
|
|
314
314
|
*/
|
|
315
315
|
export async function fragmentSegments(
|
|
316
316
|
data: SerializedSegmentData[],
|
|
@@ -407,7 +407,7 @@ const PIN_KEY_PREFIX = /^[^D.]*D\d+\./;
|
|
|
407
407
|
* Deserializations run in parallel; a record that fails to decode is skipped
|
|
408
408
|
* and reported to `onUndecodable`: its loader runs instead, which a document
|
|
409
409
|
* HIT has already committed a prelude against, so serveShellHit retires the
|
|
410
|
-
* entry
|
|
410
|
+
* entry. Returns undefined when the snapshot carries no loader
|
|
411
411
|
* records, without touching the Flight codec (kept lazy for cold paths and
|
|
412
412
|
* non-RSC configs).
|
|
413
413
|
*/
|
package/src/cache/types.ts
CHANGED
|
@@ -93,16 +93,21 @@ export interface SegmentCacheStore<TEnv = unknown> {
|
|
|
93
93
|
/**
|
|
94
94
|
* Custom key generator applied to all cache operations using this store.
|
|
95
95
|
* Receives the full RequestContext and the default-generated key.
|
|
96
|
-
*
|
|
96
|
+
* The return value is the cache key (unless route overrides with `key`
|
|
97
|
+
* option), with a route record's request kind appended when it does not
|
|
98
|
+
* start with that kind (below).
|
|
97
99
|
*
|
|
98
100
|
* Resolution priority:
|
|
99
101
|
* 1. Route-level `key` function (replaces the URL part; the request kind stays)
|
|
100
102
|
* 2. Store-level `keyGenerator` (modifies default key)
|
|
101
103
|
* 3. Default key generation (prefix:pathname:params)
|
|
102
104
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
105
|
+
* A route record keeps its request kind (a document, a navigation or an
|
|
106
|
+
* intercept navigation), whose records hold different segments: the kind
|
|
107
|
+
* is `defaultKey`'s prefix (`doc:`, `partial:`, `intercept:<intercept>:`),
|
|
108
|
+
* and a result that does not start with it gets the kind appended as a
|
|
109
|
+
* key part of its own. Build on `defaultKey` (`${defaultKey}|${segment}`)
|
|
110
|
+
* and the result is the record key byte for byte.
|
|
106
111
|
*
|
|
107
112
|
* Return `defaultKey` unchanged to leave a request unpartitioned. An empty
|
|
108
113
|
* string is not a key: where the result partitions a nested cache() on
|
|
@@ -114,7 +119,7 @@ export interface SegmentCacheStore<TEnv = unknown> {
|
|
|
114
119
|
* ```typescript
|
|
115
120
|
* keyGenerator: (ctx, defaultKey) => {
|
|
116
121
|
* const segment = ctx.request.headers.get('x-user-segment') || 'default';
|
|
117
|
-
* return `${
|
|
122
|
+
* return `${defaultKey}|${segment}`;
|
|
118
123
|
* }
|
|
119
124
|
* ```
|
|
120
125
|
*
|
|
@@ -122,7 +127,7 @@ export interface SegmentCacheStore<TEnv = unknown> {
|
|
|
122
127
|
* ```typescript
|
|
123
128
|
* keyGenerator: (ctx, defaultKey) => {
|
|
124
129
|
* const region = ctx.env.REGION || 'us';
|
|
125
|
-
* return `${
|
|
130
|
+
* return `${defaultKey}|${region}`;
|
|
126
131
|
* }
|
|
127
132
|
* ```
|
|
128
133
|
*
|
|
@@ -130,7 +135,7 @@ export interface SegmentCacheStore<TEnv = unknown> {
|
|
|
130
135
|
* ```typescript
|
|
131
136
|
* keyGenerator: (ctx, defaultKey) => {
|
|
132
137
|
* const locale = cookies().get('locale')?.value || 'en';
|
|
133
|
-
* return `${
|
|
138
|
+
* return `${defaultKey}|${locale}`;
|
|
134
139
|
* }
|
|
135
140
|
* ```
|
|
136
141
|
*/
|
|
@@ -356,7 +356,8 @@ export interface VercelCacheStoreOptions<TEnv = unknown> {
|
|
|
356
356
|
|
|
357
357
|
/**
|
|
358
358
|
* Custom key generator applied to all cache operations (region/locale/user
|
|
359
|
-
* segmentation). Receives the request context and the default key.
|
|
359
|
+
* segmentation). Receives the request context and the default key. A route
|
|
360
|
+
* record keeps its request kind (see SegmentCacheStore.keyGenerator).
|
|
360
361
|
*/
|
|
361
362
|
keyGenerator?: (
|
|
362
363
|
ctx: RequestContext<TEnv>,
|
package/src/client-urls/types.ts
CHANGED
|
@@ -85,8 +85,9 @@ export interface ClientUrlLoaderRecord {
|
|
|
85
85
|
*/
|
|
86
86
|
readonly ssr?: false;
|
|
87
87
|
/**
|
|
88
|
-
* loader(Def, { prefetch: false }): a
|
|
89
|
-
* intercept's target from its source, defers this loader
|
|
88
|
+
* loader(Def, { prefetch: false }): a viewport or render prefetch of the
|
|
89
|
+
* route, or of an intercept's target from its source, defers this loader
|
|
90
|
+
* (a hover prefetch runs it) and the click
|
|
90
91
|
* that adopts it runs it in its fill request (see {@link LoaderOptions}).
|
|
91
92
|
* Projected onto the materialized stub.
|
|
92
93
|
*/
|
|
@@ -169,12 +170,13 @@ export interface ClientUrlHelpers {
|
|
|
169
170
|
* holes).
|
|
170
171
|
*
|
|
171
172
|
* Pass `{ prefetch: false }` to keep this loader out of the route's
|
|
172
|
-
*
|
|
173
|
+
* viewport and render prefetches; a hover prefetch runs it (see
|
|
174
|
+
* {@link LoaderOptions}). An in-group click still shows
|
|
173
175
|
* the destination at once, commits the adopted prefetch with the other
|
|
174
176
|
* loaders' data, and runs only the flagged loaders in its one fill
|
|
175
177
|
* request. A `layout()` loader with the flag is flagged on every route
|
|
176
|
-
* below it. Inside intercept() it defers the loader in a
|
|
177
|
-
* intercept from its source, as in urls().
|
|
178
|
+
* below it. Inside intercept() it defers the loader in a viewport or
|
|
179
|
+
* render prefetch of the intercept from its source, as in urls().
|
|
178
180
|
*/
|
|
179
181
|
readonly loader: <TData>(
|
|
180
182
|
definition: LoaderDefinition<TData>,
|
package/src/prefetch-markers.ts
CHANGED
|
@@ -9,6 +9,16 @@
|
|
|
9
9
|
/** Request header of a `<Link>` prefetch (browser/prefetch/fetch.ts). */
|
|
10
10
|
export const PREFETCH_HEADER = "X-Rango-Prefetch";
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* PREFETCH_HEADER's value on an intent prefetch: a hover or an explicit
|
|
14
|
+
* `prefetch()` call (browser/prefetch/fetch.ts prefetchDirect). The server
|
|
15
|
+
* defers nothing in it: `prefetch: false` loaders and the work behind a
|
|
16
|
+
* flagged `loading()` run, as in the navigation it starts early. A
|
|
17
|
+
* speculative prefetch (viewport, render) sends `1`, and any value other than
|
|
18
|
+
* this one reads as speculative.
|
|
19
|
+
*/
|
|
20
|
+
export const PREFETCH_INTENT = "intent";
|
|
21
|
+
|
|
12
22
|
/**
|
|
13
23
|
* The last row of a prefetch whose render reported an error
|
|
14
24
|
* (rsc/prefetch-render-error.ts): a Flight hint with a code React does not
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The source group of a prefetch answer a revalidate() rule kept from the
|
|
3
|
+
* page being left (#1331). Shared by the server, which reports
|
|
4
|
+
* the group in `x-rsc-prefetch-scope: params; <label>` (rsc/rsc-rendering.ts),
|
|
5
|
+
* and the browser, which stores the answer under that label and rebuilds it
|
|
6
|
+
* from the page on screen to look it up (browser/prefetch/cache.ts).
|
|
7
|
+
* Dependency free, like prefetch-markers.ts: the browser must not import the
|
|
8
|
+
* server modules that record the reads (router/source-reads.ts).
|
|
9
|
+
*
|
|
10
|
+
* Why a label is enough: a rule's decision depends on the source page only
|
|
11
|
+
* through what it read. Two pages with equal values of everything the kept
|
|
12
|
+
* rules read run every rule the same way, so one answer serves both.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** What the revalidate() rules that kept a segment read of the source page. */
|
|
16
|
+
export interface SourceReads {
|
|
17
|
+
/**
|
|
18
|
+
* `currentParams` was read. Its key set is then part of the group: a rule
|
|
19
|
+
* can test a key's presence (`in`, `Object.keys`) without reading a value.
|
|
20
|
+
*/
|
|
21
|
+
keys: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Each `currentParams` value read; a spread or JSON.stringify reads all.
|
|
24
|
+
* Non-empty only with `keys`: a value is read through `currentParams`.
|
|
25
|
+
*/
|
|
26
|
+
params: Set<string>;
|
|
27
|
+
/** `fromRouteName` was read. */
|
|
28
|
+
route: boolean;
|
|
29
|
+
/** `currentUrl` was read: the answer is the exact page's. */
|
|
30
|
+
url: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The page a label describes: the source on the server, the screen in the browser. */
|
|
34
|
+
export interface SourcePage {
|
|
35
|
+
params: Readonly<Record<string, string | undefined>>;
|
|
36
|
+
/** The public route name (route-name.ts publicRouteName). */
|
|
37
|
+
routeName: string | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** What a label reads, without the values: one per kind of rule set. */
|
|
41
|
+
export interface SourceShape {
|
|
42
|
+
keys: boolean;
|
|
43
|
+
route: boolean;
|
|
44
|
+
/** Sorted. */
|
|
45
|
+
names: readonly string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** `x-rsc-prefetch-scope` value prefix of a params-grouped answer. */
|
|
49
|
+
export const PARAMS_SCOPE_PREFIX = "params; ";
|
|
50
|
+
|
|
51
|
+
export function createSourceReads(): SourceReads {
|
|
52
|
+
return { keys: false, params: new Set(), route: false, url: false };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function hasSourceReads(reads: SourceReads): boolean {
|
|
56
|
+
return reads.keys || reads.route || reads.url || reads.params.size > 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Union `from` into `into`. */
|
|
60
|
+
export function mergeSourceReads(into: SourceReads, from: SourceReads): void {
|
|
61
|
+
into.keys ||= from.keys;
|
|
62
|
+
into.route ||= from.route;
|
|
63
|
+
into.url ||= from.url;
|
|
64
|
+
for (const name of from.params) into.params.add(name);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function shapeOfReads(reads: SourceReads): SourceShape {
|
|
68
|
+
return {
|
|
69
|
+
keys: reads.keys,
|
|
70
|
+
route: reads.route,
|
|
71
|
+
names: [...reads.params].sort(),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The label of `page` under `shape`: `&`-joined, `$k=<keys>` first, then
|
|
77
|
+
* `$r[=<route>]`, then `<name>[=<value>]` per read param, sorted. A missing
|
|
78
|
+
* value has no `=`. Names and values are URI-encoded, so a `$` entry never
|
|
79
|
+
* collides with a param (encodeURIComponent encodes `$`).
|
|
80
|
+
*/
|
|
81
|
+
export function formatSourceLabel(
|
|
82
|
+
shape: SourceShape,
|
|
83
|
+
page: SourcePage,
|
|
84
|
+
): string {
|
|
85
|
+
const parts: string[] = [];
|
|
86
|
+
if (shape.keys) {
|
|
87
|
+
parts.push(
|
|
88
|
+
"$k=" + Object.keys(page.params).sort().map(encodeURIComponent).join(","),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (shape.route) parts.push(entry("$r", page.routeName));
|
|
92
|
+
for (const name of shape.names) {
|
|
93
|
+
parts.push(entry(encodeURIComponent(name), page.params[name]));
|
|
94
|
+
}
|
|
95
|
+
return parts.join("&");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function entry(name: string, value: string | undefined): string {
|
|
99
|
+
return value === undefined ? name : name + "=" + encodeURIComponent(value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The label of an `x-rsc-prefetch-scope` value, or undefined when the value
|
|
104
|
+
* is not a params scope.
|
|
105
|
+
*/
|
|
106
|
+
export function paramsScopeLabel(
|
|
107
|
+
header: string | null | undefined,
|
|
108
|
+
): string | undefined {
|
|
109
|
+
if (!header?.startsWith(PARAMS_SCOPE_PREFIX)) return undefined;
|
|
110
|
+
return header.slice(PARAMS_SCOPE_PREFIX.length);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The shape of a label formatSourceLabel wrote. */
|
|
114
|
+
export function parseSourceShape(label: string): SourceShape {
|
|
115
|
+
let keys = false;
|
|
116
|
+
let route = false;
|
|
117
|
+
const names: string[] = [];
|
|
118
|
+
for (const part of label.split("&")) {
|
|
119
|
+
if (part === "") continue;
|
|
120
|
+
const eq = part.indexOf("=");
|
|
121
|
+
const name = eq === -1 ? part : part.slice(0, eq);
|
|
122
|
+
if (name === "$k") keys = true;
|
|
123
|
+
else if (name === "$r") route = true;
|
|
124
|
+
else names.push(decodeURIComponent(name));
|
|
125
|
+
}
|
|
126
|
+
return { keys, route, names: names.sort() };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A stable id of a shape, for indexing. */
|
|
130
|
+
export function sourceShapeId(shape: SourceShape): string {
|
|
131
|
+
return (
|
|
132
|
+
(shape.keys ? "k" : "") +
|
|
133
|
+
(shape.route ? "r" : "") +
|
|
134
|
+
":" +
|
|
135
|
+
shape.names.map(encodeURIComponent).join(",")
|
|
136
|
+
);
|
|
137
|
+
}
|
|
@@ -333,9 +333,10 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
333
333
|
* loader(RecommendationsLoader), // still streams behind loading()
|
|
334
334
|
* ```
|
|
335
335
|
*
|
|
336
|
-
* Pass `{ prefetch: false }` to keep the loader out of
|
|
337
|
-
* prefetch skips it and the click
|
|
338
|
-
* one follow-up request
|
|
336
|
+
* Pass `{ prefetch: false }` to keep the loader out of speculative
|
|
337
|
+
* prefetches (viewport, render): such a prefetch skips it and the click
|
|
338
|
+
* that adopts it fetches it in one follow-up request; a hover prefetch runs
|
|
339
|
+
* it (see {@link LoaderOptions}). Only on a segment the
|
|
339
340
|
* client does not have yet: a loader the client already holds runs in the
|
|
340
341
|
* prefetch when it revalidates.
|
|
341
342
|
*
|
|
@@ -361,14 +362,16 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
361
362
|
* // Skip loading on SSR, only show on client navigation
|
|
362
363
|
* loading(<Skeleton />, { ssr: false })
|
|
363
364
|
*
|
|
364
|
-
* // Keep everything behind this fallback out of
|
|
365
|
+
* // Keep everything behind this fallback out of viewport and render
|
|
366
|
+
* // prefetches (a hover prefetch runs it)
|
|
365
367
|
* loading(<Skeleton />, { prefetch: false })
|
|
366
368
|
* ```
|
|
367
369
|
* @param component - The loading UI to show during navigation
|
|
368
370
|
* @param options - Configuration options
|
|
369
371
|
* @param options.ssr - If false, skip showing loading on document requests (SSR)
|
|
370
372
|
* @param options.prefetch - If false, the flag applies when this entry's
|
|
371
|
-
* segment is new to the client. A prefetch then skips
|
|
373
|
+
* segment is new to the client. A viewport or render prefetch then skips
|
|
374
|
+
* the whole unit (a hover prefetch runs it): the
|
|
372
375
|
* handler, its loaders, its orphan layouts and parallel slots and, for a
|
|
373
376
|
* layout, every deeper entry; the click that adopts the prefetch shows
|
|
374
377
|
* this fallback and fetches them in one follow-up request, which reads
|
|
@@ -20,6 +20,11 @@ import { prefixBasename } from "../router/basename.js";
|
|
|
20
20
|
* Supports an optional state parameter to carry location state through the
|
|
21
21
|
* redirect. On the target page, state can be read via useLocationState()
|
|
22
22
|
* (use { flash: true } in createLocationState for read-once flash messages).
|
|
23
|
+
* On a client navigation or action the state travels in the redirect's
|
|
24
|
+
* payload. On a document request (a page load, a no-JS form post) it travels
|
|
25
|
+
* in a one-shot cookie the target's document request reads and clears, as
|
|
26
|
+
* JSON: give it plain JSON data there. `{ external: true }` carries none.
|
|
27
|
+
* Two entries of the same definition keep only the last.
|
|
23
28
|
*
|
|
24
29
|
* @param url - The URL to redirect to
|
|
25
30
|
* @param statusOrOptions - HTTP status code (default: 302) or options object
|