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