@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.
- package/CLAUDE.md +7 -0
- package/README.md +19 -0
- package/dist/vite/index.js +1298 -0
- package/package.json +140 -0
- package/skills/caching/SKILL.md +319 -0
- package/skills/document-cache/SKILL.md +152 -0
- package/skills/hooks/SKILL.md +359 -0
- package/skills/intercept/SKILL.md +292 -0
- package/skills/layout/SKILL.md +216 -0
- package/skills/loader/SKILL.md +365 -0
- package/skills/middleware/SKILL.md +442 -0
- package/skills/parallel/SKILL.md +255 -0
- package/skills/route/SKILL.md +141 -0
- package/skills/router-setup/SKILL.md +403 -0
- package/skills/theme/SKILL.md +54 -0
- package/skills/typesafety/SKILL.md +352 -0
- package/src/__mocks__/version.ts +6 -0
- package/src/__tests__/component-utils.test.ts +76 -0
- package/src/__tests__/route-definition.test.ts +63 -0
- package/src/__tests__/urls.test.tsx +436 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +893 -0
- package/src/browser/navigation-client.ts +162 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +559 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +275 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +178 -0
- package/src/browser/react/use-href.tsx +208 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-navigation.ts +150 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +164 -0
- package/src/browser/rsc-router.tsx +353 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/server-action-bridge.ts +747 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +464 -0
- package/src/cache/__tests__/document-cache.test.ts +522 -0
- package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
- package/src/cache/__tests__/memory-store.test.ts +484 -0
- package/src/cache/cache-scope.ts +565 -0
- package/src/cache/cf/__tests__/cf-cache-store.test.ts +428 -0
- package/src/cache/cf/cf-cache-store.ts +428 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/document-cache.ts +340 -0
- package/src/cache/index.ts +58 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +387 -0
- package/src/client.rsc.tsx +88 -0
- package/src/client.tsx +621 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +23 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +259 -0
- package/src/handle.ts +120 -0
- package/src/handles/MetaTags.tsx +193 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/href-client.ts +128 -0
- package/src/href-context.ts +33 -0
- package/src/href.ts +177 -0
- package/src/index.rsc.ts +79 -0
- package/src/index.ts +87 -0
- package/src/loader.rsc.ts +204 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +198 -0
- package/src/route-definition.ts +1371 -0
- package/src/route-map-builder.ts +146 -0
- package/src/route-types.ts +198 -0
- package/src/route-utils.ts +89 -0
- package/src/router/__tests__/match-context.test.ts +104 -0
- package/src/router/__tests__/match-pipelines.test.ts +537 -0
- package/src/router/__tests__/match-result.test.ts +566 -0
- package/src/router/__tests__/on-error.test.ts +935 -0
- package/src/router/__tests__/pattern-matching.test.ts +577 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +158 -0
- package/src/router/loader-resolution.ts +326 -0
- package/src/router/manifest.ts +138 -0
- package/src/router/match-context.ts +264 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +261 -0
- package/src/router/match-middleware/cache-store.ts +266 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +268 -0
- package/src/router/match-middleware/segment-resolution.ts +174 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +214 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.test.ts +1355 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +272 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +299 -0
- package/src/router/types.ts +96 -0
- package/src/router.ts +3876 -0
- package/src/rsc/__tests__/helpers.test.ts +175 -0
- package/src/rsc/handler.ts +1060 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +237 -0
- package/src/segment-system.tsx +456 -0
- package/src/server/__tests__/request-context.test.ts +171 -0
- package/src/server/context.ts +417 -0
- package/src/server/handle-store.ts +230 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +554 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +146 -0
- package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
- package/src/ssr/index.tsx +234 -0
- package/src/theme/ThemeProvider.tsx +291 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/__tests__/theme.test.ts +120 -0
- package/src/theme/constants.ts +55 -0
- package/src/theme/index.ts +58 -0
- package/src/theme/theme-context.ts +70 -0
- package/src/theme/theme-script.ts +152 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types.ts +1561 -0
- package/src/urls.ts +726 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +357 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/index.ts +787 -0
- package/src/vite/package-resolution.ts +125 -0
- package/src/vite/version.d.ts +12 -0
- 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
|
+
});
|