@rangojs/router 0.27.0 → 0.28.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 (212) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/__internal.d.ts +7 -1
  3. package/dist/types/browser/event-controller.d.ts +2 -1
  4. package/dist/types/browser/history-state.d.ts +3 -9
  5. package/dist/types/browser/navigation-store.d.ts +8 -6
  6. package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
  7. package/dist/types/browser/segment-reconciler.d.ts +6 -4
  8. package/dist/types/browser/types.d.ts +2 -1
  9. package/dist/types/browser/wire-protocol.d.ts +25 -0
  10. package/dist/types/cache/cache-key-utils.d.ts +11 -5
  11. package/dist/types/cache/cache-scope.d.ts +34 -22
  12. package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
  13. package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
  14. package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
  15. package/dist/types/cache/handle-snapshot.d.ts +9 -4
  16. package/dist/types/cache/segment-codec.d.ts +14 -0
  17. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  18. package/dist/types/cache/tag-invalidation.d.ts +10 -0
  19. package/dist/types/cache/taint.d.ts +5 -3
  20. package/dist/types/cache/types.d.ts +7 -3
  21. package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
  22. package/dist/types/prefetch-markers.d.ts +5 -14
  23. package/dist/types/prerender/store.d.ts +31 -0
  24. package/dist/types/prerender/warm-request.d.ts +3 -3
  25. package/dist/types/prerender.d.ts +2 -2
  26. package/dist/types/route-name.d.ts +5 -0
  27. package/dist/types/router/error-handling.d.ts +4 -4
  28. package/dist/types/router/instrument.d.ts +4 -3
  29. package/dist/types/router/intercept-resolution.d.ts +25 -9
  30. package/dist/types/router/match-context.d.ts +4 -11
  31. package/dist/types/router/match-handlers.d.ts +2 -6
  32. package/dist/types/router/match-middleware/background-revalidation.d.ts +10 -20
  33. package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
  34. package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
  35. package/dist/types/router/prerender-match.d.ts +21 -6
  36. package/dist/types/router/preview-match.d.ts +2 -7
  37. package/dist/types/router/request-classification.d.ts +5 -0
  38. package/dist/types/router/router-context.d.ts +7 -46
  39. package/dist/types/router/router-interfaces.d.ts +8 -14
  40. package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
  41. package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
  42. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
  43. package/dist/types/router/segment-resolution/resolution-pass.d.ts +93 -0
  44. package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
  45. package/dist/types/router/segment-resolution.d.ts +1 -2
  46. package/dist/types/router/segment-wrappers.d.ts +11 -48
  47. package/dist/types/router/telemetry.d.ts +2 -0
  48. package/dist/types/router/types.d.ts +10 -0
  49. package/dist/types/rsc/full-payload.d.ts +2 -2
  50. package/dist/types/rsc/handler-context.d.ts +6 -6
  51. package/dist/types/rsc/json-route-result.d.ts +5 -9
  52. package/dist/types/rsc/request-stages.d.ts +203 -0
  53. package/dist/types/rsc/response-cache-serve.d.ts +7 -12
  54. package/dist/types/rsc/response-route-handler.d.ts +2 -2
  55. package/dist/types/rsc/routine-plan.d.ts +1 -1
  56. package/dist/types/rsc/server-action.d.ts +2 -2
  57. package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
  58. package/dist/types/rsc/shell-capture.d.ts +132 -15
  59. package/dist/types/rsc/shell-serve.d.ts +37 -12
  60. package/dist/types/server/context.d.ts +9 -8
  61. package/dist/types/server/handle-store.d.ts +16 -0
  62. package/dist/types/server/render-pass.d.ts +72 -0
  63. package/dist/types/server/request-context.d.ts +24 -104
  64. package/dist/types/server/rewrite-trail.d.ts +2 -2
  65. package/dist/types/testing/dispatch.d.ts +35 -18
  66. package/dist/types/types/cache-types.d.ts +6 -3
  67. package/dist/types/types/error-types.d.ts +9 -1
  68. package/dist/types/types/handler-context.d.ts +9 -1
  69. package/dist/types/types/segments.d.ts +1 -1
  70. package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
  71. package/dist/types/vite/discovery/build-env.d.ts +22 -0
  72. package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
  73. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
  74. package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
  75. package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
  76. package/dist/types/vite/discovery/state.d.ts +12 -0
  77. package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
  78. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  79. package/dist/types/vite/router-discovery.d.ts +8 -47
  80. package/dist/types/vite/temp-rsc-server.d.ts +39 -0
  81. package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
  82. package/dist/types/wire-params.d.ts +34 -0
  83. package/dist/vite/index.js +3809 -3710
  84. package/package.json +1 -1
  85. package/skills/cache-guide/SKILL.md +8 -5
  86. package/skills/caching/SKILL.md +37 -18
  87. package/skills/document-cache/SKILL.md +13 -1
  88. package/skills/handler-use/SKILL.md +1 -1
  89. package/skills/intercept/SKILL.md +17 -7
  90. package/skills/middleware/SKILL.md +18 -0
  91. package/skills/ppr/SKILL.md +18 -3
  92. package/skills/prerender/SKILL.md +39 -15
  93. package/skills/rango/SKILL.md +5 -1
  94. package/skills/server-actions/SKILL.md +8 -1
  95. package/skills/testing/response-routes.md +6 -4
  96. package/skills/use-cache/SKILL.md +46 -8
  97. package/src/__internal.ts +7 -1
  98. package/src/browser/connection-warmup.ts +3 -1
  99. package/src/browser/event-controller.ts +5 -3
  100. package/src/browser/history-state.ts +6 -12
  101. package/src/browser/navigation-bridge.ts +14 -13
  102. package/src/browser/navigation-client.ts +18 -25
  103. package/src/browser/navigation-store.ts +25 -27
  104. package/src/browser/navigation-transaction.ts +4 -1
  105. package/src/browser/prefetch/cache.ts +2 -1
  106. package/src/browser/prefetch/fetch.ts +8 -16
  107. package/src/browser/prefetch/queue.ts +4 -4
  108. package/src/browser/react/NavigationProvider.tsx +2 -1
  109. package/src/browser/react/filter-segment-order.ts +2 -2
  110. package/src/browser/rsc-router.tsx +1 -0
  111. package/src/browser/segment-reconciler.ts +66 -53
  112. package/src/browser/server-action-bridge.ts +50 -57
  113. package/src/browser/types.ts +2 -1
  114. package/src/browser/wire-protocol.ts +41 -0
  115. package/src/cache/cache-key-utils.ts +11 -5
  116. package/src/cache/cache-runtime.ts +110 -50
  117. package/src/cache/cache-scope.ts +110 -64
  118. package/src/cache/cf/cf-cache-constants.ts +2 -2
  119. package/src/cache/cf/cf-cache-store.ts +486 -874
  120. package/src/cache/cf/cf-kv-utils.ts +1 -1
  121. package/src/cache/document-cache.ts +34 -13
  122. package/src/cache/handle-snapshot.ts +19 -7
  123. package/src/cache/segment-codec.ts +55 -0
  124. package/src/cache/shell-snapshot.ts +2 -2
  125. package/src/cache/tag-invalidation.ts +21 -2
  126. package/src/cache/taint.ts +5 -3
  127. package/src/cache/types.ts +7 -3
  128. package/src/cache/vercel/vercel-cache-store.ts +243 -310
  129. package/src/handles/pushed-by.ts +1 -1
  130. package/src/prefetch-markers.ts +5 -16
  131. package/src/prerender/store.ts +76 -0
  132. package/src/prerender/warm-request.ts +8 -4
  133. package/src/prerender.ts +2 -2
  134. package/src/route-name.ts +8 -0
  135. package/src/router/error-handling.ts +4 -4
  136. package/src/router/instrument.ts +4 -3
  137. package/src/router/intercept-resolution.ts +77 -75
  138. package/src/router/loader-resolution.ts +5 -4
  139. package/src/router/match-api.ts +4 -5
  140. package/src/router/match-context.ts +20 -12
  141. package/src/router/match-handlers.ts +8 -20
  142. package/src/router/match-middleware/background-revalidation.ts +41 -53
  143. package/src/router/match-middleware/cache-lookup.ts +675 -726
  144. package/src/router/match-middleware/cache-store.ts +4 -8
  145. package/src/router/match-middleware/intercept-resolution.ts +3 -23
  146. package/src/router/match-middleware/segment-resolution.ts +34 -61
  147. package/src/router/middleware.ts +69 -11
  148. package/src/router/navigation-snapshot.ts +4 -4
  149. package/src/router/prerender-match.ts +100 -44
  150. package/src/router/preview-match.ts +6 -41
  151. package/src/router/request-classification.ts +13 -7
  152. package/src/router/route-snapshot.ts +2 -5
  153. package/src/router/router-context.ts +10 -95
  154. package/src/router/router-interfaces.ts +8 -14
  155. package/src/router/segment-resolution/loader-cache.ts +14 -9
  156. package/src/router/segment-resolution/loader-mask.ts +5 -5
  157. package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
  158. package/src/router/segment-resolution/resolution-pass.ts +137 -0
  159. package/src/router/segment-resolution/revalidation.ts +897 -776
  160. package/src/router/segment-resolution.ts +4 -13
  161. package/src/router/segment-wrappers.ts +51 -269
  162. package/src/router/telemetry.ts +5 -0
  163. package/src/router/types.ts +10 -0
  164. package/src/router.ts +12 -13
  165. package/src/rsc/full-payload.ts +6 -5
  166. package/src/rsc/handler-context.ts +8 -14
  167. package/src/rsc/handler.ts +126 -507
  168. package/src/rsc/json-route-result.ts +5 -9
  169. package/src/rsc/loader-fetch.ts +6 -5
  170. package/src/rsc/progressive-enhancement.ts +5 -0
  171. package/src/rsc/request-stages.ts +757 -0
  172. package/src/rsc/response-cache-serve.ts +10 -14
  173. package/src/rsc/response-route-handler.ts +13 -5
  174. package/src/rsc/rewrite-passes.ts +2 -1
  175. package/src/rsc/routine-plan.ts +1 -1
  176. package/src/rsc/rsc-rendering.ts +164 -349
  177. package/src/rsc/server-action.ts +87 -113
  178. package/src/rsc/shell-capture-constants.ts +1 -1
  179. package/src/rsc/shell-capture.ts +212 -169
  180. package/src/rsc/shell-serve.ts +121 -18
  181. package/src/rsc/ssr-setup.ts +5 -4
  182. package/src/segment-system.tsx +3 -3
  183. package/src/server/context.ts +22 -25
  184. package/src/server/handle-store.ts +55 -0
  185. package/src/server/render-pass.ts +291 -0
  186. package/src/server/request-context.ts +26 -115
  187. package/src/server/rewrite-trail.ts +6 -4
  188. package/src/ssr/index.tsx +1 -1
  189. package/src/testing/dispatch.ts +223 -710
  190. package/src/testing/e2e/page-helpers.ts +2 -2
  191. package/src/testing/serve-shell-request.ts +23 -36
  192. package/src/types/cache-types.ts +6 -3
  193. package/src/types/error-types.ts +9 -1
  194. package/src/types/handler-context.ts +12 -1
  195. package/src/types/segments.ts +1 -1
  196. package/src/vite/discovery/build-discovery-phase.ts +204 -0
  197. package/src/vite/discovery/build-env.ts +148 -0
  198. package/src/vite/discovery/bundle-scan-phase.ts +128 -0
  199. package/src/vite/discovery/dev-prerender-cache.ts +8 -3
  200. package/src/vite/discovery/dev-server-phase.ts +1776 -0
  201. package/src/vite/discovery/discover-routers.ts +1 -1
  202. package/src/vite/discovery/discovery-errors.ts +1 -1
  203. package/src/vite/discovery/prerender-collection.ts +24 -39
  204. package/src/vite/discovery/state.ts +15 -0
  205. package/src/vite/plugins/version-plugin.ts +1 -1
  206. package/src/vite/plugins/virtual-entries.ts +1 -1
  207. package/src/vite/router-discovery.ts +29 -2428
  208. package/src/vite/temp-rsc-server.ts +223 -0
  209. package/src/vite/utils/prerender-utils.ts +47 -0
  210. package/src/wire-params.ts +35 -0
  211. package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
  212. package/src/router/segment-resolution/fresh.ts +0 -945
@@ -33,8 +33,9 @@ import {
33
33
  // pulls @vitejs/plugin-rsc (a virtual: module the plain node/vitest runner cannot
34
34
  // resolve). It is imported LAZILY at the two call sites below (deserializeSegments
35
35
  // in lookupRoute, serializeSegments in cacheRoute) so that requiring cache-scope —
36
- // e.g. dispatch's lazy `import("../cache/cache-scope.js")` for the response-route
37
- // cache path — does not crash a consumer test that never mocks plugin-rsc. Behavior
36
+ // e.g. through the request stages dispatch loads for a response route
37
+ // (rsc/request-stages.ts) — does not crash a consumer test that never mocks
38
+ // plugin-rsc. Behavior
38
39
  // is unchanged: both methods are async and already awaited the codec.
39
40
  import {
40
41
  captureHandles,
@@ -54,14 +55,16 @@ import {
54
55
  import type { RequestContext } from "../server/request-context.js";
55
56
  import { runIdentityExempt } from "./cache-exec-scope.js";
56
57
  import { isWarmReplace, noteWarmWrite } from "../prerender/warm-request.js";
58
+ import { WIRE } from "../wire-params.js";
57
59
 
58
60
  /**
59
- * Narrow the request's route-record window (RequestContext._routeRecordWindow)
60
- * to a record this request read or wrote: fresh until `freshUntil` (the
61
- * record's `expiresAt`), then stale for `swr` seconds. A shell capture starts
62
- * with no window of its own (shell-capture.ts deriveShellCaptureContext sets
63
- * it undefined), so it narrows only to the records the capture itself read
64
- * or wrote.
61
+ * Narrow a shell capture's route-record window
62
+ * (ShellCaptureState.routeRecordWindow) to a record the capture read or
63
+ * wrote: fresh until `freshUntil` (the record's `expiresAt`), then stale for
64
+ * `swr` seconds. A capture starts with no window of its own
65
+ * (RENDER_PASS_FIELDS["shell-capture"] creates it undefined), so it narrows
66
+ * only to the records the capture itself read or wrote. Only a capture reads
67
+ * the window (shell-capture.ts capShellWindow): another render notes nothing.
65
68
  */
66
69
  function noteRouteRecordWindow(
67
70
  requestCtx: RequestContext,
@@ -70,6 +73,8 @@ function noteRouteRecordWindow(
70
73
  swr: number,
71
74
  written: boolean,
72
75
  ): void {
76
+ const captureState = requestCtx._shellCaptureState;
77
+ if (!captureState) return;
73
78
  const next = {
74
79
  freshUntil,
75
80
  staleUntil: freshUntil + swr * 1000,
@@ -77,9 +82,9 @@ function noteRouteRecordWindow(
77
82
  swr,
78
83
  written,
79
84
  };
80
- const current = requestCtx._routeRecordWindow;
85
+ const current = captureState.routeRecordWindow;
81
86
  // The record that runs out first names the config; freshness is the least.
82
- requestCtx._routeRecordWindow = current
87
+ captureState.routeRecordWindow = current
83
88
  ? {
84
89
  ...(current.staleUntil <= next.staleUntil ? current : next),
85
90
  freshUntil: Math.min(current.freshUntil, freshUntil),
@@ -123,23 +128,36 @@ function validatedSwr(value: number | undefined): number | undefined {
123
128
  return isValidCacheSeconds(value, "swr") ? value : undefined;
124
129
  }
125
130
 
131
+ /**
132
+ * The request kind a route record answers: `doc` (a document request),
133
+ * `partial` (a navigation) or `intercept:<intercept>`, where `intercept` is
134
+ * the intercept an intercept navigation selected (interceptRecordKey,
135
+ * router/intercept-resolution.ts). The kinds' records hold different
136
+ * segments (an intercept's holds its slot and no route page), so every record
137
+ * key names its kind, a `key()`'s included (CacheScope.resolveKeyFrom).
138
+ * Keyed by the URL alone, a record written under one intercept answered a
139
+ * navigation that selected another; keyed by a `key()` alone, a document GET
140
+ * rendered an intercept's record and dropped the page.
141
+ */
142
+ function routeRecordKind(intercept?: string, prefixOverride?: "doc"): string {
143
+ if (intercept !== undefined) return `intercept:${intercept}`;
144
+ if (prefixOverride !== undefined) return prefixOverride;
145
+ const isPartial =
146
+ getRequestContext()?.originalUrl?.searchParams.has(WIRE.partial) ?? false;
147
+ return isPartial ? "partial" : "doc";
148
+ }
149
+
150
+ /** `{kind}:` + the request's key base (routeRecordKind, requestKeyBase). */
126
151
  function getDefaultRouteCacheKey(
127
152
  pathname: string,
128
- params?: Record<string, string>,
129
- isIntercept?: boolean,
130
- prefixOverride?: "doc",
153
+ params: Record<string, string> | undefined,
154
+ kind: string,
131
155
  ): string {
132
156
  const ctx = getRequestContext();
133
- const isPartial = ctx?.originalUrl?.searchParams.has("_rsc_partial") ?? false;
134
157
  const searchParams = ctx?.url.searchParams;
135
158
  const host = ctx?.url.host ?? "localhost";
136
159
 
137
- // Intercept navigations get their own cache namespace
138
- const prefix = isIntercept
139
- ? "intercept"
140
- : (prefixOverride ?? (isPartial ? "partial" : "doc"));
141
-
142
- return `${prefix}:${requestKeyBase(ctx, host, pathname, searchParams, params, ctx?._searchParamsFilter)}`;
160
+ return `${kind}:${requestKeyBase(ctx, host, pathname, searchParams, params, ctx?._searchParamsFilter)}`;
143
161
  }
144
162
 
145
163
  // ============================================================================
@@ -236,15 +254,23 @@ function memoizedKey(
236
254
  return key;
237
255
  }
238
256
 
257
+ /**
258
+ * A route record that answered. `restoreHandles` is set when the lookup left
259
+ * the handle restore to its caller (lookupRouteDetailed `deferHandles`),
260
+ * which names the segments it sends (handle-snapshot.ts restoreHandles).
261
+ */
262
+ export interface CachedRouteHit {
263
+ segments: ResolvedSegment[];
264
+ shouldRevalidate: boolean;
265
+ restoreHandles?: (sentIds: ReadonlySet<string>) => Promise<void>;
266
+ }
267
+
239
268
  /**
240
269
  * Discriminated outcome of a route cache lookup — see
241
270
  * {@link CacheScope.lookupRouteDetailed} for what each status licenses.
242
271
  */
243
272
  export type CacheRouteLookupOutcome =
244
- | {
245
- status: "hit";
246
- result: { segments: ResolvedSegment[]; shouldRevalidate: boolean };
247
- }
273
+ | { status: "hit"; result: CachedRouteHit }
248
274
  | { status: "miss" | "bypass" | "error" };
249
275
 
250
276
  /**
@@ -455,15 +481,13 @@ export class CacheScope {
455
481
  private resolveKey(
456
482
  pathname: string,
457
483
  params: Record<string, string>,
458
- isIntercept?: boolean,
484
+ intercept?: string,
459
485
  ): Promise<string> {
486
+ const kind = routeRecordKind(intercept, this.defaultKeyPrefix);
460
487
  return this.resolveKeyFrom(
461
- getDefaultRouteCacheKey(
462
- pathname,
463
- params,
464
- isIntercept,
465
- this.defaultKeyPrefix,
466
- ),
488
+ getDefaultRouteCacheKey(pathname, params, kind),
489
+ "",
490
+ kind,
467
491
  );
468
492
  }
469
493
 
@@ -500,14 +524,18 @@ export class CacheScope {
500
524
  * enclosing partition (#970, #974). A scope whose own config sets no
501
525
  * `key()` appends its own default key (resolveDefaultKey). Reusing the
502
526
  * outer `key()` result alone would give every route under the inner
503
- * cache() one record: a `key()` replaces the whole default key, so an
504
- * outer key that names no route (`tier:gold`) would make `/a` HIT `/b`'s
505
- * record. A scope with its own `key()` adds no default key: a `key()` is a
506
- * full override, and its store's keyGenerator does not run. With no
507
- * partition part it is resolveDefaultKey alone. With a `key()` on the
508
- * chain but no request context it rejects (keyWithoutContextError).
509
- * `partitionedPrefix` prefixes a key built from partition parts only (the
510
- * response family's `response:`).
527
+ * cache() one record: a `key()` replaces the default key's URL part, so
528
+ * an outer key that names no route (`tier:gold`) would make `/a` HIT
529
+ * `/b`'s record. A scope with its own `key()` adds no default key: its `key()`
530
+ * replaces the URL part, and its store's keyGenerator does not run. It
531
+ * appends `kind` instead (routeRecordKind): the `key()` decides which
532
+ * 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
535
+ * context it rejects (keyWithoutContextError). `partitionedPrefix`
536
+ * prefixes a key built from partition parts only (the response family's
537
+ * `response:`, which passes no `kind`: a response route answers every
538
+ * request alike).
511
539
  *
512
540
  * Each `key()` result lives on the request context (`_resolvedCacheKeys`),
513
541
  * by function: it runs once per request whatever the default key
@@ -518,7 +546,11 @@ export class CacheScope {
518
546
  * itself: a `key()` calling `cookies()` would trip the capture guard there,
519
547
  * and a capture must store the partition its request resolved.
520
548
  */
521
- resolveKeyFrom(defaultKey: string, partitionedPrefix = ""): Promise<string> {
549
+ resolveKeyFrom(
550
+ defaultKey: string,
551
+ partitionedPrefix = "",
552
+ kind?: string,
553
+ ): Promise<string> {
522
554
  if (this.fixedKey !== undefined) return Promise.resolve(this.fixedKey);
523
555
  const requestCtx = _getRequestContext();
524
556
  if (!requestCtx || !this.mayPartition) {
@@ -534,7 +566,8 @@ export class CacheScope {
534
566
  ]).then(([[keys, storeParts], ownDefault]) => {
535
567
  const generated = positionalParts(storeParts);
536
568
  if (keys.length === 0 && generated.length === 0) return ownDefault!;
537
- if (ownDefault !== undefined) generated.push(ownDefault);
569
+ const last = ownDefault ?? kind;
570
+ if (last !== undefined) generated.push(last);
538
571
  return partitionedPrefix + composeCacheKeys(keys, generated);
539
572
  });
540
573
  }
@@ -558,7 +591,7 @@ export class CacheScope {
558
591
  ? undefined
559
592
  : this.getStore()?.keyGenerator;
560
593
  if (!this.mayPartition && !keyGenerator) return undefined;
561
- const defaultKey = getDefaultRouteCacheKey(pathname, params, false, "doc");
594
+ const defaultKey = getDefaultRouteCacheKey(pathname, params, "doc");
562
595
  const requestCtx = _getRequestContext();
563
596
  if (!requestCtx) {
564
597
  return this.keyFns.length > 0
@@ -735,24 +768,26 @@ export class CacheScope {
735
768
  *
736
769
  * @param pathname - URL pathname for cache key generation
737
770
  * @param params - Route params for cache key generation
738
- * @param isIntercept - Whether this is an intercept navigation (uses different cache key)
771
+ * @param intercept - The intercept an intercept navigation selected
772
+ * (interceptRecordKey): its record has its own key
739
773
  * @param ownedPushes - What the record is to each loader's handle pushes
740
774
  * (withCacheLookup: loader-cache.ts loaderPins), called on a hit
775
+ * @param deferHandles - Restore no handle value here: the caller calls the
776
+ * hit's `restoreHandles` once it decided which segments it sends
741
777
  */
742
778
  async lookupRoute(
743
779
  pathname: string,
744
780
  params: Record<string, string>,
745
- isIntercept?: boolean,
781
+ intercept?: string,
746
782
  ownedPushes?: () => OwnedPushDelivery,
747
- ): Promise<{
748
- segments: ResolvedSegment[];
749
- shouldRevalidate: boolean;
750
- } | null> {
783
+ deferHandles?: boolean,
784
+ ): Promise<CachedRouteHit | null> {
751
785
  const outcome = await this.lookupRouteDetailed(
752
786
  pathname,
753
787
  params,
754
- isIntercept,
788
+ intercept,
755
789
  ownedPushes,
790
+ deferHandles,
756
791
  );
757
792
  return outcome.status === "hit" ? outcome.result : null;
758
793
  }
@@ -778,8 +813,9 @@ export class CacheScope {
778
813
  async lookupRouteDetailed(
779
814
  pathname: string,
780
815
  params: Record<string, string>,
781
- isIntercept?: boolean,
816
+ intercept?: string,
782
817
  ownedPushes?: () => OwnedPushDelivery,
818
+ deferHandles?: boolean,
783
819
  ): Promise<CacheRouteLookupOutcome> {
784
820
  if (!this.enabled) return { status: "bypass" };
785
821
  if (!this.conditionAllows("read")) return { status: "bypass" };
@@ -801,7 +837,7 @@ export class CacheScope {
801
837
  // happens here, where a miss is a safe outcome.
802
838
  let key: string | undefined;
803
839
  try {
804
- key = await this.resolveKey(pathname, params, isIntercept);
840
+ key = await this.resolveKey(pathname, params, intercept);
805
841
 
806
842
  const result = await store.get(key);
807
843
 
@@ -882,17 +918,23 @@ export class CacheScope {
882
918
  await this.onHit?.();
883
919
 
884
920
  const handleStore = _getRequestContext()?._handleStore;
921
+ let restoreHandles: CachedRouteHit["restoreHandles"];
885
922
  // An empty handles string means the route pushed none (the common
886
923
  // case); a decode failure skips the restore but keeps the segments.
887
924
  if (handleStore) {
888
- await restoreRecordHandles(
889
- handleStore,
890
- cached,
891
- ownedPushes?.(),
925
+ const owned = ownedPushes?.();
926
+ const keepIds =
892
927
  this.boundary !== undefined
893
928
  ? new Set(segments.map((s) => s.id))
894
- : undefined,
895
- );
929
+ : undefined;
930
+ if (deferHandles) {
931
+ // The authority holds from here, as with the restore below.
932
+ if (owned) handleStore.setRecordAuthority(owned);
933
+ restoreHandles = (sent) =>
934
+ restoreRecordHandles(handleStore, cached, owned, keepIds, sent);
935
+ } else {
936
+ await restoreRecordHandles(handleStore, cached, owned, keepIds);
937
+ }
896
938
  }
897
939
 
898
940
  if (INTERNAL_RANGO_DEBUG) {
@@ -907,7 +949,10 @@ export class CacheScope {
907
949
  if (ambientContext)
908
950
  this.noteRecordWindow(ambientContext, cached.expiresAt, false);
909
951
 
910
- return { status: "hit", result: { segments, shouldRevalidate } };
952
+ return {
953
+ status: "hit",
954
+ result: { segments, shouldRevalidate, restoreHandles },
955
+ };
911
956
  } catch (error) {
912
957
  // Covers a store.get() failure AND a throwing consumer key()/keyGenerator
913
958
  // (resolveKey). Either way degrade to an uncached render — reported as
@@ -969,7 +1014,8 @@ export class CacheScope {
969
1014
  * @param pathname - URL pathname for cache key generation
970
1015
  * @param params - Route params for cache key generation
971
1016
  * @param segments - All resolved segments to cache
972
- * @param isIntercept - Whether this is an intercept navigation (uses different cache key)
1017
+ * @param intercept - The intercept an intercept navigation selected
1018
+ * (interceptRecordKey): its record has its own key
973
1019
  * @param start - Where the render of `segments` started: the record is not
974
1020
  * written when one of its tags was invalidated since (#977,
975
1021
  * predatesInvalidation). A shell capture's doc record passes none: it
@@ -979,7 +1025,7 @@ export class CacheScope {
979
1025
  pathname: string,
980
1026
  params: Record<string, string>,
981
1027
  segments: ResolvedSegment[],
982
- isIntercept?: boolean,
1028
+ intercept?: string,
983
1029
  start?: ExecutionStart,
984
1030
  ): Promise<void> {
985
1031
  if (!this.enabled || segments.length === 0) return;
@@ -1006,7 +1052,7 @@ export class CacheScope {
1006
1052
  this.noteRecordWindow(requestCtx, Date.now() + ttl * 1000, true);
1007
1053
 
1008
1054
  // Resolve cache key early (while request context is available)
1009
- const key = await this.resolveKey(pathname, params, isIntercept);
1055
+ const key = await this.resolveKey(pathname, params, intercept);
1010
1056
 
1011
1057
  // Doc-namespaced scopes (the shell implicit scope and the capture's
1012
1058
  // composed doc scope) publish the canonical document segment key so
@@ -1022,7 +1068,7 @@ export class CacheScope {
1022
1068
  recordRequestTags(tags, requestCtx);
1023
1069
 
1024
1070
  // Check if this is a partial request (navigation) vs document request
1025
- const isPartial = requestCtx.originalUrl.searchParams.has("_rsc_partial");
1071
+ const isPartial = requestCtx.originalUrl.searchParams.has(WIRE.partial);
1026
1072
 
1027
1073
  if (INTERNAL_RANGO_DEBUG) {
1028
1074
  debugCacheLog(
@@ -1204,7 +1250,7 @@ export function resolveShellPartition(
1204
1250
  ): Promise<string | null> | undefined {
1205
1251
  if (routeScope?.enabled) return routeScope.resolvePartition(pathname, params);
1206
1252
  if (!appStore?.keyGenerator) return undefined;
1207
- const defaultKey = getDefaultRouteCacheKey(pathname, params, false, "doc");
1253
+ const defaultKey = getDefaultRouteCacheKey(pathname, params, "doc");
1208
1254
  return resolveCacheKey(undefined, appStore, defaultKey).then((resolved) =>
1209
1255
  resolved === defaultKey ? null : resolved,
1210
1256
  );
@@ -1259,7 +1305,7 @@ export function createShellImplicitDocScope(
1259
1305
  * normal partial navigation replay), substitute an implicit doc-level scope
1260
1306
  * so withCacheLookup/withCacheStore treat the WHOLE matched route as a
1261
1307
  * cache() boundary — the shell entry IS a cache() of the handler layer, with
1262
- * loaders as the live carve-outs (resolveFreshLoadersAndYield).
1308
+ * loaders as the live carve-outs (serveStoredSegments).
1263
1309
  *
1264
1310
  * A document HIT tail (`docTail`) always gets the implicit scope: it replays
1265
1311
  * the shell's own record and never consults a route-derived scope, whose
@@ -55,14 +55,14 @@ export const CACHE_REVALIDATING_AT_HEADER = "x-edge-cache-revalidating-at";
55
55
  * re-arm re-puts the full window and restarts CF's retention clock, pinning a
56
56
  * perpetually-stale entry (one whose background revalidation keeps failing) past
57
57
  * its intended hard-expiry indefinitely. Mirrors the KVSegmentEnvelope `e`
58
- * field and the remaining-ttl math in promoteSegmentToL1/promoteItemToL1.
58
+ * field and the remaining-ttl math in promoteToL1.
59
59
  * @internal
60
60
  */
61
61
  export const CACHE_EXPIRES_AT_HEADER = "x-edge-cache-expires-at";
62
62
 
63
63
  /**
64
64
  * Header stashing the route author's original Cache-Control on L1 document
65
- * entries. putResponse/promoteResponseToL1 overwrite Cache-Control with a long
65
+ * entries. putResponse/promoteToL1 overwrite Cache-Control with a long
66
66
  * `max-age` so the CF Cache API retains the entry across the whole SWR window;
67
67
  * getResponse restores this original value before serving so the client and any
68
68
  * upstream CDN see the author's intended directive, not the internal edge TTL.