@boredland/node-ts-cache 1.0.0 → 1.0.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 +2 -2
- package/dist/index.d.mts +109 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +5 -2
- package/.github/workflows/ci.yml +0 -27
- package/biome.json +0 -50
- package/renovate.json +0 -37
- package/src/cacheContainer.ts +0 -90
- package/src/debug.ts +0 -3
- package/src/hash.ts +0 -15
- package/src/index.ts +0 -4
- package/src/lruStorage.test.ts +0 -613
- package/src/lruStorage.ts +0 -32
- package/src/storage.ts +0 -27
- package/src/withCache.ts +0 -110
- package/tsconfig.json +0 -48
- package/tsdown.config.ts +0 -9
package/src/lruStorage.test.ts
DELETED
|
@@ -1,613 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { CacheContainer } from "./cacheContainer.ts";
|
|
3
|
-
import { LRUStorage } from "./lruStorage.ts";
|
|
4
|
-
import { withCacheFactory } from "./withCache.ts";
|
|
5
|
-
|
|
6
|
-
describe("LRUStorage with withCache", () => {
|
|
7
|
-
let storage: LRUStorage;
|
|
8
|
-
let container: CacheContainer;
|
|
9
|
-
let withCache: ReturnType<typeof withCacheFactory>;
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
storage = new LRUStorage({ max: 10 });
|
|
13
|
-
container = new CacheContainer(storage);
|
|
14
|
-
withCache = withCacheFactory(container);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
describe("Basic caching functionality", () => {
|
|
18
|
-
it("should cache function results", async () => {
|
|
19
|
-
const mockFn = vi.fn(async (x: number) => x * 2);
|
|
20
|
-
const cachedFn = withCache(mockFn);
|
|
21
|
-
|
|
22
|
-
const result1 = await cachedFn(5);
|
|
23
|
-
const result2 = await cachedFn(5);
|
|
24
|
-
|
|
25
|
-
expect(result1).toBe(10);
|
|
26
|
-
expect(result2).toBe(10);
|
|
27
|
-
expect(mockFn).toHaveResolvedTimes(1);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("should differentiate between different parameters", async () => {
|
|
31
|
-
const mockFn = vi.fn(async (x: number) => x * 2);
|
|
32
|
-
const cachedFn = withCache(mockFn);
|
|
33
|
-
|
|
34
|
-
const result1 = await cachedFn(5);
|
|
35
|
-
const result2 = await cachedFn(10);
|
|
36
|
-
|
|
37
|
-
expect(result1).toBe(10);
|
|
38
|
-
expect(result2).toBe(20);
|
|
39
|
-
expect(mockFn).toHaveResolvedTimes(2);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("should use custom calculateKey function", async () => {
|
|
43
|
-
const mockFn = vi.fn(async (x: number, y: number) => x + y);
|
|
44
|
-
const cachedFn = withCache(mockFn, {
|
|
45
|
-
calculateKey: ([x, y]) => `${x}-${y}`,
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const result1 = await cachedFn(1, 2);
|
|
49
|
-
const result2 = await cachedFn(1, 2);
|
|
50
|
-
|
|
51
|
-
expect(result1).toBe(3);
|
|
52
|
-
expect(result2).toBe(3);
|
|
53
|
-
expect(mockFn).toHaveResolvedTimes(1);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("should support prefix option", async () => {
|
|
57
|
-
const mockFn = vi.fn(async (x: number) => x * 2);
|
|
58
|
-
const cachedFn1 = withCache(mockFn, { prefix: "version1" });
|
|
59
|
-
const cachedFn2 = withCache(mockFn, { prefix: "version2" });
|
|
60
|
-
|
|
61
|
-
const result1 = await cachedFn1(5);
|
|
62
|
-
const result2 = await cachedFn2(5);
|
|
63
|
-
|
|
64
|
-
expect(result1).toBe(10);
|
|
65
|
-
expect(result2).toBe(10);
|
|
66
|
-
expect(mockFn).toHaveResolvedTimes(2);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("should work with multiple parameters", async () => {
|
|
70
|
-
const mockFn = vi.fn(async (a: number, b: string) => `${a}-${b}`);
|
|
71
|
-
const cachedFn = withCache(mockFn);
|
|
72
|
-
|
|
73
|
-
const result1 = await cachedFn(1, "a");
|
|
74
|
-
const result2 = await cachedFn(1, "a");
|
|
75
|
-
|
|
76
|
-
expect(result1).toBe("1-a");
|
|
77
|
-
expect(result2).toBe("1-a");
|
|
78
|
-
expect(mockFn).toHaveResolvedTimes(1);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
describe("TTL and expiration", () => {
|
|
83
|
-
it("should cache items with TTL in eager mode", async () => {
|
|
84
|
-
const mockFn = vi.fn(async (x: number) => x * 2);
|
|
85
|
-
const cachedFn = withCache(mockFn, { ttl: 100, strategy: "eager" });
|
|
86
|
-
|
|
87
|
-
const result1 = await cachedFn(5);
|
|
88
|
-
expect(result1).toBe(10);
|
|
89
|
-
expect(mockFn).toHaveResolvedTimes(1);
|
|
90
|
-
|
|
91
|
-
// Item should still be cached before expiration
|
|
92
|
-
const result2 = await cachedFn(5);
|
|
93
|
-
expect(result2).toBe(10);
|
|
94
|
-
expect(mockFn).toHaveResolvedTimes(1);
|
|
95
|
-
|
|
96
|
-
// Wait for expiration
|
|
97
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
98
|
-
|
|
99
|
-
// After expiration, item is removed and function is called again
|
|
100
|
-
const result3 = await cachedFn(5);
|
|
101
|
-
expect(result3).toBe(10);
|
|
102
|
-
expect(mockFn).toHaveResolvedTimes(2);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("should use lazy strategy to invalidate cache on touch", async () => {
|
|
106
|
-
const mockFn = vi.fn(async (x: number) => x * 2);
|
|
107
|
-
const cachedFn = withCache(mockFn, { ttl: 100, strategy: "lazy" });
|
|
108
|
-
|
|
109
|
-
const result1 = await cachedFn(5);
|
|
110
|
-
expect(result1).toBe(10);
|
|
111
|
-
expect(mockFn).toHaveResolvedTimes(1);
|
|
112
|
-
|
|
113
|
-
// Item should be cached for one subsequent call
|
|
114
|
-
const result2 = await cachedFn(5);
|
|
115
|
-
expect(result2).toBe(10);
|
|
116
|
-
expect(mockFn).toHaveResolvedTimes(1);
|
|
117
|
-
|
|
118
|
-
// Wait for expiration
|
|
119
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
120
|
-
|
|
121
|
-
// After expiration, the cached item is stale, but should be returned
|
|
122
|
-
const result3 = await cachedFn(5);
|
|
123
|
-
expect(result3).toBe(10);
|
|
124
|
-
expect(mockFn).toHaveResolvedTimes(1);
|
|
125
|
-
|
|
126
|
-
// Next call should have invalidated the cache and call the function again
|
|
127
|
-
const result4 = await cachedFn(5);
|
|
128
|
-
expect(result4).toBe(10);
|
|
129
|
-
expect(mockFn).toHaveResolvedTimes(2);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("should use swr strategy to return stale cache and revalidate in background", async () => {
|
|
133
|
-
const mockFn = vi.fn(async (x: number) => {
|
|
134
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
135
|
-
return x * 2;
|
|
136
|
-
});
|
|
137
|
-
const cachedFn = withCache(mockFn, { ttl: 100, strategy: "swr" });
|
|
138
|
-
|
|
139
|
-
const result1 = await cachedFn(5);
|
|
140
|
-
expect(result1).toBe(10);
|
|
141
|
-
expect(mockFn).toHaveResolvedTimes(1);
|
|
142
|
-
|
|
143
|
-
// Item should be cached
|
|
144
|
-
const result2 = await cachedFn(5);
|
|
145
|
-
expect(result2).toBe(10);
|
|
146
|
-
expect(mockFn).toHaveResolvedTimes(1);
|
|
147
|
-
|
|
148
|
-
// Wait for expiration
|
|
149
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
150
|
-
|
|
151
|
-
// With swr strategy, expired items are returned immediately
|
|
152
|
-
const result3 = await cachedFn(5);
|
|
153
|
-
expect(result3).toBe(10);
|
|
154
|
-
expect(mockFn).toHaveResolvedTimes(1);
|
|
155
|
-
// The stale cache is returned, but revalidation is queued in background
|
|
156
|
-
// Wait a bit for background revalidation to complete
|
|
157
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
158
|
-
expect(mockFn).toHaveResolvedTimes(2);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
describe("shouldStore option", () => {
|
|
163
|
-
it("should not cache when shouldStore returns false", async () => {
|
|
164
|
-
const mockFn = vi.fn(async (x: number) => x * 2);
|
|
165
|
-
const cachedFn = withCache(mockFn, {
|
|
166
|
-
shouldStore: (result: unknown) => (result as number) > 15,
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
const result1 = await cachedFn(5);
|
|
170
|
-
expect(result1).toBe(10);
|
|
171
|
-
|
|
172
|
-
const result2 = await cachedFn(5);
|
|
173
|
-
expect(result2).toBe(10);
|
|
174
|
-
expect(mockFn).toHaveResolvedTimes(2);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("should cache when shouldStore returns true", async () => {
|
|
178
|
-
const mockFn = vi.fn(async (x: number) => x * 2);
|
|
179
|
-
const cachedFn = withCache(mockFn, {
|
|
180
|
-
shouldStore: (result: unknown) => (result as number) > 5,
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
const result1 = await cachedFn(5);
|
|
184
|
-
expect(result1).toBe(10);
|
|
185
|
-
|
|
186
|
-
const result2 = await cachedFn(5);
|
|
187
|
-
expect(result2).toBe(10);
|
|
188
|
-
expect(mockFn).toHaveResolvedTimes(1);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("should evaluate shouldStore on complex results", async () => {
|
|
192
|
-
const mockFn = vi.fn(async (x: number) => ({
|
|
193
|
-
value: x * 2,
|
|
194
|
-
success: x > 0,
|
|
195
|
-
}));
|
|
196
|
-
const cachedFn = withCache(mockFn, {
|
|
197
|
-
shouldStore: (result: unknown) =>
|
|
198
|
-
(result as { success: boolean }).success,
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
const result1 = await cachedFn(5);
|
|
202
|
-
expect(result1).toEqual({ value: 10, success: true });
|
|
203
|
-
|
|
204
|
-
const result2 = await cachedFn(5);
|
|
205
|
-
expect(result2).toEqual({ value: 10, success: true });
|
|
206
|
-
expect(mockFn).toHaveResolvedTimes(1);
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
describe("LRU eviction", () => {
|
|
211
|
-
it("should evict least recently used items when max capacity is reached", async () => {
|
|
212
|
-
// Create storage with small capacity
|
|
213
|
-
const smallStorage = new LRUStorage({ max: 3 });
|
|
214
|
-
const smallContainer = new CacheContainer(smallStorage);
|
|
215
|
-
const smallWithCache = withCacheFactory(smallContainer);
|
|
216
|
-
|
|
217
|
-
const mockFn = vi.fn(async (x: number) => x * 2);
|
|
218
|
-
const cachedFn = smallWithCache(mockFn);
|
|
219
|
-
|
|
220
|
-
// Fill cache to capacity
|
|
221
|
-
await cachedFn(1); // key1
|
|
222
|
-
await cachedFn(2); // key2
|
|
223
|
-
await cachedFn(3); // key3
|
|
224
|
-
|
|
225
|
-
expect(mockFn).toHaveResolvedTimes(3);
|
|
226
|
-
|
|
227
|
-
// Access all three to verify they're cached
|
|
228
|
-
await cachedFn(1);
|
|
229
|
-
await cachedFn(2);
|
|
230
|
-
await cachedFn(3);
|
|
231
|
-
expect(mockFn).toHaveResolvedTimes(3);
|
|
232
|
-
|
|
233
|
-
// Add a new item, which should evict the least recently used
|
|
234
|
-
await cachedFn(4); // This should evict key1 (least recently used)
|
|
235
|
-
|
|
236
|
-
expect(mockFn).toHaveResolvedTimes(4);
|
|
237
|
-
|
|
238
|
-
// key1 should be evicted and function should be called again
|
|
239
|
-
await cachedFn(1);
|
|
240
|
-
expect(mockFn).toHaveResolvedTimes(5);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it("should keep recently accessed items in cache", async () => {
|
|
244
|
-
const smallStorage = new LRUStorage({ max: 2 });
|
|
245
|
-
const smallContainer = new CacheContainer(smallStorage);
|
|
246
|
-
const smallWithCache = withCacheFactory(smallContainer);
|
|
247
|
-
|
|
248
|
-
const mockFn = vi.fn(async (x: number) => x * 2);
|
|
249
|
-
const cachedFn = smallWithCache(mockFn);
|
|
250
|
-
|
|
251
|
-
await cachedFn(1);
|
|
252
|
-
await cachedFn(2);
|
|
253
|
-
expect(mockFn).toHaveResolvedTimes(2);
|
|
254
|
-
|
|
255
|
-
// Access 1 again to make it recently used
|
|
256
|
-
await cachedFn(1);
|
|
257
|
-
expect(mockFn).toHaveResolvedTimes(2);
|
|
258
|
-
|
|
259
|
-
// Add 3, should evict 2 (not 1)
|
|
260
|
-
await cachedFn(3);
|
|
261
|
-
expect(mockFn).toHaveResolvedTimes(3);
|
|
262
|
-
|
|
263
|
-
// 1 should still be cached
|
|
264
|
-
await cachedFn(1);
|
|
265
|
-
expect(mockFn).toHaveResolvedTimes(3);
|
|
266
|
-
|
|
267
|
-
// 2 should have been evicted
|
|
268
|
-
await cachedFn(2);
|
|
269
|
-
expect(mockFn).toHaveResolvedTimes(4);
|
|
270
|
-
});
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
describe("Clear and removal", () => {
|
|
274
|
-
it("should clear all cache entries", async () => {
|
|
275
|
-
const mockFn = vi.fn(async (x: number) => x * 2);
|
|
276
|
-
const cachedFn = withCache(mockFn);
|
|
277
|
-
|
|
278
|
-
await cachedFn(1);
|
|
279
|
-
await cachedFn(2);
|
|
280
|
-
expect(mockFn).toHaveResolvedTimes(2);
|
|
281
|
-
|
|
282
|
-
// Clear cache
|
|
283
|
-
await container.clear();
|
|
284
|
-
|
|
285
|
-
// Should call function again
|
|
286
|
-
await cachedFn(1);
|
|
287
|
-
await cachedFn(2);
|
|
288
|
-
expect(mockFn).toHaveResolvedTimes(4);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it("should remove individual cache entries", async () => {
|
|
292
|
-
const mockFn = vi.fn(async (x: number) => x * 2);
|
|
293
|
-
// Create a named function so we know the function name
|
|
294
|
-
const namedAsyncFn = Object.defineProperty(mockFn, "name", {
|
|
295
|
-
value: "testFn",
|
|
296
|
-
}) as unknown as (x: number) => Promise<number>;
|
|
297
|
-
|
|
298
|
-
const cachedFn = withCache(namedAsyncFn, {
|
|
299
|
-
calculateKey: ([x]) => `custom-key-${x}`,
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
await cachedFn(5);
|
|
303
|
-
expect(mockFn).toHaveResolvedTimes(1);
|
|
304
|
-
|
|
305
|
-
// Manually remove the key - must include the function name and prefix
|
|
306
|
-
// The key format is: ${operation.name}:${prefix}:${calculateKeyResult}
|
|
307
|
-
const fullKey = "testFn:default:custom-key-5";
|
|
308
|
-
await container.unsetKey(fullKey);
|
|
309
|
-
|
|
310
|
-
// Should call function again
|
|
311
|
-
await cachedFn(5);
|
|
312
|
-
expect(mockFn).toHaveResolvedTimes(2);
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
describe("Complex scenarios", () => {
|
|
317
|
-
it("should handle multiple concurrent calls", async () => {
|
|
318
|
-
const mockFn = vi.fn(async (x: number) => {
|
|
319
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
320
|
-
return x * 2;
|
|
321
|
-
});
|
|
322
|
-
const cachedFn = withCache(mockFn);
|
|
323
|
-
|
|
324
|
-
const results = await Promise.all([
|
|
325
|
-
cachedFn(5),
|
|
326
|
-
cachedFn(5),
|
|
327
|
-
cachedFn(5),
|
|
328
|
-
]);
|
|
329
|
-
|
|
330
|
-
expect(results).toEqual([10, 10, 10]);
|
|
331
|
-
expect(mockFn).toHaveResolvedTimes(3);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it("should handle different data types", async () => {
|
|
335
|
-
const mockFn = vi.fn(async (data: Record<string, string>) => ({
|
|
336
|
-
received: data,
|
|
337
|
-
timestamp: Date.now(),
|
|
338
|
-
}));
|
|
339
|
-
const cachedFn = withCache(mockFn);
|
|
340
|
-
|
|
341
|
-
const result1 = await cachedFn({ name: "test" });
|
|
342
|
-
const result2 = await cachedFn({ name: "test" });
|
|
343
|
-
|
|
344
|
-
// Verify both results are the same object (cached)
|
|
345
|
-
expect(result1).toBe(result2);
|
|
346
|
-
expect(mockFn).toHaveResolvedTimes(1);
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it("should combine multiple cache options", async () => {
|
|
350
|
-
const mockFn = vi.fn(async (x: number) => x * 2);
|
|
351
|
-
const cachedFn = withCache(mockFn, {
|
|
352
|
-
prefix: "combined",
|
|
353
|
-
ttl: 100,
|
|
354
|
-
strategy: "lazy",
|
|
355
|
-
shouldStore: (result: unknown) => (result as number) > 5,
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
const result1 = await cachedFn(5);
|
|
359
|
-
expect(result1).toBe(10);
|
|
360
|
-
expect(mockFn).toHaveResolvedTimes(1);
|
|
361
|
-
|
|
362
|
-
const result2 = await cachedFn(5);
|
|
363
|
-
expect(result2).toBe(10);
|
|
364
|
-
expect(mockFn).toHaveResolvedTimes(1);
|
|
365
|
-
|
|
366
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
367
|
-
|
|
368
|
-
const result3 = await cachedFn(5);
|
|
369
|
-
expect(result3).toBe(10);
|
|
370
|
-
expect(mockFn).toHaveResolvedTimes(1);
|
|
371
|
-
});
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
describe("Storage operations", () => {
|
|
375
|
-
it("should correctly store and retrieve items", async () => {
|
|
376
|
-
const key = "test-key";
|
|
377
|
-
const content = { value: "test", number: 123 };
|
|
378
|
-
|
|
379
|
-
await storage.setItem(key, {
|
|
380
|
-
content,
|
|
381
|
-
meta: {
|
|
382
|
-
createdAt: Date.now(),
|
|
383
|
-
ttl: null,
|
|
384
|
-
isLazy: true,
|
|
385
|
-
},
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
const retrieved = await storage.getItem(key);
|
|
389
|
-
expect(retrieved?.content).toEqual(content);
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
it("should remove items from storage", async () => {
|
|
393
|
-
const key = "test-key";
|
|
394
|
-
const content = { value: "test" };
|
|
395
|
-
|
|
396
|
-
await storage.setItem(key, {
|
|
397
|
-
content,
|
|
398
|
-
meta: {
|
|
399
|
-
createdAt: Date.now(),
|
|
400
|
-
ttl: null,
|
|
401
|
-
isLazy: true,
|
|
402
|
-
},
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
await storage.removeItem(key);
|
|
406
|
-
|
|
407
|
-
const retrieved = await storage.getItem(key);
|
|
408
|
-
expect(retrieved).toBeUndefined();
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
it("should clear all storage items", async () => {
|
|
412
|
-
const key1 = "key1";
|
|
413
|
-
const key2 = "key2";
|
|
414
|
-
|
|
415
|
-
await storage.setItem(key1, {
|
|
416
|
-
content: "value1",
|
|
417
|
-
meta: {
|
|
418
|
-
createdAt: Date.now(),
|
|
419
|
-
ttl: null,
|
|
420
|
-
isLazy: true,
|
|
421
|
-
},
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
await storage.setItem(key2, {
|
|
425
|
-
content: "value2",
|
|
426
|
-
meta: {
|
|
427
|
-
createdAt: Date.now(),
|
|
428
|
-
ttl: null,
|
|
429
|
-
isLazy: true,
|
|
430
|
-
},
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
await storage.clear();
|
|
434
|
-
|
|
435
|
-
const result1 = await storage.getItem(key1);
|
|
436
|
-
const result2 = await storage.getItem(key2);
|
|
437
|
-
|
|
438
|
-
expect(result1).toBeUndefined();
|
|
439
|
-
expect(result2).toBeUndefined();
|
|
440
|
-
});
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
describe("Error handling", () => {
|
|
444
|
-
it("should not cache errors", async () => {
|
|
445
|
-
const mockFn = vi.fn(async (x: number) => {
|
|
446
|
-
if (x < 0) throw new Error("Negative number");
|
|
447
|
-
return x * 2;
|
|
448
|
-
});
|
|
449
|
-
const cachedFn = withCache(mockFn);
|
|
450
|
-
|
|
451
|
-
const result = await cachedFn(5);
|
|
452
|
-
expect(result).toBe(10);
|
|
453
|
-
|
|
454
|
-
await expect(cachedFn(-5)).rejects.toThrow("Negative number");
|
|
455
|
-
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
456
|
-
});
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
describe("Concurrency limiting in revalidation queue", () => {
|
|
460
|
-
it("should limit concurrent revalidations to revalidationConcurrency in swr mode", async () => {
|
|
461
|
-
const mockFn = vi.fn(async (x: number) => {
|
|
462
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
463
|
-
return x * 2;
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
const cachedFn = withCache(mockFn, {
|
|
467
|
-
ttl: 100,
|
|
468
|
-
strategy: "swr",
|
|
469
|
-
revalidationConcurrency: 1,
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
// Prime the cache
|
|
473
|
-
await cachedFn(5);
|
|
474
|
-
expect(mockFn).toHaveResolvedTimes(1);
|
|
475
|
-
|
|
476
|
-
// Wait for expiration so cache is stale
|
|
477
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
478
|
-
|
|
479
|
-
// Track concurrent execution count
|
|
480
|
-
let maxConcurrent = 0;
|
|
481
|
-
let currentConcurrent = 0;
|
|
482
|
-
const originalImpl = mockFn.getMockImplementation();
|
|
483
|
-
|
|
484
|
-
mockFn.mockImplementation(async (x: number) => {
|
|
485
|
-
currentConcurrent++;
|
|
486
|
-
maxConcurrent = Math.max(maxConcurrent, currentConcurrent);
|
|
487
|
-
// biome-ignore lint/style/noNonNullAssertion: we're sure it is here
|
|
488
|
-
const result = await originalImpl!(x);
|
|
489
|
-
currentConcurrent--;
|
|
490
|
-
return result;
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
// Trigger multiple revalidations
|
|
494
|
-
const results = await Promise.all([
|
|
495
|
-
cachedFn(5),
|
|
496
|
-
cachedFn(5),
|
|
497
|
-
cachedFn(5),
|
|
498
|
-
]);
|
|
499
|
-
|
|
500
|
-
// wait until all would have been called
|
|
501
|
-
await new Promise((resolve) => setTimeout(resolve, 450));
|
|
502
|
-
|
|
503
|
-
expect(results).toEqual([10, 10, 10]);
|
|
504
|
-
// With concurrency: 1, should never have more than 1 concurrent revalidation
|
|
505
|
-
expect(maxConcurrent).toBe(1);
|
|
506
|
-
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
it("should only queue revalidations in swr strategy when cache is expired", async () => {
|
|
510
|
-
const mockFn = vi.fn(async (x: number) => {
|
|
511
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
512
|
-
return x * 2;
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
const cachedFn = withCache(mockFn, {
|
|
516
|
-
ttl: 100,
|
|
517
|
-
strategy: "swr",
|
|
518
|
-
revalidationConcurrency: 1,
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
// Prime the cache
|
|
522
|
-
await cachedFn(5);
|
|
523
|
-
expect(mockFn).toHaveResolvedTimes(1);
|
|
524
|
-
|
|
525
|
-
// Track concurrent execution count
|
|
526
|
-
let maxConcurrent = 0;
|
|
527
|
-
let currentConcurrent = 0;
|
|
528
|
-
const originalImpl = mockFn.getMockImplementation();
|
|
529
|
-
|
|
530
|
-
mockFn.mockImplementation(async (x: number) => {
|
|
531
|
-
currentConcurrent++;
|
|
532
|
-
maxConcurrent = Math.max(maxConcurrent, currentConcurrent);
|
|
533
|
-
// biome-ignore lint/style/noNonNullAssertion: we're sure it is here
|
|
534
|
-
const result = await originalImpl!(x);
|
|
535
|
-
currentConcurrent--;
|
|
536
|
-
return result;
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
// Call while cache is still valid - should NOT queue revalidation
|
|
540
|
-
const results = await Promise.all([
|
|
541
|
-
cachedFn(5),
|
|
542
|
-
cachedFn(5),
|
|
543
|
-
cachedFn(5),
|
|
544
|
-
]);
|
|
545
|
-
|
|
546
|
-
expect(results).toEqual([10, 10, 10]);
|
|
547
|
-
// Since cache is not expired, no revalidation should be queued
|
|
548
|
-
// Function should only be called once (initial call)
|
|
549
|
-
expect(mockFn).toHaveResolvedTimes(1);
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
it("should use concurrency limit per unique cache key", async () => {
|
|
553
|
-
const mockFn1 = vi.fn(async (x: number) => {
|
|
554
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
555
|
-
return x * 2;
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
const mockFn2 = vi.fn(async (x: number) => {
|
|
559
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
560
|
-
return x * 3;
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
const cachedFn1 = withCache(mockFn1, {
|
|
564
|
-
ttl: 100,
|
|
565
|
-
strategy: "swr",
|
|
566
|
-
revalidationConcurrency: 1,
|
|
567
|
-
prefix: "fn1",
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
const cachedFn2 = withCache(mockFn2, {
|
|
571
|
-
ttl: 100,
|
|
572
|
-
strategy: "swr",
|
|
573
|
-
revalidationConcurrency: 1,
|
|
574
|
-
prefix: "fn2",
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
// Prime both caches
|
|
578
|
-
await cachedFn1(5);
|
|
579
|
-
await cachedFn2(5);
|
|
580
|
-
expect(mockFn1).toHaveResolvedTimes(1);
|
|
581
|
-
expect(mockFn2).toHaveResolvedTimes(1);
|
|
582
|
-
|
|
583
|
-
// Wait for expiration
|
|
584
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
585
|
-
|
|
586
|
-
// Track concurrent execution for each
|
|
587
|
-
let maxConcurrent1 = 0;
|
|
588
|
-
let currentConcurrent1 = 0;
|
|
589
|
-
const originalImpl1 = mockFn1.getMockImplementation();
|
|
590
|
-
|
|
591
|
-
mockFn1.mockImplementation(async (x: number) => {
|
|
592
|
-
currentConcurrent1++;
|
|
593
|
-
maxConcurrent1 = Math.max(maxConcurrent1, currentConcurrent1);
|
|
594
|
-
// biome-ignore lint/style/noNonNullAssertion: we're sure it is here
|
|
595
|
-
const result = await originalImpl1!(x);
|
|
596
|
-
currentConcurrent1--;
|
|
597
|
-
return result;
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
// Trigger revalidations for both functions concurrently
|
|
601
|
-
const results = await Promise.all([
|
|
602
|
-
cachedFn1(5),
|
|
603
|
-
cachedFn1(5),
|
|
604
|
-
cachedFn2(5),
|
|
605
|
-
cachedFn2(5),
|
|
606
|
-
]);
|
|
607
|
-
|
|
608
|
-
expect(results).toEqual([10, 10, 15, 15]);
|
|
609
|
-
// Each function's queue should limit its own concurrency
|
|
610
|
-
expect(maxConcurrent1).toBe(1);
|
|
611
|
-
});
|
|
612
|
-
});
|
|
613
|
-
});
|
package/src/lruStorage.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { LRUCache } from "lru-cache";
|
|
2
|
-
import type { CachedItem } from "./cacheContainer.ts";
|
|
3
|
-
import type { Storage } from "./storage.ts";
|
|
4
|
-
|
|
5
|
-
export class LRUStorage implements Storage {
|
|
6
|
-
private cache: LRUCache<string, CachedItem, unknown>;
|
|
7
|
-
|
|
8
|
-
constructor({
|
|
9
|
-
max = 10_000,
|
|
10
|
-
}: Partial<LRUCache<string, CachedItem, unknown>> = {}) {
|
|
11
|
-
this.cache = new LRUCache<string, CachedItem, unknown>({
|
|
12
|
-
max,
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async clear(): Promise<void> {
|
|
17
|
-
this.cache.clear();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async getItem(key: string) {
|
|
21
|
-
const item = this.cache.get(key);
|
|
22
|
-
return item;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async setItem(key: string, content: CachedItem) {
|
|
26
|
-
this.cache.set(key, content);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async removeItem(key: string) {
|
|
30
|
-
this.cache.delete(key);
|
|
31
|
-
}
|
|
32
|
-
}
|
package/src/storage.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { CachedItem } from "./cacheContainer.ts";
|
|
2
|
-
|
|
3
|
-
export interface Storage {
|
|
4
|
-
/**
|
|
5
|
-
* returns a cached item from the storage layer
|
|
6
|
-
* @param key - key to look up
|
|
7
|
-
*/
|
|
8
|
-
getItem(key: string): Promise<CachedItem | undefined>;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* sets a cached item on the storage layer
|
|
12
|
-
* @param key - key to store
|
|
13
|
-
* @param content - content to store, including some meta data
|
|
14
|
-
*/
|
|
15
|
-
setItem(key: string, content: CachedItem): Promise<void>;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* removes item from the storage layer
|
|
19
|
-
* @param key - key to remove
|
|
20
|
-
*/
|
|
21
|
-
removeItem(key: string): Promise<void>;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* remove all keys from the storage layer
|
|
25
|
-
*/
|
|
26
|
-
clear(): Promise<void>;
|
|
27
|
-
}
|