@aitytech/agentkits-memory 1.0.0 → 2.0.0
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/LICENSE +21 -0
- package/README.md +267 -149
- package/assets/agentkits-memory-add-memory.png +0 -0
- package/assets/agentkits-memory-memory-detail.png +0 -0
- package/assets/agentkits-memory-memory-list.png +0 -0
- package/assets/logo.svg +24 -0
- package/dist/better-sqlite3-backend.d.ts +192 -0
- package/dist/better-sqlite3-backend.d.ts.map +1 -0
- package/dist/better-sqlite3-backend.js +801 -0
- package/dist/better-sqlite3-backend.js.map +1 -0
- package/dist/cli/save.js +0 -0
- package/dist/cli/setup.d.ts +6 -2
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +289 -42
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/viewer.js +25 -56
- package/dist/cli/viewer.js.map +1 -1
- package/dist/cli/web-viewer.d.ts +14 -0
- package/dist/cli/web-viewer.d.ts.map +1 -0
- package/dist/cli/web-viewer.js +1769 -0
- package/dist/cli/web-viewer.js.map +1 -0
- package/dist/embeddings/embedding-cache.d.ts +131 -0
- package/dist/embeddings/embedding-cache.d.ts.map +1 -0
- package/dist/embeddings/embedding-cache.js +217 -0
- package/dist/embeddings/embedding-cache.js.map +1 -0
- package/dist/embeddings/index.d.ts +11 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +11 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/embeddings/local-embeddings.d.ts +140 -0
- package/dist/embeddings/local-embeddings.d.ts.map +1 -0
- package/dist/embeddings/local-embeddings.js +293 -0
- package/dist/embeddings/local-embeddings.js.map +1 -0
- package/dist/hooks/context.d.ts +6 -1
- package/dist/hooks/context.d.ts.map +1 -1
- package/dist/hooks/context.js +12 -2
- package/dist/hooks/context.js.map +1 -1
- package/dist/hooks/observation.d.ts +6 -1
- package/dist/hooks/observation.d.ts.map +1 -1
- package/dist/hooks/observation.js +12 -2
- package/dist/hooks/observation.js.map +1 -1
- package/dist/hooks/service.d.ts +1 -6
- package/dist/hooks/service.d.ts.map +1 -1
- package/dist/hooks/service.js +33 -85
- package/dist/hooks/service.js.map +1 -1
- package/dist/hooks/session-init.d.ts +6 -1
- package/dist/hooks/session-init.d.ts.map +1 -1
- package/dist/hooks/session-init.js +12 -2
- package/dist/hooks/session-init.js.map +1 -1
- package/dist/hooks/summarize.d.ts +6 -1
- package/dist/hooks/summarize.d.ts.map +1 -1
- package/dist/hooks/summarize.js +12 -2
- package/dist/hooks/summarize.js.map +1 -1
- package/dist/index.d.ts +10 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +172 -94
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +17 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/migration.js +3 -3
- package/dist/migration.js.map +1 -1
- package/dist/search/hybrid-search.d.ts +262 -0
- package/dist/search/hybrid-search.d.ts.map +1 -0
- package/dist/search/hybrid-search.js +688 -0
- package/dist/search/hybrid-search.js.map +1 -0
- package/dist/search/index.d.ts +13 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +13 -0
- package/dist/search/index.js.map +1 -0
- package/dist/search/token-economics.d.ts +161 -0
- package/dist/search/token-economics.d.ts.map +1 -0
- package/dist/search/token-economics.js +239 -0
- package/dist/search/token-economics.js.map +1 -0
- package/dist/types.d.ts +0 -68
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +23 -8
- package/src/__tests__/better-sqlite3-backend.test.ts +1466 -0
- package/src/__tests__/cache-manager.test.ts +499 -0
- package/src/__tests__/embedding-integration.test.ts +481 -0
- package/src/__tests__/hnsw-index.test.ts +727 -0
- package/src/__tests__/index.test.ts +432 -0
- package/src/better-sqlite3-backend.ts +1000 -0
- package/src/cli/setup.ts +358 -47
- package/src/cli/viewer.ts +28 -63
- package/src/cli/web-viewer.ts +1956 -0
- package/src/embeddings/__tests__/embedding-cache.test.ts +269 -0
- package/src/embeddings/__tests__/local-embeddings.test.ts +495 -0
- package/src/embeddings/embedding-cache.ts +318 -0
- package/src/embeddings/index.ts +20 -0
- package/src/embeddings/local-embeddings.ts +419 -0
- package/src/hooks/__tests__/handlers.test.ts +58 -17
- package/src/hooks/__tests__/integration.test.ts +77 -26
- package/src/hooks/context.ts +13 -2
- package/src/hooks/observation.ts +13 -2
- package/src/hooks/service.ts +39 -100
- package/src/hooks/session-init.ts +13 -2
- package/src/hooks/summarize.ts +13 -2
- package/src/index.ts +210 -116
- package/src/mcp/server.ts +20 -3
- package/src/search/__tests__/hybrid-search.test.ts +669 -0
- package/src/search/__tests__/token-economics.test.ts +276 -0
- package/src/search/hybrid-search.ts +968 -0
- package/src/search/index.ts +29 -0
- package/src/search/token-economics.ts +367 -0
- package/src/types.ts +0 -96
- package/src/__tests__/sqljs-backend.test.ts +0 -410
- package/src/migration.ts +0 -574
- package/src/sql.js.d.ts +0 -70
- package/src/sqljs-backend.ts +0 -789
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CacheManager
|
|
3
|
+
*
|
|
4
|
+
* Tests LRU cache functionality, TTL expiration, memory pressure handling,
|
|
5
|
+
* and cache statistics.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
9
|
+
import { CacheManager, TieredCacheManager } from '../cache-manager.js';
|
|
10
|
+
|
|
11
|
+
describe('CacheManager', () => {
|
|
12
|
+
let cache: CacheManager<string>;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
cache = new CacheManager<string>({
|
|
16
|
+
maxSize: 100,
|
|
17
|
+
ttl: 60000, // 1 minute
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
cache.shutdown();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('basic operations', () => {
|
|
26
|
+
it('should set and get values', () => {
|
|
27
|
+
cache.set('key1', 'value1');
|
|
28
|
+
expect(cache.get('key1')).toBe('value1');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should return null for missing keys', () => {
|
|
32
|
+
expect(cache.get('nonexistent')).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should delete values', () => {
|
|
36
|
+
cache.set('key1', 'value1');
|
|
37
|
+
expect(cache.delete('key1')).toBe(true);
|
|
38
|
+
expect(cache.get('key1')).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should return false when deleting nonexistent key', () => {
|
|
42
|
+
expect(cache.delete('nonexistent')).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should check if key exists', () => {
|
|
46
|
+
cache.set('key1', 'value1');
|
|
47
|
+
expect(cache.has('key1')).toBe(true);
|
|
48
|
+
expect(cache.has('nonexistent')).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should clear all entries', () => {
|
|
52
|
+
cache.set('key1', 'value1');
|
|
53
|
+
cache.set('key2', 'value2');
|
|
54
|
+
cache.clear();
|
|
55
|
+
expect(cache.size).toBe(0);
|
|
56
|
+
expect(cache.get('key1')).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should return all keys', () => {
|
|
60
|
+
cache.set('key1', 'value1');
|
|
61
|
+
cache.set('key2', 'value2');
|
|
62
|
+
const keys = cache.keys();
|
|
63
|
+
expect(keys).toContain('key1');
|
|
64
|
+
expect(keys).toContain('key2');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should update existing entries', () => {
|
|
68
|
+
cache.set('key1', 'value1');
|
|
69
|
+
cache.set('key1', 'value2');
|
|
70
|
+
expect(cache.get('key1')).toBe('value2');
|
|
71
|
+
expect(cache.size).toBe(1);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('LRU eviction', () => {
|
|
76
|
+
it('should evict least recently used when at capacity', () => {
|
|
77
|
+
const smallCache = new CacheManager<string>({ maxSize: 3, ttl: 60000 });
|
|
78
|
+
|
|
79
|
+
smallCache.set('key1', 'value1');
|
|
80
|
+
smallCache.set('key2', 'value2');
|
|
81
|
+
smallCache.set('key3', 'value3');
|
|
82
|
+
|
|
83
|
+
// Access key1 to make it recently used
|
|
84
|
+
smallCache.get('key1');
|
|
85
|
+
|
|
86
|
+
// Add new entry, should evict key2 (LRU)
|
|
87
|
+
smallCache.set('key4', 'value4');
|
|
88
|
+
|
|
89
|
+
expect(smallCache.get('key1')).toBe('value1');
|
|
90
|
+
expect(smallCache.get('key2')).toBeNull(); // Evicted
|
|
91
|
+
expect(smallCache.get('key3')).toBe('value3');
|
|
92
|
+
expect(smallCache.get('key4')).toBe('value4');
|
|
93
|
+
|
|
94
|
+
smallCache.shutdown();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should move accessed items to front of LRU', () => {
|
|
98
|
+
const smallCache = new CacheManager<string>({ maxSize: 3, ttl: 60000 });
|
|
99
|
+
|
|
100
|
+
smallCache.set('key1', 'value1');
|
|
101
|
+
smallCache.set('key2', 'value2');
|
|
102
|
+
smallCache.set('key3', 'value3');
|
|
103
|
+
|
|
104
|
+
// Access key1 multiple times
|
|
105
|
+
smallCache.get('key1');
|
|
106
|
+
smallCache.get('key1');
|
|
107
|
+
|
|
108
|
+
// Add two new entries
|
|
109
|
+
smallCache.set('key4', 'value4');
|
|
110
|
+
smallCache.set('key5', 'value5');
|
|
111
|
+
|
|
112
|
+
// key1 should still exist, key2 and key3 should be evicted
|
|
113
|
+
expect(smallCache.get('key1')).toBe('value1');
|
|
114
|
+
expect(smallCache.has('key2')).toBe(false);
|
|
115
|
+
expect(smallCache.has('key3')).toBe(false);
|
|
116
|
+
|
|
117
|
+
smallCache.shutdown();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('TTL expiration', () => {
|
|
122
|
+
it('should expire entries after TTL', async () => {
|
|
123
|
+
const shortTtlCache = new CacheManager<string>({
|
|
124
|
+
maxSize: 100,
|
|
125
|
+
ttl: 50, // 50ms TTL
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
shortTtlCache.set('key1', 'value1');
|
|
129
|
+
expect(shortTtlCache.get('key1')).toBe('value1');
|
|
130
|
+
|
|
131
|
+
// Wait for expiration
|
|
132
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
133
|
+
|
|
134
|
+
expect(shortTtlCache.get('key1')).toBeNull();
|
|
135
|
+
|
|
136
|
+
shortTtlCache.shutdown();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should respect custom TTL per entry', async () => {
|
|
140
|
+
cache.set('short', 'value1', 50); // 50ms TTL
|
|
141
|
+
cache.set('long', 'value2', 5000); // 5s TTL
|
|
142
|
+
|
|
143
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
144
|
+
|
|
145
|
+
expect(cache.get('short')).toBeNull(); // Expired
|
|
146
|
+
expect(cache.get('long')).toBe('value2'); // Still valid
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should not return expired entries via has()', async () => {
|
|
150
|
+
const shortTtlCache = new CacheManager<string>({
|
|
151
|
+
maxSize: 100,
|
|
152
|
+
ttl: 50,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
shortTtlCache.set('key1', 'value1');
|
|
156
|
+
expect(shortTtlCache.has('key1')).toBe(true);
|
|
157
|
+
|
|
158
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
159
|
+
|
|
160
|
+
expect(shortTtlCache.has('key1')).toBe(false);
|
|
161
|
+
|
|
162
|
+
shortTtlCache.shutdown();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('memory pressure', () => {
|
|
167
|
+
it('should evict when memory limit exceeded', () => {
|
|
168
|
+
const memoryCache = new CacheManager<string>({
|
|
169
|
+
maxSize: 1000,
|
|
170
|
+
maxMemory: 100, // Very small memory limit
|
|
171
|
+
ttl: 60000,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Add entries until memory is exceeded
|
|
175
|
+
for (let i = 0; i < 10; i++) {
|
|
176
|
+
memoryCache.set(`key${i}`, 'a'.repeat(20)); // Each ~40 bytes
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Should have evicted some entries
|
|
180
|
+
expect(memoryCache.size).toBeLessThan(10);
|
|
181
|
+
|
|
182
|
+
memoryCache.shutdown();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('statistics', () => {
|
|
187
|
+
it('should track hits and misses', () => {
|
|
188
|
+
cache.set('key1', 'value1');
|
|
189
|
+
|
|
190
|
+
cache.get('key1'); // Hit
|
|
191
|
+
cache.get('key1'); // Hit
|
|
192
|
+
cache.get('nonexistent'); // Miss
|
|
193
|
+
|
|
194
|
+
const stats = cache.getStats();
|
|
195
|
+
expect(stats.hits).toBe(2);
|
|
196
|
+
expect(stats.misses).toBe(1);
|
|
197
|
+
expect(stats.hitRate).toBeCloseTo(2 / 3);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should track evictions', () => {
|
|
201
|
+
const smallCache = new CacheManager<string>({ maxSize: 2, ttl: 60000 });
|
|
202
|
+
|
|
203
|
+
smallCache.set('key1', 'value1');
|
|
204
|
+
smallCache.set('key2', 'value2');
|
|
205
|
+
smallCache.set('key3', 'value3'); // Evicts key1
|
|
206
|
+
|
|
207
|
+
const stats = smallCache.getStats();
|
|
208
|
+
expect(stats.evictions).toBe(1);
|
|
209
|
+
|
|
210
|
+
smallCache.shutdown();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should track size and memory usage', () => {
|
|
214
|
+
cache.set('key1', 'value1');
|
|
215
|
+
cache.set('key2', 'value2');
|
|
216
|
+
|
|
217
|
+
const stats = cache.getStats();
|
|
218
|
+
expect(stats.size).toBe(2);
|
|
219
|
+
expect(stats.memoryUsage).toBeGreaterThan(0);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('getOrSet', () => {
|
|
224
|
+
it('should return cached value if exists', async () => {
|
|
225
|
+
cache.set('key1', 'cached');
|
|
226
|
+
|
|
227
|
+
const loader = vi.fn().mockResolvedValue('loaded');
|
|
228
|
+
const result = await cache.getOrSet('key1', loader);
|
|
229
|
+
|
|
230
|
+
expect(result).toBe('cached');
|
|
231
|
+
expect(loader).not.toHaveBeenCalled();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should load and cache value if not exists', async () => {
|
|
235
|
+
const loader = vi.fn().mockResolvedValue('loaded');
|
|
236
|
+
const result = await cache.getOrSet('key1', loader);
|
|
237
|
+
|
|
238
|
+
expect(result).toBe('loaded');
|
|
239
|
+
expect(loader).toHaveBeenCalledTimes(1);
|
|
240
|
+
expect(cache.get('key1')).toBe('loaded');
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('prefetch', () => {
|
|
245
|
+
it('should load missing keys in batch', async () => {
|
|
246
|
+
cache.set('key1', 'existing');
|
|
247
|
+
|
|
248
|
+
const loader = vi.fn().mockResolvedValue(
|
|
249
|
+
new Map([
|
|
250
|
+
['key2', 'loaded2'],
|
|
251
|
+
['key3', 'loaded3'],
|
|
252
|
+
])
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
await cache.prefetch(['key1', 'key2', 'key3'], loader);
|
|
256
|
+
|
|
257
|
+
// Loader should only be called for missing keys
|
|
258
|
+
expect(loader).toHaveBeenCalledWith(['key2', 'key3']);
|
|
259
|
+
expect(cache.get('key1')).toBe('existing');
|
|
260
|
+
expect(cache.get('key2')).toBe('loaded2');
|
|
261
|
+
expect(cache.get('key3')).toBe('loaded3');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should not call loader if all keys exist', async () => {
|
|
265
|
+
cache.set('key1', 'value1');
|
|
266
|
+
cache.set('key2', 'value2');
|
|
267
|
+
|
|
268
|
+
const loader = vi.fn();
|
|
269
|
+
await cache.prefetch(['key1', 'key2'], loader);
|
|
270
|
+
|
|
271
|
+
expect(loader).not.toHaveBeenCalled();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('warmUp', () => {
|
|
276
|
+
it('should populate cache with initial data', () => {
|
|
277
|
+
cache.warmUp([
|
|
278
|
+
{ key: 'key1', data: 'value1' },
|
|
279
|
+
{ key: 'key2', data: 'value2' },
|
|
280
|
+
{ key: 'key3', data: 'value3', ttl: 1000 },
|
|
281
|
+
]);
|
|
282
|
+
|
|
283
|
+
expect(cache.get('key1')).toBe('value1');
|
|
284
|
+
expect(cache.get('key2')).toBe('value2');
|
|
285
|
+
expect(cache.get('key3')).toBe('value3');
|
|
286
|
+
expect(cache.size).toBe(3);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('invalidatePattern', () => {
|
|
291
|
+
it('should invalidate keys matching string pattern', () => {
|
|
292
|
+
cache.set('user:1', 'value1');
|
|
293
|
+
cache.set('user:2', 'value2');
|
|
294
|
+
cache.set('post:1', 'value3');
|
|
295
|
+
|
|
296
|
+
const count = cache.invalidatePattern('user:');
|
|
297
|
+
|
|
298
|
+
expect(count).toBe(2);
|
|
299
|
+
expect(cache.has('user:1')).toBe(false);
|
|
300
|
+
expect(cache.has('user:2')).toBe(false);
|
|
301
|
+
expect(cache.has('post:1')).toBe(true);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should invalidate keys matching regex pattern', () => {
|
|
305
|
+
cache.set('cache:session:abc', 'value1');
|
|
306
|
+
cache.set('cache:session:def', 'value2');
|
|
307
|
+
cache.set('cache:data:xyz', 'value3');
|
|
308
|
+
|
|
309
|
+
const count = cache.invalidatePattern(/cache:session:/);
|
|
310
|
+
|
|
311
|
+
expect(count).toBe(2);
|
|
312
|
+
expect(cache.has('cache:session:abc')).toBe(false);
|
|
313
|
+
expect(cache.has('cache:data:xyz')).toBe(true);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe('events', () => {
|
|
318
|
+
it('should emit cache:hit event', () => {
|
|
319
|
+
const handler = vi.fn();
|
|
320
|
+
cache.on('cache:hit', handler);
|
|
321
|
+
|
|
322
|
+
cache.set('key1', 'value1');
|
|
323
|
+
cache.get('key1');
|
|
324
|
+
|
|
325
|
+
expect(handler).toHaveBeenCalledWith({ key: 'key1' });
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should emit cache:miss event', () => {
|
|
329
|
+
const handler = vi.fn();
|
|
330
|
+
cache.on('cache:miss', handler);
|
|
331
|
+
|
|
332
|
+
cache.get('nonexistent');
|
|
333
|
+
|
|
334
|
+
expect(handler).toHaveBeenCalledWith({ key: 'nonexistent' });
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should emit cache:set event', () => {
|
|
338
|
+
const handler = vi.fn();
|
|
339
|
+
cache.on('cache:set', handler);
|
|
340
|
+
|
|
341
|
+
cache.set('key1', 'value1');
|
|
342
|
+
|
|
343
|
+
expect(handler).toHaveBeenCalledWith({ key: 'key1', ttl: 60000 });
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should emit cache:delete event', () => {
|
|
347
|
+
const handler = vi.fn();
|
|
348
|
+
cache.on('cache:delete', handler);
|
|
349
|
+
|
|
350
|
+
cache.set('key1', 'value1');
|
|
351
|
+
cache.delete('key1');
|
|
352
|
+
|
|
353
|
+
expect(handler).toHaveBeenCalledWith({ key: 'key1' });
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe('shutdown', () => {
|
|
358
|
+
it('should clear cache and stop cleanup timer', () => {
|
|
359
|
+
cache.set('key1', 'value1');
|
|
360
|
+
cache.shutdown();
|
|
361
|
+
|
|
362
|
+
expect(cache.size).toBe(0);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe('TieredCacheManager', () => {
|
|
368
|
+
let tieredCache: TieredCacheManager<string>;
|
|
369
|
+
let l2Store: Map<string, string>;
|
|
370
|
+
|
|
371
|
+
beforeEach(() => {
|
|
372
|
+
l2Store = new Map();
|
|
373
|
+
|
|
374
|
+
tieredCache = new TieredCacheManager<string>(
|
|
375
|
+
{ maxSize: 10, ttl: 60000 },
|
|
376
|
+
{
|
|
377
|
+
loader: async (key) => l2Store.get(key) ?? null,
|
|
378
|
+
writer: async (key, value) => {
|
|
379
|
+
l2Store.set(key, value);
|
|
380
|
+
},
|
|
381
|
+
}
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
afterEach(() => {
|
|
386
|
+
tieredCache.shutdown();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
describe('tiered caching', () => {
|
|
390
|
+
it('should store in L1 cache and write through to L2', async () => {
|
|
391
|
+
await tieredCache.set('key1', 'value1');
|
|
392
|
+
const result = await tieredCache.get('key1');
|
|
393
|
+
expect(result).toBe('value1');
|
|
394
|
+
expect(l2Store.get('key1')).toBe('value1');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('should fall back to L2 when L1 misses', async () => {
|
|
398
|
+
// Directly set in L2 (simulating data loaded from storage)
|
|
399
|
+
l2Store.set('key1', 'value1');
|
|
400
|
+
|
|
401
|
+
// Should find in L2
|
|
402
|
+
const result = await tieredCache.get('key1');
|
|
403
|
+
expect(result).toBe('value1');
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('should promote L2 hits to L1', async () => {
|
|
407
|
+
// Set in L2 only
|
|
408
|
+
l2Store.set('key1', 'value1');
|
|
409
|
+
|
|
410
|
+
// First access loads from L2
|
|
411
|
+
await tieredCache.get('key1');
|
|
412
|
+
|
|
413
|
+
// Now should be in L1 (faster subsequent access)
|
|
414
|
+
const stats = tieredCache.getStats();
|
|
415
|
+
expect(stats.size).toBe(1);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('should delete from L1', async () => {
|
|
419
|
+
await tieredCache.set('key1', 'value1');
|
|
420
|
+
expect(tieredCache.delete('key1')).toBe(true);
|
|
421
|
+
|
|
422
|
+
// Should miss in L1, but L2 still has it
|
|
423
|
+
expect(l2Store.has('key1')).toBe(true);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('should clear L1 cache', async () => {
|
|
427
|
+
await tieredCache.set('key1', 'value1');
|
|
428
|
+
await tieredCache.set('key2', 'value2');
|
|
429
|
+
|
|
430
|
+
tieredCache.clear();
|
|
431
|
+
|
|
432
|
+
const stats = tieredCache.getStats();
|
|
433
|
+
expect(stats.size).toBe(0);
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
describe('without L2', () => {
|
|
438
|
+
it('should work with L1 only', async () => {
|
|
439
|
+
const l1Only = new TieredCacheManager<string>({ maxSize: 10, ttl: 60000 });
|
|
440
|
+
|
|
441
|
+
await l1Only.set('key1', 'value1');
|
|
442
|
+
const result = await l1Only.get('key1');
|
|
443
|
+
expect(result).toBe('value1');
|
|
444
|
+
|
|
445
|
+
l1Only.shutdown();
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('should return null for missing keys without L2', async () => {
|
|
449
|
+
const l1Only = new TieredCacheManager<string>({ maxSize: 10, ttl: 60000 });
|
|
450
|
+
|
|
451
|
+
const result = await l1Only.get('nonexistent');
|
|
452
|
+
expect(result).toBeNull();
|
|
453
|
+
|
|
454
|
+
l1Only.shutdown();
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
describe('statistics', () => {
|
|
459
|
+
it('should return L1 statistics', async () => {
|
|
460
|
+
await tieredCache.set('key1', 'value1');
|
|
461
|
+
await tieredCache.get('key1');
|
|
462
|
+
|
|
463
|
+
const stats = tieredCache.getStats();
|
|
464
|
+
expect(stats.hits).toBeGreaterThanOrEqual(1);
|
|
465
|
+
expect(stats.size).toBe(1);
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
describe('events', () => {
|
|
470
|
+
it('should emit l1:hit event', async () => {
|
|
471
|
+
const handler = vi.fn();
|
|
472
|
+
tieredCache.on('l1:hit', handler);
|
|
473
|
+
|
|
474
|
+
await tieredCache.set('key1', 'value1');
|
|
475
|
+
await tieredCache.get('key1');
|
|
476
|
+
|
|
477
|
+
expect(handler).toHaveBeenCalled();
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('should emit l2:hit event', async () => {
|
|
481
|
+
const handler = vi.fn();
|
|
482
|
+
tieredCache.on('l2:hit', handler);
|
|
483
|
+
|
|
484
|
+
l2Store.set('key1', 'value1');
|
|
485
|
+
await tieredCache.get('key1');
|
|
486
|
+
|
|
487
|
+
expect(handler).toHaveBeenCalledWith({ key: 'key1' });
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('should emit l2:write event', async () => {
|
|
491
|
+
const handler = vi.fn();
|
|
492
|
+
tieredCache.on('l2:write', handler);
|
|
493
|
+
|
|
494
|
+
await tieredCache.set('key1', 'value1');
|
|
495
|
+
|
|
496
|
+
expect(handler).toHaveBeenCalledWith({ key: 'key1' });
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
});
|