@rangojs/router 0.0.0-experimental.28 → 0.0.0-experimental.29-prefetch-cache.29972e92
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/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/src/browser/navigation-client.ts +97 -72
- package/src/browser/prefetch/cache.ts +88 -25
- package/src/browser/prefetch/fetch.ts +28 -30
package/dist/vite/index.js
CHANGED
|
@@ -1745,7 +1745,7 @@ import { resolve } from "node:path";
|
|
|
1745
1745
|
// package.json
|
|
1746
1746
|
var package_default = {
|
|
1747
1747
|
name: "@rangojs/router",
|
|
1748
|
-
version: "0.0.0-experimental.
|
|
1748
|
+
version: "0.0.0-experimental.29-prefetch-cache.29972e92",
|
|
1749
1749
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
1750
1750
|
keywords: [
|
|
1751
1751
|
"react",
|
package/package.json
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
emptyResponse,
|
|
18
18
|
teeWithCompletion,
|
|
19
19
|
} from "./response-adapter.js";
|
|
20
|
+
import { buildPrefetchKey, consumePrefetch } from "./prefetch/cache.js";
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Create a navigation client for fetching RSC payloads
|
|
@@ -24,21 +25,12 @@ import {
|
|
|
24
25
|
* The client handles building URLs with RSC parameters and
|
|
25
26
|
* deserializing the response using the RSC runtime.
|
|
26
27
|
*
|
|
28
|
+
* Checks the in-memory prefetch cache before making a network request.
|
|
29
|
+
* The cache key is source-dependent (includes the previous URL) so
|
|
30
|
+
* prefetch responses match the exact diff the server would produce.
|
|
31
|
+
*
|
|
27
32
|
* @param deps - RSC browser dependencies (createFromFetch)
|
|
28
33
|
* @returns NavigationClient instance
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* ```typescript
|
|
32
|
-
* import { createFromFetch } from "@vitejs/plugin-rsc/browser";
|
|
33
|
-
*
|
|
34
|
-
* const client = createNavigationClient({ createFromFetch });
|
|
35
|
-
*
|
|
36
|
-
* const payload = await client.fetchPartial({
|
|
37
|
-
* targetUrl: "/shop/products",
|
|
38
|
-
* segmentIds: ["root", "shop"],
|
|
39
|
-
* previousUrl: "/",
|
|
40
|
-
* });
|
|
41
|
-
* ```
|
|
42
34
|
*/
|
|
43
35
|
export function createNavigationClient(
|
|
44
36
|
deps: Pick<RscBrowserDependencies, "createFromFetch">,
|
|
@@ -47,8 +39,9 @@ export function createNavigationClient(
|
|
|
47
39
|
/**
|
|
48
40
|
* Fetch a partial RSC payload for navigation
|
|
49
41
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
42
|
+
* First checks the in-memory prefetch cache for a matching entry.
|
|
43
|
+
* If found, uses the cached response instantly. Otherwise sends
|
|
44
|
+
* current segment IDs to the server for diff-based rendering.
|
|
52
45
|
*
|
|
53
46
|
* @param options - Fetch options
|
|
54
47
|
* @returns RSC payload with segments and metadata, plus stream completion promise
|
|
@@ -80,7 +73,8 @@ export function createNavigationClient(
|
|
|
80
73
|
});
|
|
81
74
|
}
|
|
82
75
|
|
|
83
|
-
// Build fetch URL with partial rendering params
|
|
76
|
+
// Build fetch URL with partial rendering params (used for both
|
|
77
|
+
// cache key lookup and actual fetch if cache misses)
|
|
84
78
|
const fetchUrl = new URL(targetUrl, window.location.origin);
|
|
85
79
|
fetchUrl.searchParams.set("_rsc_partial", "true");
|
|
86
80
|
fetchUrl.searchParams.set("_rsc_segments", segmentIds.join(","));
|
|
@@ -90,11 +84,17 @@ export function createNavigationClient(
|
|
|
90
84
|
if (version) {
|
|
91
85
|
fetchUrl.searchParams.set("_rsc_v", version);
|
|
92
86
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
87
|
+
|
|
88
|
+
// Check in-memory prefetch cache before making a network request.
|
|
89
|
+
// The cache key includes the source URL (previousUrl) because the
|
|
90
|
+
// server's diff response depends on the source page context.
|
|
91
|
+
// Skip cache for stale revalidation (needs fresh data), HMR (needs
|
|
92
|
+
// fresh modules), and intercept contexts (source-dependent responses).
|
|
93
|
+
const cacheKey = buildPrefetchKey(previousUrl, fetchUrl);
|
|
94
|
+
const cachedResponse =
|
|
95
|
+
!staleRevalidation && !hmr && !interceptSourceUrl
|
|
96
|
+
? consumePrefetch(cacheKey)
|
|
97
|
+
: null;
|
|
98
98
|
|
|
99
99
|
// Track when the stream completes
|
|
100
100
|
let resolveStreamComplete: () => void;
|
|
@@ -102,63 +102,88 @@ export function createNavigationClient(
|
|
|
102
102
|
resolveStreamComplete = resolve;
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
const responsePromise = fetch(fetchUrl, {
|
|
107
|
-
headers: {
|
|
108
|
-
"X-RSC-Router-Client-Path": previousUrl,
|
|
109
|
-
"X-Rango-State": getRangoState(),
|
|
110
|
-
...(tx && { "X-RSC-Router-Request-Id": tx.requestId }),
|
|
111
|
-
...(interceptSourceUrl && {
|
|
112
|
-
"X-RSC-Router-Intercept-Source": interceptSourceUrl,
|
|
113
|
-
}),
|
|
114
|
-
...(hmr && { "X-RSC-HMR": "1" }),
|
|
115
|
-
},
|
|
116
|
-
signal,
|
|
117
|
-
}).then((response) => {
|
|
118
|
-
// Check for version mismatch - server wants us to reload
|
|
119
|
-
const reload = extractRscHeaderUrl(response, "X-RSC-Reload");
|
|
120
|
-
if (reload === "blocked") {
|
|
121
|
-
resolveStreamComplete();
|
|
122
|
-
return emptyResponse();
|
|
123
|
-
}
|
|
124
|
-
if (reload) {
|
|
125
|
-
if (tx) {
|
|
126
|
-
browserDebugLog(tx, "version mismatch, reloading", {
|
|
127
|
-
reloadUrl: reload.url,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
window.location.href = reload.url;
|
|
131
|
-
return new Promise<Response>(() => {});
|
|
132
|
-
}
|
|
105
|
+
let responsePromise: Promise<Response>;
|
|
133
106
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
// navigation bridge catches it and re-navigates with _skipCache.
|
|
138
|
-
const redirect = extractRscHeaderUrl(response, "X-RSC-Redirect");
|
|
139
|
-
if (redirect === "blocked") {
|
|
140
|
-
resolveStreamComplete();
|
|
141
|
-
return emptyResponse();
|
|
107
|
+
if (cachedResponse) {
|
|
108
|
+
if (tx) {
|
|
109
|
+
browserDebugLog(tx, "prefetch cache hit", { key: cacheKey });
|
|
142
110
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
111
|
+
// Cached response body is already fully buffered (arrayBuffer),
|
|
112
|
+
// so stream completion is immediate.
|
|
113
|
+
responsePromise = Promise.resolve(cachedResponse).then((response) => {
|
|
114
|
+
return teeWithCompletion(
|
|
115
|
+
response,
|
|
116
|
+
() => {
|
|
117
|
+
if (tx) browserDebugLog(tx, "stream complete (from cache)");
|
|
118
|
+
resolveStreamComplete();
|
|
119
|
+
},
|
|
120
|
+
signal,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
if (tx) {
|
|
125
|
+
browserDebugLog(tx, "fetching", {
|
|
126
|
+
path: `${fetchUrl.pathname}${fetchUrl.search}`,
|
|
127
|
+
});
|
|
151
128
|
}
|
|
152
129
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
130
|
+
responsePromise = fetch(fetchUrl, {
|
|
131
|
+
headers: {
|
|
132
|
+
"X-RSC-Router-Client-Path": previousUrl,
|
|
133
|
+
"X-Rango-State": getRangoState(),
|
|
134
|
+
...(tx && { "X-RSC-Router-Request-Id": tx.requestId }),
|
|
135
|
+
...(interceptSourceUrl && {
|
|
136
|
+
"X-RSC-Router-Intercept-Source": interceptSourceUrl,
|
|
137
|
+
}),
|
|
138
|
+
...(hmr && { "X-RSC-HMR": "1" }),
|
|
158
139
|
},
|
|
159
140
|
signal,
|
|
160
|
-
)
|
|
161
|
-
|
|
141
|
+
}).then((response) => {
|
|
142
|
+
// Check for version mismatch - server wants us to reload
|
|
143
|
+
const reload = extractRscHeaderUrl(response, "X-RSC-Reload");
|
|
144
|
+
if (reload === "blocked") {
|
|
145
|
+
resolveStreamComplete();
|
|
146
|
+
return emptyResponse();
|
|
147
|
+
}
|
|
148
|
+
if (reload) {
|
|
149
|
+
if (tx) {
|
|
150
|
+
browserDebugLog(tx, "version mismatch, reloading", {
|
|
151
|
+
reloadUrl: reload.url,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
window.location.href = reload.url;
|
|
155
|
+
return new Promise<Response>(() => {});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Server-side redirect without state: the server returned 204 with
|
|
159
|
+
// X-RSC-Redirect instead of a 3xx (which fetch would auto-follow
|
|
160
|
+
// to a URL rendering full HTML). Throw ServerRedirect so the
|
|
161
|
+
// navigation bridge catches it and re-navigates with _skipCache.
|
|
162
|
+
const redirect = extractRscHeaderUrl(response, "X-RSC-Redirect");
|
|
163
|
+
if (redirect === "blocked") {
|
|
164
|
+
resolveStreamComplete();
|
|
165
|
+
return emptyResponse();
|
|
166
|
+
}
|
|
167
|
+
if (redirect) {
|
|
168
|
+
if (tx) {
|
|
169
|
+
browserDebugLog(tx, "server redirect", {
|
|
170
|
+
redirectUrl: redirect.url,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
resolveStreamComplete();
|
|
174
|
+
throw new ServerRedirect(redirect.url, undefined);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return teeWithCompletion(
|
|
178
|
+
response,
|
|
179
|
+
() => {
|
|
180
|
+
if (tx) browserDebugLog(tx, "stream complete");
|
|
181
|
+
resolveStreamComplete();
|
|
182
|
+
},
|
|
183
|
+
signal,
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
162
187
|
|
|
163
188
|
try {
|
|
164
189
|
// Deserialize RSC payload
|
|
@@ -1,47 +1,111 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Prefetch
|
|
2
|
+
* Prefetch Cache
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* In-memory cache storing prefetch Response objects for instant cache hits
|
|
5
|
+
* on subsequent navigation. Cache key is source-dependent (includes the
|
|
6
|
+
* current page URL) because the server's diff-based response depends on
|
|
7
|
+
* where the user navigates from.
|
|
8
|
+
*
|
|
9
|
+
* Replaces the previous browser HTTP cache approach which was unreliable
|
|
10
|
+
* due to response draining race conditions and browser inconsistencies.
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
13
|
import { cancelAllPrefetches } from "./queue.js";
|
|
10
14
|
import { invalidateRangoState } from "../rango-state.js";
|
|
11
15
|
|
|
16
|
+
const PREFETCH_CACHE_TTL = 30_000; // 30 seconds
|
|
17
|
+
const MAX_PREFETCH_CACHE_SIZE = 50;
|
|
18
|
+
|
|
19
|
+
interface PrefetchCacheEntry {
|
|
20
|
+
response: Response;
|
|
21
|
+
timestamp: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const cache = new Map<string, PrefetchCacheEntry>();
|
|
12
25
|
const inflight = new Set<string>();
|
|
13
|
-
const prefetched = new Set<string>();
|
|
14
26
|
|
|
15
27
|
// Generation counter incremented on each clearPrefetchCache(). Fetches that
|
|
16
|
-
// started before a clear carry a stale generation and must not
|
|
17
|
-
//
|
|
18
|
-
// due to Rango-State rotation).
|
|
28
|
+
// started before a clear carry a stale generation and must not store their
|
|
29
|
+
// response (the data may be stale due to a server action invalidation).
|
|
19
30
|
let generation = 0;
|
|
20
31
|
|
|
21
32
|
/**
|
|
22
|
-
*
|
|
33
|
+
* Build a source-dependent cache key.
|
|
34
|
+
* Includes the source page href so the same target prefetched from
|
|
35
|
+
* different pages gets separate entries — the server response varies
|
|
36
|
+
* based on the source page context (diff-based rendering).
|
|
37
|
+
*/
|
|
38
|
+
export function buildPrefetchKey(sourceHref: string, targetUrl: URL): string {
|
|
39
|
+
return sourceHref + "\0" + targetUrl.pathname + targetUrl.search;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if a prefetch is already cached, in-flight, or queued for the given key.
|
|
23
44
|
*/
|
|
24
45
|
export function hasPrefetch(key: string): boolean {
|
|
25
|
-
|
|
46
|
+
if (inflight.has(key)) return true;
|
|
47
|
+
const entry = cache.get(key);
|
|
48
|
+
if (!entry) return false;
|
|
49
|
+
if (Date.now() - entry.timestamp > PREFETCH_CACHE_TTL) {
|
|
50
|
+
cache.delete(key);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
26
54
|
}
|
|
27
55
|
|
|
28
56
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
57
|
+
* Consume a cached prefetch response. Returns null if not found or expired.
|
|
58
|
+
* One-time consumption: the entry is deleted after retrieval.
|
|
31
59
|
*/
|
|
32
|
-
export function
|
|
33
|
-
|
|
60
|
+
export function consumePrefetch(key: string): Response | null {
|
|
61
|
+
const entry = cache.get(key);
|
|
62
|
+
if (!entry) return null;
|
|
63
|
+
if (Date.now() - entry.timestamp > PREFETCH_CACHE_TTL) {
|
|
64
|
+
cache.delete(key);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
cache.delete(key);
|
|
68
|
+
return entry.response;
|
|
34
69
|
}
|
|
35
70
|
|
|
36
71
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
72
|
+
* Store a prefetch response in the in-memory cache.
|
|
73
|
+
* The response body must be fully buffered (e.g. via arrayBuffer()) before
|
|
74
|
+
* storing, so the cached Response is self-contained and network-independent.
|
|
75
|
+
*
|
|
76
|
+
* Skips storage if the generation has changed since the fetch started
|
|
77
|
+
* (a server action invalidated the cache mid-flight).
|
|
40
78
|
*/
|
|
41
|
-
export function
|
|
42
|
-
|
|
43
|
-
|
|
79
|
+
export function storePrefetch(
|
|
80
|
+
key: string,
|
|
81
|
+
response: Response,
|
|
82
|
+
fetchGeneration: number,
|
|
83
|
+
): void {
|
|
84
|
+
if (fetchGeneration !== generation) return;
|
|
85
|
+
|
|
86
|
+
// Evict expired entries
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
for (const [k, entry] of cache) {
|
|
89
|
+
if (now - entry.timestamp > PREFETCH_CACHE_TTL) {
|
|
90
|
+
cache.delete(k);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// FIFO eviction if at capacity
|
|
95
|
+
if (cache.size >= MAX_PREFETCH_CACHE_SIZE) {
|
|
96
|
+
const oldest = cache.keys().next().value;
|
|
97
|
+
if (oldest) cache.delete(oldest);
|
|
44
98
|
}
|
|
99
|
+
|
|
100
|
+
cache.set(key, { response, timestamp: now });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Capture the current generation. The returned value is passed to
|
|
105
|
+
* storePrefetch so it can detect stale completions.
|
|
106
|
+
*/
|
|
107
|
+
export function currentGeneration(): number {
|
|
108
|
+
return generation;
|
|
45
109
|
}
|
|
46
110
|
|
|
47
111
|
export function markPrefetchInflight(key: string): void {
|
|
@@ -53,15 +117,14 @@ export function clearPrefetchInflight(key: string): void {
|
|
|
53
117
|
}
|
|
54
118
|
|
|
55
119
|
/**
|
|
56
|
-
* Invalidate prefetch state. Called when server actions mutate data.
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* Also cancels any in-flight or queued speculative prefetches.
|
|
120
|
+
* Invalidate all prefetch state. Called when server actions mutate data.
|
|
121
|
+
* Clears the in-memory cache, cancels in-flight prefetches, and rotates
|
|
122
|
+
* the Rango state key so CDN-cached responses are also invalidated.
|
|
60
123
|
*/
|
|
61
124
|
export function clearPrefetchCache(): void {
|
|
62
125
|
generation++;
|
|
63
126
|
inflight.clear();
|
|
64
|
-
|
|
127
|
+
cache.clear();
|
|
65
128
|
cancelAllPrefetches();
|
|
66
129
|
invalidateRangoState();
|
|
67
130
|
}
|
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
* Prefetch Fetch
|
|
3
3
|
*
|
|
4
4
|
* Fetch-based prefetch logic used by Link (hover/viewport/render strategies)
|
|
5
|
-
* and useRouter().prefetch(). Sends
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* and useRouter().prefetch(). Sends the same headers and segment IDs as a
|
|
6
|
+
* real navigation so the server returns a proper diff. The Response is fully
|
|
7
|
+
* buffered and stored in an in-memory cache for instant consumption on
|
|
8
|
+
* subsequent navigation.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import {
|
|
12
|
+
buildPrefetchKey,
|
|
11
13
|
hasPrefetch,
|
|
12
14
|
markPrefetchInflight,
|
|
13
|
-
|
|
15
|
+
storePrefetch,
|
|
14
16
|
clearPrefetchInflight,
|
|
15
17
|
currentGeneration,
|
|
16
18
|
} from "./cache.js";
|
|
@@ -20,9 +22,9 @@ import { shouldPrefetch } from "./policy.js";
|
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* Build an RSC partial URL for prefetching.
|
|
23
|
-
* Includes
|
|
24
|
-
*
|
|
25
|
-
*
|
|
25
|
+
* Includes _rsc_segments so the server can diff against currently mounted
|
|
26
|
+
* segments, and _rsc_v for version mismatch detection.
|
|
27
|
+
* Returns null for malformed or cross-origin URLs.
|
|
26
28
|
*/
|
|
27
29
|
function buildPrefetchUrl(
|
|
28
30
|
url: string,
|
|
@@ -49,18 +51,9 @@ function buildPrefetchUrl(
|
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* X-RSC-Router-Client-Path (source page context).
|
|
56
|
-
*/
|
|
57
|
-
function buildPrefetchKey(targetUrl: URL): string {
|
|
58
|
-
return window.location.href + "\0" + targetUrl.pathname + targetUrl.search;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Core prefetch fetch logic. Returns a Promise and accepts an optional
|
|
63
|
-
* AbortSignal for cancellation by the prefetch queue.
|
|
54
|
+
* Core prefetch fetch logic. Fetches the response, fully buffers the body,
|
|
55
|
+
* and stores it in the in-memory cache. Returns a Promise and accepts an
|
|
56
|
+
* optional AbortSignal for cancellation by the prefetch queue.
|
|
64
57
|
*/
|
|
65
58
|
function executePrefetchFetch(
|
|
66
59
|
key: string,
|
|
@@ -79,14 +72,19 @@ function executePrefetchFetch(
|
|
|
79
72
|
"X-Rango-Prefetch": "1",
|
|
80
73
|
},
|
|
81
74
|
})
|
|
82
|
-
.then((response) => {
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
75
|
+
.then(async (response) => {
|
|
76
|
+
if (!response.ok) return;
|
|
77
|
+
// Fully buffer the response body so the cached Response is
|
|
78
|
+
// self-contained and doesn't depend on the network connection.
|
|
79
|
+
// This eliminates the race condition where the user clicks before
|
|
80
|
+
// the response body has been fully downloaded.
|
|
81
|
+
const buffer = await response.arrayBuffer();
|
|
82
|
+
const cachedResponse = new Response(buffer, {
|
|
83
|
+
headers: response.headers,
|
|
84
|
+
status: response.status,
|
|
85
|
+
statusText: response.statusText,
|
|
86
|
+
});
|
|
87
|
+
storePrefetch(key, cachedResponse, gen);
|
|
90
88
|
})
|
|
91
89
|
.catch(() => {
|
|
92
90
|
// Silently ignore prefetch failures (including abort)
|
|
@@ -97,7 +95,7 @@ function executePrefetchFetch(
|
|
|
97
95
|
}
|
|
98
96
|
|
|
99
97
|
/**
|
|
100
|
-
* Prefetch (direct): fetch with low priority and store in
|
|
98
|
+
* Prefetch (direct): fetch with low priority and store in in-memory cache.
|
|
101
99
|
* Used by hover strategy -- fires immediately without queueing.
|
|
102
100
|
*/
|
|
103
101
|
export function prefetchDirect(
|
|
@@ -109,7 +107,7 @@ export function prefetchDirect(
|
|
|
109
107
|
|
|
110
108
|
const targetUrl = buildPrefetchUrl(url, segmentIds, version);
|
|
111
109
|
if (!targetUrl) return;
|
|
112
|
-
const key = buildPrefetchKey(targetUrl);
|
|
110
|
+
const key = buildPrefetchKey(window.location.href, targetUrl);
|
|
113
111
|
if (hasPrefetch(key)) return;
|
|
114
112
|
executePrefetchFetch(key, targetUrl.toString());
|
|
115
113
|
}
|
|
@@ -127,7 +125,7 @@ export function prefetchQueued(
|
|
|
127
125
|
if (!shouldPrefetch()) return "";
|
|
128
126
|
const targetUrl = buildPrefetchUrl(url, segmentIds, version);
|
|
129
127
|
if (!targetUrl) return "";
|
|
130
|
-
const key = buildPrefetchKey(targetUrl);
|
|
128
|
+
const key = buildPrefetchKey(window.location.href, targetUrl);
|
|
131
129
|
if (hasPrefetch(key)) return key;
|
|
132
130
|
const fetchUrlStr = targetUrl.toString();
|
|
133
131
|
enqueuePrefetch(key, (signal) =>
|