@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
@@ -7,73 +7,15 @@
7
7
  import type { RouteEntry, TrailingSlashMode } from "../types";
8
8
  import type { EntryData } from "../server/context";
9
9
  import { debugLog, isRouterDebugEnabled } from "./logging.js";
10
+ import { escapeRegExp } from "../regex-escape.js";
10
11
  import { safeDecodeURIComponent } from "./url-params.js";
12
+ import { parsePattern, type ParsedSegment } from "./parse-pattern.js";
11
13
 
12
- /**
13
- * Parsed segment info
14
- */
15
- export interface ParsedSegment {
16
- type: "static" | "param" | "wildcard";
17
- value: string; // static text, param name, or "*"
18
- optional: boolean;
19
- constraint?: string[]; // enum values like ["en", "gb"]
20
- suffix?: string; // literal text after param in same segment (e.g., ".html")
21
- }
22
-
23
- /**
24
- * Parse a route pattern into segments
25
- *
26
- * Supports:
27
- * - Static: /blog, /about
28
- * - Params: /:slug, /:id
29
- * - Optional: /:locale?, /:page?
30
- * - Constrained: /:locale(en|gb), /:type(post|page)
31
- * - Optional + Constrained: /:locale(en|gb)?
32
- * - Wildcard: /*
33
- */
34
- export function parsePattern(pattern: string): ParsedSegment[] {
35
- const segments: ParsedSegment[] = [];
36
- // Match: /segment where segment can be:
37
- // - static text
38
- // - :param
39
- // - :param?
40
- // - :param(a|b)
41
- // - :param(a|b)?
42
- // - *
43
- const segmentRegex =
44
- /\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?([^/]*)|(\*)|([^/]+))/g;
45
-
46
- let match;
47
- while ((match = segmentRegex.exec(pattern)) !== null) {
48
- const [
49
- ,
50
- ,
51
- paramName,
52
- ,
53
- constraint,
54
- optional,
55
- suffix,
56
- wildcard,
57
- staticText,
58
- ] = match;
59
-
60
- if (wildcard) {
61
- segments.push({ type: "wildcard", value: "*", optional: false });
62
- } else if (paramName) {
63
- segments.push({
64
- type: "param",
65
- value: paramName,
66
- optional: optional === "?",
67
- constraint: constraint ? constraint.split("|") : undefined,
68
- suffix: suffix || undefined,
69
- });
70
- } else if (staticText) {
71
- segments.push({ type: "static", value: staticText, optional: false });
72
- }
73
- }
74
-
75
- return segments;
76
- }
14
+ // `parsePattern`/`ParsedSegment` live in the dependency-free `parse-pattern.ts`
15
+ // so the client reverse helper can import them without dragging this module's
16
+ // server-only deps into the browser bundle. Re-exported here for existing
17
+ // importers (build/route-trie, middleware, tests).
18
+ export { parsePattern, type ParsedSegment };
77
19
 
78
20
  /**
79
21
  * Compiled pattern result containing regex, param metadata, and trailing slash info.
@@ -81,7 +23,6 @@ export function parsePattern(pattern: string): ParsedSegment[] {
81
23
  export interface CompiledPattern {
82
24
  regex: RegExp;
83
25
  paramNames: string[];
84
- optionalParams: Set<string>;
85
26
  hasTrailingSlash: boolean;
86
27
  /**
87
28
  * Param-name → allowed values for constrained params (e.g. `:lang(en|gb)`).
@@ -90,6 +31,14 @@ export interface CompiledPattern {
90
31
  * path's behavior (trie-matching.ts:validateAndBuild).
91
32
  */
92
33
  constraints?: Record<string, string[]>;
34
+ /**
35
+ * The pattern's catch-all param, if any (`*` for bare `/*`, the name for a
36
+ * named `:name+`/`:name*`). A zero-or-more catch-all (`oneOrMore: false`)
37
+ * whose optional group is absent binds "" rather than being omitted — so
38
+ * `/docs` matches `/docs/:slug*` with `slug === ""`. `oneOrMore` keeps the
39
+ * same polarity as `ParsedSegment.oneOrMore` and the trie's `w1`.
40
+ */
41
+ catchAll?: { name: string; oneOrMore: boolean };
93
42
  }
94
43
 
95
44
  // Module-level cache for compiled patterns. Route patterns are a finite set
@@ -149,18 +98,36 @@ export function compilePattern(pattern: string): CompiledPattern {
149
98
 
150
99
  const segments = parsePattern(normalizedPattern);
151
100
  const paramNames: string[] = [];
152
- const optionalParams = new Set<string>();
153
101
  let constraints: Record<string, string[]> | undefined;
102
+ let catchAll: { name: string; oneOrMore: boolean } | undefined;
154
103
 
155
104
  let regexPattern = "";
156
105
 
157
106
  for (const segment of segments) {
158
107
  if (segment.type === "wildcard") {
159
- paramNames.push("*");
160
- regexPattern += "/(.*)";
108
+ // Wildcards capture the remainder under `segment.value` ("*" for the bare
109
+ // form, the param name for a named catch-all).
110
+ paramNames.push(segment.value);
111
+ catchAll = { name: segment.value, oneOrMore: Boolean(segment.oneOrMore) };
112
+ if (segment.oneOrMore) {
113
+ // `:name+` — one-or-more, rejects the zero-segment (bare-prefix) case.
114
+ regexPattern += "/(.+)";
115
+ } else {
116
+ // Zero-or-more catch-all: named `:name*` OR the bare `/*` (both parse to
117
+ // `oneOrMore: false`). The whole `/segment` is optional so the bare
118
+ // prefix matches directly, aligning the regex fallback with the trie
119
+ // (which already matches the bare prefix binding "" — trie-matching.ts);
120
+ // buildParamsFromMatch binds "" when the optional group is absent.
121
+ //
122
+ // The bare `/*` previously used a required `/(.*)`, so `/files/*` failed
123
+ // to match `/files` and fell through to trailing-slash normalization,
124
+ // emitting a corrupt `/file` redirect instead of a match (issue #636,
125
+ // parity row C1). It is the same alignment #635 made for named `:name*`.
126
+ regexPattern += "(?:/(.*))?";
127
+ }
161
128
  } else if (segment.type === "param") {
162
129
  paramNames.push(segment.value);
163
- const suffixPattern = segment.suffix ? escapeRegex(segment.suffix) : "";
130
+ const suffixPattern = segment.suffix ? escapeRegExp(segment.suffix) : "";
164
131
  // Constrained params capture anything here; the allowed values are
165
132
  // checked post-decode in findMatch so URL-encoded constraint values
166
133
  // (e.g. `:lang(en GB)` via `/en%20GB`) still match.
@@ -171,7 +138,6 @@ export function compilePattern(pattern: string): CompiledPattern {
171
138
  }
172
139
 
173
140
  if (segment.optional) {
174
- optionalParams.add(segment.value);
175
141
  // Optional: make the whole /segment optional
176
142
  regexPattern += `(?:/${valuePattern}${suffixPattern})?`;
177
143
  } else {
@@ -179,11 +145,10 @@ export function compilePattern(pattern: string): CompiledPattern {
179
145
  }
180
146
  } else {
181
147
  // Static segment
182
- regexPattern += `/${escapeRegex(segment.value)}`;
148
+ regexPattern += `/${escapeRegExp(segment.value)}`;
183
149
  }
184
150
  }
185
151
 
186
- // Handle root path
187
152
  if (regexPattern === "") {
188
153
  regexPattern = "/";
189
154
  }
@@ -210,9 +175,9 @@ export function compilePattern(pattern: string): CompiledPattern {
210
175
  return {
211
176
  regex: new RegExp(`^${regexPattern}$`),
212
177
  paramNames,
213
- optionalParams,
214
178
  hasTrailingSlash,
215
179
  ...(constraints ? { constraints } : {}),
180
+ ...(catchAll ? { catchAll } : {}),
216
181
  };
217
182
  }
218
183
 
@@ -241,29 +206,35 @@ function satisfiesConstraints(
241
206
  return true;
242
207
  }
243
208
 
244
- /**
245
- * Escape special regex characters in a string
246
- */
247
- function escapeRegex(str: string): string {
248
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
249
- }
250
-
251
209
  /**
252
210
  * Build the named-params record from a regex match. Optional segments that
253
211
  * didn't capture leave the corresponding group `undefined`; we skip those
254
212
  * keys so `ctx.params.<name>` reads as `undefined` rather than `""`. This
255
213
  * keeps the runtime aligned with the `ExtractParams` type and matches the
256
214
  * trie matcher's contract (see `trie-matching.ts:validateAndBuild`).
215
+ *
216
+ * A zero-or-more catch-all (`compiled.catchAll`, `oneOrMore: false`) whose
217
+ * optional group didn't capture binds "" instead of being omitted, so `/docs`
218
+ * matches `/docs/:slug*` with `slug === ""`. Exported so the `renderRoute`
219
+ * testing harness (`matchLeaf`) shares this exact logic instead of forking it.
257
220
  */
258
- function buildParamsFromMatch(
221
+ export function buildParamsFromMatch(
259
222
  match: RegExpExecArray,
260
223
  paramNames: string[],
224
+ catchAll?: { name: string; oneOrMore: boolean },
261
225
  ): Record<string, string> {
262
226
  const params: Record<string, string> = {};
263
227
  paramNames.forEach((name, index) => {
264
228
  const captured = match[index + 1];
265
229
  if (captured !== undefined) {
230
+ // A catch-all remainder decodes identically whether split-per-segment or
231
+ // whole-string (a literal `/` never lives inside a `%XX` escape), so a
232
+ // single decode is correct and cheapest.
266
233
  params[name] = safeDecodeURIComponent(captured);
234
+ } else if (catchAll && name === catchAll.name && !catchAll.oneOrMore) {
235
+ // A zero-or-more catch-all (`:name*` or the bare `/*`) whose optional
236
+ // group was absent binds "" rather than being omitted.
237
+ params[name] = "";
267
238
  }
268
239
  });
269
240
  return params;
@@ -285,36 +256,49 @@ function buildParamsFromMatch(
285
256
  export function extractStaticPrefix(pattern: string): string {
286
257
  if (!pattern || pattern === "/") return "";
287
258
 
288
- // Find the first occurrence of : or *
289
- const paramIndex = pattern.indexOf(":");
290
- const wildcardIndex = pattern.indexOf("*");
291
-
292
- let cutIndex = -1;
293
- if (paramIndex !== -1 && wildcardIndex !== -1) {
294
- cutIndex = Math.min(paramIndex, wildcardIndex);
295
- } else if (paramIndex !== -1) {
296
- cutIndex = paramIndex;
297
- } else if (wildcardIndex !== -1) {
298
- cutIndex = wildcardIndex;
299
- }
300
-
301
- if (cutIndex === -1) {
302
- // No params or wildcards - entire pattern is static
303
- return pattern;
259
+ // Walk segments and stop at the first that is a real param (`:name`) or a
260
+ // wildcard (`*`). A literal `:` or `*` not at a segment boundary (e.g. the
261
+ // `a:b` in `/a:b/c/:id`, or `tel:+1`) is a STATIC segment and must NOT
262
+ // terminate the prefix — `pattern.indexOf(":")` misread it as a param marker,
263
+ // returning "" and dropping the findMatch fast-skip optimization for that
264
+ // entry on every request. Classification mirrors parsePattern: a leading `:`
265
+ // marks a param, a leading `*` marks a wildcard.
266
+ const hasLeadingSlash = pattern.startsWith("/");
267
+ const body = hasLeadingSlash ? pattern.slice(1) : pattern;
268
+ const segments = body.split("/");
269
+
270
+ const staticSegments: string[] = [];
271
+ for (const segment of segments) {
272
+ if (segment.startsWith(":") || segment.startsWith("*")) {
273
+ break;
274
+ }
275
+ staticSegments.push(segment);
304
276
  }
305
277
 
306
- if (cutIndex === 0) {
307
- // Pattern starts with : or * - no static prefix
308
- return "";
309
- }
278
+ // No leading static segment (first segment is a param/wildcard) -> no prefix.
279
+ if (staticSegments.length === 0) return "";
280
+ // Every segment was static (no param/wildcard) -> the whole pattern is the
281
+ // prefix. Preserve a trailing slash only when it existed in the input; a
282
+ // split of "/a/b" yields ["a","b"] (no empty tail) so a re-join is exact.
283
+ if (staticSegments.length === segments.length) return pattern;
310
284
 
311
- // Find the last / before the param
312
- const lastSlash = pattern.lastIndexOf("/", cutIndex - 1);
313
- if (lastSlash === -1 || lastSlash === 0) {
314
- return "";
315
- }
285
+ const prefix = staticSegments.join("/");
286
+ return hasLeadingSlash ? "/" + prefix : prefix;
287
+ }
316
288
 
317
- return pattern.slice(0, lastSlash);
289
+ /**
290
+ * Join a URL prefix to a sub-prefix, collapsing the duplicate slash when the
291
+ * base ends with "/" and the sub-prefix starts with "/". This mirrors the
292
+ * canonical join in `include()` (urls/include-helper.ts) and `runWithPrefixes`
293
+ * (server/context.ts) so a nested lazy include's runtime staticPrefix matches
294
+ * the build-time trie's `sp` (e.g. `include("/parent/", …)` containing
295
+ * `include("/child", …)` resolves to `/parent/child`, not `/parent//child`).
296
+ */
297
+ export function joinPrefix(base: string | undefined, prefix: string): string {
298
+ if (!base) return prefix;
299
+ return base.endsWith("/") && prefix.startsWith("/")
300
+ ? base + prefix.slice(1)
301
+ : base + prefix;
318
302
  }
319
303
 
320
304
  /**
@@ -322,8 +306,8 @@ export function extractStaticPrefix(pattern: string): string {
322
306
  *
323
307
  * Note: Optional params that are absent in the path are omitted from the
324
308
  * returned `params` (read as `undefined`), matching the trie matcher and
325
- * the `ExtractParams<"/:locale?/...">` type. Use the pattern definition or
326
- * `optionalParams` to determine which keys are optional.
309
+ * the `ExtractParams<"/:locale?/...">` type. Use the pattern definition to
310
+ * determine which keys are optional.
327
311
  *
328
312
  * Trailing slash handling (priority order):
329
313
  * 1. Per-route `trailingSlash` config from route()
@@ -341,10 +325,7 @@ export interface RouteMatchResult<TEnv = any> {
341
325
  entry: RouteEntry<TEnv>;
342
326
  routeKey: string;
343
327
  params: Record<string, string>;
344
- optionalParams: Set<string>;
345
328
  redirectTo?: string;
346
- /** Ancestry shortCodes for layout pruning (from trie match) */
347
- ancestry?: string[];
348
329
  /** Route has pre-rendered data available (from trie) */
349
330
  pr?: true;
350
331
  /** Passthrough: handler kept for live fallback on unknown params (from trie) */
@@ -422,8 +403,6 @@ export function findMatch<TEnv>(
422
403
  : pathname + "/";
423
404
 
424
405
  for (const entry of routesEntries) {
425
- // Short-circuit: skip entry if pathname doesn't start with static prefix
426
- // staticPrefix is pre-computed at registration time, so this is O(1)
427
406
  if (entry.staticPrefix && !pathname.startsWith(entry.staticPrefix)) {
428
407
  if (effectiveDebug) {
429
408
  debugStats.entriesSkipped++;
@@ -435,8 +414,6 @@ export function findMatch<TEnv>(
435
414
  continue;
436
415
  }
437
416
 
438
- // Check if this is a lazy entry that needs evaluation
439
- // When staticPrefix matches but routes are not yet populated, signal caller to evaluate
440
417
  if (entry.lazy && !entry.lazyEvaluated) {
441
418
  if (effectiveDebug) {
442
419
  debugLog("findMatch", "lazy entry requires evaluation", {
@@ -457,7 +434,6 @@ export function findMatch<TEnv>(
457
434
  debugStats.routesChecked++;
458
435
  }
459
436
 
460
- // Join prefix and pattern, handling edge cases
461
437
  let fullPattern: string;
462
438
  if (entry.prefix === "" || entry.prefix === "/") {
463
439
  fullPattern = pattern;
@@ -467,19 +443,12 @@ export function findMatch<TEnv>(
467
443
  fullPattern = entry.prefix + pattern;
468
444
  }
469
445
 
470
- const {
471
- regex,
472
- paramNames,
473
- optionalParams,
474
- hasTrailingSlash,
475
- constraints,
476
- } = getCompiledPattern(fullPattern);
446
+ const { regex, paramNames, hasTrailingSlash, constraints, catchAll } =
447
+ getCompiledPattern(fullPattern);
477
448
 
478
- // Get trailing slash mode for this route (per-route config or pattern-based)
479
449
  const trailingSlashMode: TrailingSlashMode | undefined =
480
450
  entry.trailingSlash?.[routeKey];
481
451
 
482
- // Prerender flag from entry metadata (set by urls() for prerender handlers)
483
452
  const prFlag = entry.prerenderRouteKeys?.has(routeKey)
484
453
  ? { pr: true as const }
485
454
  : {};
@@ -487,13 +456,10 @@ export function findMatch<TEnv>(
487
456
  ? { pt: true as const }
488
457
  : {};
489
458
 
490
- // Try exact match first
491
459
  const match = regex.exec(pathname);
492
460
  if (match) {
493
- const params = buildParamsFromMatch(match, paramNames);
461
+ const params = buildParamsFromMatch(match, paramNames, catchAll);
494
462
 
495
- // Validate constraints against decoded values; a failure falls
496
- // through to the next route so other patterns can still match.
497
463
  if (!satisfiesConstraints(params, constraints)) {
498
464
  continue;
499
465
  }
@@ -506,29 +472,24 @@ export function findMatch<TEnv>(
506
472
  });
507
473
  }
508
474
 
509
- // Check if trailing slash mode requires redirect even on exact match
510
475
  if (
511
476
  trailingSlashMode === "always" &&
512
477
  !pathnameHasTrailingSlash &&
513
478
  pathname !== "/"
514
479
  ) {
515
- // Mode says always have trailing slash, but pathname doesn't have it
516
480
  return {
517
481
  entry,
518
482
  routeKey,
519
483
  params,
520
- optionalParams,
521
484
  redirectTo: pathname + "/",
522
485
  ...prFlag,
523
486
  ...ptFlag,
524
487
  };
525
488
  } else if (trailingSlashMode === "never" && pathnameHasTrailingSlash) {
526
- // Mode says never have trailing slash, but pathname has it
527
489
  return {
528
490
  entry,
529
491
  routeKey,
530
492
  params,
531
- optionalParams,
532
493
  redirectTo: pathname.slice(0, -1),
533
494
  ...prFlag,
534
495
  ...ptFlag,
@@ -539,40 +500,33 @@ export function findMatch<TEnv>(
539
500
  entry,
540
501
  routeKey,
541
502
  params,
542
- optionalParams,
543
503
  ...prFlag,
544
504
  ...ptFlag,
545
505
  };
546
506
  }
547
507
 
548
- // Try alternate pathname (opposite trailing slash)
549
508
  const altMatch = regex.exec(alternatePathname);
550
509
  if (altMatch) {
551
- const params = buildParamsFromMatch(altMatch, paramNames);
510
+ const params = buildParamsFromMatch(altMatch, paramNames, catchAll);
552
511
 
553
512
  if (!satisfiesConstraints(params, constraints)) {
554
513
  continue;
555
514
  }
556
515
 
557
- // Determine redirect behavior based on mode
558
516
  if (trailingSlashMode === "ignore") {
559
- // Match without redirect
560
517
  return {
561
518
  entry,
562
519
  routeKey,
563
520
  params,
564
- optionalParams,
565
521
  ...prFlag,
566
522
  ...ptFlag,
567
523
  };
568
524
  } else if (trailingSlashMode === "never") {
569
- // Redirect to no trailing slash
570
525
  if (pathnameHasTrailingSlash) {
571
526
  return {
572
527
  entry,
573
528
  routeKey,
574
529
  params,
575
- optionalParams,
576
530
  redirectTo: alternatePathname,
577
531
  ...prFlag,
578
532
  ...ptFlag,
@@ -582,18 +536,15 @@ export function findMatch<TEnv>(
582
536
  entry,
583
537
  routeKey,
584
538
  params,
585
- optionalParams,
586
539
  ...prFlag,
587
540
  ...ptFlag,
588
541
  };
589
542
  } else if (trailingSlashMode === "always") {
590
- // Redirect to with trailing slash
591
543
  if (!pathnameHasTrailingSlash) {
592
544
  return {
593
545
  entry,
594
546
  routeKey,
595
547
  params,
596
- optionalParams,
597
548
  redirectTo: alternatePathname,
598
549
  ...prFlag,
599
550
  ...ptFlag,
@@ -603,13 +554,10 @@ export function findMatch<TEnv>(
603
554
  entry,
604
555
  routeKey,
605
556
  params,
606
- optionalParams,
607
557
  ...prFlag,
608
558
  ...ptFlag,
609
559
  };
610
560
  } else {
611
- // No explicit mode - use pattern-based detection
612
- // Redirect to canonical form (what the pattern defines)
613
561
  const canonicalPath = hasTrailingSlash
614
562
  ? alternatePathname
615
563
  : pathname.slice(0, -1);
@@ -617,7 +565,6 @@ export function findMatch<TEnv>(
617
565
  entry,
618
566
  routeKey,
619
567
  params,
620
- optionalParams,
621
568
  redirectTo: canonicalPath,
622
569
  ...prFlag,
623
570
  ...ptFlag,
@@ -638,7 +585,7 @@ export function* traverseBack(entry: EntryData): Generator<EntryData> {
638
585
  let current: EntryData | null = entry;
639
586
  const items = [] as EntryData[];
640
587
  while (current !== null) {
641
- items.push(current); // Move up to next parent
588
+ items.push(current);
642
589
  current = current.parent;
643
590
  }
644
591
  for (let i = items.length - 1; i >= 0; i--) {
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Resolve the prefetch cache TTL once, at router init, into the three derived
3
+ * values the rest of the router consumes: seconds (for the Cache-Control
4
+ * max-age), milliseconds (for the client-side in-memory cache), and the
5
+ * Cache-Control header string (or false when caching is disabled).
6
+ *
7
+ * `false` disables prefetch caching (seconds 0). A non-finite input
8
+ * (NaN/Infinity, e.g. from an env-derived number that failed to parse) is
9
+ * treated as the default rather than producing a malformed
10
+ * `Cache-Control: max-age=NaN` header; CDNs/browsers ignore such a directive,
11
+ * which would silently disable caching on EVERY prefetch response. Negative
12
+ * finite values clamp to 0 (disabled).
13
+ *
14
+ * Policy note: this is the prefetch-TTL policy specifically. Other finite-number
15
+ * guards in the codebase deliberately differ — defer.ts treats Infinity as an
16
+ * intentional disable, and profile-registry.ts THROWS on non-finite/negative ttl
17
+ * at config time. They are NOT the same guard, so don't unify them.
18
+ */
19
+ export interface ResolvedPrefetchCacheTTL {
20
+ /** TTL in seconds for the Cache-Control max-age directive. */
21
+ seconds: number;
22
+ /** TTL in milliseconds for the client-side in-memory prefetch cache. */
23
+ ms: number;
24
+ /** Cache-Control header value, or false when caching is disabled. */
25
+ cacheControl: string | false;
26
+ }
27
+
28
+ const DEFAULT_PREFETCH_CACHE_TTL_SECONDS = 300;
29
+
30
+ export function resolvePrefetchCacheTTL(
31
+ rawTTL: number | false | undefined,
32
+ ): ResolvedPrefetchCacheTTL {
33
+ let seconds: number;
34
+ if (rawTTL === false) {
35
+ seconds = 0;
36
+ } else if (rawTTL === undefined) {
37
+ seconds = DEFAULT_PREFETCH_CACHE_TTL_SECONDS;
38
+ } else if (typeof rawTTL === "number" && Number.isFinite(rawTTL)) {
39
+ seconds = Math.max(0, Math.floor(rawTTL));
40
+ } else {
41
+ // Non-finite (NaN/Infinity): fall back to the default rather than emit a
42
+ // malformed max-age=NaN/Infinity that CDNs and browsers reject.
43
+ seconds = DEFAULT_PREFETCH_CACHE_TTL_SECONDS;
44
+ }
45
+
46
+ return {
47
+ seconds,
48
+ ms: seconds * 1000,
49
+ cacheControl: seconds === 0 ? false : `private, max-age=${seconds}`,
50
+ };
51
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Resolve the client-side prefetch limits — the in-memory prefetch cache size
3
+ * and the speculative-prefetch queue concurrency — once, at router init. Both
4
+ * are positive-integer counts shipped to the browser in payload metadata; the
5
+ * browser entry feeds them to the prefetch cache (`initPrefetchCache`) and the
6
+ * prefetch queue (`setPrefetchConcurrency`) at startup.
7
+ *
8
+ * Policy note: this is a SEPARATE policy from resolvePrefetchCacheTTL
9
+ * (prefetch-cache-ttl.ts). These are counts, not durations, so the disable
10
+ * paths differ: a TTL of `false`/0 intentionally turns prefetching off, but a
11
+ * cache size or concurrency of 0 would silently break prefetching while leaving
12
+ * it nominally enabled — never the intent of passing a number. So any value
13
+ * that is not a finite integer >= 1 (0, negative, fractional below 1, NaN,
14
+ * Infinity, undefined) falls back to the default. Finite values >= 1 are
15
+ * floored. Do not unify this guard with the TTL guard.
16
+ */
17
+
18
+ export const DEFAULT_PREFETCH_CACHE_SIZE = 100;
19
+ export const DEFAULT_PREFETCH_CONCURRENCY = 2;
20
+
21
+ export function resolvePrefetchCacheSize(raw: number | undefined): number {
22
+ return resolvePositiveCount(raw, DEFAULT_PREFETCH_CACHE_SIZE);
23
+ }
24
+
25
+ export function resolvePrefetchConcurrency(raw: number | undefined): number {
26
+ return resolvePositiveCount(raw, DEFAULT_PREFETCH_CONCURRENCY);
27
+ }
28
+
29
+ function resolvePositiveCount(
30
+ raw: number | undefined,
31
+ fallback: number,
32
+ ): number {
33
+ if (typeof raw === "number" && Number.isFinite(raw) && raw >= 1) {
34
+ return Math.floor(raw);
35
+ }
36
+ return fallback;
37
+ }