@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
@@ -8,7 +8,7 @@
8
8
  * Cache key resolution (3-tier, matching CacheScope.resolveKey):
9
9
  * 1. options.key(requestCtx) — full override
10
10
  * 2. store.keyGenerator(requestCtx, defaultKey) — store-level modification
11
- * 3. loader:{loaderId}:{pathname}:{sortedParams} — default
11
+ * 3. loader:{loaderId}:{host}{pathname}:{sortedParams} — default
12
12
  *
13
13
  * Values are serialized via RSC Flight (serializeResult/deserializeResult),
14
14
  * supporting ReactNode, Promises, null, and all RSC-serializable types.
@@ -19,18 +19,32 @@
19
19
  */
20
20
 
21
21
  import type { LoaderEntry } from "../../server/context.js";
22
- import type { HandlerContext } from "../../types.js";
22
+ import type { HandlerContext, InternalHandlerContext } from "../../types.js";
23
23
  import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
24
- import { getRequestContext } from "../../server/request-context.js";
24
+ import {
25
+ getRequestContext,
26
+ _getRequestContext,
27
+ runWithRequestContext,
28
+ } from "../../server/request-context.js";
25
29
  import { sortedRouteParams } from "../../cache/cache-key-utils.js";
26
30
  import {
27
31
  resolveTtl,
28
32
  resolveSwrWindow,
29
33
  resolveCacheKey,
30
34
  resolveCacheStore,
35
+ resolveTagsOption,
31
36
  DEFAULT_ROUTE_TTL,
32
37
  } from "../../cache/cache-policy.js";
33
38
  import { readThroughItem } from "../../cache/read-through-swr.js";
39
+ import {
40
+ maskNestedContainerThenables,
41
+ overlayLoaderContainer,
42
+ } from "./loader-snapshot.js";
43
+ import { recordRequestTags } from "../../cache/cache-tag.js";
44
+ import {
45
+ isShellCaptureActive,
46
+ createMaskedLoaderPromise,
47
+ } from "./loader-mask.js";
34
48
  // Lazy-loaded to avoid pulling @vitejs/plugin-rsc/rsc into modules that
35
49
  // import segment-resolution but never use loader caching.
36
50
  let _serializeResult: typeof import("../../cache/segment-codec.js").serializeResult;
@@ -55,12 +69,13 @@ function debugLoaderCacheLog(message: string): void {
55
69
 
56
70
  function getDefaultLoaderCacheKey(
57
71
  loaderId: string,
72
+ host: string,
58
73
  pathname: string,
59
74
  params: Record<string, string>,
60
75
  ): string {
61
76
  const paramStr = sortedRouteParams(params);
62
77
  const base = paramStr ? `${pathname}:${paramStr}` : pathname;
63
- return `loader:${loaderId}:${base}`;
78
+ return `loader:${loaderId}:${host}${base}`;
64
79
  }
65
80
 
66
81
  /**
@@ -74,7 +89,13 @@ async function resolveLoaderKey(
74
89
  params: Record<string, string>,
75
90
  ): Promise<string> {
76
91
  const options = loaderEntry.cache!.options;
77
- const defaultKey = getDefaultLoaderCacheKey(loaderId, pathname, params);
92
+ // The host is part of the loader cache identity, matching the route-level
93
+ // cache (cache-scope getCacheKeyBase: `${host}${pathname}`) and "use cache"
94
+ // (cache-runtime pushes ctx.url.host). Without it, a multi-tenant host router
95
+ // serving the same pathname for different hosts would leak one host's cached
96
+ // loader data to another.
97
+ const host = getRequestContext()?.url?.host ?? "localhost";
98
+ const defaultKey = getDefaultLoaderCacheKey(loaderId, host, pathname, params);
78
99
  if (options === false) return defaultKey;
79
100
  return resolveCacheKey(options.key, store, defaultKey, "LoaderCache");
80
101
  }
@@ -87,23 +108,8 @@ async function resolveLoaderKey(
87
108
  */
88
109
  function resolveTags(loaderEntry: LoaderEntry): string[] | undefined {
89
110
  const options = loaderEntry.cache?.options;
90
- if (!options || !options.tags) return undefined;
91
-
92
- if (typeof options.tags === "function") {
93
- const requestCtx = getRequestContext();
94
- if (!requestCtx) return undefined;
95
- try {
96
- return options.tags(requestCtx);
97
- } catch (error) {
98
- console.error(
99
- `[LoaderCache] Tags function failed, caching without tags:`,
100
- error,
101
- );
102
- return undefined;
103
- }
104
- }
105
-
106
- return options.tags;
111
+ if (!options) return undefined;
112
+ return resolveTagsOption(options.tags, getRequestContext(), "LoaderCache");
107
113
  }
108
114
 
109
115
  function getLoaderStore(
@@ -119,11 +125,86 @@ function getLoaderStore(
119
125
  *
120
126
  * When the LoaderEntry has no cache config, delegates directly to ctx.use(loader).
121
127
  * When cached, checks store first and stores on miss via waitUntil.
128
+ *
129
+ * Loader metering is NOT done here — it lives at the ctx.use execution funnel
130
+ * (observePhase; see instrument.ts). A cache HIT returns without calling ctx.use,
131
+ * so it emits no loader phase (the loader did not execute; the hit is only a
132
+ * LoaderCache debug log).
122
133
  */
123
134
  export function resolveLoaderData<TEnv>(
124
135
  loaderEntry: LoaderEntry,
125
136
  ctx: HandlerContext<any, TEnv>,
126
137
  pathname: string,
138
+ bakeSegmentKey?: string | null,
139
+ ): Promise<any> {
140
+ // One ALS read serves the capture check, the record registration, and the
141
+ // seed lookup — this runs for every loader on every request.
142
+ const reqCtx = _getRequestContext();
143
+ // PPR shell capture policy — gated here, the single funnel every loader
144
+ // segment path routes through (fresh resolveLoaders, cache-hit
145
+ // resolveLoadersOnly, revalidation resolveLoadersOnlyWithRevalidation).
146
+ //
147
+ // Two lanes (docs/design/loader-container-bake.md):
148
+ // - LIVE lane (entry has renderable loading(); no `bakeSegmentKey`): never
149
+ // execute during capture. The slot gets a never-resolving promise so the
150
+ // LoaderBoundary postpones (a hole). See loader-mask.ts.
151
+ // - BAKE lane (no renderable loading(); callers pass `bakeSegmentKey`):
152
+ // execute during capture exactly like axis 1 — the settled container bakes
153
+ // into the prelude, nested pending promises postpone at the consumer's own
154
+ // Suspense. The container promise is registered on the derived context so
155
+ // captureAndStoreShell pins it into the snapshot's loader family; on a
156
+ // shell HIT the recorded container is overlaid onto the fresh run so the
157
+ // payload matches the frozen prelude byte-for-byte.
158
+ if (isShellCaptureActive(reqCtx)) {
159
+ if (!bakeSegmentKey) {
160
+ return createMaskedLoaderPromise();
161
+ }
162
+ const containerPromise = executeLoaderData(loaderEntry, ctx, pathname);
163
+ // Pre-attach a no-op catch: a bake-lane rejection during capture must
164
+ // surface through the drain's refusal (and the wrapper's error boundary),
165
+ // never as an unhandled rejection that can kill the worker before the
166
+ // drain probes this record.
167
+ containerPromise.catch(() => {});
168
+ // Nested-promise SHAPE is the liveness declaration: mask nested thenables
169
+ // in the capture's copy of the container so the consuming subtree
170
+ // postpones as a hole no matter when the promise settles, elide records a
171
+ // HOLE marker, and every HIT streams the fresh value. Without this, a
172
+ // nested promise that settled before the quiet window baked its value into
173
+ // the SHARED shell and the snapshot pinned it for every visitor
174
+ // (per-request basket data served cross-session, found live). The raw
175
+ // container is untouched: handler-side ctx.use consumption (the
176
+ // consumption-lane rule, semantic-matrix PPR3) keeps real values.
177
+ const maskedPromise = containerPromise.then((container: unknown) =>
178
+ maskNestedContainerThenables(container),
179
+ );
180
+ maskedPromise.catch(() => {});
181
+ reqCtx?._shellCaptureLoaderRecords?.set(bakeSegmentKey, maskedPromise);
182
+ return maskedPromise;
183
+ }
184
+
185
+ if (bakeSegmentKey) {
186
+ const seed = reqCtx?._shellLoaderSeed;
187
+ if (seed && seed.has(bakeSegmentKey)) {
188
+ const recorded = seed.get(bakeSegmentKey);
189
+ // HIT tail: run fresh (only the loader body can mint the live nested
190
+ // promises), then pin the recorded paths over it. A fresh REJECTION here
191
+ // skips the overlay and flows to the per-loader error boundary — the
192
+ // payload then diverges from the prelude (same residual class as uncached
193
+ // nondeterminism in shell material).
194
+ return executeLoaderData(loaderEntry, ctx, pathname).then(
195
+ (fresh: unknown) => overlayLoaderContainer(fresh, recorded),
196
+ );
197
+ }
198
+ }
199
+
200
+ return executeLoaderData(loaderEntry, ctx, pathname);
201
+ }
202
+
203
+ /** The pre-policy loader execution: cache read-through or plain ctx.use. */
204
+ function executeLoaderData<TEnv>(
205
+ loaderEntry: LoaderEntry,
206
+ ctx: HandlerContext<any, TEnv>,
207
+ pathname: string,
127
208
  ): Promise<any> {
128
209
  const cacheConfig = loaderEntry.cache;
129
210
 
@@ -148,15 +229,51 @@ export function resolveLoaderData<TEnv>(
148
229
 
149
230
  const loaderId = loaderEntry.loader.$$id;
150
231
 
232
+ // A handler that later awaits this same loader via ctx.use(loader) must get
233
+ // THIS memoized promise, not a fresh execution. Rather than rebind ctx.use
234
+ // once per cached loader (O(N) chained wrappers + a synchronous
235
+ // capture-before-overwrite invariant), install a single stable interceptor on
236
+ // the first cached loader that consults a per-ctx override table, then just
237
+ // prime the table for each subsequent cached loader. The captured pre-
238
+ // interceptor `originalUse` (whatever setup mode installed it) runs the
239
+ // cache-miss execute, so a loader never awaits its own in-flight promise.
240
+ const internal = ctx as InternalHandlerContext<any, TEnv>;
241
+ let overrides = internal._loaderCacheOverrides;
242
+ if (!overrides) {
243
+ overrides = internal._loaderCacheOverrides = new Map();
244
+ const originalUse = ctx.use;
245
+ internal._loaderCacheOriginalUse = originalUse;
246
+ ctx.use = ((item: any) => {
247
+ const cached = overrides!.get(item?.$$id);
248
+ if (cached) return cached;
249
+ return originalUse(item);
250
+ }) as typeof ctx.use;
251
+ }
252
+ const runMiss = internal._loaderCacheOriginalUse!;
253
+
254
+ // Dedup the cache read-through across repeated resolutions of the SAME
255
+ // loaderId in one request. An orphan layout with parallel slots inherits its
256
+ // parent route's loaders, so resolveOrphanLayout (fresh.ts) re-resolves the
257
+ // parent's loaders under a different shortCode — calling resolveLoaderData
258
+ // again for the same loaderId. The cache key (loader:{loaderId}:{host}
259
+ // {pathname}:{sortedParams}) does not include the shortCode and ctx/params
260
+ // are identical, so both resolutions produce the same data. Reuse the already
261
+ // in-flight dataPromise instead of issuing a second getItem/setItem (e.g. a
262
+ // second KV round-trip) for one logical cached loader. The shortCode only
263
+ // affects the emitted segmentId in resolveLoaders, not the cached value.
264
+ const existing = overrides.get(loaderId);
265
+ if (existing) return existing;
266
+
267
+ // Compute ttl/swr/tags only AFTER the dedup short-circuit: a deduped second
268
+ // resolution of the same loaderId (the orphan-layout inheritance path) must
269
+ // not re-run the user tags() callback. These values are only consumed inside
270
+ // the read-through below, so they belong here, past the dedup gate.
151
271
  const ttl = resolveTtl(options.ttl, store.defaults, DEFAULT_ROUTE_TTL);
152
272
  const swrWindow = resolveSwrWindow(options.swr, store.defaults);
153
273
  const swr = swrWindow || undefined;
154
274
  const tags = resolveTags(loaderEntry);
275
+ recordRequestTags(tags);
155
276
 
156
- // Wrap ctx.use() so cache HIT primes the handler's memoization map.
157
- // ctx.use() closes over the match context's loaderPromises (not request context's).
158
- // By intercepting ctx.use(), we inject cached data into the correct map.
159
- const originalUse = ctx.use;
160
277
  const dataPromise = (async () => {
161
278
  const codec = await getCodec();
162
279
  const key = await resolveLoaderKey(
@@ -167,11 +284,20 @@ export function resolveLoaderData<TEnv>(
167
284
  ctx.params,
168
285
  );
169
286
 
287
+ // Capture the request context up front (foreground, ALS present) so the
288
+ // background stale revalidation can re-establish it. On workerd a waitUntil
289
+ // task runs detached from the request's I/O context, so a loader body that
290
+ // reads the ambient getRequestContext() would otherwise throw "called
291
+ // outside of a request context" and the revalidation would fail silently.
292
+ // The wrap is applied via wrapBackground (background path only); the
293
+ // foreground miss runs execute() directly since its context is present.
294
+ const requestCtxForExecute = getRequestContext();
170
295
  return readThroughItem({
171
296
  getItem: (k) => store.getItem!(k),
172
297
  setItem: (k, v, o) => store.setItem!(k, v, o),
173
298
  key,
174
- execute: () => originalUse(loaderEntry.loader),
299
+ execute: () => runMiss(loaderEntry.loader),
300
+ wrapBackground: (run) => runWithRequestContext(requestCtxForExecute, run),
175
301
  serialize: (d) => codec.serializeResult(d),
176
302
  deserialize: (v) => codec.deserializeResult(v),
177
303
  storeOptions: { ttl, swr, tags },
@@ -179,21 +305,11 @@ export function resolveLoaderData<TEnv>(
179
305
  onStale: () => debugLoaderCacheLog(`[LoaderCache] STALE: ${key}`),
180
306
  onMiss: () => debugLoaderCacheLog(`[LoaderCache] MISS: ${key}`),
181
307
  onCached: () => debugLoaderCacheLog(`[LoaderCache] Cached: ${key}`),
182
- host: getRequestContext(),
308
+ host: requestCtxForExecute,
183
309
  });
184
310
  })();
185
311
 
186
- // Temporarily replace ctx.use() so the handler's call returns cached data.
187
- // This is needed because ctx.use() closes over the match context's loaderPromises
188
- // map which is separate from the request context. By wrapping use(), we intercept
189
- // the handler's call and return the shared dataPromise.
190
- const wrappedUse = ((item: any) => {
191
- if (item === loaderEntry.loader || item?.$$id === loaderId) {
192
- return dataPromise;
193
- }
194
- return originalUse(item);
195
- }) as typeof ctx.use;
196
- ctx.use = wrappedUse;
312
+ overrides.set(loaderId, dataPromise);
197
313
 
198
314
  return dataPromise;
199
315
  }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * PPR shell-capture loader masking.
3
+ *
4
+ * During a shell CAPTURE re-render (Axis 2, see docs/design/ppr-shell-resume.md)
5
+ * route loaders are the "live lane": they must NOT execute — no side effects, no
6
+ * cost, no cache round-trips. Instead every loader segment's value slot receives
7
+ * a never-resolving promise, so the loader-consuming Suspense subtree stays
8
+ * pending and React's static `prerender` marks it as a postponed hole. The frozen
9
+ * shell (prelude) captures only the fallback; the resumed serve pass runs the
10
+ * loaders fresh through the unchanged execution path and streams their output
11
+ * into the holes.
12
+ *
13
+ * Capture mode is signalled by `requestCtx._shellCaptureRun`, set to true ONLY on
14
+ * the derived request context of the background capture task (shell-capture.ts) —
15
+ * NOT by the foreground render, whose `_shellCapture` descriptor merely means "a
16
+ * capture is wanted" and must not change behavior. This module is the single home
17
+ * for the mask so every loader execution site gates the same way (loader-cache.ts
18
+ * `resolveLoaderData`, fresh.ts `resolveLoaders`).
19
+ */
20
+
21
+ import {
22
+ _getRequestContext,
23
+ type RequestContext,
24
+ } from "../../server/request-context.js";
25
+
26
+ /**
27
+ * True when the current render is the active PPR shell capture and route loaders
28
+ * must be masked rather than executed. Reads `_shellCaptureRun` off the ALS
29
+ * request context (the capture task re-establishes its derived context via
30
+ * runWithRequestContext), so it is accurate at the loader resolution sites, which
31
+ * run synchronously inside the pipeline's context frame.
32
+ */
33
+ export function isShellCaptureActive(
34
+ reqCtx: RequestContext<any> | undefined = _getRequestContext(),
35
+ ): boolean {
36
+ return reqCtx?._shellCaptureRun === true;
37
+ }
38
+
39
+ // createMaskedLoaderPromise moved to the leaf module mask-nested.ts (shared
40
+ // with the handle-push funnel in request-context, which cannot import THIS
41
+ // module without a cycle). Re-exported to keep the mask API in one place.
42
+ export { createMaskedLoaderPromise } from "./mask-nested.js";
43
+
44
+ /**
45
+ * Lane decision for an entry's loaders under PPR (the loading() value decides;
46
+ * docs/design/loader-container-bake.md):
47
+ *
48
+ * - RENDERABLE loading() (the LoaderBoundary Suspense fallback) — the LIVE
49
+ * lane: masked at capture, guaranteed fresh on every serve. Returns true.
50
+ * - No loading() (absent, or explicitly `false`, incl. `loading(x, { ssr:
51
+ * false })` under the SSR manifest) — the BAKE lane: the loader executes
52
+ * during capture, its settled container bakes, nested pending promises hole
53
+ * at the consumer's own Suspense. Returns false.
54
+ *
55
+ * Mirrors segment-system's isRenderableLoading so the mask decision and the
56
+ * tree's boundary placement can never disagree.
57
+ */
58
+ export function entryLoadingMasksLoaders(loading: unknown): boolean {
59
+ return loading !== undefined && loading !== null && loading !== false;
60
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Loader-family capture snapshot: elide + overlay for bake-lane loader
3
+ * containers (docs/design/loader-container-bake.md).
4
+ *
5
+ * A loader on an entry with NO renderable loading() executes during shell
6
+ * capture; its settled container bakes into the prelude while every promise
7
+ * still pending at the quiet window postpones as a hole. To keep a HIT's fresh
8
+ * payload byte-identical to that frozen prelude, the capture pins the container
9
+ * in the shell snapshot:
10
+ *
11
+ * - elide: deep-walk the settled container; a PENDING nested promise is a
12
+ * hole, replaced by {@link LOADER_HOLE_KEY} — and since
13
+ * loader-cache masks every nested thenable at capture
14
+ * ({@link maskNestedContainerThenables}), nested promises are
15
+ * ALWAYS pending here for new captures. The SETTLED-marker branch
16
+ * below is legacy: it fires only if a settled thenable reaches
17
+ * elide anyway, and the overlay keeps decoding SETTLED markers
18
+ * from pre-mask snapshots. The result is promise-free and
19
+ * Flight-serializable.
20
+ * - overlay: on a HIT the loader runs fresh (only the loader body can mint
21
+ * the live nested promises), then the recorded container is laid
22
+ * over it: recorded paths win (they are what the prelude froze),
23
+ * hole-marker paths take the fresh run's value (the live hole),
24
+ * SETTLED-marker paths become Promise.resolve(pinned) — consumers
25
+ * wrote use(data.x) against a promise-shaped container, and
26
+ * handing them the raw value throws React #438 on every HIT (the
27
+ * storefront PDP regression) — and fresh-only paths pass through
28
+ * (they cannot contradict prelude bytes that never rendered them).
29
+ */
30
+
31
+ import { isThenable } from "../../handles/is-thenable.js";
32
+
33
+ // Capture-side nested-thenable masking lives in the LEAF module mask-nested.ts
34
+ // (request-context also needs it for handle pushes and cannot import through
35
+ // loader-mask without a cycle). Re-exported here so loader-cache and the unit
36
+ // tests keep one import site for the snapshot family.
37
+ export { maskNestedContainerThenables } from "./mask-nested.js";
38
+
39
+ /**
40
+ * Marker object standing in for a pending nested promise in a recorded loader
41
+ * container. Shape-checked (not identity-checked) because the record round-trips
42
+ * through Flight serialization and a JSON-embedding store envelope.
43
+ */
44
+ export const LOADER_HOLE_KEY = "$rangoLoaderHole" as const;
45
+
46
+ export interface LoaderHoleMarker {
47
+ [LOADER_HOLE_KEY]: 1;
48
+ }
49
+
50
+ export function isLoaderHoleMarker(value: unknown): value is LoaderHoleMarker {
51
+ return (
52
+ typeof value === "object" &&
53
+ value !== null &&
54
+ (value as Record<string, unknown>)[LOADER_HOLE_KEY] === 1
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Marker wrapping the inlined value of a NESTED promise that settled during
60
+ * capture. The value baked (physics), but the container key was a promise —
61
+ * the overlay must hand consumers a Promise.resolve(value), not the raw value,
62
+ * or an unconditional use(data.x) throws React #438 on every HIT. The ROOT
63
+ * container is never wrapped: loader-cache overlays against the awaited fresh
64
+ * container value.
65
+ */
66
+ export const LOADER_SETTLED_KEY = "$rangoLoaderSettled" as const;
67
+
68
+ export interface LoaderSettledMarker {
69
+ [LOADER_SETTLED_KEY]: 1;
70
+ value: unknown;
71
+ /**
72
+ * Present when the pinned subtree contains hole markers. Computed once at
73
+ * capture (elide already visits every node) so the per-HIT overlay never
74
+ * rescans the pinned structure to pick its rehydration path.
75
+ */
76
+ holes?: 1;
77
+ }
78
+
79
+ export function isLoaderSettledMarker(
80
+ value: unknown,
81
+ ): value is LoaderSettledMarker {
82
+ return (
83
+ typeof value === "object" &&
84
+ value !== null &&
85
+ (value as Record<string, unknown>)[LOADER_SETTLED_KEY] === 1
86
+ );
87
+ }
88
+
89
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
90
+ if (typeof value !== "object" || value === null) return false;
91
+ const proto = Object.getPrototypeOf(value);
92
+ return proto === Object.prototype || proto === null;
93
+ }
94
+
95
+ /**
96
+ * Probe whether a promise is already settled without waiting for it: races it
97
+ * against an immediately-resolved sentinel across two microtask hops (then
98
+ * chaining means a resolved inner value needs one extra hop to surface).
99
+ * Returns the settled state, or "pending" if it has not settled by then.
100
+ */
101
+ async function probeSettled(
102
+ p: PromiseLike<unknown>,
103
+ ): Promise<
104
+ | { state: "fulfilled"; value: unknown }
105
+ | { state: "rejected" }
106
+ | { state: "pending" }
107
+ > {
108
+ const PENDING = Symbol("pending");
109
+ // Two sentinel hops: Promise.resolve(p) adoption costs a microtask, so a
110
+ // single-hop sentinel would misreport an already-fulfilled promise as pending.
111
+ const sentinel = Promise.resolve()
112
+ .then(() => undefined)
113
+ .then(() => PENDING as unknown);
114
+ try {
115
+ const raced = await Promise.race([Promise.resolve(p), sentinel]);
116
+ if (raced === PENDING) return { state: "pending" };
117
+ return { state: "fulfilled", value: raced };
118
+ } catch {
119
+ return { state: "rejected" };
120
+ }
121
+ }
122
+
123
+ export type ElideResult =
124
+ | { state: "ok"; value: unknown; hasHole: boolean }
125
+ | { state: "rejected" };
126
+
127
+ /**
128
+ * Deep-elide a settled bake-lane container for recording. Settled nested
129
+ * promises pin their value behind a settled marker (they baked, but consumers
130
+ * hold a promise-shaped key); pending ones become hole markers; a REJECTED
131
+ * nested promise poisons the record (error UI must never bake into a shared
132
+ * shell) — the caller refuses the capture. Only plain objects/arrays are
133
+ * traversed; anything else (Date, Map, class instance) is a pinned leaf.
134
+ * Cycles are cut as pinned references (best effort — Flight rejects true
135
+ * cycles later regardless).
136
+ *
137
+ * The ROOT container promise-chain unwraps with NO marker: loader-cache
138
+ * overlays against the AWAITED fresh container, so the recorded root must be
139
+ * the unwrapped structure. Everything below it goes through elideNested.
140
+ */
141
+ export async function elideLoaderContainer(
142
+ value: unknown,
143
+ seen: Set<object> = new Set(),
144
+ ): Promise<ElideResult> {
145
+ if (isThenable(value)) {
146
+ const probed = await probeSettled(value);
147
+ if (probed.state === "pending") {
148
+ return { state: "ok", value: { [LOADER_HOLE_KEY]: 1 }, hasHole: true };
149
+ }
150
+ if (probed.state === "rejected") return { state: "rejected" };
151
+ return elideLoaderContainer(probed.value, seen);
152
+ }
153
+ return elideNested(value, seen);
154
+ }
155
+
156
+ async function elideNested(
157
+ value: unknown,
158
+ seen: Set<object>,
159
+ ): Promise<ElideResult> {
160
+ if (isThenable(value)) {
161
+ const probed = await probeSettled(value);
162
+ if (probed.state === "pending") {
163
+ return { state: "ok", value: { [LOADER_HOLE_KEY]: 1 }, hasHole: true };
164
+ }
165
+ if (probed.state === "rejected") return { state: "rejected" };
166
+ const inner = await elideNested(probed.value, seen);
167
+ if (inner.state === "rejected") return inner;
168
+ const marker: LoaderSettledMarker = inner.hasHole
169
+ ? { [LOADER_SETTLED_KEY]: 1, value: inner.value, holes: 1 }
170
+ : { [LOADER_SETTLED_KEY]: 1, value: inner.value };
171
+ return { state: "ok", value: marker, hasHole: inner.hasHole };
172
+ }
173
+
174
+ if (Array.isArray(value)) {
175
+ if (seen.has(value)) return { state: "ok", value, hasHole: false };
176
+ seen.add(value);
177
+ const out: unknown[] = new Array(value.length);
178
+ let hasHole = false;
179
+ for (let i = 0; i < value.length; i++) {
180
+ const r = await elideNested(value[i], seen);
181
+ if (r.state === "rejected") return r;
182
+ out[i] = r.value;
183
+ hasHole ||= r.hasHole;
184
+ }
185
+ return { state: "ok", value: out, hasHole };
186
+ }
187
+
188
+ if (isPlainObject(value)) {
189
+ if (seen.has(value)) return { state: "ok", value, hasHole: false };
190
+ seen.add(value);
191
+ const out: Record<string, unknown> = {};
192
+ let hasHole = false;
193
+ for (const key of Object.keys(value)) {
194
+ const r = await elideNested(value[key], seen);
195
+ if (r.state === "rejected") return r;
196
+ out[key] = r.value;
197
+ hasHole ||= r.hasHole;
198
+ }
199
+ return { state: "ok", value: out, hasHole };
200
+ }
201
+
202
+ return { state: "ok", value, hasHole: false };
203
+ }
204
+
205
+ /**
206
+ * Overlay a recorded (elided) container onto the fresh run's container for a
207
+ * shell HIT. Recorded wins per path; hole markers take the fresh value (the
208
+ * live nested promise); fresh-only object keys pass through. Where the shapes
209
+ * disagree structurally, recorded wins wholesale — it is what the prelude
210
+ * froze, and parity beats freshness inside the shell.
211
+ */
212
+ export function overlayLoaderContainer(
213
+ fresh: unknown,
214
+ recorded: unknown,
215
+ ): unknown {
216
+ if (isLoaderHoleMarker(recorded)) return fresh;
217
+
218
+ if (isLoaderSettledMarker(recorded)) {
219
+ const pinned = recorded.value;
220
+ // Deep holes inside a settled container (capture-computed `holes` bit)
221
+ // need the fresh promise's resolved value to fill them; a fully-pinned
222
+ // container resolves immediately (the prelude already shows it — never
223
+ // gate it on fresh latency). A rejecting fresh run degrades holes to
224
+ // undefined instead of poisoning the pin.
225
+ if (recorded.holes === 1) {
226
+ return Promise.resolve(fresh).then(
227
+ (freshValue) => overlayLoaderContainer(freshValue, pinned),
228
+ () => overlayLoaderContainer(undefined, pinned),
229
+ );
230
+ }
231
+ return Promise.resolve(overlayLoaderContainer(undefined, pinned));
232
+ }
233
+
234
+ if (Array.isArray(recorded)) {
235
+ const freshArr = Array.isArray(fresh) ? fresh : [];
236
+ return recorded.map((item, i) => overlayLoaderContainer(freshArr[i], item));
237
+ }
238
+
239
+ if (isPlainObject(recorded)) {
240
+ if (!isPlainObject(fresh)) {
241
+ // Shape drift: still honor markers (holes fall back to undefined).
242
+ const out: Record<string, unknown> = {};
243
+ for (const key of Object.keys(recorded)) {
244
+ out[key] = overlayLoaderContainer(undefined, recorded[key]);
245
+ }
246
+ return out;
247
+ }
248
+ const out: Record<string, unknown> = {};
249
+ for (const key of Object.keys(recorded)) {
250
+ out[key] = overlayLoaderContainer(fresh[key], recorded[key]);
251
+ }
252
+ for (const key of Object.keys(fresh)) {
253
+ if (!(key in out)) out[key] = fresh[key];
254
+ }
255
+ return out;
256
+ }
257
+
258
+ return recorded;
259
+ }