@rangojs/router 0.0.0-experimental.2

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.
Files changed (155) hide show
  1. package/CLAUDE.md +7 -0
  2. package/README.md +19 -0
  3. package/dist/vite/index.js +1298 -0
  4. package/package.json +140 -0
  5. package/skills/caching/SKILL.md +319 -0
  6. package/skills/document-cache/SKILL.md +152 -0
  7. package/skills/hooks/SKILL.md +359 -0
  8. package/skills/intercept/SKILL.md +292 -0
  9. package/skills/layout/SKILL.md +216 -0
  10. package/skills/loader/SKILL.md +365 -0
  11. package/skills/middleware/SKILL.md +442 -0
  12. package/skills/parallel/SKILL.md +255 -0
  13. package/skills/route/SKILL.md +141 -0
  14. package/skills/router-setup/SKILL.md +403 -0
  15. package/skills/theme/SKILL.md +54 -0
  16. package/skills/typesafety/SKILL.md +352 -0
  17. package/src/__mocks__/version.ts +6 -0
  18. package/src/__tests__/component-utils.test.ts +76 -0
  19. package/src/__tests__/route-definition.test.ts +63 -0
  20. package/src/__tests__/urls.test.tsx +436 -0
  21. package/src/browser/event-controller.ts +876 -0
  22. package/src/browser/index.ts +18 -0
  23. package/src/browser/link-interceptor.ts +121 -0
  24. package/src/browser/lru-cache.ts +69 -0
  25. package/src/browser/merge-segment-loaders.ts +126 -0
  26. package/src/browser/navigation-bridge.ts +893 -0
  27. package/src/browser/navigation-client.ts +162 -0
  28. package/src/browser/navigation-store.ts +823 -0
  29. package/src/browser/partial-update.ts +559 -0
  30. package/src/browser/react/Link.tsx +248 -0
  31. package/src/browser/react/NavigationProvider.tsx +275 -0
  32. package/src/browser/react/ScrollRestoration.tsx +94 -0
  33. package/src/browser/react/context.ts +53 -0
  34. package/src/browser/react/index.ts +52 -0
  35. package/src/browser/react/location-state-shared.ts +120 -0
  36. package/src/browser/react/location-state.ts +62 -0
  37. package/src/browser/react/use-action.ts +240 -0
  38. package/src/browser/react/use-client-cache.ts +56 -0
  39. package/src/browser/react/use-handle.ts +178 -0
  40. package/src/browser/react/use-href.tsx +208 -0
  41. package/src/browser/react/use-link-status.ts +134 -0
  42. package/src/browser/react/use-navigation.ts +150 -0
  43. package/src/browser/react/use-segments.ts +188 -0
  44. package/src/browser/request-controller.ts +164 -0
  45. package/src/browser/rsc-router.tsx +353 -0
  46. package/src/browser/scroll-restoration.ts +324 -0
  47. package/src/browser/server-action-bridge.ts +747 -0
  48. package/src/browser/shallow.ts +35 -0
  49. package/src/browser/types.ts +464 -0
  50. package/src/cache/__tests__/document-cache.test.ts +522 -0
  51. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  52. package/src/cache/__tests__/memory-store.test.ts +484 -0
  53. package/src/cache/cache-scope.ts +565 -0
  54. package/src/cache/cf/__tests__/cf-cache-store.test.ts +428 -0
  55. package/src/cache/cf/cf-cache-store.ts +428 -0
  56. package/src/cache/cf/index.ts +19 -0
  57. package/src/cache/document-cache.ts +340 -0
  58. package/src/cache/index.ts +58 -0
  59. package/src/cache/memory-segment-store.ts +150 -0
  60. package/src/cache/memory-store.ts +253 -0
  61. package/src/cache/types.ts +387 -0
  62. package/src/client.rsc.tsx +88 -0
  63. package/src/client.tsx +621 -0
  64. package/src/component-utils.ts +76 -0
  65. package/src/components/DefaultDocument.tsx +23 -0
  66. package/src/default-error-boundary.tsx +88 -0
  67. package/src/deps/browser.ts +8 -0
  68. package/src/deps/html-stream-client.ts +2 -0
  69. package/src/deps/html-stream-server.ts +2 -0
  70. package/src/deps/rsc.ts +10 -0
  71. package/src/deps/ssr.ts +2 -0
  72. package/src/errors.ts +259 -0
  73. package/src/handle.ts +120 -0
  74. package/src/handles/MetaTags.tsx +193 -0
  75. package/src/handles/index.ts +6 -0
  76. package/src/handles/meta.ts +247 -0
  77. package/src/href-client.ts +128 -0
  78. package/src/href-context.ts +33 -0
  79. package/src/href.ts +177 -0
  80. package/src/index.rsc.ts +79 -0
  81. package/src/index.ts +87 -0
  82. package/src/loader.rsc.ts +204 -0
  83. package/src/loader.ts +47 -0
  84. package/src/network-error-thrower.tsx +21 -0
  85. package/src/outlet-context.ts +15 -0
  86. package/src/root-error-boundary.tsx +277 -0
  87. package/src/route-content-wrapper.tsx +198 -0
  88. package/src/route-definition.ts +1371 -0
  89. package/src/route-map-builder.ts +146 -0
  90. package/src/route-types.ts +198 -0
  91. package/src/route-utils.ts +89 -0
  92. package/src/router/__tests__/match-context.test.ts +104 -0
  93. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  94. package/src/router/__tests__/match-result.test.ts +566 -0
  95. package/src/router/__tests__/on-error.test.ts +935 -0
  96. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  97. package/src/router/error-handling.ts +287 -0
  98. package/src/router/handler-context.ts +158 -0
  99. package/src/router/loader-resolution.ts +326 -0
  100. package/src/router/manifest.ts +138 -0
  101. package/src/router/match-context.ts +264 -0
  102. package/src/router/match-middleware/background-revalidation.ts +236 -0
  103. package/src/router/match-middleware/cache-lookup.ts +261 -0
  104. package/src/router/match-middleware/cache-store.ts +266 -0
  105. package/src/router/match-middleware/index.ts +81 -0
  106. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  107. package/src/router/match-middleware/segment-resolution.ts +174 -0
  108. package/src/router/match-pipelines.ts +214 -0
  109. package/src/router/match-result.ts +214 -0
  110. package/src/router/metrics.ts +62 -0
  111. package/src/router/middleware.test.ts +1355 -0
  112. package/src/router/middleware.ts +748 -0
  113. package/src/router/pattern-matching.ts +272 -0
  114. package/src/router/revalidation.ts +190 -0
  115. package/src/router/router-context.ts +299 -0
  116. package/src/router/types.ts +96 -0
  117. package/src/router.ts +3876 -0
  118. package/src/rsc/__tests__/helpers.test.ts +175 -0
  119. package/src/rsc/handler.ts +1060 -0
  120. package/src/rsc/helpers.ts +64 -0
  121. package/src/rsc/index.ts +56 -0
  122. package/src/rsc/nonce.ts +18 -0
  123. package/src/rsc/types.ts +237 -0
  124. package/src/segment-system.tsx +456 -0
  125. package/src/server/__tests__/request-context.test.ts +171 -0
  126. package/src/server/context.ts +417 -0
  127. package/src/server/handle-store.ts +230 -0
  128. package/src/server/loader-registry.ts +174 -0
  129. package/src/server/request-context.ts +554 -0
  130. package/src/server/root-layout.tsx +10 -0
  131. package/src/server/tsconfig.json +14 -0
  132. package/src/server.ts +146 -0
  133. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  134. package/src/ssr/index.tsx +234 -0
  135. package/src/theme/ThemeProvider.tsx +291 -0
  136. package/src/theme/ThemeScript.tsx +61 -0
  137. package/src/theme/__tests__/theme.test.ts +120 -0
  138. package/src/theme/constants.ts +55 -0
  139. package/src/theme/index.ts +58 -0
  140. package/src/theme/theme-context.ts +70 -0
  141. package/src/theme/theme-script.ts +152 -0
  142. package/src/theme/types.ts +182 -0
  143. package/src/theme/use-theme.ts +44 -0
  144. package/src/types.ts +1561 -0
  145. package/src/urls.ts +726 -0
  146. package/src/use-loader.tsx +346 -0
  147. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  148. package/src/vite/expose-action-id.ts +344 -0
  149. package/src/vite/expose-handle-id.ts +209 -0
  150. package/src/vite/expose-loader-id.ts +357 -0
  151. package/src/vite/expose-location-state-id.ts +177 -0
  152. package/src/vite/index.ts +787 -0
  153. package/src/vite/package-resolution.ts +125 -0
  154. package/src/vite/version.d.ts +12 -0
  155. package/src/vite/virtual-entries.ts +109 -0
@@ -0,0 +1,428 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import {
3
+ CFCacheStore,
4
+ CACHE_STALE_AT_HEADER,
5
+ CACHE_STATUS_HEADER,
6
+ } from "../cf-cache-store";
7
+ import type { CachedEntryData } from "../../types";
8
+
9
+ // ============================================================================
10
+ // Mock Cloudflare Cache API
11
+ // ============================================================================
12
+
13
+ class MockCache {
14
+ private store = new Map<string, Response>();
15
+
16
+ async match(request: Request): Promise<Response | undefined> {
17
+ return this.store.get(request.url)?.clone();
18
+ }
19
+
20
+ async put(request: Request, response: Response): Promise<void> {
21
+ this.store.set(request.url, response.clone());
22
+ }
23
+
24
+ async delete(request: Request): Promise<boolean> {
25
+ return this.store.delete(request.url);
26
+ }
27
+
28
+ clear(): void {
29
+ this.store.clear();
30
+ }
31
+ }
32
+
33
+ class MockCaches {
34
+ private caches = new Map<string, MockCache>();
35
+ private _default = new MockCache();
36
+
37
+ async open(name: string): Promise<MockCache> {
38
+ if (!this.caches.has(name)) {
39
+ this.caches.set(name, new MockCache());
40
+ }
41
+ return this.caches.get(name)!;
42
+ }
43
+
44
+ get default(): MockCache {
45
+ return this._default;
46
+ }
47
+
48
+ clear(): void {
49
+ this._default.clear();
50
+ this.caches.forEach((cache) => cache.clear());
51
+ this.caches.clear();
52
+ }
53
+ }
54
+
55
+ // Install mock globally
56
+ const mockCaches = new MockCaches();
57
+ (globalThis as any).caches = mockCaches;
58
+
59
+ // ============================================================================
60
+ // Mock ExecutionContext
61
+ // ============================================================================
62
+
63
+ const createMockCtx = () => ({
64
+ waitUntil: vi.fn((p: Promise<any>) => p),
65
+ passThroughOnException: vi.fn(),
66
+ });
67
+
68
+ // ============================================================================
69
+ // Test Data
70
+ // ============================================================================
71
+
72
+ const createTestData = (): CachedEntryData => ({
73
+ segments: [
74
+ {
75
+ encoded: "test-component",
76
+ metadata: {
77
+ id: "test-segment",
78
+ type: "route",
79
+ namespace: "test",
80
+ index: 0,
81
+ params: {},
82
+ },
83
+ },
84
+ ],
85
+ handles: {},
86
+ expiresAt: Date.now() + 60000,
87
+ });
88
+
89
+ // ============================================================================
90
+ // Tests
91
+ // ============================================================================
92
+
93
+ describe("CFCacheStore", () => {
94
+ beforeEach(() => {
95
+ mockCaches.clear();
96
+ vi.useFakeTimers();
97
+ });
98
+
99
+ describe("constructor", () => {
100
+ it("should require ctx", () => {
101
+ expect(() => new CFCacheStore({} as any)).toThrow(
102
+ "[CFCacheStore] ExecutionContext (ctx) is required"
103
+ );
104
+ });
105
+
106
+ it("should accept ctx and custom options", () => {
107
+ const store = new CFCacheStore({
108
+ ctx: createMockCtx(),
109
+ namespace: "custom-cache",
110
+ baseUrl: "https://custom.internal/",
111
+ defaults: { ttl: 120, swr: 600 },
112
+ });
113
+ expect(store.defaults).toEqual({ ttl: 120, swr: 600 });
114
+ });
115
+ });
116
+
117
+ describe("get/set", () => {
118
+ it("should return null for missing key", async () => {
119
+ const store = new CFCacheStore({ ctx: createMockCtx() });
120
+ const result = await store.get("missing-key");
121
+ expect(result).toBeNull();
122
+ });
123
+
124
+ it("should store and retrieve data", async () => {
125
+ const mockCtx = createMockCtx();
126
+ const store = new CFCacheStore({ ctx: mockCtx });
127
+ const data = createTestData();
128
+
129
+ await store.set("test-key", data, 60);
130
+ // Execute waitUntil callback
131
+ await mockCtx.waitUntil.mock.results[0].value;
132
+
133
+ const result = await store.get("test-key");
134
+
135
+ expect(result).not.toBeNull();
136
+ expect(result!.data).toEqual(data);
137
+ expect(result!.shouldRevalidate).toBe(false);
138
+ });
139
+
140
+ it("should set Cache-Control header with TTL", async () => {
141
+ const mockCtx = createMockCtx();
142
+ const store = new CFCacheStore({ ctx: mockCtx });
143
+ const data = createTestData();
144
+
145
+ await store.set("test-key", data, 60);
146
+ await mockCtx.waitUntil.mock.results[0].value;
147
+
148
+ // Uses caches.default by default
149
+ const cache = mockCaches.default;
150
+ const request = new Request("https://rsc-cache.internal.com/test-key");
151
+ const response = await cache.match(request);
152
+
153
+ expect(response?.headers.get("Cache-Control")).toBe("public, max-age=60");
154
+ });
155
+
156
+ it("should extend TTL with SWR window", async () => {
157
+ const mockCtx = createMockCtx();
158
+ const store = new CFCacheStore({ ctx: mockCtx });
159
+ const data = createTestData();
160
+
161
+ await store.set("test-key", data, 60, 300);
162
+ await mockCtx.waitUntil.mock.results[0].value;
163
+
164
+ const cache = mockCaches.default;
165
+ const request = new Request("https://rsc-cache.internal.com/test-key");
166
+ const response = await cache.match(request);
167
+
168
+ expect(response?.headers.get("Cache-Control")).toBe("public, max-age=360");
169
+ });
170
+
171
+ it("should use store defaults for SWR if not provided", async () => {
172
+ const mockCtx = createMockCtx();
173
+ const store = new CFCacheStore({ ctx: mockCtx, defaults: { swr: 120 } });
174
+ const data = createTestData();
175
+
176
+ await store.set("test-key", data, 60);
177
+ await mockCtx.waitUntil.mock.results[0].value;
178
+
179
+ const cache = mockCaches.default;
180
+ const request = new Request("https://rsc-cache.internal.com/test-key");
181
+ const response = await cache.match(request);
182
+
183
+ expect(response?.headers.get("Cache-Control")).toBe("public, max-age=180");
184
+ });
185
+
186
+ it("should use named cache when namespace is provided", async () => {
187
+ const mockCtx = createMockCtx();
188
+ const store = new CFCacheStore({ ctx: mockCtx, namespace: "custom-cache" });
189
+ const data = createTestData();
190
+
191
+ await store.set("test-key", data, 60);
192
+ await mockCtx.waitUntil.mock.results[0].value;
193
+
194
+ const cache = await mockCaches.open("custom-cache");
195
+ const request = new Request("https://rsc-cache.internal.com/test-key");
196
+ const response = await cache.match(request);
197
+
198
+ expect(response?.headers.get("Cache-Control")).toBe("public, max-age=60");
199
+ });
200
+
201
+ it("should use waitUntil for non-blocking writes", async () => {
202
+ const mockCtx = createMockCtx();
203
+ const store = new CFCacheStore({ ctx: mockCtx });
204
+ const data = createTestData();
205
+
206
+ await store.set("test-key", data, 60);
207
+
208
+ expect(mockCtx.waitUntil).toHaveBeenCalledTimes(1);
209
+ expect(mockCtx.waitUntil).toHaveBeenCalledWith(expect.any(Promise));
210
+
211
+ // Wait for the write to complete
212
+ await mockCtx.waitUntil.mock.results[0].value;
213
+
214
+ // Now the entry should be in cache
215
+ const result = await store.get("test-key");
216
+ expect(result).not.toBeNull();
217
+ });
218
+ });
219
+
220
+ describe("staleness headers", () => {
221
+ it("should set stale-at header based on TTL", async () => {
222
+ vi.setSystemTime(new Date("2024-01-01T00:00:00Z"));
223
+
224
+ const mockCtx = createMockCtx();
225
+ const store = new CFCacheStore({ ctx: mockCtx });
226
+ const data = createTestData();
227
+
228
+ await store.set("test-key", data, 60);
229
+ await mockCtx.waitUntil.mock.results[0].value;
230
+
231
+ const cache = mockCaches.default;
232
+ const request = new Request("https://rsc-cache.internal.com/test-key");
233
+ const response = await cache.match(request);
234
+
235
+ const staleAt = Number(response?.headers.get(CACHE_STALE_AT_HEADER));
236
+ const expectedStaleAt = Date.now() + 60 * 1000;
237
+
238
+ expect(staleAt).toBe(expectedStaleAt);
239
+ });
240
+
241
+ it("should set status header to HIT", async () => {
242
+ const mockCtx = createMockCtx();
243
+ const store = new CFCacheStore({ ctx: mockCtx });
244
+ const data = createTestData();
245
+
246
+ await store.set("test-key", data, 60);
247
+ await mockCtx.waitUntil.mock.results[0].value;
248
+
249
+ const cache = mockCaches.default;
250
+ const request = new Request("https://rsc-cache.internal.com/test-key");
251
+ const response = await cache.match(request);
252
+
253
+ expect(response?.headers.get(CACHE_STATUS_HEADER)).toBe("HIT");
254
+ });
255
+ });
256
+
257
+ describe("staleness detection and atomic revalidation", () => {
258
+ it("should return shouldRevalidate=false for fresh entries", async () => {
259
+ vi.setSystemTime(new Date("2024-01-01T00:00:00Z"));
260
+
261
+ const mockCtx = createMockCtx();
262
+ const store = new CFCacheStore({ ctx: mockCtx });
263
+ const data = createTestData();
264
+
265
+ await store.set("test-key", data, 60);
266
+ await mockCtx.waitUntil.mock.results[0].value;
267
+
268
+ // Still fresh
269
+ vi.advanceTimersByTime(30 * 1000);
270
+
271
+ const result = await store.get("test-key");
272
+ expect(result?.shouldRevalidate).toBe(false);
273
+ });
274
+
275
+ it("should return shouldRevalidate=true and atomically mark REVALIDATING for stale entries", async () => {
276
+ vi.setSystemTime(new Date("2024-01-01T00:00:00Z"));
277
+
278
+ const mockCtx = createMockCtx();
279
+ const store = new CFCacheStore({ ctx: mockCtx });
280
+ const data = createTestData();
281
+
282
+ await store.set("test-key", data, 60, 300);
283
+ await mockCtx.waitUntil.mock.results[0].value;
284
+
285
+ // Past TTL but within SWR window
286
+ vi.advanceTimersByTime(120 * 1000);
287
+
288
+ // First get should return shouldRevalidate=true and mark as REVALIDATING
289
+ const result = await store.get("test-key");
290
+ expect(result?.shouldRevalidate).toBe(true);
291
+
292
+ // Verify the entry is now marked as REVALIDATING
293
+ const cache = mockCaches.default;
294
+ const request = new Request(
295
+ "https://rsc-cache.internal.com/" + encodeURIComponent("test-key")
296
+ );
297
+ const response = await cache.match(request);
298
+ expect(response?.headers.get(CACHE_STATUS_HEADER)).toBe("REVALIDATING");
299
+ });
300
+
301
+ it("should return shouldRevalidate=false when already REVALIDATING", async () => {
302
+ vi.setSystemTime(new Date("2024-01-01T00:00:00Z"));
303
+
304
+ const mockCtx = createMockCtx();
305
+ const store = new CFCacheStore({ ctx: mockCtx });
306
+ const data = createTestData();
307
+
308
+ await store.set("test-key", data, 60, 300);
309
+ await mockCtx.waitUntil.mock.results[0].value;
310
+
311
+ // Make it stale
312
+ vi.advanceTimersByTime(120 * 1000);
313
+
314
+ // First get - atomically marks as REVALIDATING
315
+ const result1 = await store.get("test-key");
316
+ expect(result1?.shouldRevalidate).toBe(true);
317
+
318
+ // Second get - already REVALIDATING, should not trigger again
319
+ const result2 = await store.get("test-key");
320
+ expect(result2?.shouldRevalidate).toBe(false);
321
+ });
322
+
323
+ it("should prevent thundering herd with sequential requests", async () => {
324
+ // Note: Real thundering herd prevention relies on CF Cache API's atomic semantics.
325
+ // This test verifies sequential requests work correctly - first triggers revalidation,
326
+ // subsequent ones see REVALIDATING status and don't trigger again.
327
+ vi.setSystemTime(new Date("2024-01-01T00:00:00Z"));
328
+
329
+ const mockCtx = createMockCtx();
330
+ const store = new CFCacheStore({ ctx: mockCtx });
331
+ const data = createTestData();
332
+
333
+ await store.set("test-key", data, 60, 300);
334
+ await mockCtx.waitUntil.mock.results[0].value;
335
+
336
+ // Make it stale
337
+ vi.advanceTimersByTime(120 * 1000);
338
+
339
+ // Sequential requests - first triggers revalidation
340
+ const result1 = await store.get("test-key");
341
+ expect(result1?.shouldRevalidate).toBe(true);
342
+ expect(result1?.data).toBeDefined();
343
+
344
+ // Subsequent requests see REVALIDATING status
345
+ const result2 = await store.get("test-key");
346
+ expect(result2?.shouldRevalidate).toBe(false);
347
+ expect(result2?.data).toBeDefined();
348
+
349
+ const result3 = await store.get("test-key");
350
+ expect(result3?.shouldRevalidate).toBe(false);
351
+ expect(result3?.data).toBeDefined();
352
+ });
353
+ });
354
+
355
+ describe("delete", () => {
356
+ it("should delete existing entry", async () => {
357
+ const mockCtx = createMockCtx();
358
+ const store = new CFCacheStore({ ctx: mockCtx });
359
+ const data = createTestData();
360
+
361
+ await store.set("test-key", data, 60);
362
+ await mockCtx.waitUntil.mock.results[0].value;
363
+
364
+ const deleted = await store.delete("test-key");
365
+
366
+ expect(deleted).toBe(true);
367
+
368
+ const result = await store.get("test-key");
369
+ expect(result).toBeNull();
370
+ });
371
+
372
+ it("should return false for non-existent entry", async () => {
373
+ const store = new CFCacheStore({ ctx: createMockCtx() });
374
+ const deleted = await store.delete("missing-key");
375
+ expect(deleted).toBe(false);
376
+ });
377
+ });
378
+
379
+ describe("key encoding", () => {
380
+ it("should handle special characters in keys", async () => {
381
+ const mockCtx = createMockCtx();
382
+ const store = new CFCacheStore({ ctx: mockCtx });
383
+ const data = createTestData();
384
+
385
+ const key = "route:products/category=electronics&page=1";
386
+ await store.set(key, data, 60);
387
+ await mockCtx.waitUntil.mock.results[0].value;
388
+
389
+ const result = await store.get(key);
390
+ expect(result).not.toBeNull();
391
+ expect(result!.data).toEqual(data);
392
+ });
393
+ });
394
+
395
+ describe("baseUrl configuration", () => {
396
+ it("should use explicit baseUrl when provided", async () => {
397
+ const mockCtx = createMockCtx();
398
+ const store = new CFCacheStore({
399
+ ctx: mockCtx,
400
+ baseUrl: "https://custom.example.com/",
401
+ });
402
+ const data = createTestData();
403
+
404
+ await store.set("custom-key", data, 60);
405
+ await mockCtx.waitUntil.mock.results[0].value;
406
+
407
+ // Verify round-trip works with custom baseUrl
408
+ const result = await store.get("custom-key");
409
+ expect(result).not.toBeNull();
410
+ expect(result!.data).toEqual(data);
411
+ });
412
+
413
+ it("should use fallback when no requestContext available", async () => {
414
+ // Default behavior when getRequestContext returns null
415
+ const mockCtx = createMockCtx();
416
+ const store = new CFCacheStore({ ctx: mockCtx });
417
+ const data = createTestData();
418
+
419
+ await store.set("fallback-key", data, 60);
420
+ await mockCtx.waitUntil.mock.results[0].value;
421
+
422
+ // Verify round-trip works with fallback baseUrl
423
+ const result = await store.get("fallback-key");
424
+ expect(result).not.toBeNull();
425
+ expect(result!.data).toEqual(data);
426
+ });
427
+ });
428
+ });