rwsdk 1.0.0-beta.40 → 1.0.0-beta.42
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/lib/e2e/dev.mjs +14 -10
- package/dist/lib/e2e/index.d.mts +1 -0
- package/dist/lib/e2e/index.mjs +1 -0
- package/dist/lib/e2e/release.mjs +9 -4
- package/dist/lib/e2e/testHarness.d.mts +9 -4
- package/dist/lib/e2e/testHarness.mjs +20 -2
- package/dist/runtime/client/client.d.ts +10 -3
- package/dist/runtime/client/client.js +76 -13
- package/dist/runtime/client/navigation.d.ts +6 -2
- package/dist/runtime/client/navigation.js +29 -17
- package/dist/runtime/client/navigationCache.d.ts +68 -0
- package/dist/runtime/client/navigationCache.js +294 -0
- package/dist/runtime/client/navigationCache.test.d.ts +1 -0
- package/dist/runtime/client/navigationCache.test.js +456 -0
- package/dist/runtime/client/types.d.ts +25 -3
- package/dist/runtime/client/types.js +7 -1
- package/dist/runtime/lib/realtime/client.js +17 -1
- package/dist/runtime/render/normalizeActionResult.js +8 -1
- package/package.json +2 -1
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
const TAB_ID_STORAGE_KEY = "rwsdk-navigation-tab-id";
|
|
2
|
+
const BUILD_ID = "rwsdk"; // Stable build identifier
|
|
3
|
+
let cacheState = null;
|
|
4
|
+
function getOrInitializeCacheState() {
|
|
5
|
+
if (cacheState) {
|
|
6
|
+
return cacheState;
|
|
7
|
+
}
|
|
8
|
+
// Get or generate tabId
|
|
9
|
+
let tabId;
|
|
10
|
+
if (typeof window !== "undefined" && window.sessionStorage) {
|
|
11
|
+
try {
|
|
12
|
+
const stored = sessionStorage.getItem(TAB_ID_STORAGE_KEY);
|
|
13
|
+
if (stored) {
|
|
14
|
+
tabId = stored;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
tabId = crypto.randomUUID();
|
|
18
|
+
sessionStorage.setItem(TAB_ID_STORAGE_KEY, tabId);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Fallback to in-memory tabId if sessionStorage is unavailable
|
|
23
|
+
tabId = crypto.randomUUID();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
// Fallback for non-browser environments
|
|
28
|
+
tabId = crypto.randomUUID();
|
|
29
|
+
}
|
|
30
|
+
cacheState = {
|
|
31
|
+
tabId,
|
|
32
|
+
generation: 0,
|
|
33
|
+
buildId: BUILD_ID,
|
|
34
|
+
};
|
|
35
|
+
return cacheState;
|
|
36
|
+
}
|
|
37
|
+
function getCurrentCacheName() {
|
|
38
|
+
const state = getOrInitializeCacheState();
|
|
39
|
+
return `rsc-prefetch:${state.buildId}:${state.tabId}:${state.generation}`;
|
|
40
|
+
}
|
|
41
|
+
function incrementGeneration() {
|
|
42
|
+
const state = getOrInitializeCacheState();
|
|
43
|
+
state.generation++;
|
|
44
|
+
return state.generation;
|
|
45
|
+
}
|
|
46
|
+
function getCurrentGeneration() {
|
|
47
|
+
const state = getOrInitializeCacheState();
|
|
48
|
+
return state.generation;
|
|
49
|
+
}
|
|
50
|
+
function getTabId() {
|
|
51
|
+
const state = getOrInitializeCacheState();
|
|
52
|
+
return state.tabId;
|
|
53
|
+
}
|
|
54
|
+
function getBuildId() {
|
|
55
|
+
const state = getOrInitializeCacheState();
|
|
56
|
+
return state.buildId;
|
|
57
|
+
}
|
|
58
|
+
function getBrowserNavigationCacheEnvironment() {
|
|
59
|
+
if (typeof window === "undefined") {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
isSecureContext: window.isSecureContext,
|
|
64
|
+
origin: window.location.origin,
|
|
65
|
+
// CacheStorage is only available in secure contexts in supporting browsers.
|
|
66
|
+
caches: "caches" in window ? window.caches : undefined,
|
|
67
|
+
fetch: window.fetch.bind(window),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Creates a default NavigationCacheStorage implementation that wraps the browser's CacheStorage API.
|
|
72
|
+
* This maintains the current generation-based cache naming and eviction logic.
|
|
73
|
+
*/
|
|
74
|
+
export function createDefaultNavigationCacheStorage(env) {
|
|
75
|
+
const runtimeEnv = env ?? getBrowserNavigationCacheEnvironment();
|
|
76
|
+
if (!runtimeEnv) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const { caches } = runtimeEnv;
|
|
80
|
+
if (!caches) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
async open(cacheName) {
|
|
85
|
+
const cache = await caches.open(cacheName);
|
|
86
|
+
return {
|
|
87
|
+
async put(request, response) {
|
|
88
|
+
await cache.put(request, response);
|
|
89
|
+
},
|
|
90
|
+
async match(request) {
|
|
91
|
+
return (await cache.match(request)) ?? undefined;
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
async delete(cacheName) {
|
|
96
|
+
return await caches.delete(cacheName);
|
|
97
|
+
},
|
|
98
|
+
async keys() {
|
|
99
|
+
return await caches.keys();
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Preloads the RSC navigation response for a given URL into the Cache API.
|
|
105
|
+
*
|
|
106
|
+
* This issues a GET request with the `__rsc` query parameter set, and, on a
|
|
107
|
+
* successful response, stores it in a versioned Cache using `cache.put`.
|
|
108
|
+
*
|
|
109
|
+
* See MDN for Cache interface semantics:
|
|
110
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/Cache
|
|
111
|
+
*/
|
|
112
|
+
export async function preloadNavigationUrl(rawUrl, env, cacheStorage) {
|
|
113
|
+
const runtimeEnv = env ?? getBrowserNavigationCacheEnvironment();
|
|
114
|
+
if (!runtimeEnv) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const { origin, fetch } = runtimeEnv;
|
|
118
|
+
// Use provided cacheStorage or create default one
|
|
119
|
+
const storage = cacheStorage ?? createDefaultNavigationCacheStorage(runtimeEnv);
|
|
120
|
+
// CacheStorage may be evicted by the browser at any time. We treat it as a
|
|
121
|
+
// best-effort optimization.
|
|
122
|
+
if (!storage) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const url = rawUrl instanceof URL
|
|
127
|
+
? new URL(rawUrl.toString())
|
|
128
|
+
: new URL(rawUrl, origin);
|
|
129
|
+
if (url.origin !== origin) {
|
|
130
|
+
// Only cache same-origin navigations.
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// Ensure we are fetching the RSC navigation response.
|
|
134
|
+
url.searchParams.set("__rsc", "");
|
|
135
|
+
const request = new Request(url.toString(), {
|
|
136
|
+
method: "GET",
|
|
137
|
+
redirect: "manual",
|
|
138
|
+
});
|
|
139
|
+
const cacheName = getCurrentCacheName();
|
|
140
|
+
const cache = await storage.open(cacheName);
|
|
141
|
+
const response = await fetch(request);
|
|
142
|
+
// Avoid caching obvious error responses; browsers may still evict entries
|
|
143
|
+
// at any time, see MDN Cache docs for details.
|
|
144
|
+
if (response.status >= 400) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
await cache.put(request, response.clone());
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
// Best-effort optimization; never let cache failures break navigation.
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Attempts to retrieve a cached navigation response for the given URL.
|
|
156
|
+
*
|
|
157
|
+
* Returns the cached Response if found, or undefined if not cached or if
|
|
158
|
+
* CacheStorage is unavailable.
|
|
159
|
+
*/
|
|
160
|
+
export async function getCachedNavigationResponse(rawUrl, env, cacheStorage) {
|
|
161
|
+
const runtimeEnv = env ?? getBrowserNavigationCacheEnvironment();
|
|
162
|
+
if (!runtimeEnv) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
const { origin } = runtimeEnv;
|
|
166
|
+
// Use provided cacheStorage, check global, or create default one
|
|
167
|
+
let storage = cacheStorage;
|
|
168
|
+
if (!storage && typeof globalThis !== "undefined") {
|
|
169
|
+
storage = globalThis.__rsc_cacheStorage;
|
|
170
|
+
}
|
|
171
|
+
storage = storage ?? createDefaultNavigationCacheStorage(runtimeEnv);
|
|
172
|
+
if (!storage) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const url = rawUrl instanceof URL
|
|
177
|
+
? new URL(rawUrl.toString())
|
|
178
|
+
: new URL(rawUrl, origin);
|
|
179
|
+
if (url.origin !== origin) {
|
|
180
|
+
// Only cache same-origin navigations.
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
// Ensure we are matching the RSC navigation response.
|
|
184
|
+
url.searchParams.set("__rsc", "");
|
|
185
|
+
const request = new Request(url.toString(), {
|
|
186
|
+
method: "GET",
|
|
187
|
+
redirect: "manual",
|
|
188
|
+
});
|
|
189
|
+
const cacheName = getCurrentCacheName();
|
|
190
|
+
const cache = await storage.open(cacheName);
|
|
191
|
+
const cachedResponse = await cache.match(request);
|
|
192
|
+
return cachedResponse ?? undefined;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
// Best-effort optimization; never let cache failures break navigation.
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Cleans up old generation caches for the current tab.
|
|
201
|
+
*
|
|
202
|
+
* This should be called after navigation commits to evict cache entries from
|
|
203
|
+
* previous navigations. It runs asynchronously via requestIdleCallback or
|
|
204
|
+
* setTimeout to avoid blocking the critical path.
|
|
205
|
+
*/
|
|
206
|
+
export async function evictOldGenerationCaches(env, cacheStorage) {
|
|
207
|
+
const runtimeEnv = env ?? getBrowserNavigationCacheEnvironment();
|
|
208
|
+
if (!runtimeEnv) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// Use provided cacheStorage or create default one
|
|
212
|
+
const storage = cacheStorage ?? createDefaultNavigationCacheStorage(runtimeEnv);
|
|
213
|
+
if (!storage) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const currentGeneration = getCurrentGeneration();
|
|
217
|
+
const tabId = getTabId();
|
|
218
|
+
const buildId = getBuildId();
|
|
219
|
+
// Schedule cleanup in idle time to avoid blocking navigation
|
|
220
|
+
const cleanup = async () => {
|
|
221
|
+
try {
|
|
222
|
+
// List all cache names
|
|
223
|
+
const cacheNames = await storage.keys();
|
|
224
|
+
const prefix = `rsc-prefetch:${buildId}:${tabId}:`;
|
|
225
|
+
// Find all caches for this tab
|
|
226
|
+
const tabCaches = cacheNames.filter((name) => name.startsWith(prefix));
|
|
227
|
+
// Delete caches with generation numbers less than current
|
|
228
|
+
const deletePromises = tabCaches.map((cacheName) => {
|
|
229
|
+
const match = cacheName.match(new RegExp(`${prefix}(\\d+)$`));
|
|
230
|
+
if (match) {
|
|
231
|
+
const generation = parseInt(match[1], 10);
|
|
232
|
+
if (generation < currentGeneration) {
|
|
233
|
+
return storage.delete(cacheName);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return Promise.resolve(false);
|
|
237
|
+
});
|
|
238
|
+
await Promise.all(deletePromises);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
// Best-effort cleanup; never let failures break navigation.
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
// Use requestIdleCallback if available, otherwise setTimeout
|
|
245
|
+
if (typeof requestIdleCallback !== "undefined") {
|
|
246
|
+
requestIdleCallback(cleanup, { timeout: 5000 });
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
setTimeout(cleanup, 0);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Increments the generation counter and schedules cleanup of old caches.
|
|
254
|
+
*
|
|
255
|
+
* This should be called after navigation commits to mark the current generation
|
|
256
|
+
* as complete and prepare for the next navigation cycle.
|
|
257
|
+
*/
|
|
258
|
+
export function onNavigationCommit(env, cacheStorage) {
|
|
259
|
+
incrementGeneration();
|
|
260
|
+
void evictOldGenerationCaches(env, cacheStorage);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Scan the document for `<link rel="prefetch" href="...">` elements that point
|
|
264
|
+
* to same-origin paths and prefetch their RSC navigation responses into the
|
|
265
|
+
* Cache API.
|
|
266
|
+
*
|
|
267
|
+
* This is invoked after client navigations to warm the navigation cache in
|
|
268
|
+
* the background. We intentionally keep Cache usage write-only for now; reads
|
|
269
|
+
* still go through the normal fetch path.
|
|
270
|
+
*/
|
|
271
|
+
export async function preloadFromLinkTags(doc = document, env, cacheStorage) {
|
|
272
|
+
if (typeof doc === "undefined") {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const links = Array.from(doc.querySelectorAll('link[rel="prefetch"][href]'));
|
|
276
|
+
await Promise.all(links.map((link) => {
|
|
277
|
+
const href = link.getAttribute("href");
|
|
278
|
+
if (!href) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
// Treat paths that start with "/" as route-like; assets (e.g. .js, .css)
|
|
282
|
+
// are already handled by the existing modulepreload pipeline.
|
|
283
|
+
if (!href.startsWith("/")) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
const url = new URL(href, env?.origin ?? window.location.origin);
|
|
288
|
+
return preloadNavigationUrl(url, env, cacheStorage);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|