@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
@@ -3,12 +3,16 @@
3
3
  *
4
4
  * Fetch-based prefetch logic used by Link (hover/viewport/render strategies)
5
5
  * and useRouter().prefetch(). Sends the same headers and segment IDs as a
6
- * real navigation so the server returns a proper diff. The Response is fully
7
- * buffered and stored in an in-memory cache for instant consumption on
8
- * subsequent navigation.
6
+ * real navigation so the server returns a proper diff. The response is fetched
7
+ * AND eagerly decoded (createFromFetch) up front: decoding the Flight stream
8
+ * resolves the route's client references, so the route's JS chunks are imported
9
+ * during prefetch rather than on click. The decoded payload is stored in an
10
+ * in-memory cache and reused verbatim by navigation, so a prefetched click
11
+ * loads no new code.
9
12
  *
10
13
  * In-flight promises are tracked in the cache so that navigation can reuse
11
- * a prefetch that is still downloading instead of starting a duplicate request.
14
+ * a prefetch that is still downloading/decoding instead of starting a
15
+ * duplicate request.
12
16
  */
13
17
 
14
18
  import {
@@ -18,13 +22,51 @@ import {
18
22
  markPrefetchInflight,
19
23
  setInflightPromiseWithAliases,
20
24
  storePrefetch,
25
+ removePrefetch,
21
26
  clearPrefetchInflight,
22
27
  currentGeneration,
28
+ type DecodedPrefetch,
23
29
  } from "./cache.js";
24
30
  import { getRangoState } from "../rango-state.js";
31
+ import { isActionFenceActive } from "../action-fence.js";
25
32
  import { enqueuePrefetch } from "./queue.js";
26
33
  import { shouldPrefetch } from "./policy.js";
27
- import { debugLog } from "../logging.js";
34
+ import { debugLog, IS_BROWSER_DEBUG } from "../logging.js";
35
+ import { teeWithCompletion, isForeignRouterId } from "../response-adapter.js";
36
+ import type { RscPayload } from "../types.js";
37
+
38
+ /**
39
+ * Decoder injected at app startup (see setPrefetchDecoder). This is
40
+ * `deps.createFromFetch` — decoupled from the RSC runtime exactly like the
41
+ * navigation client. Prefetch decodes through it so the route's client chunks
42
+ * are pulled during the prefetch, not on click.
43
+ */
44
+ type PrefetchDecoder = (response: Promise<Response>) => Promise<RscPayload>;
45
+
46
+ let decoder: PrefetchDecoder | null = null;
47
+
48
+ /**
49
+ * Hard ceiling for ANY prefetch fetch (hover/direct AND queue-driven). A server
50
+ * that stalls leaves the fetch pending forever — its `.finally()` never runs,
51
+ * `clearPrefetchInflight` never fires, and `hasPrefetch(key)` stays true,
52
+ * permanently deduping every future prefetch of that URL. The hover path passes
53
+ * no signal; the queue passes its own AbortController signal that only aborts on
54
+ * navigation, never on a stall — so the timeout is layered on BOTH (combined
55
+ * with any caller signal via AbortSignal.any) and aborts the stalled fetch so it
56
+ * settles (rejects) and the inflight key is always released. Generous so a
57
+ * slow-but-live response is never cut short.
58
+ */
59
+ const PREFETCH_FETCH_TIMEOUT_MS = 30_000;
60
+
61
+ /**
62
+ * Wire the RSC decoder used to eagerly decode prefetched responses. Called
63
+ * once from initBrowserApp with the same createFromFetch the navigation client
64
+ * uses. Until set, prefetch warming is inert (prefetches are skipped) — the
65
+ * browser app always sets it before any Link can fire.
66
+ */
67
+ export function setPrefetchDecoder(fn: PrefetchDecoder): void {
68
+ decoder = fn;
69
+ }
28
70
 
29
71
  /**
30
72
  * Check if a URL resolves to the current page (same pathname + search).
@@ -78,71 +120,199 @@ function buildPrefetchUrl(
78
120
  }
79
121
 
80
122
  /**
81
- * Core prefetch fetch logic. Fetches the response, tees the body, and stores
82
- * one branch in the in-memory cache. The returned Promise resolves to the
83
- * sibling navigation branch (or null on failure) so navigation can safely
84
- * reuse an in-flight prefetch via consumeInflightPrefetch().
123
+ * Core prefetch fetch logic. Fetches the response, eagerly decodes it, and
124
+ * stores the decoded payload in the in-memory cache. The returned Promise
125
+ * resolves to the decoded entry (or null on failure / control header) so
126
+ * navigation can safely reuse an in-flight prefetch via
127
+ * consumeInflightPrefetch().
128
+ *
129
+ * Eager decode is the warming step: createFromFetch parses the Flight stream,
130
+ * which resolves the route's client references and imports its JS chunks. The
131
+ * stored payload is reused as-is by navigation, so the click loads no new code.
132
+ *
133
+ * Control headers are NOT acted on here. A speculative prefetch must never
134
+ * reload the page or throw a redirect — if the response carries X-RSC-Reload
135
+ * or X-RSC-Redirect, we drop it (resolve null) and let the real navigation
136
+ * re-fetch and honor it.
85
137
  *
86
138
  * Inflight + storage key selection:
87
139
  *
88
140
  * - `forceSourceScope` (Link opted in with `prefetchKey=":source"`): single
89
- * inflight registration under `sourceKey`; response stored under
90
- * `sourceKey`. No wildcard leak is possible.
141
+ * inflight registration under `sourceKey`; entry stored under `sourceKey`.
142
+ * No wildcard leak is possible.
91
143
  *
92
144
  * - Otherwise: dual inflight registration under both `wildcardKey` and
93
145
  * `sourceKey` so same-source navigations adopt directly via their own
94
146
  * source key. Storage key is chosen at response time from the
95
147
  * `X-RSC-Prefetch-Scope` header — `"source"` → `sourceKey` (intercept
96
- * modals etc.), anything else → `wildcardKey`. Cross-source navigations
97
- * that adopted via `wildcardKey` must bail out in `navigation-client.ts`
98
- * if the adopted response turns out to be source-scoped.
148
+ * modals etc.), anything else → `wildcardKey`. The entry records its scope
149
+ * so cross-source navigations that adopted via `wildcardKey` can bail out
150
+ * in `navigation-client.ts` when the adopted entry turns out source-scoped.
99
151
  */
100
152
  function executePrefetchFetch(
101
153
  wildcardKey: string,
102
154
  sourceKey: string,
103
155
  fetchUrl: string,
104
156
  forceSourceScope: boolean,
157
+ /** Rango state captured once by the caller (keys + header share one read). */
158
+ rangoState: string,
159
+ expectedRouterId?: string,
105
160
  signal?: AbortSignal,
106
- ): Promise<Response | null> {
161
+ ): Promise<DecodedPrefetch | null> {
107
162
  const gen = currentGeneration();
108
163
  const inflightKeys = forceSourceScope
109
164
  ? [sourceKey]
110
165
  : [wildcardKey, sourceKey];
111
166
  for (const k of inflightKeys) markPrefetchInflight(k);
112
167
 
113
- const promise: Promise<Response | null> = fetch(fetchUrl, {
168
+ // Always layer a stall timeout. It covers BOTH "no response ever arrives"
169
+ // (strands the inflight key) AND "the body stalls after headers" (leaves a
170
+ // published entry whose payload/streamComplete never settle, that future
171
+ // prefetches dedupe against and navigation awaits forever). Applies to the
172
+ // hover/direct path (no caller signal) and the queue-driven path (whose caller
173
+ // signal only aborts on navigation, never on a stall). On fire it aborts the
174
+ // fetch/stream and evicts the published entry if one exists; it is NOT cleared
175
+ // when headers arrive (see below) — it is cleared when the stream completes, or
176
+ // in `.finally()` for paths that publish no streaming entry.
177
+ let publishedKey: string | undefined;
178
+ let publishedEntry: DecodedPrefetch | undefined;
179
+ const timeoutController = new AbortController();
180
+ const timeoutId: ReturnType<typeof setTimeout> = setTimeout(() => {
181
+ timeoutController.abort();
182
+ // Body stalled after headers: evict the published-but-never-settling entry
183
+ // so future prefetches/navigation refetch. Identity-guarded (pass the exact
184
+ // entry) so a fresh entry republished under the same key — after this one was
185
+ // consumed — is NOT dropped. The abort cancels the tee (its finally resolves
186
+ // streamComplete) and rejects the eager decode.
187
+ if (publishedKey !== undefined && publishedEntry !== undefined) {
188
+ removePrefetch(publishedKey, publishedEntry);
189
+ }
190
+ }, PREFETCH_FETCH_TIMEOUT_MS);
191
+ let effectiveSignal: AbortSignal;
192
+ if (!signal) {
193
+ effectiveSignal = timeoutController.signal;
194
+ } else if (typeof AbortSignal.any === "function") {
195
+ // Combine the caller's signal (navigation-abort) with the timeout so either
196
+ // can settle the fetch.
197
+ effectiveSignal = AbortSignal.any([signal, timeoutController.signal]);
198
+ } else {
199
+ // Legacy runtime without AbortSignal.any: forward the caller's abort onto the
200
+ // timeout controller so a single signal carries both reasons.
201
+ effectiveSignal = timeoutController.signal;
202
+ if (signal.aborted) timeoutController.abort();
203
+ else
204
+ signal.addEventListener("abort", () => timeoutController.abort(), {
205
+ once: true,
206
+ });
207
+ }
208
+
209
+ const promise: Promise<DecodedPrefetch | null> = fetch(fetchUrl, {
114
210
  priority: "low" as RequestPriority,
115
- signal,
211
+ // During an action's flight the state is not rotated, so the old
212
+ // X-Rango-State still matches the Vary-keyed HTTP-cache entry; bypass it so
213
+ // a prefetch fetches fresh rather than warming the map with stale bytes (the
214
+ // fence's HTTP-cache-bypass requirement applies to prefetch as well as
215
+ // navigation fetches).
216
+ ...(isActionFenceActive() && { cache: "no-store" as RequestCache }),
217
+ signal: effectiveSignal,
116
218
  headers: {
117
- "X-Rango-State": getRangoState(),
219
+ "X-Rango-State": rangoState,
118
220
  "X-RSC-Router-Client-Path": window.location.href,
119
221
  "X-Rango-Prefetch": "1",
120
222
  },
121
223
  })
122
224
  .then((response) => {
123
- if (!response.ok) return null;
124
- // Don't buffer with arrayBuffer() that blocks until the entire
125
- // body downloads, defeating streaming for slow loaders.
126
- // Tee the body: one branch for navigation, one for cache storage.
127
- const [navStream, cacheStream] = response.body!.tee();
128
- const responseInit = {
129
- headers: response.headers,
130
- status: response.status,
131
- statusText: response.statusText,
132
- };
133
- let storageKey: string;
134
- if (forceSourceScope) {
135
- storageKey = sourceKey;
136
- } else {
137
- const scope = response.headers.get("x-rsc-prefetch-scope");
138
- storageKey = scope === "source" ? sourceKey : wildcardKey;
225
+ if (!response.ok || !decoder) return null;
226
+ // Control headers mean this response is stale (reload) or redirecting.
227
+ // Don't warm it drop so navigation re-fetches and acts on the header.
228
+ if (
229
+ response.headers.has("X-RSC-Reload") ||
230
+ response.headers.has("X-RSC-Redirect")
231
+ ) {
232
+ return null;
139
233
  }
140
- storePrefetch(storageKey, new Response(cacheStream, responseInit), gen);
141
- return new Response(navStream, responseInit);
234
+ // Integrity check: never warm (or decode/import the chunks of) a foreign
235
+ // app's payload. A speculative prefetch must never reload — just drop it;
236
+ // navigation re-fetches and the server steers it.
237
+ if (isForeignRouterId(response, expectedRouterId)) {
238
+ return null;
239
+ }
240
+
241
+ const scope: "source" | "wildcard" =
242
+ forceSourceScope ||
243
+ response.headers.get("x-rsc-prefetch-scope") === "source"
244
+ ? "source"
245
+ : "wildcard";
246
+ const storageKey = scope === "source" ? sourceKey : wildcardKey;
247
+
248
+ // Track stream completion off a tee so navigation's scroll/revalidation
249
+ // gating matches the fresh-fetch path; decode the other branch. The
250
+ // completion callback reports whether the stream ended on a clean EOF
251
+ // (true) or was aborted/errored (false) — only a clean end can mark the
252
+ // entry complete (see below).
253
+ let resolveStreamComplete!: () => void;
254
+ let endedCleanly = false;
255
+ const streamComplete = new Promise<void>((resolve) => {
256
+ resolveStreamComplete = resolve;
257
+ });
258
+ const tracked = teeWithCompletion(
259
+ response,
260
+ (clean) => {
261
+ endedCleanly = clean;
262
+ resolveStreamComplete();
263
+ },
264
+ effectiveSignal,
265
+ // Speculative prefetch: a never-consumed/aborted stream error is benign.
266
+ true,
267
+ );
268
+
269
+ // Eager decode: parsing the Flight stream imports the route's client
270
+ // chunks now, not on click.
271
+ const payload = decoder(Promise.resolve(tracked));
272
+ // Mark handled so an unconsumed prefetch decode error stays quiet; the
273
+ // error is still surfaced to navigation if it consumes the entry.
274
+ payload.catch(() => {});
275
+
276
+ const entry: DecodedPrefetch = {
277
+ payload,
278
+ streamComplete,
279
+ scope,
280
+ complete: false,
281
+ };
282
+ storePrefetch(storageKey, entry, gen);
283
+ // The stall timeout now owns the body stream: arm eviction (publishedKey)
284
+ // and clear the timer once the stream completes. The tee's finally resolves
285
+ // streamComplete on normal completion AND on abort, so a healthy body pays
286
+ // no lingering timer while a stalled one is evicted when the timer fires.
287
+ publishedKey = storageKey;
288
+ publishedEntry = entry;
289
+ // Evict a broken prefetch IMMEDIATELY on the earliest failure signal — do not
290
+ // wait for both branches to settle. A decode that rejects while the tracking
291
+ // stream is still draining (or hung) would otherwise leave the rejected payload
292
+ // consumable (navigation reads entry.payload regardless of `complete`) until EOF
293
+ // or the stall timeout. removePrefetch is identity-guarded, so a fresh entry
294
+ // republished under the same key is never dropped, and a double call is a no-op.
295
+ payload.catch(() => removePrefetch(storageKey, entry));
296
+ streamComplete.then(() => {
297
+ if (!endedCleanly) removePrefetch(storageKey, entry);
298
+ });
299
+ // Mark complete ONLY on a fully-healthy prefetch (decode resolved AND clean EOF).
300
+ Promise.allSettled([payload, streamComplete]).then(([decode]) => {
301
+ if (decode.status === "fulfilled" && endedCleanly) {
302
+ entry.complete = true;
303
+ }
304
+ clearTimeout(timeoutId);
305
+ });
306
+ return entry;
142
307
  })
143
308
  .catch(() => null)
144
309
  .finally(() => {
145
310
  clearPrefetchInflight(inflightKeys[0]!);
311
+ // Clear the stall timer here ONLY for paths that published no streaming
312
+ // entry (null return / fetch error / abort): the operation is fully done.
313
+ // When an entry WAS published, the timer stays armed to bound the body
314
+ // stream and is cleared on streamComplete (above) or on fire (eviction).
315
+ if (publishedKey === undefined) clearTimeout(timeoutId);
146
316
  });
147
317
 
148
318
  setInflightPromiseWithAliases(inflightKeys, promise);
@@ -203,26 +373,32 @@ export function prefetchDirect(
203
373
  const wildcardKey = buildPrefetchKey(rangoState, targetUrl);
204
374
  const sourceKey = buildSourceKey(rangoState, sourceHref, targetUrl);
205
375
  if (hasPrefetchHit(forceSourceScope, wildcardKey, sourceKey)) {
206
- debugLog("[prefetch] direct dedup (key already exists)", {
376
+ if (IS_BROWSER_DEBUG) {
377
+ debugLog("[prefetch] direct dedup (key already exists)", {
378
+ url,
379
+ wildcardKey,
380
+ sourceKey,
381
+ forceSourceScope,
382
+ });
383
+ }
384
+ return;
385
+ }
386
+ if (IS_BROWSER_DEBUG) {
387
+ debugLog("[prefetch] direct fetch", {
207
388
  url,
208
389
  wildcardKey,
209
390
  sourceKey,
391
+ source: sourceHref,
210
392
  forceSourceScope,
211
393
  });
212
- return;
213
394
  }
214
- debugLog("[prefetch] direct fetch", {
215
- url,
216
- wildcardKey,
217
- sourceKey,
218
- source: sourceHref,
219
- forceSourceScope,
220
- });
221
395
  executePrefetchFetch(
222
396
  wildcardKey,
223
397
  sourceKey,
224
398
  targetUrl.toString(),
225
399
  forceSourceScope,
400
+ rangoState,
401
+ routerId,
226
402
  );
227
403
  }
228
404
 
@@ -252,12 +428,14 @@ export function prefetchQueued(
252
428
  const sourceKey = buildSourceKey(rangoState, sourceHref, targetUrl);
253
429
  const queueKey = forceSourceScope ? sourceKey : wildcardKey;
254
430
  if (hasPrefetchHit(forceSourceScope, wildcardKey, sourceKey)) {
255
- debugLog("[prefetch] queued dedup (key already exists)", {
256
- url,
257
- wildcardKey,
258
- sourceKey,
259
- forceSourceScope,
260
- });
431
+ if (IS_BROWSER_DEBUG) {
432
+ debugLog("[prefetch] queued dedup (key already exists)", {
433
+ url,
434
+ wildcardKey,
435
+ sourceKey,
436
+ forceSourceScope,
437
+ });
438
+ }
261
439
  return queueKey;
262
440
  }
263
441
  const fetchUrlStr = targetUrl.toString();
@@ -275,6 +453,8 @@ export function prefetchQueued(
275
453
  sourceKey,
276
454
  fetchUrlStr,
277
455
  forceSourceScope,
456
+ rangoState,
457
+ routerId,
278
458
  signal,
279
459
  ).then(() => {});
280
460
  });
@@ -16,9 +16,24 @@
16
16
 
17
17
  import { wait, waitForIdle, waitForViewportImages } from "./resource-ready.js";
18
18
 
19
- const MAX_CONCURRENT = 2;
19
+ // Max prefetches executing at once. Mirrors DEFAULT_PREFETCH_CONCURRENCY
20
+ // (router/prefetch-limits.ts); kept as a local literal so the client bundle
21
+ // doesn't pull in router-layer code. Overridden at startup by
22
+ // setPrefetchConcurrency from server metadata.
23
+ let maxConcurrent = 2;
20
24
  const IMAGE_WAIT_TIMEOUT = 2000;
21
25
 
26
+ /**
27
+ * Set the max number of concurrently-executing speculative prefetches.
28
+ * Called once at app startup with the value from server metadata. A value
29
+ * below 1 (or non-finite) is ignored, keeping the default.
30
+ */
31
+ export function setPrefetchConcurrency(n: number): void {
32
+ if (Number.isFinite(n) && n >= 1) {
33
+ maxConcurrent = Math.floor(n);
34
+ }
35
+ }
36
+
22
37
  let active = 0;
23
38
  const queue: Array<{
24
39
  key: string;
@@ -42,7 +57,7 @@ function startExecution(
42
57
  abortControllers.delete(key);
43
58
  // Only decrement if this key wasn't already cleared by cancelAllPrefetches.
44
59
  // Without this guard, cancelled tasks' .finally() would underflow active
45
- // below zero, breaking the MAX_CONCURRENT guarantee.
60
+ // below zero, breaking the maxConcurrent guarantee.
46
61
  if (executing.delete(key)) {
47
62
  active--;
48
63
  }
@@ -63,7 +78,7 @@ function startExecution(
63
78
  */
64
79
  function scheduleDrain(): void {
65
80
  if (drainScheduled) return;
66
- if (active >= MAX_CONCURRENT || queue.length === 0) return;
81
+ if (active >= maxConcurrent || queue.length === 0) return;
67
82
  drainScheduled = true;
68
83
  const gen = drainGeneration;
69
84
  waitForIdle()
@@ -71,16 +86,19 @@ function scheduleDrain(): void {
71
86
  Promise.race([waitForViewportImages(), wait(IMAGE_WAIT_TIMEOUT)]),
72
87
  )
73
88
  .then(() => {
74
- drainScheduled = false;
75
- // Stale drain: a cancel/abort happened while we were waiting.
76
- // A fresh scheduleDrain will be called by whatever enqueues next.
89
+ // Stale drain: a cancel/abort happened while we were waiting, and a fresh
90
+ // scheduleDrain may already own drainScheduled for the new generation.
91
+ // Bail WITHOUT clearing the flag so we don't clobber the live wait's
92
+ // single-in-flight-drain coalescing (clearing it here would let the next
93
+ // enqueue start a third overlapping wait).
77
94
  if (gen !== drainGeneration) return;
95
+ drainScheduled = false;
78
96
  if (queue.length > 0) drain();
79
97
  });
80
98
  }
81
99
 
82
100
  function drain(): void {
83
- while (active < MAX_CONCURRENT && queue.length > 0) {
101
+ while (active < maxConcurrent && queue.length > 0) {
84
102
  const item = queue.shift()!;
85
103
  queued.delete(item.key);
86
104
  startExecution(item.key, item.execute);