@rangojs/router 0.27.0 → 0.29.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 (243) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/__internal.d.ts +7 -1
  3. package/dist/types/browser/action-fence.d.ts +2 -1
  4. package/dist/types/browser/event-controller.d.ts +8 -2
  5. package/dist/types/browser/history-state.d.ts +3 -9
  6. package/dist/types/browser/navigation-store.d.ts +8 -6
  7. package/dist/types/browser/partial-update.d.ts +11 -0
  8. package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
  9. package/dist/types/browser/react/use-action.d.ts +7 -2
  10. package/dist/types/browser/react/use-handle.d.ts +19 -16
  11. package/dist/types/browser/react/use-link-status.d.ts +5 -2
  12. package/dist/types/browser/segment-reconciler.d.ts +32 -4
  13. package/dist/types/browser/types.d.ts +24 -5
  14. package/dist/types/browser/wire-protocol.d.ts +26 -0
  15. package/dist/types/cache/cache-key-utils.d.ts +11 -5
  16. package/dist/types/cache/cache-scope.d.ts +34 -22
  17. package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
  18. package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
  19. package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
  20. package/dist/types/cache/handle-snapshot.d.ts +9 -4
  21. package/dist/types/cache/segment-codec.d.ts +14 -0
  22. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  23. package/dist/types/cache/taint.d.ts +5 -3
  24. package/dist/types/cache/types.d.ts +7 -3
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
  26. package/dist/types/prefetch-markers.d.ts +5 -14
  27. package/dist/types/prerender/store.d.ts +31 -0
  28. package/dist/types/prerender/warm-request.d.ts +3 -3
  29. package/dist/types/prerender.d.ts +2 -2
  30. package/dist/types/route-name.d.ts +5 -0
  31. package/dist/types/router/error-handling.d.ts +11 -5
  32. package/dist/types/router/instrument.d.ts +4 -3
  33. package/dist/types/router/intercept-resolution.d.ts +25 -9
  34. package/dist/types/router/match-context.d.ts +6 -11
  35. package/dist/types/router/match-handlers.d.ts +2 -6
  36. package/dist/types/router/match-middleware/background-revalidation.d.ts +13 -22
  37. package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
  38. package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
  39. package/dist/types/router/navigation-snapshot.d.ts +7 -0
  40. package/dist/types/router/prerender-match.d.ts +21 -6
  41. package/dist/types/router/preview-match.d.ts +2 -7
  42. package/dist/types/router/request-classification.d.ts +5 -0
  43. package/dist/types/router/router-context.d.ts +7 -46
  44. package/dist/types/router/router-interfaces.d.ts +8 -14
  45. package/dist/types/router/segment-resolution/helpers.d.ts +19 -3
  46. package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
  47. package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
  48. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
  49. package/dist/types/router/segment-resolution/resolution-pass.d.ts +95 -0
  50. package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
  51. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +2 -0
  52. package/dist/types/router/segment-resolution.d.ts +1 -2
  53. package/dist/types/router/segment-wrappers.d.ts +11 -48
  54. package/dist/types/router/telemetry.d.ts +2 -0
  55. package/dist/types/router/types.d.ts +11 -1
  56. package/dist/types/rsc/full-payload.d.ts +2 -2
  57. package/dist/types/rsc/handler-context.d.ts +6 -6
  58. package/dist/types/rsc/json-route-result.d.ts +5 -9
  59. package/dist/types/rsc/request-stages.d.ts +203 -0
  60. package/dist/types/rsc/response-cache-serve.d.ts +7 -12
  61. package/dist/types/rsc/response-route-handler.d.ts +2 -2
  62. package/dist/types/rsc/routine-plan.d.ts +1 -1
  63. package/dist/types/rsc/server-action.d.ts +2 -2
  64. package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
  65. package/dist/types/rsc/shell-capture.d.ts +132 -15
  66. package/dist/types/rsc/shell-serve.d.ts +37 -12
  67. package/dist/types/server/context.d.ts +9 -8
  68. package/dist/types/server/handle-store.d.ts +16 -0
  69. package/dist/types/server/render-pass.d.ts +72 -0
  70. package/dist/types/server/request-context.d.ts +53 -104
  71. package/dist/types/server/rewrite-trail.d.ts +2 -2
  72. package/dist/types/testing/dispatch.d.ts +35 -18
  73. package/dist/types/testing/render-route.d.ts +43 -8
  74. package/dist/types/testing/serve-shell-request.d.ts +8 -1
  75. package/dist/types/types/cache-types.d.ts +6 -3
  76. package/dist/types/types/error-types.d.ts +30 -3
  77. package/dist/types/types/handler-context.d.ts +9 -1
  78. package/dist/types/types/segments.d.ts +1 -1
  79. package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
  80. package/dist/types/vite/discovery/build-env.d.ts +22 -0
  81. package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
  82. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
  83. package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
  84. package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
  85. package/dist/types/vite/discovery/state.d.ts +12 -0
  86. package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
  87. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  88. package/dist/types/vite/router-discovery.d.ts +8 -47
  89. package/dist/types/vite/temp-rsc-server.d.ts +39 -0
  90. package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
  91. package/dist/types/wire-params.d.ts +39 -0
  92. package/dist/vite/index.js +3822 -3727
  93. package/package.json +1 -1
  94. package/skills/cache-guide/SKILL.md +8 -5
  95. package/skills/caching/SKILL.md +37 -18
  96. package/skills/client-urls/SKILL.md +4 -2
  97. package/skills/document-cache/SKILL.md +13 -1
  98. package/skills/handler-use/SKILL.md +1 -1
  99. package/skills/hooks/SKILL.md +1 -1
  100. package/skills/hooks/data.md +8 -1
  101. package/skills/hooks/handle-and-actions.md +33 -2
  102. package/skills/hooks/navigation.md +121 -15
  103. package/skills/hooks/state.md +6 -4
  104. package/skills/hooks/urls.md +16 -5
  105. package/skills/intercept/SKILL.md +23 -7
  106. package/skills/middleware/SKILL.md +18 -0
  107. package/skills/observability/SKILL.md +30 -0
  108. package/skills/ppr/SKILL.md +18 -3
  109. package/skills/prerender/SKILL.md +39 -15
  110. package/skills/rango/SKILL.md +5 -1
  111. package/skills/server-actions/SKILL.md +15 -1
  112. package/skills/testing/client-components.md +25 -18
  113. package/skills/testing/response-routes.md +6 -4
  114. package/skills/testing/serve-shell-request.md +7 -7
  115. package/skills/use-cache/SKILL.md +46 -8
  116. package/skills/view-transitions/SKILL.md +1 -1
  117. package/src/__internal.ts +7 -1
  118. package/src/browser/action-fence.ts +2 -1
  119. package/src/browser/connection-warmup.ts +3 -1
  120. package/src/browser/event-controller.ts +37 -13
  121. package/src/browser/history-state.ts +6 -12
  122. package/src/browser/navigation-bridge.ts +59 -26
  123. package/src/browser/navigation-client.ts +20 -25
  124. package/src/browser/navigation-store.ts +25 -27
  125. package/src/browser/navigation-transaction.ts +4 -1
  126. package/src/browser/network-error-handler.ts +4 -0
  127. package/src/browser/partial-update.ts +33 -6
  128. package/src/browser/prefetch/cache.ts +2 -1
  129. package/src/browser/prefetch/fetch.ts +8 -16
  130. package/src/browser/prefetch/queue.ts +4 -4
  131. package/src/browser/react/NavigationProvider.tsx +2 -1
  132. package/src/browser/react/filter-segment-order.ts +2 -2
  133. package/src/browser/react/use-action.ts +24 -48
  134. package/src/browser/react/use-handle.ts +22 -18
  135. package/src/browser/react/use-link-status.ts +48 -60
  136. package/src/browser/react/use-navigation.ts +92 -41
  137. package/src/browser/rsc-router.tsx +1 -0
  138. package/src/browser/segment-reconciler.ts +109 -53
  139. package/src/browser/server-action-bridge.ts +81 -61
  140. package/src/browser/types.ts +25 -5
  141. package/src/browser/wire-protocol.ts +43 -0
  142. package/src/cache/cache-key-utils.ts +11 -5
  143. package/src/cache/cache-runtime.ts +132 -75
  144. package/src/cache/cache-scope.ts +110 -64
  145. package/src/cache/cf/cf-cache-constants.ts +2 -2
  146. package/src/cache/cf/cf-cache-store.ts +486 -874
  147. package/src/cache/cf/cf-kv-utils.ts +1 -1
  148. package/src/cache/document-cache.ts +64 -46
  149. package/src/cache/handle-snapshot.ts +19 -7
  150. package/src/cache/read-through-swr.ts +20 -18
  151. package/src/cache/segment-codec.ts +55 -0
  152. package/src/cache/shell-snapshot.ts +2 -2
  153. package/src/cache/tag-invalidation.ts +6 -2
  154. package/src/cache/taint.ts +5 -3
  155. package/src/cache/types.ts +7 -3
  156. package/src/cache/vercel/vercel-cache-store.ts +243 -310
  157. package/src/handles/pushed-by.ts +1 -1
  158. package/src/prefetch-markers.ts +5 -16
  159. package/src/prerender/store.ts +76 -0
  160. package/src/prerender/warm-request.ts +8 -4
  161. package/src/prerender.ts +2 -2
  162. package/src/route-name.ts +8 -0
  163. package/src/router/error-handling.ts +22 -10
  164. package/src/router/instrument.ts +4 -3
  165. package/src/router/intercept-resolution.ts +82 -75
  166. package/src/router/loader-resolution.ts +5 -4
  167. package/src/router/match-api.ts +5 -5
  168. package/src/router/match-context.ts +23 -12
  169. package/src/router/match-handlers.ts +8 -20
  170. package/src/router/match-middleware/background-revalidation.ts +46 -57
  171. package/src/router/match-middleware/cache-lookup.ts +677 -726
  172. package/src/router/match-middleware/cache-store.ts +4 -8
  173. package/src/router/match-middleware/intercept-resolution.ts +3 -23
  174. package/src/router/match-middleware/segment-resolution.ts +34 -61
  175. package/src/router/middleware.ts +69 -11
  176. package/src/router/navigation-snapshot.ts +14 -4
  177. package/src/router/prerender-match.ts +100 -44
  178. package/src/router/preview-match.ts +6 -41
  179. package/src/router/request-classification.ts +13 -7
  180. package/src/router/route-snapshot.ts +2 -5
  181. package/src/router/router-context.ts +10 -95
  182. package/src/router/router-interfaces.ts +8 -14
  183. package/src/router/segment-resolution/helpers.ts +49 -13
  184. package/src/router/segment-resolution/loader-cache.ts +15 -9
  185. package/src/router/segment-resolution/loader-mask.ts +5 -5
  186. package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
  187. package/src/router/segment-resolution/resolution-pass.ts +139 -0
  188. package/src/router/segment-resolution/revalidation.ts +939 -789
  189. package/src/router/segment-resolution/streamed-handler-boundary.ts +3 -0
  190. package/src/router/segment-resolution.ts +4 -13
  191. package/src/router/segment-wrappers.ts +51 -269
  192. package/src/router/telemetry.ts +5 -0
  193. package/src/router/types.ts +11 -1
  194. package/src/router.ts +19 -17
  195. package/src/rsc/full-payload.ts +6 -5
  196. package/src/rsc/handler-context.ts +8 -14
  197. package/src/rsc/handler.ts +126 -507
  198. package/src/rsc/json-route-result.ts +5 -9
  199. package/src/rsc/loader-fetch.ts +6 -5
  200. package/src/rsc/progressive-enhancement.ts +5 -0
  201. package/src/rsc/request-stages.ts +759 -0
  202. package/src/rsc/response-cache-serve.ts +10 -14
  203. package/src/rsc/response-route-handler.ts +13 -5
  204. package/src/rsc/rewrite-passes.ts +2 -1
  205. package/src/rsc/routine-plan.ts +1 -1
  206. package/src/rsc/rsc-rendering.ts +164 -349
  207. package/src/rsc/server-action.ts +87 -113
  208. package/src/rsc/shell-capture-constants.ts +1 -1
  209. package/src/rsc/shell-capture.ts +275 -227
  210. package/src/rsc/shell-serve.ts +121 -18
  211. package/src/rsc/ssr-setup.ts +5 -4
  212. package/src/segment-system.tsx +3 -3
  213. package/src/server/context.ts +22 -25
  214. package/src/server/handle-store.ts +55 -0
  215. package/src/server/render-pass.ts +291 -0
  216. package/src/server/request-context.ts +75 -115
  217. package/src/server/rewrite-trail.ts +6 -4
  218. package/src/ssr/index.tsx +1 -1
  219. package/src/testing/dispatch.ts +226 -710
  220. package/src/testing/e2e/page-helpers.ts +2 -2
  221. package/src/testing/render-route.tsx +199 -13
  222. package/src/testing/serve-shell-request.ts +36 -40
  223. package/src/types/cache-types.ts +6 -3
  224. package/src/types/error-types.ts +30 -3
  225. package/src/types/handler-context.ts +12 -1
  226. package/src/types/segments.ts +1 -1
  227. package/src/vite/discovery/build-discovery-phase.ts +204 -0
  228. package/src/vite/discovery/build-env.ts +148 -0
  229. package/src/vite/discovery/bundle-scan-phase.ts +128 -0
  230. package/src/vite/discovery/dev-prerender-cache.ts +8 -3
  231. package/src/vite/discovery/dev-server-phase.ts +1776 -0
  232. package/src/vite/discovery/discover-routers.ts +1 -1
  233. package/src/vite/discovery/discovery-errors.ts +1 -1
  234. package/src/vite/discovery/prerender-collection.ts +24 -39
  235. package/src/vite/discovery/state.ts +15 -0
  236. package/src/vite/plugins/version-plugin.ts +1 -1
  237. package/src/vite/plugins/virtual-entries.ts +1 -1
  238. package/src/vite/router-discovery.ts +29 -2428
  239. package/src/vite/temp-rsc-server.ts +223 -0
  240. package/src/vite/utils/prerender-utils.ts +47 -0
  241. package/src/wire-params.ts +40 -0
  242. package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
  243. 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
- runWithRequestContext,
25
+ runAsBackgroundWork,
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,26 +361,46 @@ export function createDocumentCacheMiddleware<TEnv = any>(
359
361
  `[DocumentCache] STALE ${typeLabel}: ${url.pathname} (revalidating)`,
360
362
  );
361
363
 
362
- runBackground(requestCtx, async () => {
363
- try {
364
- // Re-establish the request-context ALS around the whole background
365
- // task: next() re-runs the full handler pipeline, and on workerd a
366
- // waitUntil task runs detached from the request's I/O context, so a
367
- // handler/component reading getRequestContext() would otherwise
368
- // throw. Same fix as the route-level/use-cache background
369
- // revalidation paths. The rango.background span (kind=
370
- // document-revalidation) wraps the re-render AND the store write so
371
- // the task's spans — the re-run's own rango.* set and the drain/put
372
- // platform spans — nest under one explanatory parent instead of
373
- // dangling under the ended foreground phases. Running the put
374
- // inside the ALS matches the MISS path, where putResponse already
375
- // executes within the foreground request context.
376
- await runWithRequestContext(requestCtx, () =>
377
- observePhase(
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
+
381
+ // Re-establish the request-context ALS around the whole background
382
+ // task: next() re-runs the full handler pipeline, and on workerd a
383
+ // waitUntil task runs detached from the request's I/O context, so a
384
+ // handler/component reading getRequestContext() would otherwise
385
+ // throw. Same fix as the route-level/use-cache background
386
+ // revalidation paths. The rango.background span (kind=
387
+ // document-revalidation) wraps the re-render AND the store write so
388
+ // the task's spans — the re-run's own rango.* set and the drain/put
389
+ // platform spans — nest under one explanatory parent instead of
390
+ // dangling under the ended foreground phases. Running the put
391
+ // inside the ALS matches the MISS path, where putResponse already
392
+ // executes within the foreground request context. No visitor made
393
+ // the invalidations the re-run makes: the pass's fresh-reads
394
+ // writer is the sink's, which sets nothing. onError marks the
395
+ // re-run's errors `background`, the failure caught below included.
396
+ runBackground(requestCtx, () =>
397
+ runAsBackgroundWork(rerunCtx, async () => {
398
+ try {
399
+ await observePhase(
378
400
  PHASES.background("document-revalidation"),
379
401
  async () => {
380
402
  const fresh = await next();
381
- const directives = shouldCacheResponse(fresh, requestCtx);
403
+ const directives = shouldCacheResponse(fresh, rerunCtx);
382
404
 
383
405
  if (!directives || !fresh.body) return;
384
406
 
@@ -386,10 +408,10 @@ export function createDocumentCacheMiddleware<TEnv = any>(
386
408
  // drain the fresh render fully before snapshotting tags
387
409
  // (same render-complete barrier as the miss path).
388
410
  const body = await new Response(fresh.body).arrayBuffer();
389
- throwIfRenderErrored(requestCtx);
411
+ throwIfRenderErrored(rerunCtx);
390
412
  // Not written when one of its tags was invalidated since
391
413
  // the render started (#977).
392
- const tags = collectRequestTags(requestCtx);
414
+ const tags = collectRequestTags(rerunCtx);
393
415
  if (await predatesInvalidation(store, tags, start)) return;
394
416
  await store.putResponse!(
395
417
  cacheKey,
@@ -404,24 +426,20 @@ export function createDocumentCacheMiddleware<TEnv = any>(
404
426
  `[DocumentCache] REVALIDATED ${typeLabel}: ${url.pathname}`,
405
427
  );
406
428
  },
407
- ),
408
- );
409
- } catch (error) {
410
- // The route now rewrites (route middleware): nothing to refresh
411
- // this entry with.
412
- if (isRewrite(error)) return;
413
- // Pass requestCtx explicitly: this runs in a detached waitUntil task
414
- // where the ALS context is gone, so onError only fires if we hand it
415
- // the captured context (reportCacheError falls back to _getRequestContext
416
- // otherwise, which is null here).
417
- reportCacheError(
418
- error,
419
- "cache-write",
420
- "[DocumentCache] revalidation",
421
- requestCtx,
422
- );
423
- }
424
- });
429
+ );
430
+ } catch (error) {
431
+ // The route now rewrites (route middleware): nothing to refresh
432
+ // this entry with.
433
+ if (isRewrite(error)) return;
434
+ reportCacheError(
435
+ error,
436
+ "cache-write",
437
+ "[DocumentCache] revalidation",
438
+ requestCtx,
439
+ );
440
+ }
441
+ }),
442
+ );
425
443
 
426
444
  return drainOnResponseCallbacks(
427
445
  addCacheStatusHeader(cached.response, "STALE"),
@@ -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
  /**
@@ -117,29 +117,31 @@ export async function readThroughItem<T>(
117
117
  // span cover the write too (a slow or failing store write is part of
118
118
  // the revalidation, not an untraced tail). The foreground miss below
119
119
  // stays unwrapped: its context is already established.
120
+ // Reported inside the task, within wrapBackground's frame, so onError
121
+ // marks it `background`; rethrown so the span records the failure.
120
122
  const revalidateTask = async () => {
121
- const fresh = await execute();
122
- const serialized = await serialize(fresh);
123
- if (serialized !== null) {
124
- await setItem(key, serialized, storeOptions);
123
+ try {
124
+ const fresh = await execute();
125
+ const serialized = await serialize(fresh);
126
+ if (serialized !== null) {
127
+ await setItem(key, serialized, storeOptions);
128
+ }
129
+ } catch (error) {
130
+ reportCacheError(
131
+ error,
132
+ "stale-revalidation",
133
+ "[read-through] background revalidation",
134
+ host ?? undefined,
135
+ );
136
+ throw error;
125
137
  }
126
138
  };
127
139
  runBackground(
128
140
  host,
129
- async () => {
130
- try {
131
- await (wrapBackground
132
- ? wrapBackground(revalidateTask)
133
- : revalidateTask());
134
- } catch (error) {
135
- reportCacheError(
136
- error,
137
- "stale-revalidation",
138
- "[read-through] background revalidation",
139
- host ?? undefined,
140
- );
141
- }
142
- },
141
+ () =>
142
+ (wrapBackground ? wrapBackground(revalidateTask) : revalidateTask())
143
+ // Reported above.
144
+ .catch(() => {}),
143
145
  true,
144
146
  );
145
147
  return data;
@@ -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,10 @@
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 {
40
+ _getRequestContext,
41
+ isBackgroundWork,
42
+ } from "../server/request-context.js";
40
43
  import { reportingAsync } from "./cache-error.js";
41
44
  import { normalizeTags } from "./cache-tag.js";
42
45
  import type { SegmentCacheStore } from "./types.js";
@@ -136,7 +139,8 @@ function warnPartialTagStore(fn: string, incapable: number): void {
136
139
  */
137
140
  function markFreshReads(stores: SegmentCacheStore[]): void {
138
141
  const ctx = _getRequestContext();
139
- if (!ctx?._setFreshReadsCookie) return;
142
+ // No visitor made an invalidation in background work (runAsBackgroundWork).
143
+ if (!ctx?._setFreshReadsCookie || isBackgroundWork()) return;
140
144
  let windowMs = 0;
141
145
  for (const store of stores) {
142
146
  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<