@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.
Files changed (192) hide show
  1. package/README.md +3 -2
  2. package/dist/bin/browser-7KIJ5HSV.js +15 -0
  3. package/dist/bin/chunk-3B7CKBRO.js +463 -0
  4. package/dist/bin/chunk-524VACCG.js +71 -0
  5. package/dist/bin/chunk-LD7UBHT4.js +196 -0
  6. package/dist/bin/chunk-TO6CRWLN.js +1429 -0
  7. package/dist/bin/chunk-TTXC2U3G.js +2477 -0
  8. package/dist/bin/client-urls-projection-DQLWPDAT.js +28 -0
  9. package/dist/bin/forward-user-plugins-URBZKJP4.js +62 -0
  10. package/dist/bin/rango.js +42 -10143
  11. package/dist/bin/runtime-EPUDEHO4.js +596 -0
  12. package/dist/bin/runtime-discovery-CZ2QHT4Q.js +352 -0
  13. package/dist/bin/state-ACD2MGKJ.js +16 -0
  14. package/dist/bin/temp-rsc-server-QRR6HD6T.js +3190 -0
  15. package/dist/types/bin/rango.d.ts +1 -0
  16. package/dist/types/browser/action-fence.d.ts +20 -0
  17. package/dist/types/browser/cookie-name.d.ts +51 -10
  18. package/dist/types/browser/history-state.d.ts +13 -2
  19. package/dist/types/browser/invalidate-client-cache.d.ts +8 -6
  20. package/dist/types/browser/navigation-bridge.d.ts +36 -1
  21. package/dist/types/browser/navigation-client.d.ts +19 -0
  22. package/dist/types/browser/navigation-store-handle.d.ts +37 -4
  23. package/dist/types/browser/navigation-store.d.ts +2 -2
  24. package/dist/types/browser/navigation-transaction.d.ts +0 -1
  25. package/dist/types/browser/partial-update.d.ts +13 -0
  26. package/dist/types/browser/prefetch/cache.d.ts +96 -8
  27. package/dist/types/browser/prefetch/fetch.d.ts +26 -2
  28. package/dist/types/browser/prefetch/queue.d.ts +3 -2
  29. package/dist/types/browser/rango-state.d.ts +34 -5
  30. package/dist/types/browser/react/Link.d.ts +19 -13
  31. package/dist/types/browser/react/location-state-shared.d.ts +2 -1
  32. package/dist/types/browser/types.d.ts +25 -0
  33. package/dist/types/build/generate-route-types.d.ts +1 -1
  34. package/dist/types/build/route-types/include-resolution.d.ts +9 -1
  35. package/dist/types/build/route-types/router-processing.d.ts +3 -1
  36. package/dist/types/cache/cache-key-utils.d.ts +12 -8
  37. package/dist/types/cache/cache-scope.d.ts +4 -2
  38. package/dist/types/cache/cf/cf-cache-types.d.ts +9 -5
  39. package/dist/types/cache/document-cache-policy.d.ts +9 -1
  40. package/dist/types/cache/memory-segment-store.d.ts +3 -2
  41. package/dist/types/cache/segment-codec.d.ts +2 -2
  42. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  43. package/dist/types/cache/types.d.ts +12 -7
  44. package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -1
  45. package/dist/types/client-urls/types.d.ts +7 -5
  46. package/dist/types/prefetch-markers.d.ts +9 -0
  47. package/dist/types/prefetch-source-scope.d.ts +66 -0
  48. package/dist/types/route-definition/helpers-types.d.ts +8 -5
  49. package/dist/types/route-definition/redirect.d.ts +5 -0
  50. package/dist/types/router/intercept-resolution.d.ts +25 -11
  51. package/dist/types/router/match-context.d.ts +1 -0
  52. package/dist/types/router/match-handlers.d.ts +1 -1
  53. package/dist/types/router/middleware-types.d.ts +6 -0
  54. package/dist/types/router/prefetch-cache-ttl.d.ts +11 -3
  55. package/dist/types/router/router-context.d.ts +1 -1
  56. package/dist/types/router/router-options.d.ts +6 -3
  57. package/dist/types/router/segment-resolution/loader-cache.d.ts +8 -0
  58. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +23 -7
  59. package/dist/types/router/segment-resolution/revalidation.d.ts +19 -0
  60. package/dist/types/router/source-reads.d.ts +53 -0
  61. package/dist/types/router/types.d.ts +1 -1
  62. package/dist/types/rsc/helpers.d.ts +8 -3
  63. package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
  64. package/dist/types/rsc/request-stages.d.ts +4 -0
  65. package/dist/types/rsc/shell-serve.d.ts +6 -1
  66. package/dist/types/server/context.d.ts +23 -6
  67. package/dist/types/server/cookie-store.d.ts +15 -1
  68. package/dist/types/server/document-location-state.d.ts +59 -0
  69. package/dist/types/server/handle-store.d.ts +2 -0
  70. package/dist/types/server/request-context.d.ts +65 -12
  71. package/dist/types/testing/e2e/server.d.ts +16 -1
  72. package/dist/types/testing/render-route.d.ts +83 -2
  73. package/dist/types/testing/serve-shell-request.d.ts +15 -3
  74. package/dist/types/types/handler-context.d.ts +21 -4
  75. package/dist/types/types/loader-types.d.ts +7 -4
  76. package/dist/types/types/segments.d.ts +18 -1
  77. package/dist/types/urls/path-helper-types.d.ts +7 -5
  78. package/dist/types/vite/temp-rsc-server.d.ts +2 -2
  79. package/dist/types/vite/utils/forward-user-plugins.d.ts +32 -2
  80. package/dist/vite/index.js +451 -191
  81. package/package.json +2 -2
  82. package/skills/app-structure/SKILL.md +74 -0
  83. package/skills/app-structure/e-commerce.md +10 -9
  84. package/skills/app-structure/large-app.md +15 -2
  85. package/skills/caching/SKILL.md +10 -5
  86. package/skills/client-urls/SKILL.md +18 -11
  87. package/skills/comparison/references/framework-comparison.md +16 -10
  88. package/skills/document-cache/SKILL.md +13 -0
  89. package/skills/hooks/navigation.md +8 -8
  90. package/skills/hooks/state.md +132 -12
  91. package/skills/intercept/SKILL.md +33 -11
  92. package/skills/layout/SKILL.md +11 -0
  93. package/skills/links/SKILL.md +101 -4
  94. package/skills/loader/SKILL.md +49 -22
  95. package/skills/migrate-nextjs/SKILL.md +8 -2
  96. package/skills/migrate-react-router/data-and-actions.md +5 -0
  97. package/skills/ppr/SKILL.md +38 -5
  98. package/skills/prerender/SKILL.md +4 -1
  99. package/skills/route/SKILL.md +6 -6
  100. package/skills/router-setup/SKILL.md +2 -1
  101. package/skills/server-actions/SKILL.md +49 -8
  102. package/skills/testing/client-components.md +73 -32
  103. package/skills/testing/serve-shell-request.md +45 -20
  104. package/skills/testing/server-actions.md +8 -0
  105. package/skills/ux-architecture/SKILL.md +67 -15
  106. package/src/bin/rango.ts +39 -13
  107. package/src/browser/action-fence.ts +72 -0
  108. package/src/browser/cookie-name.ts +82 -14
  109. package/src/browser/history-state.ts +24 -2
  110. package/src/browser/invalidate-client-cache.ts +9 -7
  111. package/src/browser/navigation-bridge.ts +164 -32
  112. package/src/browser/navigation-client.ts +95 -20
  113. package/src/browser/navigation-store-handle.ts +67 -5
  114. package/src/browser/navigation-store.ts +86 -17
  115. package/src/browser/navigation-transaction.ts +0 -6
  116. package/src/browser/partial-update.ts +31 -12
  117. package/src/browser/prefetch/cache.ts +188 -11
  118. package/src/browser/prefetch/fetch.ts +211 -51
  119. package/src/browser/prefetch/queue.ts +3 -2
  120. package/src/browser/rango-state.ts +50 -8
  121. package/src/browser/react/Link.tsx +24 -16
  122. package/src/browser/react/location-state-shared.ts +8 -1
  123. package/src/browser/react/use-navigation.ts +1 -1
  124. package/src/browser/rsc-router.tsx +13 -3
  125. package/src/browser/server-action-bridge.ts +3 -3
  126. package/src/browser/types.ts +25 -0
  127. package/src/build/generate-route-types.ts +1 -0
  128. package/src/build/route-types/include-resolution.ts +40 -3
  129. package/src/build/route-types/router-processing.ts +170 -25
  130. package/src/build/route-types/scan-filter.ts +1 -0
  131. package/src/build/runtime-discovery.ts +46 -8
  132. package/src/cache/cache-key-utils.ts +12 -8
  133. package/src/cache/cache-runtime.ts +5 -1
  134. package/src/cache/cache-scope.ts +28 -7
  135. package/src/cache/cf/cf-cache-types.ts +9 -5
  136. package/src/cache/document-cache-policy.ts +37 -1
  137. package/src/cache/document-cache.ts +12 -0
  138. package/src/cache/memory-segment-store.ts +3 -2
  139. package/src/cache/segment-codec.ts +2 -2
  140. package/src/cache/shell-snapshot.ts +1 -1
  141. package/src/cache/types.ts +12 -7
  142. package/src/cache/vercel/vercel-cache-store.ts +2 -1
  143. package/src/client-urls/types.ts +7 -5
  144. package/src/prefetch-markers.ts +10 -0
  145. package/src/prefetch-source-scope.ts +137 -0
  146. package/src/route-definition/helpers-types.ts +8 -5
  147. package/src/route-definition/redirect.ts +5 -0
  148. package/src/router/intercept-resolution.ts +94 -50
  149. package/src/router/loader-resolution.ts +8 -6
  150. package/src/router/match-api.ts +33 -29
  151. package/src/router/match-context.ts +5 -0
  152. package/src/router/match-handlers.ts +2 -1
  153. package/src/router/match-middleware/cache-lookup.ts +39 -14
  154. package/src/router/match-result.ts +45 -85
  155. package/src/router/middleware-types.ts +6 -0
  156. package/src/router/navigation-snapshot.ts +18 -3
  157. package/src/router/prefetch-cache-ttl.ts +15 -3
  158. package/src/router/revalidation.ts +63 -13
  159. package/src/router/router-context.ts +2 -1
  160. package/src/router/router-options.ts +6 -3
  161. package/src/router/segment-resolution/loader-cache.ts +21 -3
  162. package/src/router/segment-resolution/prefetch-deferral.ts +38 -31
  163. package/src/router/segment-resolution/revalidation.ts +40 -12
  164. package/src/router/source-reads.ts +122 -0
  165. package/src/router/types.ts +2 -1
  166. package/src/router.ts +11 -12
  167. package/src/rsc/helpers.ts +20 -6
  168. package/src/rsc/prefetch-render-error.ts +7 -2
  169. package/src/rsc/progressive-enhancement.ts +8 -2
  170. package/src/rsc/request-stages.ts +87 -1
  171. package/src/rsc/rsc-rendering.ts +78 -32
  172. package/src/rsc/server-action.ts +2 -1
  173. package/src/rsc/shell-capture.ts +4 -5
  174. package/src/rsc/shell-serve.ts +12 -1
  175. package/src/segment-system.tsx +1 -1
  176. package/src/server/context.ts +37 -6
  177. package/src/server/cookie-store.ts +22 -1
  178. package/src/server/document-location-state.ts +334 -0
  179. package/src/server/handle-store.ts +2 -0
  180. package/src/server/request-context.ts +137 -33
  181. package/src/testing/dispatch.ts +6 -0
  182. package/src/testing/e2e/fixture.ts +4 -12
  183. package/src/testing/e2e/server.ts +91 -4
  184. package/src/testing/render-route.tsx +309 -10
  185. package/src/testing/run-revalidate.ts +11 -1
  186. package/src/testing/serve-shell-request.ts +22 -3
  187. package/src/types/handler-context.ts +21 -4
  188. package/src/types/loader-types.ts +7 -4
  189. package/src/types/segments.ts +18 -1
  190. package/src/urls/path-helper-types.ts +7 -5
  191. package/src/vite/temp-rsc-server.ts +2 -2
  192. package/src/vite/utils/forward-user-plugins.ts +13 -5
@@ -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. With no partition part it is
534
- * resolveDefaultKey alone. With a `key()` on the chain but no request
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
- if (keys.length === 0 && generated.length === 0) return ownDefault!;
569
- const last = ownDefault ?? kind;
570
- if (last !== undefined) generated.push(last);
571
- return partitionedPrefix + composeCacheKeys(keys, generated);
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
- * Return value becomes the final cache key (unless route overrides with `key` option).
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
- * prepend-style generators below are safe.
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 `${segment}:${defaultKey}`;
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 `${region}:${defaultKey}`;
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 `${locale}:${defaultKey}`;
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
- "_payloadVisitorTheme" | "_payloadDeferred" | "_payloadVisitorLocation"
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 `${locale}:${defaultKey}`;
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 change the
313
- * deduplicateLoaderSegments loading-presence check.
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 (dc-t89). Returns undefined when the snapshot carries no loader
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
  */
@@ -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
- * Return value becomes the final cache key (unless route overrides with `key` option).
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
- * Keep `defaultKey` in the result: its prefix names the request kind (a
104
- * document, a navigation or an intercept navigation), whose records hold
105
- * different segments. A result without it gives every kind one record.
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 `${segment}:${defaultKey}`;
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 `${region}:${defaultKey}`;
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 `${locale}:${defaultKey}`;
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>,
@@ -85,8 +85,9 @@ export interface ClientUrlLoaderRecord {
85
85
  */
86
86
  readonly ssr?: false;
87
87
  /**
88
- * loader(Def, { prefetch: false }): a prefetch of the route, or of an
89
- * intercept's target from its source, defers this loader and the click
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
- * prefetches (see {@link LoaderOptions}): an in-group click still shows
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 prefetch of the
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>,
@@ -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 prefetches: a
337
- * prefetch skips it and the click that adopts the prefetch fetches it in
338
- * one follow-up request (see {@link LoaderOptions}). Only on a segment the
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 prefetches
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 the whole unit: the
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