@rangojs/router 0.31.0 → 0.33.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.
- package/README.md +3 -2
- package/dist/bin/browser-7KIJ5HSV.js +15 -0
- package/dist/bin/chunk-3B7CKBRO.js +463 -0
- package/dist/bin/chunk-524VACCG.js +71 -0
- package/dist/bin/chunk-LD7UBHT4.js +196 -0
- package/dist/bin/chunk-TO6CRWLN.js +1429 -0
- package/dist/bin/chunk-TTXC2U3G.js +2477 -0
- package/dist/bin/client-urls-projection-DQLWPDAT.js +28 -0
- package/dist/bin/forward-user-plugins-URBZKJP4.js +62 -0
- package/dist/bin/rango.js +42 -10143
- package/dist/bin/runtime-EPUDEHO4.js +596 -0
- package/dist/bin/runtime-discovery-CZ2QHT4Q.js +352 -0
- package/dist/bin/state-ACD2MGKJ.js +16 -0
- package/dist/bin/temp-rsc-server-QRR6HD6T.js +3190 -0
- package/dist/types/bin/rango.d.ts +1 -0
- package/dist/types/browser/action-fence.d.ts +20 -0
- package/dist/types/browser/cookie-name.d.ts +51 -10
- package/dist/types/browser/history-state.d.ts +13 -2
- package/dist/types/browser/invalidate-client-cache.d.ts +8 -6
- package/dist/types/browser/navigation-bridge.d.ts +36 -1
- package/dist/types/browser/navigation-client.d.ts +19 -0
- package/dist/types/browser/navigation-store-handle.d.ts +37 -4
- package/dist/types/browser/navigation-store.d.ts +2 -2
- package/dist/types/browser/navigation-transaction.d.ts +0 -1
- package/dist/types/browser/partial-update.d.ts +13 -0
- package/dist/types/browser/prefetch/cache.d.ts +96 -8
- package/dist/types/browser/prefetch/fetch.d.ts +26 -2
- package/dist/types/browser/prefetch/queue.d.ts +3 -2
- package/dist/types/browser/rango-state.d.ts +34 -5
- package/dist/types/browser/react/Link.d.ts +19 -13
- package/dist/types/browser/react/location-state-shared.d.ts +2 -1
- package/dist/types/browser/types.d.ts +25 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/include-resolution.d.ts +9 -1
- package/dist/types/build/route-types/router-processing.d.ts +3 -1
- package/dist/types/cache/cache-key-utils.d.ts +12 -8
- package/dist/types/cache/cache-scope.d.ts +4 -2
- package/dist/types/cache/cf/cf-cache-types.d.ts +9 -5
- package/dist/types/cache/document-cache-policy.d.ts +9 -1
- package/dist/types/cache/memory-segment-store.d.ts +3 -2
- package/dist/types/cache/segment-codec.d.ts +2 -2
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/types.d.ts +12 -7
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -1
- package/dist/types/client-urls/types.d.ts +7 -5
- package/dist/types/prefetch-markers.d.ts +9 -0
- package/dist/types/prefetch-source-scope.d.ts +66 -0
- package/dist/types/route-definition/helpers-types.d.ts +8 -5
- package/dist/types/route-definition/redirect.d.ts +5 -0
- package/dist/types/router/intercept-resolution.d.ts +25 -11
- package/dist/types/router/match-context.d.ts +1 -0
- package/dist/types/router/match-handlers.d.ts +1 -1
- package/dist/types/router/middleware-types.d.ts +6 -0
- package/dist/types/router/prefetch-cache-ttl.d.ts +11 -3
- package/dist/types/router/router-context.d.ts +1 -1
- package/dist/types/router/router-options.d.ts +6 -3
- package/dist/types/router/segment-resolution/loader-cache.d.ts +8 -0
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +23 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +19 -0
- package/dist/types/router/source-reads.d.ts +53 -0
- package/dist/types/router/types.d.ts +1 -1
- package/dist/types/rsc/helpers.d.ts +8 -3
- package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
- package/dist/types/rsc/request-stages.d.ts +4 -0
- package/dist/types/rsc/shell-serve.d.ts +6 -1
- package/dist/types/server/context.d.ts +23 -6
- package/dist/types/server/cookie-store.d.ts +15 -1
- package/dist/types/server/document-location-state.d.ts +59 -0
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +65 -12
- package/dist/types/testing/e2e/server.d.ts +16 -1
- package/dist/types/testing/render-route.d.ts +83 -2
- package/dist/types/testing/serve-shell-request.d.ts +15 -3
- package/dist/types/types/handler-context.d.ts +21 -4
- package/dist/types/types/loader-types.d.ts +7 -4
- package/dist/types/types/segments.d.ts +18 -1
- package/dist/types/urls/path-helper-types.d.ts +7 -5
- package/dist/types/vite/temp-rsc-server.d.ts +2 -2
- package/dist/types/vite/utils/forward-user-plugins.d.ts +32 -2
- package/dist/vite/index.js +451 -191
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +74 -0
- package/skills/app-structure/e-commerce.md +10 -9
- package/skills/app-structure/large-app.md +15 -2
- package/skills/caching/SKILL.md +10 -5
- package/skills/client-urls/SKILL.md +18 -11
- package/skills/comparison/references/framework-comparison.md +16 -10
- package/skills/document-cache/SKILL.md +13 -0
- package/skills/hooks/navigation.md +8 -8
- package/skills/hooks/state.md +132 -12
- package/skills/intercept/SKILL.md +33 -11
- package/skills/layout/SKILL.md +11 -0
- package/skills/links/SKILL.md +101 -4
- package/skills/loader/SKILL.md +49 -22
- package/skills/migrate-nextjs/SKILL.md +8 -2
- package/skills/migrate-react-router/data-and-actions.md +5 -0
- package/skills/ppr/SKILL.md +38 -5
- package/skills/prerender/SKILL.md +4 -1
- package/skills/route/SKILL.md +6 -6
- package/skills/router-setup/SKILL.md +2 -1
- package/skills/server-actions/SKILL.md +49 -8
- package/skills/testing/client-components.md +73 -32
- package/skills/testing/serve-shell-request.md +45 -20
- package/skills/testing/server-actions.md +8 -0
- package/skills/ux-architecture/SKILL.md +67 -15
- package/src/bin/rango.ts +39 -13
- package/src/browser/action-fence.ts +72 -0
- package/src/browser/cookie-name.ts +82 -14
- package/src/browser/history-state.ts +24 -2
- package/src/browser/invalidate-client-cache.ts +9 -7
- package/src/browser/navigation-bridge.ts +164 -32
- package/src/browser/navigation-client.ts +95 -20
- package/src/browser/navigation-store-handle.ts +67 -5
- package/src/browser/navigation-store.ts +86 -17
- package/src/browser/navigation-transaction.ts +0 -6
- package/src/browser/partial-update.ts +31 -12
- package/src/browser/prefetch/cache.ts +188 -11
- package/src/browser/prefetch/fetch.ts +211 -51
- package/src/browser/prefetch/queue.ts +3 -2
- package/src/browser/rango-state.ts +50 -8
- package/src/browser/react/Link.tsx +24 -16
- package/src/browser/react/location-state-shared.ts +8 -1
- package/src/browser/react/use-navigation.ts +1 -1
- package/src/browser/rsc-router.tsx +13 -3
- package/src/browser/server-action-bridge.ts +3 -3
- package/src/browser/types.ts +25 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/include-resolution.ts +40 -3
- package/src/build/route-types/router-processing.ts +170 -25
- package/src/build/route-types/scan-filter.ts +1 -0
- package/src/build/runtime-discovery.ts +46 -8
- package/src/cache/cache-key-utils.ts +12 -8
- package/src/cache/cache-runtime.ts +5 -1
- package/src/cache/cache-scope.ts +28 -7
- package/src/cache/cf/cf-cache-types.ts +9 -5
- package/src/cache/document-cache-policy.ts +37 -1
- package/src/cache/document-cache.ts +12 -0
- package/src/cache/memory-segment-store.ts +3 -2
- package/src/cache/segment-codec.ts +2 -2
- package/src/cache/shell-snapshot.ts +1 -1
- package/src/cache/types.ts +12 -7
- package/src/cache/vercel/vercel-cache-store.ts +2 -1
- package/src/client-urls/types.ts +7 -5
- package/src/prefetch-markers.ts +10 -0
- package/src/prefetch-source-scope.ts +137 -0
- package/src/route-definition/helpers-types.ts +8 -5
- package/src/route-definition/redirect.ts +5 -0
- package/src/router/intercept-resolution.ts +94 -50
- package/src/router/loader-resolution.ts +8 -6
- package/src/router/match-api.ts +33 -29
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +2 -1
- package/src/router/match-middleware/cache-lookup.ts +39 -14
- package/src/router/match-result.ts +45 -85
- package/src/router/middleware-types.ts +6 -0
- package/src/router/navigation-snapshot.ts +18 -3
- package/src/router/prefetch-cache-ttl.ts +15 -3
- package/src/router/revalidation.ts +63 -13
- package/src/router/router-context.ts +2 -1
- package/src/router/router-options.ts +6 -3
- package/src/router/segment-resolution/loader-cache.ts +21 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +38 -31
- package/src/router/segment-resolution/revalidation.ts +40 -12
- package/src/router/source-reads.ts +122 -0
- package/src/router/types.ts +2 -1
- package/src/router.ts +11 -12
- package/src/rsc/helpers.ts +20 -6
- package/src/rsc/prefetch-render-error.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +8 -2
- package/src/rsc/request-stages.ts +87 -1
- package/src/rsc/rsc-rendering.ts +78 -32
- package/src/rsc/server-action.ts +2 -1
- package/src/rsc/shell-capture.ts +4 -5
- package/src/rsc/shell-serve.ts +12 -1
- package/src/segment-system.tsx +1 -1
- package/src/server/context.ts +37 -6
- package/src/server/cookie-store.ts +22 -1
- package/src/server/document-location-state.ts +334 -0
- package/src/server/handle-store.ts +2 -0
- package/src/server/request-context.ts +137 -33
- package/src/testing/dispatch.ts +6 -0
- package/src/testing/e2e/fixture.ts +4 -12
- package/src/testing/e2e/server.ts +91 -4
- package/src/testing/render-route.tsx +309 -10
- package/src/testing/run-revalidate.ts +11 -1
- package/src/testing/serve-shell-request.ts +22 -3
- package/src/types/handler-context.ts +21 -4
- package/src/types/loader-types.ts +7 -4
- package/src/types/segments.ts +18 -1
- package/src/urls/path-helper-types.ts +7 -5
- package/src/vite/temp-rsc-server.ts +2 -2
- package/src/vite/utils/forward-user-plugins.ts +13 -5
|
@@ -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
|
|
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
|
|
@@ -44,6 +55,13 @@ import { notifyPrefetchCacheInvalidated } from "./invalidation.js";
|
|
|
44
55
|
import { invalidateRangoState } from "../rango-state.js";
|
|
45
56
|
import type { RscPayload } from "../types.js";
|
|
46
57
|
import { WIRE } from "../../wire-params.js";
|
|
58
|
+
import {
|
|
59
|
+
formatSourceLabel,
|
|
60
|
+
parseSourceShape,
|
|
61
|
+
sourceShapeId,
|
|
62
|
+
type SourcePage,
|
|
63
|
+
type SourceShape,
|
|
64
|
+
} from "../../prefetch-source-scope.js";
|
|
47
65
|
|
|
48
66
|
/**
|
|
49
67
|
* A prefetch that has been fetched AND eagerly decoded. Storing the decoded
|
|
@@ -64,9 +82,23 @@ export interface DecodedPrefetch {
|
|
|
64
82
|
* Prefetch scope as tagged by the server via `X-RSC-Prefetch-Scope`.
|
|
65
83
|
* `"source"` means the response is source-page-sensitive and must not be
|
|
66
84
|
* reused by a navigation from a different page — navigation enforces this
|
|
67
|
-
* when it adopted an inflight entry through the wildcard key.
|
|
85
|
+
* when it adopted an inflight entry through the wildcard key. `"params"`
|
|
86
|
+
* means it serves the pages whose label equals `paramsLabel`.
|
|
68
87
|
*/
|
|
69
|
-
scope:
|
|
88
|
+
scope: PrefetchScope;
|
|
89
|
+
/** The label of a `"params"` entry (prefetch-source-scope.ts). */
|
|
90
|
+
paramsLabel?: string;
|
|
91
|
+
/**
|
|
92
|
+
* The stale epoch (`currentPrefetchEpoch()`) the prefetch's fetch started
|
|
93
|
+
* under. A server action without a directive marks every prefetch stale
|
|
94
|
+
* (`markPrefetchesStale()` bumps the epoch) instead of flushing it: an entry
|
|
95
|
+
* from an older epoch is still served, and the navigation that adopts it
|
|
96
|
+
* revalidates its page once it settled (`isPrefetchStale`). A stale entry
|
|
97
|
+
* is served at most once, and only to a click (`consumePrefetch`'s
|
|
98
|
+
* `acceptStale`). A respawn keeps the original fetch's epoch, as it keeps
|
|
99
|
+
* its timestamp.
|
|
100
|
+
*/
|
|
101
|
+
epoch: number;
|
|
70
102
|
/**
|
|
71
103
|
* Synchronously-readable flag, flipped to true when `streamComplete` resolves
|
|
72
104
|
* (the entire RSC stream has drained). Navigation reads this at click time to
|
|
@@ -108,6 +140,9 @@ export interface DecodedPrefetch {
|
|
|
108
140
|
dispose?: () => void;
|
|
109
141
|
}
|
|
110
142
|
|
|
143
|
+
/** Where a prefetch is stored: see the module comment. */
|
|
144
|
+
export type PrefetchScope = "source" | "params" | "wildcard";
|
|
145
|
+
|
|
111
146
|
let cacheTTL = 300_000;
|
|
112
147
|
let fragmentPassthroughEnabled = true;
|
|
113
148
|
|
|
@@ -163,8 +198,25 @@ const inflightAliases = new Map<string, string[]>();
|
|
|
163
198
|
|
|
164
199
|
const adoptedKeys = new Set<string>();
|
|
165
200
|
|
|
201
|
+
/**
|
|
202
|
+
* What the stored params-scoped labels of a target read, by
|
|
203
|
+
* `buildPrefetchKey("", target)`: the shapes paramsCandidateKeys rebuilds
|
|
204
|
+
* labels from. Bounded like the cache; cleared with it.
|
|
205
|
+
*/
|
|
206
|
+
const paramsShapes = new Map<string, Map<string, SourceShape>>();
|
|
207
|
+
|
|
166
208
|
let generation = 0;
|
|
167
209
|
|
|
210
|
+
/**
|
|
211
|
+
* The stale epoch. `markPrefetchesStale()` bumps it; an entry whose fetch
|
|
212
|
+
* started under an older one is stale (`isPrefetchStale`). Separate from
|
|
213
|
+
* `generation`: a stale mark keeps every entry and every in-flight fetch,
|
|
214
|
+
* where a generation bump drops them. So a Flight response that has already
|
|
215
|
+
* arrived is never aborted by an action (aborting it mid-stream errors
|
|
216
|
+
* the stream): an in-flight prefetch finishes and lands stale.
|
|
217
|
+
*/
|
|
218
|
+
let staleEpoch = 0;
|
|
219
|
+
|
|
168
220
|
/**
|
|
169
221
|
* Build a cache key by combining a scope prefix with the target URL.
|
|
170
222
|
*
|
|
@@ -172,7 +224,7 @@ let generation = 0;
|
|
|
172
224
|
* one of:
|
|
173
225
|
* - Wildcard (source-agnostic): prefix is the Rango state value from
|
|
174
226
|
* `getRangoState()`. Shared across all source pages. Invalidated
|
|
175
|
-
* automatically when Rango state bumps (deploy or
|
|
227
|
+
* automatically when Rango state bumps (deploy or invalidateClientCache()).
|
|
176
228
|
* `_rsc_segments` is omitted so sibling payloads prefetched from one page
|
|
177
229
|
* remain reusable after another prefetched navigation commits. Key shape:
|
|
178
230
|
* `rangoState\0/target?...`.
|
|
@@ -218,6 +270,76 @@ export function buildSourceKey(
|
|
|
218
270
|
);
|
|
219
271
|
}
|
|
220
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Build a params-scoped cache key. Key shape:
|
|
275
|
+
* `rangoState\0p:<label>\0/target?...`, the target normalized like the
|
|
276
|
+
* wildcard key's: the answer serves every page with this label, whatever
|
|
277
|
+
* segments it holds, as a wildcard answer serves every page.
|
|
278
|
+
*/
|
|
279
|
+
export function buildParamsKey(
|
|
280
|
+
rangoState: string,
|
|
281
|
+
label: string,
|
|
282
|
+
targetUrl: URL,
|
|
283
|
+
): string {
|
|
284
|
+
return buildPrefetchKey(rangoState + "\0p:" + label, targetUrl);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Remember that a params-scoped answer for `targetUrl` is stored under a
|
|
289
|
+
* label of this shape. Called where the response header is read, with the
|
|
290
|
+
* store (fetch.ts executePrefetchFetch).
|
|
291
|
+
*/
|
|
292
|
+
export function rememberParamsShape(targetUrl: URL, label: string): void {
|
|
293
|
+
const target = buildPrefetchKey("", targetUrl);
|
|
294
|
+
const shape = parseSourceShape(label);
|
|
295
|
+
let shapes = paramsShapes.get(target);
|
|
296
|
+
if (!shapes) {
|
|
297
|
+
if (paramsShapes.size >= maxPrefetchCacheSize) {
|
|
298
|
+
const oldest = paramsShapes.keys().next().value;
|
|
299
|
+
if (oldest !== undefined) paramsShapes.delete(oldest);
|
|
300
|
+
}
|
|
301
|
+
shapes = new Map();
|
|
302
|
+
paramsShapes.set(target, shapes);
|
|
303
|
+
}
|
|
304
|
+
shapes.set(sourceShapeId(shape), shape);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The params-scoped keys a request for `targetUrl` from `page` may adopt:
|
|
309
|
+
* the page's label under each shape stored for the target, the shapes that
|
|
310
|
+
* read the most first. Empty without a page (before boot).
|
|
311
|
+
*/
|
|
312
|
+
export function paramsCandidateKeys(
|
|
313
|
+
rangoState: string,
|
|
314
|
+
targetUrl: URL,
|
|
315
|
+
page: SourcePage | null,
|
|
316
|
+
): string[] {
|
|
317
|
+
if (!page) return [];
|
|
318
|
+
const shapes = paramsShapes.get(buildPrefetchKey("", targetUrl));
|
|
319
|
+
if (!shapes) return [];
|
|
320
|
+
return [...shapes.values()]
|
|
321
|
+
.sort((a, b) => weightOf(b) - weightOf(a))
|
|
322
|
+
.map((shape) =>
|
|
323
|
+
buildParamsKey(rangoState, formatSourceLabel(shape, page), targetUrl),
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const weightOf = (shape: SourceShape): number =>
|
|
328
|
+
shape.names.length + (shape.route ? 1 : 0) + (shape.keys ? 1 : 0);
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Whether `page` is in the group a params-scoped entry serves: its label
|
|
332
|
+
* under the entry's shape equals the entry's.
|
|
333
|
+
*/
|
|
334
|
+
export function pageMatchesParamsLabel(
|
|
335
|
+
label: string,
|
|
336
|
+
page: SourcePage | null,
|
|
337
|
+
): boolean {
|
|
338
|
+
return (
|
|
339
|
+
page !== null && formatSourceLabel(parseSourceShape(label), page) === label
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
221
343
|
/**
|
|
222
344
|
* Walk an inflight key plus any sibling aliases registered via
|
|
223
345
|
* `setInflightPromiseWithAliases`, invoking `fn` for each.
|
|
@@ -233,13 +355,19 @@ function forEachAlias(key: string, fn: (k: string) => void): void {
|
|
|
233
355
|
|
|
234
356
|
/**
|
|
235
357
|
* Check if a prefetch is already cached, in-flight, or queued for the given key.
|
|
358
|
+
*
|
|
359
|
+
* A stale stored entry (`isPrefetchStale`) counts as absent and is dropped, so
|
|
360
|
+
* a new hover or viewport trigger replaces it with a fresh prefetch, and a
|
|
361
|
+
* click made while that one is in flight adopts it rather than the stale one.
|
|
362
|
+
* An in-flight fetch counts whatever its epoch: it is never aborted, and a
|
|
363
|
+
* duplicate request for its key would race it for the slot.
|
|
236
364
|
*/
|
|
237
365
|
export function hasPrefetch(key: string): boolean {
|
|
238
366
|
if (inflight.has(key)) return true;
|
|
239
367
|
if (cacheTTL <= 0) return false;
|
|
240
368
|
const entry = cache.get(key);
|
|
241
369
|
if (!entry) return false;
|
|
242
|
-
if (Date.now() - entry.timestamp > cacheTTL) {
|
|
370
|
+
if (Date.now() - entry.timestamp > cacheTTL || isPrefetchStale(entry.entry)) {
|
|
243
371
|
entry.entry.dispose?.();
|
|
244
372
|
cache.delete(key);
|
|
245
373
|
return false;
|
|
@@ -258,10 +386,20 @@ export function hasPrefetch(key: string): boolean {
|
|
|
258
386
|
* TTL bounds the age of the data, not of the latest adoption. Without
|
|
259
387
|
* `respawn` the entry is deleted (legacy one-shot behavior).
|
|
260
388
|
*
|
|
389
|
+
* A stale entry (`isPrefetchStale`) is served only to a caller that passes
|
|
390
|
+
* `acceptStale` (a click, which revalidates its page once it settled,
|
|
391
|
+
* navigation-bridge.ts): for any other caller it is a miss and stays in its
|
|
392
|
+
* slot. It is served at most once: its slot is not re-armed, so a second
|
|
393
|
+
* click on the same link fetches fresh rather than showing pre-write data
|
|
394
|
+
* older than what the first click's revalidation showed.
|
|
395
|
+
*
|
|
261
396
|
* Does NOT check in-flight prefetches — use consumeInflightPrefetch()
|
|
262
397
|
* for that (returns a Promise instead of a resolved entry).
|
|
263
398
|
*/
|
|
264
|
-
export function consumePrefetch(
|
|
399
|
+
export function consumePrefetch(
|
|
400
|
+
key: string,
|
|
401
|
+
options?: { acceptStale?: boolean },
|
|
402
|
+
): DecodedPrefetch | null {
|
|
265
403
|
if (cacheTTL <= 0) return null;
|
|
266
404
|
const cached = cache.get(key);
|
|
267
405
|
if (!cached) return null;
|
|
@@ -271,10 +409,15 @@ export function consumePrefetch(key: string): DecodedPrefetch | null {
|
|
|
271
409
|
return null;
|
|
272
410
|
}
|
|
273
411
|
const entry = cached.entry;
|
|
274
|
-
|
|
412
|
+
const stale = isPrefetchStale(entry);
|
|
413
|
+
if (stale && !options?.acceptStale) return null;
|
|
414
|
+
if (entry.respawn && !stale) {
|
|
275
415
|
cached.entry = entry.respawn();
|
|
276
416
|
} else {
|
|
277
417
|
cache.delete(key);
|
|
418
|
+
// The buffered reserve (fetch.ts) is never replayed: release it. It is a
|
|
419
|
+
// separate tee branch, so the payload the caller adopts keeps streaming.
|
|
420
|
+
if (stale) entry.dispose?.();
|
|
278
421
|
}
|
|
279
422
|
return entry;
|
|
280
423
|
}
|
|
@@ -318,12 +461,18 @@ export function consumeInflightPrefetch(
|
|
|
318
461
|
* Store an eagerly-decoded prefetch in the in-memory cache.
|
|
319
462
|
*
|
|
320
463
|
* Skips storage if the generation has changed since the fetch started
|
|
321
|
-
* (
|
|
464
|
+
* (the cache was flushed mid-flight). An entry from an older stale epoch (an
|
|
465
|
+
* action marked prefetches stale mid-flight) is stored, and stays stale.
|
|
466
|
+
*
|
|
467
|
+
* `inflightKey` is the key the prefetch was in flight under, when the
|
|
468
|
+
* storage key is not one of its aliases (a params-scoped answer): a
|
|
469
|
+
* navigation that adopted it marked the aliases.
|
|
322
470
|
*/
|
|
323
471
|
export function storePrefetch(
|
|
324
472
|
key: string,
|
|
325
473
|
entry: DecodedPrefetch,
|
|
326
474
|
fetchGeneration: number,
|
|
475
|
+
inflightKey: string = key,
|
|
327
476
|
): void {
|
|
328
477
|
if (cacheTTL <= 0) return;
|
|
329
478
|
if (fetchGeneration !== generation) return;
|
|
@@ -333,8 +482,8 @@ export function storePrefetch(
|
|
|
333
482
|
// publish it to the cache map, or a later navigation would be served the
|
|
334
483
|
// exhausted entry and lose that route's handles. Clear the marker (under all
|
|
335
484
|
// aliases) now that the decision is made.
|
|
336
|
-
if (adoptedKeys.has(
|
|
337
|
-
forEachAlias(
|
|
485
|
+
if (adoptedKeys.has(inflightKey)) {
|
|
486
|
+
forEachAlias(inflightKey, (k) => adoptedKeys.delete(k));
|
|
338
487
|
return;
|
|
339
488
|
}
|
|
340
489
|
|
|
@@ -398,6 +547,33 @@ export function currentGeneration(): number {
|
|
|
398
547
|
return generation;
|
|
399
548
|
}
|
|
400
549
|
|
|
550
|
+
/**
|
|
551
|
+
* Capture the current stale epoch. A prefetch fetch stamps its entry with the
|
|
552
|
+
* value read when it started (`DecodedPrefetch.epoch`).
|
|
553
|
+
*/
|
|
554
|
+
export function currentPrefetchEpoch(): number {
|
|
555
|
+
return staleEpoch;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Whether an action marked prefetches stale after this entry's fetch began. */
|
|
559
|
+
export function isPrefetchStale(
|
|
560
|
+
entry: Pick<DecodedPrefetch, "epoch">,
|
|
561
|
+
): boolean {
|
|
562
|
+
return entry.epoch < staleEpoch;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Mark every stored and in-flight prefetch stale: what a server action
|
|
567
|
+
* without a directive does (navigation-store.ts markCacheAsStaleAndBroadcast).
|
|
568
|
+
* Only bumps the epoch. Nothing is dropped, aborted or rotated, and visible
|
|
569
|
+
* links are not told (no `notifyPrefetchCacheInvalidated()`), so they do not
|
|
570
|
+
* prefetch again. A click on a stale entry commits it at once; the navigation
|
|
571
|
+
* then revalidates its page (navigation-bridge.ts navigate).
|
|
572
|
+
*/
|
|
573
|
+
export function markPrefetchesStale(): void {
|
|
574
|
+
staleEpoch++;
|
|
575
|
+
}
|
|
576
|
+
|
|
401
577
|
export function markPrefetchInflight(key: string): void {
|
|
402
578
|
inflight.add(key);
|
|
403
579
|
}
|
|
@@ -457,6 +633,7 @@ export function clearPrefetchCache(rotateRangoState = true): void {
|
|
|
457
633
|
inflightPromises.clear();
|
|
458
634
|
inflightAliases.clear();
|
|
459
635
|
adoptedKeys.clear();
|
|
636
|
+
paramsShapes.clear();
|
|
460
637
|
for (const cached of cache.values()) cached.entry.dispose?.();
|
|
461
638
|
cache.clear();
|
|
462
639
|
earliestTimestamp = Infinity;
|