@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
@@ -21,7 +21,11 @@ import { getRequestContext } from "../server/request-context.js";
21
21
  import { executeInterceptMiddleware } from "./middleware.js";
22
22
  import { createReverseFunction } from "./handler-context.js";
23
23
  import { getGlobalRouteMap } from "../route-map-builder.js";
24
- import { handleHandlerResult } from "./segment-resolution.js";
24
+ import {
25
+ handleHandlerResult,
26
+ warnOnStreamedResponse,
27
+ buildLoaderErrorContext,
28
+ } from "./segment-resolution.js";
25
29
  import type { SegmentResolutionDeps } from "./types.js";
26
30
  import { debugLog } from "./logging.js";
27
31
  import { runInsideLoaderScope } from "../server/context.js";
@@ -109,10 +113,25 @@ export async function resolveInterceptEntry<TEnv>(
109
113
  };
110
114
  stale?: boolean;
111
115
  },
116
+ options?: {
117
+ /**
118
+ * Skip the intercept's middleware execution. Set ONLY by the post-response
119
+ * background re-render paths (proactive caching, stale background
120
+ * revalidation), whose sole purpose is to re-render the segment tree to
121
+ * populate the cache. The foreground request already ran the intercept
122
+ * middleware before the response was sent — it validated auth, set cookies,
123
+ * and wrote context vars into the request context's shared `_variables`,
124
+ * which the background render reuses. Re-running middleware here would fire
125
+ * its side effects a SECOND time, and a middleware that short-circuits with
126
+ * a Response would `throw` and silently abort the cache write. Never set on
127
+ * the foreground path.
128
+ */
129
+ skipMiddleware?: boolean;
130
+ },
112
131
  ): Promise<ResolvedSegment[]> {
113
132
  const segments: ResolvedSegment[] = [];
114
133
 
115
- if (interceptEntry.middleware.length > 0) {
134
+ if (!options?.skipMiddleware && interceptEntry.middleware.length > 0) {
116
135
  const requestCtx = getRequestContext();
117
136
  if (!requestCtx?.res) {
118
137
  throw new Error(
@@ -202,6 +221,9 @@ export async function resolveInterceptEntry<TEnv>(
202
221
  parentEntry,
203
222
  segmentId,
204
223
  context.pathname,
224
+ // Report a throwing intercept loader to onError + loader.error telemetry,
225
+ // matching the fresh/revalidation paths.
226
+ buildLoaderErrorContext(context),
205
227
  ),
206
228
  );
207
229
  }
@@ -228,6 +250,12 @@ export async function resolveInterceptEntry<TEnv>(
228
250
  let loaderDataPromise: Promise<any[]> | any[] | undefined;
229
251
 
230
252
  if (interceptEntry.loading && loaderPromises.length > 0) {
253
+ if (handlerResult instanceof Promise) {
254
+ warnOnStreamedResponse(
255
+ handlerResult,
256
+ `intercept ${interceptEntry.slotName}`,
257
+ );
258
+ }
231
259
  component =
232
260
  handlerResult instanceof Promise
233
261
  ? handlerResult
@@ -369,6 +397,11 @@ export async function resolveInterceptLoadersOnly<TEnv>(
369
397
  parentEntry,
370
398
  segmentId,
371
399
  context.pathname,
400
+ // Report a throwing intercept loader to onError + loader.error telemetry,
401
+ // matching the fresh/revalidation paths. resolveInterceptLoadersOnly is
402
+ // only called on the cache-hit partial-update path (handleCacheHitIntercept),
403
+ // so flag isPartial:true exactly like revalidation.ts's partial path.
404
+ { ...buildLoaderErrorContext(context), isPartial: true },
372
405
  ),
373
406
  );
374
407
  }
@@ -1,5 +1,5 @@
1
1
  import { registerRouteMap } from "../route-map-builder.js";
2
- import { extractStaticPrefix } from "./pattern-matching.js";
2
+ import { extractStaticPrefix, joinPrefix } from "./pattern-matching.js";
3
3
  import {
4
4
  type EntryData,
5
5
  RangoContext,
@@ -9,6 +9,10 @@ import {
9
9
  import type { UrlPatterns } from "../urls.js";
10
10
  import type { AllUseItems, IncludeItem } from "../route-types.js";
11
11
  import type { ResolvedRouteMap, RouteEntry, TrailingSlashMode } from "../types";
12
+ import {
13
+ isIncludeProvider,
14
+ resolveIncludeModule,
15
+ } from "../urls/include-provider.js";
12
16
 
13
17
  export interface LazyEvalDeps<TEnv = any> {
14
18
  routesEntries: RouteEntry<TEnv>[];
@@ -18,9 +22,6 @@ export interface LazyEvalDeps<TEnv = any> {
18
22
  routerId?: string;
19
23
  }
20
24
 
21
- // Detect lazy includes in handler result and create placeholder entries
22
- // Lazy includes are IncludeItem with lazy: true and _lazyContext
23
- // Moved to outer scope so it can be reused by evaluateLazyEntry for nested includes
24
25
  export function findLazyIncludes<TEnv = any>(
25
26
  items: AllUseItems[],
26
27
  ): Array<{
@@ -56,7 +57,6 @@ export function findLazyIncludes<TEnv = any>(
56
57
  });
57
58
  }
58
59
  }
59
- // Recursively check nested items (in layouts, etc.)
60
60
  if ((item as any).uses && Array.isArray((item as any).uses)) {
61
61
  lazyItems.push(...findLazyIncludes((item as any).uses));
62
62
  }
@@ -73,19 +73,11 @@ export function findLazyIncludes<TEnv = any>(
73
73
  export function evaluateLazyEntry<TEnv = any>(
74
74
  entry: RouteEntry<TEnv>,
75
75
  deps: LazyEvalDeps<TEnv>,
76
- ): void {
76
+ ): void | Promise<void> {
77
77
  if (!entry.lazy || entry.lazyEvaluated || !entry.lazyPatterns) {
78
78
  return;
79
79
  }
80
80
 
81
- // Check for pre-computed routes from build-time data.
82
- // Only leaf nodes (no nested includes) are precomputed, so entries with
83
- // nested lazy includes fall through to the handler below.
84
- // When multiple entries share the same staticPrefix (e.g., several
85
- // include("/", ...) calls), the precomputed data merges all their routes
86
- // into one entry. Assigning that merged set to the first matching entry
87
- // causes findMatch to pick the wrong handler for routes belonging to a
88
- // different include. Skip the shortcut when the prefix is shared.
89
81
  const currentPrecomputed = deps.getPrecomputedByPrefix();
90
82
  if (currentPrecomputed) {
91
83
  const routes = currentPrecomputed.get(entry.staticPrefix);
@@ -99,37 +91,74 @@ export function evaluateLazyEntry<TEnv = any>(
99
91
  for (const [name, pattern] of Object.entries(routes)) {
100
92
  deps.mergedRouteMap[name] = pattern;
101
93
  }
102
- registerRouteMap(deps.mergedRouteMap);
94
+ // Register only this entry's routes (the delta): the full
95
+ // mergedRouteMap is seeded from the generated manifest at
96
+ // createRouter() time and already registered there — re-passing it
97
+ // made this request-path call O(total routes) (issue #666).
98
+ registerRouteMap(routes);
103
99
  return;
104
100
  }
105
101
  }
106
102
  }
107
103
 
108
- // Mark as evaluated immediately to prevent concurrent evaluation.
109
- // JS is single-threaded but handlers.handler() could theoretically yield,
110
- // and the while-loop in findMatch retries after evaluation.
111
- entry.lazyEvaluated = true;
104
+ // Async provider (`() => import("./routes")`): the route module is evaluated
105
+ // off the startup path, on the first request reaching this prefix. Concurrent
106
+ // first-hits share one in-flight promise so the import + expansion run exactly
107
+ // once. The eager path below stays fully synchronous (no Promise), so the
108
+ // per-entry match loop pays no microtask for normal includes.
109
+ const lazyPatterns = entry.lazyPatterns;
110
+ if (isIncludeProvider(lazyPatterns)) {
111
+ const inflight = (entry as { _lazyInflight?: Promise<void> })._lazyInflight;
112
+ if (inflight) return inflight;
113
+ const work = (async () => {
114
+ const resolved = resolveIncludeModule(
115
+ await lazyPatterns(),
116
+ entry.staticPrefix,
117
+ );
118
+ // Cache the resolved patterns: any later re-entry expands synchronously.
119
+ entry.lazyPatterns = resolved as unknown as UrlPatterns<TEnv>;
120
+ runExpansion(entry, deps, resolved as UrlPatterns<TEnv>);
121
+ })();
122
+ (entry as { _lazyInflight?: Promise<void> })._lazyInflight = work;
123
+ const clear = () => {
124
+ (entry as { _lazyInflight?: Promise<void> })._lazyInflight = undefined;
125
+ };
126
+ // On failure, clear the flag (lazyEvaluated stays false) so a later request
127
+ // can retry the import rather than wedging the route permanently.
128
+ work.then(clear, clear);
129
+ return work;
130
+ }
131
+
132
+ runExpansion(entry, deps, lazyPatterns as UrlPatterns<TEnv>);
133
+ }
112
134
 
113
- const lazyPatterns = entry.lazyPatterns as UrlPatterns<TEnv>;
135
+ /**
136
+ * Synchronously expand a lazy entry's (already-resolved) patterns into routes
137
+ * and splice any nested lazy includes as new entries. Runs once per entry.
138
+ */
139
+ function runExpansion<TEnv = any>(
140
+ entry: RouteEntry<TEnv>,
141
+ deps: LazyEvalDeps<TEnv>,
142
+ lazyPatterns: UrlPatterns<TEnv>,
143
+ ): void {
144
+ // lazyEvaluated is set at the END, only after the handler ran and the routes
145
+ // (and any nested includes) were spliced. Setting it up-front would mark the
146
+ // entry done even if the handler throws mid-expansion: the async provider
147
+ // path clears _lazyInflight on rejection so a later request can retry, but a
148
+ // premature lazyEvaluated=true would make evaluateLazyEntry short-circuit
149
+ // (line ~77) forever, wedging the route at 404 until the isolate restarts.
114
150
  const lazyContext = entry.lazyContext;
115
151
 
116
- // Create a new context for evaluating the lazy patterns
117
152
  const manifest = new Map<string, EntryData>();
118
153
  const patterns = new Map<string, string>();
119
154
  const patternsByPrefix = new Map<string, Map<string, string>>();
120
155
  const trailingSlashMap = new Map<string, TrailingSlashMode>();
121
156
 
122
- // Capture the handler result to detect nested lazy includes
123
157
  let handlerResult: AllUseItems[] = [];
124
158
 
125
- // Merge captured counters from include() to maintain consistent
126
- // shortCode indices with sibling entries from pattern extraction
127
- const lazyCounters: Record<string, number> = {};
128
- if (lazyContext?.counters) {
129
- for (const [key, value] of Object.entries(lazyContext.counters)) {
130
- lazyCounters[key] = value;
131
- }
132
- }
159
+ const lazyCounters: Record<string, number> = lazyContext?.counters
160
+ ? { ...lazyContext.counters }
161
+ : {};
133
162
 
134
163
  RangoContext.run(
135
164
  {
@@ -145,10 +174,8 @@ export function evaluateLazyEntry<TEnv = any>(
145
174
  includeScope: lazyContext?.includeScope,
146
175
  },
147
176
  () => {
148
- // Run the lazy patterns handler with the original context prefixes
149
- // The prefix comes from the IncludeItem stored in lazyPatterns
150
177
  const includePrefix = (entry as any)._lazyPrefix || "";
151
- const fullPrefix = (lazyContext?.urlPrefix || "") + includePrefix;
178
+ const fullPrefix = joinPrefix(lazyContext?.urlPrefix, includePrefix);
152
179
 
153
180
  if (fullPrefix || lazyContext?.namePrefix) {
154
181
  runWithPrefixes(fullPrefix, lazyContext?.namePrefix, () => {
@@ -160,11 +187,9 @@ export function evaluateLazyEntry<TEnv = any>(
160
187
  },
161
188
  );
162
189
 
163
- // Populate the entry's routes from the patterns
164
190
  const routesObject: Record<string, string> = {};
165
191
  for (const [name, pattern] of patterns.entries()) {
166
192
  routesObject[name] = pattern;
167
- // Also add to merged route map for reverse() support
168
193
  const existingPattern = deps.mergedRouteMap[name];
169
194
  if (existingPattern !== undefined && existingPattern !== pattern) {
170
195
  console.warn(
@@ -175,46 +200,38 @@ export function evaluateLazyEntry<TEnv = any>(
175
200
  deps.mergedRouteMap[name] = pattern;
176
201
  }
177
202
 
178
- // Update the entry in-place
179
203
  entry.routes = routesObject as ResolvedRouteMap<any>;
180
204
 
181
- // Note: Do NOT clear lazyPatterns/lazyContext here.
182
- // loadManifest() needs them on every request to re-run the handler
183
- // in the correct AsyncLocalStorage context (Store.manifest).
184
-
185
- // Update trailing slash config if available
186
205
  if (trailingSlashMap.size > 0) {
187
206
  entry.trailingSlash = Object.fromEntries(trailingSlashMap);
188
207
  }
189
208
 
190
- // Detect nested lazy includes and register them as new entries
191
209
  const nestedLazyIncludes = findLazyIncludes(handlerResult);
192
210
  for (const lazyInclude of nestedLazyIncludes) {
193
- // Compute the full URL prefix (combining parent prefix if any)
194
- const fullPrefix = lazyInclude.context.urlPrefix
195
- ? lazyInclude.context.urlPrefix + lazyInclude.prefix
196
- : lazyInclude.prefix;
211
+ const fullPrefix = joinPrefix(
212
+ lazyInclude.context.urlPrefix,
213
+ lazyInclude.prefix,
214
+ );
197
215
 
198
216
  const nestedEntry: RouteEntry<TEnv> & { _lazyPrefix?: string } = {
199
217
  prefix: "",
200
218
  staticPrefix: extractStaticPrefix(fullPrefix),
201
- routes: {} as ResolvedRouteMap<any>, // Empty until first match
219
+ routes: {} as ResolvedRouteMap<any>,
202
220
  trailingSlash: entry.trailingSlash,
203
- handler: (lazyInclude.patterns as UrlPatterns<TEnv>).handler,
221
+ // include entries don't invoke their own handler (real handlers come from
222
+ // the expanded routes); use the parent's placeholder. A provider has no
223
+ // `.handler` until resolved, so never read it here.
224
+ handler: isIncludeProvider(lazyInclude.patterns)
225
+ ? entry.handler
226
+ : (lazyInclude.patterns as UrlPatterns<TEnv>).handler,
204
227
  mountIndex: deps.nextMountIndex(),
205
228
  routerId: deps.routerId,
206
- // Lazy evaluation fields
207
229
  lazy: true,
208
230
  lazyPatterns: lazyInclude.patterns,
209
231
  lazyContext: lazyInclude.context,
210
232
  lazyEvaluated: false,
211
- // Store the include prefix for evaluation
212
233
  _lazyPrefix: lazyInclude.prefix,
213
234
  };
214
- // Insert nested lazy entry before any entry whose staticPrefix is a
215
- // prefix of (but shorter than) this lazy entry's staticPrefix.
216
- // This ensures more specific lazy includes are matched before
217
- // less specific eager entries (e.g., "/href/nested" before "/href/:id").
218
235
  const nestedPrefix = nestedEntry.staticPrefix;
219
236
  let insertIndex = deps.routesEntries.length;
220
237
  if (nestedPrefix) {
@@ -232,6 +249,12 @@ export function evaluateLazyEntry<TEnv = any>(
232
249
  deps.routesEntries.splice(insertIndex, 0, nestedEntry);
233
250
  }
234
251
 
235
- // Re-register route map for runtime reverse() usage
236
- registerRouteMap(deps.mergedRouteMap);
252
+ // Delta only see the matching comment on the precomputed branch above and
253
+ // the WHY block on registerRouteMap (issue #666).
254
+ registerRouteMap(routesObject);
255
+
256
+ // Expansion fully succeeded (handler ran, routes + nested includes spliced) —
257
+ // mark done now so a mid-expansion throw above leaves lazyEvaluated=false and
258
+ // the entry retriable.
259
+ entry.lazyEvaluated = true;
237
260
  }
@@ -5,8 +5,8 @@
5
5
  */
6
6
 
7
7
  import type { ReactNode } from "react";
8
- import { track } from "../server/context";
9
8
  import type { EntryData } from "../server/context";
9
+ import { observePhase, PHASES } from "./instrument.js";
10
10
  import { contextGet } from "../context-var.js";
11
11
  import type {
12
12
  ResolvedSegment,
@@ -19,14 +19,16 @@ import type {
19
19
  ErrorBoundaryFallbackProps,
20
20
  ErrorInfo,
21
21
  } from "../types";
22
- import type { LoaderRevalidationResult, ActionContext } from "./types";
23
22
  import { isHandle, collectHandleData, type Handle } from "../handle.js";
23
+ import { withDefer } from "../defer.js";
24
24
  import { buildHandleSnapshot } from "../server/handle-store.js";
25
25
  import { getFetchableLoader } from "../server/fetchable-loader-store.js";
26
26
  import { _getRequestContext } from "../server/request-context.js";
27
27
  import {
28
28
  isInsideLoaderScope,
29
29
  runInsideLoaderBodyScope,
30
+ isInsidePushCallbackScope,
31
+ runInsidePushCallbackScope,
30
32
  } from "../server/context.js";
31
33
  import { debugLog } from "./logging.js";
32
34
 
@@ -69,7 +71,9 @@ export function wrapLoaderWithErrorHandling<T>(
69
71
  ) => ErrorInfo,
70
72
  onError?: LoaderErrorCallback,
71
73
  ): Promise<LoaderDataResult<T>> {
72
- // Extract loader name from segmentId (format: "M1L0D0.loaderName")
74
+ // Extract the trailing token from segmentId (format: "<shortCode>D<i>.<loaderId>").
75
+ // The token is the loader's $$id (hash#export in prod, pathfrag#export in dev),
76
+ // not a clean display name.
73
77
  const loaderName = segmentId.split(".").pop() || "unknown";
74
78
 
75
79
  return Promise.resolve(promise)
@@ -105,16 +109,40 @@ export function wrapLoaderWithErrorHandling<T>(
105
109
  };
106
110
  }
107
111
 
108
- // Render fallback on server
112
+ // Render fallback on server. The user ErrorBoundaryHandler may throw
113
+ // synchronously; if it does we must NOT let that rejection escape — the
114
+ // wrapped LoaderDataResult promise is contracted to never reject (see
115
+ // segment-resolution/fresh.ts `await Promise.all(...wrapped)`), and a
116
+ // rejection here would collapse the whole entry and discard healthy
117
+ // sibling loader data. On a fallback-render throw, fall back to the
118
+ // no-boundary result (fallback: null) so the client throws the ORIGINAL
119
+ // error, and the wrapped promise still resolves to a LoaderDataResult.
109
120
  let renderedFallback: ReactNode;
110
- if (typeof fallback === "function") {
111
- // ErrorBoundaryHandler - call with error info
112
- const props: ErrorBoundaryFallbackProps = {
121
+ try {
122
+ if (typeof fallback === "function") {
123
+ // ErrorBoundaryHandler - call with error info
124
+ const props: ErrorBoundaryFallbackProps = {
125
+ error: errorInfo,
126
+ };
127
+ renderedFallback = fallback(props);
128
+ } else {
129
+ renderedFallback = fallback;
130
+ }
131
+ } catch (fallbackError) {
132
+ debugLog("loader", "error boundary fallback render threw", {
133
+ segmentId,
134
+ message: errorInfo.message,
135
+ fallbackError:
136
+ fallbackError instanceof Error
137
+ ? fallbackError.message
138
+ : String(fallbackError),
139
+ });
140
+ return {
141
+ __loaderResult: true,
142
+ ok: false,
113
143
  error: errorInfo,
144
+ fallback: null,
114
145
  };
115
- renderedFallback = fallback(props);
116
- } else {
117
- renderedFallback = fallback;
118
146
  }
119
147
 
120
148
  debugLog("loader", "loader error wrapped with boundary fallback", {
@@ -290,6 +318,12 @@ function createLoaderExecutor<TEnv>(
290
318
  );
291
319
  }
292
320
  const segmentOrder = reqCtx._renderBarrierSegmentOrder ?? [];
321
+ // The complete snapshot is cached at barrier resolution for
322
+ // non-streaming trees, and by rendered() after handleStore.settled for
323
+ // streaming trees (where the eager snapshot would have been incomplete
324
+ // because loading() handlers were still in flight). Either way it is
325
+ // present by the time a loader reads a handle; the fresh build is only
326
+ // a defensive fallback.
293
327
  const snapshot =
294
328
  reqCtx._renderBarrierHandleSnapshot ??
295
329
  buildHandleSnapshot(reqCtx._handleStore, segmentOrder);
@@ -311,15 +345,7 @@ function createLoaderExecutor<TEnv>(
311
345
  );
312
346
  }
313
347
 
314
- // Guard: reject streaming trees
315
348
  const reqCtx = reqCtxRef ?? _getRequestContext();
316
- if (reqCtx?._treeHasStreaming) {
317
- throw new Error(
318
- `ctx.rendered() is not supported when the matched route tree uses loading(). ` +
319
- `Streaming handlers may not have settled when rendered() resolves. ` +
320
- `Remove loading() from the route tree or restructure to avoid rendered().`,
321
- );
322
- }
323
349
 
324
350
  if (renderedPromise) return renderedPromise;
325
351
 
@@ -330,7 +356,10 @@ function createLoaderExecutor<TEnv>(
330
356
  }
331
357
 
332
358
  // Bidirectional deadlock check: if a handler already started
333
- // awaiting this loader, calling rendered() would deadlock.
359
+ // awaiting this loader, calling rendered() would deadlock. This is the
360
+ // real cycle guard (it holds for both streaming and non-streaming): the
361
+ // handler blocks segment resolution, which blocks the barrier, which
362
+ // blocks this loader.
334
363
  if (reqCtx._handlerLoaderDeps?.has(currentLoaderId)) {
335
364
  throw new Error(
336
365
  `Deadlock: loader "${currentLoaderId}" called ctx.rendered() but a handler ` +
@@ -348,14 +377,40 @@ function createLoaderExecutor<TEnv>(
348
377
  }
349
378
  reqCtx._renderBarrierWaiters.add(currentLoaderId);
350
379
 
351
- renderedPromise = reqCtx._renderBarrier.then(() => {
380
+ // Streaming trees (loading()): the barrier resolves once the segment
381
+ // tree is resolved, but loading() handlers stream behind Suspense and
382
+ // their handle pushes are still in flight then. Their async execution
383
+ // IS tracked in the handle store (trackHandler -> store.track), so after
384
+ // the barrier we seal (no further handlers register once the tree is
385
+ // resolved) and wait for settled — every tracked handler, streaming
386
+ // included, has finished pushing. The loader's own segment streams in
387
+ // after, so this does not block the shell; the deadlock guard above
388
+ // keeps a handler from depending on this loader.
389
+ const streaming = reqCtx._treeHasStreaming === true;
390
+ renderedPromise = reqCtx._renderBarrier.then(async () => {
391
+ if (streaming) {
392
+ reqCtx._handleStore.seal();
393
+ await reqCtx._handleStore.settled;
394
+ // The eager snapshot was intentionally left unbuilt for streaming
395
+ // (it would have been incomplete). Build the complete one once, now
396
+ // that the store has settled, so every ctx.use(handle) reads the
397
+ // cached snapshot instead of rebuilding it per call.
398
+ reqCtx._renderBarrierHandleSnapshot ??= buildHandleSnapshot(
399
+ reqCtx._handleStore,
400
+ reqCtx._renderBarrierSegmentOrder ?? [],
401
+ );
402
+ }
352
403
  renderedResolved = true;
353
404
  });
354
405
  return renderedPromise;
355
406
  },
356
407
  };
357
408
 
358
- const doneLoader = track(`loader:${loader.$$id}`, 2);
409
+ // Meter this loader once via observePhase (loader:<id> perf metric +
410
+ // rango.loader span); loaderFn runs inside the span callback so its KV/D1/
411
+ // fetch spans nest under it. This is one of the observePhase loader funnels —
412
+ // see instrument.ts for the single-metering contract.
413
+ //
359
414
  // Run the loader body inside loader scope so request-scoped reads
360
415
  // (cookies()/headers() and non-cacheable ctx.get) are exempt from the
361
416
  // cache-purity guards: loaders always run fresh, so their reads never leak
@@ -365,14 +420,27 @@ function createLoaderExecutor<TEnv>(
365
420
  // throw. rendered() gating uses the captured isDslLoader (above), so this
366
421
  // does not grant rendered() to handler-invoked loaders. Uses a body-only
367
422
  // scope, so isInsideLoaderScope() / barrier / deadlock gating is unchanged.
368
- const promise = Promise.resolve(
369
- runInsideLoaderBodyScope(() =>
370
- loaderFn(loaderCtx as LoaderContext<any, TEnv>),
371
- ),
372
- ).finally(() => {
373
- pendingLoaders.delete(loader.$$id);
374
- doneLoader();
375
- });
423
+ //
424
+ // `handlerInvoked` (!isDslLoader) rides on the scope for the CONSUMPTION-
425
+ // LANE RULE: a handler-consumed loader's value is a BAKED copy in every
426
+ // shared artifact (cache(), "use cache", the PPR shell), so its identity
427
+ // reads are exempt from the shell-capture guard — same allowance the
428
+ // cache-purity guards give it. DSL segment loaders keep their lane
429
+ // machinery (live = masked at capture, bake = guarded). A DSL loader's
430
+ // nested deps inherit isDslLoader=false only when the CHAIN started in a
431
+ // handler; a chain started by the segment funnel stays DSL (the loader
432
+ // scope ALS survives the body's awaits).
433
+ const promise = observePhase(PHASES.loader(loader.$$id), () =>
434
+ Promise.resolve(
435
+ runInsideLoaderBodyScope(
436
+ () => loaderFn(loaderCtx as LoaderContext<any, TEnv>),
437
+ loader.$$id,
438
+ !isDslLoader,
439
+ ),
440
+ ).finally(() => {
441
+ pendingLoaders.delete(loader.$$id);
442
+ }),
443
+ );
376
444
 
377
445
  loaderPromises.set(loader.$$id, promise);
378
446
  return promise;
@@ -404,12 +472,6 @@ export function setupLoaderAccess<TEnv>(
404
472
 
405
473
  const useLoader = createLoaderExecutor(ctx, loaderPromises);
406
474
 
407
- // Track whether we're inside a handle push callback. Loaders started
408
- // from push callbacks (e.g. push(async () => ctx.use(Loader))) do NOT
409
- // block segment resolution, so they must not be registered as handler
410
- // dependencies for deadlock detection.
411
- let insideHandlePush = false;
412
-
413
475
  ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
414
476
  if (isHandle(item)) {
415
477
  const handle = item;
@@ -424,35 +486,40 @@ export function setupLoaderAccess<TEnv>(
424
486
  );
425
487
  }
426
488
 
427
- return (
428
- dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
429
- ) => {
430
- if (!store) return;
431
-
432
- if (typeof dataOrFn === "function") {
433
- // Mark scope so ctx.use(loader) calls inside the callback
434
- // are not registered as handler-to-loader deps.
435
- insideHandlePush = true;
436
- try {
437
- const result = (dataOrFn as () => Promise<unknown>)();
489
+ return withDefer(
490
+ (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
491
+ if (!store) return;
492
+
493
+ if (typeof dataOrFn === "function") {
494
+ // Run the callback inside the push-callback scope so ctx.use(loader)
495
+ // calls it makes including after its own awaits, for an async
496
+ // callback — are not registered as handler-to-loader deps and do not
497
+ // trip the deadlock guard. A pushed promise value is not tracked by
498
+ // handleStore.settled and does not block segment resolution, so it
499
+ // cannot form a rendered() deadlock. The ALS scope (not a plain
500
+ // boolean) is what survives the callback's awaits.
501
+ const result = runInsidePushCallbackScope(() =>
502
+ (dataOrFn as () => Promise<unknown>)(),
503
+ );
438
504
  store.push(handle.$$id, segmentId, result);
439
- } finally {
440
- insideHandlePush = false;
505
+ return;
441
506
  }
442
- return;
443
- }
444
507
 
445
- store.push(handle.$$id, segmentId, dataOrFn);
446
- };
508
+ store.push(handle.$$id, segmentId, dataOrFn);
509
+ },
510
+ );
447
511
  }
448
512
 
449
513
  // Deadlock guard and handler-to-loader dependency tracking.
450
514
  // Skip when inside a DSL loader scope (resolveLoaderData also calls
451
515
  // ctx.use() but that's DSL-to-DSL, not handler-to-loader) or when
452
516
  // inside a handle push callback (push callbacks don't block segment
453
- // resolution so they can't cause rendered() deadlocks).
517
+ // resolution so they can't cause rendered() deadlocks). The push-callback
518
+ // check is an ALS scope so it also exempts an ASYNC callback's continuation
519
+ // after its first await — relevant on streaming trees, where the guard
520
+ // state now stays live until handleStore.settled.
454
521
  const loader = item as LoaderDefinition<any, any>;
455
- if (!isInsideLoaderScope() && !insideHandlePush) {
522
+ if (!isInsideLoaderScope() && !isInsidePushCallbackScope()) {
456
523
  const reqCtx = reqCtxRef ?? _getRequestContext();
457
524
  if (reqCtx) {
458
525
  // Direction 1: handler awaits loader that already called rendered()
@@ -466,13 +533,18 @@ export function setupLoaderAccess<TEnv>(
466
533
  `Move the data dependency to a loader-to-loader pattern instead.`,
467
534
  );
468
535
  }
469
- // Direction 2: track dep so rendered() can detect the deadlock
470
- // if the loader calls it later. Skip when the barrier has already
471
- // resolved no deadlock is possible (rendered() resolves immediately).
472
- // _renderBarrierSegmentOrder is undefined before resolution, string[]
473
- // after. This also prevents false positives from handle push callbacks
474
- // that resume after their first await (post-barrier-resolution).
475
- if (reqCtx._renderBarrierSegmentOrder === undefined) {
536
+ // Direction 2: track dep so rendered() can detect the deadlock if the
537
+ // loader calls it later. Skip once the guard window is CLOSED — for a
538
+ // non-streaming tree that is when the barrier resolves (rendered()
539
+ // resolves immediately), and for a streaming tree it is when
540
+ // handleStore.settled completes (rendered() keeps waiting until then, so
541
+ // a loading() handler resuming after the barrier can still form a
542
+ // cycle). Using the explicit guard-closed flag rather than
543
+ // _renderBarrierSegmentOrder keeps tracking live across the streaming
544
+ // settle wait. (Handle push callbacks are already excluded above via
545
+ // isInsidePushCallbackScope(), so they cannot produce false positives
546
+ // here.)
547
+ if (!reqCtx._renderBarrierGuardClosed) {
476
548
  if (!reqCtx._handlerLoaderDeps) reqCtx._handlerLoaderDeps = new Set();
477
549
  reqCtx._handlerLoaderDeps.add(loader.$$id);
478
550
  }
@@ -506,18 +578,21 @@ export function setupBuildUse<TEnv>(ctx: HandlerContext<any, TEnv>): void {
506
578
  );
507
579
  }
508
580
 
509
- return (
510
- dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
511
- ) => {
512
- if (!store) return;
581
+ // Wrap with withDefer so ctx.use(Handle).defer(...) works on the build /
582
+ // prerender path, matching production setupLoaderAccess. Without it a
583
+ // prerender handler calling .defer() throws "defer is not a function".
584
+ return withDefer(
585
+ (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
586
+ if (!store) return;
513
587
 
514
- const valueOrPromise =
515
- typeof dataOrFn === "function"
516
- ? (dataOrFn as () => Promise<unknown>)()
517
- : dataOrFn;
588
+ const valueOrPromise =
589
+ typeof dataOrFn === "function"
590
+ ? (dataOrFn as () => Promise<unknown>)()
591
+ : dataOrFn;
518
592
 
519
- store.push(handle.$$id, segmentId, valueOrPromise);
520
- };
593
+ store.push(handle.$$id, segmentId, valueOrPromise);
594
+ },
595
+ );
521
596
  }
522
597
 
523
598
  // Loader case: not available during pre-rendering
@@ -545,8 +620,11 @@ export function setupLoaderAccessSilent<TEnv>(
545
620
 
546
621
  ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
547
622
  if (isHandle(item)) {
548
- // Silent mode - return a no-op so handle data is not pushed during caching
549
- return (_dataOrFn: unknown) => {};
623
+ // Silent mode - return a no-op so handle data is not pushed during caching.
624
+ // Wrap with withDefer so ctx.use(Handle).defer(...) still resolves to a
625
+ // callable resolver (also a no-op here), matching production's push shape
626
+ // instead of throwing "defer is not a function".
627
+ return withDefer((_dataOrFn: unknown) => {});
550
628
  }
551
629
 
552
630
  return useLoader(item as LoaderDefinition<any, any>, null);