@ivogt/rsc-router 0.0.0-experimental.1
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/README.md +19 -0
- package/package.json +131 -0
- package/src/__mocks__/version.ts +6 -0
- package/src/__tests__/route-definition.test.ts +63 -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 +891 -0
- package/src/browser/navigation-client.ts +155 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +545 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +228 -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-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 +149 -0
- package/src/browser/rsc-router.tsx +310 -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 +443 -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 +361 -0
- package/src/cache/cf/cf-cache-store.ts +274 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/index.ts +52 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +366 -0
- package/src/client.rsc.tsx +88 -0
- package/src/client.tsx +609 -0
- package/src/components/DefaultDocument.tsx +20 -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 +178 -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.ts +139 -0
- package/src/index.rsc.ts +69 -0
- package/src/index.ts +84 -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 +1333 -0
- package/src/route-map-builder.ts +140 -0
- package/src/route-types.ts +148 -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 +60 -0
- package/src/router/loader-resolution.ts +326 -0
- package/src/router/manifest.ts +116 -0
- package/src/router/match-context.ts +261 -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 +250 -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 +212 -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 +271 -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 +3484 -0
- package/src/rsc/__tests__/helpers.test.ts +175 -0
- package/src/rsc/handler.ts +942 -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 +225 -0
- package/src/segment-system.tsx +405 -0
- package/src/server/__tests__/request-context.test.ts +171 -0
- package/src/server/context.ts +340 -0
- package/src/server/handle-store.ts +230 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +470 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +126 -0
- package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
- package/src/ssr/index.tsx +215 -0
- package/src/types.ts +1473 -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 +608 -0
- package/src/vite/version.d.ts +12 -0
- package/src/vite/virtual-entries.ts +109 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
|
|
2
|
+
import { MemoryCacheStore } from "../memory-store";
|
|
3
|
+
|
|
4
|
+
describe("MemoryCacheStore", () => {
|
|
5
|
+
let store: MemoryCacheStore;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
store = new MemoryCacheStore();
|
|
9
|
+
vi.useFakeTimers();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.useRealTimers();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("basic operations", () => {
|
|
17
|
+
it("should return undefined for missing key", async () => {
|
|
18
|
+
const result = await store.match("missing-key");
|
|
19
|
+
expect(result).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should store and retrieve string values", async () => {
|
|
23
|
+
await store.put("key", "hello world");
|
|
24
|
+
const result = await store.match("key");
|
|
25
|
+
|
|
26
|
+
expect(result).toBeDefined();
|
|
27
|
+
expect(result!.value).toBe("hello world");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should store and retrieve object values", async () => {
|
|
31
|
+
const obj = { foo: "bar", nested: { a: 1 } };
|
|
32
|
+
await store.put("key", obj);
|
|
33
|
+
const result = await store.match("key");
|
|
34
|
+
|
|
35
|
+
expect(result).toBeDefined();
|
|
36
|
+
expect(result!.value).toEqual(obj);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should store and retrieve ArrayBuffer values", async () => {
|
|
40
|
+
const buffer = new TextEncoder().encode("test data").buffer;
|
|
41
|
+
await store.put("key", buffer);
|
|
42
|
+
const result = await store.match("key");
|
|
43
|
+
|
|
44
|
+
expect(result).toBeDefined();
|
|
45
|
+
expect(result!.value).toBeInstanceOf(ArrayBuffer);
|
|
46
|
+
const decoded = new TextDecoder().decode(result!.value as ArrayBuffer);
|
|
47
|
+
expect(decoded).toBe("test data");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should delete entries", async () => {
|
|
51
|
+
await store.put("key", "value");
|
|
52
|
+
const deleted = await store.delete("key");
|
|
53
|
+
|
|
54
|
+
expect(deleted).toBe(true);
|
|
55
|
+
const result = await store.match("key");
|
|
56
|
+
expect(result).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should return false when deleting non-existent key", async () => {
|
|
60
|
+
const deleted = await store.delete("missing");
|
|
61
|
+
expect(deleted).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should clear all entries", () => {
|
|
65
|
+
store.put("key1", "value1");
|
|
66
|
+
store.put("key2", "value2");
|
|
67
|
+
|
|
68
|
+
store.clear();
|
|
69
|
+
|
|
70
|
+
expect(store.size).toBe(0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should report correct size", async () => {
|
|
74
|
+
expect(store.size).toBe(0);
|
|
75
|
+
|
|
76
|
+
await store.put("key1", "value1");
|
|
77
|
+
expect(store.size).toBe(1);
|
|
78
|
+
|
|
79
|
+
await store.put("key2", "value2");
|
|
80
|
+
expect(store.size).toBe(2);
|
|
81
|
+
|
|
82
|
+
await store.delete("key1");
|
|
83
|
+
expect(store.size).toBe(1);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("TTL and expiration", () => {
|
|
88
|
+
it("should use default TTL of 60 seconds", async () => {
|
|
89
|
+
vi.setSystemTime(new Date("2024-01-01T00:00:00Z"));
|
|
90
|
+
|
|
91
|
+
await store.put("key", "value");
|
|
92
|
+
|
|
93
|
+
// Should be available before TTL
|
|
94
|
+
vi.advanceTimersByTime(59 * 1000);
|
|
95
|
+
let result = await store.match("key");
|
|
96
|
+
expect(result).toBeDefined();
|
|
97
|
+
|
|
98
|
+
// Should expire after TTL
|
|
99
|
+
vi.advanceTimersByTime(2 * 1000);
|
|
100
|
+
result = await store.match("key");
|
|
101
|
+
expect(result).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should respect custom TTL", async () => {
|
|
105
|
+
vi.setSystemTime(new Date("2024-01-01T00:00:00Z"));
|
|
106
|
+
|
|
107
|
+
await store.put("key", "value", { ttl: 10 });
|
|
108
|
+
|
|
109
|
+
// Should be available before TTL
|
|
110
|
+
vi.advanceTimersByTime(9 * 1000);
|
|
111
|
+
let result = await store.match("key");
|
|
112
|
+
expect(result).toBeDefined();
|
|
113
|
+
|
|
114
|
+
// Should expire after TTL
|
|
115
|
+
vi.advanceTimersByTime(2 * 1000);
|
|
116
|
+
result = await store.match("key");
|
|
117
|
+
expect(result).toBeUndefined();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should delete expired entries on match", async () => {
|
|
121
|
+
vi.setSystemTime(new Date("2024-01-01T00:00:00Z"));
|
|
122
|
+
|
|
123
|
+
await store.put("key", "value", { ttl: 5 });
|
|
124
|
+
expect(store.size).toBe(1);
|
|
125
|
+
|
|
126
|
+
vi.advanceTimersByTime(6 * 1000);
|
|
127
|
+
await store.match("key");
|
|
128
|
+
|
|
129
|
+
expect(store.size).toBe(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should purge all expired entries", async () => {
|
|
133
|
+
vi.setSystemTime(new Date("2024-01-01T00:00:00Z"));
|
|
134
|
+
|
|
135
|
+
await store.put("key1", "value1", { ttl: 5 });
|
|
136
|
+
await store.put("key2", "value2", { ttl: 10 });
|
|
137
|
+
await store.put("key3", "value3", { ttl: 20 });
|
|
138
|
+
|
|
139
|
+
vi.advanceTimersByTime(7 * 1000);
|
|
140
|
+
|
|
141
|
+
const purged = store.purgeExpired();
|
|
142
|
+
|
|
143
|
+
expect(purged).toBe(1);
|
|
144
|
+
expect(store.size).toBe(2);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("Response handling", () => {
|
|
149
|
+
it("should store and reconstruct Response objects", async () => {
|
|
150
|
+
const response = new Response("response body", {
|
|
151
|
+
status: 201,
|
|
152
|
+
headers: {
|
|
153
|
+
"Content-Type": "text/plain",
|
|
154
|
+
"X-Custom": "header",
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await store.put("key", response);
|
|
159
|
+
const result = await store.match<Response>("key");
|
|
160
|
+
|
|
161
|
+
expect(result).toBeDefined();
|
|
162
|
+
expect(result!.value).toBeInstanceOf(Response);
|
|
163
|
+
|
|
164
|
+
const retrieved = result!.value as Response;
|
|
165
|
+
expect(retrieved.status).toBe(201);
|
|
166
|
+
expect(retrieved.headers.get("Content-Type")).toBe("text/plain");
|
|
167
|
+
expect(retrieved.headers.get("X-Custom")).toBe("header");
|
|
168
|
+
expect(await retrieved.text()).toBe("response body");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should handle Response with default status", async () => {
|
|
172
|
+
const response = new Response("body");
|
|
173
|
+
|
|
174
|
+
await store.put("key", response);
|
|
175
|
+
const result = await store.match<Response>("key");
|
|
176
|
+
|
|
177
|
+
expect(result!.value).toBeInstanceOf(Response);
|
|
178
|
+
expect((result!.value as Response).status).toBe(200);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("ReadableStream handling", () => {
|
|
183
|
+
it("should store and reconstruct ReadableStream", async () => {
|
|
184
|
+
const chunks = ["hello", " ", "world"];
|
|
185
|
+
const stream = new ReadableStream({
|
|
186
|
+
start(controller) {
|
|
187
|
+
for (const chunk of chunks) {
|
|
188
|
+
controller.enqueue(new TextEncoder().encode(chunk));
|
|
189
|
+
}
|
|
190
|
+
controller.close();
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await store.put("key", stream);
|
|
195
|
+
const result = await store.match<ReadableStream>("key");
|
|
196
|
+
|
|
197
|
+
expect(result).toBeDefined();
|
|
198
|
+
expect(result!.value).toBeInstanceOf(ReadableStream);
|
|
199
|
+
|
|
200
|
+
const reader = (result!.value as ReadableStream).getReader();
|
|
201
|
+
const data: Uint8Array[] = [];
|
|
202
|
+
|
|
203
|
+
while (true) {
|
|
204
|
+
const { done, value } = await reader.read();
|
|
205
|
+
if (done) break;
|
|
206
|
+
data.push(value);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const decoded = new TextDecoder().decode(
|
|
210
|
+
data.reduce((acc, chunk) => {
|
|
211
|
+
const combined = new Uint8Array(acc.length + chunk.length);
|
|
212
|
+
combined.set(acc);
|
|
213
|
+
combined.set(chunk, acc.length);
|
|
214
|
+
return combined;
|
|
215
|
+
}, new Uint8Array())
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
expect(decoded).toBe("hello world");
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("metadata", () => {
|
|
223
|
+
it("should track value type in metadata", async () => {
|
|
224
|
+
await store.put("string", "test");
|
|
225
|
+
await store.put("object", { foo: "bar" });
|
|
226
|
+
|
|
227
|
+
const stringResult = await store.match("string");
|
|
228
|
+
const objectResult = await store.match("object");
|
|
229
|
+
|
|
230
|
+
expect(stringResult!.metadata.valueType).toBe("string");
|
|
231
|
+
expect(objectResult!.metadata.valueType).toBe("object");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("should store custom metadata", async () => {
|
|
235
|
+
await store.put("key", "value", {
|
|
236
|
+
metadata: {
|
|
237
|
+
custom: "data",
|
|
238
|
+
} as any,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const result = await store.match("key");
|
|
242
|
+
expect((result!.metadata as any).custom).toBe("data");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("should track expiresAt in metadata", async () => {
|
|
246
|
+
vi.setSystemTime(new Date("2024-01-01T00:00:00Z"));
|
|
247
|
+
|
|
248
|
+
await store.put("key", "value", { ttl: 60 });
|
|
249
|
+
|
|
250
|
+
const result = await store.match("key");
|
|
251
|
+
expect(result!.metadata.expiresAt).toBe(
|
|
252
|
+
new Date("2024-01-01T00:00:00Z").getTime() + 60 * 1000
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("should track response headers and status in metadata", async () => {
|
|
257
|
+
const response = new Response("body", {
|
|
258
|
+
status: 404,
|
|
259
|
+
headers: { "X-Test": "value" },
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await store.put("key", response);
|
|
263
|
+
|
|
264
|
+
const result = await store.match("key");
|
|
265
|
+
expect(result!.metadata.valueType).toBe("response");
|
|
266
|
+
expect(result!.metadata.responseStatus).toBe(404);
|
|
267
|
+
expect(result!.metadata.responseHeaders!["x-test"]).toBe("value");
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe("edge cases", () => {
|
|
272
|
+
it("should handle empty string values", async () => {
|
|
273
|
+
await store.put("key", "");
|
|
274
|
+
const result = await store.match("key");
|
|
275
|
+
|
|
276
|
+
expect(result).toBeDefined();
|
|
277
|
+
expect(result!.value).toBe("");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("should handle null in object values", async () => {
|
|
281
|
+
const obj = { foo: null, bar: "baz" };
|
|
282
|
+
await store.put("key", obj);
|
|
283
|
+
const result = await store.match("key");
|
|
284
|
+
|
|
285
|
+
expect(result!.value).toEqual(obj);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("should handle special characters in keys", async () => {
|
|
289
|
+
const key = "route:/users/123?filter=active&sort=name";
|
|
290
|
+
await store.put(key, "value");
|
|
291
|
+
const result = await store.match(key);
|
|
292
|
+
|
|
293
|
+
expect(result).toBeDefined();
|
|
294
|
+
expect(result!.value).toBe("value");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("should overwrite existing entries", async () => {
|
|
298
|
+
await store.put("key", "first");
|
|
299
|
+
await store.put("key", "second");
|
|
300
|
+
|
|
301
|
+
const result = await store.match("key");
|
|
302
|
+
expect(result!.value).toBe("second");
|
|
303
|
+
expect(store.size).toBe(1);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("should handle very long keys", async () => {
|
|
307
|
+
const longKey = "k".repeat(10000);
|
|
308
|
+
await store.put(longKey, "value");
|
|
309
|
+
const result = await store.match(longKey);
|
|
310
|
+
|
|
311
|
+
expect(result).toBeDefined();
|
|
312
|
+
expect(result!.value).toBe("value");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("should handle unicode characters in keys and values", async () => {
|
|
316
|
+
const unicodeKey = "路由/用户/日本語";
|
|
317
|
+
const unicodeValue = { message: "こんにちは世界 🌍" };
|
|
318
|
+
|
|
319
|
+
await store.put(unicodeKey, unicodeValue);
|
|
320
|
+
const result = await store.match(unicodeKey);
|
|
321
|
+
|
|
322
|
+
expect(result).toBeDefined();
|
|
323
|
+
expect(result!.value).toEqual(unicodeValue);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("should handle deeply nested objects", async () => {
|
|
327
|
+
const deepObject = {
|
|
328
|
+
level1: {
|
|
329
|
+
level2: {
|
|
330
|
+
level3: {
|
|
331
|
+
level4: {
|
|
332
|
+
value: "deep",
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
await store.put("key", deepObject);
|
|
340
|
+
const result = await store.match("key");
|
|
341
|
+
|
|
342
|
+
expect(result!.value).toEqual(deepObject);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("should handle arrays as values", async () => {
|
|
346
|
+
const arr = [1, "two", { three: 3 }, [4, 5]];
|
|
347
|
+
await store.put("key", arr);
|
|
348
|
+
const result = await store.match("key");
|
|
349
|
+
|
|
350
|
+
expect(result!.value).toEqual(arr);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("should handle TTL of 0 (immediate expiration)", async () => {
|
|
354
|
+
vi.setSystemTime(new Date("2024-01-01T00:00:00Z"));
|
|
355
|
+
|
|
356
|
+
await store.put("key", "value", { ttl: 0 });
|
|
357
|
+
|
|
358
|
+
vi.advanceTimersByTime(1);
|
|
359
|
+
const result = await store.match("key");
|
|
360
|
+
expect(result).toBeUndefined();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("should handle very large TTL", async () => {
|
|
364
|
+
vi.setSystemTime(new Date("2024-01-01T00:00:00Z"));
|
|
365
|
+
|
|
366
|
+
await store.put("key", "value", { ttl: 365 * 24 * 60 * 60 }); // 1 year
|
|
367
|
+
|
|
368
|
+
// Advance 6 months
|
|
369
|
+
vi.advanceTimersByTime(180 * 24 * 60 * 60 * 1000);
|
|
370
|
+
const result = await store.match("key");
|
|
371
|
+
expect(result).toBeDefined();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("should handle Response with empty body", async () => {
|
|
375
|
+
const response = new Response(null, { status: 200 });
|
|
376
|
+
|
|
377
|
+
await store.put("key", response);
|
|
378
|
+
const result = await store.match<Response>("key");
|
|
379
|
+
|
|
380
|
+
expect(result).toBeDefined();
|
|
381
|
+
expect((result!.value as Response).status).toBe(200);
|
|
382
|
+
expect(await (result!.value as Response).text()).toBe("");
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("should handle Response with 204 No Content status", async () => {
|
|
386
|
+
const response = new Response(null, { status: 204 });
|
|
387
|
+
|
|
388
|
+
await store.put("key", response);
|
|
389
|
+
const result = await store.match<Response>("key");
|
|
390
|
+
|
|
391
|
+
expect(result).toBeDefined();
|
|
392
|
+
expect((result!.value as Response).status).toBe(204);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("should handle Response with 304 Not Modified status", async () => {
|
|
396
|
+
const response = new Response(null, { status: 304 });
|
|
397
|
+
|
|
398
|
+
await store.put("key", response);
|
|
399
|
+
const result = await store.match<Response>("key");
|
|
400
|
+
|
|
401
|
+
expect(result).toBeDefined();
|
|
402
|
+
expect((result!.value as Response).status).toBe(304);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("should handle Response with JSON body", async () => {
|
|
406
|
+
const jsonBody = { users: [{ id: 1 }, { id: 2 }] };
|
|
407
|
+
const response = new Response(JSON.stringify(jsonBody), {
|
|
408
|
+
headers: { "Content-Type": "application/json" },
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
await store.put("key", response);
|
|
412
|
+
const result = await store.match<Response>("key");
|
|
413
|
+
|
|
414
|
+
const retrieved = result!.value as Response;
|
|
415
|
+
const body = await retrieved.json();
|
|
416
|
+
expect(body).toEqual(jsonBody);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("should handle binary data in ArrayBuffer", async () => {
|
|
420
|
+
// Create binary data with all byte values
|
|
421
|
+
const binaryData = new Uint8Array(256);
|
|
422
|
+
for (let i = 0; i < 256; i++) {
|
|
423
|
+
binaryData[i] = i;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
await store.put("key", binaryData.buffer);
|
|
427
|
+
const result = await store.match<ArrayBuffer>("key");
|
|
428
|
+
|
|
429
|
+
const retrieved = new Uint8Array(result!.value as ArrayBuffer);
|
|
430
|
+
expect(retrieved).toEqual(binaryData);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("should handle concurrent puts to same key", async () => {
|
|
434
|
+
// Simulate concurrent writes
|
|
435
|
+
const promises = [
|
|
436
|
+
store.put("key", "value1"),
|
|
437
|
+
store.put("key", "value2"),
|
|
438
|
+
store.put("key", "value3"),
|
|
439
|
+
];
|
|
440
|
+
|
|
441
|
+
await Promise.all(promises);
|
|
442
|
+
|
|
443
|
+
const result = await store.match("key");
|
|
444
|
+
// Last write wins
|
|
445
|
+
expect(["value1", "value2", "value3"]).toContain(result!.value);
|
|
446
|
+
expect(store.size).toBe(1);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("should handle concurrent reads and writes", async () => {
|
|
450
|
+
await store.put("key", "initial");
|
|
451
|
+
|
|
452
|
+
const operations = [
|
|
453
|
+
store.match("key"),
|
|
454
|
+
store.put("key", "updated"),
|
|
455
|
+
store.match("key"),
|
|
456
|
+
];
|
|
457
|
+
|
|
458
|
+
const results = await Promise.all(operations);
|
|
459
|
+
|
|
460
|
+
// First read gets initial or updated
|
|
461
|
+
expect(["initial", "updated"]).toContain(
|
|
462
|
+
(results[0] as any)?.value ?? "updated"
|
|
463
|
+
);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("should correctly purge multiple expired entries", async () => {
|
|
467
|
+
vi.setSystemTime(new Date("2024-01-01T00:00:00Z"));
|
|
468
|
+
|
|
469
|
+
await store.put("short1", "v1", { ttl: 5 });
|
|
470
|
+
await store.put("short2", "v2", { ttl: 5 });
|
|
471
|
+
await store.put("short3", "v3", { ttl: 5 });
|
|
472
|
+
await store.put("long1", "v4", { ttl: 100 });
|
|
473
|
+
await store.put("long2", "v5", { ttl: 100 });
|
|
474
|
+
|
|
475
|
+
expect(store.size).toBe(5);
|
|
476
|
+
|
|
477
|
+
vi.advanceTimersByTime(10 * 1000);
|
|
478
|
+
const purged = store.purgeExpired();
|
|
479
|
+
|
|
480
|
+
expect(purged).toBe(3);
|
|
481
|
+
expect(store.size).toBe(2);
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
});
|