@rangojs/router 0.17.0 → 0.19.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 (293) hide show
  1. package/README.md +3 -2
  2. package/dist/types/browser/cookie-name.d.ts +6 -0
  3. package/dist/types/browser/event-controller.d.ts +3 -3
  4. package/dist/types/browser/history-state.d.ts +18 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  6. package/dist/types/browser/partial-update.d.ts +16 -1
  7. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  8. package/dist/types/browser/react/Link.d.ts +19 -9
  9. package/dist/types/browser/react/context.d.ts +2 -2
  10. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  11. package/dist/types/browser/react/location-state.d.ts +1 -1
  12. package/dist/types/browser/transition-when.d.ts +80 -0
  13. package/dist/types/browser/types.d.ts +104 -9
  14. package/dist/types/build/generate-manifest.d.ts +8 -0
  15. package/dist/types/cache/background-task.d.ts +24 -2
  16. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +48 -2
  18. package/dist/types/cache/cache-policy.d.ts +18 -1
  19. package/dist/types/cache/cache-runtime.d.ts +7 -0
  20. package/dist/types/cache/cache-scope.d.ts +192 -26
  21. package/dist/types/cache/cache-tag.d.ts +116 -11
  22. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  23. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  24. package/dist/types/cache/cf/cf-cache-store.d.ts +224 -72
  25. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  26. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  27. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +19 -0
  28. package/dist/types/cache/document-cache.d.ts +10 -0
  29. package/dist/types/cache/handle-snapshot.d.ts +41 -8
  30. package/dist/types/cache/index.d.ts +1 -0
  31. package/dist/types/cache/invalidation-order.d.ts +23 -0
  32. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  33. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  34. package/dist/types/cache/shell-memo.d.ts +178 -0
  35. package/dist/types/cache/shell-snapshot.d.ts +141 -105
  36. package/dist/types/cache/tag-invalidation.d.ts +54 -3
  37. package/dist/types/cache/types.d.ts +267 -96
  38. package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
  39. package/dist/types/client-urls/navigation.d.ts +28 -2
  40. package/dist/types/client-urls/server-projection.d.ts +5 -2
  41. package/dist/types/client-urls/types.d.ts +5 -5
  42. package/dist/types/client.d.ts +1 -1
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +1 -1
  45. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  46. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  47. package/dist/types/route-name.d.ts +7 -0
  48. package/dist/types/router/error-handling.d.ts +1 -1
  49. package/dist/types/router/instrument.d.ts +8 -0
  50. package/dist/types/router/intercept-resolution.d.ts +2 -0
  51. package/dist/types/router/match-context.d.ts +5 -0
  52. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  53. package/dist/types/router/middleware-types.d.ts +6 -1
  54. package/dist/types/router/router-options.d.ts +4 -1
  55. package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
  56. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  57. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
  58. package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
  59. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  60. package/dist/types/router/transition-when.d.ts +24 -10
  61. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  62. package/dist/types/rsc/full-payload.d.ts +21 -0
  63. package/dist/types/rsc/handler.d.ts +1 -1
  64. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  65. package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
  66. package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
  67. package/dist/types/rsc/shell-capture.d.ts +202 -74
  68. package/dist/types/rsc/shell-serve.d.ts +144 -50
  69. package/dist/types/rsc/types.d.ts +9 -0
  70. package/dist/types/segment-system.d.ts +7 -0
  71. package/dist/types/server/context.d.ts +155 -33
  72. package/dist/types/server/cookie-store.d.ts +33 -0
  73. package/dist/types/server/handle-store.d.ts +52 -1
  74. package/dist/types/server/request-context.d.ts +210 -63
  75. package/dist/types/server/request-headers.d.ts +44 -0
  76. package/dist/types/server.d.ts +1 -0
  77. package/dist/types/ssr/index.d.ts +9 -0
  78. package/dist/types/ssr/ssr-root.d.ts +9 -8
  79. package/dist/types/testing/e2e/index.d.ts +1 -1
  80. package/dist/types/testing/flight.entry.d.ts +7 -0
  81. package/dist/types/testing/index.d.ts +3 -2
  82. package/dist/types/testing/internal/action-id.d.ts +7 -0
  83. package/dist/types/testing/location-state-key.d.ts +24 -0
  84. package/dist/types/testing/render-route.d.ts +17 -1
  85. package/dist/types/testing/run-loader.d.ts +18 -3
  86. package/dist/types/testing/run-transition-when.d.ts +44 -69
  87. package/dist/types/testing/serve-shell-request.d.ts +133 -0
  88. package/dist/types/testing/shell-status.d.ts +44 -19
  89. package/dist/types/transition-when-ref.d.ts +79 -0
  90. package/dist/types/types/cache-types.d.ts +15 -3
  91. package/dist/types/types/handler-context.d.ts +14 -1
  92. package/dist/types/types/index.d.ts +1 -1
  93. package/dist/types/types/segments.d.ts +86 -71
  94. package/dist/types/urls/path-helper-types.d.ts +3 -4
  95. package/dist/types/urls/pattern-types.d.ts +21 -18
  96. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  97. package/dist/types/vite/router-discovery.d.ts +29 -2
  98. package/dist/vite/index.js +1005 -105
  99. package/package.json +1 -1
  100. package/skills/breadcrumbs/SKILL.md +17 -7
  101. package/skills/cache-guide/SKILL.md +175 -76
  102. package/skills/caching/SKILL.md +361 -33
  103. package/skills/catalog.json +1 -1
  104. package/skills/client-urls/SKILL.md +12 -7
  105. package/skills/cloudflare/SKILL.md +15 -0
  106. package/skills/comparison/references/framework-comparison.md +52 -37
  107. package/skills/defer-hydration/SKILL.md +50 -30
  108. package/skills/deployment-caching/SKILL.md +5 -2
  109. package/skills/document-cache/SKILL.md +32 -2
  110. package/skills/handler-use/SKILL.md +2 -0
  111. package/skills/hooks/SKILL.md +3 -1
  112. package/skills/hooks/handle-and-actions.md +3 -1
  113. package/skills/hooks/navigation.md +23 -2
  114. package/skills/hooks/state.md +101 -7
  115. package/skills/intercept/SKILL.md +29 -12
  116. package/skills/loader/SKILL.md +197 -44
  117. package/skills/middleware/SKILL.md +34 -9
  118. package/skills/migrate-nextjs/SKILL.md +68 -37
  119. package/skills/migrate-react-router/SKILL.md +2 -1
  120. package/skills/migrate-react-router/component-migration.md +3 -1
  121. package/skills/migrate-react-router/data-and-actions.md +3 -1
  122. package/skills/migrate-react-router/route-mapping.md +7 -4
  123. package/skills/observability/SKILL.md +65 -0
  124. package/skills/parallel/SKILL.md +13 -8
  125. package/skills/ppr/SKILL.md +715 -371
  126. package/skills/prerender/SKILL.md +30 -17
  127. package/skills/rango/SKILL.md +72 -34
  128. package/skills/route/SKILL.md +14 -7
  129. package/skills/router-setup/SKILL.md +4 -2
  130. package/skills/scripts/SKILL.md +8 -1
  131. package/skills/server-actions/SKILL.md +11 -0
  132. package/skills/shell-manifest/SKILL.md +21 -12
  133. package/skills/testing/SKILL.md +49 -43
  134. package/skills/testing/cache-prerender.md +15 -14
  135. package/skills/testing/client-components.md +45 -3
  136. package/skills/testing/loader.md +19 -1
  137. package/skills/testing/middleware.md +1 -0
  138. package/skills/testing/predicates.md +49 -49
  139. package/skills/testing/render-handler.md +1 -0
  140. package/skills/testing/serve-shell-request.md +202 -0
  141. package/skills/testing/server-actions.md +9 -3
  142. package/skills/theme/SKILL.md +56 -4
  143. package/skills/typesafety/env-and-bindings.md +18 -2
  144. package/skills/use-cache/SKILL.md +91 -23
  145. package/skills/vercel/SKILL.md +14 -2
  146. package/skills/view-transitions/SKILL.md +150 -29
  147. package/src/__augment-tests__/augmented.check.ts +23 -0
  148. package/src/browser/cookie-name.ts +23 -0
  149. package/src/browser/event-controller.ts +4 -4
  150. package/src/browser/history-state.ts +75 -1
  151. package/src/browser/link-interceptor.ts +4 -0
  152. package/src/browser/navigation-bridge.ts +77 -18
  153. package/src/browser/navigation-client.ts +4 -3
  154. package/src/browser/navigation-store.ts +43 -1
  155. package/src/browser/navigation-transaction.ts +31 -11
  156. package/src/browser/partial-update.ts +146 -35
  157. package/src/browser/prefetch/cache.ts +6 -6
  158. package/src/browser/prefetch/fetch.ts +2 -2
  159. package/src/browser/react/Link.tsx +38 -33
  160. package/src/browser/react/NavigationProvider.tsx +2 -2
  161. package/src/browser/react/context.ts +2 -2
  162. package/src/browser/react/location-state-shared.ts +241 -49
  163. package/src/browser/react/location-state.ts +5 -2
  164. package/src/browser/rsc-router.tsx +9 -0
  165. package/src/browser/server-action-bridge.ts +84 -15
  166. package/src/browser/transition-when.ts +205 -0
  167. package/src/browser/types.ts +124 -10
  168. package/src/build/generate-manifest.ts +77 -1
  169. package/src/cache/background-task.ts +55 -2
  170. package/src/cache/cache-exec-scope.ts +48 -1
  171. package/src/cache/cache-key-utils.ts +62 -2
  172. package/src/cache/cache-policy.ts +31 -4
  173. package/src/cache/cache-runtime.ts +209 -129
  174. package/src/cache/cache-scope.ts +573 -69
  175. package/src/cache/cache-tag.ts +475 -19
  176. package/src/cache/cf/cf-base64.ts +36 -4
  177. package/src/cache/cf/cf-cache-constants.ts +12 -0
  178. package/src/cache/cf/cf-cache-store.ts +1094 -376
  179. package/src/cache/cf/cf-cache-types.ts +32 -4
  180. package/src/cache/cf/cf-shell-frame.ts +258 -0
  181. package/src/cache/cf/cf-tag-marker-memo.ts +55 -40
  182. package/src/cache/document-cache.ts +53 -7
  183. package/src/cache/handle-snapshot.ts +53 -9
  184. package/src/cache/index.ts +2 -0
  185. package/src/cache/invalidation-order.ts +84 -0
  186. package/src/cache/isolate-tag-memo.ts +308 -0
  187. package/src/cache/request-tag-mask.ts +147 -0
  188. package/src/cache/shell-memo.ts +341 -0
  189. package/src/cache/shell-snapshot.ts +249 -277
  190. package/src/cache/tag-invalidation.ts +109 -18
  191. package/src/cache/types.ts +274 -102
  192. package/src/cache/vercel/vercel-cache-store.ts +561 -74
  193. package/src/client-urls/client-root.tsx +8 -4
  194. package/src/client-urls/client-urls.ts +8 -6
  195. package/src/client-urls/navigation.ts +55 -4
  196. package/src/client-urls/server-projection.ts +31 -10
  197. package/src/client-urls/types.ts +12 -5
  198. package/src/client.tsx +2 -0
  199. package/src/host/cookie-handler.ts +2 -1
  200. package/src/index.rsc.ts +3 -0
  201. package/src/index.ts +3 -0
  202. package/src/loader-redirect.tsx +7 -3
  203. package/src/prerender/build-shell-capture.ts +173 -110
  204. package/src/route-definition/dsl-helpers.ts +6 -3
  205. package/src/route-definition/helpers-types.ts +11 -11
  206. package/src/route-name.ts +12 -0
  207. package/src/router/content-negotiation.ts +4 -1
  208. package/src/router/error-handling.ts +5 -2
  209. package/src/router/handler-context.ts +42 -5
  210. package/src/router/instrument.ts +54 -2
  211. package/src/router/intercept-resolution.ts +30 -8
  212. package/src/router/loader-resolution.ts +89 -33
  213. package/src/router/logging.ts +5 -3
  214. package/src/router/match-api.ts +89 -42
  215. package/src/router/match-context.ts +5 -0
  216. package/src/router/match-handlers.ts +14 -29
  217. package/src/router/match-middleware/background-revalidation.ts +3 -5
  218. package/src/router/match-middleware/cache-lookup.ts +52 -19
  219. package/src/router/match-middleware/cache-store.ts +36 -10
  220. package/src/router/match-result.ts +1 -0
  221. package/src/router/middleware-types.ts +6 -1
  222. package/src/router/middleware.ts +20 -4
  223. package/src/router/navigation-snapshot.ts +6 -5
  224. package/src/router/prerender-match.ts +10 -0
  225. package/src/router/request-classification.ts +7 -4
  226. package/src/router/router-options.ts +4 -1
  227. package/src/router/segment-resolution/fresh.ts +18 -13
  228. package/src/router/segment-resolution/helpers.ts +4 -2
  229. package/src/router/segment-resolution/loader-cache.ts +366 -70
  230. package/src/router/segment-resolution/loader-mask.ts +3 -4
  231. package/src/router/segment-resolution/loader-snapshot.ts +215 -126
  232. package/src/router/segment-resolution/mask-nested.ts +161 -42
  233. package/src/router/segment-resolution/revalidation.ts +27 -13
  234. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  235. package/src/router/telemetry.ts +6 -3
  236. package/src/router/transition-when.ts +34 -52
  237. package/src/router.ts +2 -1
  238. package/src/rsc/attach-transition-when.ts +47 -0
  239. package/src/rsc/full-payload.ts +47 -3
  240. package/src/rsc/handler.ts +24 -10
  241. package/src/rsc/loader-fetch.ts +2 -1
  242. package/src/rsc/manifest-init.ts +4 -0
  243. package/src/rsc/origin-guard.ts +8 -5
  244. package/src/rsc/progressive-enhancement.ts +10 -32
  245. package/src/rsc/response-cache-serve.ts +69 -78
  246. package/src/rsc/response-route-handler.ts +2 -2
  247. package/src/rsc/rsc-rendering.ts +712 -338
  248. package/src/rsc/server-action.ts +8 -20
  249. package/src/rsc/shell-build-manifest.ts +49 -29
  250. package/src/rsc/shell-capture-constants.ts +182 -12
  251. package/src/rsc/shell-capture.ts +1126 -612
  252. package/src/rsc/shell-serve.ts +331 -85
  253. package/src/rsc/ssr-setup.ts +3 -2
  254. package/src/rsc/types.ts +9 -0
  255. package/src/segment-system.tsx +40 -7
  256. package/src/server/context.ts +328 -72
  257. package/src/server/cookie-store.ts +184 -133
  258. package/src/server/handle-store.ts +245 -27
  259. package/src/server/request-context.ts +364 -100
  260. package/src/server/request-headers.ts +134 -0
  261. package/src/server.ts +4 -0
  262. package/src/ssr/index.tsx +96 -37
  263. package/src/ssr/ssr-root.tsx +9 -8
  264. package/src/testing/dispatch.ts +4 -7
  265. package/src/testing/e2e/index.ts +1 -0
  266. package/src/testing/flight.entry.ts +14 -0
  267. package/src/testing/index.ts +5 -0
  268. package/src/testing/internal/action-id.ts +25 -0
  269. package/src/testing/internal/context.ts +1 -1
  270. package/src/testing/location-state-key.ts +45 -0
  271. package/src/testing/render-handler.ts +1 -1
  272. package/src/testing/render-route.tsx +109 -16
  273. package/src/testing/run-client-revalidate.ts +3 -19
  274. package/src/testing/run-loader.ts +67 -5
  275. package/src/testing/run-transition-when.ts +109 -172
  276. package/src/testing/serve-shell-request.ts +381 -0
  277. package/src/testing/shell-status.ts +73 -47
  278. package/src/theme/ThemeProvider.tsx +7 -6
  279. package/src/transition-when-ref.ts +220 -0
  280. package/src/types/cache-types.ts +15 -3
  281. package/src/types/handler-context.ts +14 -1
  282. package/src/types/index.ts +3 -0
  283. package/src/types/segments.ts +94 -91
  284. package/src/urls/path-helper-types.ts +3 -4
  285. package/src/urls/pattern-types.ts +21 -18
  286. package/src/vite/discovery/discover-routers.ts +3 -0
  287. package/src/vite/discovery/shell-prerender-phase.ts +9 -2
  288. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  289. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  290. package/src/vite/rango.ts +2 -0
  291. package/src/vite/router-discovery.ts +89 -9
  292. package/dist/types/rsc/transition-gate.d.ts +0 -23
  293. package/src/rsc/transition-gate.ts +0 -86
@@ -15,6 +15,7 @@
15
15
  * wraps revalidation inside route middleware — identical to a normal render.
16
16
  */
17
17
 
18
+ import { requestHeaders } from "../server/request-headers.js";
18
19
  import {
19
20
  getRequestContext,
20
21
  setRequestContextParams,
@@ -22,7 +23,8 @@ import {
22
23
  import { observePhase, PHASES } from "../router/instrument.js";
23
24
  import { resolveActionRefId } from "../router/is-action.js";
24
25
  import type { TraceSpan } from "../router/tracing.js";
25
- import { gateTransitions } from "./transition-gate.js";
26
+ import { attachTransitionWhen } from "./attach-transition-when.js";
27
+ import { publicRouteName } from "../route-name.js";
26
28
  import type { RscPayload } from "./types.js";
27
29
  import {
28
30
  hasBodyContent,
@@ -93,7 +95,7 @@ export async function executeServerAction<TEnv>(
93
95
  const temporaryReferences = ctx.createTemporaryReferenceSet();
94
96
 
95
97
  // Decode action arguments from request body
96
- const contentType = request.headers.get("content-type") || "";
98
+ const contentType = requestHeaders(request).get("content-type") || "";
97
99
  let args: unknown[] = [];
98
100
  let actionFormData: FormData | undefined;
99
101
 
@@ -381,14 +383,6 @@ async function revalidateAfterActionInner<TEnv>(
381
383
  ): Promise<Response> {
382
384
  const reqCtx = getRequestContext();
383
385
  const { actionContext } = continuation;
384
- // Expose the action that triggered this revalidation to the transition({ when })
385
- // gate (covers both the error-boundary and success gate calls below). Mirrors
386
- // the action fields a revalidate() predicate sees.
387
- reqCtx._gateActionId = actionContext?.actionId;
388
- reqCtx._gateActionUrl = actionContext?.actionUrl;
389
- reqCtx._gateActionResult = actionContext?.actionResult;
390
- reqCtx._gateFormData = actionContext?.formData;
391
-
392
386
  // Mark the rest of this request as an action revalidation render. The "use
393
387
  // cache" runtime reads this to re-execute a stale entry in the foreground
394
388
  // (fresh data in the action response) rather than serving stale + revalidating
@@ -523,16 +517,13 @@ function renderActionBoundaryResponse<TEnv>(
523
517
  // routerId exposed for the frontend (current app identity); see
524
518
  // rsc-rendering.ts partial branch.
525
519
  routerId: ctx.router.id,
526
- segments: gateTransitions(
527
- errorBoundary.segments,
528
- reqCtx,
529
- ctx.router.onError,
530
- ),
520
+ segments: attachTransitionWhen(errorBoundary.segments, reqCtx),
531
521
  isPartial: true,
532
522
  matched: errorBoundary.matched,
533
523
  diff: errorBoundary.diff,
534
524
  resolvedIds: errorBoundary.resolvedIds,
535
525
  params: errorBoundary.params,
526
+ routeName: publicRouteName(errorBoundary.routeName),
536
527
  isError: true,
537
528
  handles: handleStore.stream(),
538
529
  version: ctx.version,
@@ -589,16 +580,13 @@ function renderRevalidationResponse<TEnv>(
589
580
  // routerId exposed for the frontend (current app identity); see
590
581
  // rsc-rendering.ts partial branch.
591
582
  routerId: ctx.router.id,
592
- segments: gateTransitions(
593
- matchResult.segments,
594
- reqCtx,
595
- ctx.router.onError,
596
- ),
583
+ segments: attachTransitionWhen(matchResult.segments, reqCtx),
597
584
  isPartial: true,
598
585
  matched: matchResult.matched,
599
586
  diff: matchResult.diff,
600
587
  resolvedIds: matchResult.resolvedIds,
601
588
  params: matchResult.params,
589
+ routeName: publicRouteName(matchResult.routeName),
602
590
  slots: matchResult.slots,
603
591
  handles: handleStore.stream(),
604
592
  version: ctx.version,
@@ -37,7 +37,11 @@ import {
37
37
  hasIntactShellPayload,
38
38
  isValidShellHit,
39
39
  } from "./shell-serve.js";
40
- import { SHELL_CAPTURE_MAX_WAIT_MS } from "./shell-capture.js";
40
+ import {
41
+ SHELL_CAPTURE_MAX_WAIT_MS,
42
+ resetShellWarningsForTests,
43
+ warnOnce,
44
+ } from "./shell-capture-constants.js";
41
45
  import { buildShellManifestKey } from "../prerender/shell-manifest-key.js";
42
46
 
43
47
  /** One baked manifest record (the __ps asset module's default export). */
@@ -45,10 +49,8 @@ export interface BuildShellEntry {
45
49
  entry: ShellCacheEntry;
46
50
  /** Resolved ppr ttl (seconds) — drives staleness/recapture, never expiry. */
47
51
  ttl: number;
48
- swr?: number;
49
52
  /** The putShell-barrier tag union baked at build (static + recorded). */
50
53
  tags?: string[];
51
- routeName: string;
52
54
  }
53
55
 
54
56
  interface ShellManifestModule {
@@ -81,16 +83,16 @@ function loadManifest(): Promise<ShellManifestModule | null> {
81
83
  }
82
84
 
83
85
  /**
84
- * Per-spec verdict memo for the version + integrity gates. A manifest record
86
+ * Per-spec verdict memo for the version + structural gates. A manifest record
85
87
  * is immutable for the process lifetime (content-hashed asset module), so its
86
- * gate verdict is constant — without this, EVERY request to a baked ppr route
87
- * re-decodes the full prelude base64 (hasIntactShellPayload) on the hot path:
88
- * a fresh build hit never populates the runtime store, so the store MISS +
89
- * read-through is the steady state, not a warm-up. Only the per-request
90
- * gates (tag markers, staleness) stay outside the memo. `null` memoizes a
91
- * failed verdict — deterministically invalid, don't re-pay the decode.
92
- * Spec-only keying is sound because buildVersion is process-constant on the
93
- * manifest path (folded into the shipped worker; dev never loads a manifest).
88
+ * gate verdict is constant: a fresh build hit never populates the runtime
89
+ * store, so the store MISS + read-through is the steady state, not a warm-up.
90
+ * Only the per-request gates (tag markers, staleness) stay outside the memo;
91
+ * the document HIT decodes the prelude once per serve (openShellDocument),
92
+ * partial replay never does. `null` memoizes a failed verdict —
93
+ * deterministically invalid. Spec-only keying is sound because buildVersion is
94
+ * process-constant on the manifest path (folded into the shipped worker; dev
95
+ * never loads a manifest).
94
96
  */
95
97
  const validatedSpecs = new Map<string, ValidatedBuildShellEntry | null>();
96
98
 
@@ -121,15 +123,16 @@ async function validatedManifestRecord(
121
123
  return verdict ?? undefined;
122
124
  }
123
125
 
124
- /** Reset the memoized manifest (unit tests swap the global loader). */
126
+ /**
127
+ * Reset the memoized manifest (unit tests swap the global loader) and the
128
+ * shell path's once-per-key warnings, the tag-check one included.
129
+ */
125
130
  export function resetBuildShellManifestForTests(): void {
126
131
  manifestPromise = null;
127
132
  validatedSpecs.clear();
133
+ resetShellWarningsForTests();
128
134
  }
129
135
 
130
- /** Keys already warned about a tag-check-incapable store (once per key). */
131
- const warnedTagCheckUnsupported = new Set<string>();
132
-
133
136
  export interface BuildShellHit {
134
137
  entry: DocumentShellCacheEntry;
135
138
  /** Past createdAt + ttl: serve, but schedule the runtime recapture. */
@@ -259,6 +262,24 @@ async function fetchDevShellEntry(
259
262
  }
260
263
  }
261
264
 
265
+ /**
266
+ * Whether a build-time shell exists for `pathname` (the caller checks the
267
+ * request is search-less, like the entries lookupBuildShell serves): a
268
+ * manifest entry for the path in production, a Prerender route in dev. No
269
+ * asset load and no dev capture: it only decides whether a request that
270
+ * cannot read the build shell deserves a warning.
271
+ */
272
+ export async function hasBuildShell(
273
+ pathname: string,
274
+ dev?: DevShellLookup,
275
+ ): Promise<boolean> {
276
+ if (globalThis.__loadShellManifestModule === undefined) {
277
+ return dev?.isPrerenderRoute === true;
278
+ }
279
+ const mod = await loadManifest();
280
+ return mod?.default[buildShellManifestKey(pathname)] !== undefined;
281
+ }
282
+
262
283
  /**
263
284
  * Look up the baked shell entry for a request, applying every serve gate:
264
285
  * search-less requests only (the build captured the bare pathname; a
@@ -315,20 +336,19 @@ export async function lookupBuildShell(
315
336
  // memo-only answers would let the immutable asset resurrect on the
316
337
  // next request). Declared intent that cannot be honored deserves a
317
338
  // diagnostic; the route keeps runtime-capture semantics.
318
- const key = buildShellManifestKey(url.pathname);
319
- if (!warnedTagCheckUnsupported.has(key)) {
320
- warnedTagCheckUnsupported.add(key);
321
- console.warn(
339
+ warnOnce(
340
+ "build-shell-tag-check",
341
+ buildShellManifestKey(url.pathname),
342
+ () =>
322
343
  `[rango] Build-time shell for "${url.pathname}" carries cache tags, but ` +
323
- "the app cache store cannot answer tag-invalidation history durably " +
324
- (typeof check !== "function"
325
- ? "(isTagsInvalidatedSince() is not implemented), "
326
- : "(no durable tag history — a CFCacheStore without a KV namespace), ") +
327
- "so updateTag() could not evict it. The entry is not served; the route " +
328
- "keeps runtime shell capture. Use MemorySegmentCacheStore, a KV-backed " +
329
- "CFCacheStore, or VercelCacheStore (or add the method to your custom store).",
330
- );
331
- }
344
+ "the app cache store cannot answer tag-invalidation history durably " +
345
+ (typeof check !== "function"
346
+ ? "(isTagsInvalidatedSince() is not implemented), "
347
+ : "(no durable tag history — a CFCacheStore without a KV namespace), ") +
348
+ "so updateTag() could not evict it. The entry is not served; the route " +
349
+ "keeps runtime shell capture. Use MemorySegmentCacheStore, a KV-backed " +
350
+ "CFCacheStore, or VercelCacheStore (or add the method to your custom store).",
351
+ );
332
352
  return null;
333
353
  }
334
354
  if (await check.call(store, record.tags, entry.createdAt)) return null;
@@ -1,31 +1,160 @@
1
+ /**
2
+ * React-free leaf of the PPR shell path: its constants, the shell key
3
+ * builders, the status header names and bypass reasons, and the once-per-key
4
+ * warning registry. The ssr graph (ssr/index.tsx), the testing barrels
5
+ * (testing/shell-status.ts) and cookie-store.ts import from here without
6
+ * pulling the capture orchestration module or shell-serve.ts (React).
7
+ */
8
+
9
+ import { sortedSearchString } from "../cache/cache-key-utils.js";
10
+ import type { SearchParamsFilter } from "../cache/search-params-filter.js";
11
+
1
12
  /**
2
13
  * Default upper bound on the capture prerender wait before forcing the abort
3
14
  * that freezes the shell — the ONLY wall-clock on the capture path, and a
4
15
  * pathological guard: it should never fire once the caller's `quiesce` is a
5
16
  * task-quantized, frozen-byte signal (the capture gate in shell-capture.ts).
6
- *
7
- * Leaf module on purpose (imports nothing): shell-capture.ts re-exports it
8
- * (single import site for the rsc graph — shell-build-manifest.ts sizes the
9
- * dev fetch envelope from that re-export) and ssr/index.tsx uses it as
10
- * captureShellHTML's own fallback; the ssr graph must not pull the capture
11
- * orchestration module for one number.
17
+ * ssr/index.tsx uses it as captureShellHTML's own fallback.
12
18
  *
13
19
  * 15s, raised from 5s: captures are background work (waitUntil), so the budget
14
20
  * costs latency-to-HIT only — never a served response — and 5s spuriously
15
21
  * refused legitimately-slow deferred shell material (a real storefront's meta
16
22
  * chains settle at ~7s). Ceiling math: workerd's waitUntil lifetime is ~30s
17
- * past response completion, and a guaranteed two-attempt envelope needs
18
- * 2x budget + the in-place retry delay + store I/O, i.e. budget <= ~14s. The
19
- * 15s default deliberately sits just past that: attempt 1 always gets its
20
- * full 15s, but when it consumed the whole budget the in-place RETRY may be
21
- * truncated by the platform kill on workerd — degrading to the existing
22
- * best-effort contract (the key stays MISS; a later request re-captures).
23
+ * past response completion. An attempt that consumed the whole budget is not
24
+ * retried in place (shell-capture.ts CaptureAttemptStats.noShellCause), so
25
+ * the envelope is one budget plus store I/O; the retry follows only an
26
+ * attempt that ended early (a cold-module abort). Past the ceiling the
27
+ * platform kill degrades to the existing best-effort contract (the key stays
28
+ * MISS; a later request re-captures).
23
29
  * Node/dev and build-time captures have no waitUntil ceiling. The per-route
24
30
  * `ppr.captureTimeout` knob remains for tightening below the default. See
25
31
  * docs/design/ppr-shell-resume.md (Cost model).
26
32
  */
27
33
  export const SHELL_CAPTURE_MAX_WAIT_MS = 15_000;
28
34
 
35
+ /** Debug/status header on a ppr document response: `HIT` | `MISS`. */
36
+ export const SHELL_STATUS_HEADER: string = "x-rango-shell";
37
+
38
+ /**
39
+ * Partial-navigation replay status header, set only after the captured
40
+ * segment record was consumed (`HIT`) or with the reason it was not.
41
+ */
42
+ export const PPR_REPLAY_STATUS_HEADER: string = "x-rango-ppr-replay";
43
+
44
+ /**
45
+ * Bounded reasons a partial request to a ppr route falls open to the ordinary
46
+ * match (`x-rango-ppr-replay: BYPASS; reason=<reason>`). One list for the
47
+ * producer (rsc-rendering.ts) and the parser (testing/shell-status.ts): a
48
+ * reason the parser does not list parses as null.
49
+ */
50
+ export const PPR_REPLAY_BYPASS_REASONS = [
51
+ "method",
52
+ "dynamic",
53
+ "nonce",
54
+ "store-unavailable",
55
+ "passive-read-unsupported",
56
+ "no-navigation-context",
57
+ "prerender-store",
58
+ "intercept",
59
+ "cache-disabled",
60
+ "read-error",
61
+ "no-entry",
62
+ "invalid-version",
63
+ "corrupt-entry",
64
+ "no-segment-snapshot",
65
+ "snapshot-miss",
66
+ "explicit-cache-hit",
67
+ ] as const;
68
+
69
+ /** One of {@link PPR_REPLAY_BYPASS_REASONS}. */
70
+ export type PprReplayBypassReason = (typeof PPR_REPLAY_BYPASS_REASONS)[number];
71
+
72
+ /**
73
+ * The shell key's search portion (`?`-prefixed sorted search with the
74
+ * cache.searchParams filter applied, or "") — ALSO the string the capture and
75
+ * resume SSR renders seed their store location with (SSRRenderOptions.search
76
+ * / ShellCaptureOptions.search / ShellResumeOptions.search). Search is part
77
+ * of shell identity, so static-part `useSearchParams` reads render exactly
78
+ * what the key names; deriving seed and key from this one helper is what
79
+ * keeps capture, resume, and lookup byte-agreed (drift = replay mismatch or
80
+ * permanent MISS).
81
+ */
82
+ export function shellSearchSeed(url: URL, filter?: SearchParamsFilter): string {
83
+ const sorted = sortedSearchString(url.searchParams, filter);
84
+ return sorted ? `?${sorted}` : "";
85
+ }
86
+
87
+ /**
88
+ * Shell cache key: host + pathname + sorted search + a `:shell` namespace suffix
89
+ * (so it can never collide with a document-cache key; the store further isolates
90
+ * the shell family internally).
91
+ *
92
+ * The key includes the request HOST: in a multi-tenant host-router deployment
93
+ * (one worker, one shared KV/runtime-cache store) a host-less key would serve
94
+ * tenant A's captured shell to tenant B's users.
95
+ *
96
+ * `filter` is the request's compiled `cache.searchParams` config
97
+ * (ctx._searchParamsFilter): excluded params collapse onto one shell slot.
98
+ * Callers on the serve/capture path MUST pass it -- key drift between capture
99
+ * and lookup makes every shell request a permanent miss. The testing helper
100
+ * `shellCacheKey` (testing/shell-status.ts) builds its key with it too.
101
+ */
102
+ export function buildShellKey(url: URL, filter?: SearchParamsFilter): string {
103
+ return `${url.host}${url.pathname}${shellSearchSeed(url, filter)}:shell`;
104
+ }
105
+
106
+ /**
107
+ * The shell key for one request partition (cache-scope.ts
108
+ * resolveShellPartition): the route cache() `key()` or store keyGenerator
109
+ * result that partitions the route's record partitions its shell too, so
110
+ * each partition captures and serves its own.
111
+ *
112
+ * The partition is URI-encoded: it is request-derived, and raw it could end
113
+ * in a suffix another key is built with (navigationShellKey), so a partition
114
+ * `gold:navigation` named gold's navigation entry and its document shell
115
+ * replayed to gold's navigations. Encoded, it holds no `:` or `|`.
116
+ */
117
+ export function partitionShellKey(key: string, partition: string): string {
118
+ return `${key}|${encodeURIComponent(partition)}`;
119
+ }
120
+
121
+ /**
122
+ * The navigation-only entry beside a document shell key (a cold partial
123
+ * request's capture, rsc-rendering.ts). A suffix after the (encoded)
124
+ * partition: partitionShellKey keeps the two unambiguous.
125
+ */
126
+ export function navigationShellKey(key: string): string {
127
+ return `${key}:navigation`;
128
+ }
129
+
130
+ /**
131
+ * Fixed number of macrotask hops between `quiesce` resolving and the abort. These
132
+ * give React's fizz worker turns to flush the settled shell into the prelude and
133
+ * mark still-pending boundaries as POSTPONED (rather than errored) before
134
+ * controller.abort() lands. Not a wall-clock wait. Taken by captureShellHTML
135
+ * (ssr/index.tsx) and by the testing SSR stub (testing/serve-shell-request.ts),
136
+ * so the stub freezes the same Flight the real capture would.
137
+ *
138
+ * Why 16 and not 2: the capture's Flight input is the doc record's stored
139
+ * fragments (settleCaptureRecord, shell-capture.ts), already serialized, so the
140
+ * render emits the whole shell payload in the first tick and the gate declares
141
+ * quiesce within a few ms, before fizz has rendered the shell to <body>. When
142
+ * the capture Flight dribbled out as handlers ran, Flight-quiet meant "the shell
143
+ * has rendered" and 2 hops sufficed.
144
+ *
145
+ * Hops alone are NOT render-readiness: fizz cannot emit even <html> until the
146
+ * payload root settles, nor complete a shell whose client components are
147
+ * still loading — real module-runner I/O in dev (100ms+ cold), which no fixed
148
+ * count of near-zero-cost task hops can buy. captureShellHTML therefore
149
+ * awaits the payload-settled signal (SsrRootOptions.onPayloadSettled) and
150
+ * then the client-reference loads in flight in the isolate (captureClientLoads),
151
+ * deadline-bounded, between quiesce and these hops; the hops then only flush
152
+ * the settled tree and mark pending boundaries POSTPONED. Still task-based
153
+ * (masked loaders never emit, so more hops never lets a hole settle). Bounded
154
+ * by maxWaitMs end to end.
155
+ */
156
+ export const POST_QUIESCE_TASK_HOPS = 16;
157
+
29
158
  /**
30
159
  * Hard cap on one capture TASK — runShellCapture end to end (both attempts +
31
160
  * the in-place retry delay). SHELL_CAPTURE_MAX_WAIT_MS arms only inside
@@ -56,3 +185,44 @@ export const PPR_LANE_HINT: string =
56
185
  "non-promise data; every other loader is live (masked at capture, fresh per " +
57
186
  "request) and needs loading() or an inline <Suspense> above its reader. See the " +
58
187
  '/ppr skill, "The loader lane rule" (node_modules/@rangojs/router/skills/ppr/SKILL.md).';
188
+
189
+ /**
190
+ * Once-per-key console warnings of the PPR shell path (the capture, the serve
191
+ * gates, the build-shell read-through), per isolate. One registry, so one
192
+ * reset clears them all ({@link resetShellWarningsForTests}).
193
+ */
194
+ const warnedShellKeys = new Set<string>();
195
+
196
+ function shellWarningId(scope: string, key: string): string {
197
+ return `${scope}\u0000${key}`;
198
+ }
199
+
200
+ /** Whether {@link warnOnce} already warned for `scope` and `key`. */
201
+ export function hasWarnedOnce(scope: string, key: string): boolean {
202
+ return warnedShellKeys.has(shellWarningId(scope, key));
203
+ }
204
+
205
+ /**
206
+ * `console.warn(message())` once per `scope` and `key`; `message` is built only
207
+ * when it prints. Returns true when it warned.
208
+ */
209
+ export function warnOnce(
210
+ scope: string,
211
+ key: string,
212
+ message: () => string,
213
+ ): boolean {
214
+ const id = shellWarningId(scope, key);
215
+ if (warnedShellKeys.has(id)) return false;
216
+ warnedShellKeys.add(id);
217
+ console.warn(message());
218
+ return true;
219
+ }
220
+
221
+ /**
222
+ * @internal Forget every once-per-key shell warning. Tests only: the capture,
223
+ * serve and build-shell resets (testing/serve-shell-request.ts
224
+ * resetShellTestState) call it.
225
+ */
226
+ export function resetShellWarningsForTests(): void {
227
+ warnedShellKeys.clear();
228
+ }