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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (411) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +296 -887
  3. package/dist/bin/rango.js +459 -91
  4. package/dist/testing/vitest.js +36 -2
  5. package/dist/vite/index.js +1708 -414
  6. package/package.json +35 -10
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +82 -5
  9. package/skills/bundle-analysis/SKILL.md +2 -2
  10. package/skills/cache-guide/SKILL.md +14 -9
  11. package/skills/caching/SKILL.md +221 -12
  12. package/skills/catalog.json +271 -0
  13. package/skills/comparison/SKILL.md +50 -0
  14. package/skills/comparison/agents/openai.yaml +4 -0
  15. package/skills/comparison/references/framework-comparison.md +837 -0
  16. package/skills/composability/SKILL.md +83 -2
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +5 -3
  19. package/skills/defer-hydration/SKILL.md +235 -0
  20. package/skills/document-cache/SKILL.md +11 -3
  21. package/skills/fonts/SKILL.md +1 -1
  22. package/skills/handler-use/SKILL.md +9 -9
  23. package/skills/hooks/SKILL.md +73 -900
  24. package/skills/hooks/data.md +273 -0
  25. package/skills/hooks/handle-and-actions.md +103 -0
  26. package/skills/hooks/navigation.md +110 -0
  27. package/skills/hooks/outlets.md +41 -0
  28. package/skills/hooks/state.md +228 -0
  29. package/skills/hooks/urls.md +135 -0
  30. package/skills/host-router/SKILL.md +84 -7
  31. package/skills/i18n/SKILL.md +1 -1
  32. package/skills/intercept/SKILL.md +51 -17
  33. package/skills/layout/SKILL.md +38 -16
  34. package/skills/links/SKILL.md +1 -1
  35. package/skills/loader/SKILL.md +48 -20
  36. package/skills/middleware/SKILL.md +11 -5
  37. package/skills/migrate-nextjs/SKILL.md +203 -20
  38. package/skills/migrate-react-router/SKILL.md +59 -675
  39. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  40. package/skills/migrate-react-router/component-migration.md +196 -0
  41. package/skills/migrate-react-router/data-and-actions.md +225 -0
  42. package/skills/migrate-react-router/route-mapping.md +271 -0
  43. package/skills/mime-routes/SKILL.md +3 -3
  44. package/skills/observability/SKILL.md +70 -5
  45. package/skills/parallel/SKILL.md +32 -8
  46. package/skills/ppr/SKILL.md +622 -0
  47. package/skills/prerender/SKILL.md +59 -28
  48. package/skills/rango/SKILL.md +124 -50
  49. package/skills/response-routes/SKILL.md +78 -46
  50. package/skills/route/SKILL.md +85 -6
  51. package/skills/router-setup/SKILL.md +41 -6
  52. package/skills/scripts/SKILL.md +179 -0
  53. package/skills/server-actions/SKILL.md +28 -3
  54. package/skills/shell-manifest/SKILL.md +185 -0
  55. package/skills/streams-and-websockets/SKILL.md +1 -1
  56. package/skills/tailwind/SKILL.md +28 -4
  57. package/skills/testing/SKILL.md +68 -654
  58. package/skills/testing/bindings.md +103 -0
  59. package/skills/testing/cache-prerender.md +127 -0
  60. package/skills/testing/client-components.md +124 -0
  61. package/skills/testing/e2e-parity.md +125 -0
  62. package/skills/testing/flight.md +91 -0
  63. package/skills/testing/handles.md +131 -0
  64. package/skills/testing/loader.md +128 -0
  65. package/skills/testing/middleware.md +99 -0
  66. package/skills/testing/render-handler.md +122 -0
  67. package/skills/testing/response-routes.md +95 -0
  68. package/skills/testing/reverse-and-types.md +85 -0
  69. package/skills/testing/server-actions.md +107 -0
  70. package/skills/testing/server-tree.md +128 -0
  71. package/skills/testing/setup.md +123 -0
  72. package/skills/theme/SKILL.md +1 -1
  73. package/skills/typesafety/SKILL.md +45 -918
  74. package/skills/typesafety/env-and-bindings.md +254 -0
  75. package/skills/typesafety/generated-files-and-cli.md +335 -0
  76. package/skills/typesafety/params-and-search.md +153 -0
  77. package/skills/typesafety/route-types.md +209 -0
  78. package/skills/use-cache/SKILL.md +47 -17
  79. package/skills/vercel/SKILL.md +128 -0
  80. package/skills/view-transitions/SKILL.md +44 -1
  81. package/src/__augment-tests__/augmented.check.ts +2 -3
  82. package/src/__internal.ts +0 -65
  83. package/src/browser/action-coordinator.ts +1 -1
  84. package/src/browser/action-fence.ts +47 -0
  85. package/src/browser/app-shell.ts +14 -27
  86. package/src/browser/connection-warmup.ts +134 -0
  87. package/src/browser/cookie-name.ts +140 -0
  88. package/src/browser/event-controller.ts +178 -100
  89. package/src/browser/invalidate-client-cache.ts +52 -0
  90. package/src/browser/logging.ts +28 -0
  91. package/src/browser/merge-segment-loaders.ts +6 -4
  92. package/src/browser/navigation-bridge.ts +81 -68
  93. package/src/browser/navigation-client.ts +115 -70
  94. package/src/browser/navigation-store-handle.ts +38 -0
  95. package/src/browser/navigation-store.ts +153 -88
  96. package/src/browser/navigation-transaction.ts +0 -32
  97. package/src/browser/network-error-handler.ts +34 -7
  98. package/src/browser/partial-update.ts +157 -144
  99. package/src/browser/prefetch/cache.ts +148 -81
  100. package/src/browser/prefetch/fetch.ts +231 -51
  101. package/src/browser/prefetch/queue.ts +25 -7
  102. package/src/browser/rango-state.ts +157 -115
  103. package/src/browser/react/Link.tsx +40 -7
  104. package/src/browser/react/NavigationProvider.tsx +140 -99
  105. package/src/browser/react/ScrollRestoration.tsx +10 -6
  106. package/src/browser/react/filter-segment-order.ts +17 -2
  107. package/src/browser/react/index.ts +0 -51
  108. package/src/browser/react/location-state-shared.ts +14 -15
  109. package/src/browser/react/location-state.ts +0 -1
  110. package/src/browser/react/use-action.ts +6 -15
  111. package/src/browser/react/use-handle.ts +0 -5
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +33 -8
  114. package/src/browser/react/use-navigation.ts +10 -5
  115. package/src/browser/react/use-params.ts +0 -2
  116. package/src/browser/react/use-router.ts +6 -4
  117. package/src/browser/react/use-search-params.ts +0 -5
  118. package/src/browser/react/use-segments.ts +0 -13
  119. package/src/browser/response-adapter.ts +74 -8
  120. package/src/browser/rsc-router.tsx +97 -22
  121. package/src/browser/scroll-restoration.ts +15 -8
  122. package/src/browser/segment-reconciler.ts +31 -21
  123. package/src/browser/server-action-bridge.ts +216 -38
  124. package/src/browser/types.ts +94 -22
  125. package/src/browser/validate-redirect-origin.ts +43 -16
  126. package/src/build/generate-manifest.ts +155 -131
  127. package/src/build/generate-route-types.ts +1 -1
  128. package/src/build/index.ts +11 -5
  129. package/src/build/prefix-tree-utils.ts +123 -0
  130. package/src/build/route-trie.ts +152 -22
  131. package/src/build/route-types/ast-route-extraction.ts +15 -8
  132. package/src/build/route-types/codegen.ts +12 -1
  133. package/src/build/route-types/include-resolution.ts +455 -61
  134. package/src/build/route-types/param-extraction.ts +6 -3
  135. package/src/build/route-types/per-module-writer.ts +15 -2
  136. package/src/build/route-types/router-processing.ts +77 -41
  137. package/src/build/route-types/source-scan.ts +105 -7
  138. package/src/build/runtime-discovery.ts +4 -1
  139. package/src/cache/cache-error.ts +104 -0
  140. package/src/cache/cache-key-utils.ts +58 -13
  141. package/src/cache/cache-policy.ts +108 -34
  142. package/src/cache/cache-runtime.ts +454 -101
  143. package/src/cache/cache-scope.ts +159 -54
  144. package/src/cache/cache-tag.ts +149 -0
  145. package/src/cache/cf/cf-base64.ts +33 -0
  146. package/src/cache/cf/cf-cache-constants.ts +127 -0
  147. package/src/cache/cf/cf-cache-store.ts +2170 -377
  148. package/src/cache/cf/cf-cache-types.ts +349 -0
  149. package/src/cache/cf/cf-kv-utils.ts +46 -0
  150. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  151. package/src/cache/cf/index.ts +6 -16
  152. package/src/cache/document-cache.ts +126 -41
  153. package/src/cache/handle-snapshot.ts +70 -0
  154. package/src/cache/index.ts +23 -20
  155. package/src/cache/memory-segment-store.ts +243 -37
  156. package/src/cache/profile-registry.ts +46 -31
  157. package/src/cache/read-through-swr.ts +56 -12
  158. package/src/cache/segment-codec.ts +13 -21
  159. package/src/cache/shell-snapshot.ts +417 -0
  160. package/src/cache/tag-invalidation.ts +230 -0
  161. package/src/cache/types.ts +194 -99
  162. package/src/cache/vercel/index.ts +11 -0
  163. package/src/cache/vercel/vercel-cache-store.ts +1132 -0
  164. package/src/client.rsc.tsx +39 -22
  165. package/src/client.tsx +28 -58
  166. package/src/cloudflare/index.ts +11 -0
  167. package/src/cloudflare/tracing.ts +108 -0
  168. package/src/component-utils.ts +19 -0
  169. package/src/components/DefaultDocument.tsx +8 -2
  170. package/src/context-var.ts +13 -1
  171. package/src/decode-loader-results.ts +18 -2
  172. package/src/defer.ts +185 -0
  173. package/src/deps/ssr.ts +0 -1
  174. package/src/encode-kv.ts +49 -0
  175. package/src/errors.ts +0 -3
  176. package/src/escape-script.ts +52 -0
  177. package/src/handle.ts +57 -40
  178. package/src/handles/MetaTags.tsx +24 -53
  179. package/src/handles/Scripts.tsx +183 -0
  180. package/src/handles/breadcrumbs.ts +35 -8
  181. package/src/handles/deferred-resolution.ts +127 -0
  182. package/src/handles/is-thenable.ts +18 -0
  183. package/src/handles/meta.ts +14 -40
  184. package/src/handles/script.ts +244 -0
  185. package/src/host/cookie-handler.ts +9 -60
  186. package/src/host/errors.ts +13 -22
  187. package/src/host/index.ts +7 -0
  188. package/src/host/pattern-matcher.ts +23 -52
  189. package/src/host/router.ts +1 -65
  190. package/src/host/testing.ts +40 -27
  191. package/src/host/types.ts +6 -2
  192. package/src/href-client.ts +7 -12
  193. package/src/index.rsc.ts +88 -8
  194. package/src/index.ts +90 -16
  195. package/src/internal-debug.ts +11 -10
  196. package/src/loader.rsc.ts +19 -9
  197. package/src/loader.ts +12 -4
  198. package/src/outlet-provider.tsx +1 -5
  199. package/src/prerender/param-hash.ts +16 -16
  200. package/src/prerender/store.ts +32 -37
  201. package/src/prerender.ts +75 -7
  202. package/src/redirect-origin.ts +114 -0
  203. package/src/regex-escape.ts +8 -0
  204. package/src/render-error-thrower.tsx +20 -0
  205. package/src/response-utils.ts +25 -0
  206. package/src/root-error-boundary.tsx +1 -19
  207. package/src/route-content-wrapper.tsx +13 -49
  208. package/src/route-definition/dsl-helpers.ts +60 -53
  209. package/src/route-definition/helper-factories.ts +0 -2
  210. package/src/route-definition/helpers-types.ts +46 -46
  211. package/src/route-definition/index.ts +1 -2
  212. package/src/route-definition/redirect.ts +44 -11
  213. package/src/route-definition/resolve-handler-use.ts +6 -1
  214. package/src/route-definition/use-item-types.ts +3 -6
  215. package/src/route-map-builder.ts +41 -20
  216. package/src/route-types.ts +0 -5
  217. package/src/router/content-negotiation.ts +58 -23
  218. package/src/router/error-handling.ts +44 -17
  219. package/src/router/find-match.ts +129 -30
  220. package/src/router/handler-context.ts +6 -1
  221. package/src/router/instrument.ts +355 -0
  222. package/src/router/intercept-resolution.ts +35 -2
  223. package/src/router/lazy-includes.ts +79 -56
  224. package/src/router/loader-resolution.ts +151 -73
  225. package/src/router/logging.ts +0 -6
  226. package/src/router/manifest.ts +74 -40
  227. package/src/router/match-api.ts +76 -52
  228. package/src/router/match-context.ts +0 -22
  229. package/src/router/match-handlers.ts +181 -178
  230. package/src/router/match-middleware/background-revalidation.ts +40 -24
  231. package/src/router/match-middleware/cache-lookup.ts +115 -194
  232. package/src/router/match-middleware/cache-store.ts +61 -50
  233. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  234. package/src/router/match-middleware/segment-resolution.ts +0 -22
  235. package/src/router/match-pipelines.ts +1 -42
  236. package/src/router/match-result.ts +36 -67
  237. package/src/router/metrics.ts +0 -34
  238. package/src/router/middleware-types.ts +0 -116
  239. package/src/router/middleware.ts +231 -120
  240. package/src/router/navigation-snapshot.ts +7 -56
  241. package/src/router/params-util.ts +23 -0
  242. package/src/router/parse-pattern.ts +115 -0
  243. package/src/router/pattern-matching.ts +99 -152
  244. package/src/router/prefetch-cache-ttl.ts +51 -0
  245. package/src/router/prefetch-limits.ts +37 -0
  246. package/src/router/prerender-match.ts +111 -66
  247. package/src/router/preview-match.ts +3 -1
  248. package/src/router/request-classification.ts +47 -42
  249. package/src/router/revalidation.ts +75 -81
  250. package/src/router/route-snapshot.ts +14 -3
  251. package/src/router/router-context.ts +6 -29
  252. package/src/router/router-interfaces.ts +70 -8
  253. package/src/router/router-options.ts +126 -4
  254. package/src/router/segment-resolution/fresh.ts +104 -80
  255. package/src/router/segment-resolution/helpers.ts +86 -6
  256. package/src/router/segment-resolution/loader-cache.ts +155 -39
  257. package/src/router/segment-resolution/loader-mask.ts +60 -0
  258. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  259. package/src/router/segment-resolution/mask-nested.ts +83 -0
  260. package/src/router/segment-resolution/revalidation.ts +215 -304
  261. package/src/router/segment-resolution/static-store.ts +19 -5
  262. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  263. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  264. package/src/router/segment-resolution.ts +5 -1
  265. package/src/router/segment-wrappers.ts +6 -5
  266. package/src/router/state-cookie-name.ts +33 -0
  267. package/src/router/substitute-pattern-params.ts +54 -35
  268. package/src/router/telemetry-otel.ts +160 -200
  269. package/src/router/telemetry.ts +9 -23
  270. package/src/router/timeout.ts +0 -20
  271. package/src/router/tracing.ts +215 -0
  272. package/src/router/trie-matching.ts +171 -64
  273. package/src/router/types.ts +1 -63
  274. package/src/router/url-params.ts +13 -5
  275. package/src/router.ts +119 -48
  276. package/src/rsc/full-payload.ts +70 -0
  277. package/src/rsc/handler-context.ts +1 -0
  278. package/src/rsc/handler.ts +267 -152
  279. package/src/rsc/helpers.ts +78 -4
  280. package/src/rsc/index.ts +1 -4
  281. package/src/rsc/json-route-result.ts +38 -0
  282. package/src/rsc/loader-fetch.ts +114 -38
  283. package/src/rsc/manifest-init.ts +29 -42
  284. package/src/rsc/nonce.ts +10 -1
  285. package/src/rsc/origin-guard.ts +11 -15
  286. package/src/rsc/progressive-enhancement.ts +120 -13
  287. package/src/rsc/redirect-guard.ts +100 -0
  288. package/src/rsc/response-cache-serve.ts +238 -0
  289. package/src/rsc/response-error.ts +79 -12
  290. package/src/rsc/response-route-handler.ts +58 -141
  291. package/src/rsc/rsc-rendering.ts +492 -49
  292. package/src/rsc/runtime-warnings.ts +14 -0
  293. package/src/rsc/server-action.ts +268 -82
  294. package/src/rsc/shell-capture.ts +1190 -0
  295. package/src/rsc/shell-serve.ts +181 -0
  296. package/src/rsc/transition-gate.ts +89 -0
  297. package/src/rsc/types.ts +45 -3
  298. package/src/runtime-env.ts +18 -0
  299. package/src/search-params.ts +31 -26
  300. package/src/segment-loader-promise.ts +49 -4
  301. package/src/segment-system.tsx +260 -95
  302. package/src/server/context.ts +99 -9
  303. package/src/server/cookie-parse.ts +32 -0
  304. package/src/server/cookie-store.ts +125 -2
  305. package/src/server/handle-store.ts +21 -38
  306. package/src/server/loader-registry.ts +33 -42
  307. package/src/server/request-context.ts +379 -138
  308. package/src/ssr/index.tsx +491 -182
  309. package/src/ssr/inject-rsc-eager.ts +167 -0
  310. package/src/ssr/ssr-root.tsx +228 -0
  311. package/src/static-handler.ts +10 -13
  312. package/src/testing/cache-status.ts +44 -48
  313. package/src/testing/collect-handle.ts +14 -31
  314. package/src/testing/dispatch.ts +533 -160
  315. package/src/testing/e2e/fixture.ts +45 -11
  316. package/src/testing/e2e/index.ts +1 -22
  317. package/src/testing/e2e/matchers.ts +0 -16
  318. package/src/testing/e2e/parity.ts +85 -4
  319. package/src/testing/e2e/server.ts +12 -0
  320. package/src/testing/flight-matchers.ts +7 -14
  321. package/src/testing/flight-normalize.ts +11 -0
  322. package/src/testing/flight-runtime.d.ts +36 -0
  323. package/src/testing/flight-tree.ts +682 -0
  324. package/src/testing/flight.entry.ts +30 -0
  325. package/src/testing/flight.ts +145 -70
  326. package/src/testing/generated-routes.ts +26 -50
  327. package/src/testing/index.ts +18 -19
  328. package/src/testing/internal/context.ts +184 -68
  329. package/src/testing/internal/flight-client-globals.ts +30 -0
  330. package/src/testing/internal/seed-vars.ts +54 -0
  331. package/src/testing/render-handler.ts +357 -0
  332. package/src/testing/render-route.tsx +134 -115
  333. package/src/testing/run-loader.ts +140 -51
  334. package/src/testing/run-middleware.ts +59 -33
  335. package/src/testing/run-transition-when.ts +164 -0
  336. package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
  337. package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
  338. package/src/testing/vitest.ts +138 -16
  339. package/src/theme/ThemeProvider.tsx +56 -84
  340. package/src/theme/ThemeScript.tsx +7 -9
  341. package/src/theme/constants.ts +52 -13
  342. package/src/theme/index.ts +0 -7
  343. package/src/theme/theme-context.ts +1 -5
  344. package/src/theme/theme-script.ts +22 -21
  345. package/src/theme/use-theme.ts +0 -3
  346. package/src/types/boundaries.ts +0 -35
  347. package/src/types/cache-types.ts +13 -4
  348. package/src/types/error-types.ts +30 -90
  349. package/src/types/global-namespace.ts +15 -15
  350. package/src/types/handler-context.ts +45 -15
  351. package/src/types/index.ts +2 -10
  352. package/src/types/loader-types.ts +6 -3
  353. package/src/types/request-scope.ts +8 -22
  354. package/src/types/route-config.ts +20 -52
  355. package/src/types/route-entry.ts +0 -6
  356. package/src/types/segments.ts +100 -13
  357. package/src/urls/include-helper.ts +10 -12
  358. package/src/urls/include-provider.ts +71 -0
  359. package/src/urls/index.ts +2 -8
  360. package/src/urls/path-helper-types.ts +52 -14
  361. package/src/urls/path-helper.ts +5 -54
  362. package/src/urls/pattern-types.ts +36 -0
  363. package/src/urls/type-extraction.ts +76 -42
  364. package/src/urls/urls-function.ts +0 -14
  365. package/src/use-loader.tsx +0 -186
  366. package/src/vercel/index.ts +11 -0
  367. package/src/vercel/tracing.ts +88 -0
  368. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  369. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  370. package/src/vite/discovery/discover-routers.ts +34 -43
  371. package/src/vite/discovery/discovery-errors.ts +61 -0
  372. package/src/vite/discovery/prerender-collection.ts +33 -46
  373. package/src/vite/discovery/state.ts +12 -1
  374. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  375. package/src/vite/index.ts +9 -0
  376. package/src/vite/inject-client-debug.ts +88 -0
  377. package/src/vite/plugin-types.ts +143 -10
  378. package/src/vite/plugins/cjs-to-esm.ts +8 -12
  379. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  380. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  381. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  382. package/src/vite/plugins/expose-action-id.ts +2 -73
  383. package/src/vite/plugins/expose-id-utils.ts +85 -56
  384. package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
  385. package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
  386. package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
  387. package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
  388. package/src/vite/plugins/expose-internal-ids.ts +10 -1
  389. package/src/vite/plugins/performance-tracks.ts +0 -3
  390. package/src/vite/plugins/refresh-cmd.ts +1 -1
  391. package/src/vite/plugins/use-cache-transform.ts +21 -46
  392. package/src/vite/plugins/vercel-output.ts +384 -0
  393. package/src/vite/plugins/version-injector.ts +22 -27
  394. package/src/vite/plugins/version-plugin.ts +6 -66
  395. package/src/vite/plugins/virtual-entries.ts +137 -26
  396. package/src/vite/rango.ts +146 -135
  397. package/src/vite/router-discovery.ts +189 -48
  398. package/src/vite/utils/ast-handler-extract.ts +11 -20
  399. package/src/vite/utils/bundle-analysis.ts +6 -13
  400. package/src/vite/utils/client-chunks.ts +0 -6
  401. package/src/vite/utils/directive-prologue.ts +40 -0
  402. package/src/vite/utils/forward-user-plugins.ts +0 -22
  403. package/src/vite/utils/manifest-utils.ts +4 -75
  404. package/src/vite/utils/package-resolution.ts +1 -73
  405. package/src/vite/utils/prerender-utils.ts +71 -44
  406. package/src/vite/utils/shared-utils.ts +55 -37
  407. package/src/browser/react/use-client-cache.ts +0 -58
  408. package/src/browser/shallow.ts +0 -40
  409. package/src/handles/index.ts +0 -7
  410. package/src/network-error-thrower.tsx +0 -23
  411. package/src/router/middleware-cookies.ts +0 -55
@@ -10,7 +10,10 @@ import type {
10
10
  HandleData,
11
11
  StreamingToken,
12
12
  } from "./types.js";
13
- import { filterSegmentOrder } from "./react/filter-segment-order.js";
13
+ import {
14
+ filterSegmentOrder,
15
+ filterRouteSegmentIds,
16
+ } from "./react/filter-segment-order.js";
14
17
 
15
18
  // Polyfill Symbol.dispose for Safari and older browsers
16
19
  if (typeof Symbol.dispose === "undefined") {
@@ -165,6 +168,22 @@ export interface ActionHandle extends Disposable {
165
168
  startStreaming(): StreamingToken;
166
169
  /** Record segments that were revalidated */
167
170
  recordRevalidatedSegments(segmentIds: string[]): void;
171
+ /**
172
+ * Claim the subset of a location-state payload this action may write. A key
173
+ * is claimed only if no later-initiated action in the SAME cohort has already
174
+ * claimed it, so same-key concurrent writes resolve to the last-initiated
175
+ * action while distinct keys from every action survive. Recording the claim
176
+ * stops a later-settling earlier action from overwriting it. Arbitration is
177
+ * scoped to the action's cohort (its originating history entry, captured at
178
+ * startAction), so an action on one entry cannot suppress an action on
179
+ * another that happens to write the same slot.
180
+ *
181
+ * Contract: this guarantees the FINAL value is the last-initiated action's,
182
+ * not that the loser is never momentarily visible. When an earlier-initiated
183
+ * action settles first, its value is merged and observable until the
184
+ * later-initiated winner's response lands and overwrites it.
185
+ */
186
+ claimLocationState(state: Record<string, unknown>): Record<string, unknown>;
168
187
  /** Complete the action with result */
169
188
  complete(result?: unknown): void;
170
189
  /** Fail the action with error */
@@ -191,7 +210,12 @@ export interface EventController {
191
210
  abortNavigation(): void;
192
211
 
193
212
  // Action operations
194
- startAction(actionId: string, args: unknown[]): ActionHandle;
213
+ startAction(
214
+ actionId: string,
215
+ args: unknown[],
216
+ /** Originating history entry key; scopes location-state arbitration. */
217
+ cohort?: string,
218
+ ): ActionHandle;
195
219
  abortAllActions(): void;
196
220
 
197
221
  // State access
@@ -225,6 +249,14 @@ export interface EventController {
225
249
  resolvedIds?: string[],
226
250
  ): void;
227
251
  getHandleState(): HandleState;
252
+ /**
253
+ * Update ONLY `routeSegmentIds` (what `useSegments` reads) from `matched`,
254
+ * leaving `data` and `segmentOrder` (what `useHandle` collects over) untouched.
255
+ * Used while a deferred handle is resolving: the route has changed (so
256
+ * `useSegments` must reflect the new segment ids) but `useHandle` still holds
257
+ * its previous value until the deferred snapshot is applied.
258
+ */
259
+ setRouteSegmentIds(matched: string[]): void;
228
260
 
229
261
  // Params operations
230
262
  setParams(params: Record<string, string>): void;
@@ -237,10 +269,6 @@ export interface EventController {
237
269
  hadAnyConcurrentActions(): boolean;
238
270
  }
239
271
 
240
- // ============================================================================
241
- // Default States
242
- // ============================================================================
243
-
244
272
  const DEFAULT_ACTION_STATE: TrackedActionState = {
245
273
  state: "idle",
246
274
  actionId: null,
@@ -249,6 +277,13 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
249
277
  result: null,
250
278
  };
251
279
 
280
+ // Shared empty inflight-actions list. getState() hands back this exact reference
281
+ // whenever no action is inflight (the overwhelmingly common case), so the derived
282
+ // snapshot's `inflightActions` is referentially stable across notifies instead of
283
+ // a fresh [] each call. Read-only by contract (consumers only read length/spread),
284
+ // same as the shared DEFAULT_ACTION_STATE.
285
+ const EMPTY_INFLIGHT_ACTIONS: InflightAction[] = [];
286
+
252
287
  /**
253
288
  * Check if a subscription ID matches an action's full ID.
254
289
  *
@@ -261,16 +296,12 @@ function matchesActionId(
261
296
  entryActionId: string,
262
297
  ): boolean {
263
298
  if (subscriptionId.includes("#")) {
264
- // Full ID: exact match
265
299
  return subscriptionId === entryActionId;
266
300
  }
267
- // Action name only: suffix match (matches "anything#actionName")
268
301
  return entryActionId.endsWith(`#${subscriptionId}`);
269
302
  }
270
303
 
271
- // Coalesce rapid notifications into one microtask-deferred fan-out; the
272
- // setTimeout(0) batching prevents render storms. Each notifier owns its timer
273
- // so listener kinds coalesce independently.
304
+ // Batch rapid notifications into one microtask to prevent render storms
274
305
  function makeDebouncedNotifier(listeners: Set<() => void>): () => void {
275
306
  let timeout: ReturnType<typeof setTimeout> | null = null;
276
307
  return () => {
@@ -282,13 +313,6 @@ function makeDebouncedNotifier(listeners: Set<() => void>): () => void {
282
313
  };
283
314
  }
284
315
 
285
- // ============================================================================
286
- // Implementation
287
- // ============================================================================
288
-
289
- /**
290
- * Configuration for creating an event controller
291
- */
292
316
  export interface EventControllerConfig {
293
317
  initialLocation?: NavigationLocation;
294
318
  }
@@ -306,51 +330,68 @@ export interface EventControllerConfig {
306
330
  export function createEventController(
307
331
  config?: EventControllerConfig,
308
332
  ): EventController {
309
- // ========================================================================
310
- // Source of Truth
311
- // ========================================================================
312
-
313
- // Current navigation in progress (null = idle)
314
333
  let currentNavigation: NavigationEntry | null = null;
315
334
 
316
- // All in-flight actions (keyed by unique instance ID)
317
335
  const inflightActions = new Map<string, ActionEntry>();
318
336
 
319
- // Committed location (updated when navigation completes)
320
337
  let location: NavigationLocation =
321
338
  config?.initialLocation ??
322
339
  (typeof window !== "undefined"
323
340
  ? new URL(window.location.href)
324
341
  : new URL("/", "http://localhost"));
325
342
 
326
- // Track if any concurrent actions occurred (for consolidation)
327
343
  let hadAnyConcurrentActions = false;
328
344
 
329
- // Track segments revalidated by concurrent actions
330
345
  const concurrentRevalidatedSegments = new Set<string>();
331
346
 
332
- // Active streaming count (independent of navigation/action lifecycle)
347
+ // Monotonic dispatch counter: every startAction() takes the next value
348
+ // (private), so a larger sequence means the action was initiated later.
349
+ let actionDispatchSeq = 0;
350
+
351
+ // Concurrent location-state arbitration, scoped PER COHORT (history entry).
352
+ // Each cohort owns a slotKey->winningDispatchSeq map plus a refcount of its
353
+ // inflight actions; claimLocationState() consults its own cohort's map so
354
+ // same-key writes within one entry resolve to the last-initiated action
355
+ // regardless of settle order, while actions on different entries never
356
+ // compete. A cohort's map is freed once its LAST action's cleanup runs — the
357
+ // same brief post-settle grace (doSettle's 100ms timer) as other action
358
+ // teardown, not when every action everywhere settles — so a long-running
359
+ // action in one cohort can never retain arbitration keys from other cohorts
360
+ // that have since drained. It is never cleared in clearConsolidation, which
361
+ // fires
362
+ // per-action on divert/error and would let a later-settling earlier action
363
+ // wrongly reclaim a key a sibling already won.
364
+ const cohortArbitration = new Map<
365
+ string,
366
+ { keySeq: Map<string, number>; inflight: number }
367
+ >();
368
+
333
369
  let activeStreamCount = 0;
334
370
 
335
- // Handle data from RSC payload
336
371
  let handleData: HandleData = {};
337
372
  let handleSegmentOrder: string[] = [];
338
373
  let routeSegmentIds: string[] = [];
339
374
 
340
- // Merged route params from current match
341
375
  let routeParams: Record<string, string> = {};
342
376
 
343
- // ========================================================================
344
- // Listeners
345
- // ========================================================================
346
-
347
377
  const stateListeners = new Set<StateListener>();
348
378
  const actionListeners = new Map<string, Set<ActionStateListener>>();
349
379
  const handleListeners = new Set<HandleListener>();
350
380
 
351
- const notify = makeDebouncedNotifier(stateListeners);
381
+ const notifyStateListeners = makeDebouncedNotifier(stateListeners);
382
+
383
+ // Memoized derived snapshot. Every state mutation already funnels through
384
+ // notify(), so invalidating here (synchronously, before the debounced fire)
385
+ // means a getState() call between two mutations reuses the same object — an
386
+ // unchanged state returns the SAME reference — while any real change recomputes
387
+ // on the next read. Kept null when dirty.
388
+ let cachedDerivedState: DerivedNavigationState | null = null;
389
+
390
+ function notify(): void {
391
+ cachedDerivedState = null;
392
+ notifyStateListeners();
393
+ }
352
394
 
353
- // Debounce per-action notifications
354
395
  const actionNotifyTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
355
396
 
356
397
  function notifyAction(actionId: string) {
@@ -362,8 +403,6 @@ export function createEventController(
362
403
  actionId,
363
404
  setTimeout(() => {
364
405
  actionNotifyTimeouts.delete(actionId);
365
- // Notify all listeners whose subscription ID matches this action
366
- // This includes exact matches and suffix matches (e.g., "addToCart" matches "hash#addToCart")
367
406
  for (const [subscriptionId, listeners] of actionListeners) {
368
407
  if (matchesActionId(subscriptionId, actionId)) {
369
408
  const state = getActionState(subscriptionId);
@@ -376,33 +415,32 @@ export function createEventController(
376
415
 
377
416
  const notifyHandles = makeDebouncedNotifier(handleListeners);
378
417
 
379
- // ========================================================================
380
- // Derived State
381
- // ========================================================================
382
-
383
418
  function getState(): DerivedNavigationState {
384
- // Build inflight actions list (for compatibility with existing API)
385
- const inflightActionsList: InflightAction[] = [...inflightActions.values()]
386
- .filter((a) => a.phase !== "settling")
387
- .map((a) => ({
388
- id: a.id,
389
- actionId: a.actionId,
390
- payload: a.payload,
391
- startedAt: a.startedAt,
392
- }));
393
-
394
- // State: loading if navigation OR actions are in progress
395
- // Background revalidations (skipLoadingState) don't affect visible state
419
+ if (cachedDerivedState) return cachedDerivedState;
420
+
421
+ // Skip the spread/filter/map entirely when idle — the common case — and hand
422
+ // back the shared frozen empty list for referential stability.
423
+ const inflightActionsList: InflightAction[] =
424
+ inflightActions.size === 0
425
+ ? EMPTY_INFLIGHT_ACTIONS
426
+ : [...inflightActions.values()]
427
+ .filter((a) => a.phase !== "settling")
428
+ .map((a) => ({
429
+ id: a.id,
430
+ actionId: a.actionId,
431
+ payload: a.payload,
432
+ startedAt: a.startedAt,
433
+ }));
434
+
396
435
  const hasActiveActions = inflightActionsList.length > 0;
397
436
  const isVisibleNavigation =
398
437
  currentNavigation !== null &&
399
438
  !currentNavigation.options?.skipLoadingState;
400
439
  const state = isVisibleNavigation || hasActiveActions ? "loading" : "idle";
401
440
 
402
- // Streaming: true if any active streams (navigation or action) or loading
403
441
  const isStreaming = activeStreamCount > 0 || state === "loading";
404
442
 
405
- return {
443
+ cachedDerivedState = {
406
444
  state,
407
445
  isStreaming,
408
446
  // True when a navigation is active (fetching or streaming, before
@@ -418,11 +456,14 @@ export function createEventController(
418
456
  : null,
419
457
  inflightActions: inflightActionsList,
420
458
  };
459
+ return cachedDerivedState;
421
460
  }
422
461
 
423
462
  function getActionState(actionId: string): TrackedActionState {
424
- // Prefer the most-recent non-settling entry; fall back to most-recent
425
- // settling so a just-settled action's result/error stays readable.
463
+ // Nothing inflight skip building/scanning the list and return the shared
464
+ // idle snapshot (the same reference use-action falls back to).
465
+ if (inflightActions.size === 0) return DEFAULT_ACTION_STATE;
466
+
426
467
  const entry = [...inflightActions.values()]
427
468
  .filter((a) => matchesActionId(actionId, a.actionId))
428
469
  .reduce<ActionEntry | undefined>((best, a) => {
@@ -437,7 +478,6 @@ export function createEventController(
437
478
  return { ...DEFAULT_ACTION_STATE };
438
479
  }
439
480
 
440
- // Derive state from phase
441
481
  let state: ActionLifecycleState;
442
482
  switch (entry.phase) {
443
483
  case "fetching":
@@ -552,10 +592,29 @@ export function createEventController(
552
592
  // Action Operations
553
593
  // ========================================================================
554
594
 
555
- function startAction(actionId: string, args: unknown[]): ActionHandle {
595
+ function startAction(
596
+ actionId: string,
597
+ args: unknown[],
598
+ cohort?: string,
599
+ ): ActionHandle {
556
600
  const id = `${actionId}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
601
+ // Private to this handle: never exposed on the returned ActionHandle.
602
+ const dispatchSeq = actionDispatchSeq++;
557
603
  const abort = new AbortController();
558
604
 
605
+ // Register this action under its cohort (originating history entry). The
606
+ // cohort's arbitration is created on first use and freed when its last
607
+ // action settles (see doSettle). Keyless entries share the "" cohort.
608
+ const cohortId = cohort ?? "";
609
+ let arb = cohortArbitration.get(cohortId);
610
+ if (!arb) {
611
+ arb = { keySeq: new Map<string, number>(), inflight: 0 };
612
+ cohortArbitration.set(cohortId, arb);
613
+ }
614
+ // const so the captured reference stays non-undefined inside doSettle.
615
+ const arbitration = arb;
616
+ arbitration.inflight++;
617
+
559
618
  // Track if this action started while others were pending (concurrent)
560
619
  const hadConcurrent = inflightActions.size > 0;
561
620
  if (hadConcurrent) {
@@ -579,11 +638,28 @@ export function createEventController(
579
638
  let settled = false;
580
639
  let streamingEnded = false;
581
640
  let actionCompleted = false;
641
+ let cohortReleased = false;
582
642
  let pendingResult:
583
643
  | { type: "success"; value?: unknown }
584
644
  | { type: "error"; value: unknown }
585
645
  | null = null;
586
646
 
647
+ // Release this action's hold on its cohort arbitration exactly once: drop
648
+ // the refcount and, only if the map still points at THIS arbitration object,
649
+ // delete it. A newer generation may have replaced it (e.g. abortAllActions
650
+ // cleared the map and a fresh action recreated the same cohort id), so a
651
+ // stale settlement must never delete the newer one by id.
652
+ function releaseCohort() {
653
+ if (cohortReleased) return;
654
+ cohortReleased = true;
655
+ if (
656
+ --arbitration.inflight <= 0 &&
657
+ cohortArbitration.get(cohortId) === arbitration
658
+ ) {
659
+ cohortArbitration.delete(cohortId);
660
+ }
661
+ }
662
+
587
663
  function doSettle() {
588
664
  if (settled) return;
589
665
  settled = true;
@@ -591,6 +667,9 @@ export function createEventController(
591
667
  // Cleanup after brief delay (allow useAction to read result)
592
668
  setTimeout(() => {
593
669
  inflightActions.delete(id);
670
+ // Free this cohort's arbitration once its last action has settled, so
671
+ // a long-running action elsewhere cannot pin keys from a drained entry.
672
+ releaseCohort();
594
673
  // Check for consolidation
595
674
  if (inflightActions.size === 0) {
596
675
  // All actions done - reset tracking
@@ -667,6 +746,26 @@ export function createEventController(
667
746
  segmentIds.forEach((id) => concurrentRevalidatedSegments.add(id));
668
747
  },
669
748
 
749
+ claimLocationState(state: Record<string, unknown>) {
750
+ const winning: Record<string, unknown> = {};
751
+ // Arbitrate against this action's OWN captured arbitration object, not a
752
+ // live map lookup: a concurrent map clear/replace (abortAllActions, a
753
+ // stale settlement) must not make this action silently stop recording
754
+ // and accept every key.
755
+ const keySeq = arbitration.keySeq;
756
+ for (const key of Object.keys(state)) {
757
+ const prevSeq = keySeq.get(key);
758
+ // Strictly-greater: a later-initiated action wins a key over an
759
+ // earlier one in the same cohort regardless of arrival order. Equal
760
+ // cannot happen (dispatchSeq is unique per action).
761
+ if (prevSeq === undefined || dispatchSeq > prevSeq) {
762
+ keySeq.set(key, dispatchSeq);
763
+ winning[key] = state[key];
764
+ }
765
+ }
766
+ return winning;
767
+ },
768
+
670
769
  complete(result?: unknown) {
671
770
  settleWith({ type: "success", value: result });
672
771
  },
@@ -688,6 +787,10 @@ export function createEventController(
688
787
  [Symbol.dispose]() {
689
788
  // If aborted, another navigation/error took over - don't touch state
690
789
  if (abort.signal.aborted) {
790
+ // Aborted actions skip doSettle, so release the cohort hold here to
791
+ // keep the per-cohort refcount balanced (no leak when an action is
792
+ // aborted individually rather than via abortAllActions).
793
+ releaseCohort();
691
794
  inflightActions.delete(id);
692
795
  notify();
693
796
  notifyAction(actionId);
@@ -722,6 +825,7 @@ export function createEventController(
722
825
  }
723
826
  hadAnyConcurrentActions = false;
724
827
  concurrentRevalidatedSegments.clear();
828
+ cohortArbitration.clear();
725
829
  notify();
726
830
  // Notify all action listeners directly by subscription ID.
727
831
  // actionListeners keys are subscription IDs (possibly short names like
@@ -748,9 +852,7 @@ export function createEventController(
748
852
  const newSegmentOrder = filterSegmentOrder(rawMatched);
749
853
  // Separate list for useSegments(): "layouts and routes only" — strip
750
854
  // parallels (".@") and loader sub-ids (D digit) without reordering.
751
- const newRouteSegmentIds = rawMatched.filter(
752
- (id) => !id.includes(".@") && !/D\d+\./.test(id),
753
- );
855
+ const newRouteSegmentIds = filterRouteSegmentIds(rawMatched);
754
856
 
755
857
  if (isPartial && newSegmentOrder.length > 0) {
756
858
  // Partial update: merge new data with existing
@@ -797,6 +899,18 @@ export function createEventController(
797
899
  };
798
900
  }
799
901
 
902
+ function setRouteSegmentIds(matched: string[]): void {
903
+ const next = filterRouteSegmentIds(matched);
904
+ if (
905
+ next.length === routeSegmentIds.length &&
906
+ next.every((id, i) => id === routeSegmentIds[i])
907
+ ) {
908
+ return;
909
+ }
910
+ routeSegmentIds = next;
911
+ notifyHandles();
912
+ }
913
+
800
914
  // ========================================================================
801
915
  // Subscriptions
802
916
  // ========================================================================
@@ -865,6 +979,7 @@ export function createEventController(
865
979
  // Handles
866
980
  setHandleData,
867
981
  getHandleState,
982
+ setRouteSegmentIds,
868
983
 
869
984
  // Params
870
985
  setParams,
@@ -881,40 +996,3 @@ export function createEventController(
881
996
  hadAnyConcurrentActions: () => hadAnyConcurrentActions,
882
997
  };
883
998
  }
884
-
885
- // ============================================================================
886
- // Singleton
887
- // ============================================================================
888
-
889
- let controllerInstance: EventController | null = null;
890
-
891
- /**
892
- * Initialize the global event controller
893
- */
894
- export function initEventController(
895
- config?: EventControllerConfig,
896
- ): EventController {
897
- if (!controllerInstance) {
898
- controllerInstance = createEventController(config);
899
- }
900
- return controllerInstance;
901
- }
902
-
903
- /**
904
- * Get the global event controller
905
- */
906
- export function getEventController(): EventController {
907
- if (!controllerInstance) {
908
- throw new Error(
909
- "Event controller not initialized. Call initEventController first.",
910
- );
911
- }
912
- return controllerInstance;
913
- }
914
-
915
- /**
916
- * Reset the controller instance (for testing)
917
- */
918
- export function resetEventController(): void {
919
- controllerInstance = null;
920
- }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Client seat of `invalidateClientCache()` (the `default` export condition).
3
+ *
4
+ * Makes the current client behave as if a server action had just completed:
5
+ * the history cache is marked stale (SWR), the prefetch map is flushed, the
6
+ * state rotates, and sibling tabs are broadcast to — the same
7
+ * `markCacheAsStaleAndBroadcast()` path the server-action bridge uses. This is
8
+ * the gentler mark-stale (not hard-clear) behavior, so Back renders the cached
9
+ * entry instantly and revalidates.
10
+ */
11
+
12
+ import { getRegisteredStore } from "./navigation-store-handle.js";
13
+ import { clearPrefetchCache } from "./prefetch/cache.js";
14
+
15
+ export function invalidateClientCache(): void {
16
+ if (typeof document === "undefined") {
17
+ // SSR pass of a client component also resolves the default condition. A
18
+ // render-time call must not take down the page; no-op with a dev warning.
19
+ if (process.env.NODE_ENV !== "production") {
20
+ console.warn(
21
+ "[rango] invalidateClientCache() was called during a server render; " +
22
+ "it is a no-op outside the browser.",
23
+ );
24
+ }
25
+ return;
26
+ }
27
+
28
+ const store = getRegisteredStore();
29
+ if (store) {
30
+ store.markCacheAsStaleAndBroadcast();
31
+ return;
32
+ }
33
+
34
+ // Pre-boot: no store registered yet. clearPrefetchCache() (which rotates the
35
+ // state) is complete at this point — there is no history cache to mark and no
36
+ // sibling state worth broadcasting.
37
+ clearPrefetchCache();
38
+ }
39
+
40
+ /**
41
+ * Client no-op for `keepClientCache()`. It is a server action directive (the
42
+ * `react-server` condition sets a response header the action bridge reads);
43
+ * there is nothing to suppress from the client side.
44
+ */
45
+ export function keepClientCache(): void {
46
+ if (process.env.NODE_ENV !== "production") {
47
+ console.warn(
48
+ "[rango] keepClientCache() has no effect on the client; it is a server " +
49
+ "action directive. Call it from inside a server action.",
50
+ );
51
+ }
52
+ }
@@ -1,5 +1,15 @@
1
1
  import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
2
2
 
3
+ /**
4
+ * Module-level debug flag for guarding call sites whose debug arguments do
5
+ * non-trivial work (joins, maps, spreads, object literals). Because the Vite
6
+ * transform folds INTERNAL_RANGO_DEBUG to a literal, an `if (IS_BROWSER_DEBUG)`
7
+ * guard is minifier-DCE-able — the whole block drops from the production bundle.
8
+ * Prefer this over isBrowserDebugEnabled() at hot call sites: a const guard folds
9
+ * where a function call may not.
10
+ */
11
+ export const IS_BROWSER_DEBUG: boolean = INTERNAL_RANGO_DEBUG;
12
+
3
13
  interface BrowserLogContext {
4
14
  requestId: string;
5
15
  txId: string;
@@ -53,3 +63,21 @@ export function debugLog(msg: string, ...args: unknown[]): void {
53
63
  console.log(msg, ...args);
54
64
  }
55
65
  }
66
+
67
+ /**
68
+ * Boot-sequence debug log: one line per initial-document step (flight decode,
69
+ * handle stream, bridge wiring, initial tree build, hydration commit), each
70
+ * stamped with performance.now() so the gap BEFORE hydrateRoot is visible.
71
+ * The initial document path was otherwise silent — FE debug only started
72
+ * talking at the first soft navigation, so a boot stall (e.g. an await that
73
+ * holds initBrowserApp, and with it hydrateRoot) was invisible.
74
+ */
75
+ export function bootLog(step: string, details?: Record<string, unknown>): void {
76
+ if (!INTERNAL_RANGO_DEBUG) return;
77
+ const prefix = `[Browser][boot] ${step} @ ${Math.round(performance.now())}ms`;
78
+ if (details) {
79
+ console.log(prefix, details);
80
+ return;
81
+ }
82
+ console.log(prefix);
83
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ResolvedSegment } from "./types.js";
2
- import { debugLog } from "./logging.js";
2
+ import { debugLog, IS_BROWSER_DEBUG } from "./logging.js";
3
3
 
4
4
  /**
5
5
  * Merge partial loader data from server with cached loader data.
@@ -19,9 +19,11 @@ export function mergeSegmentLoaders(
19
19
  const serverLoaderIds = fromServer.loaderIds || [];
20
20
  const cachedLoaderIds = fromCache.loaderIds || [];
21
21
 
22
- debugLog(
23
- `[Browser] Merging partial loaders: server has ${serverLoaderIds.join(", ")}, cache has ${cachedLoaderIds.join(", ")}`,
24
- );
22
+ if (IS_BROWSER_DEBUG) {
23
+ debugLog(
24
+ `[Browser] Merging partial loaders: server has ${serverLoaderIds.join(", ")}, cache has ${cachedLoaderIds.join(", ")}`,
25
+ );
26
+ }
25
27
 
26
28
  return {
27
29
  ...fromCache,