@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
@@ -71,7 +71,7 @@ export async function kvKeyDigest(value: string): Promise<string> {
71
71
  * of restarting it -- copying set()'s original full-window max-age would reset
72
72
  * CF's retention clock on every re-arm and pin a perpetually-stale entry forever.
73
73
  * An entry lacking a valid deadline (legacy/tampered) floors to max-age=1, so it
74
- * hard-expires in ~1s and self-heals via KV. Mirrors promoteSegmentToL1's math.
74
+ * hard-expires in ~1s and self-heals via KV. Mirrors promoteToL1's math.
75
75
  * @internal
76
76
  */
77
77
  export function remainingCacheControl(headers: Headers, now: number): string {
@@ -14,17 +14,18 @@
14
14
  */
15
15
 
16
16
  import { requestHeaders } from "../server/request-headers.js";
17
- import { FILL_PARAM, STALE_PARAM } from "../prefetch-markers.js";
18
17
  import type { MiddlewareFn, MiddlewareContext } from "../router/middleware.js";
19
18
  import {
20
19
  documentCacheStoresRender,
21
20
  shouldCacheResponse,
22
21
  } from "./document-cache-policy.js";
23
22
  import {
23
+ createRequestContext,
24
24
  getRequestContext,
25
25
  runWithRequestContext,
26
26
  type RequestContext,
27
27
  } from "../server/request-context.js";
28
+ import { deriveRenderPass } from "../server/render-pass.js";
28
29
  import { mayNeedSSR } from "../rsc/ssr-setup.js";
29
30
  import { requestKeyBase } from "./cache-key-utils.js";
30
31
  import { runBackground } from "./background-task.js";
@@ -45,6 +46,7 @@ import {
45
46
  SEGMENT_FRAGMENT_CAPABILITY_HEADER,
46
47
  SEGMENT_FRAGMENT_RECOVERY_HEADER,
47
48
  } from "../segment-fragments.js";
49
+ import { WIRE } from "../wire-params.js";
48
50
 
49
51
  export { documentCacheStoresRender };
50
52
 
@@ -229,12 +231,12 @@ export function createDocumentCacheMiddleware<TEnv = any>(
229
231
  }
230
232
 
231
233
  // Skip RSC action requests (mutations shouldn't be cached)
232
- if (rawUrl.searchParams.has("_rsc_action")) {
234
+ if (rawUrl.searchParams.has(WIRE.action)) {
233
235
  return next();
234
236
  }
235
237
 
236
238
  // Skip loader requests (have their own caching)
237
- if (rawUrl.searchParams.has("_rsc_loader")) {
239
+ if (rawUrl.searchParams.has(WIRE.loader)) {
238
240
  return next();
239
241
  }
240
242
 
@@ -245,9 +247,9 @@ export function createDocumentCacheMiddleware<TEnv = any>(
245
247
  // the key cannot see. Partial only: _rsc params leave the key, so a
246
248
  // document GET carrying them must not force an origin render.
247
249
  if (
248
- rawUrl.searchParams.has("_rsc_partial") &&
249
- (rawUrl.searchParams.has(FILL_PARAM) ||
250
- rawUrl.searchParams.get(STALE_PARAM) === "true")
250
+ rawUrl.searchParams.has(WIRE.partial) &&
251
+ (rawUrl.searchParams.has(WIRE.fill) ||
252
+ rawUrl.searchParams.get(WIRE.stale) === "true")
251
253
  ) {
252
254
  return next();
253
255
  }
@@ -283,7 +285,7 @@ export function createDocumentCacheMiddleware<TEnv = any>(
283
285
  // Determine request type for cache key differentiation.
284
286
  // Uses rawUrl for _rsc* param checks and mayNeedSSR for Accept-based
285
287
  // detection. Full-document RSC fetches must not share the HTML cache slot.
286
- const isPartial = rawUrl.searchParams.has("_rsc_partial");
288
+ const isPartial = rawUrl.searchParams.has(WIRE.partial);
287
289
  const isRscRequest = !mayNeedSSR(ctx.request, rawUrl);
288
290
  const typeLabel = isRscRequest ? "RSC" : "HTML";
289
291
 
@@ -296,7 +298,7 @@ export function createDocumentCacheMiddleware<TEnv = any>(
296
298
  // gracefully to the origin handler instead of rejecting the request.
297
299
  // This is a deliberate fail-open-to-origin policy: the fallback is
298
300
  // "serve uncached from origin", not "use a different cache key".
299
- const clientSegments = rawUrl.searchParams.get("_rsc_segments") || "";
301
+ const clientSegments = rawUrl.searchParams.get(WIRE.segments) || "";
300
302
  const segmentHash =
301
303
  isPartial && clientSegments ? `:${hashSegmentIds(clientSegments)}` : "";
302
304
  // Fragment envelopes require a capable Rango decoder. Keep that wire
@@ -359,6 +361,23 @@ export function createDocumentCacheMiddleware<TEnv = any>(
359
361
  `[DocumentCache] STALE ${typeLabel}: ${url.pathname} (revalidating)`,
360
362
  );
361
363
 
364
+ // The re-run renders on its own pass (render-pass.ts). Its response
365
+ // writes go to a sink that starts from this request's response stub
366
+ // as of now, as a MISS's render would: they build the re-run's own
367
+ // response, never the visitor's, and the visitor's later writes stay
368
+ // off it.
369
+ const rerunCtx = deriveRenderPass(requestCtx, "document-revalidation", {
370
+ handleStore: requestCtx._handleStore,
371
+ sink: createRequestContext({
372
+ env: ctx.env,
373
+ request: ctx.request,
374
+ url,
375
+ variables: {},
376
+ initialResponse: requestCtx.res,
377
+ themeConfig: requestCtx._themeConfig,
378
+ }),
379
+ });
380
+
362
381
  runBackground(requestCtx, async () => {
363
382
  try {
364
383
  // Re-establish the request-context ALS around the whole background
@@ -372,13 +391,15 @@ export function createDocumentCacheMiddleware<TEnv = any>(
372
391
  // platform spans — nest under one explanatory parent instead of
373
392
  // dangling under the ended foreground phases. Running the put
374
393
  // inside the ALS matches the MISS path, where putResponse already
375
- // executes within the foreground request context.
376
- await runWithRequestContext(requestCtx, () =>
394
+ // executes within the foreground request context. No visitor made
395
+ // the invalidations the re-run makes: the pass's fresh-reads
396
+ // writer is the sink's, which sets nothing.
397
+ await runWithRequestContext(rerunCtx, () =>
377
398
  observePhase(
378
399
  PHASES.background("document-revalidation"),
379
400
  async () => {
380
401
  const fresh = await next();
381
- const directives = shouldCacheResponse(fresh, requestCtx);
402
+ const directives = shouldCacheResponse(fresh, rerunCtx);
382
403
 
383
404
  if (!directives || !fresh.body) return;
384
405
 
@@ -386,10 +407,10 @@ export function createDocumentCacheMiddleware<TEnv = any>(
386
407
  // drain the fresh render fully before snapshotting tags
387
408
  // (same render-complete barrier as the miss path).
388
409
  const body = await new Response(fresh.body).arrayBuffer();
389
- throwIfRenderErrored(requestCtx);
410
+ throwIfRenderErrored(rerunCtx);
390
411
  // Not written when one of its tags was invalidated since
391
412
  // the render started (#977).
392
- const tags = collectRequestTags(requestCtx);
413
+ const tags = collectRequestTags(rerunCtx);
393
414
  if (await predatesInvalidation(store, tags, start)) return;
394
415
  await store.putResponse!(
395
416
  cacheKey,
@@ -14,7 +14,7 @@ import type { HandleOwners, SegmentHandleData } from "./types.js";
14
14
  // in plain node/vitest). It is imported LAZILY inside the two async encode/decode
15
15
  // helpers below so that modules which import handle-snapshot only for the
16
16
  // plugin-rsc-free captureHandles/restoreHandles (e.g. cache-scope, on dispatch's
17
- // lazy response-route cache path) do not pull plugin-rsc at module load. Behavior
17
+ // response-route path) do not pull plugin-rsc at module load. Behavior
18
18
  // is unchanged: both helpers are async and already awaited the codec.
19
19
 
20
20
  const HANDLE_ENCODE_TIMEOUT_MS = 5000;
@@ -100,7 +100,7 @@ export async function decodeHandleValue<T>(encoded: string): Promise<T | null> {
100
100
  *
101
101
  * DSL-loader pushes stay out of the CACHE WRITE: a HIT runs loaders exactly
102
102
  * as an uncached render of the same request would (cache-lookup.ts
103
- * resolveFreshLoadersAndYield), so replaying a recorded copy would duplicate
103
+ * serveStoredSegments), so replaying a recorded copy would duplicate
104
104
  * the live push. Handler pushes, including
105
105
  * handler-invoked ctx.use(Loader) bodies (skipped with their handler on a
106
106
  * HIT), are kept. The store tags loader pushes by array position at push
@@ -179,29 +179,39 @@ export type OwnedPushDelivery = (loaderId: string) => RecordAuthority;
179
179
  * owner. Neither claims its loader, so the loader's own cache() HIT still
180
180
  * delivers (the pushes holding a promise a restored copy lets through, or its
181
181
  * entry's pushes in a placeholder's place).
182
+ *
183
+ * `sent` (a partial request) names the segments the response sends. Any
184
+ * other segment is kept with its handler's values, as on a live walk: only
185
+ * its loaders' copies are restored, which follow their loader as above.
182
186
  */
183
187
  export function restoreHandles(
184
188
  handles: Record<string, SegmentHandleData>,
185
189
  handleStore: HandleStore,
186
190
  owners?: HandleOwners,
187
191
  owned?: OwnedPushDelivery,
192
+ sent?: ReadonlySet<string>,
188
193
  ): void {
189
194
  for (const [segId, segHandles] of Object.entries(handles)) {
190
195
  if (Object.keys(segHandles).length === 0) continue;
191
196
  const segOwners = owners?.[segId];
197
+ const kept = sent !== undefined && !sent.has(segId);
192
198
  if (!segOwners) {
193
- handleStore.replaySegmentData(segId, segHandles);
199
+ if (!kept) handleStore.replaySegmentData(segId, segHandles);
194
200
  continue;
195
201
  }
196
202
  const cleared: SegmentHandleData = {};
197
- for (const handleName in segHandles) cleared[handleName] = [];
203
+ for (const handleName in segHandles) {
204
+ if (!kept || segOwners[handleName]?.some(Boolean)) {
205
+ cleared[handleName] = [];
206
+ }
207
+ }
198
208
  handleStore.replaySegmentData(segId, cleared);
199
209
  for (const [handleName, values] of Object.entries(segHandles)) {
200
210
  const valueOwners = segOwners[handleName];
201
211
  for (let i = 0; i < values.length; i++) {
202
212
  const owner = valueOwners?.[i];
203
213
  if (!owner) {
204
- handleStore.push(handleName, segId, values[i]);
214
+ if (!kept) handleStore.push(handleName, segId, values[i]);
205
215
  continue;
206
216
  }
207
217
  const authority = owned?.(owner);
@@ -220,13 +230,15 @@ export function restoreHandles(
220
230
  * (with or without a handles blob: a pinned loader whose capture pushed
221
231
  * nothing has no copy, and its run must still add no settled push), then
222
232
  * decode and restoreHandles. `keepIds` limits the restore to the segments a
223
- * boundary-scoped scope serves. A decode failure skips the restore.
233
+ * boundary-scoped scope serves; `sent` is restoreHandles'. A decode failure
234
+ * skips the restore.
224
235
  */
225
236
  export async function restoreRecordHandles(
226
237
  handleStore: HandleStore,
227
238
  record: { handles?: string; handleOwners?: HandleOwners },
228
239
  owned: OwnedPushDelivery | undefined,
229
240
  keepIds?: ReadonlySet<string>,
241
+ sent?: ReadonlySet<string>,
230
242
  ): Promise<void> {
231
243
  if (owned) handleStore.setRecordAuthority(owned);
232
244
  if (!record.handles) return;
@@ -237,7 +249,7 @@ export async function restoreRecordHandles(
237
249
  if (!keepIds.has(id)) delete decoded[id];
238
250
  }
239
251
  }
240
- restoreHandles(decoded, handleStore, record.handleOwners, owned);
252
+ restoreHandles(decoded, handleStore, record.handleOwners, owned, sent);
241
253
  }
242
254
 
243
255
  /**
@@ -129,6 +129,61 @@ export async function serializeResult(
129
129
  }
130
130
  }
131
131
 
132
+ /** The Flight encode of a value and a decoded copy of it (teeResult). */
133
+ export interface TeedResult {
134
+ /** The copy: settles when the root row arrives; later rows stream in. */
135
+ decoded: Promise<unknown>;
136
+ /** The whole encoded text, or null when the stream failed. */
137
+ serialized: Promise<string | null>;
138
+ }
139
+
140
+ /**
141
+ * Encode `value` once and decode a copy of it as the encode streams, so a
142
+ * "use cache" miss renders the copy as a hit renders a stored entry, and
143
+ * stores the same bytes. Null when the encode could not start.
144
+ */
145
+ export function teeResult(
146
+ value: unknown,
147
+ onError?: (error: unknown) => void,
148
+ ): TeedResult | null {
149
+ try {
150
+ const stream = renderToReadableStream(value, {
151
+ temporaryReferences: createTemporaryReferenceSet(),
152
+ onError,
153
+ });
154
+ const [toCopy, toStore] = stream.tee();
155
+ const decoded = Promise.resolve(
156
+ createFromReadableStream(
157
+ toCopy,
158
+ { temporaryReferences: createTemporaryReferenceSet() },
159
+ PRESERVE_SERVER_REFS,
160
+ ),
161
+ );
162
+ // Awaited by the caller only after the write is scheduled.
163
+ decoded.catch(() => {});
164
+ return { decoded, serialized: streamToString(toStore).catch(() => null) };
165
+ } catch (error) {
166
+ if (INTERNAL_RANGO_DEBUG) {
167
+ console.warn("[segment-codec] teeResult failed:", error);
168
+ }
169
+ return null;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Flight's refusal of a value it cannot encode (a function, an `on*`
175
+ * function, a class instance, a local symbol), by the message its server
176
+ * throws; the development build words a function in a children position on
177
+ * its own. A component that throws, or a "use cache" guard refusal, is not
178
+ * one.
179
+ */
180
+ const UNSUPPORTED_VALUE =
181
+ /^(Only plain objects|Functions cannot be passed directly to Client Components|Functions are not valid as a child of Client Components|Event handlers cannot be passed to Client Component props|Only global symbols)/;
182
+
183
+ export function isUnsupportedValueError(error: unknown): boolean {
184
+ return error instanceof Error && UNSUPPORTED_VALUE.test(error.message);
185
+ }
186
+
132
187
  /**
133
188
  * RSC-deserialize a cached result string.
134
189
  * Counterpart to serializeResult() — always receives a non-empty string.
@@ -383,7 +383,7 @@ export interface ShellLoaderSeedEntry {
383
383
 
384
384
  /**
385
385
  * A pin's loader. A pin is stored under its loader segment id,
386
- * `${shortCode}D${index}.${loaderId}` (fresh.ts resolveLoaders), and a
386
+ * `${shortCode}D${index}.${loaderId}` (resolveOwnerLoaders), and a
387
387
  * shortCode holds no "D" and no "." (server/context.ts getShortCode: M, L, P,
388
388
  * R, C, I and digits), so the first `D<index>.` ends it. A loader id can hold
389
389
  * both.
@@ -462,7 +462,7 @@ export const SHELL_HANDLES_RECORD_KEY = "handles";
462
462
 
463
463
  /**
464
464
  * The `handles` record of a prerender-served entry, which a HIT tail restores
465
- * after the prerender store's handles (cache-lookup.ts yieldFromStore), or
465
+ * after the prerender store's handles (cache-lookup.ts decodeStoredEntry), or
466
466
  * undefined: an entry with a doc record, or one written before the record
467
467
  * existed (v0.21), which serves as it did.
468
468
  */
@@ -36,7 +36,12 @@
36
36
  * predatesInvalidation, and docs/design/caching.md "Read-your-own-writes".
37
37
  */
38
38
 
39
- import { _getRequestContext } from "../server/request-context.js";
39
+ import { AsyncLocalStorage } from "node:async_hooks";
40
+ import {
41
+ _getRequestContext,
42
+ runWithRequestContext,
43
+ type RequestContext,
44
+ } from "../server/request-context.js";
40
45
  import { reportingAsync } from "./cache-error.js";
41
46
  import { normalizeTags } from "./cache-tag.js";
42
47
  import type { SegmentCacheStore } from "./types.js";
@@ -129,6 +134,20 @@ function warnPartialTagStore(fn: string, incapable: number): void {
129
134
  );
130
135
  }
131
136
 
137
+ const backgroundRefresh = new AsyncLocalStorage<true>();
138
+
139
+ /**
140
+ * Run a stale entry's background refresh on the visitor's request context: a
141
+ * "use cache" function's (cache-runtime.ts) or a cached loader's
142
+ * (loader-cache.ts). No visitor made the invalidations in it, so they set no
143
+ * fresh-reads cookie, as in a stale cache() HIT's re-render or a document
144
+ * cache entry's re-run, whose passes take a sink's no-op writer
145
+ * (server/render-pass.ts).
146
+ */
147
+ export function runAsBackgroundRefresh<T>(ctx: RequestContext, fn: () => T): T {
148
+ return runWithRequestContext(ctx, () => backgroundRefresh.run(true, fn));
149
+ }
150
+
132
151
  /**
133
152
  * Set the fresh-reads cookie on this request's response for as long as the
134
153
  * invalidated stores' isolate memos can stay stale (isolate-tag-memo.ts), so
@@ -136,7 +155,7 @@ function warnPartialTagStore(fn: string, incapable: number): void {
136
155
  */
137
156
  function markFreshReads(stores: SegmentCacheStore[]): void {
138
157
  const ctx = _getRequestContext();
139
- if (!ctx?._setFreshReadsCookie) return;
158
+ if (!ctx?._setFreshReadsCookie || backgroundRefresh.getStore()) return;
140
159
  let windowMs = 0;
141
160
  for (const store of stores) {
142
161
  windowMs = Math.max(windowMs, store.freshReadsWindowMs ?? 0);
@@ -21,9 +21,11 @@ export function isTainted(value: unknown): boolean {
21
21
  }
22
22
 
23
23
  /**
24
- * Symbol stamped on tainted ctx objects PASSED AS ARGUMENTS to a "use cache"
25
- * function during its execution. ctx.set(), ctx.header(), etc. check this
26
- * flag and throw if present — those side effects are lost on cache hit.
24
+ * Symbol stamped on tainted handler and middleware ctx objects PASSED AS
25
+ * ARGUMENTS to a "use cache" function during its execution. ctx.set(),
26
+ * ctx.header(), etc. check this flag and throw if present — those side
27
+ * effects are lost on cache hit. A request context passed as an argument is
28
+ * not stamped (registerCachedFunction in cache-runtime.ts).
27
29
  *
28
30
  * Argument objects only. Ambient access (cookies()/headers() and ctx methods
29
31
  * reached via getRequestContext()) is guarded by the AsyncLocalStorage scope
@@ -96,10 +96,14 @@ export interface SegmentCacheStore<TEnv = unknown> {
96
96
  * Return value becomes the final cache key (unless route overrides with `key` option).
97
97
  *
98
98
  * Resolution priority:
99
- * 1. Route-level `key` function (full override)
99
+ * 1. Route-level `key` function (replaces the URL part; the request kind stays)
100
100
  * 2. Store-level `keyGenerator` (modifies default key)
101
101
  * 3. Default key generation (prefix:pathname:params)
102
102
  *
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.
106
+ *
103
107
  * Return `defaultKey` unchanged to leave a request unpartitioned. An empty
104
108
  * string is not a key: where the result partitions a nested cache() on
105
109
  * another store, or a ppr shell, it fails key resolution, so the request
@@ -293,7 +297,7 @@ export interface SegmentCacheStore<TEnv = unknown> {
293
297
  /**
294
298
  * Declares the shell family present-but-inert: getShell/putShell exist but
295
299
  * no-op (a custom store whose backing tier is conditionally unavailable).
296
- * scheduleShellCapture skips captures whose only write target is inert —
300
+ * planShellCapture skips captures whose only write target is inert —
297
301
  * the background render would be dead work that still occupies the
298
302
  * per-isolate serialized capture queue (a promise-heavy route bakes for
299
303
  * seconds per MISS with nothing stored). Absent/false means the family,
@@ -659,7 +663,7 @@ export type ShellSnapshotFamily = "segment" | "loader" | "handles";
659
663
  * format (CachedEntryData `handles` + `handleOwners`). A Prerender + ppr
660
664
  * route's handler layer comes from the prerender store, whose build-time
661
665
  * entry has no loader push, so the entry keeps these for the HIT to restore
662
- * as a doc record's (issue #1057, yieldFromStore in cache-lookup.ts). An
666
+ * as a doc record's (issue #1057, decodeStoredEntry in cache-lookup.ts). An
663
667
  * entry written before it (v0.21) has none and serves as it did.
664
668
  */
665
669
  export type ShellSnapshotHandlesValue = Pick<