@rangojs/router 0.19.1 → 0.21.0

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 (185) hide show
  1. package/dist/bin/rango.js +26 -14
  2. package/dist/types/browser/event-controller.d.ts +32 -0
  3. package/dist/types/browser/history-state.d.ts +16 -4
  4. package/dist/types/browser/navigation-transaction.d.ts +7 -0
  5. package/dist/types/browser/rango-state.d.ts +3 -1
  6. package/dist/types/browser/react/context.d.ts +11 -0
  7. package/dist/types/browser/react/location-state-shared.d.ts +84 -10
  8. package/dist/types/browser/react/location-state.d.ts +7 -2
  9. package/dist/types/browser/react/subscribe-to-nothing.d.ts +7 -0
  10. package/dist/types/cache/cache-scope.d.ts +4 -5
  11. package/dist/types/cache/cf/cf-cache-store.d.ts +27 -6
  12. package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
  13. package/dist/types/cache/handle-snapshot.d.ts +36 -50
  14. package/dist/types/cache/index.d.ts +2 -0
  15. package/dist/types/cache/shell-snapshot.d.ts +19 -3
  16. package/dist/types/cache/types.d.ts +21 -10
  17. package/dist/types/cache/vercel/vercel-cache-store.d.ts +28 -16
  18. package/dist/types/client-urls/navigation.d.ts +7 -1
  19. package/dist/types/client-urls/optimistic-location.d.ts +10 -4
  20. package/dist/types/handles/deferred-resolution.d.ts +11 -1
  21. package/dist/types/prerender/build-shell-capture.d.ts +8 -6
  22. package/dist/types/router/match-middleware/cache-lookup.d.ts +1 -1
  23. package/dist/types/router/navigation-snapshot.d.ts +8 -1
  24. package/dist/types/router/router-interfaces.d.ts +2 -1
  25. package/dist/types/router/router-options.d.ts +17 -7
  26. package/dist/types/router/segment-resolution/loader-cache.d.ts +21 -9
  27. package/dist/types/router-versions.d.ts +39 -0
  28. package/dist/types/rsc/handler-context.d.ts +5 -0
  29. package/dist/types/rsc/shell-build-manifest.d.ts +3 -2
  30. package/dist/types/rsc/types.d.ts +4 -6
  31. package/dist/types/server/build-version-table.d.ts +44 -0
  32. package/dist/types/server/handle-store.d.ts +87 -46
  33. package/dist/types/server/request-context.d.ts +22 -8
  34. package/dist/types/ssr/entry-preload-priority.d.ts +69 -0
  35. package/dist/types/ssr/index.d.ts +1 -0
  36. package/dist/types/ssr/preinit-client-references.d.ts +17 -5
  37. package/dist/types/testing/build-versions.d.ts +32 -0
  38. package/dist/types/testing/dom.entry.d.ts +1 -1
  39. package/dist/types/testing/flight.entry.d.ts +1 -1
  40. package/dist/types/testing/index.d.ts +2 -0
  41. package/dist/types/testing/internal/seed-vars.d.ts +7 -2
  42. package/dist/types/testing/location-state-key.d.ts +5 -1
  43. package/dist/types/testing/render-route.d.ts +77 -6
  44. package/dist/types/testing/serve-shell-request.d.ts +21 -0
  45. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  46. package/dist/types/types/handler-context.d.ts +8 -5
  47. package/dist/types/vite/discovery/build-versions.d.ts +393 -0
  48. package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
  49. package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
  50. package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
  51. package/dist/types/vite/discovery/state.d.ts +14 -0
  52. package/dist/types/vite/encryption-key.d.ts +18 -0
  53. package/dist/types/vite/plugin-types.d.ts +47 -6
  54. package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
  55. package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
  56. package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
  57. package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
  58. package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
  59. package/dist/types/vite/plugins/virtual-entries.d.ts +21 -2
  60. package/dist/types/vite/router-discovery.d.ts +6 -0
  61. package/dist/types/vite/utils/client-chunks.d.ts +24 -2
  62. package/dist/types/vite/utils/compare-strings.d.ts +7 -0
  63. package/dist/types/vite/utils/package-resolution.d.ts +8 -0
  64. package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
  65. package/dist/vite/index.js +1544 -420
  66. package/package.json +1 -2
  67. package/skills/bundle-analysis/SKILL.md +1 -1
  68. package/skills/cache-guide/SKILL.md +84 -20
  69. package/skills/caching/SKILL.md +16 -5
  70. package/skills/client-urls/SKILL.md +16 -0
  71. package/skills/cloudflare/SKILL.md +2 -1
  72. package/skills/comparison/references/framework-comparison.md +16 -12
  73. package/skills/hooks/SKILL.md +3 -0
  74. package/skills/hooks/handle-and-actions.md +6 -0
  75. package/skills/hooks/navigation.md +28 -0
  76. package/skills/hooks/state.md +209 -8
  77. package/skills/hooks/urls.md +40 -1
  78. package/skills/host-router/SKILL.md +13 -0
  79. package/skills/intercept/SKILL.md +6 -0
  80. package/skills/loader/SKILL.md +19 -1
  81. package/skills/middleware/SKILL.md +2 -0
  82. package/skills/ppr/SKILL.md +119 -40
  83. package/skills/prerender/SKILL.md +7 -3
  84. package/skills/rango/SKILL.md +8 -7
  85. package/skills/router-setup/SKILL.md +23 -3
  86. package/skills/testing/SKILL.md +30 -26
  87. package/skills/testing/client-components.md +193 -8
  88. package/skills/testing/handles.md +2 -0
  89. package/skills/testing/loader.md +1 -1
  90. package/skills/testing/middleware.md +1 -1
  91. package/skills/testing/render-handler.md +18 -18
  92. package/skills/testing/serve-shell-request.md +63 -14
  93. package/skills/testing/server-actions.md +17 -17
  94. package/skills/testing/server-tree.md +1 -1
  95. package/skills/typesafety/generated-files-and-cli.md +1 -1
  96. package/skills/use-cache/SKILL.md +16 -5
  97. package/skills/vercel/SKILL.md +3 -3
  98. package/src/bin/rango.ts +1 -1
  99. package/src/browser/event-controller.ts +75 -2
  100. package/src/browser/history-state.ts +39 -17
  101. package/src/browser/navigation-bridge.ts +41 -18
  102. package/src/browser/navigation-transaction.ts +15 -10
  103. package/src/browser/partial-update.ts +10 -2
  104. package/src/browser/rango-state.ts +3 -1
  105. package/src/browser/react/Link.tsx +2 -3
  106. package/src/browser/react/NavigationProvider.tsx +25 -1
  107. package/src/browser/react/context.ts +13 -0
  108. package/src/browser/react/location-state-shared.ts +200 -34
  109. package/src/browser/react/location-state.ts +40 -75
  110. package/src/browser/react/subscribe-to-nothing.ts +7 -0
  111. package/src/browser/react/use-handle.ts +20 -1
  112. package/src/browser/rsc-router.tsx +33 -11
  113. package/src/browser/server-action-bridge.ts +17 -1
  114. package/src/cache/cache-scope.ts +18 -8
  115. package/src/cache/cf/cf-cache-store.ts +75 -55
  116. package/src/cache/cf/cf-cache-types.ts +12 -4
  117. package/src/cache/handle-snapshot.ts +69 -87
  118. package/src/cache/index.ts +4 -0
  119. package/src/cache/shell-snapshot.ts +35 -5
  120. package/src/cache/types.ts +21 -10
  121. package/src/cache/vercel/vercel-cache-store.ts +48 -20
  122. package/src/client-urls/client-root.tsx +1 -0
  123. package/src/client-urls/navigation.ts +10 -0
  124. package/src/client-urls/optimistic-location.ts +10 -4
  125. package/src/handles/deferred-resolution.ts +31 -0
  126. package/src/prerender/build-shell-capture.ts +11 -9
  127. package/src/router/manifest.ts +2 -0
  128. package/src/router/match-middleware/cache-lookup.ts +11 -18
  129. package/src/router/navigation-snapshot.ts +25 -6
  130. package/src/router/request-classification.ts +15 -12
  131. package/src/router/router-interfaces.ts +2 -1
  132. package/src/router/router-options.ts +17 -7
  133. package/src/router/segment-resolution/loader-cache.ts +95 -60
  134. package/src/router-versions.ts +60 -0
  135. package/src/rsc/full-payload.ts +6 -2
  136. package/src/rsc/handler-context.ts +5 -0
  137. package/src/rsc/handler.ts +14 -4
  138. package/src/rsc/rsc-rendering.ts +12 -1
  139. package/src/rsc/shell-build-manifest.ts +10 -7
  140. package/src/rsc/shell-capture.ts +36 -26
  141. package/src/rsc/types.ts +4 -6
  142. package/src/server/build-version-table.ts +115 -0
  143. package/src/server/handle-store.ts +344 -127
  144. package/src/server/request-context.ts +62 -11
  145. package/src/ssr/entry-preload-priority.ts +308 -0
  146. package/src/ssr/index.tsx +46 -9
  147. package/src/ssr/preinit-client-references.ts +17 -5
  148. package/src/ssr/ssr-root.tsx +16 -4
  149. package/src/testing/build-versions.ts +64 -0
  150. package/src/testing/dispatch.ts +6 -0
  151. package/src/testing/dom.entry.ts +1 -0
  152. package/src/testing/flight.entry.ts +1 -0
  153. package/src/testing/index.ts +3 -0
  154. package/src/testing/internal/context.ts +2 -1
  155. package/src/testing/internal/seed-vars.ts +14 -2
  156. package/src/testing/location-state-key.ts +18 -8
  157. package/src/testing/render-handler.ts +2 -1
  158. package/src/testing/render-route.tsx +277 -43
  159. package/src/testing/serve-shell-request.ts +80 -11
  160. package/src/testing/vitest-stubs/version.ts +4 -1
  161. package/src/types/handler-context.ts +8 -5
  162. package/src/vite/discovery/build-versions.ts +1281 -0
  163. package/src/vite/discovery/installed-packages.ts +159 -0
  164. package/src/vite/discovery/prerender-collection.ts +38 -1
  165. package/src/vite/discovery/router-versions-phase.ts +336 -0
  166. package/src/vite/discovery/shell-prerender-phase.ts +28 -38
  167. package/src/vite/discovery/state.ts +19 -0
  168. package/src/vite/discovery/virtual-module-codegen.ts +7 -2
  169. package/src/vite/encryption-key.ts +101 -9
  170. package/src/vite/plugin-types.ts +48 -6
  171. package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
  172. package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
  173. package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
  174. package/src/vite/plugins/expose-internal-ids.ts +12 -3
  175. package/src/vite/plugins/stable-rsc-output.ts +147 -0
  176. package/src/vite/plugins/version-plugin.ts +19 -24
  177. package/src/vite/plugins/version.d.ts +17 -2
  178. package/src/vite/plugins/virtual-entries.ts +40 -7
  179. package/src/vite/rango.ts +30 -3
  180. package/src/vite/router-discovery.ts +82 -13
  181. package/src/vite/utils/client-chunks.ts +84 -14
  182. package/src/vite/utils/compare-strings.ts +9 -0
  183. package/src/vite/utils/package-resolution.ts +9 -0
  184. package/src/vite/utils/prerender-utils.ts +4 -1
  185. package/src/vite/utils/shared-utils.ts +1 -1
package/dist/bin/rango.js CHANGED
@@ -1599,14 +1599,33 @@ var init_merge_full_manifests = __esm({
1599
1599
  }
1600
1600
  });
1601
1601
 
1602
+ // src/router-versions.ts
1603
+ var DEFAULT_ROUTER_VERSIONS_KEY;
1604
+ var init_router_versions = __esm({
1605
+ "src/router-versions.ts"() {
1606
+ "use strict";
1607
+ DEFAULT_ROUTER_VERSIONS_KEY = "*";
1608
+ }
1609
+ });
1610
+
1602
1611
  // src/vite/plugins/virtual-entries.ts
1603
1612
  function getVirtualVersionContent(version) {
1604
- return `export const VERSION = ${JSON.stringify(version)};`;
1605
- }
1606
- var VIRTUAL_ENTRY_BROWSER, VIRTUAL_IDS;
1613
+ return [
1614
+ `export const VERSION = ${JSON.stringify(version)};`,
1615
+ `export const ROUTER_VERSIONS = undefined;`
1616
+ ].join("\n");
1617
+ }
1618
+ function getVirtualBuildVersionContent() {
1619
+ return [
1620
+ `export const ROUTER_VERSIONS = ${ROUTER_VERSIONS_PLACEHOLDER};`,
1621
+ `export const VERSION = ROUTER_VERSIONS[${JSON.stringify(DEFAULT_ROUTER_VERSIONS_KEY)}][1];`
1622
+ ].join("\n");
1623
+ }
1624
+ var VIRTUAL_ENTRY_BROWSER, VIRTUAL_IDS, ROUTER_VERSIONS_PLACEHOLDER;
1607
1625
  var init_virtual_entries = __esm({
1608
1626
  "src/vite/plugins/virtual-entries.ts"() {
1609
1627
  "use strict";
1628
+ init_router_versions();
1610
1629
  VIRTUAL_ENTRY_BROWSER = `
1611
1630
  import {
1612
1631
  createFromReadableStream,
@@ -1660,6 +1679,7 @@ initializeApp().catch(console.error);
1660
1679
  rsc: "virtual:rsc-router/entry.rsc.js",
1661
1680
  version: "@rangojs/router:version"
1662
1681
  };
1682
+ ROUTER_VERSIONS_PLACEHOLDER = "__RANGO_ROUTER_VERSIONS__";
1663
1683
  }
1664
1684
  });
1665
1685
 
@@ -1772,8 +1792,7 @@ function getClientModuleSignature(source) {
1772
1792
  };
1773
1793
  }
1774
1794
  function createVersionPlugin() {
1775
- const buildVersion = Date.now().toString(16);
1776
- let currentVersion = buildVersion;
1795
+ let currentVersion = Date.now().toString(16);
1777
1796
  let isDev = false;
1778
1797
  let server = null;
1779
1798
  let resolvedCacheDir;
@@ -1793,13 +1812,6 @@ function createVersionPlugin() {
1793
1812
  return {
1794
1813
  name: "@rangojs/router:version",
1795
1814
  enforce: "pre",
1796
- // The build-time shell capture phase (producer B, #699) stamps entries
1797
- // with THIS instance's version — the value folded into the shipped worker
1798
- // — never the discovery temp server's own version-plugin value (a
1799
- // different Date.now() stamp that would fail the serve-side gate).
1800
- api: {
1801
- getBuildVersion: () => currentVersion
1802
- },
1803
1815
  configResolved(config) {
1804
1816
  isDev = config.command === "serve";
1805
1817
  resolvedCacheDir = config.cacheDir ? String(config.cacheDir).replace(/\\/g, "/") : void 0;
@@ -1821,7 +1833,7 @@ function createVersionPlugin() {
1821
1833
  },
1822
1834
  load(id) {
1823
1835
  if (id === "\0" + VIRTUAL_IDS.version) {
1824
- return getVirtualVersionContent(currentVersion);
1836
+ return isDev ? getVirtualVersionContent(currentVersion) : getVirtualBuildVersionContent();
1825
1837
  }
1826
1838
  return null;
1827
1839
  },
@@ -2150,7 +2162,7 @@ if (command === "generate") {
2150
2162
  Modes:
2151
2163
  (default) Static parser with error on unresolvable includes
2152
2164
  --runtime Vite-based runtime discovery (100% coverage)
2153
- Requires vite and @vitejs/plugin-rsc
2165
+ Requires vite
2154
2166
  --static Static parser, accept partial output with warnings
2155
2167
 
2156
2168
  Options:
@@ -1,5 +1,6 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { NavigationLocation, NavigateOptionsInternal, TrackedActionState, InflightAction, RscMetadata, HandleData, StreamingToken } from "./types.js";
3
+ import { type LocationStateSnapshot } from "./react/location-state-shared.js";
3
4
  /**
4
5
  * Phase of a navigation operation
5
6
  */
@@ -196,6 +197,23 @@ export interface EventController {
196
197
  */
197
198
  resolvedIds?: string[]): void;
198
199
  getHandleState(): HandleState;
200
+ /**
201
+ * Keep the current handle state as the one the document's HTML was rendered
202
+ * with. initBrowserApp calls it before hydrateRoot, once the document's
203
+ * handle snapshot is applied.
204
+ */
205
+ freezeHydrationHandleState(): void;
206
+ /**
207
+ * The handle state the document's HTML was rendered with, once the live
208
+ * state has moved on from it; undefined while the live state is still that
209
+ * state (nothing changed since the freeze, or nothing was frozen: a tree
210
+ * that was mounted, an SSR pass). A render React is hydrating reads it
211
+ * (useHandle), whenever its boundary hydrates: the late handle channel
212
+ * (rsc-router.tsx) waits for the root to hydrate, not for a `<Suspense>`
213
+ * boundary that hydrates after it, and by then the live state can hold
214
+ * values that boundary's HTML does not (issue #1035).
215
+ */
216
+ getHydrationHandleState(): HandleState | undefined;
199
217
  /**
200
218
  * Update ONLY `routeSegmentIds` (what `useSegments` reads) from `matched`,
201
219
  * leaving `data` and `segmentOrder` (what `useHandle` collects over) untouched.
@@ -206,6 +224,20 @@ export interface EventController {
206
224
  setRouteSegmentIds(matched: string[]): void;
207
225
  setParams(params: Record<string, string>): void;
208
226
  getParams(): Record<string, string>;
227
+ /**
228
+ * Record the committed history entry's location state from `entryState`:
229
+ * the state object the commit pushed, restored or merged into the entry,
230
+ * so the entry and its readers have one source. Notifies nobody:
231
+ * NavigationProvider takes the snapshot with the entry's payload
232
+ * (LocationStateContext). `treeless` marks a commit no payload follows (a
233
+ * shallow navigation, a commit that keeps every segment, an action's
234
+ * state): the provider takes it with this commit's state notification, in
235
+ * the batch of the location.
236
+ */
237
+ commitLocationState(entryState: unknown, treeless?: boolean): void;
238
+ getLocationState(): LocationStateSnapshot;
239
+ /** True once after a `treeless` commit. */
240
+ takeTreelessLocationState(): boolean;
209
241
  getCurrentNavigation(): NavigationEntry | null;
210
242
  getInflightActions(): Map<string, ActionEntry>;
211
243
  /** Whether any concurrent actions have occurred (shared across all handles) */
@@ -37,8 +37,20 @@ export declare const SHELL_MISS_MARKER: string;
37
37
  */
38
38
  export declare function stripShellMissMarker(): void;
39
39
  /**
40
- * Merge server-set location state into the current history entry.
41
- * Replaces the current history state and dispatches notification event
42
- * so useLocationState hooks re-read from history.state.
40
+ * Remove the `clearOnReload` slots of the entry a document load starts on:
41
+ * the server rendered this document without them. One pass before hydration
42
+ * (initBrowserApp; renderRoute's `hydrate` mode), so no reader needs to know
43
+ * the page began as a document load, and a slot nobody reads is removed too.
44
+ * No server-set state exists yet to lose: a document response carries none
45
+ * (rsc-rendering.ts attaches it to partial payloads only).
46
+ *
47
+ * A slot is recognised by its key alone (the `~r` suffix): at start-up the
48
+ * module that defines it may not be loaded.
43
49
  */
44
- export declare function mergeLocationState(locationState: Record<string, unknown>): void;
50
+ export declare function clearLocationStateOnDocumentLoad(): void;
51
+ /**
52
+ * Merge server-set location state into the current history entry and return
53
+ * the entry state written. Readers see it when the caller commits it
54
+ * (EventController.commitLocationState).
55
+ */
56
+ export declare function mergeLocationState(locationState: Record<string, unknown>): Record<string, unknown>;
@@ -36,6 +36,11 @@ interface CommitOptions {
36
36
  * partial updater reuses it instead of deciding again.
37
37
  */
38
38
  transitionGatedOff?: boolean;
39
+ /**
40
+ * No payload follows this commit: every segment is kept and the tree on
41
+ * screen stays (EventController.commitLocationState).
42
+ */
43
+ treeless?: boolean;
39
44
  }
40
45
  /**
41
46
  * Options that can override the pre-configured commit settings
@@ -57,6 +62,8 @@ interface BoundCommitOverrides {
57
62
  serverState?: Record<string, unknown>;
58
63
  /** The committed route's name (payload metadata), remembered per history entry. */
59
64
  routeName?: string;
65
+ /** No payload follows this commit (CommitOptions.treeless). */
66
+ treeless?: boolean;
60
67
  }
61
68
  /**
62
69
  * Bound transaction with pre-configured commit options (without segmentIds/segments)
@@ -7,7 +7,9 @@
7
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 at boot.
10
+ * - Build version: the router's document version. It changes when a deploy
11
+ * changes the router's server code or the client assets, busting all cached
12
+ * prefetches at boot; a deploy that changes neither keeps them.
11
13
  * - Timestamp rotates on invalidation (server action, invalidateClientCache).
12
14
  *
13
15
  * Storage is a session cookie named by the server-resolved name passed to
@@ -2,6 +2,7 @@ import { type Context } from "react";
2
2
  import type { NavigationStore, NavigateOptionsInternal } from "../types.js";
3
3
  import type { EventController } from "../event-controller.js";
4
4
  import type { PrefetchStrategy } from "../../router/prefetch-default.js";
5
+ import type { LocationStateSnapshot } from "./location-state-shared.js";
5
6
  /**
6
7
  * Navigation context value provided by NavigationProvider
7
8
  *
@@ -55,3 +56,13 @@ export interface NavigationStoreContextValue {
55
56
  * Use useNavigation hook for reactive state access.
56
57
  */
57
58
  export declare const NavigationStoreContext: Context<NavigationStoreContextValue | null>;
59
+ /**
60
+ * The location state of the history entry whose tree is on screen, for
61
+ * useLocationState. NavigationProvider holds it as React state next to the
62
+ * payload and sets both in one update, so a render sees an entry's state
63
+ * exactly when it sees that entry's tree: a transition React holds renders
64
+ * the destination's value, an urgent render of the content still on screen
65
+ * (a reader mounting there included) the value of the entry being left
66
+ * (#1029). Undefined on the server and outside a provider.
67
+ */
68
+ export declare const LocationStateContext: Context<LocationStateSnapshot>;
@@ -9,12 +9,82 @@ export interface LocationStateEntry {
9
9
  readonly __rsc_ls_value: unknown;
10
10
  readonly __rsc_ls_lazy?: boolean;
11
11
  }
12
+ /** Prefix of every typed location-state slot in `history.state`. */
13
+ export declare const LOCATION_STATE_KEY_PREFIX: string;
14
+ export declare function isLocationStateKey(key: string): boolean;
15
+ /**
16
+ * `clearOnReload` is part of a slot's KEY (`<key>~r`); the value is always
17
+ * stored raw. Slots outlive deploys, so nothing that reads one may depend on a
18
+ * stored format: a definition without the option reads the plain key and never
19
+ * sees such a slot.
20
+ *
21
+ * An injected key ends in `#<ExportName>` (expose-id-utils.ts makeStubId; only
22
+ * identifier exports are keyed) and "~" cannot occur in an identifier, so no
23
+ * file or export name produces a suffix. withLocationStateKey rejects it.
24
+ */
25
+ export declare const LOCATION_STATE_KEY_SUFFIX_SEPARATOR: string;
26
+ export declare const CLEAR_ON_RELOAD_KEY_SUFFIX: string;
27
+ /**
28
+ * @internal The version the document loaded under (rsc-router.tsx
29
+ * initBrowserApp). Deliberately not getAppVersion(): a dev HMR bump moves that
30
+ * one inside a running session (bridge.updateVersion), and the state that
31
+ * session wrote must stay readable until the next document load.
32
+ */
33
+ export declare function setLocationStateVersion(version: string | undefined): void;
34
+ /**
35
+ * The state of a `{ state }` snapshot when its location state may be read:
36
+ * an object recorded under the client's version. Without a snapshot, the
37
+ * current entry (`window.history` is a `{ state }`), and nothing on the
38
+ * server.
39
+ */
40
+ export declare function readableLocationState(location?: {
41
+ readonly state: unknown;
42
+ } | undefined): Record<string, unknown> | undefined;
43
+ /**
44
+ * One history entry's location state as useLocationState readers receive it:
45
+ * its typed slots and plain `state`, or undefined when it holds none.
46
+ */
47
+ export type LocationStateSnapshot = Readonly<Record<string, unknown>> | undefined;
48
+ /**
49
+ * The snapshot of `entryState`: the state object a commit pushes, restores or
50
+ * merges into its history entry. Only state recorded under the client's
51
+ * version is in it (readableLocationState).
52
+ *
53
+ * The snapshot is a React context value, so identity decides who re-renders.
54
+ * It is `previous` itself when every slot holds the value `previous` holds:
55
+ * an entry without state after another, a primitive carried over, or the
56
+ * object a reader got from `previous` passed back by the navigation. Any
57
+ * other value is new, equal content included: a link clicked twice announces
58
+ * its state twice (a flash message, a scroll target). A new value is a
59
+ * structured clone, as history.state holds it, so a reader never shares an
60
+ * object with the caller of the navigation.
61
+ */
62
+ export declare function locationStateSnapshot(entryState: unknown, previous?: LocationStateSnapshot): LocationStateSnapshot;
63
+ /**
64
+ * Record the client's version on a state object that carries location state
65
+ * and no record yet. A client without a version (a renderRoute tree) writes
66
+ * none.
67
+ */
68
+ export declare function stampLocationState(state: Record<string, unknown>): void;
69
+ /**
70
+ * `entryState` plus `additions`, recorded under the client's version. The
71
+ * record is per entry, so location state another version left there is dropped
72
+ * first, with its record: under the new one it would be readable again. Router
73
+ * bookkeeping (idx, scroll key, intercept) is kept.
74
+ */
75
+ export declare function addLocationState(entryState: unknown, additions?: Record<string, unknown>): Record<string, unknown>;
12
76
  /**
13
77
  * Options for createLocationState
14
78
  */
15
79
  export interface LocationStateOptions {
16
80
  /** When true, the state is cleared from history after first read (flash message pattern) */
17
81
  flash?: boolean;
82
+ /**
83
+ * When true, the state does not survive a document load of its entry (a
84
+ * refresh, a back/forward that loads the document): the server rendered
85
+ * without it. Not together with `flash`.
86
+ */
87
+ clearOnReload?: boolean;
18
88
  }
19
89
  type LocationStateUnsafeFn = (...args: never[]) => unknown;
20
90
  type LocationStateUnsafeCtor = abstract new (...args: never[]) => unknown;
@@ -116,13 +186,16 @@ export interface LocationStateDefinition<TArgs extends unknown[], TState> {
116
186
  (...args: TArgs): LocationStateEntry;
117
187
  /**
118
188
  * Injected by Vite plugin - do not set manually. Unit tests without the
119
- * plugin use withLocationStateKey() from @rangojs/router/testing.
189
+ * plugin use withLocationStateKey() from @rangojs/router/testing. Reads back
190
+ * as the slot's key in `history.state`: the injected key plus the
191
+ * `clearOnReload` suffix.
120
192
  */
121
193
  __rsc_ls_key: string;
122
194
  /** Whether this state auto-clears after first read */
123
195
  readonly __rsc_ls_flash: boolean;
124
196
  /**
125
- * Read the current value from history.state.
197
+ * Read the current value from history.state. State the entry holds from
198
+ * another version of the app (an older build) reads as undefined.
126
199
  *
127
200
  * Returns undefined during SSR (no `window`). To stay hydration-safe, do
128
201
  * NOT call read() inline during the initial render — the server returns
@@ -145,13 +218,14 @@ export interface LocationStateDefinition<TArgs extends unknown[], TState> {
145
218
  /**
146
219
  * Statically write the value into the current history entry under this
147
220
  * definition's key, preserving any other keys already on history.state
148
- * (e.g. router bookkeeping, other LocationState slots).
221
+ * (e.g. router bookkeeping, other LocationState slots). Location state
222
+ * another version of the app left on the entry is dropped.
149
223
  *
150
- * This is the non-reactive counterpart to read(): it does not dispatch any
151
- * event, so components reading via useLocationState() will NOT re-render
152
- * until the next navigation/popstate. Use it when you only need the value
153
- * to be there on the next read() or on the next mount (including after
154
- * back/forward and hard refresh of the same entry).
224
+ * This is the non-reactive counterpart to read(): it changes the history
225
+ * entry and nothing a useLocationState() reader shows, whether the reader
226
+ * is mounted now or mounts later. read() returns the value at once; a
227
+ * reader gets it when the entry is next restored: back/forward to it, or
228
+ * (unless the definition sets `clearOnReload`) a hard refresh of it.
155
229
  *
156
230
  * Client-only: throws when called on the server (no history available).
157
231
  */
@@ -161,8 +235,8 @@ export interface LocationStateDefinition<TArgs extends unknown[], TState> {
161
235
  * leaving any other keys on history.state untouched. Idempotent: removing
162
236
  * a slot that isn't present is a no-op.
163
237
  *
164
- * Same non-reactive semantics as write(): no event is dispatched, so
165
- * useLocationState() readers will NOT re-render until the next navigation.
238
+ * Same non-reactive semantics as write(): useLocationState() readers keep
239
+ * the value until the entry is next restored.
166
240
  *
167
241
  * Client-only: throws when called on the server (no history available).
168
242
  */
@@ -1,10 +1,15 @@
1
1
  import { type LocationStateDefinition } from "./location-state-shared.js";
2
2
  export { createLocationState, isLocationStateEntry, resolveLocationStateEntries, type LocationStateEntry, type LocationStateDefinition, type LocationStateOptions, } from "./location-state-shared.js";
3
3
  /**
4
- * Hook to read location state from history.state
4
+ * Hook to read the location state of the history entry on screen
5
+ *
6
+ * A reader sees an entry's state together with that entry's tree: on a
7
+ * navigation the value changes in the React commit that brings the
8
+ * destination, so content a pending navigation keeps on screen keeps the
9
+ * state of the entry being left, for a reader that mounts there too (#1029).
5
10
  *
6
11
  * Behavior depends on the definition:
7
- * - Normal state: persists across navigations, reactive to popstate
12
+ * - Normal state: persists across navigations and back/forward
8
13
  * - Flash state (created with { flash: true }): read once, cleared after paint
9
14
  *
10
15
  * Overloaded:
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `subscribe` for a useSyncExternalStore whose snapshot nothing notifies: the
3
+ * hook is there for its server snapshot (useLocationState, useHandle), or the
4
+ * value only changes with a render (Link's non-adaptive prefetch strategy).
5
+ * One function, so its identity is stable across renders and hooks.
6
+ */
7
+ export declare const subscribeToNothing: () => () => void;
@@ -286,11 +286,10 @@ export declare class CacheScope {
286
286
  * @param pathname - URL pathname for cache key generation
287
287
  * @param params - Route params for cache key generation
288
288
  * @param isIntercept - Whether this is an intercept navigation (uses different cache key)
289
- * @param ownedPushes - How restoreHandles delivers the record's
290
- * loader-owned values (withCacheLookup: the handler context's
291
- * _claimLoaderPushes, and on a document HIT tail the owners it restores)
289
+ * @param ownedPushes - What the record is to each loader's handle pushes
290
+ * (withCacheLookup: loader-cache.ts loaderPins), called on a hit
292
291
  */
293
- lookupRoute(pathname: string, params: Record<string, string>, isIntercept?: boolean, ownedPushes?: OwnedPushDelivery): Promise<{
292
+ lookupRoute(pathname: string, params: Record<string, string>, isIntercept?: boolean, ownedPushes?: () => OwnedPushDelivery): Promise<{
294
293
  segments: ResolvedSegment[];
295
294
  shouldRevalidate: boolean;
296
295
  } | null>;
@@ -312,7 +311,7 @@ export declare class CacheScope {
312
311
  * A corrupt entry that was evicted reads as `miss`: the store was consulted
313
312
  * and holds nothing servable, which is the post-eviction truth.
314
313
  */
315
- lookupRouteDetailed(pathname: string, params: Record<string, string>, isIntercept?: boolean, ownedPushes?: OwnedPushDelivery): Promise<CacheRouteLookupOutcome>;
314
+ lookupRouteDetailed(pathname: string, params: Record<string, string>, isIntercept?: boolean, ownedPushes?: () => OwnedPushDelivery): Promise<CacheRouteLookupOutcome>;
316
315
  /**
317
316
  * Record this scope's segment-DSL cache({ tags }) into the request tag union
318
317
  * synchronously, under the same gate cacheRoute() uses for a write.
@@ -21,7 +21,8 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
21
21
  private readonly namespace?;
22
22
  private readonly explicitBaseUrl?;
23
23
  private readonly waitUntil?;
24
- private readonly version?;
24
+ /** The `version` option: used for every versioned family when set. */
25
+ private readonly explicitVersion?;
25
26
  private readonly edgeLookupTimeoutMs;
26
27
  private readonly edgeReadTimeoutMs;
27
28
  private readonly kvReadTimeoutMs;
@@ -327,9 +328,12 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
327
328
  * on the same context) read their markers as before.
328
329
  */
329
330
  private recordShellHit;
330
- /** The isolate marker memo's key for a tag: markers are per namespace and version. */
331
+ /**
332
+ * The isolate marker memo's key for a tag: markers are per namespace, and
333
+ * shared by every version (tagMarkerKey).
334
+ */
331
335
  private markerMemoKey;
332
- /** The memo key: one per namespace, build version, base URL, and shell key. */
336
+ /** The memo key: one per namespace, document version, base URL, and shell key. */
333
337
  private shellMemoKey;
334
338
  /**
335
339
  * Serve a memoized shell (shell-memo.ts). The tag-marker check is the only
@@ -400,9 +404,16 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
400
404
  private kvReadShellDocument;
401
405
  /** Promote a valid KV shell into the per-colo Cache API tier. */
402
406
  private promoteShellToL1;
407
+ private shellRequest;
408
+ private shellKVKey;
403
409
  /**
404
410
  * Convert string key to Request object for CF Cache API.
405
- * Includes version in URL if specified (for cache invalidation on code changes).
411
+ *
412
+ * `family` picks the version in the URL path, so a version change misses:
413
+ * `"data"` for segment entries and `fn:` items (cached RSC data, valid while
414
+ * the router's server code is unchanged), `"document"` for `doc:` responses
415
+ * and `shell2:` shells (their bytes carry asset URLs and the handler
416
+ * version), `null` for tag markers, which apply to every version.
406
417
  * @internal
407
418
  */
408
419
  private keyToRequest;
@@ -477,7 +488,13 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
477
488
  * @internal
478
489
  */
479
490
  private kvGetOrEvict;
480
- /** KV key for a tag's invalidation marker. */
491
+ /**
492
+ * KV key for a tag's invalidation marker. Unversioned: a content-hash
493
+ * version can come back (deploy A, deploy B, roll back to A), and a marker
494
+ * written while B was live has to be visible to A's entries. The staleness
495
+ * check compares the marker time with the entry's taggedAt and nothing else
496
+ * (isGloballyInvalidated), so one marker per tag serves every version.
497
+ */
481
498
  private tagMarkerKey;
482
499
  /**
483
500
  * Header entries carrying an entry's tags (JSON-encoded, comma-safe) and the
@@ -554,7 +571,11 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
554
571
  * @internal
555
572
  */
556
573
  private isL1Invalidated;
557
- /** Synthetic Cache API request for a tag's L1-cached invalidation marker. */
574
+ /**
575
+ * Synthetic Cache API request for a tag's L1-cached invalidation marker.
576
+ * Unversioned for the same reason as tagMarkerKey; the cache namespace
577
+ * (getCache) still scopes it.
578
+ */
558
579
  private tagMarkerRequest;
559
580
  /**
560
581
  * Read a tag's latest invalidation timestamp (or null if never invalidated)
@@ -276,11 +276,19 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
276
276
  */
277
277
  tagCacheTtl?: number;
278
278
  /**
279
- * Cache version string override. When this changes, all cached entries are
280
- * effectively invalidated (new keys won't match old entries).
279
+ * Cache version string override. When set, this exact value prefixes every
280
+ * versioned key the store writes (`v/{version}/...`), for cached data and
281
+ * stored HTML alike; changing it makes every earlier entry unreachable.
281
282
  *
282
- * Defaults to the auto-generated VERSION from the `@rangojs/router:version` virtual module.
283
- * Only set this if you need a custom versioning strategy.
283
+ * Leave it unset for the default: the store keys with the versions of the
284
+ * router serving the request. Segment entries and `"use cache"` items use
285
+ * the router's data version; document responses and PPR shells use its
286
+ * document version (see `createRouter({ version })`). Those are computed
287
+ * from the router's built code, so a deploy that does not change the router
288
+ * keeps its entries.
289
+ *
290
+ * Tag invalidation markers are never versioned: an invalidation applies to
291
+ * the entries of every version, including one that comes back in a rollback.
284
292
  */
285
293
  version?: string;
286
294
  /**
@@ -6,7 +6,7 @@
6
6
  * during segment resolution and stored alongside cached segments.
7
7
  */
8
8
  import type { ResolvedSegment } from "../types.js";
9
- import type { HandleStore } from "../server/handle-store.js";
9
+ import type { HandleStore, RecordAuthority } from "../server/handle-store.js";
10
10
  import type { HandleOwners, SegmentHandleData } from "./types.js";
11
11
  type HandleRecord = Record<string, SegmentHandleData>;
12
12
  /**
@@ -44,52 +44,25 @@ export declare function decodeHandleValue<T>(encoded: string): Promise<T | null>
44
44
  export declare function captureHandles(segments: ResolvedSegment[], handleStore: HandleStore): Record<string, SegmentHandleData>;
45
45
  /** The owners of captureHandles' values; undefined when none is owned. */
46
46
  export declare function captureHandleOwners(segments: ResolvedSegment[], handleStore: HandleStore): HandleOwners | undefined;
47
- /** How restoreHandles delivers a record's loader-owned values. */
48
- export interface OwnedPushDelivery {
49
- /**
50
- * Asked once per owner delivered through pushReplayed
51
- * (setupLoaderAccess _claimLoaderPushes); an owner it refuses is skipped.
52
- */
53
- claim?: (loaderId: string) => boolean;
54
- /**
55
- * A PPR shell's record (capture, document HIT tail, navigation replay): the
56
- * loaders the route runs on the live lane (liveLaneLoaderIds), marked on
57
- * the store (HandleStore.markLiveLane). A hole's values are replayed
58
- * unclaimed, so its own cache() HIT redelivers its entry's pushes in their
59
- * place (HandleStore.redeliverReplays), and its run ending without a push
60
- * drops them (HandleStore.settleLoaderRun).
61
- */
62
- liveLane?: ReadonlySet<string>;
63
- /**
64
- * A document HIT tail: every owner outside `liveLane` goes through
65
- * pushRestored and stands, unclaimed.
66
- */
67
- restore?: boolean;
68
- }
47
+ /**
48
+ * What the record being restored is to each loader's handle pushes on this
49
+ * request (HandleStore RecordAuthority). Built by loaderPins (loader-cache.ts)
50
+ * over the pins resolveLoaderData serves a loader's value from, so a value
51
+ * and its record copies get one answer.
52
+ */
53
+ export type OwnedPushDelivery = (loaderId: string) => RecordAuthority;
69
54
  /**
70
55
  * Restore handle data from a cached snapshot into the handle store.
71
56
  * Used when serving cached segments to replay their handle data: a route
72
57
  * cache() record owns its segments' arrays, so it REPLACES them.
73
58
  *
74
59
  * A segment with `owners` is replaced with empty arrays and re-pushed in
75
- * recorded order, an owned value through pushReplayed: if the owning loader
76
- * runs (a client navigation replaying a ppr route's cache() record runs its
77
- * `ssr: false` loaders), its live pushes replace the recorded ones in place
78
- * instead of appending a second copy. `owned.claim` is asked once per
79
- * owner, so the loader's own cache() HIT does not replay its pushes a second
80
- * time (loader-cache.ts replayLoaderHandles). A record without owners
81
- * restores as a plain replay.
82
- *
83
- * `owned.restore`: a document HIT tail restoring the shell's doc record
84
- * (withCacheLookup). A bake-lane owner's values are the prelude's, so they go
85
- * through pushRestored and stand: a run of that loader on the HIT (a
86
- * promise-carrying `ssr: false` loader, one whose record asks for a run)
87
- * reads the store, and its settled pushes are dropped, not swapped in.
88
- * Nothing is claimed, so the loader's own cache() HIT still replays the
89
- * pushes the record could not keep (the thenable ones pushRestored lets
90
- * through). A loader the route also runs on the live lane is a hole: its
91
- * values keep pushReplayed, unclaimed, and its live run replaces them
92
- * (#936), even inside a restored loader's body (`owned.liveLane`).
60
+ * recorded order. A loader-owned value stands (pushRestored) where `owned`
61
+ * says the record supplies that loader's value too ("pin", "copies"), and is
62
+ * a placeholder (pushPlaceholder) everywhere else: without `owned`, every
63
+ * owner. Neither claims its loader, so the loader's own cache() HIT still
64
+ * delivers (the thenable pushes a restored copy lets through, or its entry's
65
+ * pushes in a placeholder's place).
93
66
  */
94
67
  export declare function restoreHandles(handles: Record<string, SegmentHandleData>, handleStore: HandleStore, owners?: HandleOwners, owned?: OwnedPushDelivery): void;
95
68
  /**
@@ -99,14 +72,27 @@ export declare function restoreHandles(handles: Record<string, SegmentHandleData
99
72
  * restoreHandles would wipe those. `segmentId` redirects every value to one
100
73
  * segment (the caller's).
101
74
  *
102
- * Owner-keyed groups (useCacheRecordKey, `${seq}:${loaderId}`) are a loader's
103
- * pushes, which reach the page once per request: `claim` (setupLoaderAccess
104
- * _claimLoaderPushes) skips a loader that already ran or was replayed in this
105
- * request, and a claimed loader that runs later replaces its replayed values
106
- * (HandleStore.pushReplayed). `${seq}:` groups are the function's own pushes.
107
- * A key without ":" is a segment id (records written before owner keys) and
108
- * replays unchanged. A loader's own cache() replays through loader-cache.ts
109
- * replayLoaderHandles.
75
+ * The replay of every cached unit that records by owner: a "use cache"
76
+ * function (useCacheRecordKey) and a loader's own cache() (loader-cache.ts
77
+ * recordOwnerKey, with `unitLoader` the cached loader).
78
+ *
79
+ * A `${seq}:${loaderId}` group is that loader's pushes, which reach the page
80
+ * once per request: `claim` (setupLoaderAccess _claimLoaderPushes) is asked
81
+ * once per loader and skips one that already ran or was replayed in this
82
+ * request; a claimed loader that runs later replaces its replayed values
83
+ * (HandleStore.pushReplayed). A group without an owner (`${seq}:`, or a key
84
+ * without ":" from a record written before owner keys, which is a segment
85
+ * id) is the unit's own: `unitLoader`'s, or no loader's for a function,
86
+ * whose pushes are plain.
87
+ *
88
+ * The claimed loaders' placeholders give way to this delivery
89
+ * (HandleStore.replacePlaceholders), `unitLoader`'s even when the entry
90
+ * recorded no push for it: the entry stands for that loader's run.
91
+ *
92
+ * No `claim` means the caller's pushes are diverted off the page (a stale
93
+ * refresh: cache-runtime.ts refreshView, setupLoaderAccess
94
+ * _runLoaderIsolated): every group is delivered, into the diverting capture,
95
+ * and the page's placeholders stay, or the page would lose the push.
110
96
  */
111
- export declare function appendHandles(handles: Record<string, SegmentHandleData>, handleStore: HandleStore, segmentId?: string, claim?: (loaderId: string) => boolean): void;
97
+ export declare function appendHandles(handles: Record<string, SegmentHandleData>, handleStore: HandleStore, segmentId: string | undefined, claim: ((loaderId: string) => boolean) | undefined, unitLoader?: string): void;
112
98
  export {};
@@ -8,3 +8,5 @@ export { TRACKING_SEARCH_PARAMS, type CacheSearchParams, } from "./search-params
8
8
  export { CacheScope, createCacheScope } from "./cache-scope.js";
9
9
  export { createDocumentCacheMiddleware, type DocumentCacheOptions, } from "./document-cache.js";
10
10
  export type { CacheErrorCategory } from "./cache-error.js";
11
+ export { getCacheVersions } from "../server/build-version-table.js";
12
+ export type { RouterVersions } from "../router-versions.js";