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.
@@ -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 {};