@rangojs/router 0.0.0-experimental.97 → 0.0.0-experimental.98914650

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 (356) hide show
  1. package/README.md +24 -9
  2. package/dist/bin/rango.js +157 -63
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +1584 -639
  5. package/package.json +71 -21
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/breadcrumbs/SKILL.md +60 -0
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +222 -30
  10. package/skills/caching/SKILL.md +263 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/css/SKILL.md +76 -0
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +3 -1
  15. package/skills/hooks/SKILL.md +235 -28
  16. package/skills/host-router/SKILL.md +122 -22
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +29 -5
  19. package/skills/layout/SKILL.md +13 -9
  20. package/skills/links/SKILL.md +173 -17
  21. package/skills/loader/SKILL.md +170 -23
  22. package/skills/middleware/SKILL.md +16 -10
  23. package/skills/migrate-nextjs/SKILL.md +38 -16
  24. package/skills/mime-routes/SKILL.md +27 -0
  25. package/skills/observability/SKILL.md +137 -0
  26. package/skills/parallel/SKILL.md +11 -7
  27. package/skills/prerender/SKILL.md +14 -33
  28. package/skills/rango/SKILL.md +250 -25
  29. package/skills/react-compiler/SKILL.md +168 -0
  30. package/skills/response-routes/SKILL.md +114 -47
  31. package/skills/route/SKILL.md +42 -5
  32. package/skills/router-setup/SKILL.md +3 -3
  33. package/skills/server-actions/SKILL.md +78 -42
  34. package/skills/tailwind/SKILL.md +27 -3
  35. package/skills/testing/SKILL.md +129 -0
  36. package/skills/testing/bindings.md +89 -0
  37. package/skills/testing/cache-prerender.md +124 -0
  38. package/skills/testing/client-components.md +122 -0
  39. package/skills/testing/e2e-parity.md +125 -0
  40. package/skills/testing/flight.md +92 -0
  41. package/skills/testing/handles.md +129 -0
  42. package/skills/testing/loader.md +128 -0
  43. package/skills/testing/middleware.md +99 -0
  44. package/skills/testing/render-handler.md +121 -0
  45. package/skills/testing/response-routes.md +95 -0
  46. package/skills/testing/reverse-and-types.md +84 -0
  47. package/skills/testing/server-actions.md +107 -0
  48. package/skills/testing/server-tree.md +128 -0
  49. package/skills/testing/setup.md +120 -0
  50. package/skills/typesafety/SKILL.md +316 -26
  51. package/skills/use-cache/SKILL.md +36 -5
  52. package/skills/vercel/SKILL.md +107 -0
  53. package/skills/view-transitions/SKILL.md +294 -0
  54. package/src/__augment-tests__/augment.ts +81 -0
  55. package/src/__augment-tests__/augmented.check.ts +116 -0
  56. package/src/__internal.ts +0 -65
  57. package/src/browser/action-coordinator.ts +53 -36
  58. package/src/browser/action-fence.ts +47 -0
  59. package/src/browser/app-shell.ts +14 -27
  60. package/src/browser/cookie-name.ts +140 -0
  61. package/src/browser/event-controller.ts +37 -143
  62. package/src/browser/history-state.ts +21 -0
  63. package/src/browser/index.ts +3 -3
  64. package/src/browser/invalidate-client-cache.ts +52 -0
  65. package/src/browser/navigation-bridge.ts +30 -59
  66. package/src/browser/navigation-client.ts +96 -84
  67. package/src/browser/navigation-store-handle.ts +38 -0
  68. package/src/browser/navigation-store.ts +32 -82
  69. package/src/browser/navigation-transaction.ts +9 -59
  70. package/src/browser/partial-update.ts +60 -127
  71. package/src/browser/prefetch/cache.ts +82 -72
  72. package/src/browser/prefetch/fetch.ts +108 -33
  73. package/src/browser/prefetch/queue.ts +6 -3
  74. package/src/browser/rango-state.ts +157 -115
  75. package/src/browser/react/Link.tsx +0 -2
  76. package/src/browser/react/NavigationProvider.tsx +41 -48
  77. package/src/browser/react/ScrollRestoration.tsx +10 -6
  78. package/src/browser/react/filter-segment-order.ts +0 -2
  79. package/src/browser/react/index.ts +0 -48
  80. package/src/browser/react/location-state-shared.ts +166 -8
  81. package/src/browser/react/location-state.ts +39 -14
  82. package/src/browser/react/use-action.ts +6 -15
  83. package/src/browser/react/use-handle.ts +17 -14
  84. package/src/browser/react/use-link-status.ts +0 -4
  85. package/src/browser/react/use-navigation.ts +0 -3
  86. package/src/browser/react/use-params.ts +11 -11
  87. package/src/browser/react/use-reverse.ts +106 -0
  88. package/src/browser/react/use-router.ts +20 -5
  89. package/src/browser/react/use-search-params.ts +0 -5
  90. package/src/browser/react/use-segments.ts +0 -13
  91. package/src/browser/response-adapter.ts +52 -1
  92. package/src/browser/rsc-router.tsx +70 -34
  93. package/src/browser/scroll-restoration.ts +22 -14
  94. package/src/browser/segment-structure-assert.ts +2 -2
  95. package/src/browser/server-action-bridge.ts +168 -44
  96. package/src/browser/types.ts +36 -21
  97. package/src/browser/validate-redirect-origin.ts +43 -16
  98. package/src/build/collect-fallback-refs.ts +107 -0
  99. package/src/build/generate-manifest.ts +60 -35
  100. package/src/build/generate-route-types.ts +3 -0
  101. package/src/build/index.ts +8 -2
  102. package/src/build/prefix-tree-utils.ts +123 -0
  103. package/src/build/route-trie.ts +89 -10
  104. package/src/build/route-types/codegen.ts +4 -4
  105. package/src/build/route-types/include-resolution.ts +1 -1
  106. package/src/build/route-types/param-extraction.ts +6 -3
  107. package/src/build/route-types/per-module-writer.ts +7 -4
  108. package/src/build/route-types/router-processing.ts +122 -22
  109. package/src/build/route-types/scan-filter.ts +1 -1
  110. package/src/build/route-types/source-scan.ts +118 -0
  111. package/src/build/runtime-discovery.ts +9 -20
  112. package/src/cache/cache-error.ts +104 -0
  113. package/src/cache/cache-policy.ts +68 -28
  114. package/src/cache/cache-runtime.ts +134 -32
  115. package/src/cache/cache-scope.ts +100 -74
  116. package/src/cache/cache-tag.ts +98 -0
  117. package/src/cache/cf/cf-cache-store.ts +2255 -238
  118. package/src/cache/cf/index.ts +6 -16
  119. package/src/cache/document-cache.ts +61 -20
  120. package/src/cache/handle-snapshot.ts +63 -0
  121. package/src/cache/index.ts +22 -20
  122. package/src/cache/memory-segment-store.ts +136 -37
  123. package/src/cache/profile-registry.ts +6 -30
  124. package/src/cache/read-through-swr.ts +41 -11
  125. package/src/cache/segment-codec.ts +0 -16
  126. package/src/cache/tag-invalidation.ts +230 -0
  127. package/src/cache/types.ts +33 -100
  128. package/src/cache/vercel/index.ts +11 -0
  129. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  130. package/src/client.rsc.tsx +6 -21
  131. package/src/client.tsx +25 -61
  132. package/src/component-utils.ts +19 -0
  133. package/src/context-var.ts +17 -5
  134. package/src/decode-loader-results.ts +36 -0
  135. package/src/defer.ts +196 -0
  136. package/src/deps/ssr.ts +0 -1
  137. package/src/errors.ts +30 -4
  138. package/src/handle.ts +31 -23
  139. package/src/handles/MetaTags.tsx +0 -14
  140. package/src/handles/breadcrumbs.ts +16 -5
  141. package/src/handles/meta.ts +0 -39
  142. package/src/host/cookie-handler.ts +0 -36
  143. package/src/host/errors.ts +0 -24
  144. package/src/host/index.ts +8 -2
  145. package/src/host/pattern-matcher.ts +7 -50
  146. package/src/host/router.ts +107 -99
  147. package/src/host/testing.ts +40 -27
  148. package/src/host/types.ts +37 -4
  149. package/src/host/utils.ts +1 -1
  150. package/src/href-client.ts +137 -22
  151. package/src/index.rsc.ts +63 -9
  152. package/src/index.ts +64 -9
  153. package/src/internal-debug.ts +2 -4
  154. package/src/loader-store.ts +500 -0
  155. package/src/loader.rsc.ts +20 -13
  156. package/src/loader.ts +12 -11
  157. package/src/missing-id-error.ts +68 -0
  158. package/src/network-error-thrower.tsx +1 -6
  159. package/src/outlet-provider.tsx +1 -5
  160. package/src/prerender/param-hash.ts +10 -11
  161. package/src/prerender/store.ts +32 -37
  162. package/src/prerender.ts +61 -6
  163. package/src/redirect-origin.ts +100 -0
  164. package/src/response-utils.ts +9 -0
  165. package/src/reverse.ts +65 -40
  166. package/src/root-error-boundary.tsx +1 -19
  167. package/src/route-content-wrapper.tsx +7 -72
  168. package/src/route-definition/dsl-helpers.ts +244 -281
  169. package/src/route-definition/helper-factories.ts +29 -139
  170. package/src/route-definition/helpers-types.ts +40 -17
  171. package/src/route-definition/redirect.ts +43 -9
  172. package/src/route-definition/resolve-handler-use.ts +6 -0
  173. package/src/route-definition/use-item-types.ts +32 -0
  174. package/src/route-map-builder.ts +0 -16
  175. package/src/route-types.ts +19 -41
  176. package/src/router/basename.ts +14 -0
  177. package/src/router/content-negotiation.ts +15 -15
  178. package/src/router/error-handling.ts +13 -17
  179. package/src/router/find-match.ts +44 -23
  180. package/src/router/handler-context.ts +4 -41
  181. package/src/router/intercept-resolution.ts +14 -19
  182. package/src/router/lazy-includes.ts +9 -46
  183. package/src/router/loader-resolution.ts +91 -46
  184. package/src/router/logging.ts +0 -6
  185. package/src/router/manifest.ts +18 -29
  186. package/src/router/match-api.ts +0 -20
  187. package/src/router/match-context.ts +0 -22
  188. package/src/router/match-handlers.ts +57 -58
  189. package/src/router/match-middleware/background-revalidation.ts +0 -7
  190. package/src/router/match-middleware/cache-lookup.ts +150 -271
  191. package/src/router/match-middleware/cache-store.ts +3 -33
  192. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  193. package/src/router/match-middleware/segment-resolution.ts +0 -22
  194. package/src/router/match-pipelines.ts +1 -42
  195. package/src/router/match-result.ts +31 -80
  196. package/src/router/metrics.ts +0 -34
  197. package/src/router/middleware-types.ts +5 -112
  198. package/src/router/middleware.ts +118 -133
  199. package/src/router/navigation-snapshot.ts +0 -51
  200. package/src/router/params-util.ts +23 -0
  201. package/src/router/pattern-matching.ts +62 -67
  202. package/src/router/prerender-match.ts +99 -63
  203. package/src/router/preview-match.ts +3 -1
  204. package/src/router/request-classification.ts +28 -62
  205. package/src/router/revalidation.ts +50 -56
  206. package/src/router/route-snapshot.ts +0 -1
  207. package/src/router/router-context.ts +0 -27
  208. package/src/router/router-interfaces.ts +68 -35
  209. package/src/router/router-options.ts +55 -1
  210. package/src/router/router-registry.ts +2 -5
  211. package/src/router/segment-resolution/fresh.ts +44 -63
  212. package/src/router/segment-resolution/helpers.ts +34 -0
  213. package/src/router/segment-resolution/loader-cache.ts +40 -37
  214. package/src/router/segment-resolution/revalidation.ts +203 -285
  215. package/src/router/segment-resolution/static-store.ts +19 -5
  216. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  217. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  218. package/src/router/segment-resolution.ts +4 -1
  219. package/src/router/segment-wrappers.ts +0 -3
  220. package/src/router/state-cookie-name.ts +33 -0
  221. package/src/router/substitute-pattern-params.ts +56 -0
  222. package/src/router/telemetry-otel.ts +0 -20
  223. package/src/router/telemetry.ts +96 -19
  224. package/src/router/timeout.ts +0 -20
  225. package/src/router/trie-matching.ts +87 -48
  226. package/src/router/types.ts +9 -63
  227. package/src/router/url-params.ts +0 -5
  228. package/src/router.ts +80 -41
  229. package/src/rsc/handler-context.ts +3 -2
  230. package/src/rsc/handler.ts +83 -78
  231. package/src/rsc/helpers.ts +93 -5
  232. package/src/rsc/index.ts +1 -1
  233. package/src/rsc/json-route-result.ts +38 -0
  234. package/src/rsc/manifest-init.ts +28 -41
  235. package/src/rsc/origin-guard.ts +39 -25
  236. package/src/rsc/progressive-enhancement.ts +12 -1
  237. package/src/rsc/redirect-guard.ts +99 -0
  238. package/src/rsc/response-error.ts +79 -12
  239. package/src/rsc/response-route-handler.ts +76 -62
  240. package/src/rsc/rsc-rendering.ts +41 -60
  241. package/src/rsc/runtime-warnings.ts +23 -10
  242. package/src/rsc/server-action.ts +62 -67
  243. package/src/rsc/ssr-setup.ts +16 -0
  244. package/src/rsc/types.ts +10 -5
  245. package/src/runtime-env.ts +18 -0
  246. package/src/search-params.ts +4 -20
  247. package/src/segment-loader-promise.ts +14 -2
  248. package/src/segment-system.tsx +199 -142
  249. package/src/serialize.ts +243 -0
  250. package/src/server/context.ts +150 -51
  251. package/src/server/cookie-store.ts +80 -5
  252. package/src/server/handle-store.ts +7 -24
  253. package/src/server/loader-registry.ts +5 -24
  254. package/src/server/request-context.ts +165 -87
  255. package/src/ssr/index.tsx +14 -14
  256. package/src/static-handler.ts +10 -13
  257. package/src/testing/cache-status.ts +162 -0
  258. package/src/testing/collect-handle.ts +40 -0
  259. package/src/testing/dispatch.ts +618 -0
  260. package/src/testing/dom.entry.ts +22 -0
  261. package/src/testing/e2e/fixture.ts +188 -0
  262. package/src/testing/e2e/index.ts +128 -0
  263. package/src/testing/e2e/matchers.ts +35 -0
  264. package/src/testing/e2e/page-helpers.ts +272 -0
  265. package/src/testing/e2e/parity.ts +387 -0
  266. package/src/testing/e2e/server.ts +195 -0
  267. package/src/testing/flight-matchers.ts +97 -0
  268. package/src/testing/flight-normalize.ts +11 -0
  269. package/src/testing/flight-runtime.d.ts +57 -0
  270. package/src/testing/flight-tree.ts +682 -0
  271. package/src/testing/flight.entry.ts +52 -0
  272. package/src/testing/flight.ts +232 -0
  273. package/src/testing/generated-routes.ts +183 -0
  274. package/src/testing/index.ts +99 -0
  275. package/src/testing/internal/context.ts +348 -0
  276. package/src/testing/internal/flight-client-globals.ts +30 -0
  277. package/src/testing/internal/seed-vars.ts +54 -0
  278. package/src/testing/render-handler.ts +330 -0
  279. package/src/testing/render-route.tsx +566 -0
  280. package/src/testing/run-loader.ts +378 -0
  281. package/src/testing/run-middleware.ts +205 -0
  282. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  283. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  284. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  285. package/src/testing/vitest-stubs/version.ts +5 -0
  286. package/src/testing/vitest.ts +305 -0
  287. package/src/theme/ThemeProvider.tsx +0 -52
  288. package/src/theme/ThemeScript.tsx +0 -6
  289. package/src/theme/constants.ts +0 -12
  290. package/src/theme/index.ts +0 -7
  291. package/src/theme/theme-context.ts +1 -5
  292. package/src/theme/theme-script.ts +0 -14
  293. package/src/theme/use-theme.ts +0 -3
  294. package/src/types/boundaries.ts +0 -35
  295. package/src/types/cache-types.ts +13 -4
  296. package/src/types/error-types.ts +30 -90
  297. package/src/types/global-namespace.ts +54 -41
  298. package/src/types/handler-context.ts +97 -22
  299. package/src/types/index.ts +1 -10
  300. package/src/types/loader-types.ts +6 -3
  301. package/src/types/request-scope.ts +0 -19
  302. package/src/types/route-config.ts +6 -50
  303. package/src/types/route-entry.ts +0 -6
  304. package/src/types/segments.ts +18 -14
  305. package/src/urls/include-helper.ts +9 -56
  306. package/src/urls/index.ts +1 -11
  307. package/src/urls/path-helper-types.ts +19 -5
  308. package/src/urls/path-helper.ts +17 -106
  309. package/src/urls/pattern-types.ts +36 -19
  310. package/src/urls/response-types.ts +20 -19
  311. package/src/urls/type-extraction.ts +58 -139
  312. package/src/urls/urls-function.ts +1 -18
  313. package/src/use-loader.tsx +292 -107
  314. package/src/vite/debug.ts +1 -0
  315. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  316. package/src/vite/discovery/discover-routers.ts +95 -82
  317. package/src/vite/discovery/discovery-errors.ts +194 -0
  318. package/src/vite/discovery/prerender-collection.ts +26 -34
  319. package/src/vite/discovery/route-types-writer.ts +40 -84
  320. package/src/vite/discovery/state.ts +39 -1
  321. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  322. package/src/vite/index.ts +4 -0
  323. package/src/vite/plugin-types.ts +185 -10
  324. package/src/vite/plugins/cjs-to-esm.ts +3 -18
  325. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  326. package/src/vite/plugins/client-ref-hashing.ts +12 -11
  327. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  328. package/src/vite/plugins/expose-action-id.ts +4 -75
  329. package/src/vite/plugins/expose-id-utils.ts +3 -54
  330. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  331. package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
  332. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  333. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  334. package/src/vite/plugins/expose-internal-ids.ts +57 -67
  335. package/src/vite/plugins/performance-tracks.ts +9 -16
  336. package/src/vite/plugins/refresh-cmd.ts +1 -1
  337. package/src/vite/plugins/use-cache-transform.ts +26 -49
  338. package/src/vite/plugins/vercel-output.ts +258 -0
  339. package/src/vite/plugins/version-injector.ts +2 -32
  340. package/src/vite/plugins/version-plugin.ts +32 -23
  341. package/src/vite/plugins/virtual-entries.ts +35 -17
  342. package/src/vite/rango.ts +148 -115
  343. package/src/vite/router-discovery.ts +220 -68
  344. package/src/vite/utils/ast-handler-extract.ts +15 -31
  345. package/src/vite/utils/bundle-analysis.ts +10 -15
  346. package/src/vite/utils/client-chunks.ts +184 -0
  347. package/src/vite/utils/forward-user-plugins.ts +171 -0
  348. package/src/vite/utils/manifest-utils.ts +4 -59
  349. package/src/vite/utils/package-resolution.ts +1 -73
  350. package/src/vite/utils/prerender-utils.ts +0 -34
  351. package/src/vite/utils/shared-utils.ts +95 -43
  352. package/src/browser/action-response-classifier.ts +0 -99
  353. package/src/browser/react/use-client-cache.ts +0 -58
  354. package/src/browser/shallow.ts +0 -40
  355. package/src/handles/index.ts +0 -7
  356. package/src/router/middleware-cookies.ts +0 -55
@@ -1,152 +1,194 @@
1
1
  /**
2
2
  * Rango State
3
3
  *
4
- * Manages a localStorage-based state key for HTTP cache invalidation.
5
- * The key is sent as the `X-Rango-State` header on both prefetch and
6
- * navigation requests. The server responds with `Vary: X-Rango-State`,
7
- * so the browser HTTP cache keys responses by (URL, X-Rango-State value).
4
+ * Manages a session-cookie-based state value for HTTP cache invalidation. The
5
+ * value is sent as the `X-Rango-State` header on prefetch and navigation
6
+ * requests; the server responds with `Vary: X-Rango-State`, so the browser HTTP
7
+ * cache keys responses by (URL, X-Rango-State value).
8
8
  *
9
9
  * Value format: `{buildVersion}:{invalidationTimestamp}`
10
- * - Build version changes on deploy, busting all cached prefetches.
11
- * - Timestamp changes on server action invalidation.
10
+ * - Build version changes on deploy, busting all cached prefetches at boot.
11
+ * - Timestamp rotates on invalidation (server action, invalidateClientCache).
12
12
  *
13
- * Storage key is namespaced per routerId (`rango-state:{routerId}`) so
14
- * tabs in different apps on the same origin do not collide. Two tabs in
15
- * the same app share a key one tab's invalidation is picked up by the
16
- * other via the `storage` event. A smooth cross-app transition in this
17
- * tab rebinds to the target app's key; other tabs still in the old app
18
- * keep their own key intact.
13
+ * Storage is a session cookie named by the server-resolved name passed to
14
+ * initRangoState (`{prefix}_{routerId}`, default prefix `rango-state`). The
15
+ * cookie jar is shared across tabs, so a per-request read IS the cross-tab
16
+ * value sync no `storage` event is needed. An in-memory mirror is a
17
+ * write-through copy that is authoritative only when the cookie is unreadable
18
+ * (e.g. a sandboxed frame, or site data blocked wholesale): the failure
19
+ * direction is always toward freshness.
19
20
  *
20
- * If no routerId is supplied, falls back to a single legacy key for
21
- * backward compatibility (single-app deployments unaffected).
21
+ * Precedence is load-bearing: when `document.cookie` is readable, the
22
+ * per-request read wins; the mirror is a fallback, never a cache of the read.
23
+ * Caching the read across requests would reintroduce the staleness this
24
+ * mechanism removes.
22
25
  */
23
26
 
24
- const LEGACY_STORAGE_KEY = "rango-state";
27
+ import {
28
+ DEFAULT_STATE_COOKIE_PREFIX,
29
+ decodeStateValue,
30
+ getRawCookieValue,
31
+ mintStateValue,
32
+ serializeStateCookie,
33
+ } from "./cookie-name.js";
25
34
 
26
- function buildStorageKey(routerId: string | undefined): string {
27
- return routerId ? `${LEGACY_STORAGE_KEY}:${routerId}` : LEGACY_STORAGE_KEY;
28
- }
35
+ let cookieName: string = DEFAULT_STATE_COOKIE_PREFIX;
29
36
 
30
- // Module-level cache avoids hitting localStorage on every getRangoState() call.
31
- // Initialized from localStorage on first access or by initRangoState().
32
- let cachedState: string | null = null;
33
-
34
- // The localStorage key this tab is currently bound to. Rebinds on
35
- // initRangoState (document boot) and setRangoStateLocal (smooth app
36
- // switch). The storage listener filters cross-tab events by this key so
37
- // events from tabs in a different app are ignored.
38
- let currentStorageKey: string = LEGACY_STORAGE_KEY;
39
-
40
- // Cross-tab sync: the `storage` event fires in OTHER tabs when one tab writes
41
- // to localStorage, keeping cachedState fresh without polling.
42
- let storageListenerAttached = false;
43
-
44
- function attachStorageListener(): void {
45
- if (storageListenerAttached || typeof window === "undefined") return;
46
- window.addEventListener("storage", (e) => {
47
- // Only react to events for this tab's current app namespace. Events
48
- // under other routerId-scoped keys belong to other apps and must not
49
- // clobber this tab's state.
50
- if (e.key !== currentStorageKey) return;
51
- cachedState = e.newValue;
52
- });
53
- storageListenerAttached = true;
54
- }
37
+ let currentVersion = "0";
38
+
39
+ let mirror: string | null = null;
40
+ let cookieBacked = false;
41
+
42
+ let externalRotationObserver: ((value: string) => void) | null = null;
55
43
 
56
44
  /**
57
- * Initialize the Rango state key in localStorage.
58
- * Called once at app startup with the build version from the server.
59
- * The routerId scopes the storage key to this app; in multi-app setups
60
- * each app owns its own `rango-state:{routerId}` key and cannot observe
61
- * invalidations from sibling apps on the same origin.
62
- *
63
- * If localStorage already has a matching-version entry under the key,
64
- * keeps it (preserves invalidation state across refresh). Otherwise
65
- * writes a new value.
45
+ * Register the observer invoked when a read detects an EXTERNAL rotation (a
46
+ * sibling tab, a server `Set-Cookie`, or a cookie clear). Self-rotations
47
+ * (invalidateRangoState) update the mirror synchronously and never fire it.
66
48
  */
67
- export function initRangoState(version: string, routerId?: string): void {
68
- currentStorageKey = buildStorageKey(routerId);
69
- if (typeof window === "undefined") return;
49
+ export function setRangoStateObserver(
50
+ observer: ((value: string) => void) | null,
51
+ ): void {
52
+ externalRotationObserver = observer;
53
+ }
70
54
 
71
- attachStorageListener();
55
+ function notifyExternalRotation(value: string): void {
56
+ externalRotationObserver?.(value);
57
+ }
72
58
 
59
+ interface CookieRead {
60
+ /** False when there is no document or the read threw (sandboxed frame). */
61
+ readable: boolean;
62
+ /** The cookie value, or null when readable but absent. */
63
+ value: string | null;
64
+ }
65
+
66
+ function readCookie(name: string): CookieRead {
67
+ if (typeof document === "undefined") return { readable: false, value: null };
68
+ let raw: string;
73
69
  try {
74
- const existing = localStorage.getItem(currentStorageKey);
75
- if (existing) {
76
- const colonIdx = existing.indexOf(":");
77
- if (colonIdx > 0) {
78
- const existingVersion = existing.slice(0, colonIdx);
79
- if (existingVersion === version) {
80
- cachedState = existing;
81
- return;
82
- }
83
- }
84
- }
85
- // New version or first load
86
- const newState = `${version}:${Date.now()}`;
87
- localStorage.setItem(currentStorageKey, newState);
88
- cachedState = newState;
70
+ raw = document.cookie;
89
71
  } catch {
90
- // localStorage may be unavailable (private browsing in some browsers)
91
- cachedState = `${version}:${Date.now()}`;
72
+ return { readable: false, value: null };
92
73
  }
74
+ return { readable: true, value: getRawCookieValue(raw, name) };
93
75
  }
94
76
 
95
- /**
96
- * Get the current Rango state key value.
97
- * Used as the `X-Rango-State` header value for prefetch and navigation requests.
98
- */
99
- export function getRangoState(): string {
100
- if (cachedState) return cachedState;
77
+ function writeCookie(name: string, value: string): void {
78
+ if (typeof document === "undefined") return;
79
+ const secure =
80
+ typeof location !== "undefined" && location.protocol === "https:";
81
+ try {
82
+ document.cookie = serializeStateCookie(name, value, secure);
83
+ } catch {}
84
+ }
101
85
 
102
- if (typeof window === "undefined") return "0:0";
86
+ function mintValue(): string {
87
+ return mintStateValue(currentVersion, mirror);
88
+ }
103
89
 
104
- try {
105
- const stored = localStorage.getItem(currentStorageKey);
106
- if (stored) {
107
- cachedState = stored;
108
- return stored;
90
+ /**
91
+ * Initialize the Rango state cookie at app startup. `version` is the build
92
+ * version; `stateCookieName` is the server-resolved cookie name from payload
93
+ * metadata (falls back to the bare default prefix when a payload arrives
94
+ * without it). Keeps an existing matching-version cookie (preserves the cache
95
+ * key across reloads); mints fresh on a version change or a missing cookie.
96
+ */
97
+ export function initRangoState(
98
+ version: string,
99
+ stateCookieName?: string,
100
+ ): void {
101
+ currentVersion = version;
102
+ cookieName = stateCookieName || DEFAULT_STATE_COOKIE_PREFIX;
103
+ cleanupLegacyStorage();
104
+
105
+ const read = readCookie(cookieName);
106
+ if (!read.readable) {
107
+ // Cookies unreadable: the mirror is the source of truth for this session.
108
+ mirror = mintValue();
109
+ cookieBacked = false;
110
+ return;
111
+ }
112
+ if (read.value !== null) {
113
+ const decoded = decodeStateValue(read.value);
114
+ if (decoded && decoded.version === version) {
115
+ // Keep: a matching-version cookie survives the reload warm.
116
+ mirror = read.value;
117
+ cookieBacked = true;
118
+ return;
109
119
  }
110
- } catch {
111
- // Fallback for unavailable localStorage
112
120
  }
113
-
114
- return "0:0";
121
+ // Absent, malformed, or a version change (deploy): mint fresh and write.
122
+ mirror = mintValue();
123
+ cookieBacked = false;
124
+ writeCookie(cookieName, mirror);
115
125
  }
116
126
 
117
127
  /**
118
- * Update the in-memory rango-state to a new version WITHOUT writing
119
- * localStorage. Intended for smooth cross-app transitions in this tab only:
120
- * subsequent requests from this tab send the new token, but other tabs
121
- * still in the previous app do not observe a storage event. Rebinds this
122
- * tab's storage key to the target app's namespace (`rango-state:{routerId}`)
123
- * so subsequent storage events only reflect the new app. On the next hard
124
- * reload, initRangoState reconciles localStorage from the server's
125
- * authoritative version.
128
+ * Get the current Rango state value, used as the `X-Rango-State` header on
129
+ * prefetch and navigation requests. Reads the cookie every call (the read is
130
+ * the cross-tab sync channel) and reconciles the mirror.
126
131
  */
127
- export function setRangoStateLocal(version: string, routerId?: string): void {
128
- currentStorageKey = buildStorageKey(routerId);
129
- cachedState = `${version}:${Date.now()}`;
132
+ export function getRangoState(): string {
133
+ const read = readCookie(cookieName);
134
+
135
+ if (!read.readable) {
136
+ // Mirror authoritative when the jar is unreadable.
137
+ return mirror ?? "0:0";
138
+ }
139
+
140
+ if (read.value !== null) {
141
+ if (read.value !== mirror) {
142
+ // External rotation (sibling tab / server Set-Cookie): adopt it. The
143
+ // mirror update makes this idempotent across a burst of reads.
144
+ mirror = read.value;
145
+ cookieBacked = true;
146
+ notifyExternalRotation(read.value);
147
+ } else {
148
+ cookieBacked = true;
149
+ }
150
+ return read.value;
151
+ }
152
+
153
+ // Readable but absent.
154
+ if (cookieBacked) {
155
+ // present -> absent: an external clear. Mint fresh, write back, and notify
156
+ // once (cookieBacked flips to false so we don't re-fire on the next read).
157
+ mirror = mintValue();
158
+ cookieBacked = false;
159
+ writeCookie(cookieName, mirror);
160
+ notifyExternalRotation(mirror);
161
+ } else if (mirror === null) {
162
+ // First access with no cookie yet (pre-boot): mint silently — there is
163
+ // nothing to invalidate.
164
+ mirror = mintValue();
165
+ writeCookie(cookieName, mirror);
166
+ }
167
+ return mirror;
130
168
  }
131
169
 
132
170
  /**
133
- * Invalidate the Rango state key. Called when server actions mutate data.
134
- * Updates the timestamp portion while keeping the version prefix.
135
- * The new value takes effect immediately for all subsequent fetches,
136
- * causing Vary mismatches with previously cached responses.
171
+ * Invalidate the Rango state (self-rotation). Called when the client clears its
172
+ * prefetch caches (e.g. via the server-action bridge). Rotates the timestamp,
173
+ * keeps the version, writes the cookie, and updates the mirror synchronously so
174
+ * the external-rotation observer is NOT triggered by our own write.
137
175
  */
138
176
  export function invalidateRangoState(): void {
139
- const current = getRangoState();
140
- const colonIdx = current.indexOf(":");
141
- const version = colonIdx > 0 ? current.slice(0, colonIdx) : "0";
142
- const newState = `${version}:${Date.now()}`;
143
- cachedState = newState;
144
-
145
- if (typeof window === "undefined") return;
177
+ mirror = mintValue();
178
+ cookieBacked = false;
179
+ writeCookie(cookieName, mirror);
180
+ }
146
181
 
182
+ function cleanupLegacyStorage(): void {
183
+ if (typeof localStorage === "undefined") return;
147
184
  try {
148
- localStorage.setItem(currentStorageKey, newState);
149
- } catch {
150
- // Silently handle localStorage errors
151
- }
185
+ const toRemove: string[] = [];
186
+ for (let i = 0; i < localStorage.length; i++) {
187
+ const key = localStorage.key(i);
188
+ if (key === "rango-state" || (key && key.startsWith("rango-state:"))) {
189
+ toRemove.push(key);
190
+ }
191
+ }
192
+ for (const key of toRemove) localStorage.removeItem(key);
193
+ } catch {}
152
194
  }
@@ -39,8 +39,6 @@ import {
39
39
  unobserveForPrefetch,
40
40
  } from "../prefetch/observer.js";
41
41
 
42
- // Touch device detection for adaptive strategy.
43
- // Checked once at module load (Link.tsx is "use client", runs only in browser).
44
42
  const isTouchDevice =
45
43
  typeof window !== "undefined" && window.matchMedia("(hover: none)").matches;
46
44
 
@@ -28,7 +28,8 @@ import { NonceContext } from "./nonce-context.js";
28
28
  import type { ResolvedThemeConfig, Theme } from "../../theme/types.js";
29
29
  import { cancelAllPrefetches } from "../prefetch/queue.js";
30
30
  import { handleNavigationEnd } from "../scroll-restoration.js";
31
- import type { AppShellRef } from "../app-shell.js";
31
+ import { createAppShellRef, type AppShellRef } from "../app-shell.js";
32
+ import { debugLog } from "../logging.js";
32
33
 
33
34
  /**
34
35
  * Process handles from an async generator, updating the event controller
@@ -70,7 +71,7 @@ async function processHandles(
70
71
  // This prevents handle data from cancelled navigations polluting
71
72
  // the current route's breadcrumbs (e.g., quick popstate after clicking a link).
72
73
  if (historyKey !== store.getHistoryKey()) {
73
- console.log(
74
+ debugLog(
74
75
  "[NavigationProvider] Stopping handle processing - user navigated away",
75
76
  );
76
77
  return;
@@ -157,9 +158,11 @@ export interface NavigationProviderProps {
157
158
  basename?: string;
158
159
 
159
160
  /**
160
- * Live app-shell ref. When provided, the context's `basename` and `version`
161
- * properties become live getters that track app-switch updates without
162
- * invalidating the memoized context value.
161
+ * App-shell ref. When provided, the context's `basename` and `version` are
162
+ * read through it (live getters) so they don't close over a stale snapshot or
163
+ * invalidate the memoized context value. The shell is set once at init and is
164
+ * not swapped within a session — a cross-app navigation is a full document
165
+ * load (X-RSC-Reload), so the target app establishes its own shell on load.
163
166
  */
164
167
  appShellRef?: AppShellRef;
165
168
  }
@@ -217,38 +220,34 @@ export function NavigationProvider({
217
220
  await bridge.refresh();
218
221
  }, []);
219
222
 
220
- // Context value is stable (store, eventController, navigate, refresh never
221
- // change). When an appShellRef is supplied, `basename` and `version` are
222
- // installed as live getters so app-switch transitions (which update the ref)
223
- // propagate to consumers without forcing a tree-wide rerender.
223
+ // basename/version are always read through a shell ref so the context value
224
+ // has a single shape. Both are set once: a supplied appShellRef is seeded
225
+ // from the init payload (a cross-app navigation reloads, so it is not swapped
226
+ // in-session), and the standalone fallback wraps the mount-time props.
227
+ const fallbackShellRef = useRef<AppShellRef | null>(null);
228
+ if (!fallbackShellRef.current) {
229
+ fallbackShellRef.current = createAppShellRef({ basename, version });
230
+ }
231
+ const shellRef = appShellRef ?? fallbackShellRef.current;
232
+
224
233
  const contextValue = useMemo<NavigationStoreContextValue>(() => {
225
- if (appShellRef) {
226
- const value = {
227
- store,
228
- eventController,
229
- navigate,
230
- refresh,
231
- } as NavigationStoreContextValue;
232
- Object.defineProperty(value, "basename", {
233
- configurable: true,
234
- enumerable: true,
235
- get: () => appShellRef.get().basename,
236
- });
237
- Object.defineProperty(value, "version", {
238
- configurable: true,
239
- enumerable: true,
240
- get: () => appShellRef.get().version,
241
- });
242
- return value;
243
- }
244
- return {
234
+ const value = {
245
235
  store,
246
236
  eventController,
247
237
  navigate,
248
238
  refresh,
249
- version,
250
- basename,
251
- };
239
+ } as NavigationStoreContextValue;
240
+ Object.defineProperty(value, "basename", {
241
+ configurable: true,
242
+ enumerable: true,
243
+ get: () => shellRef.get().basename,
244
+ });
245
+ Object.defineProperty(value, "version", {
246
+ configurable: true,
247
+ enumerable: true,
248
+ get: () => shellRef.get().version,
249
+ });
250
+ return value;
252
251
  }, []);
253
252
 
254
253
  // Connection warmup: keep TLS alive after idle periods.
@@ -410,21 +409,15 @@ export function NavigationProvider({
410
409
  }).catch((err) =>
411
410
  console.error("[NavigationProvider] Error consuming handles:", err),
412
411
  );
413
- } else if (update.metadata.cachedHandleData) {
414
- // For back/forward navigation from cache, restore the cached handleData
415
- // This restores breadcrumbs to the exact state they were when the page was cached
416
- eventController.setHandleData(
417
- update.metadata.cachedHandleData,
418
- update.metadata.matched,
419
- false, // full replace - restore entire cached state
420
- );
421
412
  } else if (update.metadata.matched) {
422
- // For cached navigations without handleData, update segmentOrder to clean up stale data
413
+ // cachedHandleData present -> full restore (back/forward); absent ->
414
+ // partial cleanup of segments no longer matched.
415
+ const cached = update.metadata.cachedHandleData;
423
416
  eventController.setHandleData(
424
- {}, // Empty data - all existing data not in matched will be cleaned up
417
+ cached ?? {},
425
418
  update.metadata.matched,
426
- true, // partial update - will clean up segments not in matched
427
- update.metadata.resolvedIds,
419
+ cached === undefined,
420
+ cached === undefined ? update.metadata.resolvedIds : undefined,
428
421
  );
429
422
  }
430
423
  });
@@ -447,10 +440,10 @@ export function NavigationProvider({
447
440
  let content = <RootErrorBoundary>{root}</RootErrorBoundary>;
448
441
 
449
442
  // Wrap with ThemeProvider when theme is enabled. The ThemeProvider is
450
- // document-lifetime: its config comes from the initial load and does NOT
451
- // swap on cross-app transitions, because the ThemeProvider sits above the
452
- // segment tree and a smooth (no-reload) app switch cannot safely remount
453
- // it. A new theme config only takes effect on a full document load.
443
+ // document-lifetime: its config comes from the initial load and persists for
444
+ // the session. It sits above the segment tree and is not remounted in-session;
445
+ // a cross-app navigation is a full document load (X-RSC-Reload), so the target
446
+ // app's theme config takes effect on its own load.
454
447
  if (themeConfig) {
455
448
  content = (
456
449
  <ThemeProvider config={themeConfig} initialTheme={initialTheme}>
@@ -14,17 +14,21 @@ export interface ScrollRestorationProps {
14
14
  * Return location.pathname to restore scroll based on path
15
15
  * (useful for keeping scroll position on the same page).
16
16
  *
17
+ * Provide a stable reference: a module-level function or one wrapped in
18
+ * useCallback. The init effect re-runs when getKey's identity changes, and
19
+ * teardown clears in-memory scroll positions — a fresh inline arrow on every
20
+ * parent render would discard unpersisted positions mid-session.
21
+ *
17
22
  * @example
18
23
  * ```tsx
24
+ * // Stable module-level getKey (recommended)
25
+ * const byPathname = (location) => location.pathname;
26
+ *
19
27
  * // Restore based on pathname (same URL = same scroll)
20
- * <ScrollRestoration
21
- * getKey={(location) => location.pathname}
22
- * />
28
+ * <ScrollRestoration getKey={byPathname} />
23
29
  *
24
30
  * // Restore based on unique history entry (default)
25
- * <ScrollRestoration
26
- * getKey={(location) => location.key}
27
- * />
31
+ * // <ScrollRestoration /> — omit getKey to use location.key
28
32
  * ```
29
33
  */
30
34
  getKey?: (location: {
@@ -46,8 +46,6 @@ export function filterSegmentOrder(matched: string[]): string[] {
46
46
  const slots = slotsByParent.get(id);
47
47
  if (slots) result.push(...slots);
48
48
  }
49
- // Defensive: any slot whose parent is missing from the filtered list still
50
- // gets included rather than silently dropped. Shouldn't happen in practice.
51
49
  for (const [parent, slots] of slotsByParent) {
52
50
  if (!nonSlotSet.has(parent)) result.push(...slots);
53
51
  }
@@ -1,52 +1,4 @@
1
- // React exports for browser navigation
2
-
3
- // Hook with Zustand-style selectors
4
- export { useNavigation } from "./use-navigation.js";
5
-
6
- // Router actions hook (stable reference, no re-renders)
7
- export { useRouter } from "./use-router.js";
8
-
9
- // URL hooks
10
- export { usePathname } from "./use-pathname.js";
11
- export { useSearchParams } from "./use-search-params.js";
12
- export { useParams } from "./use-params.js";
13
-
14
- // Action state tracking hook
15
- export { useAction, type TrackedActionState } from "./use-action.js";
16
-
17
- // Segments state hook
18
- export { useSegments, type SegmentsState } from "./use-segments.js";
19
-
20
- // Handle data hook
21
- export { useHandle } from "./use-handle.js";
22
-
23
- // Client cache controls hook
24
- export {
25
- useClientCache,
26
- type ClientCacheControls,
27
- } from "./use-client-cache.js";
28
-
29
- // Provider
30
1
  export {
31
2
  NavigationProvider,
32
3
  type NavigationProviderProps,
33
4
  } from "./NavigationProvider.js";
34
-
35
- // Context (for advanced usage)
36
- export {
37
- NavigationStoreContext,
38
- type NavigationStoreContextValue,
39
- } from "./context.js";
40
-
41
- // Link component
42
- export { Link, type LinkProps, type PrefetchStrategy } from "./Link.js";
43
-
44
- // Link status hook
45
- export { useLinkStatus, type LinkStatus } from "./use-link-status.js";
46
-
47
- // Scroll restoration
48
- export {
49
- ScrollRestoration,
50
- useScrollRestoration,
51
- type ScrollRestorationProps,
52
- } from "./ScrollRestoration.js";