@rangojs/router 0.16.0 → 0.18.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 (309) hide show
  1. package/README.md +255 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/cookie-name.d.ts +6 -0
  4. package/dist/types/browser/history-state.d.ts +16 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  6. package/dist/types/browser/partial-update.d.ts +11 -0
  7. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  8. package/dist/types/browser/rsc-router.d.ts +1 -1
  9. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  10. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  11. package/dist/types/cache/cache-key-utils.d.ts +17 -2
  12. package/dist/types/cache/cache-policy.d.ts +17 -0
  13. package/dist/types/cache/cache-scope.d.ts +132 -19
  14. package/dist/types/cache/cache-tag.d.ts +92 -7
  15. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
  18. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  19. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  20. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
  21. package/dist/types/cache/handle-capture.d.ts +41 -3
  22. package/dist/types/cache/handle-snapshot.d.ts +54 -13
  23. package/dist/types/cache/index.d.ts +1 -0
  24. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  25. package/dist/types/cache/segment-codec.d.ts +10 -3
  26. package/dist/types/cache/shell-memo.d.ts +178 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +110 -21
  28. package/dist/types/cache/tag-invalidation.d.ts +30 -3
  29. package/dist/types/cache/taint.d.ts +1 -1
  30. package/dist/types/cache/types.d.ts +220 -44
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
  32. package/dist/types/client.d.ts +4 -4
  33. package/dist/types/client.rsc.d.ts +4 -5
  34. package/dist/types/components/DefaultDocument.d.ts +5 -2
  35. package/dist/types/context-var.d.ts +6 -5
  36. package/dist/types/escape-script.d.ts +1 -1
  37. package/dist/types/handles/MetaTags.d.ts +5 -4
  38. package/dist/types/handles/Scripts.d.ts +8 -7
  39. package/dist/types/handles/meta.d.ts +2 -2
  40. package/dist/types/handles/script.d.ts +17 -16
  41. package/dist/types/href-client.d.ts +1 -1
  42. package/dist/types/html.d.ts +12 -0
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +3 -2
  45. package/dist/types/loader-store.d.ts +7 -4
  46. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  47. package/dist/types/reverse.d.ts +36 -4
  48. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  49. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  50. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  51. package/dist/types/route-types.d.ts +1 -1
  52. package/dist/types/router/debug-manifest.d.ts +3 -2
  53. package/dist/types/router/error-handling.d.ts +20 -0
  54. package/dist/types/router/handler-context.d.ts +6 -0
  55. package/dist/types/router/instrument.d.ts +8 -0
  56. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  57. package/dist/types/router/middleware-types.d.ts +9 -3
  58. package/dist/types/router/router-context.d.ts +3 -1
  59. package/dist/types/router/router-interfaces.d.ts +24 -2
  60. package/dist/types/router/router-options.d.ts +8 -5
  61. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  62. package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
  63. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  64. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
  65. package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  67. package/dist/types/router/segment-wrappers.d.ts +1 -1
  68. package/dist/types/rsc/full-payload.d.ts +14 -0
  69. package/dist/types/rsc/handler.d.ts +1 -1
  70. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  71. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  72. package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
  73. package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
  74. package/dist/types/rsc/shell-capture.d.ts +151 -19
  75. package/dist/types/rsc/shell-serve.d.ts +140 -17
  76. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  77. package/dist/types/rsc/types.d.ts +17 -0
  78. package/dist/types/server/context.d.ts +139 -15
  79. package/dist/types/server/cookie-store.d.ts +21 -0
  80. package/dist/types/server/handle-store.d.ts +26 -2
  81. package/dist/types/server/request-context.d.ts +182 -53
  82. package/dist/types/ssr/index.d.ts +24 -0
  83. package/dist/types/ssr/ssr-root.d.ts +9 -8
  84. package/dist/types/static-handler.d.ts +4 -1
  85. package/dist/types/testing/flight.entry.d.ts +7 -0
  86. package/dist/types/testing/generated-routes.d.ts +2 -2
  87. package/dist/types/testing/internal/context.d.ts +1 -1
  88. package/dist/types/testing/render-route.d.ts +32 -6
  89. package/dist/types/testing/run-loader.d.ts +18 -3
  90. package/dist/types/testing/serve-shell-request.d.ts +132 -0
  91. package/dist/types/testing/shell-status.d.ts +23 -10
  92. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  93. package/dist/types/testing/vitest.d.ts +35 -1
  94. package/dist/types/theme/ThemeScript.d.ts +3 -3
  95. package/dist/types/types/cache-types.d.ts +14 -10
  96. package/dist/types/types/handler-context.d.ts +28 -1
  97. package/dist/types/types/loader-types.d.ts +8 -6
  98. package/dist/types/urls/pattern-types.d.ts +22 -18
  99. package/dist/types/urls/response-types.d.ts +3 -12
  100. package/dist/types/use-loader.d.ts +12 -3
  101. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  102. package/dist/types/vite/rango.d.ts +1 -1
  103. package/dist/types/vite/router-discovery.d.ts +29 -2
  104. package/dist/vite/index.js +1420 -1348
  105. package/package.json +2 -2
  106. package/skills/api-client/SKILL.md +22 -10
  107. package/skills/breadcrumbs/SKILL.md +75 -22
  108. package/skills/bundle-analysis/SKILL.md +22 -22
  109. package/skills/cache-guide/SKILL.md +306 -124
  110. package/skills/caching/SKILL.md +530 -98
  111. package/skills/catalog.json +16 -16
  112. package/skills/client-urls/SKILL.md +65 -38
  113. package/skills/cloudflare/SKILL.md +87 -20
  114. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  115. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  116. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  117. package/skills/comparison/SKILL.md +5 -0
  118. package/skills/comparison/references/framework-comparison.md +75 -57
  119. package/skills/composability/SKILL.md +38 -29
  120. package/skills/css/SKILL.md +10 -13
  121. package/skills/debug-manifest/SKILL.md +74 -31
  122. package/skills/defer-hydration/SKILL.md +52 -31
  123. package/skills/deployment-caching/SKILL.md +11 -3
  124. package/skills/document-cache/SKILL.md +98 -28
  125. package/skills/fonts/SKILL.md +16 -8
  126. package/skills/handler-use/SKILL.md +55 -17
  127. package/skills/hooks/SKILL.md +28 -20
  128. package/skills/hooks/data.md +23 -3
  129. package/skills/hooks/handle-and-actions.md +34 -17
  130. package/skills/hooks/navigation.md +39 -10
  131. package/skills/hooks/outlets.md +13 -6
  132. package/skills/hooks/state.md +39 -15
  133. package/skills/hooks/urls.md +7 -5
  134. package/skills/host-router/SKILL.md +64 -12
  135. package/skills/i18n/SKILL.md +60 -34
  136. package/skills/intercept/SKILL.md +161 -26
  137. package/skills/layout/SKILL.md +106 -50
  138. package/skills/links/SKILL.md +51 -19
  139. package/skills/loader/SKILL.md +386 -128
  140. package/skills/middleware/SKILL.md +129 -26
  141. package/skills/migrate-nextjs/SKILL.md +156 -70
  142. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  143. package/skills/migrate-react-router/SKILL.md +51 -39
  144. package/skills/migrate-react-router/component-migration.md +28 -7
  145. package/skills/migrate-react-router/data-and-actions.md +21 -6
  146. package/skills/migrate-react-router/route-mapping.md +19 -10
  147. package/skills/mime-routes/SKILL.md +12 -7
  148. package/skills/observability/SKILL.md +92 -14
  149. package/skills/parallel/SKILL.md +88 -47
  150. package/skills/ppr/SKILL.md +866 -379
  151. package/skills/prerender/SKILL.md +210 -108
  152. package/skills/rango/SKILL.md +115 -65
  153. package/skills/react-compiler/SKILL.md +6 -5
  154. package/skills/response-routes/SKILL.md +133 -25
  155. package/skills/route/SKILL.md +170 -60
  156. package/skills/router-setup/SKILL.md +420 -240
  157. package/skills/scripts/SKILL.md +39 -21
  158. package/skills/server-actions/SKILL.md +231 -59
  159. package/skills/shell-manifest/SKILL.md +28 -14
  160. package/skills/streams-and-websockets/SKILL.md +32 -14
  161. package/skills/tailwind/SKILL.md +37 -33
  162. package/skills/testing/SKILL.md +73 -33
  163. package/skills/testing/cache-prerender.md +60 -33
  164. package/skills/testing/client-components.md +59 -6
  165. package/skills/testing/e2e-parity.md +12 -2
  166. package/skills/testing/flight.md +19 -11
  167. package/skills/testing/handles.md +8 -5
  168. package/skills/testing/loader.md +26 -6
  169. package/skills/testing/middleware.md +8 -3
  170. package/skills/testing/predicates.md +120 -0
  171. package/skills/testing/render-handler.md +70 -22
  172. package/skills/testing/response-routes.md +11 -4
  173. package/skills/testing/reverse-and-types.md +20 -14
  174. package/skills/testing/serve-shell-request.md +188 -0
  175. package/skills/testing/server-actions.md +12 -3
  176. package/skills/testing/server-tree.md +9 -0
  177. package/skills/testing/setup.md +9 -5
  178. package/skills/theme/SKILL.md +155 -30
  179. package/skills/typesafety/SKILL.md +20 -9
  180. package/skills/typesafety/env-and-bindings.md +137 -50
  181. package/skills/typesafety/generated-files-and-cli.md +26 -14
  182. package/skills/typesafety/params-and-search.md +44 -19
  183. package/skills/typesafety/route-types.md +50 -14
  184. package/skills/use-cache/SKILL.md +224 -83
  185. package/skills/vercel/SKILL.md +35 -13
  186. package/skills/view-transitions/SKILL.md +42 -21
  187. package/src/__augment-tests__/augmented.check.ts +26 -1
  188. package/src/browser/cookie-name.ts +23 -0
  189. package/src/browser/history-state.ts +23 -0
  190. package/src/browser/index.ts +2 -2
  191. package/src/browser/navigation-bridge.ts +3 -4
  192. package/src/browser/navigation-transaction.ts +22 -2
  193. package/src/browser/partial-update.ts +35 -25
  194. package/src/browser/react/ScrollRestoration.tsx +6 -6
  195. package/src/browser/rsc-router.tsx +6 -1
  196. package/src/browser/scroll-restoration.ts +53 -6
  197. package/src/cache/cache-exec-scope.ts +37 -4
  198. package/src/cache/cache-key-utils.ts +23 -8
  199. package/src/cache/cache-policy.ts +22 -0
  200. package/src/cache/cache-runtime.ts +264 -68
  201. package/src/cache/cache-scope.ts +419 -43
  202. package/src/cache/cache-tag.ts +375 -11
  203. package/src/cache/cf/cf-base64.ts +36 -4
  204. package/src/cache/cf/cf-cache-constants.ts +12 -0
  205. package/src/cache/cf/cf-cache-store.ts +990 -348
  206. package/src/cache/cf/cf-cache-types.ts +32 -4
  207. package/src/cache/cf/cf-shell-frame.ts +258 -0
  208. package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
  209. package/src/cache/document-cache.ts +15 -0
  210. package/src/cache/handle-capture.ts +90 -10
  211. package/src/cache/handle-snapshot.ts +133 -28
  212. package/src/cache/index.ts +2 -0
  213. package/src/cache/isolate-tag-memo.ts +308 -0
  214. package/src/cache/segment-codec.ts +26 -7
  215. package/src/cache/shell-memo.ts +341 -0
  216. package/src/cache/shell-snapshot.ts +217 -27
  217. package/src/cache/tag-invalidation.ts +108 -22
  218. package/src/cache/taint.ts +1 -1
  219. package/src/cache/types.ts +225 -44
  220. package/src/cache/vercel/vercel-cache-store.ts +545 -61
  221. package/src/client.rsc.tsx +10 -9
  222. package/src/client.tsx +5 -4
  223. package/src/components/DefaultDocument.tsx +7 -2
  224. package/src/context-var.ts +6 -5
  225. package/src/escape-script.ts +1 -1
  226. package/src/handles/MetaTags.tsx +13 -11
  227. package/src/handles/Scripts.tsx +11 -10
  228. package/src/handles/meta.ts +2 -2
  229. package/src/handles/script.ts +17 -16
  230. package/src/href-client.ts +1 -1
  231. package/src/html.ts +12 -0
  232. package/src/index.rsc.ts +5 -0
  233. package/src/index.ts +1 -0
  234. package/src/loader-store.ts +7 -4
  235. package/src/prerender/build-shell-capture.ts +73 -22
  236. package/src/reverse.ts +44 -4
  237. package/src/route-definition/dsl-helpers.ts +121 -33
  238. package/src/route-definition/helpers-types.ts +33 -9
  239. package/src/route-definition/resolve-handler-use.ts +121 -6
  240. package/src/route-types.ts +0 -3
  241. package/src/router/debug-manifest.ts +10 -2
  242. package/src/router/error-handling.ts +51 -15
  243. package/src/router/handler-context.ts +71 -28
  244. package/src/router/instrument.ts +54 -2
  245. package/src/router/intercept-resolution.ts +141 -43
  246. package/src/router/loader-resolution.ts +115 -39
  247. package/src/router/match-api.ts +40 -26
  248. package/src/router/match-handlers.ts +5 -0
  249. package/src/router/match-middleware/background-revalidation.ts +161 -96
  250. package/src/router/match-middleware/cache-lookup.ts +110 -10
  251. package/src/router/match-middleware/cache-store.ts +48 -101
  252. package/src/router/middleware-types.ts +9 -6
  253. package/src/router/middleware.ts +75 -13
  254. package/src/router/prerender-match.ts +59 -19
  255. package/src/router/route-snapshot.ts +11 -1
  256. package/src/router/router-context.ts +2 -0
  257. package/src/router/router-interfaces.ts +25 -2
  258. package/src/router/router-options.ts +8 -5
  259. package/src/router/segment-resolution/fresh.ts +62 -21
  260. package/src/router/segment-resolution/helpers.ts +4 -2
  261. package/src/router/segment-resolution/loader-cache.ts +407 -76
  262. package/src/router/segment-resolution/loader-mask.ts +12 -22
  263. package/src/router/segment-resolution/loader-snapshot.ts +208 -56
  264. package/src/router/segment-resolution/mask-nested.ts +138 -44
  265. package/src/router/segment-resolution/revalidation.ts +61 -16
  266. package/src/router/segment-wrappers.ts +4 -0
  267. package/src/rsc/full-payload.ts +25 -1
  268. package/src/rsc/handler.ts +14 -2
  269. package/src/rsc/progressive-enhancement.ts +3 -2
  270. package/src/rsc/render-pipeline.ts +3 -0
  271. package/src/rsc/response-cache-serve.ts +9 -6
  272. package/src/rsc/response-route-handler.ts +11 -3
  273. package/src/rsc/rsc-rendering.ts +608 -225
  274. package/src/rsc/shell-build-manifest.ts +27 -9
  275. package/src/rsc/shell-capture-constants.ts +73 -6
  276. package/src/rsc/shell-capture.ts +1068 -296
  277. package/src/rsc/shell-serve.ts +316 -34
  278. package/src/rsc/ssr-setup.ts +14 -8
  279. package/src/rsc/types.ts +18 -0
  280. package/src/server/context.ts +300 -32
  281. package/src/server/cookie-store.ts +87 -61
  282. package/src/server/handle-store.ts +232 -7
  283. package/src/server/request-context.ts +297 -67
  284. package/src/ssr/index.tsx +136 -44
  285. package/src/ssr/ssr-root.tsx +9 -8
  286. package/src/static-handler.ts +4 -1
  287. package/src/testing/flight-runtime.d.ts +21 -0
  288. package/src/testing/flight.entry.ts +14 -0
  289. package/src/testing/generated-routes.ts +2 -2
  290. package/src/testing/internal/context.ts +1 -1
  291. package/src/testing/internal/flight-client-globals.ts +7 -5
  292. package/src/testing/render-route.tsx +85 -24
  293. package/src/testing/run-loader.ts +68 -6
  294. package/src/testing/serve-shell-request.ts +378 -0
  295. package/src/testing/shell-status.ts +34 -12
  296. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  297. package/src/testing/vitest.ts +62 -1
  298. package/src/theme/ThemeProvider.tsx +7 -6
  299. package/src/theme/ThemeScript.tsx +3 -3
  300. package/src/types/cache-types.ts +14 -10
  301. package/src/types/handler-context.ts +28 -1
  302. package/src/types/loader-types.ts +8 -6
  303. package/src/urls/pattern-types.ts +22 -18
  304. package/src/urls/response-types.ts +3 -23
  305. package/src/use-loader.tsx +12 -3
  306. package/src/vite/discovery/shell-prerender-phase.ts +9 -0
  307. package/src/vite/plugins/use-cache-transform.ts +118 -74
  308. package/src/vite/rango.ts +7 -3
  309. package/src/vite/router-discovery.ts +14 -4
@@ -20,9 +20,15 @@ import React from "react";
20
20
  import { isPprEntry, type EntryData } from "../server/context.js";
21
21
  import { sortedSearchString } from "../cache/cache-key-utils.js";
22
22
  import type { SearchParamsFilter } from "../cache/search-params-filter.js";
23
+ import { base64ToBytes } from "../cache/cf/cf-base64.js";
24
+ import { getNonceAttribute } from "../theme/theme-script.js";
23
25
  import type {
24
26
  DocumentShellCacheEntry,
25
27
  ShellCacheEntry,
28
+ ShellDocumentRead,
29
+ ShellReadStats,
30
+ ShellSnapshotFailure,
31
+ ShellSnapshotRecord,
26
32
  SegmentCacheStore,
27
33
  } from "../cache/types.js";
28
34
  import { SHELL_CAPTURE_MAX_WAIT_MS } from "./shell-capture-constants.js";
@@ -132,6 +138,66 @@ export function buildShellKey(url: URL, filter?: SearchParamsFilter): string {
132
138
  return `${url.host}${url.pathname}${shellSearchSeed(url, filter)}:shell`;
133
139
  }
134
140
 
141
+ export {
142
+ navigationShellKey,
143
+ partitionShellKey,
144
+ } from "./shell-capture-constants.js";
145
+
146
+ /**
147
+ * The forced-MISS query marker. A document HIT that cannot finish its tail
148
+ * reloads the page with it (shellReloadScript); the serve gate renders a
149
+ * request carrying it on axis 1, with no shell read and no capture, so the
150
+ * reload can never be a HIT that degrades again. The handler drops it from
151
+ * the request on entry (withoutShellMissMarker) and keeps only the flag
152
+ * (RequestContext._shellForcedMiss), so nothing else sees it.
153
+ */
154
+ export const SHELL_MISS_PARAM: string = "_rsc_shell";
155
+
156
+ /**
157
+ * The request without the forced-MISS marker, or undefined when it carries
158
+ * none. The handler calls it before anything reads the request: left in, the
159
+ * marker reached `ctx.request.url`, `originalUrl`, middleware, and the SSR
160
+ * search seed (so `useSearchParams` rendered it), and a `"use cache"` key
161
+ * built from the request URL was never read again. GET and HEAD only: the
162
+ * reload is a navigation, and a request with a body cannot be re-created
163
+ * without re-streaming it. The substring test keeps the URL parse off every
164
+ * other request.
165
+ */
166
+ export function withoutShellMissMarker(request: Request): Request | undefined {
167
+ if (!request.url.includes(SHELL_MISS_PARAM)) return undefined;
168
+ if (request.method !== "GET" && request.method !== "HEAD") return undefined;
169
+ const url = new URL(request.url);
170
+ if (!url.searchParams.has(SHELL_MISS_PARAM)) return undefined;
171
+ url.searchParams.delete(SHELL_MISS_PARAM);
172
+ return new Request(url, request);
173
+ }
174
+
175
+ /**
176
+ * The inline script a degraded document HIT ends with: reload once into a
177
+ * forced MISS. The marker in the URL is the loop bound — a request carrying
178
+ * it is never a HIT, so this script never runs on the reload, and if it ever
179
+ * did (a server that ignores the marker) it would return without touching
180
+ * the page. window.stop() runs only when it reloads: it keeps the half-sent
181
+ * Flight stream from being closed by DOMContentLoaded, which would throw
182
+ * React's "Connection closed" (#412) before the reload lands. `nonce` is the
183
+ * request's CSP nonce; a ppr route with an active nonce is axis 1
184
+ * (rsc-rendering.ts shellServePlan), so on a HIT it is normally absent.
185
+ */
186
+ export function shellReloadScript(nonce?: string): string {
187
+ return inlineShellScript(
188
+ "(function(){var u=new URL(location.href);" +
189
+ `if(u.searchParams.has(${JSON.stringify(SHELL_MISS_PARAM)}))return;` +
190
+ `u.searchParams.set(${JSON.stringify(SHELL_MISS_PARAM)},"miss");` +
191
+ "try{window.stop()}catch(e){}location.replace(u.href)})()",
192
+ nonce,
193
+ );
194
+ }
195
+
196
+ /** An inline script a shell HIT's tail appends, carrying the CSP nonce if any. */
197
+ export function inlineShellScript(body: string, nonce?: string): string {
198
+ return `<script${getNonceAttribute(nonce)}>${body}</script>`;
199
+ }
200
+
135
201
  /**
136
202
  * The shell key's search portion (`?`-prefixed sorted search with the
137
203
  * cache.searchParams filter applied, or "") — ALSO the string the capture and
@@ -166,17 +232,25 @@ export function isValidShellHit(
166
232
  );
167
233
  }
168
234
 
235
+ /** The postponed blob is stored and parses (null is the DATA variant). */
236
+ function hasParseablePostponed(
237
+ entry: ShellCacheEntry,
238
+ ): entry is ShellCacheEntry & { postponed: string | null } {
239
+ if (entry.postponed === undefined) return false;
240
+ try {
241
+ if (entry.postponed !== null) JSON.parse(entry.postponed);
242
+ return true;
243
+ } catch {
244
+ return false;
245
+ }
246
+ }
247
+
169
248
  /**
170
- * DOCUMENT-half integrity gate and type narrowing, run BEFORE the HIT response
171
- * commits: a stored entry whose prelude is not decodable base64 or whose
172
- * postponed blob is not parseable JSON would otherwise throw AFTER the 200 +
173
- * full static prelude flushed (`serveShellHit` decodes at stream construction,
174
- * `resumeShellHTML` parses in the tail) — the client gets a visually complete
175
- * page that never hydrates, re-served on every request until the entry ages
176
- * out (no eviction path exists; failure schedules no recapture by itself).
177
- * Checking here turns a corrupt entry (store-layer fault) into a plain MISS
178
- * the recapture overwrites. Cost: one duplicate decode/parse per HIT, sub-ms
179
- * against a prelude flush that dominates the path.
249
+ * DOCUMENT-half structural gate and type narrowing, without decoding the
250
+ * prelude: the prelude is a string and the postponed blob parses. Partial
251
+ * replay (which never serves the prelude) and the build-manifest read-through
252
+ * use it as-is; the document HIT path runs it inside
253
+ * {@link openShellDocument}, whose single decode is the prelude's check.
180
254
  *
181
255
  * navigationOnly entries store no document half (prelude/postponed absent) and
182
256
  * therefore never pass. The partial-replay path skips this gate for them
@@ -187,25 +261,85 @@ export function isValidShellHit(
187
261
  export function hasIntactShellPayload(
188
262
  entry: ShellCacheEntry,
189
263
  ): entry is DocumentShellCacheEntry {
190
- if (typeof entry.prelude !== "string" || entry.postponed === undefined) {
191
- return false;
264
+ return typeof entry.prelude === "string" && hasParseablePostponed(entry);
265
+ }
266
+
267
+ /** A document shell ready to serve: its prelude decoded exactly once. */
268
+ export interface ShellDocument {
269
+ entry: ShellCacheEntry;
270
+ prelude: Uint8Array;
271
+ /** React's postponed state, parse-checked (null = DATA variant). */
272
+ postponed: string | null;
273
+ /**
274
+ * The capture snapshot: on the entry, or still arriving on its own promise
275
+ * after a prelude-first read (SegmentCacheStore.readShellDocument), which
276
+ * only the tail awaits.
277
+ */
278
+ snapshot:
279
+ | ShellSnapshotRecord[]
280
+ | undefined
281
+ | Promise<ShellSnapshotRecord[] | undefined>;
282
+ /** Why a prelude-first read's snapshot is missing (ShellDocumentRead). */
283
+ snapshotFailure?: Promise<ShellSnapshotFailure | undefined>;
284
+ /** The prelude-first read's stats, when perf metrics are on. */
285
+ stats?: ShellReadStats;
286
+ }
287
+
288
+ /**
289
+ * Pre-commit integrity gate for a document HIT, and its only prelude decode.
290
+ * A stored entry whose prelude is not decodable base64 or whose postponed blob
291
+ * is not parseable JSON would otherwise throw AFTER the 200 + full static
292
+ * prelude flushed (`resumeShellHTML` parses in the tail) — the client gets a
293
+ * visually complete page that never hydrates, re-served on every request
294
+ * until the entry ages out. Returning null here turns a corrupt entry
295
+ * (store-layer fault) into a plain MISS the recapture overwrites. The decoded
296
+ * bytes are what serveShellHit enqueues, so the check costs no second decode.
297
+ * A prelude-first `read` (SegmentCacheStore.readShellDocument) already carries
298
+ * the raw bytes and a pending snapshot, so only its postponed blob is checked.
299
+ */
300
+ export function openShellDocument(
301
+ entry: ShellCacheEntry,
302
+ read?: Pick<
303
+ ShellDocumentRead,
304
+ "prelude" | "snapshot" | "snapshotFailure" | "stats"
305
+ >,
306
+ ): ShellDocument | null {
307
+ if (read) {
308
+ if (!hasParseablePostponed(entry)) return null;
309
+ return {
310
+ entry,
311
+ postponed: entry.postponed,
312
+ prelude: read.prelude,
313
+ snapshot: read.snapshot,
314
+ ...(read.snapshotFailure && { snapshotFailure: read.snapshotFailure }),
315
+ ...(read.stats && { stats: read.stats }),
316
+ };
192
317
  }
318
+ if (!hasIntactShellPayload(entry)) return null;
319
+ let prelude: Uint8Array;
193
320
  try {
194
- base64ToBytes(entry.prelude);
195
- if (entry.postponed !== null) JSON.parse(entry.postponed);
196
- return true;
321
+ prelude = base64ToBytes(entry.prelude);
197
322
  } catch {
198
- return false;
323
+ return null;
199
324
  }
325
+ return {
326
+ entry,
327
+ postponed: entry.postponed,
328
+ prelude,
329
+ snapshot: entry.snapshot,
330
+ };
200
331
  }
201
332
 
202
- /** Decode a base64 prelude back into bytes for stream composition. */
203
- export function base64ToBytes(b64: string): Uint8Array {
204
- const binary = atob(b64);
205
- const bytes = new Uint8Array(binary.length);
206
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
207
- return bytes;
208
- }
333
+ /**
334
+ * Prelude enqueue granularity. A streaming compressor in front of the worker
335
+ * emits its first byte only after compressing the whole write it was handed:
336
+ * one 629 KB enqueue delayed the first compressed byte by the full prelude's
337
+ * compression time (brotli-6 5.1 ms, gzip-6 1.1 ms in Node zlib with a flush
338
+ * per write; workerd's CompressionStream emits nothing until the whole write is
339
+ * compressed), while 32 KB chunks emit within 0.1 ms and cost slightly more
340
+ * in total compression (0.55-0.70 ms) than one write (issue #941).
341
+ */
342
+ export const SHELL_PRELUDE_CHUNK_BYTES: number = 32 * 1024;
209
343
 
210
344
  /** True when the store implements the shell entry family. */
211
345
  export function hasShellFamily(
@@ -227,8 +361,22 @@ export function hasShellFamily(
227
361
  export interface ShellTailTiming {
228
362
  key: string;
229
363
  outcome: "complete" | "redirect" | "error";
364
+ /** The capture snapshot available to the tail (read and parsed). */
365
+ snapshotMs?: number;
366
+ /** Snapshot bytes read after the commit (prelude-first stores). */
367
+ snapshotBytes?: number;
368
+ /** Snapshot bytes read after the commit, excluding the parse. */
369
+ snapshotReadMs?: number;
370
+ /** Snapshot JSON.parse CPU (reads 0 on a deployed worker: see bytes). */
371
+ snapshotParseMs?: number;
372
+ /** Snapshot records by family, e.g. `segment:1/item:5` (no commas: it rides a Server-Timing desc). */
373
+ snapshotRecords?: string;
374
+ /** Records the capture pruned from the snapshot, same format (ShellCacheEntry.prunedRecords). */
375
+ snapshotPruned?: string;
230
376
  /** Loader-family seed decode (only when the entry carried a snapshot). */
231
377
  seedMs?: number;
378
+ /** The seed decode's own duration: Flight deserialization, CPU only. */
379
+ seedCpuMs?: number;
232
380
  /** Tail router.match() settled. */
233
381
  matchMs?: number;
234
382
  /** Tail stream (resume output) handed to the response stream. */
@@ -244,13 +392,35 @@ export interface ShellTailTiming {
244
392
  }
245
393
 
246
394
  /**
247
- * Compact single-line form for the console log and the dev Server-Timing
248
- * mirror's `desc`. Plain alphanumerics/`=`/`-` only — no quoted-string
249
- * escaping needed.
395
+ * Compact single-line form for the console log and the Server-Timing mirror's
396
+ * `desc`: alphanumerics and `=`, `-`, `:`, `/` only (records=segment:1 pruned=item:5),
397
+ * so no quoted-string escaping is needed. Offsets are from the commit; the
398
+ * `-cpu` fields are CPU-only durations.
250
399
  */
251
400
  export function describeShellTailTiming(timing: ShellTailTiming): string {
252
401
  const parts: string[] = [timing.outcome];
402
+ if (timing.snapshotMs !== undefined) {
403
+ parts.push(`snapshot=${timing.snapshotMs}ms`);
404
+ }
405
+ if (timing.snapshotReadMs !== undefined) {
406
+ parts.push(`snapshot-read=${timing.snapshotReadMs}ms`);
407
+ }
408
+ if (timing.snapshotBytes !== undefined) {
409
+ parts.push(`snapshot-bytes=${timing.snapshotBytes}b`);
410
+ }
411
+ if (timing.snapshotParseMs !== undefined) {
412
+ parts.push(`snapshot-parse-cpu=${timing.snapshotParseMs}ms`);
413
+ }
414
+ if (timing.snapshotRecords !== undefined) {
415
+ parts.push(`records=${timing.snapshotRecords}`);
416
+ }
417
+ if (timing.snapshotPruned !== undefined) {
418
+ parts.push(`pruned=${timing.snapshotPruned}`);
419
+ }
253
420
  if (timing.seedMs !== undefined) parts.push(`seed=${timing.seedMs}ms`);
421
+ if (timing.seedCpuMs !== undefined) {
422
+ parts.push(`seed-cpu=${timing.seedCpuMs}ms`);
423
+ }
254
424
  if (timing.matchMs !== undefined) parts.push(`match=${timing.matchMs}ms`);
255
425
  if (timing.handoverMs !== undefined) {
256
426
  parts.push(`handover=${timing.handoverMs}ms`);
@@ -269,19 +439,19 @@ export function describeShellTailTiming(timing: ShellTailTiming): string {
269
439
  }
270
440
 
271
441
  /**
272
- * Dev-only last-tail-per-key buffer backing the `ppr:tail` Server-Timing
273
- * mirror: a HIT's tail finishes after its own headers are long gone, so its
274
- * per-stage numbers ride the NEXT ppr GET for the key when the metrics
275
- * surface is active (debugPerformance). Same shape and FIFO cap as the
276
- * capture mirror (shell-capture.ts lastCaptureEventsForTiming); dev-only so
277
- * production isolates never grow the map.
442
+ * Last-tail-per-key buffer backing the `ppr:tail` Server-Timing mirror: a
443
+ * HIT's tail finishes after its own headers are long gone, so its per-stage
444
+ * numbers ride the NEXT ppr GET for the key when the metrics surface is
445
+ * active (debugPerformance). Same shape and FIFO cap as the capture mirror
446
+ * (shell-capture.ts lastCaptureEventsForTiming). serveShellHit collects and
447
+ * publishes a timing only in dev or when the HIT itself collected metrics, so
448
+ * a production isolate without debugPerformance never grows the map.
278
449
  */
279
450
  const lastTailTimingsForServerTiming = new Map<string, ShellTailTiming>();
280
451
  const MAX_TAIL_TIMING_KEYS = 100;
281
452
 
282
- /** Buffer one terminal tail timing for the dev Server-Timing mirror. */
453
+ /** Buffer one terminal tail timing for the Server-Timing mirror. */
283
454
  export function publishShellTailTiming(timing: ShellTailTiming): void {
284
- if (process.env.NODE_ENV === "production") return;
285
455
  lastTailTimingsForServerTiming.delete(timing.key);
286
456
  if (lastTailTimingsForServerTiming.size >= MAX_TAIL_TIMING_KEYS) {
287
457
  const oldest = lastTailTimingsForServerTiming.keys().next().value;
@@ -348,3 +518,115 @@ export function warnPprNonceActiveOnce(key: string): void {
348
518
  "ppr option on this route, or stop setting a per-request nonce for it.",
349
519
  );
350
520
  }
521
+
522
+ /** Routes already warned about a ppr window the route cache() caps. */
523
+ const warnedPprWindowCapped = new Set<string>();
524
+
525
+ /**
526
+ * Dev only (the caller gates on NODE_ENV): warn once per route that the
527
+ * route's cache() entry reduces an explicit `ppr.ttl` or `ppr.swr`. `shell`
528
+ * is what the capture stores for a record written just now
529
+ * (shell-capture.ts capShellWindow), so the message states the values the
530
+ * shell actually gets. A ppr value the cap leaves whole (or raises: the
531
+ * cap can move a record's stale time into the shell's swr) is silent, and
532
+ * so is a ppr config that sets no ttl/swr.
533
+ */
534
+ export function warnPprWindowCappedOnce(
535
+ routeName: string,
536
+ ppr: { ttl?: number; swr?: number },
537
+ shell: { ttl: number; swr: number },
538
+ cache: { ttl: number; swr: number },
539
+ ): void {
540
+ if (warnedPprWindowCapped.has(routeName)) return;
541
+ const reduced: string[] = [];
542
+ for (const field of ["ttl", "swr"] as const) {
543
+ const own = ppr[field];
544
+ if (own !== undefined && shell[field] < own) {
545
+ reduced.push(`ppr.${field} ${own}`);
546
+ }
547
+ }
548
+ if (reduced.length === 0) return;
549
+ warnedPprWindowCapped.add(routeName);
550
+ console.warn(
551
+ `[rango] Route "${routeName}": its shell is stored with ttl ${shell.ttl} ` +
552
+ `and swr ${shell.swr}, below its ${reduced.join(" and ")}. A shell ` +
553
+ `never outlives the route cache() entry (ttl ${cache.ttl}, swr ` +
554
+ `${cache.swr}) it was captured from, so a document request and a ` +
555
+ "client navigation show the same handler output. Keep ppr.ttl within " +
556
+ "the cache() ttl, and ppr.ttl + ppr.swr within the cache() ttl + swr, " +
557
+ "to silence this.",
558
+ );
559
+ }
560
+
561
+ /** Routes (or paths) already warned that a partition skips their build shell. */
562
+ const partitionBuildShellWarned = new Set<string>();
563
+
564
+ /**
565
+ * Paths a partitioned request found without a build shell. Build shells are
566
+ * per path (a param route prerenders some of its paths), so a negative is
567
+ * kept per path, not per route; cleared when full, so it stays bounded.
568
+ */
569
+ const pathsWithoutBuildShell = new Set<string>();
570
+ const PATHS_WITHOUT_BUILD_SHELL_MAX = 1_000;
571
+
572
+ /**
573
+ * Whether a partitioned request needs no build-shell probe: its route was
574
+ * already warned, or its path was already found without one.
575
+ */
576
+ export function partitionBuildShellCheckDone(
577
+ pathname: string,
578
+ routeName: string | undefined,
579
+ ): boolean {
580
+ return (
581
+ partitionBuildShellWarned.has(routeName ?? pathname) ||
582
+ pathsWithoutBuildShell.has(pathname)
583
+ );
584
+ }
585
+
586
+ /**
587
+ * Record a partitioned request's build-shell probe, and warn once per route
588
+ * when it found one: the route has a build-time shell its requests cannot
589
+ * read. The route's request partition (a `cache({ key })` enclosing it, at
590
+ * any depth (#970), or the store's keyGenerator) keys the shell per
591
+ * partition, and the build captured only
592
+ * the default one, so each partition captures at runtime. Same
593
+ * declared-intent-cannot-be-honored doctrine as the warnings above; a path
594
+ * with no build shell stays silent.
595
+ */
596
+ export function notePartitionBuildShellCheck(
597
+ pathname: string,
598
+ routeName: string | undefined,
599
+ found: boolean,
600
+ ): void {
601
+ if (!found) {
602
+ if (pathsWithoutBuildShell.size >= PATHS_WITHOUT_BUILD_SHELL_MAX) {
603
+ pathsWithoutBuildShell.clear();
604
+ }
605
+ pathsWithoutBuildShell.add(pathname);
606
+ return;
607
+ }
608
+ // Concurrent first requests can both probe; one of them warns.
609
+ const route = routeName ?? pathname;
610
+ if (partitionBuildShellWarned.has(route)) return;
611
+ partitionBuildShellWarned.add(route);
612
+ console.warn(
613
+ `[rango] Route ${routeName ? `"${routeName}" ` : ""}("${pathname}") has a ` +
614
+ "build-time shell, but its request partition (a cache({ key }) enclosing the route, " +
615
+ "or the store's keyGenerator) keys its shell per partition, so the build " +
616
+ "shell is not served and each partition captures its own at runtime. A " +
617
+ "keyGenerator that returns the default key unchanged keeps the build shell.",
618
+ );
619
+ }
620
+
621
+ /**
622
+ * @internal Reset the serve path's once-per-key warnings and buffered tail
623
+ * timings. Tests only (testing/serve-shell-request.ts resetShellTestState).
624
+ */
625
+ export function resetShellServeStateForTests(): void {
626
+ warnedMissingStore.clear();
627
+ warnedNonceActive.clear();
628
+ partitionBuildShellWarned.clear();
629
+ pathsWithoutBuildShell.clear();
630
+ warnedPprWindowCapped.clear();
631
+ lastTailTimingsForServerTiming.clear();
632
+ }
@@ -82,11 +82,11 @@ export interface SsrHtmlStageOptions<TEnv> {
82
82
  /** Metrics store for ssr:module-load / ssr:stream-mode timing (per call site). */
83
83
  metricsStore: MetricsStore | undefined;
84
84
  /**
85
- * renderHTML options minus streamMode (the stage resolves streamMode). Carries
86
- * the per-site nonce and, for the PE action re-render, formState. Spread
87
- * verbatim so each call site's exact key set is preserved.
85
+ * renderHTML options minus streamMode and onError (the stage owns both).
86
+ * Carries the per-site nonce and, for the PE action re-render, formState.
87
+ * Spread verbatim so each call site's exact key set is preserved.
88
88
  */
89
- render: Omit<SSRRenderOptions, "streamMode">;
89
+ render: Omit<SSRRenderOptions, "streamMode" | "onError">;
90
90
  /** ResponseInit merged into the prepared render (e.g. content-type). */
91
91
  init?: ResponseInit;
92
92
  }
@@ -109,6 +109,7 @@ export function createSsrHtmlStage<TEnv>(
109
109
  options.url,
110
110
  options.metricsStore,
111
111
  );
112
+ const renderErrors = _getRequestContext()?._renderErrors;
112
113
  return {
113
114
  render: () =>
114
115
  // search: the LIVE request's query string, threaded out-of-band into
@@ -124,6 +125,12 @@ export function createSsrHtmlStage<TEnv>(
124
125
  // so origin-dependent markup (Link's data-external) agrees with the
125
126
  // browser's window.location across hydration.
126
127
  origin: options.url.origin,
128
+ // A component that throws inside a Suspense boundary leaves an
129
+ // errored boundary in a completed document; the document cache
130
+ // reads the list before storing it.
131
+ onError: (error) => {
132
+ renderErrors?.push(error);
133
+ },
127
134
  }),
128
135
  ...(options.init && { init: options.init }),
129
136
  };
@@ -185,8 +192,8 @@ function acceptsFlightExplicitly(request: Request, url: URL): boolean {
185
192
  * Classify whether a request may require SSR (HTML rendering).
186
193
  *
187
194
  * Returns false for requests that are definitively RSC-only: transport
188
- * params (partial/action/loader/__rsc), prerender collection, or an explicit
189
- * Accept: text/x-component. Must never return false for a request whose
195
+ * params (partial/action/loader/__rsc) or an explicit Accept:
196
+ * text/x-component. Must never return false for a request whose
190
197
  * render-time decision (isRscRequest) will be HTML — the two share
191
198
  * acceptsFlightExplicitly so the Accept rule cannot drift. document-cache.ts
192
199
  * keys its HTML/RSC response slots off this function, so any divergence from
@@ -201,8 +208,7 @@ export function mayNeedSSR(request: Request, url: URL): boolean {
201
208
  url.searchParams.has("_rsc_action") ||
202
209
  request.headers.has("rsc-action") ||
203
210
  url.searchParams.has("_rsc_loader") ||
204
- url.searchParams.has("__rsc") ||
205
- url.searchParams.has("__prerender_collect")
211
+ url.searchParams.has("__rsc")
206
212
  ) {
207
213
  return false;
208
214
  }
package/src/rsc/types.ts CHANGED
@@ -193,6 +193,13 @@ export interface SSRRenderOptions {
193
193
  * the browser across hydration. Absent on host-agnostic build captures.
194
194
  */
195
195
  origin?: string;
196
+
197
+ /**
198
+ * Called for each error Fizz reports through its onError: a component that
199
+ * threw inside a Suspense boundary, which leaves the boundary errored in an
200
+ * otherwise completed document. React's default console.error is kept.
201
+ */
202
+ onError?: (error: unknown) => void;
196
203
  }
197
204
 
198
205
  /**
@@ -219,6 +226,15 @@ export interface SSRModule {
219
226
  maxWaitMs?: number;
220
227
  search?: string;
221
228
  origin?: string;
229
+ /** Each component error the prerender reports (never the capture's own abort). */
230
+ onError?: (error: unknown) => void;
231
+ /**
232
+ * React's errorInfo for each task still pending at the capture's abort
233
+ * (its componentStack is computed when read).
234
+ */
235
+ onAbortedTask?: (
236
+ errorInfo: { componentStack?: string } | undefined,
237
+ ) => void;
222
238
  },
223
239
  ) => Promise<{ prelude: Uint8Array; postponed: string | null } | null>;
224
240
 
@@ -237,6 +253,8 @@ export interface SSRModule {
237
253
  nonce?: string;
238
254
  search?: string;
239
255
  origin?: string;
256
+ /** Each component error the resumed holes report. */
257
+ onError?: (error: unknown) => void;
240
258
  },
241
259
  ) => Promise<ReadableStream<Uint8Array>>;
242
260
  }