@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.d98a8e9d
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 +120 -25
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +2154 -861
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +57 -11
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +247 -17
- package/skills/loader/SKILL.md +219 -9
- package/skills/middleware/SKILL.md +47 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +71 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +243 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +57 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +128 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +98 -0
- package/skills/testing/client-components.md +121 -0
- package/skills/testing/e2e-parity.md +124 -0
- package/skills/testing/flight.md +89 -0
- package/skills/testing/handles.md +127 -0
- package/skills/testing/loader.md +108 -0
- package/skills/testing/middleware.md +97 -0
- package/skills/testing/render-handler.md +102 -0
- package/skills/testing/response-routes.md +94 -0
- package/skills/testing/reverse-and-types.md +83 -0
- package/skills/testing/server-actions.md +89 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +319 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +86 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +84 -11
- package/src/browser/navigation-client.ts +104 -68
- package/src/browser/navigation-store.ts +32 -9
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +64 -26
- package/src/browser/prefetch/cache.ts +183 -44
- package/src/browser/prefetch/fetch.ts +228 -37
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +30 -2
- package/src/browser/react/NavigationProvider.tsx +72 -31
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +22 -2
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +32 -1
- package/src/browser/rsc-router.tsx +69 -22
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +21 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +8 -1
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +95 -25
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +54 -13
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +96 -205
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -4
- package/src/handle.ts +32 -14
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -21
- package/src/index.rsc.ts +10 -6
- package/src/index.ts +54 -17
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +25 -7
- package/src/loader.ts +16 -9
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender.ts +27 -6
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -36
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +384 -257
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +100 -28
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +26 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/find-match.ts +54 -6
- package/src/router/handler-context.ts +21 -38
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +41 -22
- package/src/router/loader-resolution.ts +82 -36
- package/src/router/manifest.ts +41 -19
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +53 -32
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +15 -26
- package/src/router/middleware.ts +99 -84
- package/src/router/pattern-matching.ts +116 -19
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +58 -2
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +27 -6
- package/src/router/segment-resolution/revalidation.ts +147 -106
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +40 -16
- package/src/router/types.ts +8 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +52 -30
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +28 -69
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +57 -61
- package/src/rsc/rsc-rendering.ts +35 -51
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +17 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +8 -2
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +132 -116
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +175 -53
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +67 -51
- package/src/ssr/index.tsx +5 -1
- package/src/static-handler.ts +25 -3
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +581 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +326 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +110 -0
- package/src/testing/flight-normalize.ts +38 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +51 -0
- package/src/testing/flight.ts +234 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +106 -0
- package/src/testing/internal/context.ts +304 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +42 -0
- package/src/testing/render-handler.ts +323 -0
- package/src/testing/render-route.tsx +590 -0
- package/src/testing/run-loader.ts +363 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +285 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +68 -50
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +11 -0
- package/src/types/segments.ts +35 -2
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +1 -5
- package/src/urls/path-helper-types.ts +41 -7
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +106 -75
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +67 -26
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +54 -30
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +496 -486
- package/src/vite/plugins/performance-tracks.ts +29 -25
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +116 -29
- package/src/vite/router-discovery.ts +750 -100
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +8 -59
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +21 -6
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Prefetch Cache
|
|
3
3
|
*
|
|
4
|
-
* In-memory cache storing
|
|
5
|
-
* on subsequent navigation.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* In-memory cache storing eagerly-decoded prefetch payloads for instant,
|
|
5
|
+
* already-warm cache hits on subsequent navigation. A prefetch fetches the
|
|
6
|
+
* RSC partial AND decodes it (createFromFetch) up front — decoding the Flight
|
|
7
|
+
* stream resolves the route's client references, so the route's JS chunks are
|
|
8
|
+
* imported during prefetch rather than on click. The decoded payload is reused
|
|
9
|
+
* verbatim by navigation, so a prefetched click loads no new code. Two key
|
|
10
|
+
* scopes are in play:
|
|
11
|
+
* - Wildcard (default): built by `buildPrefetchKey(rangoState, target)` —
|
|
12
|
+
* shape `rangoState\0/target?...`. Shared across all source pages and
|
|
13
|
+
* invalidated automatically when Rango state bumps (deploy or
|
|
14
|
+
* server-action invalidation).
|
|
15
|
+
* - Source-scoped: built by `buildSourceKey(rangoState, sourceHref, target)`
|
|
16
|
+
* — shape `rangoState\0sourceHref\0/target?...`. Embeds the Rango state
|
|
17
|
+
* (so rotation invalidates source-scoped entries too) plus the source
|
|
18
|
+
* href (so each originating page gets its own slot). Populated when the
|
|
19
|
+
* server tags a response with `X-RSC-Prefetch-Scope: source` (intercept
|
|
20
|
+
* modals etc.), OR when a Link opts in with `prefetchKey=":source"` — in
|
|
21
|
+
* both cases so source-sensitive responses cannot bleed into navigations
|
|
22
|
+
* from other pages.
|
|
8
23
|
*
|
|
9
24
|
* Also tracks in-flight prefetch promises. Each promise resolves to the
|
|
10
|
-
*
|
|
11
|
-
* still-downloading prefetch without
|
|
25
|
+
* decoded prefetch entry (or null), letting navigation adopt a
|
|
26
|
+
* still-downloading prefetch without issuing a duplicate request. A
|
|
27
|
+
* single promise can be registered under multiple alias keys (see
|
|
28
|
+
* `setInflightPromiseWithAliases`) so same-source navigations adopt via
|
|
29
|
+
* their source key while cross-source ones fall through to the wildcard
|
|
30
|
+
* alias — with consume/clear atomically removing every alias.
|
|
12
31
|
*
|
|
13
32
|
* Replaces the previous browser HTTP cache approach which was unreliable
|
|
14
33
|
* due to response draining race conditions and browser inconsistencies.
|
|
@@ -16,6 +35,31 @@
|
|
|
16
35
|
|
|
17
36
|
import { abortAllPrefetches } from "./queue.js";
|
|
18
37
|
import { invalidateRangoState } from "../rango-state.js";
|
|
38
|
+
import type { RscPayload } from "../types.js";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A prefetch that has been fetched AND eagerly decoded. Storing the decoded
|
|
42
|
+
* payload (not the raw Response) is what makes a prefetched navigation "warm":
|
|
43
|
+
* decoding the Flight stream during prefetch pulls the route's client chunks,
|
|
44
|
+
* so the click reuses ready elements and loads no new JS.
|
|
45
|
+
*/
|
|
46
|
+
export interface DecodedPrefetch {
|
|
47
|
+
/** The eagerly-decoded RSC payload. Reused verbatim by navigation. */
|
|
48
|
+
payload: Promise<RscPayload>;
|
|
49
|
+
/**
|
|
50
|
+
* Resolves when the underlying RSC stream finishes draining. Navigation
|
|
51
|
+
* forwards this as its streamComplete so scroll/revalidation gating is
|
|
52
|
+
* unchanged from the fresh-fetch path.
|
|
53
|
+
*/
|
|
54
|
+
streamComplete: Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Prefetch scope as tagged by the server via `X-RSC-Prefetch-Scope`.
|
|
57
|
+
* `"source"` means the response is source-page-sensitive and must not be
|
|
58
|
+
* reused by a navigation from a different page — navigation enforces this
|
|
59
|
+
* when it adopted an inflight entry through the wildcard key.
|
|
60
|
+
*/
|
|
61
|
+
scope: "source" | "wildcard";
|
|
62
|
+
}
|
|
19
63
|
|
|
20
64
|
// Default TTL: 5 minutes. Overridden by initPrefetchCache() with
|
|
21
65
|
// the server-configured prefetchCacheTTL from router options.
|
|
@@ -41,7 +85,7 @@ export function isPrefetchCacheDisabled(): boolean {
|
|
|
41
85
|
const MAX_PREFETCH_CACHE_SIZE = 50;
|
|
42
86
|
|
|
43
87
|
interface PrefetchCacheEntry {
|
|
44
|
-
|
|
88
|
+
entry: DecodedPrefetch;
|
|
45
89
|
timestamp: number;
|
|
46
90
|
}
|
|
47
91
|
|
|
@@ -49,11 +93,21 @@ const cache = new Map<string, PrefetchCacheEntry>();
|
|
|
49
93
|
const inflight = new Set<string>();
|
|
50
94
|
|
|
51
95
|
/**
|
|
52
|
-
* In-flight promise map. When a prefetch fetch is in progress, its
|
|
53
|
-
* Promise<
|
|
54
|
-
*
|
|
96
|
+
* In-flight promise map. When a prefetch fetch+decode is in progress, its
|
|
97
|
+
* Promise<DecodedPrefetch | null> is stored here so navigation can await it
|
|
98
|
+
* instead of starting a duplicate request. Resolves to null when the prefetch
|
|
99
|
+
* failed, was aborted, or carried a control header (reload/redirect) that the
|
|
100
|
+
* navigation must re-fetch to act on.
|
|
55
101
|
*/
|
|
56
|
-
const inflightPromises = new Map<string, Promise<
|
|
102
|
+
const inflightPromises = new Map<string, Promise<DecodedPrefetch | null>>();
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Alias map for in-flight promises registered under multiple keys (see
|
|
106
|
+
* dual inflight in prefetch/fetch.ts). Records each key's sibling set so
|
|
107
|
+
* that consuming or clearing any one key atomically removes every alias —
|
|
108
|
+
* guaranteeing a single consumer for the shared decode.
|
|
109
|
+
*/
|
|
110
|
+
const inflightAliases = new Map<string, string[]>();
|
|
57
111
|
|
|
58
112
|
// Generation counter incremented on each clearPrefetchCache(). Fetches that
|
|
59
113
|
// started before a clear carry a stale generation and must not store their
|
|
@@ -61,13 +115,57 @@ const inflightPromises = new Map<string, Promise<Response | null>>();
|
|
|
61
115
|
let generation = 0;
|
|
62
116
|
|
|
63
117
|
/**
|
|
64
|
-
* Build a
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
118
|
+
* Build a cache key by combining a scope prefix with the target URL.
|
|
119
|
+
*
|
|
120
|
+
* Low-level primitive — callers that want a specific scope should use
|
|
121
|
+
* one of:
|
|
122
|
+
* - Wildcard (source-agnostic): prefix is the Rango state value from
|
|
123
|
+
* `getRangoState()`. Shared across all source pages. Invalidated
|
|
124
|
+
* automatically when Rango state bumps (deploy or server-action).
|
|
125
|
+
* Key shape: `rangoState\0/target?...`.
|
|
126
|
+
* - Source-scoped: use `buildSourceKey()`. Key shape:
|
|
127
|
+
* `rangoState\0sourceHref\0/target?...` — embeds the Rango state so
|
|
128
|
+
* rotation invalidates source-scoped entries alongside wildcard ones,
|
|
129
|
+
* plus the source page href so the key is unique per originating page.
|
|
130
|
+
* Populated either when the server tags a response with
|
|
131
|
+
* `X-RSC-Prefetch-Scope: source` (intercept modals, etc.) or when a
|
|
132
|
+
* Link opts in via `prefetchKey=":source"`.
|
|
133
|
+
*
|
|
134
|
+
* The `_rsc_segments` query param that travels in the target URL means
|
|
135
|
+
* clients with different mounted segment trees naturally get different
|
|
136
|
+
* keys — so segment-level diffs remain consistent across both scopes.
|
|
137
|
+
*/
|
|
138
|
+
export function buildPrefetchKey(prefix: string, targetUrl: URL): string {
|
|
139
|
+
return prefix + "\0" + targetUrl.pathname + targetUrl.search;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Build a source-scoped cache key. Key shape:
|
|
144
|
+
* `rangoState\0sourceHref\0/target?...`.
|
|
145
|
+
*
|
|
146
|
+
* - `rangoState` is included so state rotation invalidates source-scoped
|
|
147
|
+
* entries alongside wildcard ones.
|
|
148
|
+
* - `sourceHref` makes the key unique per originating page.
|
|
68
149
|
*/
|
|
69
|
-
export function
|
|
70
|
-
|
|
150
|
+
export function buildSourceKey(
|
|
151
|
+
rangoState: string,
|
|
152
|
+
sourceHref: string,
|
|
153
|
+
targetUrl: URL,
|
|
154
|
+
): string {
|
|
155
|
+
return buildPrefetchKey(rangoState + "\0" + sourceHref, targetUrl);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Walk an inflight key plus any sibling aliases registered via
|
|
160
|
+
* `setInflightPromiseWithAliases`, invoking `fn` for each.
|
|
161
|
+
*/
|
|
162
|
+
function forEachAlias(key: string, fn: (k: string) => void): void {
|
|
163
|
+
const aliases = inflightAliases.get(key);
|
|
164
|
+
if (aliases) {
|
|
165
|
+
for (const k of aliases) fn(k);
|
|
166
|
+
} else {
|
|
167
|
+
fn(key);
|
|
168
|
+
}
|
|
71
169
|
}
|
|
72
170
|
|
|
73
171
|
/**
|
|
@@ -86,14 +184,14 @@ export function hasPrefetch(key: string): boolean {
|
|
|
86
184
|
}
|
|
87
185
|
|
|
88
186
|
/**
|
|
89
|
-
* Consume a cached prefetch
|
|
90
|
-
* One-time consumption: the entry is deleted after retrieval.
|
|
187
|
+
* Consume a cached, eagerly-decoded prefetch. Returns null if not found or
|
|
188
|
+
* expired. One-time consumption: the entry is deleted after retrieval.
|
|
91
189
|
* Returns null when caching is disabled (TTL <= 0).
|
|
92
190
|
*
|
|
93
191
|
* Does NOT check in-flight prefetches — use consumeInflightPrefetch()
|
|
94
|
-
* for that (returns a Promise instead of a
|
|
192
|
+
* for that (returns a Promise instead of a resolved entry).
|
|
95
193
|
*/
|
|
96
|
-
export function consumePrefetch(key: string):
|
|
194
|
+
export function consumePrefetch(key: string): DecodedPrefetch | null {
|
|
97
195
|
if (cacheTTL <= 0) return null;
|
|
98
196
|
const entry = cache.get(key);
|
|
99
197
|
if (!entry) return null;
|
|
@@ -102,43 +200,48 @@ export function consumePrefetch(key: string): Response | null {
|
|
|
102
200
|
return null;
|
|
103
201
|
}
|
|
104
202
|
cache.delete(key);
|
|
105
|
-
return entry.
|
|
203
|
+
return entry.entry;
|
|
106
204
|
}
|
|
107
205
|
|
|
108
206
|
/**
|
|
109
207
|
* Consume an in-flight prefetch promise. Returns null if no prefetch is
|
|
110
|
-
* in-flight for this key. The returned Promise resolves to the
|
|
111
|
-
*
|
|
208
|
+
* in-flight for this key. The returned Promise resolves to the decoded
|
|
209
|
+
* prefetch entry (or null if the fetch failed/was aborted, or carried a
|
|
210
|
+
* control header the navigation must re-fetch to honor).
|
|
112
211
|
*
|
|
113
|
-
* One-time consumption: the promise entry is removed
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
212
|
+
* One-time consumption: the promise entry is removed (along with any
|
|
213
|
+
* sibling aliases registered via `setInflightPromiseWithAliases`) so a
|
|
214
|
+
* second call on any alias returns null — only one caller can adopt the
|
|
215
|
+
* shared Response stream. The `inflight` set entry is intentionally
|
|
216
|
+
* kept so that `hasPrefetch()` continues to return true while the
|
|
217
|
+
* underlying fetch is still downloading — this prevents
|
|
218
|
+
* `prefetchDirect()` or other callers from starting a duplicate request
|
|
219
|
+
* during the handoff window. The inflight flag is cleaned up naturally
|
|
220
|
+
* by `clearPrefetchInflight()` in the fetch's `.finally()`.
|
|
120
221
|
*/
|
|
121
222
|
export function consumeInflightPrefetch(
|
|
122
223
|
key: string,
|
|
123
|
-
): Promise<
|
|
224
|
+
): Promise<DecodedPrefetch | null> | null {
|
|
124
225
|
const promise = inflightPromises.get(key);
|
|
125
226
|
if (!promise) return null;
|
|
126
|
-
// Remove the promise
|
|
127
|
-
|
|
227
|
+
// Remove the promise under every alias so a second consumer cannot
|
|
228
|
+
// adopt the same stream and race on the body. `inflightAliases` is
|
|
229
|
+
// intentionally preserved — `clearPrefetchInflight()` in the fetch's
|
|
230
|
+
// `.finally()` still needs it to clear every inflight flag; deleting
|
|
231
|
+
// here would strand the sibling's flag forever.
|
|
232
|
+
forEachAlias(key, (k) => inflightPromises.delete(k));
|
|
128
233
|
return promise;
|
|
129
234
|
}
|
|
130
235
|
|
|
131
236
|
/**
|
|
132
|
-
* Store
|
|
133
|
-
* The response should be a clone() of the original so the caller can
|
|
134
|
-
* still consume the body. The clone's body streams independently.
|
|
237
|
+
* Store an eagerly-decoded prefetch in the in-memory cache.
|
|
135
238
|
*
|
|
136
239
|
* Skips storage if the generation has changed since the fetch started
|
|
137
240
|
* (a server action invalidated the cache mid-flight).
|
|
138
241
|
*/
|
|
139
242
|
export function storePrefetch(
|
|
140
243
|
key: string,
|
|
141
|
-
|
|
244
|
+
entry: DecodedPrefetch,
|
|
142
245
|
fetchGeneration: number,
|
|
143
246
|
): void {
|
|
144
247
|
if (cacheTTL <= 0) return;
|
|
@@ -146,8 +249,8 @@ export function storePrefetch(
|
|
|
146
249
|
|
|
147
250
|
// Evict expired entries
|
|
148
251
|
const now = Date.now();
|
|
149
|
-
for (const [k,
|
|
150
|
-
if (now -
|
|
252
|
+
for (const [k, cached] of cache) {
|
|
253
|
+
if (now - cached.timestamp > cacheTTL) {
|
|
151
254
|
cache.delete(k);
|
|
152
255
|
}
|
|
153
256
|
}
|
|
@@ -158,7 +261,7 @@ export function storePrefetch(
|
|
|
158
261
|
if (oldest) cache.delete(oldest);
|
|
159
262
|
}
|
|
160
263
|
|
|
161
|
-
cache.set(key, {
|
|
264
|
+
cache.set(key, { entry, timestamp: now });
|
|
162
265
|
}
|
|
163
266
|
|
|
164
267
|
/**
|
|
@@ -178,14 +281,33 @@ export function markPrefetchInflight(key: string): void {
|
|
|
178
281
|
*/
|
|
179
282
|
export function setInflightPromise(
|
|
180
283
|
key: string,
|
|
181
|
-
promise: Promise<
|
|
284
|
+
promise: Promise<DecodedPrefetch | null>,
|
|
182
285
|
): void {
|
|
183
286
|
inflightPromises.set(key, promise);
|
|
184
287
|
}
|
|
185
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Store the same in-flight Promise under multiple keys, recording them
|
|
291
|
+
* as sibling aliases. Consuming or clearing any one alias atomically
|
|
292
|
+
* removes every entry, guaranteeing the shared Response stream has a
|
|
293
|
+
* single consumer even when navigation looks up either key.
|
|
294
|
+
*/
|
|
295
|
+
export function setInflightPromiseWithAliases(
|
|
296
|
+
keys: string[],
|
|
297
|
+
promise: Promise<DecodedPrefetch | null>,
|
|
298
|
+
): void {
|
|
299
|
+
for (const k of keys) {
|
|
300
|
+
inflightPromises.set(k, promise);
|
|
301
|
+
inflightAliases.set(k, keys);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
186
305
|
export function clearPrefetchInflight(key: string): void {
|
|
187
|
-
|
|
188
|
-
|
|
306
|
+
forEachAlias(key, (k) => {
|
|
307
|
+
inflight.delete(k);
|
|
308
|
+
inflightPromises.delete(k);
|
|
309
|
+
inflightAliases.delete(k);
|
|
310
|
+
});
|
|
189
311
|
}
|
|
190
312
|
|
|
191
313
|
/**
|
|
@@ -200,7 +322,24 @@ export function clearPrefetchCache(): void {
|
|
|
200
322
|
generation++;
|
|
201
323
|
inflight.clear();
|
|
202
324
|
inflightPromises.clear();
|
|
325
|
+
inflightAliases.clear();
|
|
203
326
|
cache.clear();
|
|
204
327
|
abortAllPrefetches();
|
|
205
328
|
invalidateRangoState();
|
|
206
329
|
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Drop all in-memory prefetch state for this tab without rotating rango-state.
|
|
333
|
+
*
|
|
334
|
+
* Use for local-only invalidations (e.g. app switch in this tab) where
|
|
335
|
+
* other tabs should NOT observe a state rotation. Unlike clearPrefetchCache,
|
|
336
|
+
* does not call invalidateRangoState, so the shared X-Rango-State token
|
|
337
|
+
* stays intact and siblings in the old app keep their prefetches.
|
|
338
|
+
*/
|
|
339
|
+
export function clearPrefetchCacheLocal(): void {
|
|
340
|
+
generation++;
|
|
341
|
+
inflight.clear();
|
|
342
|
+
inflightPromises.clear();
|
|
343
|
+
cache.clear();
|
|
344
|
+
abortAllPrefetches();
|
|
345
|
+
}
|
|
@@ -3,26 +3,72 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Fetch-based prefetch logic used by Link (hover/viewport/render strategies)
|
|
5
5
|
* and useRouter().prefetch(). Sends the same headers and segment IDs as a
|
|
6
|
-
* real navigation so the server returns a proper diff. The
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* real navigation so the server returns a proper diff. The response is fetched
|
|
7
|
+
* AND eagerly decoded (createFromFetch) up front: decoding the Flight stream
|
|
8
|
+
* resolves the route's client references, so the route's JS chunks are imported
|
|
9
|
+
* during prefetch rather than on click. The decoded payload is stored in an
|
|
10
|
+
* in-memory cache and reused verbatim by navigation, so a prefetched click
|
|
11
|
+
* loads no new code.
|
|
9
12
|
*
|
|
10
13
|
* In-flight promises are tracked in the cache so that navigation can reuse
|
|
11
|
-
* a prefetch that is still downloading instead of starting a
|
|
14
|
+
* a prefetch that is still downloading/decoding instead of starting a
|
|
15
|
+
* duplicate request.
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
18
|
import {
|
|
15
19
|
buildPrefetchKey,
|
|
20
|
+
buildSourceKey,
|
|
16
21
|
hasPrefetch,
|
|
17
22
|
markPrefetchInflight,
|
|
18
|
-
|
|
23
|
+
setInflightPromiseWithAliases,
|
|
19
24
|
storePrefetch,
|
|
20
25
|
clearPrefetchInflight,
|
|
21
26
|
currentGeneration,
|
|
27
|
+
type DecodedPrefetch,
|
|
22
28
|
} from "./cache.js";
|
|
23
29
|
import { getRangoState } from "../rango-state.js";
|
|
24
30
|
import { enqueuePrefetch } from "./queue.js";
|
|
25
31
|
import { shouldPrefetch } from "./policy.js";
|
|
32
|
+
import { debugLog } from "../logging.js";
|
|
33
|
+
import { teeWithCompletion } from "../response-adapter.js";
|
|
34
|
+
import type { RscPayload } from "../types.js";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Decoder injected at app startup (see setPrefetchDecoder). This is
|
|
38
|
+
* `deps.createFromFetch` — decoupled from the RSC runtime exactly like the
|
|
39
|
+
* navigation client. Prefetch decodes through it so the route's client chunks
|
|
40
|
+
* are pulled during the prefetch, not on click.
|
|
41
|
+
*/
|
|
42
|
+
type PrefetchDecoder = (response: Promise<Response>) => Promise<RscPayload>;
|
|
43
|
+
|
|
44
|
+
let decoder: PrefetchDecoder | null = null;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Wire the RSC decoder used to eagerly decode prefetched responses. Called
|
|
48
|
+
* once from initBrowserApp with the same createFromFetch the navigation client
|
|
49
|
+
* uses. Until set, prefetch warming is inert (prefetches are skipped) — the
|
|
50
|
+
* browser app always sets it before any Link can fire.
|
|
51
|
+
*/
|
|
52
|
+
export function setPrefetchDecoder(fn: PrefetchDecoder): void {
|
|
53
|
+
decoder = fn;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if a URL resolves to the current page (same pathname + search).
|
|
58
|
+
* Used to prevent same-page prefetching, which produces a trivial diff
|
|
59
|
+
* that would corrupt the (default wildcard) prefetch cache entry.
|
|
60
|
+
*/
|
|
61
|
+
function isSamePage(url: string): boolean {
|
|
62
|
+
try {
|
|
63
|
+
const target = new URL(url, window.location.origin);
|
|
64
|
+
return (
|
|
65
|
+
target.pathname + target.search ===
|
|
66
|
+
window.location.pathname + window.location.search
|
|
67
|
+
);
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
26
72
|
|
|
27
73
|
/**
|
|
28
74
|
* Build an RSC partial URL for prefetching.
|
|
@@ -59,20 +105,49 @@ function buildPrefetchUrl(
|
|
|
59
105
|
}
|
|
60
106
|
|
|
61
107
|
/**
|
|
62
|
-
* Core prefetch fetch logic. Fetches the response,
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* reuse an in-flight prefetch via
|
|
108
|
+
* Core prefetch fetch logic. Fetches the response, eagerly decodes it, and
|
|
109
|
+
* stores the decoded payload in the in-memory cache. The returned Promise
|
|
110
|
+
* resolves to the decoded entry (or null on failure / control header) so
|
|
111
|
+
* navigation can safely reuse an in-flight prefetch via
|
|
112
|
+
* consumeInflightPrefetch().
|
|
113
|
+
*
|
|
114
|
+
* Eager decode is the warming step: createFromFetch parses the Flight stream,
|
|
115
|
+
* which resolves the route's client references and imports its JS chunks. The
|
|
116
|
+
* stored payload is reused as-is by navigation, so the click loads no new code.
|
|
117
|
+
*
|
|
118
|
+
* Control headers are NOT acted on here. A speculative prefetch must never
|
|
119
|
+
* reload the page or throw a redirect — if the response carries X-RSC-Reload
|
|
120
|
+
* or X-RSC-Redirect, we drop it (resolve null) and let the real navigation
|
|
121
|
+
* re-fetch and honor it.
|
|
122
|
+
*
|
|
123
|
+
* Inflight + storage key selection:
|
|
124
|
+
*
|
|
125
|
+
* - `forceSourceScope` (Link opted in with `prefetchKey=":source"`): single
|
|
126
|
+
* inflight registration under `sourceKey`; entry stored under `sourceKey`.
|
|
127
|
+
* No wildcard leak is possible.
|
|
128
|
+
*
|
|
129
|
+
* - Otherwise: dual inflight registration under both `wildcardKey` and
|
|
130
|
+
* `sourceKey` so same-source navigations adopt directly via their own
|
|
131
|
+
* source key. Storage key is chosen at response time from the
|
|
132
|
+
* `X-RSC-Prefetch-Scope` header — `"source"` → `sourceKey` (intercept
|
|
133
|
+
* modals etc.), anything else → `wildcardKey`. The entry records its scope
|
|
134
|
+
* so cross-source navigations that adopted via `wildcardKey` can bail out
|
|
135
|
+
* in `navigation-client.ts` when the adopted entry turns out source-scoped.
|
|
66
136
|
*/
|
|
67
137
|
function executePrefetchFetch(
|
|
68
|
-
|
|
138
|
+
wildcardKey: string,
|
|
139
|
+
sourceKey: string,
|
|
69
140
|
fetchUrl: string,
|
|
141
|
+
forceSourceScope: boolean,
|
|
70
142
|
signal?: AbortSignal,
|
|
71
|
-
): Promise<
|
|
143
|
+
): Promise<DecodedPrefetch | null> {
|
|
72
144
|
const gen = currentGeneration();
|
|
73
|
-
|
|
145
|
+
const inflightKeys = forceSourceScope
|
|
146
|
+
? [sourceKey]
|
|
147
|
+
: [wildcardKey, sourceKey];
|
|
148
|
+
for (const k of inflightKeys) markPrefetchInflight(k);
|
|
74
149
|
|
|
75
|
-
const promise: Promise<
|
|
150
|
+
const promise: Promise<DecodedPrefetch | null> = fetch(fetchUrl, {
|
|
76
151
|
priority: "low" as RequestPriority,
|
|
77
152
|
signal,
|
|
78
153
|
headers: {
|
|
@@ -82,69 +157,185 @@ function executePrefetchFetch(
|
|
|
82
157
|
},
|
|
83
158
|
})
|
|
84
159
|
.then((response) => {
|
|
85
|
-
if (!response.ok) return null;
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
160
|
+
if (!response.ok || !decoder) return null;
|
|
161
|
+
// Control headers mean this response is stale (reload) or redirecting.
|
|
162
|
+
// Don't warm it — drop so navigation re-fetches and acts on the header.
|
|
163
|
+
if (
|
|
164
|
+
response.headers.has("X-RSC-Reload") ||
|
|
165
|
+
response.headers.has("X-RSC-Redirect")
|
|
166
|
+
) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const scope: "source" | "wildcard" =
|
|
171
|
+
forceSourceScope ||
|
|
172
|
+
response.headers.get("x-rsc-prefetch-scope") === "source"
|
|
173
|
+
? "source"
|
|
174
|
+
: "wildcard";
|
|
175
|
+
const storageKey = scope === "source" ? sourceKey : wildcardKey;
|
|
176
|
+
|
|
177
|
+
// Track stream completion off a tee so navigation's scroll/revalidation
|
|
178
|
+
// gating matches the fresh-fetch path; decode the other branch.
|
|
179
|
+
let resolveStreamComplete!: () => void;
|
|
180
|
+
const streamComplete = new Promise<void>((resolve) => {
|
|
181
|
+
resolveStreamComplete = resolve;
|
|
182
|
+
});
|
|
183
|
+
const tracked = teeWithCompletion(
|
|
184
|
+
response,
|
|
185
|
+
() => resolveStreamComplete(),
|
|
186
|
+
signal,
|
|
187
|
+
// Speculative prefetch: a never-consumed/aborted stream error is benign.
|
|
188
|
+
true,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Eager decode: parsing the Flight stream imports the route's client
|
|
192
|
+
// chunks now, not on click.
|
|
193
|
+
const payload = decoder(Promise.resolve(tracked));
|
|
194
|
+
// Mark handled so an unconsumed prefetch decode error stays quiet; the
|
|
195
|
+
// error is still surfaced to navigation if it consumes the entry.
|
|
196
|
+
payload.catch(() => {});
|
|
197
|
+
|
|
198
|
+
const entry: DecodedPrefetch = { payload, streamComplete, scope };
|
|
199
|
+
storePrefetch(storageKey, entry, gen);
|
|
200
|
+
return entry;
|
|
97
201
|
})
|
|
98
202
|
.catch(() => null)
|
|
99
203
|
.finally(() => {
|
|
100
|
-
clearPrefetchInflight(
|
|
204
|
+
clearPrefetchInflight(inflightKeys[0]!);
|
|
101
205
|
});
|
|
102
206
|
|
|
103
|
-
|
|
207
|
+
setInflightPromiseWithAliases(inflightKeys, promise);
|
|
104
208
|
return promise;
|
|
105
209
|
}
|
|
106
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Dedup check for prefetch entry presence.
|
|
213
|
+
*
|
|
214
|
+
* Forced `:source` must NOT dedupe against a pre-existing wildcard entry —
|
|
215
|
+
* otherwise the source slot would stay unpopulated and navigation from
|
|
216
|
+
* this source would fall through to the (potentially wrong) wildcard
|
|
217
|
+
* response, defeating the opt-out.
|
|
218
|
+
*/
|
|
219
|
+
function hasPrefetchHit(
|
|
220
|
+
forceSourceScope: boolean,
|
|
221
|
+
wildcardKey: string,
|
|
222
|
+
sourceKey: string,
|
|
223
|
+
): boolean {
|
|
224
|
+
return forceSourceScope
|
|
225
|
+
? hasPrefetch(sourceKey)
|
|
226
|
+
: hasPrefetch(wildcardKey) || hasPrefetch(sourceKey);
|
|
227
|
+
}
|
|
228
|
+
|
|
107
229
|
/**
|
|
108
230
|
* Prefetch (direct): fetch with low priority and store in in-memory cache.
|
|
109
231
|
* Used by hover strategy -- fires immediately without queueing.
|
|
232
|
+
*
|
|
233
|
+
* By default the wildcard key (Rango-state-keyed) is used for inflight
|
|
234
|
+
* dedup and for responses that are not source-sensitive; source-scoped
|
|
235
|
+
* storage is automatic when the server emits `X-RSC-Prefetch-Scope: source`.
|
|
236
|
+
*
|
|
237
|
+
* Pass `prefetchKey=":source"` to force source-scoped inflight + storage
|
|
238
|
+
* (e.g. when the target uses a custom `revalidate()` that reads
|
|
239
|
+
* `currentUrl` and the wildcard slot would serve the wrong diff).
|
|
110
240
|
*/
|
|
111
241
|
export function prefetchDirect(
|
|
112
242
|
url: string,
|
|
113
243
|
segmentIds: string[],
|
|
114
244
|
version?: string,
|
|
115
245
|
routerId?: string,
|
|
246
|
+
prefetchKey?: ":source",
|
|
116
247
|
): void {
|
|
117
248
|
if (!shouldPrefetch()) return;
|
|
118
249
|
|
|
119
250
|
const targetUrl = buildPrefetchUrl(url, segmentIds, version, routerId);
|
|
120
251
|
if (!targetUrl) return;
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
252
|
+
const forceSourceScope = prefetchKey === ":source";
|
|
253
|
+
// Skip same-page prefetch — a same-page diff is trivial and would corrupt
|
|
254
|
+
// the wildcard cache entry used for cross-page navigation.
|
|
255
|
+
// When `:source` is forced the entry is source-scoped (single-aliased to
|
|
256
|
+
// itself), so it cannot poison any shared slot — allow it.
|
|
257
|
+
if (!forceSourceScope && isSamePage(url)) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const sourceHref = window.location.href;
|
|
261
|
+
const rangoState = getRangoState();
|
|
262
|
+
const wildcardKey = buildPrefetchKey(rangoState, targetUrl);
|
|
263
|
+
const sourceKey = buildSourceKey(rangoState, sourceHref, targetUrl);
|
|
264
|
+
if (hasPrefetchHit(forceSourceScope, wildcardKey, sourceKey)) {
|
|
265
|
+
debugLog("[prefetch] direct dedup (key already exists)", {
|
|
266
|
+
url,
|
|
267
|
+
wildcardKey,
|
|
268
|
+
sourceKey,
|
|
269
|
+
forceSourceScope,
|
|
270
|
+
});
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
debugLog("[prefetch] direct fetch", {
|
|
274
|
+
url,
|
|
275
|
+
wildcardKey,
|
|
276
|
+
sourceKey,
|
|
277
|
+
source: sourceHref,
|
|
278
|
+
forceSourceScope,
|
|
279
|
+
});
|
|
280
|
+
executePrefetchFetch(
|
|
281
|
+
wildcardKey,
|
|
282
|
+
sourceKey,
|
|
283
|
+
targetUrl.toString(),
|
|
284
|
+
forceSourceScope,
|
|
285
|
+
);
|
|
124
286
|
}
|
|
125
287
|
|
|
126
288
|
/**
|
|
127
289
|
* Prefetch (queued): goes through the concurrency-limited queue.
|
|
128
290
|
* Used by viewport/render strategies to avoid flooding the server.
|
|
129
|
-
* Returns the
|
|
291
|
+
* Returns the inflight key (wildcard by default, source-scoped when
|
|
292
|
+
* `prefetchKey=":source"` is passed).
|
|
130
293
|
*/
|
|
131
294
|
export function prefetchQueued(
|
|
132
295
|
url: string,
|
|
133
296
|
segmentIds: string[],
|
|
134
297
|
version?: string,
|
|
135
298
|
routerId?: string,
|
|
299
|
+
prefetchKey?: ":source",
|
|
136
300
|
): string {
|
|
137
301
|
if (!shouldPrefetch()) return "";
|
|
138
302
|
const targetUrl = buildPrefetchUrl(url, segmentIds, version, routerId);
|
|
139
303
|
if (!targetUrl) return "";
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
304
|
+
const forceSourceScope = prefetchKey === ":source";
|
|
305
|
+
if (!forceSourceScope && isSamePage(url)) {
|
|
306
|
+
return "";
|
|
307
|
+
}
|
|
308
|
+
const sourceHref = window.location.href;
|
|
309
|
+
const rangoState = getRangoState();
|
|
310
|
+
const wildcardKey = buildPrefetchKey(rangoState, targetUrl);
|
|
311
|
+
const sourceKey = buildSourceKey(rangoState, sourceHref, targetUrl);
|
|
312
|
+
const queueKey = forceSourceScope ? sourceKey : wildcardKey;
|
|
313
|
+
if (hasPrefetchHit(forceSourceScope, wildcardKey, sourceKey)) {
|
|
314
|
+
debugLog("[prefetch] queued dedup (key already exists)", {
|
|
315
|
+
url,
|
|
316
|
+
wildcardKey,
|
|
317
|
+
sourceKey,
|
|
318
|
+
forceSourceScope,
|
|
319
|
+
});
|
|
320
|
+
return queueKey;
|
|
321
|
+
}
|
|
142
322
|
const fetchUrlStr = targetUrl.toString();
|
|
143
|
-
enqueuePrefetch(
|
|
323
|
+
enqueuePrefetch(queueKey, (signal) => {
|
|
144
324
|
// Re-check at execution time: a hover-triggered prefetchDirect may
|
|
145
325
|
// have started or completed this key while the item sat in the queue.
|
|
146
|
-
if (
|
|
147
|
-
|
|
326
|
+
if (hasPrefetchHit(forceSourceScope, wildcardKey, sourceKey)) {
|
|
327
|
+
return Promise.resolve();
|
|
328
|
+
}
|
|
329
|
+
if (!forceSourceScope && isSamePage(url)) {
|
|
330
|
+
return Promise.resolve();
|
|
331
|
+
}
|
|
332
|
+
return executePrefetchFetch(
|
|
333
|
+
wildcardKey,
|
|
334
|
+
sourceKey,
|
|
335
|
+
fetchUrlStr,
|
|
336
|
+
forceSourceScope,
|
|
337
|
+
signal,
|
|
338
|
+
).then(() => {});
|
|
148
339
|
});
|
|
149
|
-
return
|
|
340
|
+
return queueKey;
|
|
150
341
|
}
|