@rangojs/router 0.32.0 → 0.33.1

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 (181) hide show
  1. package/README.md +3 -2
  2. package/dist/bin/browser-7KIJ5HSV.js +15 -0
  3. package/dist/bin/chunk-3B7CKBRO.js +463 -0
  4. package/dist/bin/chunk-524VACCG.js +71 -0
  5. package/dist/bin/chunk-LD7UBHT4.js +196 -0
  6. package/dist/bin/chunk-TO6CRWLN.js +1429 -0
  7. package/dist/bin/chunk-TTXC2U3G.js +2477 -0
  8. package/dist/bin/client-urls-projection-DQLWPDAT.js +28 -0
  9. package/dist/bin/forward-user-plugins-URBZKJP4.js +62 -0
  10. package/dist/bin/rango.js +42 -10173
  11. package/dist/bin/runtime-EPUDEHO4.js +596 -0
  12. package/dist/bin/runtime-discovery-3HH6BP3K.js +352 -0
  13. package/dist/bin/state-ACD2MGKJ.js +16 -0
  14. package/dist/bin/temp-rsc-server-JALTIWLT.js +3190 -0
  15. package/dist/types/bin/rango.d.ts +1 -0
  16. package/dist/types/browser/action-fence.d.ts +20 -0
  17. package/dist/types/browser/cookie-name.d.ts +51 -10
  18. package/dist/types/browser/invalidate-client-cache.d.ts +8 -6
  19. package/dist/types/browser/navigation-bridge.d.ts +36 -1
  20. package/dist/types/browser/navigation-client.d.ts +19 -0
  21. package/dist/types/browser/navigation-store-handle.d.ts +37 -4
  22. package/dist/types/browser/navigation-store.d.ts +2 -2
  23. package/dist/types/browser/navigation-transaction.d.ts +0 -1
  24. package/dist/types/browser/partial-update.d.ts +13 -0
  25. package/dist/types/browser/prefetch/cache.d.ts +96 -8
  26. package/dist/types/browser/prefetch/fetch.d.ts +26 -2
  27. package/dist/types/browser/prefetch/queue.d.ts +3 -2
  28. package/dist/types/browser/rango-state.d.ts +34 -5
  29. package/dist/types/browser/react/Link.d.ts +15 -6
  30. package/dist/types/browser/types.d.ts +25 -0
  31. package/dist/types/build/generate-route-types.d.ts +1 -1
  32. package/dist/types/build/route-types/include-resolution.d.ts +9 -1
  33. package/dist/types/build/route-types/router-processing.d.ts +3 -1
  34. package/dist/types/cache/cache-key-utils.d.ts +12 -8
  35. package/dist/types/cache/cache-scope.d.ts +4 -2
  36. package/dist/types/cache/cf/cf-cache-types.d.ts +9 -5
  37. package/dist/types/cache/document-cache-policy.d.ts +9 -1
  38. package/dist/types/cache/memory-segment-store.d.ts +3 -2
  39. package/dist/types/cache/segment-codec.d.ts +2 -2
  40. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  41. package/dist/types/cache/types.d.ts +12 -7
  42. package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -1
  43. package/dist/types/client-urls/types.d.ts +7 -5
  44. package/dist/types/prefetch-markers.d.ts +9 -0
  45. package/dist/types/prefetch-source-scope.d.ts +66 -0
  46. package/dist/types/route-definition/helpers-types.d.ts +8 -5
  47. package/dist/types/router/intercept-resolution.d.ts +25 -11
  48. package/dist/types/router/match-context.d.ts +1 -0
  49. package/dist/types/router/match-handlers.d.ts +1 -1
  50. package/dist/types/router/prefetch-cache-ttl.d.ts +11 -3
  51. package/dist/types/router/router-context.d.ts +1 -1
  52. package/dist/types/router/router-options.d.ts +6 -3
  53. package/dist/types/router/segment-resolution/loader-cache.d.ts +8 -0
  54. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +23 -7
  55. package/dist/types/router/segment-resolution/revalidation.d.ts +19 -0
  56. package/dist/types/router/source-reads.d.ts +53 -0
  57. package/dist/types/router/types.d.ts +1 -1
  58. package/dist/types/rsc/helpers.d.ts +5 -2
  59. package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
  60. package/dist/types/rsc/request-stages.d.ts +2 -0
  61. package/dist/types/rsc/shell-serve.d.ts +6 -1
  62. package/dist/types/server/context.d.ts +23 -6
  63. package/dist/types/server/cookie-store.d.ts +15 -1
  64. package/dist/types/server/document-location-state.d.ts +5 -2
  65. package/dist/types/server/handle-store.d.ts +2 -0
  66. package/dist/types/server/request-context.d.ts +43 -7
  67. package/dist/types/testing/e2e/server.d.ts +16 -1
  68. package/dist/types/testing/render-route.d.ts +66 -2
  69. package/dist/types/testing/serve-shell-request.d.ts +6 -3
  70. package/dist/types/types/handler-context.d.ts +7 -5
  71. package/dist/types/types/loader-types.d.ts +7 -4
  72. package/dist/types/types/segments.d.ts +15 -4
  73. package/dist/types/urls/path-helper-types.d.ts +7 -5
  74. package/dist/types/vite/temp-rsc-server.d.ts +2 -2
  75. package/dist/vite/index.js +449 -189
  76. package/package.json +2 -2
  77. package/skills/app-structure/SKILL.md +81 -0
  78. package/skills/app-structure/e-commerce.md +10 -9
  79. package/skills/app-structure/large-app.md +18 -2
  80. package/skills/caching/SKILL.md +10 -5
  81. package/skills/client-urls/SKILL.md +29 -11
  82. package/skills/comparison/references/framework-comparison.md +13 -8
  83. package/skills/document-cache/SKILL.md +8 -0
  84. package/skills/hooks/navigation.md +8 -8
  85. package/skills/hooks/outlets.md +4 -0
  86. package/skills/hooks/state.md +24 -8
  87. package/skills/intercept/SKILL.md +33 -12
  88. package/skills/layout/SKILL.md +60 -0
  89. package/skills/links/SKILL.md +101 -4
  90. package/skills/loader/SKILL.md +49 -22
  91. package/skills/migrate-nextjs/SKILL.md +8 -2
  92. package/skills/migrate-react-router/data-and-actions.md +5 -0
  93. package/skills/ppr/SKILL.md +26 -5
  94. package/skills/prerender/SKILL.md +4 -1
  95. package/skills/route/SKILL.md +6 -6
  96. package/skills/router-setup/SKILL.md +2 -1
  97. package/skills/server-actions/SKILL.md +41 -8
  98. package/skills/testing/client-components.md +29 -13
  99. package/skills/testing/serve-shell-request.md +7 -7
  100. package/skills/testing/server-actions.md +8 -0
  101. package/skills/ux-architecture/SKILL.md +70 -19
  102. package/src/bin/rango.ts +39 -13
  103. package/src/browser/action-fence.ts +72 -0
  104. package/src/browser/cookie-name.ts +82 -14
  105. package/src/browser/invalidate-client-cache.ts +9 -7
  106. package/src/browser/navigation-bridge.ts +164 -32
  107. package/src/browser/navigation-client.ts +95 -20
  108. package/src/browser/navigation-store-handle.ts +67 -5
  109. package/src/browser/navigation-store.ts +86 -17
  110. package/src/browser/navigation-transaction.ts +0 -6
  111. package/src/browser/partial-update.ts +31 -12
  112. package/src/browser/prefetch/cache.ts +188 -11
  113. package/src/browser/prefetch/fetch.ts +211 -51
  114. package/src/browser/prefetch/queue.ts +3 -2
  115. package/src/browser/rango-state.ts +50 -8
  116. package/src/browser/react/Link.tsx +20 -9
  117. package/src/browser/react/use-navigation.ts +1 -1
  118. package/src/browser/rsc-router.tsx +8 -1
  119. package/src/browser/server-action-bridge.ts +16 -11
  120. package/src/browser/types.ts +25 -0
  121. package/src/build/generate-route-types.ts +1 -0
  122. package/src/build/route-types/include-resolution.ts +40 -3
  123. package/src/build/route-types/router-processing.ts +170 -25
  124. package/src/build/route-types/scan-filter.ts +1 -0
  125. package/src/cache/cache-key-utils.ts +12 -8
  126. package/src/cache/cache-runtime.ts +5 -1
  127. package/src/cache/cache-scope.ts +28 -7
  128. package/src/cache/cf/cf-cache-types.ts +9 -5
  129. package/src/cache/document-cache-policy.ts +27 -0
  130. package/src/cache/document-cache.ts +5 -0
  131. package/src/cache/memory-segment-store.ts +3 -2
  132. package/src/cache/segment-codec.ts +2 -2
  133. package/src/cache/shell-snapshot.ts +1 -1
  134. package/src/cache/types.ts +12 -7
  135. package/src/cache/vercel/vercel-cache-store.ts +2 -1
  136. package/src/client-urls/types.ts +7 -5
  137. package/src/prefetch-markers.ts +10 -0
  138. package/src/prefetch-source-scope.ts +137 -0
  139. package/src/route-definition/helpers-types.ts +8 -5
  140. package/src/router/intercept-resolution.ts +94 -50
  141. package/src/router/loader-resolution.ts +8 -6
  142. package/src/router/match-api.ts +33 -29
  143. package/src/router/match-context.ts +5 -0
  144. package/src/router/match-handlers.ts +2 -1
  145. package/src/router/match-middleware/cache-lookup.ts +21 -13
  146. package/src/router/match-result.ts +39 -87
  147. package/src/router/navigation-snapshot.ts +18 -3
  148. package/src/router/prefetch-cache-ttl.ts +15 -3
  149. package/src/router/revalidation.ts +53 -23
  150. package/src/router/router-context.ts +2 -1
  151. package/src/router/router-options.ts +6 -3
  152. package/src/router/segment-resolution/loader-cache.ts +21 -3
  153. package/src/router/segment-resolution/prefetch-deferral.ts +38 -31
  154. package/src/router/segment-resolution/revalidation.ts +40 -12
  155. package/src/router/source-reads.ts +122 -0
  156. package/src/router/types.ts +2 -1
  157. package/src/router.ts +11 -12
  158. package/src/rsc/helpers.ts +8 -3
  159. package/src/rsc/prefetch-render-error.ts +7 -2
  160. package/src/rsc/progressive-enhancement.ts +3 -2
  161. package/src/rsc/request-stages.ts +59 -0
  162. package/src/rsc/rsc-rendering.ts +49 -33
  163. package/src/rsc/server-action.ts +2 -1
  164. package/src/rsc/shell-capture.ts +4 -5
  165. package/src/rsc/shell-serve.ts +12 -1
  166. package/src/segment-system.tsx +1 -1
  167. package/src/server/context.ts +37 -6
  168. package/src/server/cookie-store.ts +22 -1
  169. package/src/server/document-location-state.ts +23 -2
  170. package/src/server/handle-store.ts +2 -0
  171. package/src/server/request-context.ts +113 -28
  172. package/src/testing/dispatch.ts +3 -0
  173. package/src/testing/e2e/fixture.ts +4 -12
  174. package/src/testing/e2e/server.ts +91 -4
  175. package/src/testing/render-route.tsx +269 -4
  176. package/src/testing/serve-shell-request.ts +6 -3
  177. package/src/types/handler-context.ts +7 -5
  178. package/src/types/loader-types.ts +7 -4
  179. package/src/types/segments.ts +15 -4
  180. package/src/urls/path-helper-types.ts +7 -5
  181. package/src/vite/temp-rsc-server.ts +2 -2
@@ -1 +1,2 @@
1
+ #!/usr/bin/env node
1
2
  export {};
@@ -35,8 +35,28 @@
35
35
  * document. A future multi-router-in-one-document feature must not silently
36
36
  * inherit this global, cross-router cache suppression.
37
37
  */
38
+ /**
39
+ * Register what runs when the fence drops holding a write no landing took:
40
+ * the navigation store's markCachesStaleAfterWrite (navigation-store-handle.ts).
41
+ */
42
+ export declare function setUnlandedWriteHandler(handler: (() => void) | null): void;
38
43
  export declare function enterActionFence(): void;
39
44
  export declare function exitActionFence(): void;
45
+ /** A cookie read saw an external state rotation (rango-state.ts observer). */
46
+ export declare function noteStateRotation(): void;
47
+ /**
48
+ * A cookie read saw a write-only change (rango-state.ts write observer).
49
+ * Returns whether the fence is up and took it; with the fence down the
50
+ * caller marks the caches stale itself.
51
+ */
52
+ export declare function noteStateWrite(): boolean;
53
+ /** A landing marked every cache stale: a write seen so far is covered. */
54
+ export declare function takeStateWriteInActionFence(): void;
55
+ /**
56
+ * Whether the state rotated externally since the fence went up or the last
57
+ * landing took it, and clear it. False with the fence down.
58
+ */
59
+ export declare function takeStateRotationInActionFence(): boolean;
40
60
  export declare function isActionFenceActive(): boolean;
41
61
  /** Test-only: reset the refcount between cases. */
42
62
  export declare function __resetActionFence(): void;
@@ -36,31 +36,72 @@ export declare function stripPerClientSignals(headers: Headers): void;
36
36
  */
37
37
  export declare function getRawCookieValue(cookieString: string | null, name: string): string | null;
38
38
  /**
39
- * Encode a state value for the wire: `encodeURIComponent(version):timestamp`.
40
- * Only the build-derived version is encoded (it is arbitrary); the `:`
41
- * separator and numeric timestamp stay raw, so the `{version}:{timestamp}`
42
- * shape survives and `:` is a legal cookie-value octet.
39
+ * Encode a state value for the wire: `encodeURIComponent(version):timestamp`,
40
+ * plus `:write` when the write part is not 0. Only the build-derived version
41
+ * is encoded (it is arbitrary); the `:` separators and the numeric parts stay
42
+ * raw, so the shape survives and `:` is a legal cookie-value octet. A value no
43
+ * server action has bumped keeps the two-part shape it had before the write
44
+ * part existed.
45
+ */
46
+ export declare function encodeStateValue(version: string, timestamp: number, write?: number): string;
47
+ /**
48
+ * Parsed state value. `version` is decoded; `timestamp` (the invalidation
49
+ * part) and `write` are the raw integers. `write` is 0 for a two-part value.
43
50
  */
44
- export declare function encodeStateValue(version: string, timestamp: number): string;
45
- /** Parsed state value. `version` is decoded; `timestamp` is the raw integer. */
46
51
  export interface StateValue {
47
52
  version: string;
48
53
  timestamp: number;
54
+ write: number;
49
55
  }
50
56
  /**
51
- * Decode a wire value back into `{version, timestamp}`. Returns null for a
52
- * malformed value (no `:`, empty version, or non-numeric timestamp) so callers
53
- * mint fresh instead of trusting garbage.
57
+ * Decode a wire value back into `{version, timestamp, write}`. Accepts the
58
+ * two-part `{version}:{timestamp}` (a value no action bumped, or one an open
59
+ * tab wrote before the write part existed) and the three-part
60
+ * `{version}:{timestamp}:{write}`. Returns null for a malformed value (no `:`,
61
+ * empty version, a non-numeric part, or more parts) so callers mint fresh
62
+ * instead of trusting garbage.
54
63
  */
55
64
  export declare function decodeStateValue(raw: string): StateValue | null;
65
+ /**
66
+ * The `{version}:{timestamp}` part of a wire value: what every in-memory
67
+ * comparison on the client reads (the prefetch cache keys, the action
68
+ * fence's rotation check, the external-rotation observer). A change to it is
69
+ * a hard invalidation (a deploy, or invalidateClientCache() on either seat);
70
+ * a change to the write part alone is a server action's write, which marks
71
+ * caches stale and flushes nothing. The full value, write part included, is
72
+ * what requests send as `X-Rango-State`, so the browser HTTP cache (Vary)
73
+ * misses after every write. A raw cut, no decode: the version is
74
+ * percent-encoded, so the second `:` is always the write separator.
75
+ */
76
+ export declare function stateEpoch(raw: string): string;
56
77
  /**
57
78
  * Mint a fresh state value whose timestamp is strictly greater than the previous
58
79
  * one, so a re-mint inside the same millisecond (or a backward clock step) still
59
80
  * differs from the current value. `prevRaw` is the current wire value (the
60
81
  * client's in-memory mirror, or the server's inbound header/cookie) or null; its
61
- * timestamp is the floor. Shared by both seats so the monotonic guard lives once.
82
+ * timestamp is the floor. The write part starts over (left out): the new
83
+ * timestamp alone makes the value one no response was cached under. Shared by
84
+ * both seats so the monotonic guard lives once.
62
85
  */
63
86
  export declare function mintStateValue(version: string, prevRaw: string | null): string;
87
+ /**
88
+ * Bump the write part of `prevRaw`, keeping its version and timestamp: what
89
+ * the response of a server action without a directive sets. Keeping the
90
+ * version keeps the bump soft for a client still on another build (a form
91
+ * posted from a document served before a deploy); its own boot handles the
92
+ * deploy. Returns null when `prevRaw` is absent or malformed: a client with
93
+ * no state value sends no `X-Rango-State` and has nothing in the HTTP cache
94
+ * keyed by one.
95
+ *
96
+ * The new write part must be one no response was cached under. It is
97
+ * strictly greater than the previous one, and it is the current time in
98
+ * microseconds with random low digits: two actions that start from the same
99
+ * value (two tabs, or two in flight in one tab) can run in the same server
100
+ * millisecond (a Worker's clock does not move between I/O), and the second
101
+ * response must not set the value the first one set, under which a prefetch
102
+ * between them may have been cached with the second write missing.
103
+ */
104
+ export declare function bumpStateWrite(prevRaw: string | null): string | null;
64
105
  /**
65
106
  * Attribute string for the state cookie. Session cookie (no Max-Age/Expires),
66
107
  * Path=/ (whole app), SameSite=Lax (sent on top-level navigations), and Secure
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * Client seat of `invalidateClientCache()` (the `default` export condition).
3
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.
4
+ * The full client invalidation, more than a server action without a
5
+ * directive does: the history cache is marked stale (SWR), the prefetch map
6
+ * is flushed (in-flight prefetches aborted, visible links prefetch again), the
7
+ * state rotates, and sibling tabs are broadcast to, which adopt the new state
8
+ * and flush too (`invalidateCacheAndBroadcast()`). A directive-free action
9
+ * only marks history and prefetches stale and rotates nothing
10
+ * (`markCacheAsStaleAndBroadcast()`). History is marked stale, not cleared, so
11
+ * Back renders the cached entry instantly and revalidates.
10
12
  */
11
13
  export declare function invalidateClientCache(): void;
12
14
  /**
@@ -1,6 +1,6 @@
1
1
  import type { NavigationBridge, NavigationBridgeConfig, NavigationStore } from "./types.js";
2
2
  import { createNavigationTransaction } from "./navigation-transaction.js";
3
- import type { EventController } from "./event-controller.js";
3
+ import type { EventController, NavigationEntry } from "./event-controller.js";
4
4
  import type { PrefetchStrategy } from "../router/prefetch-default.js";
5
5
  export { createNavigationTransaction };
6
6
  /**
@@ -22,6 +22,41 @@ export interface NavigationBridgeDelegatedPrefetchOptions {
22
22
  }
23
23
  /** Register delegated anchor prefetch with the production bridge wiring. */
24
24
  export declare function setupNavigationBridgeDelegatedPrefetch(store: NavigationStore, eventController: EventController, getVersion: () => string | undefined, options?: NavigationBridgeDelegatedPrefetchOptions): () => void;
25
+ /**
26
+ * The request a stale-prefetch revalidation makes (scheduleStalePrefetch-
27
+ * Revalidation): the ids it holds, and the intercept and action error
28
+ * boundary on screen.
29
+ */
30
+ export interface StalePrefetchRevalidation {
31
+ segmentIds: string[];
32
+ intercept: {
33
+ isIntercept: boolean;
34
+ sourceUrl?: string;
35
+ };
36
+ boundary: string | undefined;
37
+ }
38
+ /**
39
+ * A click that adopted a prefetch a server action marked stale
40
+ * (prefetch/cache.ts isPrefetchStale) committed the pre-write payload, as any
41
+ * prefetched click commits. Once that navigation settled
42
+ * (NavigationEntry.settled: its commit, any `prefetch: false` fill and its
43
+ * stream, so this never supersedes the click nor cuts a response
44
+ * that arrived short), `revalidate` runs the page's request in
45
+ * Back's lane (the bridge's revalidateInBackground), with the ids on screen
46
+ * and the action error boundary it shows held, as Back onto a stale entry
47
+ * does.
48
+ *
49
+ * The entry is marked stale first, so a Back onto it revalidates when this
50
+ * one never ran or was left. Nothing runs when the click did not commit, or
51
+ * when the page moved since (a newer navigation aborts the click's entry, a
52
+ * shallow one moves the history key) or another navigation is open. An
53
+ * action in flight does not hold it back, as it does not hold Back's: its
54
+ * follow-up, when it owes one, starts a navigation that supersedes this one.
55
+ *
56
+ * Exported so renderRoute (testing/render-route.tsx) drives these guards and
57
+ * this plan rather than a copy of them.
58
+ */
59
+ export declare function scheduleStalePrefetchRevalidation(store: Pick<NavigationStore, "getCachedSegments" | "getHistoryKey" | "updateCacheHandleData">, eventController: Pick<EventController, "getUnsettledNavigation">, entry: Pick<NavigationEntry, "settled">, historyKey: string, revalidate: (request: StalePrefetchRevalidation) => void): void;
25
60
  /**
26
61
  * Create a navigation bridge for handling client-side navigation
27
62
  *
@@ -1,4 +1,23 @@
1
1
  import type { NavigationClient, RscBrowserDependencies } from "./types.js";
2
+ import { type DecodedPrefetch } from "./prefetch/cache.js";
3
+ import type { SourcePage } from "../prefetch-source-scope.js";
4
+ /**
5
+ * The completed prefetch a navigation adopts: the one stored under its
6
+ * source key, else one stored for a group of source pages the page is in
7
+ * (`paramsKeys`, paramsCandidateKeys), else the one every source page
8
+ * shares. None when the navigation cannot use a prefetch. A stale one only
9
+ * when `acceptStale` (FetchPartialOptions.acceptStalePrefetch).
10
+ */
11
+ export declare function consumeStoredPrefetch(canUsePrefetch: boolean, sourceKey: string, paramsKeys: readonly string[], wildcardKey: string, acceptStale?: boolean): {
12
+ entry: DecodedPrefetch | null;
13
+ hitKey: string | null;
14
+ };
15
+ /**
16
+ * Whether a navigation from `page` may serve an in-flight prefetch it
17
+ * adopted through the wildcard key: not one the server scoped to its own
18
+ * source page, nor one scoped to a group of source pages `page` is not in.
19
+ */
20
+ export declare function wildcardAdoptionFits(entry: DecodedPrefetch, page: SourcePage | null): boolean;
2
21
  /**
3
22
  * Create a navigation client for fetching RSC payloads
4
23
  *
@@ -7,19 +7,52 @@
7
7
  * receive it by argument: the jar-divergence observer (below) and the client
8
8
  * seat of `invalidateClientCache()` (added later).
9
9
  *
10
- * Dependency-light on purpose: it imports only `setRangoStateObserver` and the
11
- * store type, so pulling it into the default root entry does not drag the
12
- * navigation store into bundles that previously lacked it.
10
+ * Dependency-light on purpose: it imports only the action fence, the
11
+ * rango-state observers and the store type, so pulling it into the default
12
+ * root entry does not drag the navigation store into bundles that previously
13
+ * lacked it.
13
14
  */
14
15
  import type { NavigationStore } from "./types.js";
16
+ import type { SourcePage } from "../prefetch-source-scope.js";
15
17
  /**
16
18
  * Register the active navigation store at boot, and wire the jar-divergence
17
19
  * observer: when a per-request cookie read detects an EXTERNAL rotation (a
18
20
  * sibling tab, a server `Set-Cookie`, or a cookie clear), mark this tab's
19
21
  * history cache stale. The history cache is not state-keyed, so the value
20
22
  * rotation alone does not reach it. No broadcast, no prefetch clear, no
21
- * re-rotation — the value already changed externally.
23
+ * re-rotation — the value already changed externally. A rotation seen while
24
+ * an action is in flight is recorded on the fence: the action's landing then
25
+ * sends the full broadcast (navigation-store.ts markCacheAsStaleAndBroadcast).
26
+ *
27
+ * A read that sees only the write part change (a server action's response,
28
+ * in this tab or another) marks this tab's history entries and prefetches
29
+ * stale, as a soft broadcast's receiver does: a tab the broadcast does not
30
+ * reach would otherwise serve a prefetch made before the write as fresh.
31
+ * While an action is in flight the write is recorded on the fence instead:
32
+ * the action's landing marks every cache stale once (navigation-store.ts
33
+ * markStaleSoftAndBroadcast), and when no landing does, the fence's drop
34
+ * marks them.
22
35
  */
23
36
  export declare function registerNavigationStore(store: NavigationStore): void;
24
37
  /** The active navigation store, or null before boot has registered it. */
25
38
  export declare function getRegisteredStore(): NavigationStore | null;
39
+ /**
40
+ * Register the reader of the committed page's params and route name: the
41
+ * router store's own state, which useParams reads (the event controller's
42
+ * params, the displayed history entry's route name). Read by the prefetch
43
+ * cache outside render, at request time, to look up an answer shared by the
44
+ * pages with equal values of what a revalidate() rule read
45
+ * (prefetch-source-scope.ts). A reader, not a copy: there is no second
46
+ * source of truth for the params.
47
+ */
48
+ export declare function registerCommittedPageReader(read: () => SourcePage): void;
49
+ /**
50
+ * The boot reader (rsc-router.tsx): the event controller's params, which
51
+ * useParams reads and only a commit sets (an optimistic clientUrls() frame
52
+ * sets React context, not them), and the displayed entry's route name.
53
+ */
54
+ export declare function committedPageOf(controller: {
55
+ getParams(): Record<string, string>;
56
+ }, store: Pick<NavigationStore, "getHistoryEntryMemory">): () => SourcePage;
57
+ /** The committed page, or null before boot has registered its reader. */
58
+ export declare function getCommittedPage(): SourcePage | null;
@@ -55,8 +55,8 @@ export interface NavigationStoreConfig {
55
55
  cacheSize?: number;
56
56
  /**
57
57
  * Enable cross-tab cache invalidation via BroadcastChannel (default: true)
58
- * When cache is cleared (via server actions or invalidateClientCache()),
59
- * other tabs will also clear their cache
58
+ * After a server action other tabs that share a segment mark their caches
59
+ * stale; after invalidateClientCache() they adopt its state and flush
60
60
  */
61
61
  crossTabSync?: boolean;
62
62
  /**
@@ -87,7 +87,6 @@ export interface CommitResult {
87
87
  * Bound transaction with pre-configured commit options (without segmentIds/segments)
88
88
  */
89
89
  export interface BoundTransaction {
90
- readonly currentUrl: string;
91
90
  /** Bound `replace` option (history replace instead of push). */
92
91
  readonly replace?: boolean;
93
92
  /** Bound `traversal` option: a back/forward navigation. */
@@ -114,6 +114,19 @@ export type UpdateMode = {
114
114
  refresh?: boolean;
115
115
  /** The cross-tab refresh's held boundary (see `action`). */
116
116
  boundary?: string;
117
+ /**
118
+ * A click's: told of each response this navigation fetches, true when
119
+ * it is a prefetch a server action marked stale
120
+ * (FetchPartialResult.stalePrefetch), false otherwise, so a fresh
121
+ * refetch after it (a fill's start-over, an HMR retry) clears it. Its
122
+ * presence is what lets a stale prefetch answer
123
+ * (FetchPartialOptions.acceptStalePrefetch): only a navigation that
124
+ * revalidates afterwards takes one. The click commits it as it commits
125
+ * any prefetch; the bridge revalidates the page once the navigation
126
+ * settled (navigation-bridge.ts navigate). Nothing here changes how or
127
+ * when this commit lands.
128
+ */
129
+ onStalePrefetch?: (stale: boolean) => void;
117
130
  } | {
118
131
  type: "leave-intercept";
119
132
  interceptSourceUrl?: string;
@@ -7,11 +7,14 @@
7
7
  * stream resolves the route's client references, so the route's JS chunks are
8
8
  * imported during prefetch rather than on click. The decoded payload is reused
9
9
  * verbatim by navigation, so a prefetched click loads no new code. Two key
10
- * scopes are in play:
10
+ * scopes are in play. Wherever a key below says `rangoState`, callers pass the
11
+ * state's `{version}:{timestamp}` part (cookie-name.ts stateEpoch), not the
12
+ * whole value: a server action's write bump leaves the keys as they were.
11
13
  * - Wildcard (default): built by `buildPrefetchKey(rangoState, target)` —
12
14
  * shape `rangoState\0/target?...`. Shared across all source pages and
13
15
  * source segment trees; `_rsc_segments` is omitted from this key. Invalidated
14
- * automatically when Rango state bumps (deploy or server-action invalidation).
16
+ * automatically when Rango state bumps (deploy or invalidateClientCache()).
17
+ * A server action marks entries stale instead (the stale epoch, below).
15
18
  * - Source-scoped: built by `buildSourceKey(rangoState, sourceHref, target)`
16
19
  * — shape `rangoState\0sourceHref\0/target?...`. Embeds the Rango state
17
20
  * (so rotation invalidates source-scoped entries too), the source href, and
@@ -20,6 +23,14 @@
20
23
  * targets, matched or not), OR when a Link opts in with
21
24
  * `prefetchKey=":source"` — in both cases so source-sensitive responses
22
25
  * cannot bleed into navigations from other pages.
26
+ * - Params-scoped: built by `buildParamsKey(rangoState, label, target)` —
27
+ * shape `rangoState\0p:<label>\0/target?...`. Populated when the server
28
+ * tags a response with `X-RSC-Prefetch-Scope: params; <label>`: a
29
+ * revalidate() rule read the source's params or route name and kept a
30
+ * segment, so the answer is right for every page with equal values of
31
+ * what it read (prefetch-source-scope.ts). `paramsShapes` remembers, per
32
+ * target, what the stored labels read, so a lookup rebuilds the label of
33
+ * the page on screen (paramsCandidateKeys).
23
34
  *
24
35
  * Also tracks in-flight prefetch promises. Each promise resolves to the
25
36
  * decoded prefetch entry (or null), letting navigation adopt a
@@ -39,6 +50,7 @@
39
50
  * effectively per-document. Unit tests reset them via clearPrefetchCache().
40
51
  */
41
52
  import type { RscPayload } from "../types.js";
53
+ import { type SourcePage } from "../../prefetch-source-scope.js";
42
54
  /**
43
55
  * A prefetch that has been fetched AND eagerly decoded. Storing the decoded
44
56
  * payload (not the raw Response) is what makes a prefetched navigation "warm":
@@ -58,9 +70,23 @@ export interface DecodedPrefetch {
58
70
  * Prefetch scope as tagged by the server via `X-RSC-Prefetch-Scope`.
59
71
  * `"source"` means the response is source-page-sensitive and must not be
60
72
  * reused by a navigation from a different page — navigation enforces this
61
- * when it adopted an inflight entry through the wildcard key.
73
+ * when it adopted an inflight entry through the wildcard key. `"params"`
74
+ * means it serves the pages whose label equals `paramsLabel`.
62
75
  */
63
- scope: "source" | "wildcard";
76
+ scope: PrefetchScope;
77
+ /** The label of a `"params"` entry (prefetch-source-scope.ts). */
78
+ paramsLabel?: string;
79
+ /**
80
+ * The stale epoch (`currentPrefetchEpoch()`) the prefetch's fetch started
81
+ * under. A server action without a directive marks every prefetch stale
82
+ * (`markPrefetchesStale()` bumps the epoch) instead of flushing it: an entry
83
+ * from an older epoch is still served, and the navigation that adopts it
84
+ * revalidates its page once it settled (`isPrefetchStale`). A stale entry
85
+ * is served at most once, and only to a click (`consumePrefetch`'s
86
+ * `acceptStale`). A respawn keeps the original fetch's epoch, as it keeps
87
+ * its timestamp.
88
+ */
89
+ epoch: number;
64
90
  /**
65
91
  * Synchronously-readable flag, flipped to true when `streamComplete` resolves
66
92
  * (the entire RSC stream has drained). Navigation reads this at click time to
@@ -101,6 +127,8 @@ export interface DecodedPrefetch {
101
127
  */
102
128
  dispose?: () => void;
103
129
  }
130
+ /** Where a prefetch is stored: see the module comment. */
131
+ export type PrefetchScope = "source" | "params" | "wildcard";
104
132
  /**
105
133
  * Initialize the prefetch cache with the configured TTL and max size.
106
134
  * Called once at app startup with the values from server metadata. Each
@@ -121,7 +149,7 @@ export declare function isPrefetchCacheDisabled(): boolean;
121
149
  * one of:
122
150
  * - Wildcard (source-agnostic): prefix is the Rango state value from
123
151
  * `getRangoState()`. Shared across all source pages. Invalidated
124
- * automatically when Rango state bumps (deploy or server-action).
152
+ * automatically when Rango state bumps (deploy or invalidateClientCache()).
125
153
  * `_rsc_segments` is omitted so sibling payloads prefetched from one page
126
154
  * remain reusable after another prefetched navigation commits. Key shape:
127
155
  * `rangoState\0/target?...`.
@@ -148,8 +176,38 @@ export declare function buildPrefetchKey(prefix: string, targetUrl: URL): string
148
176
  * - `sourceHref` makes the key unique per originating page.
149
177
  */
150
178
  export declare function buildSourceKey(rangoState: string, sourceHref: string, targetUrl: URL): string;
179
+ /**
180
+ * Build a params-scoped cache key. Key shape:
181
+ * `rangoState\0p:<label>\0/target?...`, the target normalized like the
182
+ * wildcard key's: the answer serves every page with this label, whatever
183
+ * segments it holds, as a wildcard answer serves every page.
184
+ */
185
+ export declare function buildParamsKey(rangoState: string, label: string, targetUrl: URL): string;
186
+ /**
187
+ * Remember that a params-scoped answer for `targetUrl` is stored under a
188
+ * label of this shape. Called where the response header is read, with the
189
+ * store (fetch.ts executePrefetchFetch).
190
+ */
191
+ export declare function rememberParamsShape(targetUrl: URL, label: string): void;
192
+ /**
193
+ * The params-scoped keys a request for `targetUrl` from `page` may adopt:
194
+ * the page's label under each shape stored for the target, the shapes that
195
+ * read the most first. Empty without a page (before boot).
196
+ */
197
+ export declare function paramsCandidateKeys(rangoState: string, targetUrl: URL, page: SourcePage | null): string[];
198
+ /**
199
+ * Whether `page` is in the group a params-scoped entry serves: its label
200
+ * under the entry's shape equals the entry's.
201
+ */
202
+ export declare function pageMatchesParamsLabel(label: string, page: SourcePage | null): boolean;
151
203
  /**
152
204
  * Check if a prefetch is already cached, in-flight, or queued for the given key.
205
+ *
206
+ * A stale stored entry (`isPrefetchStale`) counts as absent and is dropped, so
207
+ * a new hover or viewport trigger replaces it with a fresh prefetch, and a
208
+ * click made while that one is in flight adopts it rather than the stale one.
209
+ * An in-flight fetch counts whatever its epoch: it is never aborted, and a
210
+ * duplicate request for its key would race it for the slot.
153
211
  */
154
212
  export declare function hasPrefetch(key: string): boolean;
155
213
  /**
@@ -163,10 +221,19 @@ export declare function hasPrefetch(key: string): boolean;
163
221
  * TTL bounds the age of the data, not of the latest adoption. Without
164
222
  * `respawn` the entry is deleted (legacy one-shot behavior).
165
223
  *
224
+ * A stale entry (`isPrefetchStale`) is served only to a caller that passes
225
+ * `acceptStale` (a click, which revalidates its page once it settled,
226
+ * navigation-bridge.ts): for any other caller it is a miss and stays in its
227
+ * slot. It is served at most once: its slot is not re-armed, so a second
228
+ * click on the same link fetches fresh rather than showing pre-write data
229
+ * older than what the first click's revalidation showed.
230
+ *
166
231
  * Does NOT check in-flight prefetches — use consumeInflightPrefetch()
167
232
  * for that (returns a Promise instead of a resolved entry).
168
233
  */
169
- export declare function consumePrefetch(key: string): DecodedPrefetch | null;
234
+ export declare function consumePrefetch(key: string, options?: {
235
+ acceptStale?: boolean;
236
+ }): DecodedPrefetch | null;
170
237
  /**
171
238
  * Consume an in-flight prefetch promise. Returns null if no prefetch is
172
239
  * in-flight for this key. The returned Promise resolves to the decoded
@@ -188,9 +255,14 @@ export declare function consumeInflightPrefetch(key: string): Promise<DecodedPre
188
255
  * Store an eagerly-decoded prefetch in the in-memory cache.
189
256
  *
190
257
  * Skips storage if the generation has changed since the fetch started
191
- * (a server action invalidated the cache mid-flight).
258
+ * (the cache was flushed mid-flight). An entry from an older stale epoch (an
259
+ * action marked prefetches stale mid-flight) is stored, and stays stale.
260
+ *
261
+ * `inflightKey` is the key the prefetch was in flight under, when the
262
+ * storage key is not one of its aliases (a params-scoped answer): a
263
+ * navigation that adopted it marked the aliases.
192
264
  */
193
- export declare function storePrefetch(key: string, entry: DecodedPrefetch, fetchGeneration: number): void;
265
+ export declare function storePrefetch(key: string, entry: DecodedPrefetch, fetchGeneration: number, inflightKey?: string): void;
194
266
  /**
195
267
  * Remove a single stored prefetch entry. Used to evict an entry whose body
196
268
  * stream stalled after headers arrived (its payload / streamComplete never
@@ -210,6 +282,22 @@ export declare function removePrefetch(key: string, entry: DecodedPrefetch): voi
210
282
  * storePrefetch so it can detect stale completions.
211
283
  */
212
284
  export declare function currentGeneration(): number;
285
+ /**
286
+ * Capture the current stale epoch. A prefetch fetch stamps its entry with the
287
+ * value read when it started (`DecodedPrefetch.epoch`).
288
+ */
289
+ export declare function currentPrefetchEpoch(): number;
290
+ /** Whether an action marked prefetches stale after this entry's fetch began. */
291
+ export declare function isPrefetchStale(entry: Pick<DecodedPrefetch, "epoch">): boolean;
292
+ /**
293
+ * Mark every stored and in-flight prefetch stale: what a server action
294
+ * without a directive does (navigation-store.ts markCacheAsStaleAndBroadcast).
295
+ * Only bumps the epoch. Nothing is dropped, aborted or rotated, and visible
296
+ * links are not told (no `notifyPrefetchCacheInvalidated()`), so they do not
297
+ * prefetch again. A click on a stale entry commits it at once; the navigation
298
+ * then revalidates its page (navigation-bridge.ts navigate).
299
+ */
300
+ export declare function markPrefetchesStale(): void;
213
301
  export declare function markPrefetchInflight(key: string): void;
214
302
  export declare function setInflightPromise(key: string, promise: Promise<DecodedPrefetch | null>): void;
215
303
  /**
@@ -14,6 +14,7 @@
14
14
  * a prefetch that is still downloading/decoding instead of starting a
15
15
  * duplicate request.
16
16
  */
17
+ import { type PrefetchScope } from "./cache.js";
17
18
  import type { RscPayload } from "../types.js";
18
19
  /**
19
20
  * Decoder injected at app startup (see setPrefetchDecoder). This is
@@ -29,9 +30,31 @@ type PrefetchDecoder = (response: Promise<Response>) => Promise<RscPayload>;
29
30
  * browser app always sets it before any Link can fire.
30
31
  */
31
32
  export declare function setPrefetchDecoder(fn: PrefetchDecoder): void;
33
+ /** Where a completed prefetch is stored, and the label of a params scope. */
34
+ export interface PrefetchSlot {
35
+ scope: PrefetchScope;
36
+ key: string;
37
+ paramsLabel?: string;
38
+ }
39
+ /**
40
+ * Where a completed prefetch is stored: under the source key when the Link
41
+ * forced it or the response says `X-RSC-Prefetch-Scope: source`; under the
42
+ * label's key when it says `params; <label>`, for the pages with equal values
43
+ * of what a revalidate() rule read; else under the key every source page
44
+ * shares (#1331). `stateKey` is the state's epoch the other keys were built
45
+ * with (cookie-name.ts stateEpoch).
46
+ */
47
+ export declare function prefetchStorageSlot(forceSourceScope: boolean, scopeHeader: string | null, sourceKey: string, wildcardKey: string, stateKey: string, targetUrl: URL): PrefetchSlot;
32
48
  /**
33
49
  * Prefetch (direct): fetch with low priority and store in in-memory cache.
34
- * Used by hover strategy -- fires immediately without queueing.
50
+ * Used by hover (on hover and viewport links, and delegated anchors) and
51
+ * useRouter().prefetch() -- fires immediately without queueing.
52
+ *
53
+ * An intent prefetch: the visitor is about to click, so it asks for the full
54
+ * response the navigation would get, `prefetch: false` loaders and flagged
55
+ * `loading()` work included. It runs only on a miss: a cached prefetch or one
56
+ * in flight, even a speculative one that deferred work, is used as it is,
57
+ * and its click sends the fill as before (docs/design/prefetch-false.md, R16).
35
58
  *
36
59
  * By default the wildcard key (Rango-state-keyed) is used for inflight
37
60
  * dedup and for responses that are not source-sensitive; source-scoped
@@ -44,7 +67,8 @@ export declare function setPrefetchDecoder(fn: PrefetchDecoder): void;
44
67
  export declare function prefetchDirect(url: string, segmentIds: string[], version?: string, routerId?: string, prefetchKey?: ":source"): void;
45
68
  /**
46
69
  * Prefetch (queued): goes through the concurrency-limited queue.
47
- * Used by viewport/render strategies to avoid flooding the server.
70
+ * Used by viewport/render strategies to avoid flooding the server. A
71
+ * speculative prefetch: the server defers `prefetch: false` work in it.
48
72
  * Returns the inflight key (wildcard by default, source-scoped when
49
73
  * `prefetchKey=":source"` is passed).
50
74
  */
@@ -42,7 +42,8 @@ export declare function enqueuePrefetch(key: string, execute: (signal: AbortSign
42
42
  export declare function cancelAllPrefetches(keepUrl?: string | null): void;
43
43
  /**
44
44
  * Hard-cancel everything including in-flight prefetches.
45
- * Used by clearPrefetchCache (server action invalidation) where
46
- * in-flight responses would be stale.
45
+ * Used by clearPrefetchCache (invalidateClientCache(), a deploy) where
46
+ * in-flight responses would be stale. A server action never calls it: it
47
+ * marks prefetches stale and lets in-flight ones finish (cache.ts).
47
48
  */
48
49
  export declare function abortAllPrefetches(): void;
@@ -6,11 +6,25 @@
6
6
  * requests; the server responds with `Vary: X-Rango-State`, so the browser HTTP
7
7
  * cache keys responses by (URL, X-Rango-State value).
8
8
  *
9
- * Value format: `{buildVersion}:{invalidationTimestamp}`
9
+ * Value format: `{buildVersion}:{invalidationTimestamp}[:{write}]`
10
10
  * - Build version: the router's document version. It changes when a deploy
11
11
  * changes the router's server code or the client assets, busting all cached
12
12
  * prefetches at boot; a deploy that changes neither keeps them.
13
- * - Timestamp rotates on invalidation (server action, invalidateClientCache).
13
+ * - Timestamp rotates on invalidation (invalidateClientCache, either seat).
14
+ * - Write: bumped by the response of a server action without a directive (its
15
+ * `Set-Cookie`, server side, so a no-JS action bumps it too); left out while
16
+ * 0. It changes the `X-Rango-State` every tab and every reload sends, so
17
+ * responses the browser HTTP cache holds from before the write miss.
18
+ *
19
+ * The client's in-memory comparisons read only `{buildVersion}:{timestamp}`
20
+ * (`stateEpoch`): the prefetch cache keys, the action fence's rotation check,
21
+ * the external-rotation observer below and the cross-tab adopt. A write-only
22
+ * change is soft: prefetches and history entries are marked stale and nothing
23
+ * is flushed. The tab that ran the action marks its own when it lands
24
+ * (navigation-store.ts markStaleSoftAndBroadcast) and tells the tabs that
25
+ * share a segment with it. Any other tab marks its own when a read sees the
26
+ * write part change (the write observer below), so a tab that ignores the
27
+ * broadcast or has crossTabSync off does not serve a pre-write prefetch fresh.
14
28
  *
15
29
  * Storage is a session cookie named by the server-resolved name passed to
16
30
  * initRangoState (`{prefix}_{routerId}`, default prefix `rango-state`). The
@@ -27,10 +41,20 @@
27
41
  */
28
42
  /**
29
43
  * Register the observer invoked when a read detects an EXTERNAL rotation (a
30
- * sibling tab, a server `Set-Cookie`, or a cookie clear). Self-rotations
44
+ * sibling tab, a server `Set-Cookie`, or a cookie clear) of the
45
+ * `{version}:{timestamp}` part. A change to the write part alone (a server
46
+ * action's write) goes to the write observer instead. Self-rotations
31
47
  * (invalidateRangoState) update the mirror synchronously and never fire it.
32
48
  */
33
49
  export declare function setRangoStateObserver(observer: ((value: string) => void) | null): void;
50
+ /**
51
+ * Register the observer invoked when a read sees the write part change while
52
+ * the `{version}:{timestamp}` part stays: a server action's response, in this
53
+ * tab or another, bumped it in the shared jar since the last read. Every read
54
+ * updates the mirror, and a prefetch reads the state before it starts, so a
55
+ * prefetch sent with the new value is never one this observer sees as old.
56
+ */
57
+ export declare function setRangoStateWriteObserver(observer: (() => void) | null): void;
34
58
  /** Return the resolved cookie name used to namespace cross-tab messages. */
35
59
  export declare function getRangoStateCookieName(): string;
36
60
  /**
@@ -49,10 +73,15 @@ export declare function initRangoState(version: string, stateCookieName?: string
49
73
  export declare function getRangoState(): string;
50
74
  /**
51
75
  * Invalidate the Rango state (self-rotation). Called when the client clears its
52
- * prefetch caches (e.g. via the server-action bridge). Rotates the timestamp,
76
+ * prefetch caches (e.g. invalidateClientCache()). Rotates the timestamp,
53
77
  * keeps the version, writes the cookie, and updates the mirror synchronously so
54
78
  * the external-rotation observer is NOT triggered by our own write.
55
79
  */
56
80
  export declare function invalidateRangoState(): void;
57
- /** Adopt the state carried by a same-origin cache-invalidation broadcast. */
81
+ /**
82
+ * Adopt the state carried by a same-origin cache-invalidation broadcast when
83
+ * its timestamp is newer (a rotation). The write part is not compared: a soft
84
+ * broadcast's newer write is already in the shared jar, and a value that
85
+ * differs only there is no rotation to adopt.
86
+ */
58
87
  export declare function adoptRangoState(value: string): boolean;