@object-ui/data-objectstack 17.5.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +219 -0
  2. package/dist/index.cjs +262 -357
  3. package/dist/index.d.cts +385 -448
  4. package/dist/index.d.ts +385 -448
  5. package/dist/index.js +258 -350
  6. package/package.json +5 -6
  7. package/dist/tsconfig.tsbuildinfo +0 -1
  8. package/src/aggregate-capability.test.ts +0 -177
  9. package/src/apiAccessDenied.test.ts +0 -233
  10. package/src/appAccessProbe.test.ts +0 -147
  11. package/src/bulkDelete.test.ts +0 -75
  12. package/src/bulkUpdate.test.ts +0 -93
  13. package/src/cache/MetadataCache.test.ts +0 -477
  14. package/src/cache/MetadataCache.ts +0 -289
  15. package/src/cloud-surface-retired-4152.pin.test.ts +0 -171
  16. package/src/connection.test.ts +0 -188
  17. package/src/contracts.ts +0 -115
  18. package/src/deleteView.homes.test.ts +0 -389
  19. package/src/deleteViewContract.types.test.ts +0 -246
  20. package/src/errors.test.ts +0 -426
  21. package/src/errors.ts +0 -293
  22. package/src/exportDownload.test.ts +0 -141
  23. package/src/filter-entry-translation.test.ts +0 -360
  24. package/src/filter-operator-ast-parity.test.ts +0 -135
  25. package/src/getObjectSchema.test.ts +0 -145
  26. package/src/httpFailureLogging.test.ts +0 -109
  27. package/src/importJob.test.ts +0 -71
  28. package/src/index.ts +0 -4442
  29. package/src/integration.ts +0 -200
  30. package/src/listViewOverrides.test.ts +0 -241
  31. package/src/listViews.test.ts +0 -175
  32. package/src/metadata-client.get-envelope.test.ts +0 -171
  33. package/src/metadata-client.saveAdvisories.test.ts +0 -226
  34. package/src/metadata-client.test.ts +0 -303
  35. package/src/metadata-client.ts +0 -1046
  36. package/src/occ.test.ts +0 -101
  37. package/src/onMutation.test.ts +0 -589
  38. package/src/onSaveAdvisory.test.ts +0 -311
  39. package/src/onWriteWarning.test.ts +0 -273
  40. package/src/orderby-serialization.test.ts +0 -115
  41. package/src/queryDataset.test.ts +0 -543
  42. package/src/searchAll.test.ts +0 -120
  43. package/src/security.ts +0 -243
  44. package/src/spec-symbol-batch6.test.ts +0 -326
  45. package/src/studio.ts +0 -152
  46. package/src/updateView.draft.test.ts +0 -242
  47. package/src/userState.test.ts +0 -315
  48. package/src/userState.ts +0 -238
  49. package/src/v3-compat.test.ts +0 -229
  50. package/src/validation-error.test.ts +0 -94
  51. package/src/viewCacheInvalidation.pin.test.ts +0 -260
  52. package/src/viewInvalidationSeam.pin.test.ts +0 -162
@@ -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
- });