@object-ui/data-objectstack 17.3.0 → 17.6.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/CHANGELOG.md +888 -0
- package/README.md +3 -11
- package/dist/index.cjs +1511 -1115
- package/dist/index.d.cts +1222 -824
- package/dist/index.d.ts +1222 -824
- package/dist/index.js +1503 -1111
- package/package.json +5 -6
- package/src/aggregate-capability.test.ts +0 -177
- package/src/bulkDelete.test.ts +0 -75
- package/src/bulkUpdate.test.ts +0 -93
- package/src/cache/MetadataCache.test.ts +0 -477
- package/src/cache/MetadataCache.ts +0 -289
- package/src/cloud.ts +0 -109
- package/src/connection.test.ts +0 -188
- package/src/contracts.ts +0 -115
- package/src/errors.test.ts +0 -426
- package/src/errors.ts +0 -293
- package/src/exportDownload.test.ts +0 -141
- package/src/filter-entry-translation.test.ts +0 -360
- package/src/filter-operator-ast-parity.test.ts +0 -71
- package/src/getObjectSchema.test.ts +0 -145
- package/src/importJob.test.ts +0 -71
- package/src/index.ts +0 -3658
- package/src/integration.ts +0 -200
- package/src/listViews.test.ts +0 -175
- package/src/metadata-client.test.ts +0 -303
- package/src/metadata-client.ts +0 -849
- package/src/occ.test.ts +0 -101
- package/src/onMutation.test.ts +0 -562
- package/src/onWriteWarning.test.ts +0 -273
- package/src/orderby-serialization.test.ts +0 -115
- package/src/queryDataset.test.ts +0 -132
- package/src/searchAll.test.ts +0 -120
- package/src/security.ts +0 -243
- package/src/spec-symbol-batch6.test.ts +0 -306
- package/src/studio.ts +0 -152
- package/src/userState.test.ts +0 -315
- package/src/userState.ts +0 -238
- package/src/v3-compat.test.ts +0 -240
- package/src/validation-error.test.ts +0 -94
|
@@ -1,477 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
10
|
-
import { MetadataCache } from './MetadataCache';
|
|
11
|
-
|
|
12
|
-
describe('MetadataCache', () => {
|
|
13
|
-
let cache: MetadataCache;
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
cache = new MetadataCache({ maxSize: 3, ttl: 1000 }); // Small size and TTL for testing
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
describe('Cache Hit/Miss Scenarios', () => {
|
|
20
|
-
it('should return cached value on cache hit', async () => {
|
|
21
|
-
const fetcher = vi.fn(async () => ({ name: 'users', fields: [] }));
|
|
22
|
-
|
|
23
|
-
// First call - cache miss
|
|
24
|
-
const result1 = await cache.get('users', fetcher);
|
|
25
|
-
expect(result1).toEqual({ name: 'users', fields: [] });
|
|
26
|
-
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
27
|
-
|
|
28
|
-
// Second call - cache hit
|
|
29
|
-
const result2 = await cache.get('users', fetcher);
|
|
30
|
-
expect(result2).toEqual({ name: 'users', fields: [] });
|
|
31
|
-
expect(fetcher).toHaveBeenCalledTimes(1); // Not called again
|
|
32
|
-
|
|
33
|
-
const stats = cache.getStats();
|
|
34
|
-
expect(stats.hits).toBe(1);
|
|
35
|
-
expect(stats.misses).toBe(1);
|
|
36
|
-
expect(stats.hitRate).toBe(0.5);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should call fetcher on cache miss', async () => {
|
|
40
|
-
const fetcher = vi.fn(async () => ({ data: 'test' }));
|
|
41
|
-
|
|
42
|
-
const result = await cache.get('test-key', fetcher);
|
|
43
|
-
|
|
44
|
-
expect(result).toEqual({ data: 'test' });
|
|
45
|
-
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
46
|
-
|
|
47
|
-
const stats = cache.getStats();
|
|
48
|
-
expect(stats.misses).toBe(1);
|
|
49
|
-
expect(stats.hits).toBe(0);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should handle multiple different keys', async () => {
|
|
53
|
-
const fetcher1 = vi.fn(async () => ({ type: 'users' }));
|
|
54
|
-
const fetcher2 = vi.fn(async () => ({ type: 'posts' }));
|
|
55
|
-
|
|
56
|
-
const result1 = await cache.get('users', fetcher1);
|
|
57
|
-
const result2 = await cache.get('posts', fetcher2);
|
|
58
|
-
|
|
59
|
-
expect(result1).toEqual({ type: 'users' });
|
|
60
|
-
expect(result2).toEqual({ type: 'posts' });
|
|
61
|
-
expect(fetcher1).toHaveBeenCalledTimes(1);
|
|
62
|
-
expect(fetcher2).toHaveBeenCalledTimes(1);
|
|
63
|
-
|
|
64
|
-
// Get again - both should be cached
|
|
65
|
-
await cache.get('users', fetcher1);
|
|
66
|
-
await cache.get('posts', fetcher2);
|
|
67
|
-
|
|
68
|
-
expect(fetcher1).toHaveBeenCalledTimes(1);
|
|
69
|
-
expect(fetcher2).toHaveBeenCalledTimes(1);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe('TTL Expiration', () => {
|
|
74
|
-
it('should expire entries after TTL', async () => {
|
|
75
|
-
const cache = new MetadataCache({ maxSize: 10, ttl: 100 }); // 100ms TTL
|
|
76
|
-
const fetcher = vi.fn(async () => ({ data: 'test' }));
|
|
77
|
-
|
|
78
|
-
// First fetch
|
|
79
|
-
await cache.get('test', fetcher);
|
|
80
|
-
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
81
|
-
|
|
82
|
-
// Immediate second fetch - should be cached
|
|
83
|
-
await cache.get('test', fetcher);
|
|
84
|
-
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
85
|
-
|
|
86
|
-
// Wait for TTL to expire
|
|
87
|
-
await new Promise(resolve => setTimeout(resolve, 150));
|
|
88
|
-
|
|
89
|
-
// Should fetch again after expiration
|
|
90
|
-
await cache.get('test', fetcher);
|
|
91
|
-
expect(fetcher).toHaveBeenCalledTimes(2);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('should update timestamp on cache hit', async () => {
|
|
95
|
-
const cache = new MetadataCache({ maxSize: 10, ttl: 200 });
|
|
96
|
-
const fetcher = vi.fn(async () => ({ data: 'test' }));
|
|
97
|
-
|
|
98
|
-
await cache.get('test', fetcher);
|
|
99
|
-
|
|
100
|
-
// Access again after 100ms
|
|
101
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
102
|
-
await cache.get('test', fetcher);
|
|
103
|
-
|
|
104
|
-
// Access again after another 110ms (total 210ms from first, ensuring TTL has passed)
|
|
105
|
-
await new Promise(resolve => setTimeout(resolve, 110));
|
|
106
|
-
|
|
107
|
-
// Should still be in cache because we're checking timestamp, not last accessed
|
|
108
|
-
// Actually, the implementation uses timestamp for expiration, not lastAccessed
|
|
109
|
-
// So after 210ms total (> 200ms TTL), it should expire
|
|
110
|
-
await cache.get('test', fetcher);
|
|
111
|
-
|
|
112
|
-
// Should have been called twice - initial + after expiration
|
|
113
|
-
expect(fetcher).toHaveBeenCalledTimes(2);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('should not return expired entries via has()', async () => {
|
|
117
|
-
const cache = new MetadataCache({ maxSize: 10, ttl: 100 });
|
|
118
|
-
const fetcher = vi.fn(async () => ({ data: 'test' }));
|
|
119
|
-
|
|
120
|
-
await cache.get('test', fetcher);
|
|
121
|
-
|
|
122
|
-
expect(cache.has('test')).toBe(true);
|
|
123
|
-
|
|
124
|
-
// Wait for expiration
|
|
125
|
-
await new Promise(resolve => setTimeout(resolve, 150));
|
|
126
|
-
|
|
127
|
-
expect(cache.has('test')).toBe(false);
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
describe('LRU Eviction', () => {
|
|
132
|
-
it('should evict least recently used entry when maxSize is reached', async () => {
|
|
133
|
-
// Cache size is 3
|
|
134
|
-
const fetcher1 = vi.fn(async () => ({ id: 1 }));
|
|
135
|
-
const fetcher2 = vi.fn(async () => ({ id: 2 }));
|
|
136
|
-
const fetcher3 = vi.fn(async () => ({ id: 3 }));
|
|
137
|
-
const fetcher4 = vi.fn(async () => ({ id: 4 }));
|
|
138
|
-
|
|
139
|
-
// Fill cache
|
|
140
|
-
await cache.get('key1', fetcher1);
|
|
141
|
-
await cache.get('key2', fetcher2);
|
|
142
|
-
await cache.get('key3', fetcher3);
|
|
143
|
-
|
|
144
|
-
expect(cache.getStats().size).toBe(3);
|
|
145
|
-
|
|
146
|
-
// Add fourth item - should evict key1 (least recently used)
|
|
147
|
-
await cache.get('key4', fetcher4);
|
|
148
|
-
|
|
149
|
-
expect(cache.getStats().size).toBe(3);
|
|
150
|
-
expect(cache.getStats().evictions).toBe(1);
|
|
151
|
-
|
|
152
|
-
// key1 should not be in cache anymore
|
|
153
|
-
await cache.get('key1', fetcher1);
|
|
154
|
-
expect(fetcher1).toHaveBeenCalledTimes(2); // Called again
|
|
155
|
-
|
|
156
|
-
// After re-adding key1, key2 should have been evicted
|
|
157
|
-
// So cache now has: key3, key4, key1
|
|
158
|
-
|
|
159
|
-
// key3, key4 should still be cached
|
|
160
|
-
await cache.get('key3', fetcher3);
|
|
161
|
-
await cache.get('key4', fetcher4);
|
|
162
|
-
expect(fetcher3).toHaveBeenCalledTimes(1);
|
|
163
|
-
expect(fetcher4).toHaveBeenCalledTimes(1);
|
|
164
|
-
|
|
165
|
-
// key2 should have been evicted when key1 was re-added
|
|
166
|
-
await cache.get('key2', fetcher2);
|
|
167
|
-
expect(fetcher2).toHaveBeenCalledTimes(2);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('should update LRU order on access', async () => {
|
|
171
|
-
const fetcher1 = vi.fn(async () => ({ id: 1 }));
|
|
172
|
-
const fetcher2 = vi.fn(async () => ({ id: 2 }));
|
|
173
|
-
const fetcher3 = vi.fn(async () => ({ id: 3 }));
|
|
174
|
-
const fetcher4 = vi.fn(async () => ({ id: 4 }));
|
|
175
|
-
|
|
176
|
-
// Fill cache: key1, key2, key3
|
|
177
|
-
await cache.get('key1', fetcher1);
|
|
178
|
-
await cache.get('key2', fetcher2);
|
|
179
|
-
await cache.get('key3', fetcher3);
|
|
180
|
-
|
|
181
|
-
// Access key1 again - should move it to the end (most recently used)
|
|
182
|
-
// Cache order: key2, key3, key1
|
|
183
|
-
await cache.get('key1', fetcher1);
|
|
184
|
-
|
|
185
|
-
// Add key4 - should evict key2 (now the LRU)
|
|
186
|
-
// Cache order: key3, key1, key4
|
|
187
|
-
await cache.get('key4', fetcher4);
|
|
188
|
-
|
|
189
|
-
// Verify key2 was evicted
|
|
190
|
-
await cache.get('key2', fetcher2);
|
|
191
|
-
expect(fetcher2).toHaveBeenCalledTimes(2);
|
|
192
|
-
|
|
193
|
-
// After re-adding key2, key3 should have been evicted
|
|
194
|
-
// Cache order: key1, key4, key2
|
|
195
|
-
|
|
196
|
-
// key1, key4 should still be cached
|
|
197
|
-
await cache.get('key1', fetcher1);
|
|
198
|
-
await cache.get('key4', fetcher4);
|
|
199
|
-
expect(fetcher1).toHaveBeenCalledTimes(1); // Only called once initially (re-access was a cache hit)
|
|
200
|
-
expect(fetcher4).toHaveBeenCalledTimes(1);
|
|
201
|
-
|
|
202
|
-
// key3 should have been evicted when key2 was re-added
|
|
203
|
-
await cache.get('key3', fetcher3);
|
|
204
|
-
expect(fetcher3).toHaveBeenCalledTimes(2);
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
describe('Concurrent Access', () => {
|
|
209
|
-
it('should handle concurrent requests for the same key', async () => {
|
|
210
|
-
let fetchCount = 0;
|
|
211
|
-
const fetcher = vi.fn(async () => {
|
|
212
|
-
fetchCount++;
|
|
213
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
214
|
-
return { data: 'test', fetchCount };
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
// Make multiple concurrent requests
|
|
218
|
-
const results = await Promise.all([
|
|
219
|
-
cache.get('test', fetcher),
|
|
220
|
-
cache.get('test', fetcher),
|
|
221
|
-
cache.get('test', fetcher),
|
|
222
|
-
]);
|
|
223
|
-
|
|
224
|
-
// All should return the same data
|
|
225
|
-
// Note: Due to async nature, the first call will fetch and others might also fetch
|
|
226
|
-
// if they check before the first one completes. This is acceptable behavior.
|
|
227
|
-
// But at least one should be cached if they complete after the first one.
|
|
228
|
-
expect(results[0]).toBeDefined();
|
|
229
|
-
expect(results[1]).toBeDefined();
|
|
230
|
-
expect(results[2]).toBeDefined();
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('should handle concurrent requests for different keys', async () => {
|
|
234
|
-
const fetcher1 = vi.fn(async () => {
|
|
235
|
-
await new Promise(resolve => setTimeout(resolve, 30));
|
|
236
|
-
return { id: 1 };
|
|
237
|
-
});
|
|
238
|
-
const fetcher2 = vi.fn(async () => {
|
|
239
|
-
await new Promise(resolve => setTimeout(resolve, 30));
|
|
240
|
-
return { id: 2 };
|
|
241
|
-
});
|
|
242
|
-
const fetcher3 = vi.fn(async () => {
|
|
243
|
-
await new Promise(resolve => setTimeout(resolve, 30));
|
|
244
|
-
return { id: 3 };
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
const results = await Promise.all([
|
|
248
|
-
cache.get('key1', fetcher1),
|
|
249
|
-
cache.get('key2', fetcher2),
|
|
250
|
-
cache.get('key3', fetcher3),
|
|
251
|
-
]);
|
|
252
|
-
|
|
253
|
-
expect(results[0]).toEqual({ id: 1 });
|
|
254
|
-
expect(results[1]).toEqual({ id: 2 });
|
|
255
|
-
expect(results[2]).toEqual({ id: 3 });
|
|
256
|
-
expect(fetcher1).toHaveBeenCalledTimes(1);
|
|
257
|
-
expect(fetcher2).toHaveBeenCalledTimes(1);
|
|
258
|
-
expect(fetcher3).toHaveBeenCalledTimes(1);
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
describe('Cache Management', () => {
|
|
263
|
-
it('should invalidate specific key', async () => {
|
|
264
|
-
const fetcher = vi.fn(async () => ({ data: 'test' }));
|
|
265
|
-
|
|
266
|
-
await cache.get('test', fetcher);
|
|
267
|
-
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
268
|
-
|
|
269
|
-
cache.invalidate('test');
|
|
270
|
-
|
|
271
|
-
await cache.get('test', fetcher);
|
|
272
|
-
expect(fetcher).toHaveBeenCalledTimes(2);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it('should invalidate all keys', async () => {
|
|
276
|
-
const fetcher1 = vi.fn(async () => ({ id: 1 }));
|
|
277
|
-
const fetcher2 = vi.fn(async () => ({ id: 2 }));
|
|
278
|
-
|
|
279
|
-
await cache.get('key1', fetcher1);
|
|
280
|
-
await cache.get('key2', fetcher2);
|
|
281
|
-
|
|
282
|
-
cache.invalidate(); // No key = invalidate all
|
|
283
|
-
|
|
284
|
-
await cache.get('key1', fetcher1);
|
|
285
|
-
await cache.get('key2', fetcher2);
|
|
286
|
-
|
|
287
|
-
expect(fetcher1).toHaveBeenCalledTimes(2);
|
|
288
|
-
expect(fetcher2).toHaveBeenCalledTimes(2);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it('should clear cache and reset stats', async () => {
|
|
292
|
-
const fetcher = vi.fn(async () => ({ data: 'test' }));
|
|
293
|
-
|
|
294
|
-
await cache.get('key1', fetcher);
|
|
295
|
-
await cache.get('key2', fetcher);
|
|
296
|
-
await cache.get('key1', fetcher); // Hit
|
|
297
|
-
|
|
298
|
-
const statsBefore = cache.getStats();
|
|
299
|
-
expect(statsBefore.size).toBe(2);
|
|
300
|
-
expect(statsBefore.hits).toBe(1);
|
|
301
|
-
expect(statsBefore.misses).toBe(2);
|
|
302
|
-
|
|
303
|
-
cache.clear();
|
|
304
|
-
|
|
305
|
-
const statsAfter = cache.getStats();
|
|
306
|
-
expect(statsAfter.size).toBe(0);
|
|
307
|
-
expect(statsAfter.hits).toBe(0);
|
|
308
|
-
expect(statsAfter.misses).toBe(0);
|
|
309
|
-
expect(statsAfter.evictions).toBe(0);
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
describe('Statistics', () => {
|
|
314
|
-
it('should track cache statistics correctly', async () => {
|
|
315
|
-
const fetcher = vi.fn(async () => ({ data: 'test' }));
|
|
316
|
-
|
|
317
|
-
// Initial stats
|
|
318
|
-
let stats = cache.getStats();
|
|
319
|
-
expect(stats.size).toBe(0);
|
|
320
|
-
expect(stats.maxSize).toBe(3);
|
|
321
|
-
expect(stats.hits).toBe(0);
|
|
322
|
-
expect(stats.misses).toBe(0);
|
|
323
|
-
expect(stats.evictions).toBe(0);
|
|
324
|
-
expect(stats.hitRate).toBe(0);
|
|
325
|
-
|
|
326
|
-
// First access - miss
|
|
327
|
-
await cache.get('key1', fetcher);
|
|
328
|
-
stats = cache.getStats();
|
|
329
|
-
expect(stats.size).toBe(1);
|
|
330
|
-
expect(stats.misses).toBe(1);
|
|
331
|
-
expect(stats.hitRate).toBe(0);
|
|
332
|
-
|
|
333
|
-
// Second access - hit
|
|
334
|
-
await cache.get('key1', fetcher);
|
|
335
|
-
stats = cache.getStats();
|
|
336
|
-
expect(stats.hits).toBe(1);
|
|
337
|
-
expect(stats.hitRate).toBe(0.5);
|
|
338
|
-
|
|
339
|
-
// Third access - hit
|
|
340
|
-
await cache.get('key1', fetcher);
|
|
341
|
-
stats = cache.getStats();
|
|
342
|
-
expect(stats.hits).toBe(2);
|
|
343
|
-
expect(stats.hitRate).toBeCloseTo(0.667, 2);
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it('should track evictions', async () => {
|
|
347
|
-
const fetcher = vi.fn(async () => ({ data: 'test' }));
|
|
348
|
-
|
|
349
|
-
// Fill cache to max
|
|
350
|
-
await cache.get('key1', fetcher);
|
|
351
|
-
await cache.get('key2', fetcher);
|
|
352
|
-
await cache.get('key3', fetcher);
|
|
353
|
-
|
|
354
|
-
let stats = cache.getStats();
|
|
355
|
-
expect(stats.evictions).toBe(0);
|
|
356
|
-
|
|
357
|
-
// Trigger eviction
|
|
358
|
-
await cache.get('key4', fetcher);
|
|
359
|
-
|
|
360
|
-
stats = cache.getStats();
|
|
361
|
-
expect(stats.evictions).toBe(1);
|
|
362
|
-
|
|
363
|
-
// Trigger more evictions
|
|
364
|
-
await cache.get('key5', fetcher);
|
|
365
|
-
await cache.get('key6', fetcher);
|
|
366
|
-
|
|
367
|
-
stats = cache.getStats();
|
|
368
|
-
expect(stats.evictions).toBe(3);
|
|
369
|
-
});
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
describe('Edge Cases', () => {
|
|
373
|
-
it('should handle fetcher that throws error', async () => {
|
|
374
|
-
const fetcher = vi.fn(async () => {
|
|
375
|
-
throw new Error('Fetch failed');
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
await expect(cache.get('test', fetcher)).rejects.toThrow('Fetch failed');
|
|
379
|
-
|
|
380
|
-
// Should not cache the error
|
|
381
|
-
const stats = cache.getStats();
|
|
382
|
-
expect(stats.size).toBe(0);
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
it('should handle null/undefined values', async () => {
|
|
386
|
-
const fetcher1 = vi.fn(async () => null);
|
|
387
|
-
const fetcher2 = vi.fn(async () => undefined);
|
|
388
|
-
|
|
389
|
-
const result1 = await cache.get('null-key', fetcher1);
|
|
390
|
-
const result2 = await cache.get('undefined-key', fetcher2);
|
|
391
|
-
|
|
392
|
-
expect(result1).toBeNull();
|
|
393
|
-
expect(result2).toBeUndefined();
|
|
394
|
-
|
|
395
|
-
// Should still cache these values
|
|
396
|
-
await cache.get('null-key', fetcher1);
|
|
397
|
-
await cache.get('undefined-key', fetcher2);
|
|
398
|
-
|
|
399
|
-
expect(fetcher1).toHaveBeenCalledTimes(1);
|
|
400
|
-
expect(fetcher2).toHaveBeenCalledTimes(1);
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
it('should handle empty string key', async () => {
|
|
404
|
-
const fetcher = vi.fn(async () => ({ data: 'test' }));
|
|
405
|
-
|
|
406
|
-
const result = await cache.get('', fetcher);
|
|
407
|
-
expect(result).toEqual({ data: 'test' });
|
|
408
|
-
|
|
409
|
-
await cache.get('', fetcher);
|
|
410
|
-
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
it('should coalesce concurrent fetches for the same key', async () => {
|
|
414
|
-
let resolveFetch: (v: { data: string }) => void = () => {};
|
|
415
|
-
const fetcher = vi.fn(
|
|
416
|
-
() =>
|
|
417
|
-
new Promise<{ data: string }>((resolve) => {
|
|
418
|
-
resolveFetch = resolve;
|
|
419
|
-
})
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
const p1 = cache.get('shared-key', fetcher);
|
|
423
|
-
const p2 = cache.get('shared-key', fetcher);
|
|
424
|
-
const p3 = cache.get('shared-key', fetcher);
|
|
425
|
-
|
|
426
|
-
// All three calls should share a single in-flight fetcher invocation
|
|
427
|
-
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
428
|
-
|
|
429
|
-
resolveFetch({ data: 'shared' });
|
|
430
|
-
const [r1, r2, r3] = await Promise.all([p1, p2, p3]);
|
|
431
|
-
|
|
432
|
-
expect(r1).toEqual({ data: 'shared' });
|
|
433
|
-
expect(r2).toBe(r1);
|
|
434
|
-
expect(r3).toBe(r1);
|
|
435
|
-
|
|
436
|
-
const stats = cache.getStats();
|
|
437
|
-
expect(stats.coalesced).toBe(2);
|
|
438
|
-
expect(stats.misses).toBe(1);
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
it('should clear inflight slot after fetch rejection so retries are possible', async () => {
|
|
442
|
-
const fetcher = vi
|
|
443
|
-
.fn()
|
|
444
|
-
.mockRejectedValueOnce(new Error('boom'))
|
|
445
|
-
.mockResolvedValueOnce({ data: 'ok' });
|
|
446
|
-
|
|
447
|
-
await expect(cache.get('retry-key', fetcher)).rejects.toThrow('boom');
|
|
448
|
-
const result = await cache.get('retry-key', fetcher);
|
|
449
|
-
|
|
450
|
-
expect(result).toEqual({ data: 'ok' });
|
|
451
|
-
expect(fetcher).toHaveBeenCalledTimes(2);
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
it('should expose prime() to seed cache from bulk endpoints', async () => {
|
|
455
|
-
cache.prime('object:account', { name: 'account', fields: [] });
|
|
456
|
-
|
|
457
|
-
const fetcher = vi.fn(async () => ({ name: 'account', fields: [{ name: 'id' }] }));
|
|
458
|
-
const result = await cache.get('object:account', fetcher);
|
|
459
|
-
|
|
460
|
-
expect(fetcher).not.toHaveBeenCalled();
|
|
461
|
-
expect(result).toEqual({ name: 'account', fields: [] });
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
it('should handle very large cache', async () => {
|
|
465
|
-
const largeCache = new MetadataCache({ maxSize: 10000, ttl: 60000 });
|
|
466
|
-
|
|
467
|
-
// Add many entries
|
|
468
|
-
for (let i = 0; i < 1000; i++) {
|
|
469
|
-
await largeCache.get(`key-${i}`, async () => ({ id: i }));
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const stats = largeCache.getStats();
|
|
473
|
-
expect(stats.size).toBe(1000);
|
|
474
|
-
expect(stats.evictions).toBe(0);
|
|
475
|
-
});
|
|
476
|
-
});
|
|
477
|
-
});
|