@rangojs/router 0.26.6 → 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 (234) 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-coordinator.d.ts +0 -4
  4. package/dist/types/browser/action-fence.d.ts +11 -3
  5. package/dist/types/browser/event-controller.d.ts +20 -1
  6. package/dist/types/browser/history-state.d.ts +7 -9
  7. package/dist/types/browser/intercept-utils.d.ts +23 -0
  8. package/dist/types/browser/navigation-store.d.ts +8 -6
  9. package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
  10. package/dist/types/browser/rsc-router.d.ts +1 -1
  11. package/dist/types/browser/segment-reconciler.d.ts +6 -4
  12. package/dist/types/browser/types.d.ts +2 -1
  13. package/dist/types/browser/wire-protocol.d.ts +25 -0
  14. package/dist/types/cache/cache-key-utils.d.ts +11 -5
  15. package/dist/types/cache/cache-scope.d.ts +34 -22
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
  18. package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
  19. package/dist/types/cache/handle-snapshot.d.ts +9 -4
  20. package/dist/types/cache/segment-codec.d.ts +14 -0
  21. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  22. package/dist/types/cache/tag-invalidation.d.ts +10 -0
  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 +4 -6
  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 +4 -4
  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 +12 -11
  35. package/dist/types/router/match-handlers.d.ts +2 -6
  36. package/dist/types/router/match-middleware/background-revalidation.d.ts +10 -20
  37. package/dist/types/router/match-middleware/cache-lookup.d.ts +35 -87
  38. package/dist/types/router/match-middleware/cache-store.d.ts +2 -2
  39. package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
  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/revalidation.d.ts +6 -1
  44. package/dist/types/router/router-context.d.ts +9 -63
  45. package/dist/types/router/router-interfaces.d.ts +8 -14
  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 +93 -0
  50. package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
  51. package/dist/types/router/segment-resolution.d.ts +1 -2
  52. package/dist/types/router/segment-wrappers.d.ts +11 -48
  53. package/dist/types/router/telemetry.d.ts +2 -0
  54. package/dist/types/router/types.d.ts +10 -0
  55. package/dist/types/rsc/full-payload.d.ts +2 -2
  56. package/dist/types/rsc/handler-context.d.ts +6 -6
  57. package/dist/types/rsc/json-route-result.d.ts +5 -9
  58. package/dist/types/rsc/request-stages.d.ts +203 -0
  59. package/dist/types/rsc/response-cache-serve.d.ts +7 -12
  60. package/dist/types/rsc/response-route-handler.d.ts +2 -2
  61. package/dist/types/rsc/routine-plan.d.ts +1 -1
  62. package/dist/types/rsc/server-action.d.ts +2 -2
  63. package/dist/types/rsc/shell-capture-constants.d.ts +2 -2
  64. package/dist/types/rsc/shell-capture.d.ts +132 -15
  65. package/dist/types/rsc/shell-serve.d.ts +37 -12
  66. package/dist/types/server/context.d.ts +9 -8
  67. package/dist/types/server/handle-store.d.ts +16 -0
  68. package/dist/types/server/render-pass.d.ts +72 -0
  69. package/dist/types/server/request-context.d.ts +24 -104
  70. package/dist/types/server/rewrite-trail.d.ts +2 -2
  71. package/dist/types/testing/dispatch.d.ts +35 -18
  72. package/dist/types/testing/serve-shell-request.d.ts +7 -0
  73. package/dist/types/types/cache-types.d.ts +6 -3
  74. package/dist/types/types/error-types.d.ts +9 -1
  75. package/dist/types/types/handler-context.d.ts +18 -7
  76. package/dist/types/types/segments.d.ts +1 -1
  77. package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
  78. package/dist/types/vite/discovery/build-env.d.ts +22 -0
  79. package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
  80. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
  81. package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
  82. package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
  83. package/dist/types/vite/discovery/state.d.ts +12 -0
  84. package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
  85. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  86. package/dist/types/vite/router-discovery.d.ts +8 -47
  87. package/dist/types/vite/temp-rsc-server.d.ts +39 -0
  88. package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
  89. package/dist/types/wire-params.d.ts +34 -0
  90. package/dist/vite/index.js +3809 -3710
  91. package/package.json +1 -1
  92. package/skills/cache-guide/SKILL.md +8 -5
  93. package/skills/caching/SKILL.md +37 -18
  94. package/skills/client-urls/SKILL.md +2 -1
  95. package/skills/document-cache/SKILL.md +18 -1
  96. package/skills/handler-use/SKILL.md +1 -1
  97. package/skills/intercept/SKILL.md +53 -13
  98. package/skills/layout/SKILL.md +11 -7
  99. package/skills/loader/SKILL.md +30 -13
  100. package/skills/middleware/SKILL.md +19 -1
  101. package/skills/migrate-nextjs/SKILL.md +5 -4
  102. package/skills/parallel/SKILL.md +5 -4
  103. package/skills/ppr/SKILL.md +19 -3
  104. package/skills/prerender/SKILL.md +39 -15
  105. package/skills/rango/SKILL.md +23 -11
  106. package/skills/server-actions/SKILL.md +51 -6
  107. package/skills/testing/predicates.md +1 -1
  108. package/skills/testing/response-routes.md +6 -4
  109. package/skills/testing/serve-shell-request.md +7 -7
  110. package/skills/use-cache/SKILL.md +46 -8
  111. package/skills/ux-architecture/SKILL.md +3 -2
  112. package/src/__internal.ts +7 -1
  113. package/src/browser/action-coordinator.ts +2 -13
  114. package/src/browser/action-fence.ts +11 -3
  115. package/src/browser/connection-warmup.ts +3 -1
  116. package/src/browser/event-controller.ts +42 -3
  117. package/src/browser/history-state.ts +15 -13
  118. package/src/browser/intercept-utils.ts +51 -0
  119. package/src/browser/navigation-bridge.ts +34 -30
  120. package/src/browser/navigation-client.ts +21 -33
  121. package/src/browser/navigation-store.ts +25 -27
  122. package/src/browser/navigation-transaction.ts +7 -11
  123. package/src/browser/partial-update.ts +8 -0
  124. package/src/browser/prefetch/cache.ts +2 -1
  125. package/src/browser/prefetch/fetch.ts +8 -16
  126. package/src/browser/prefetch/queue.ts +4 -4
  127. package/src/browser/react/NavigationProvider.tsx +2 -1
  128. package/src/browser/react/filter-segment-order.ts +2 -2
  129. package/src/browser/rsc-router.tsx +17 -2
  130. package/src/browser/segment-reconciler.ts +66 -53
  131. package/src/browser/server-action-bridge.ts +265 -127
  132. package/src/browser/types.ts +2 -1
  133. package/src/browser/wire-protocol.ts +41 -0
  134. package/src/cache/cache-key-utils.ts +11 -5
  135. package/src/cache/cache-runtime.ts +110 -50
  136. package/src/cache/cache-scope.ts +110 -64
  137. package/src/cache/cf/cf-cache-constants.ts +2 -2
  138. package/src/cache/cf/cf-cache-store.ts +486 -874
  139. package/src/cache/cf/cf-kv-utils.ts +1 -1
  140. package/src/cache/document-cache.ts +42 -15
  141. package/src/cache/handle-snapshot.ts +19 -7
  142. package/src/cache/segment-codec.ts +55 -0
  143. package/src/cache/shell-snapshot.ts +2 -2
  144. package/src/cache/tag-invalidation.ts +21 -2
  145. package/src/cache/taint.ts +5 -3
  146. package/src/cache/types.ts +7 -3
  147. package/src/cache/vercel/vercel-cache-store.ts +243 -310
  148. package/src/handles/pushed-by.ts +1 -1
  149. package/src/prefetch-markers.ts +4 -7
  150. package/src/prerender/store.ts +76 -0
  151. package/src/prerender/warm-request.ts +8 -4
  152. package/src/prerender.ts +2 -2
  153. package/src/route-name.ts +8 -0
  154. package/src/router/error-handling.ts +4 -4
  155. package/src/router/instrument.ts +4 -3
  156. package/src/router/intercept-resolution.ts +77 -75
  157. package/src/router/loader-resolution.ts +5 -4
  158. package/src/router/match-api.ts +4 -5
  159. package/src/router/match-context.ts +33 -12
  160. package/src/router/match-handlers.ts +8 -20
  161. package/src/router/match-middleware/background-revalidation.ts +41 -53
  162. package/src/router/match-middleware/cache-lookup.ts +680 -720
  163. package/src/router/match-middleware/cache-store.ts +14 -12
  164. package/src/router/match-middleware/intercept-resolution.ts +3 -23
  165. package/src/router/match-middleware/segment-resolution.ts +34 -61
  166. package/src/router/match-result.ts +1 -20
  167. package/src/router/middleware.ts +69 -11
  168. package/src/router/navigation-snapshot.ts +4 -3
  169. package/src/router/prerender-match.ts +100 -44
  170. package/src/router/preview-match.ts +6 -41
  171. package/src/router/request-classification.ts +13 -7
  172. package/src/router/revalidation.ts +33 -7
  173. package/src/router/route-snapshot.ts +2 -5
  174. package/src/router/router-context.ts +14 -117
  175. package/src/router/router-interfaces.ts +8 -14
  176. package/src/router/segment-resolution/loader-cache.ts +14 -9
  177. package/src/router/segment-resolution/loader-mask.ts +5 -5
  178. package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
  179. package/src/router/segment-resolution/resolution-pass.ts +137 -0
  180. package/src/router/segment-resolution/revalidation.ts +897 -776
  181. package/src/router/segment-resolution.ts +4 -13
  182. package/src/router/segment-wrappers.ts +51 -269
  183. package/src/router/telemetry.ts +5 -0
  184. package/src/router/types.ts +10 -0
  185. package/src/router.ts +12 -13
  186. package/src/rsc/full-payload.ts +6 -5
  187. package/src/rsc/handler-context.ts +8 -14
  188. package/src/rsc/handler.ts +126 -507
  189. package/src/rsc/json-route-result.ts +5 -9
  190. package/src/rsc/loader-fetch.ts +6 -5
  191. package/src/rsc/progressive-enhancement.ts +5 -0
  192. package/src/rsc/request-stages.ts +757 -0
  193. package/src/rsc/response-cache-serve.ts +10 -14
  194. package/src/rsc/response-route-handler.ts +13 -5
  195. package/src/rsc/rewrite-passes.ts +2 -1
  196. package/src/rsc/routine-plan.ts +1 -1
  197. package/src/rsc/rsc-rendering.ts +172 -349
  198. package/src/rsc/server-action.ts +87 -113
  199. package/src/rsc/shell-capture-constants.ts +3 -1
  200. package/src/rsc/shell-capture.ts +212 -169
  201. package/src/rsc/shell-serve.ts +121 -18
  202. package/src/rsc/ssr-setup.ts +5 -4
  203. package/src/segment-system.tsx +3 -3
  204. package/src/server/context.ts +22 -25
  205. package/src/server/handle-store.ts +55 -0
  206. package/src/server/render-pass.ts +291 -0
  207. package/src/server/request-context.ts +26 -115
  208. package/src/server/rewrite-trail.ts +6 -4
  209. package/src/ssr/index.tsx +1 -1
  210. package/src/ssr/ssr-root.tsx +1 -0
  211. package/src/testing/dispatch.ts +223 -710
  212. package/src/testing/e2e/page-helpers.ts +2 -2
  213. package/src/testing/serve-shell-request.ts +37 -32
  214. package/src/types/cache-types.ts +6 -3
  215. package/src/types/error-types.ts +9 -1
  216. package/src/types/handler-context.ts +21 -7
  217. package/src/types/segments.ts +1 -1
  218. package/src/vite/discovery/build-discovery-phase.ts +204 -0
  219. package/src/vite/discovery/build-env.ts +148 -0
  220. package/src/vite/discovery/bundle-scan-phase.ts +128 -0
  221. package/src/vite/discovery/dev-prerender-cache.ts +8 -3
  222. package/src/vite/discovery/dev-server-phase.ts +1776 -0
  223. package/src/vite/discovery/discover-routers.ts +1 -1
  224. package/src/vite/discovery/discovery-errors.ts +1 -1
  225. package/src/vite/discovery/prerender-collection.ts +24 -39
  226. package/src/vite/discovery/state.ts +15 -0
  227. package/src/vite/plugins/version-plugin.ts +1 -1
  228. package/src/vite/plugins/virtual-entries.ts +1 -1
  229. package/src/vite/router-discovery.ts +29 -2428
  230. package/src/vite/temp-rsc-server.ts +223 -0
  231. package/src/vite/utils/prerender-utils.ts +47 -0
  232. package/src/wire-params.ts +35 -0
  233. package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
  234. package/src/router/segment-resolution/fresh.ts +0 -945
@@ -36,6 +36,10 @@ import {
36
36
  warnOnce,
37
37
  } from "./shell-capture-constants.js";
38
38
  import { routerKeyPrefix } from "../cache/cache-key-utils.js";
39
+ import { reportCacheError } from "../cache/cache-error.js";
40
+ import { appendMetric } from "../router/metrics.js";
41
+ import type { MetricsStore } from "../server/context.js";
42
+ import { WIRE } from "../wire-params.js";
39
43
 
40
44
  export {
41
45
  PPR_REPLAY_STATUS_HEADER,
@@ -128,19 +132,13 @@ export function resolvePprConfig(
128
132
  }
129
133
 
130
134
  /**
131
- * The forced-MISS query marker. A document HIT that cannot finish its tail
132
- * reloads the page with it (shellReloadScript); the serve gate renders a
133
- * request carrying it on axis 1, with no shell read and no capture, so the
134
- * reload can never be a HIT that degrades again. The handler drops it from
135
- * the request on entry (withoutShellMissMarker) and keeps only the flag
136
- * (RequestContext._shellForcedMiss), so nothing else sees it.
137
- */
138
- export const SHELL_MISS_PARAM: string = "_rsc_shell";
139
-
140
- /**
141
- * The request without the forced-MISS marker, or undefined when it carries
142
- * none. The handler calls it before anything reads the request: left in, the
143
- * marker reached `ctx.request.url`, `originalUrl`, middleware, and the SSR
135
+ * The request without the forced-MISS marker (`WIRE.shellMiss`), or
136
+ * undefined when it carries none. A document HIT that cannot finish its tail
137
+ * reloads the page with the marker (shellReloadScript); the serve gate
138
+ * renders a request carrying it on axis 1, with no shell read and no
139
+ * capture, so the reload can never be a HIT that degrades again. The handler
140
+ * calls this on entry, before anything reads the request, and keeps only the
141
+ * flag (RequestContext._shellForcedMiss): left in, the marker reached `ctx.request.url`, `originalUrl`, middleware, and the SSR
144
142
  * search seed (so `useSearchParams` rendered it), and a `"use cache"` key
145
143
  * built from the request URL was never read again. GET and HEAD only: the
146
144
  * reload is a navigation, and a request with a body cannot be re-created
@@ -148,11 +146,11 @@ export const SHELL_MISS_PARAM: string = "_rsc_shell";
148
146
  * other request.
149
147
  */
150
148
  export function withoutShellMissMarker(request: Request): Request | undefined {
151
- if (!request.url.includes(SHELL_MISS_PARAM)) return undefined;
149
+ if (!request.url.includes(WIRE.shellMiss)) return undefined;
152
150
  if (request.method !== "GET" && request.method !== "HEAD") return undefined;
153
151
  const url = new URL(request.url);
154
- if (!url.searchParams.has(SHELL_MISS_PARAM)) return undefined;
155
- url.searchParams.delete(SHELL_MISS_PARAM);
152
+ if (!url.searchParams.has(WIRE.shellMiss)) return undefined;
153
+ url.searchParams.delete(WIRE.shellMiss);
156
154
  return new Request(url, request);
157
155
  }
158
156
 
@@ -168,8 +166,8 @@ export function withoutShellMissMarker(request: Request): Request | undefined {
168
166
  export function shellReloadScript(): string {
169
167
  return inlineShellScript(
170
168
  "(function(){var u=new URL(location.href);" +
171
- `if(u.searchParams.has(${JSON.stringify(SHELL_MISS_PARAM)}))return;` +
172
- `u.searchParams.set(${JSON.stringify(SHELL_MISS_PARAM)},"miss");` +
169
+ `if(u.searchParams.has(${JSON.stringify(WIRE.shellMiss)}))return;` +
170
+ `u.searchParams.set(${JSON.stringify(WIRE.shellMiss)},"miss");` +
173
171
  "try{window.stop()}catch(e){}location.replace(u.href)})()",
174
172
  );
175
173
  }
@@ -305,6 +303,111 @@ export function openShellDocument(
305
303
  };
306
304
  }
307
305
 
306
+ /**
307
+ * A document serve's store read: a getShell entry, or a prelude-first read
308
+ * (readShellDocument) whose entry is the head and whose snapshot arrives on
309
+ * its own promise.
310
+ */
311
+ export type ShellRead =
312
+ | { entry: ShellCacheEntry; shouldRevalidate?: boolean; read?: undefined }
313
+ | {
314
+ entry: ShellEntryHead;
315
+ shouldRevalidate?: boolean;
316
+ read: ShellDocumentRead;
317
+ };
318
+
319
+ /**
320
+ * The document serve's shell read from `source`, the app cache store, that
321
+ * degrades to null on failure (axis 1 MISS, never a 500) and records the raw
322
+ * `ppr:shell-read` outcome (pre-validity-gates, so a version-mismatch
323
+ * lifecycle miss stays distinguishable from a store miss). A store with a
324
+ * prelude-first read (readShellDocument) resolves before the snapshot is
325
+ * read, so the HIT commits without waiting for it. A build shell is read by
326
+ * shell-build-manifest.ts lookupBuildShell, which imports this module.
327
+ */
328
+ export async function readShell(
329
+ source: SegmentCacheStore & {
330
+ getShell: NonNullable<SegmentCacheStore["getShell"]>;
331
+ },
332
+ key: string,
333
+ opts: { tagHints?: readonly string[]; metrics?: MetricsStore },
334
+ ): Promise<ShellRead | null> {
335
+ const { tagHints, metrics } = opts;
336
+ let cached: ShellRead | null = null;
337
+ const shellReadStart = metrics ? performance.now() : 0;
338
+ try {
339
+ if (source.readShellDocument) {
340
+ const read = await source.readShellDocument(
341
+ key,
342
+ tagHints ? { tagHints } : undefined,
343
+ );
344
+ cached = read
345
+ ? { entry: read.entry, shouldRevalidate: read.shouldRevalidate, read }
346
+ : null;
347
+ } else {
348
+ cached = await source.getShell(key);
349
+ }
350
+ } catch (error) {
351
+ reportCacheError(error, "cache-read", "[ShellServe] getShell");
352
+ }
353
+ if (metrics) {
354
+ const stats = cached?.read?.stats;
355
+ appendMetric(
356
+ metrics,
357
+ "ppr:shell-read",
358
+ shellReadStart,
359
+ performance.now() - shellReadStart,
360
+ undefined,
361
+ cached ? (stats ? `hit ${stats.tier}` : "hit") : "miss",
362
+ );
363
+ if (stats) recordShellReadStats(metrics, shellReadStart, stats);
364
+ }
365
+ return cached;
366
+ }
367
+
368
+ /**
369
+ * A prelude-first read's sub-steps as `debugPerformance` rows under
370
+ * `ppr:shell-read`. The marker row starts with the prelude row because the
371
+ * two run in parallel; its desc says how long the read still waited on the
372
+ * marker once the prelude was in. On a deployed worker the clock only
373
+ * advances on I/O, so the byte counts are the cost signal for the CPU parts.
374
+ */
375
+ function recordShellReadStats(
376
+ store: MetricsStore,
377
+ readStart: number,
378
+ stats: ShellReadStats,
379
+ ): void {
380
+ let at = readStart;
381
+ const row = (label: string, ms: number | undefined, desc: string): void => {
382
+ if (ms !== undefined) appendMetric(store, label, at, ms, 1, desc);
383
+ };
384
+ if (stats.memo) {
385
+ row(
386
+ "ppr:shell-memo",
387
+ 0,
388
+ `${stats.memo.hit ? "hit" : "miss"} size=${stats.memo.bytes}b`,
389
+ );
390
+ }
391
+ // A KV hit after an L1 miss: the L1 attempt first, then the KV read.
392
+ row("ppr:shell-l1-miss", stats.l1MissMs, stats.l1MissReason ?? "miss");
393
+ at += stats.l1MissMs ?? 0;
394
+ row("ppr:shell-match", stats.matchMs, stats.tier);
395
+ at += stats.matchMs ?? 0;
396
+ row("ppr:shell-head", stats.headMs, `bytes=${stats.headBytes ?? 0}`);
397
+ at += stats.headMs ?? 0;
398
+ row("ppr:shell-prelude", stats.preludeMs, `bytes=${stats.preludeBytes ?? 0}`);
399
+ let marker = `tags=${stats.tags ?? 0} ${stats.markerSerial ? "serial" : "parallel"} commit-wait=${(stats.markerWaitMs ?? 0).toFixed(2)}ms`;
400
+ if (stats.markerMemo) marker += ` memo=${stats.markerMemo}`;
401
+ if (stats.markerHinted) {
402
+ marker += ` hint=${stats.markerHintHits ?? 0}/${stats.markerHinted.length}`;
403
+ if (stats.markerLeadMs !== undefined) {
404
+ marker += ` lead=${stats.markerLeadMs.toFixed(2)}ms`;
405
+ }
406
+ }
407
+ if (stats.freshReads) marker += " fresh-reads";
408
+ row("ppr:shell-marker", stats.markerMs, marker);
409
+ }
410
+
308
411
  /**
309
412
  * Prelude enqueue granularity. A streaming compressor in front of the worker
310
413
  * emits its first byte only after compressing the whole write it was handed:
@@ -24,6 +24,7 @@ import {
24
24
  import { _getRequestContext } from "../server/request-context.js";
25
25
  import { renderedLocation } from "./full-payload.js";
26
26
  import { markServerErrorPrinted } from "../server/print-server-error.js";
27
+ import { WIRE } from "../wire-params.js";
27
28
 
28
29
  export type SSRSetup = readonly [SSRModule, SSRStreamMode];
29
30
 
@@ -233,10 +234,10 @@ function acceptsFlightExplicitly(request: Request, url: URL): boolean {
233
234
  */
234
235
  export function mayNeedSSR(request: Request, url: URL): boolean {
235
236
  if (
236
- url.searchParams.has("_rsc_partial") ||
237
- url.searchParams.has("_rsc_action") ||
237
+ url.searchParams.has(WIRE.partial) ||
238
+ url.searchParams.has(WIRE.action) ||
238
239
  requestHeaders(request).has("rsc-action") ||
239
- url.searchParams.has("_rsc_loader") ||
240
+ url.searchParams.has(WIRE.loader) ||
240
241
  url.searchParams.has("__rsc")
241
242
  ) {
242
243
  return false;
@@ -264,7 +265,7 @@ export function isRscRequest(
264
265
  ): boolean {
265
266
  return (
266
267
  isPartial ||
267
- url.searchParams.has("_rsc_partial") ||
268
+ url.searchParams.has(WIRE.partial) ||
268
269
  url.searchParams.has("__rsc") ||
269
270
  acceptsFlightExplicitly(request, url)
270
271
  );
@@ -295,7 +295,7 @@ function wrapDefaultOutletContent(
295
295
 
296
296
  /**
297
297
  * Per-loader stream map for read-site suspension. { ssr: false } loaders were
298
- * awaited before flush (fresh.ts), so deliver the SETTLED result, not the
298
+ * awaited before flush (resolveOwnerLoaders), so deliver the SETTLED result, not the
299
299
  * settled promise: the read site decodes synchronously instead of use()ing a
300
300
  * Flight chunk whose fulfilled-at-read-time status is a scheduling race the
301
301
  * SSR-completeness contract must not depend on. Unflagged siblings keep the
@@ -1063,8 +1063,8 @@ function* segmentTreeWalk(
1063
1063
  }
1064
1064
  }
1065
1065
 
1066
- // Segments arrive in root-to-leaf order from the server (resolveSegment
1067
- // and resolveSegmentWithRevalidation push segments in this order).
1066
+ // Segments arrive in root-to-leaf order from the server
1067
+ // (resolveSegment pushes segments in this order).
1068
1068
  // All consumers (reconcileSegments, cache) preserve this order.
1069
1069
  // No sorting needed — iterate bottom-to-top to process leaf segments first.
1070
1070
  // This processes route/leaf layouts first, then parent layouts
@@ -15,6 +15,7 @@ import { invariant, DslContextError } from "../errors";
15
15
  import type { DefaultRouteName } from "../types/global-namespace.js";
16
16
  import type { ContextVar } from "../context-var.js";
17
17
  import { PPR_LANE_HINT } from "../rsc/shell-capture-constants.js";
18
+ import type { ShellCaptureState } from "../rsc/shell-capture.js";
18
19
  import {
19
20
  noteWarmIdentityRead,
20
21
  type WarmContext,
@@ -155,7 +156,7 @@ export type LoaderEntry = {
155
156
  * thrown notFound()/redirect() deterministically precede first flush.
156
157
  * Resolved at DSL-evaluation time from ctx.isSSR — entries are cached
157
158
  * per-isSSR (router/manifest.ts cache key), so the flag is already
158
- * request-mode-correct when resolveLoaders (fresh.ts) reads it and never
159
+ * request-mode-correct when resolveOwnerLoaders reads it and never
159
160
  * appears on navigation-lane entries. Set by documentModeLoader, which
160
161
  * getDocumentModeEntry also applies to a lazy include's captured ancestors.
161
162
  */
@@ -1133,7 +1134,7 @@ export interface IdentityReadWording {
1133
1134
  * only observed; it is never rendered. A cached body, loader body or
1134
1135
  * funnel they start is guarded again.
1135
1136
  * 1. A PPR shell capture (`ctx` is the capture's derived context,
1136
- * `_shellCaptureRun`) trips first: the capture context is flagged (so a
1137
+ * `_shellCaptureState`) trips first: the capture context is flagged (so a
1137
1138
  * caught throw still refuses the capture) and the read throws. The shell is
1138
1139
  * shared per host+URL, and every HIT replays what the capture read. There
1139
1140
  * is no loader-body exemption here: a bake-lane loader and a loader a
@@ -1219,7 +1220,7 @@ export function requestReadCaptureFix(
1219
1220
 
1220
1221
  /**
1221
1222
  * True when `ctx` is the active capture render: the derived request context
1222
- * shell-capture.ts builds (`_shellCaptureRun`), which only the capture sets,
1223
+ * shell-capture.ts builds (`_shellCaptureState`), which only the capture has,
1223
1224
  * so the foreground render reads identity normally to serve the real user.
1224
1225
  * On true the capture context is flagged with the read (`surface`, e.g.
1225
1226
  * "cookies()", "ctx.theme"), the fix the refusal warning gives, and the loader
@@ -1230,21 +1231,15 @@ function tripShellCaptureGuard(
1230
1231
  surface: string,
1231
1232
  fix: string,
1232
1233
  ): boolean {
1233
- if (
1234
- ctx === null ||
1235
- typeof ctx !== "object" ||
1236
- (ctx as { _shellCaptureRun?: unknown })._shellCaptureRun !== true
1237
- ) {
1238
- return false;
1239
- }
1234
+ const captureState =
1235
+ ctx !== null && typeof ctx === "object"
1236
+ ? (ctx as { _shellCaptureState?: ShellCaptureState })._shellCaptureState
1237
+ : undefined;
1238
+ if (captureState?.run !== true) return false;
1240
1239
  // Record WHICH loader body (if any) made the read, so the refusal warning
1241
1240
  // can name the real source instead of hardcoding a lane (issue #672).
1242
- const flagged = ctx as {
1243
- _shellCaptureGuardTripped?: { surface: string; fix: string };
1244
- _shellCaptureGuardTrippedLoaderId?: string;
1245
- };
1246
- flagged._shellCaptureGuardTripped = { surface, fix };
1247
- flagged._shellCaptureGuardTrippedLoaderId = getCurrentLoaderBodyId();
1241
+ captureState.guardTripped = { surface, fix };
1242
+ captureState.guardTrippedLoaderId = getCurrentLoaderBodyId();
1248
1243
  return true;
1249
1244
  }
1250
1245
 
@@ -1530,12 +1525,13 @@ export function latchPprHeaderScopeForEntries(
1530
1525
  * ONLY the ppr (shell) axis is dropped — dynamic() does NOT opt off the CACHE
1531
1526
  * axis. Two cases:
1532
1527
  * - Pure ppr funnel (no cache() boundary): clear the latch → writes re-permit.
1533
- * - ppr route nested under a cache() boundary: fresh.ts latches "ppr" at the
1534
- * funnel top (first-wins), which MASKS the positional cache() latch, but the
1535
- * handler still runs inside the cache scope (`insideCacheScope`). A cache()
1536
- * HIT skips that handler, so the write is still non-deterministic — UNMASK to
1537
- * "cache" instead of clearing, so the guard keeps throwing (accurate cache()
1538
- * wording). This is why the check keys off `insideCacheScope`, not just kind.
1528
+ * - ppr route nested under a cache() boundary: resolveAllSegments
1529
+ * latches "ppr" at the funnel top (first-wins), which MASKS the positional
1530
+ * cache() latch, but the handler still runs inside the cache scope
1531
+ * (`insideCacheScope`). A cache() HIT skips that handler, so the write is
1532
+ * still non-deterministic — UNMASK to "cache" instead of clearing, so the
1533
+ * guard keeps throwing (accurate cache() wording). This is why the check keys
1534
+ * off `insideCacheScope`, not just kind.
1539
1535
  *
1540
1536
  * A subsequent cache() entered AFTER dynamic() on a pure-ppr funnel re-latches
1541
1537
  * "cache" via latchCachedHeaderScope's `!store.cachedHeaderScope` guard (the
@@ -1568,9 +1564,10 @@ export function assertCachedHeaderWriteAllowed(
1568
1564
  const scope = RangoContext.getStore()?.cachedHeaderScope;
1569
1565
  if (!scope) return;
1570
1566
  // Exempt DSL loaders (loaderScopeALS) ONLY. A registered loader re-runs on
1571
- // every cache HIT (fresh.ts runInsideLoaderScope -> cache-lookup.ts
1572
- // resolveLoadersOnly), so its header/cookie writes merge into every response
1573
- // with no MISS/HIT divergence. A handler-invoked loader body has
1567
+ // every cache HIT (resolveOwnerLoaders runInsideLoaderScope, reached from
1568
+ // cache-lookup.ts resolveLoadersOnly), so its header/cookie
1569
+ // writes merge into every response with no MISS/HIT divergence. A
1570
+ // handler-invoked loader body has
1574
1571
  // loaderBodyScopeALS active but loaderScopeALS unset (loader-resolution.ts
1575
1572
  // derives isDslLoader from isInsideLoaderScope()); on a HIT the handler is
1576
1573
  // skipped so that loader never re-runs and its write would land only on the
@@ -89,10 +89,14 @@ interface SlotTag {
89
89
  * loader a copy stands for. Unset for a handler's push.
90
90
  */
91
91
  by?: string;
92
+ /** A stored value a HIT restored (holdRecordedValues). */
93
+ record?: true;
92
94
  }
93
95
 
94
96
  // Shared by every plain DSL-loader push; tags are never mutated in place.
95
97
  const LOADER_SLOT: SlotTag = Object.freeze({ loader: true });
98
+ // Shared by every held record value that had no tag (holdRecordedValues).
99
+ const RECORD_SLOT: SlotTag = Object.freeze({ record: true });
96
100
 
97
101
  /**
98
102
  * What a restored record is to one loader's handle pushes on this request
@@ -311,6 +315,24 @@ export interface HandleStore {
311
315
  */
312
316
  replacePlaceholders(loaderIds: readonly string[], deliver: () => void): void;
313
317
 
318
+ /**
319
+ * A cache() or Prerender() HIT restored its stored values into
320
+ * `segmentIds` (cache-lookup.ts serveStoredSegments). The MISS ran each
321
+ * segment's handler after it started the segment's loaders, so a push into
322
+ * a restored array goes in front of the stored values until
323
+ * releaseRecordedValues: a loader's push lands where the same loader timing
324
+ * puts it on the MISS.
325
+ */
326
+ holdRecordedValues(segmentIds: readonly string[]): void;
327
+
328
+ /**
329
+ * The HIT's loader walk reached the place where the MISS ran
330
+ * `segmentId`'s handler (revalidation.ts resolveLoadersOnly,
331
+ * intercept-resolution.ts resolveInterceptLoadersOnly): later pushes
332
+ * follow the record's values.
333
+ */
334
+ releaseRecordedValues(segmentId: string): void;
335
+
314
336
  /**
315
337
  * Get all collected handle data after all handlers have settled.
316
338
  * Waits for `settled` (handler lane), then returns the data at that point.
@@ -482,6 +504,8 @@ export function createHandleStore(): HandleStore {
482
504
  // it restored placeholders for (pushPlaceholder).
483
505
  const restoredLoaders = new Set<string>();
484
506
  const placeholderLoaders = new Set<string>();
507
+ // Segments whose restored record values a push still goes in front of.
508
+ const held = new Set<string>();
485
509
  let authority: ((loaderId: string) => RecordAuthority) | undefined;
486
510
  const isRestored = (loaderId: string): boolean =>
487
511
  restoredLoaders.has(loaderId) || authority?.(loaderId) === "pin";
@@ -620,6 +644,16 @@ export function createHandleStore(): HandleStore {
620
644
  return at;
621
645
  }
622
646
 
647
+ // The first restored record value in `values` (holdRecordedValues).
648
+ function firstRecordSlot(values: unknown[]): number | undefined {
649
+ const tags = slotTags.get(values);
650
+ if (!tags) return undefined;
651
+ for (let i = 0; i < values.length; i++) {
652
+ if (tags[i]?.record) return i;
653
+ }
654
+ return undefined;
655
+ }
656
+
623
657
  // Where a live push by a loader with copies lands in `values`: its first
624
658
  // push removes all of its copies and takes the first one's position in this
625
659
  // array; later pushes follow the last push of its run. undefined: no place
@@ -954,6 +988,9 @@ export function createHandleStore(): HandleStore {
954
988
  at = restoredInsertIndex(values, restoredBy);
955
989
  if (replayOf === undefined) liveOf = restoredBy;
956
990
  }
991
+ if (at === undefined && held.has(segmentId)) {
992
+ at = firstRecordSlot(values);
993
+ }
957
994
  const by =
958
995
  getHandlePushOwner() ??
959
996
  replayOf ??
@@ -1058,6 +1095,24 @@ export function createHandleStore(): HandleStore {
1058
1095
  signalEmission();
1059
1096
  },
1060
1097
 
1098
+ holdRecordedValues(segmentIds: readonly string[]): void {
1099
+ for (const segmentId of segmentIds) {
1100
+ held.add(segmentId);
1101
+ for (const handleName in data) {
1102
+ const values = data[handleName][segmentId];
1103
+ if (!values?.length) continue;
1104
+ const tags = tagsFor(values);
1105
+ for (let i = 0; i < values.length; i++) {
1106
+ tags[i] = tags[i] ? { ...tags[i], record: true } : RECORD_SLOT;
1107
+ }
1108
+ }
1109
+ }
1110
+ },
1111
+
1112
+ releaseRecordedValues(segmentId: string): void {
1113
+ held.delete(segmentId);
1114
+ },
1115
+
1061
1116
  replacePlaceholders(
1062
1117
  loaderIds: readonly string[],
1063
1118
  deliver: () => void,