@rangojs/router 0.0.0-experimental.bd6e11bc → 0.0.0-experimental.bdaf10aa

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 (411) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +296 -887
  3. package/dist/bin/rango.js +459 -91
  4. package/dist/testing/vitest.js +36 -2
  5. package/dist/vite/index.js +1708 -414
  6. package/package.json +35 -10
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +82 -5
  9. package/skills/bundle-analysis/SKILL.md +2 -2
  10. package/skills/cache-guide/SKILL.md +14 -9
  11. package/skills/caching/SKILL.md +221 -12
  12. package/skills/catalog.json +271 -0
  13. package/skills/comparison/SKILL.md +50 -0
  14. package/skills/comparison/agents/openai.yaml +4 -0
  15. package/skills/comparison/references/framework-comparison.md +837 -0
  16. package/skills/composability/SKILL.md +83 -2
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +5 -3
  19. package/skills/defer-hydration/SKILL.md +235 -0
  20. package/skills/document-cache/SKILL.md +11 -3
  21. package/skills/fonts/SKILL.md +1 -1
  22. package/skills/handler-use/SKILL.md +9 -9
  23. package/skills/hooks/SKILL.md +73 -900
  24. package/skills/hooks/data.md +273 -0
  25. package/skills/hooks/handle-and-actions.md +103 -0
  26. package/skills/hooks/navigation.md +110 -0
  27. package/skills/hooks/outlets.md +41 -0
  28. package/skills/hooks/state.md +228 -0
  29. package/skills/hooks/urls.md +135 -0
  30. package/skills/host-router/SKILL.md +84 -7
  31. package/skills/i18n/SKILL.md +1 -1
  32. package/skills/intercept/SKILL.md +51 -17
  33. package/skills/layout/SKILL.md +38 -16
  34. package/skills/links/SKILL.md +1 -1
  35. package/skills/loader/SKILL.md +48 -20
  36. package/skills/middleware/SKILL.md +11 -5
  37. package/skills/migrate-nextjs/SKILL.md +203 -20
  38. package/skills/migrate-react-router/SKILL.md +59 -675
  39. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  40. package/skills/migrate-react-router/component-migration.md +196 -0
  41. package/skills/migrate-react-router/data-and-actions.md +225 -0
  42. package/skills/migrate-react-router/route-mapping.md +271 -0
  43. package/skills/mime-routes/SKILL.md +3 -3
  44. package/skills/observability/SKILL.md +70 -5
  45. package/skills/parallel/SKILL.md +32 -8
  46. package/skills/ppr/SKILL.md +622 -0
  47. package/skills/prerender/SKILL.md +59 -28
  48. package/skills/rango/SKILL.md +124 -50
  49. package/skills/response-routes/SKILL.md +78 -46
  50. package/skills/route/SKILL.md +85 -6
  51. package/skills/router-setup/SKILL.md +41 -6
  52. package/skills/scripts/SKILL.md +179 -0
  53. package/skills/server-actions/SKILL.md +28 -3
  54. package/skills/shell-manifest/SKILL.md +185 -0
  55. package/skills/streams-and-websockets/SKILL.md +1 -1
  56. package/skills/tailwind/SKILL.md +28 -4
  57. package/skills/testing/SKILL.md +68 -654
  58. package/skills/testing/bindings.md +103 -0
  59. package/skills/testing/cache-prerender.md +127 -0
  60. package/skills/testing/client-components.md +124 -0
  61. package/skills/testing/e2e-parity.md +125 -0
  62. package/skills/testing/flight.md +91 -0
  63. package/skills/testing/handles.md +131 -0
  64. package/skills/testing/loader.md +128 -0
  65. package/skills/testing/middleware.md +99 -0
  66. package/skills/testing/render-handler.md +122 -0
  67. package/skills/testing/response-routes.md +95 -0
  68. package/skills/testing/reverse-and-types.md +85 -0
  69. package/skills/testing/server-actions.md +107 -0
  70. package/skills/testing/server-tree.md +128 -0
  71. package/skills/testing/setup.md +123 -0
  72. package/skills/theme/SKILL.md +1 -1
  73. package/skills/typesafety/SKILL.md +45 -918
  74. package/skills/typesafety/env-and-bindings.md +254 -0
  75. package/skills/typesafety/generated-files-and-cli.md +335 -0
  76. package/skills/typesafety/params-and-search.md +153 -0
  77. package/skills/typesafety/route-types.md +209 -0
  78. package/skills/use-cache/SKILL.md +47 -17
  79. package/skills/vercel/SKILL.md +128 -0
  80. package/skills/view-transitions/SKILL.md +44 -1
  81. package/src/__augment-tests__/augmented.check.ts +2 -3
  82. package/src/__internal.ts +0 -65
  83. package/src/browser/action-coordinator.ts +1 -1
  84. package/src/browser/action-fence.ts +47 -0
  85. package/src/browser/app-shell.ts +14 -27
  86. package/src/browser/connection-warmup.ts +134 -0
  87. package/src/browser/cookie-name.ts +140 -0
  88. package/src/browser/event-controller.ts +178 -100
  89. package/src/browser/invalidate-client-cache.ts +52 -0
  90. package/src/browser/logging.ts +28 -0
  91. package/src/browser/merge-segment-loaders.ts +6 -4
  92. package/src/browser/navigation-bridge.ts +81 -68
  93. package/src/browser/navigation-client.ts +115 -70
  94. package/src/browser/navigation-store-handle.ts +38 -0
  95. package/src/browser/navigation-store.ts +153 -88
  96. package/src/browser/navigation-transaction.ts +0 -32
  97. package/src/browser/network-error-handler.ts +34 -7
  98. package/src/browser/partial-update.ts +157 -144
  99. package/src/browser/prefetch/cache.ts +148 -81
  100. package/src/browser/prefetch/fetch.ts +231 -51
  101. package/src/browser/prefetch/queue.ts +25 -7
  102. package/src/browser/rango-state.ts +157 -115
  103. package/src/browser/react/Link.tsx +40 -7
  104. package/src/browser/react/NavigationProvider.tsx +140 -99
  105. package/src/browser/react/ScrollRestoration.tsx +10 -6
  106. package/src/browser/react/filter-segment-order.ts +17 -2
  107. package/src/browser/react/index.ts +0 -51
  108. package/src/browser/react/location-state-shared.ts +14 -15
  109. package/src/browser/react/location-state.ts +0 -1
  110. package/src/browser/react/use-action.ts +6 -15
  111. package/src/browser/react/use-handle.ts +0 -5
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +33 -8
  114. package/src/browser/react/use-navigation.ts +10 -5
  115. package/src/browser/react/use-params.ts +0 -2
  116. package/src/browser/react/use-router.ts +6 -4
  117. package/src/browser/react/use-search-params.ts +0 -5
  118. package/src/browser/react/use-segments.ts +0 -13
  119. package/src/browser/response-adapter.ts +74 -8
  120. package/src/browser/rsc-router.tsx +97 -22
  121. package/src/browser/scroll-restoration.ts +15 -8
  122. package/src/browser/segment-reconciler.ts +31 -21
  123. package/src/browser/server-action-bridge.ts +216 -38
  124. package/src/browser/types.ts +94 -22
  125. package/src/browser/validate-redirect-origin.ts +43 -16
  126. package/src/build/generate-manifest.ts +155 -131
  127. package/src/build/generate-route-types.ts +1 -1
  128. package/src/build/index.ts +11 -5
  129. package/src/build/prefix-tree-utils.ts +123 -0
  130. package/src/build/route-trie.ts +152 -22
  131. package/src/build/route-types/ast-route-extraction.ts +15 -8
  132. package/src/build/route-types/codegen.ts +12 -1
  133. package/src/build/route-types/include-resolution.ts +455 -61
  134. package/src/build/route-types/param-extraction.ts +6 -3
  135. package/src/build/route-types/per-module-writer.ts +15 -2
  136. package/src/build/route-types/router-processing.ts +77 -41
  137. package/src/build/route-types/source-scan.ts +105 -7
  138. package/src/build/runtime-discovery.ts +4 -1
  139. package/src/cache/cache-error.ts +104 -0
  140. package/src/cache/cache-key-utils.ts +58 -13
  141. package/src/cache/cache-policy.ts +108 -34
  142. package/src/cache/cache-runtime.ts +454 -101
  143. package/src/cache/cache-scope.ts +159 -54
  144. package/src/cache/cache-tag.ts +149 -0
  145. package/src/cache/cf/cf-base64.ts +33 -0
  146. package/src/cache/cf/cf-cache-constants.ts +127 -0
  147. package/src/cache/cf/cf-cache-store.ts +2170 -377
  148. package/src/cache/cf/cf-cache-types.ts +349 -0
  149. package/src/cache/cf/cf-kv-utils.ts +46 -0
  150. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  151. package/src/cache/cf/index.ts +6 -16
  152. package/src/cache/document-cache.ts +126 -41
  153. package/src/cache/handle-snapshot.ts +70 -0
  154. package/src/cache/index.ts +23 -20
  155. package/src/cache/memory-segment-store.ts +243 -37
  156. package/src/cache/profile-registry.ts +46 -31
  157. package/src/cache/read-through-swr.ts +56 -12
  158. package/src/cache/segment-codec.ts +13 -21
  159. package/src/cache/shell-snapshot.ts +417 -0
  160. package/src/cache/tag-invalidation.ts +230 -0
  161. package/src/cache/types.ts +194 -99
  162. package/src/cache/vercel/index.ts +11 -0
  163. package/src/cache/vercel/vercel-cache-store.ts +1132 -0
  164. package/src/client.rsc.tsx +39 -22
  165. package/src/client.tsx +28 -58
  166. package/src/cloudflare/index.ts +11 -0
  167. package/src/cloudflare/tracing.ts +108 -0
  168. package/src/component-utils.ts +19 -0
  169. package/src/components/DefaultDocument.tsx +8 -2
  170. package/src/context-var.ts +13 -1
  171. package/src/decode-loader-results.ts +18 -2
  172. package/src/defer.ts +185 -0
  173. package/src/deps/ssr.ts +0 -1
  174. package/src/encode-kv.ts +49 -0
  175. package/src/errors.ts +0 -3
  176. package/src/escape-script.ts +52 -0
  177. package/src/handle.ts +57 -40
  178. package/src/handles/MetaTags.tsx +24 -53
  179. package/src/handles/Scripts.tsx +183 -0
  180. package/src/handles/breadcrumbs.ts +35 -8
  181. package/src/handles/deferred-resolution.ts +127 -0
  182. package/src/handles/is-thenable.ts +18 -0
  183. package/src/handles/meta.ts +14 -40
  184. package/src/handles/script.ts +244 -0
  185. package/src/host/cookie-handler.ts +9 -60
  186. package/src/host/errors.ts +13 -22
  187. package/src/host/index.ts +7 -0
  188. package/src/host/pattern-matcher.ts +23 -52
  189. package/src/host/router.ts +1 -65
  190. package/src/host/testing.ts +40 -27
  191. package/src/host/types.ts +6 -2
  192. package/src/href-client.ts +7 -12
  193. package/src/index.rsc.ts +88 -8
  194. package/src/index.ts +90 -16
  195. package/src/internal-debug.ts +11 -10
  196. package/src/loader.rsc.ts +19 -9
  197. package/src/loader.ts +12 -4
  198. package/src/outlet-provider.tsx +1 -5
  199. package/src/prerender/param-hash.ts +16 -16
  200. package/src/prerender/store.ts +32 -37
  201. package/src/prerender.ts +75 -7
  202. package/src/redirect-origin.ts +114 -0
  203. package/src/regex-escape.ts +8 -0
  204. package/src/render-error-thrower.tsx +20 -0
  205. package/src/response-utils.ts +25 -0
  206. package/src/root-error-boundary.tsx +1 -19
  207. package/src/route-content-wrapper.tsx +13 -49
  208. package/src/route-definition/dsl-helpers.ts +60 -53
  209. package/src/route-definition/helper-factories.ts +0 -2
  210. package/src/route-definition/helpers-types.ts +46 -46
  211. package/src/route-definition/index.ts +1 -2
  212. package/src/route-definition/redirect.ts +44 -11
  213. package/src/route-definition/resolve-handler-use.ts +6 -1
  214. package/src/route-definition/use-item-types.ts +3 -6
  215. package/src/route-map-builder.ts +41 -20
  216. package/src/route-types.ts +0 -5
  217. package/src/router/content-negotiation.ts +58 -23
  218. package/src/router/error-handling.ts +44 -17
  219. package/src/router/find-match.ts +129 -30
  220. package/src/router/handler-context.ts +6 -1
  221. package/src/router/instrument.ts +355 -0
  222. package/src/router/intercept-resolution.ts +35 -2
  223. package/src/router/lazy-includes.ts +79 -56
  224. package/src/router/loader-resolution.ts +151 -73
  225. package/src/router/logging.ts +0 -6
  226. package/src/router/manifest.ts +74 -40
  227. package/src/router/match-api.ts +76 -52
  228. package/src/router/match-context.ts +0 -22
  229. package/src/router/match-handlers.ts +181 -178
  230. package/src/router/match-middleware/background-revalidation.ts +40 -24
  231. package/src/router/match-middleware/cache-lookup.ts +115 -194
  232. package/src/router/match-middleware/cache-store.ts +61 -50
  233. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  234. package/src/router/match-middleware/segment-resolution.ts +0 -22
  235. package/src/router/match-pipelines.ts +1 -42
  236. package/src/router/match-result.ts +36 -67
  237. package/src/router/metrics.ts +0 -34
  238. package/src/router/middleware-types.ts +0 -116
  239. package/src/router/middleware.ts +231 -120
  240. package/src/router/navigation-snapshot.ts +7 -56
  241. package/src/router/params-util.ts +23 -0
  242. package/src/router/parse-pattern.ts +115 -0
  243. package/src/router/pattern-matching.ts +99 -152
  244. package/src/router/prefetch-cache-ttl.ts +51 -0
  245. package/src/router/prefetch-limits.ts +37 -0
  246. package/src/router/prerender-match.ts +111 -66
  247. package/src/router/preview-match.ts +3 -1
  248. package/src/router/request-classification.ts +47 -42
  249. package/src/router/revalidation.ts +75 -81
  250. package/src/router/route-snapshot.ts +14 -3
  251. package/src/router/router-context.ts +6 -29
  252. package/src/router/router-interfaces.ts +70 -8
  253. package/src/router/router-options.ts +126 -4
  254. package/src/router/segment-resolution/fresh.ts +104 -80
  255. package/src/router/segment-resolution/helpers.ts +86 -6
  256. package/src/router/segment-resolution/loader-cache.ts +155 -39
  257. package/src/router/segment-resolution/loader-mask.ts +60 -0
  258. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  259. package/src/router/segment-resolution/mask-nested.ts +83 -0
  260. package/src/router/segment-resolution/revalidation.ts +215 -304
  261. package/src/router/segment-resolution/static-store.ts +19 -5
  262. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  263. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  264. package/src/router/segment-resolution.ts +5 -1
  265. package/src/router/segment-wrappers.ts +6 -5
  266. package/src/router/state-cookie-name.ts +33 -0
  267. package/src/router/substitute-pattern-params.ts +54 -35
  268. package/src/router/telemetry-otel.ts +160 -200
  269. package/src/router/telemetry.ts +9 -23
  270. package/src/router/timeout.ts +0 -20
  271. package/src/router/tracing.ts +215 -0
  272. package/src/router/trie-matching.ts +171 -64
  273. package/src/router/types.ts +1 -63
  274. package/src/router/url-params.ts +13 -5
  275. package/src/router.ts +119 -48
  276. package/src/rsc/full-payload.ts +70 -0
  277. package/src/rsc/handler-context.ts +1 -0
  278. package/src/rsc/handler.ts +267 -152
  279. package/src/rsc/helpers.ts +78 -4
  280. package/src/rsc/index.ts +1 -4
  281. package/src/rsc/json-route-result.ts +38 -0
  282. package/src/rsc/loader-fetch.ts +114 -38
  283. package/src/rsc/manifest-init.ts +29 -42
  284. package/src/rsc/nonce.ts +10 -1
  285. package/src/rsc/origin-guard.ts +11 -15
  286. package/src/rsc/progressive-enhancement.ts +120 -13
  287. package/src/rsc/redirect-guard.ts +100 -0
  288. package/src/rsc/response-cache-serve.ts +238 -0
  289. package/src/rsc/response-error.ts +79 -12
  290. package/src/rsc/response-route-handler.ts +58 -141
  291. package/src/rsc/rsc-rendering.ts +492 -49
  292. package/src/rsc/runtime-warnings.ts +14 -0
  293. package/src/rsc/server-action.ts +268 -82
  294. package/src/rsc/shell-capture.ts +1190 -0
  295. package/src/rsc/shell-serve.ts +181 -0
  296. package/src/rsc/transition-gate.ts +89 -0
  297. package/src/rsc/types.ts +45 -3
  298. package/src/runtime-env.ts +18 -0
  299. package/src/search-params.ts +31 -26
  300. package/src/segment-loader-promise.ts +49 -4
  301. package/src/segment-system.tsx +260 -95
  302. package/src/server/context.ts +99 -9
  303. package/src/server/cookie-parse.ts +32 -0
  304. package/src/server/cookie-store.ts +125 -2
  305. package/src/server/handle-store.ts +21 -38
  306. package/src/server/loader-registry.ts +33 -42
  307. package/src/server/request-context.ts +379 -138
  308. package/src/ssr/index.tsx +491 -182
  309. package/src/ssr/inject-rsc-eager.ts +167 -0
  310. package/src/ssr/ssr-root.tsx +228 -0
  311. package/src/static-handler.ts +10 -13
  312. package/src/testing/cache-status.ts +44 -48
  313. package/src/testing/collect-handle.ts +14 -31
  314. package/src/testing/dispatch.ts +533 -160
  315. package/src/testing/e2e/fixture.ts +45 -11
  316. package/src/testing/e2e/index.ts +1 -22
  317. package/src/testing/e2e/matchers.ts +0 -16
  318. package/src/testing/e2e/parity.ts +85 -4
  319. package/src/testing/e2e/server.ts +12 -0
  320. package/src/testing/flight-matchers.ts +7 -14
  321. package/src/testing/flight-normalize.ts +11 -0
  322. package/src/testing/flight-runtime.d.ts +36 -0
  323. package/src/testing/flight-tree.ts +682 -0
  324. package/src/testing/flight.entry.ts +30 -0
  325. package/src/testing/flight.ts +145 -70
  326. package/src/testing/generated-routes.ts +26 -50
  327. package/src/testing/index.ts +18 -19
  328. package/src/testing/internal/context.ts +184 -68
  329. package/src/testing/internal/flight-client-globals.ts +30 -0
  330. package/src/testing/internal/seed-vars.ts +54 -0
  331. package/src/testing/render-handler.ts +357 -0
  332. package/src/testing/render-route.tsx +134 -115
  333. package/src/testing/run-loader.ts +140 -51
  334. package/src/testing/run-middleware.ts +59 -33
  335. package/src/testing/run-transition-when.ts +164 -0
  336. package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
  337. package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
  338. package/src/testing/vitest.ts +138 -16
  339. package/src/theme/ThemeProvider.tsx +56 -84
  340. package/src/theme/ThemeScript.tsx +7 -9
  341. package/src/theme/constants.ts +52 -13
  342. package/src/theme/index.ts +0 -7
  343. package/src/theme/theme-context.ts +1 -5
  344. package/src/theme/theme-script.ts +22 -21
  345. package/src/theme/use-theme.ts +0 -3
  346. package/src/types/boundaries.ts +0 -35
  347. package/src/types/cache-types.ts +13 -4
  348. package/src/types/error-types.ts +30 -90
  349. package/src/types/global-namespace.ts +15 -15
  350. package/src/types/handler-context.ts +45 -15
  351. package/src/types/index.ts +2 -10
  352. package/src/types/loader-types.ts +6 -3
  353. package/src/types/request-scope.ts +8 -22
  354. package/src/types/route-config.ts +20 -52
  355. package/src/types/route-entry.ts +0 -6
  356. package/src/types/segments.ts +100 -13
  357. package/src/urls/include-helper.ts +10 -12
  358. package/src/urls/include-provider.ts +71 -0
  359. package/src/urls/index.ts +2 -8
  360. package/src/urls/path-helper-types.ts +52 -14
  361. package/src/urls/path-helper.ts +5 -54
  362. package/src/urls/pattern-types.ts +36 -0
  363. package/src/urls/type-extraction.ts +76 -42
  364. package/src/urls/urls-function.ts +0 -14
  365. package/src/use-loader.tsx +0 -186
  366. package/src/vercel/index.ts +11 -0
  367. package/src/vercel/tracing.ts +88 -0
  368. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  369. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  370. package/src/vite/discovery/discover-routers.ts +34 -43
  371. package/src/vite/discovery/discovery-errors.ts +61 -0
  372. package/src/vite/discovery/prerender-collection.ts +33 -46
  373. package/src/vite/discovery/state.ts +12 -1
  374. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  375. package/src/vite/index.ts +9 -0
  376. package/src/vite/inject-client-debug.ts +88 -0
  377. package/src/vite/plugin-types.ts +143 -10
  378. package/src/vite/plugins/cjs-to-esm.ts +8 -12
  379. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  380. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  381. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  382. package/src/vite/plugins/expose-action-id.ts +2 -73
  383. package/src/vite/plugins/expose-id-utils.ts +85 -56
  384. package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
  385. package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
  386. package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
  387. package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
  388. package/src/vite/plugins/expose-internal-ids.ts +10 -1
  389. package/src/vite/plugins/performance-tracks.ts +0 -3
  390. package/src/vite/plugins/refresh-cmd.ts +1 -1
  391. package/src/vite/plugins/use-cache-transform.ts +21 -46
  392. package/src/vite/plugins/vercel-output.ts +384 -0
  393. package/src/vite/plugins/version-injector.ts +22 -27
  394. package/src/vite/plugins/version-plugin.ts +6 -66
  395. package/src/vite/plugins/virtual-entries.ts +137 -26
  396. package/src/vite/rango.ts +146 -135
  397. package/src/vite/router-discovery.ts +189 -48
  398. package/src/vite/utils/ast-handler-extract.ts +11 -20
  399. package/src/vite/utils/bundle-analysis.ts +6 -13
  400. package/src/vite/utils/client-chunks.ts +0 -6
  401. package/src/vite/utils/directive-prologue.ts +40 -0
  402. package/src/vite/utils/forward-user-plugins.ts +0 -22
  403. package/src/vite/utils/manifest-utils.ts +4 -75
  404. package/src/vite/utils/package-resolution.ts +1 -73
  405. package/src/vite/utils/prerender-utils.ts +71 -44
  406. package/src/vite/utils/shared-utils.ts +55 -37
  407. package/src/browser/react/use-client-cache.ts +0 -58
  408. package/src/browser/shallow.ts +0 -40
  409. package/src/handles/index.ts +0 -7
  410. package/src/network-error-thrower.tsx +0 -23
  411. package/src/router/middleware-cookies.ts +0 -55
@@ -10,16 +10,13 @@
10
10
 
11
11
  import type { ResolvedSegment } from "../types.js";
12
12
  import type { SerializedSegmentData } from "./types.js";
13
+ import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
13
14
  import {
14
15
  renderToReadableStream,
15
16
  createTemporaryReferenceSet,
16
17
  } from "@vitejs/plugin-rsc/rsc";
17
18
  import { createFromReadableStream } from "@vitejs/plugin-rsc/rsc";
18
19
 
19
- // ============================================================================
20
- // Stream Utilities (internal)
21
- // ============================================================================
22
-
23
20
  /**
24
21
  * Convert a ReadableStream to a string.
25
22
  */
@@ -28,16 +25,16 @@ export async function streamToString(
28
25
  ): Promise<string> {
29
26
  const reader = stream.getReader();
30
27
  const decoder = new TextDecoder();
31
- let result = "";
28
+ const chunks: string[] = [];
32
29
 
33
30
  while (true) {
34
31
  const { done, value } = await reader.read();
35
32
  if (done) break;
36
- result += decoder.decode(value, { stream: true });
33
+ chunks.push(decoder.decode(value, { stream: true }));
37
34
  }
38
35
 
39
- result += decoder.decode(); // flush
40
- return result;
36
+ chunks.push(decoder.decode()); // flush
37
+ return chunks.join("");
41
38
  }
42
39
 
43
40
  /**
@@ -55,10 +52,6 @@ export function stringToStream(str: string): ReadableStream<Uint8Array> {
55
52
  });
56
53
  }
57
54
 
58
- // ============================================================================
59
- // RSC Serialization Primitives (internal)
60
- // ============================================================================
61
-
62
55
  /**
63
56
  * RSC-serialize a value using React Server Components stream.
64
57
  * Used for serializing loaderData, layout, loading components etc.
@@ -90,10 +83,6 @@ export async function rscDeserialize<T>(
90
83
  return createFromReadableStream<T>(stream, { temporaryReferences });
91
84
  }
92
85
 
93
- // ============================================================================
94
- // Null-Preserving RSC Serialization (for caching)
95
- // ============================================================================
96
-
97
86
  /**
98
87
  * RSC-serialize any value including null.
99
88
  * Unlike rscSerialize(), this does NOT skip null — it serializes it through
@@ -107,7 +96,14 @@ export async function serializeResult(value: unknown): Promise<string | null> {
107
96
  const temporaryReferences = createTemporaryReferenceSet();
108
97
  const stream = renderToReadableStream(value, { temporaryReferences });
109
98
  return await streamToString(stream);
110
- } catch {
99
+ } catch (error) {
100
+ // Returning null silently turns a non-serializable cache value into a
101
+ // permanent miss with no trace. Surface it on the internal debug channel so
102
+ // a failed serialize is diagnosable on wrangler tail, but keep returning
103
+ // null so the caller falls through to an uncached render rather than throws.
104
+ if (INTERNAL_RANGO_DEBUG) {
105
+ console.warn("[segment-codec] serializeResult failed:", error);
106
+ }
111
107
  return null;
112
108
  }
113
109
  }
@@ -122,10 +118,6 @@ export async function deserializeResult<T>(encoded: string): Promise<T> {
122
118
  return createFromReadableStream<T>(stream, { temporaryReferences });
123
119
  }
124
120
 
125
- // ============================================================================
126
- // Public API
127
- // ============================================================================
128
-
129
121
  /**
130
122
  * RSC-deserialize a single encoded component string back to a React element.
131
123
  * Used by the static handler runtime to revive pre-rendered components.
@@ -0,0 +1,417 @@
1
+ /**
2
+ * Capture data snapshot: recording + seeding stores for PPR shell parity.
3
+ *
4
+ * The scar tissue this fixes: a PPR HIT serves frozen prelude bytes, then a
5
+ * FULL FRESH Flight render for hydration. Any shell-baked (non-hole) content
6
+ * that drifts between capture time and hit time — a cache() segment with a
7
+ * shorter ttl than the shell, a tag-invalidated item — makes the fresh payload
8
+ * disagree with the prelude, so React throws a hydration text mismatch and
9
+ * regenerates the tree client-side (wiping the FOUC theme class, flashing
10
+ * content). See docs/design/ppr-shell-resume.md.
11
+ *
12
+ * The fix (Next.js resume-data-cache analog, adapted to Rango's cache rings):
13
+ * the CAPTURE render records every cache-store read-hit and write it performed
14
+ * (the {@link RecordingShellStore}); the record rides inside the ShellCacheEntry
15
+ * as its `snapshot`; on a HIT the tail render reads through a
16
+ * {@link SeededShellStore} overlay that serves those recorded values AS FRESH,
17
+ * so the shell region reproduces byte-identically while everything NOT recorded
18
+ * (the holes — masked loaders were never executed at capture, so their reads
19
+ * were never recorded) stays live.
20
+ *
21
+ * The invariant, verbatim: the snapshot is exactly the set of cache-store reads
22
+ * the capture render performed; replaying them on a HIT reproduces the shell
23
+ * content byte-identically; everything not recorded stays live.
24
+ */
25
+
26
+ import type {
27
+ SegmentCacheStore,
28
+ CacheGetResult,
29
+ CacheItemResult,
30
+ CacheItemOptions,
31
+ CachedEntryData,
32
+ ShellCacheEntry,
33
+ ShellSnapshotRecord,
34
+ ShellSnapshotItemValue,
35
+ ShellSnapshotResponseValue,
36
+ ShellSnapshotLoaderValue,
37
+ } from "./types.js";
38
+ import { bufferToBase64, base64ToBuffer } from "./cf/cf-base64.js";
39
+ import { isPerClientSignalHeader } from "../browser/cookie-name.js";
40
+
41
+ /** Compose the last-write-wins map key. NUL (`\u0000`) cannot appear in a cache key. */
42
+ function recordKey(family: ShellSnapshotRecord["family"], key: string): string {
43
+ return `${family}\u0000${key}`;
44
+ }
45
+
46
+ /** Serialize a Response to the snapshot's stored shape (base64 body). */
47
+ async function serializeResponse(
48
+ response: Response,
49
+ ): Promise<ShellSnapshotResponseValue> {
50
+ const body = await response.clone().arrayBuffer();
51
+ const headers: [string, string][] = [];
52
+ response.headers.forEach((value, name) => {
53
+ // Mirror putResponse: per-client signal headers never enter a shared entry.
54
+ if (isPerClientSignalHeader(name)) return;
55
+ headers.push([name, value]);
56
+ });
57
+ return { status: response.status, headers, body: bufferToBase64(body) };
58
+ }
59
+
60
+ /** Rebuild a live Response from a snapshot's stored response shape. */
61
+ function deserializeResponse(value: ShellSnapshotResponseValue): Response {
62
+ return new Response(base64ToBuffer(value.body), {
63
+ status: value.status,
64
+ headers: new Headers(value.headers),
65
+ });
66
+ }
67
+
68
+ /**
69
+ * A store wrapper the CAPTURE render reads through. Every call passes through to
70
+ * the underlying store unchanged; for the item/segment/response families it also
71
+ * RECORDS, last-write-wins per (family, key):
72
+ * - read-hits (get/getItem/getResponse returning non-null) — the value that
73
+ * fed the shell,
74
+ * - writes (set/setItem/putResponse) — the value a MISS computed and baked.
75
+ * The shell family (getShell/putShell) is never recorded (the snapshot rides
76
+ * inside a shell entry). Reads that MISS are not recorded (a miss produced no
77
+ * shell content; if the render then computed and wrote, that write is recorded).
78
+ *
79
+ * Deferred writes: cache writes run under waitUntil (fire-and-forget on Node,
80
+ * executionContext on workerd), so their setItem/set calls — hence their records
81
+ * — may land after the shell has quiesced. The capture collects those write
82
+ * promises via {@link trackWrite} and awaits them ({@link settleWrites}) before
83
+ * draining, so a MISS-at-capture value is still pinned.
84
+ */
85
+ export class RecordingShellStore<
86
+ TEnv = unknown,
87
+ > implements SegmentCacheStore<TEnv> {
88
+ private readonly records = new Map<string, ShellSnapshotRecord>();
89
+ private readonly writes: Promise<unknown>[] = [];
90
+
91
+ constructor(private readonly inner: SegmentCacheStore<TEnv>) {}
92
+
93
+ get defaults(): SegmentCacheStore<TEnv>["defaults"] {
94
+ return this.inner.defaults;
95
+ }
96
+ get keyGenerator(): SegmentCacheStore<TEnv>["keyGenerator"] {
97
+ return this.inner.keyGenerator;
98
+ }
99
+
100
+ private record(
101
+ family: ShellSnapshotRecord["family"],
102
+ key: string,
103
+ value: ShellSnapshotRecord["value"],
104
+ ): void {
105
+ this.records.set(recordKey(family, key), { family, key, value });
106
+ }
107
+
108
+ /** Track a deferred cache-write promise so the capture can await it pre-drain. */
109
+ trackWrite(p: Promise<unknown>): void {
110
+ this.writes.push(p);
111
+ }
112
+
113
+ /**
114
+ * Await the tracked deferred writes so their records are present before drain.
115
+ * Drains ITERATIVELY: a write task can schedule a NESTED write (the ring-3
116
+ * cacheRoute path schedules its actual store.set in a second waitUntil while the
117
+ * first is running), so each awaited batch may enqueue more. Loop until the
118
+ * queue empties or the deadline passes. Bounded: a pathologically slow write
119
+ * must never stall the capture task, so a key that does not settle in time is
120
+ * left unpinned (it drifts, the pre-snapshot behavior) rather than hanging.
121
+ */
122
+ async settleWrites(timeoutMs: number): Promise<void> {
123
+ const deadline = Date.now() + timeoutMs;
124
+ while (this.writes.length > 0) {
125
+ const remaining = deadline - Date.now();
126
+ if (remaining <= 0) return;
127
+ // Take the current batch; new writes scheduled while awaiting accumulate in
128
+ // this.writes and are drained on the next iteration.
129
+ const batch = this.writes.splice(0);
130
+ let timer: ReturnType<typeof setTimeout> | undefined;
131
+ const guard = new Promise<void>((resolve) => {
132
+ timer = setTimeout(resolve, remaining);
133
+ (timer as { unref?: () => void }).unref?.();
134
+ });
135
+ await Promise.race([Promise.allSettled(batch).then(() => {}), guard]);
136
+ if (timer) clearTimeout(timer);
137
+ }
138
+ }
139
+
140
+ /** The recorded snapshot (last-write-wins per family+key), or undefined if empty. */
141
+ drainSnapshot(): ShellSnapshotRecord[] | undefined {
142
+ return this.records.size > 0 ? [...this.records.values()] : undefined;
143
+ }
144
+
145
+ async get(key: string): Promise<CacheGetResult | null> {
146
+ const result = await this.inner.get(key);
147
+ if (result) this.record("segment", key, result.data);
148
+ return result;
149
+ }
150
+
151
+ async set(
152
+ key: string,
153
+ data: CachedEntryData,
154
+ ttl: number,
155
+ swr?: number,
156
+ ): Promise<void> {
157
+ this.record("segment", key, data);
158
+ return this.inner.set(key, data, ttl, swr);
159
+ }
160
+
161
+ async delete(key: string): Promise<boolean> {
162
+ return this.inner.delete(key);
163
+ }
164
+
165
+ async clear(): Promise<void> {
166
+ return this.inner.clear?.();
167
+ }
168
+
169
+ async getResponse(
170
+ key: string,
171
+ ): Promise<{ response: Response; shouldRevalidate: boolean } | null> {
172
+ if (!this.inner.getResponse) return null;
173
+ const result = await this.inner.getResponse(key);
174
+ if (result)
175
+ this.record("response", key, await serializeResponse(result.response));
176
+ return result;
177
+ }
178
+
179
+ async putResponse(
180
+ key: string,
181
+ response: Response,
182
+ ttl: number,
183
+ swr?: number,
184
+ tags?: string[],
185
+ ): Promise<void> {
186
+ if (!this.inner.putResponse) return;
187
+ this.record("response", key, await serializeResponse(response));
188
+ return this.inner.putResponse(key, response, ttl, swr, tags);
189
+ }
190
+
191
+ async getItem(key: string): Promise<CacheItemResult | null> {
192
+ if (!this.inner.getItem) return null;
193
+ const result = await this.inner.getItem(key);
194
+ if (result) {
195
+ const value: ShellSnapshotItemValue = {
196
+ value: result.value,
197
+ handles: result.handles,
198
+ tags: result.tags,
199
+ };
200
+ this.record("item", key, value);
201
+ }
202
+ return result;
203
+ }
204
+
205
+ async setItem(
206
+ key: string,
207
+ value: string,
208
+ options?: CacheItemOptions,
209
+ ): Promise<void> {
210
+ if (!this.inner.setItem) return;
211
+ const stored: ShellSnapshotItemValue = {
212
+ value,
213
+ handles: options?.handles,
214
+ tags: options?.tags,
215
+ };
216
+ this.record("item", key, stored);
217
+ return this.inner.setItem(key, value, options);
218
+ }
219
+
220
+ async getShell(
221
+ key: string,
222
+ ): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
223
+ return this.inner.getShell ? this.inner.getShell(key) : null;
224
+ }
225
+
226
+ async putShell(
227
+ key: string,
228
+ entry: ShellCacheEntry,
229
+ ttlSeconds?: number,
230
+ swrSeconds?: number,
231
+ tags?: string[],
232
+ ): Promise<void> {
233
+ return this.inner.putShell?.(key, entry, ttlSeconds, swrSeconds, tags);
234
+ }
235
+
236
+ async invalidateTags(tags: string[]): Promise<void> {
237
+ return this.inner.invalidateTags?.(tags);
238
+ }
239
+ }
240
+
241
+ /** True iff `store` is a RecordingShellStore (duck-typed across module copies). */
242
+ export function getRecordingStore<TEnv>(
243
+ store: SegmentCacheStore<TEnv> | undefined,
244
+ ): RecordingShellStore<TEnv> | undefined {
245
+ return store instanceof RecordingShellStore ? store : undefined;
246
+ }
247
+
248
+ /**
249
+ * Materialize the loader-family seed from a shell snapshot for a HIT's tail
250
+ * render: Flight-deserialize each recorded (promise-elided) bake-lane
251
+ * container into a segment-key -> container Map, which serveShellHit assigns
252
+ * to the tail context's `_shellLoaderSeed` for the resolveLoaderData overlay.
253
+ * Lives here so every snapshot family is decoded in this module (the
254
+ * item/segment/response families via {@link SeededShellStore}); the loader
255
+ * family is not a store read, so it seeds the context instead of a store.
256
+ *
257
+ * Deserializations run in parallel; a record that fails to decode is skipped
258
+ * (that loader drifts — the pre-snapshot behavior — instead of failing the
259
+ * HIT). Returns undefined when the snapshot carries no loader records, without
260
+ * touching the Flight codec (kept lazy for cold paths and non-RSC configs).
261
+ */
262
+ export async function buildShellLoaderSeed(
263
+ snapshot: ShellSnapshotRecord[],
264
+ ): Promise<Map<string, unknown> | undefined> {
265
+ const loaderRecords: ShellSnapshotRecord[] = [];
266
+ for (const rec of snapshot) {
267
+ if (rec.family === "loader") loaderRecords.push(rec);
268
+ }
269
+ if (loaderRecords.length === 0) return undefined;
270
+
271
+ const { deserializeResult } = await import("./segment-codec.js");
272
+ const entries = await Promise.all(
273
+ loaderRecords.map(async (rec): Promise<[string, unknown] | null> => {
274
+ try {
275
+ return [
276
+ rec.key,
277
+ await deserializeResult(
278
+ (rec.value as ShellSnapshotLoaderValue).value,
279
+ ),
280
+ ];
281
+ } catch {
282
+ return null;
283
+ }
284
+ }),
285
+ );
286
+ const seed = new Map<string, unknown>();
287
+ for (const entry of entries) {
288
+ if (entry) seed.set(entry[0], entry[1]);
289
+ }
290
+ return seed.size > 0 ? seed : undefined;
291
+ }
292
+
293
+ /**
294
+ * A read-through overlay the HIT tail render reads through. For a key present in
295
+ * the snapshot it serves the recorded value AS FRESH (shouldRevalidate: false —
296
+ * a pinned key must NOT kick SWR background revalidation) so the tail's payload
297
+ * matches the frozen prelude. Every other read falls through to the real store
298
+ * (the holes — masked loaders were never recorded — stay live). ALL writes pass
299
+ * through unchanged: a live hole's loader may legitimately write. The shell
300
+ * family always passes through.
301
+ */
302
+ export class SeededShellStore<
303
+ TEnv = unknown,
304
+ > implements SegmentCacheStore<TEnv> {
305
+ private readonly items = new Map<string, ShellSnapshotItemValue>();
306
+ private readonly segments = new Map<string, CachedEntryData>();
307
+ private readonly responses = new Map<string, ShellSnapshotResponseValue>();
308
+
309
+ constructor(
310
+ private readonly inner: SegmentCacheStore<TEnv>,
311
+ snapshot: ShellSnapshotRecord[],
312
+ ) {
313
+ for (const rec of snapshot) {
314
+ if (rec.family === "item") {
315
+ this.items.set(rec.key, rec.value as ShellSnapshotItemValue);
316
+ } else if (rec.family === "segment") {
317
+ this.segments.set(rec.key, rec.value as CachedEntryData);
318
+ } else if (rec.family === "response") {
319
+ this.responses.set(rec.key, rec.value as ShellSnapshotResponseValue);
320
+ }
321
+ // "loader" family records are not store reads — serveShellHit seeds them
322
+ // onto the tail context (_shellLoaderSeed) for the resolveLoaderData
323
+ // overlay instead.
324
+ }
325
+ }
326
+
327
+ get defaults(): SegmentCacheStore<TEnv>["defaults"] {
328
+ return this.inner.defaults;
329
+ }
330
+ get keyGenerator(): SegmentCacheStore<TEnv>["keyGenerator"] {
331
+ return this.inner.keyGenerator;
332
+ }
333
+
334
+ async get(key: string): Promise<CacheGetResult | null> {
335
+ const seeded = this.segments.get(key);
336
+ if (seeded) return { data: seeded, shouldRevalidate: false };
337
+ return this.inner.get(key);
338
+ }
339
+
340
+ async set(
341
+ key: string,
342
+ data: CachedEntryData,
343
+ ttl: number,
344
+ swr?: number,
345
+ ): Promise<void> {
346
+ return this.inner.set(key, data, ttl, swr);
347
+ }
348
+
349
+ async delete(key: string): Promise<boolean> {
350
+ return this.inner.delete(key);
351
+ }
352
+
353
+ async clear(): Promise<void> {
354
+ return this.inner.clear?.();
355
+ }
356
+
357
+ async getResponse(
358
+ key: string,
359
+ ): Promise<{ response: Response; shouldRevalidate: boolean } | null> {
360
+ const seeded = this.responses.get(key);
361
+ if (seeded) {
362
+ return { response: deserializeResponse(seeded), shouldRevalidate: false };
363
+ }
364
+ return this.inner.getResponse ? this.inner.getResponse(key) : null;
365
+ }
366
+
367
+ async putResponse(
368
+ key: string,
369
+ response: Response,
370
+ ttl: number,
371
+ swr?: number,
372
+ tags?: string[],
373
+ ): Promise<void> {
374
+ return this.inner.putResponse?.(key, response, ttl, swr, tags);
375
+ }
376
+
377
+ async getItem(key: string): Promise<CacheItemResult | null> {
378
+ const seeded = this.items.get(key);
379
+ if (seeded) {
380
+ return {
381
+ value: seeded.value,
382
+ handles: seeded.handles,
383
+ tags: seeded.tags,
384
+ shouldRevalidate: false,
385
+ };
386
+ }
387
+ return this.inner.getItem ? this.inner.getItem(key) : null;
388
+ }
389
+
390
+ async setItem(
391
+ key: string,
392
+ value: string,
393
+ options?: CacheItemOptions,
394
+ ): Promise<void> {
395
+ return this.inner.setItem?.(key, value, options);
396
+ }
397
+
398
+ async getShell(
399
+ key: string,
400
+ ): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
401
+ return this.inner.getShell ? this.inner.getShell(key) : null;
402
+ }
403
+
404
+ async putShell(
405
+ key: string,
406
+ entry: ShellCacheEntry,
407
+ ttlSeconds?: number,
408
+ swrSeconds?: number,
409
+ tags?: string[],
410
+ ): Promise<void> {
411
+ return this.inner.putShell?.(key, entry, ttlSeconds, swrSeconds, tags);
412
+ }
413
+
414
+ async invalidateTags(tags: string[]): Promise<void> {
415
+ return this.inner.invalidateTags?.(tags);
416
+ }
417
+ }