@signalium/query 0.1.0 → 1.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.
Files changed (113) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cjs/EntityMap.js +2 -2
  3. package/dist/cjs/EntityMap.js.map +1 -1
  4. package/dist/cjs/NetworkManager.js +105 -0
  5. package/dist/cjs/NetworkManager.js.map +1 -0
  6. package/dist/cjs/QueryClient.js +390 -76
  7. package/dist/cjs/QueryClient.js.map +1 -1
  8. package/dist/cjs/QueryStore.js +295 -3
  9. package/dist/cjs/QueryStore.js.map +1 -1
  10. package/dist/cjs/index.js +16 -1
  11. package/dist/cjs/index.js.map +1 -1
  12. package/dist/cjs/package.json +3 -0
  13. package/dist/cjs/parseEntities.js +3 -0
  14. package/dist/cjs/parseEntities.js.map +1 -1
  15. package/dist/cjs/proxy.js +19 -0
  16. package/dist/cjs/proxy.js.map +1 -1
  17. package/dist/cjs/query.js +40 -2
  18. package/dist/cjs/query.js.map +1 -1
  19. package/dist/cjs/stores/async.js +6 -0
  20. package/dist/cjs/stores/async.js.map +1 -0
  21. package/dist/cjs/stores/sync.js +7 -0
  22. package/dist/cjs/stores/sync.js.map +1 -0
  23. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  24. package/dist/cjs/type-utils.js +3 -0
  25. package/dist/cjs/type-utils.js.map +1 -0
  26. package/dist/cjs/types.js +19 -1
  27. package/dist/cjs/types.js.map +1 -1
  28. package/dist/esm/EntityMap.js +3 -3
  29. package/dist/esm/EntityMap.js.map +1 -1
  30. package/dist/esm/NetworkManager.d.ts +48 -0
  31. package/dist/esm/NetworkManager.d.ts.map +1 -0
  32. package/dist/esm/NetworkManager.js +101 -0
  33. package/dist/esm/NetworkManager.js.map +1 -0
  34. package/dist/esm/QueryClient.d.ts +81 -25
  35. package/dist/esm/QueryClient.d.ts.map +1 -1
  36. package/dist/esm/QueryClient.js +390 -76
  37. package/dist/esm/QueryClient.js.map +1 -1
  38. package/dist/esm/QueryStore.d.ts +64 -2
  39. package/dist/esm/QueryStore.d.ts.map +1 -1
  40. package/dist/esm/QueryStore.js +293 -2
  41. package/dist/esm/QueryStore.js.map +1 -1
  42. package/dist/esm/index.d.ts +5 -3
  43. package/dist/esm/index.d.ts.map +1 -1
  44. package/dist/esm/index.js +3 -1
  45. package/dist/esm/index.js.map +1 -1
  46. package/dist/esm/parseEntities.d.ts.map +1 -1
  47. package/dist/esm/parseEntities.js +3 -0
  48. package/dist/esm/parseEntities.js.map +1 -1
  49. package/dist/esm/proxy.d.ts +6 -0
  50. package/dist/esm/proxy.d.ts.map +1 -1
  51. package/dist/esm/proxy.js +18 -0
  52. package/dist/esm/proxy.js.map +1 -1
  53. package/dist/esm/query.d.ts +30 -29
  54. package/dist/esm/query.d.ts.map +1 -1
  55. package/dist/esm/query.js +39 -3
  56. package/dist/esm/query.js.map +1 -1
  57. package/dist/esm/stores/async.d.ts +2 -0
  58. package/dist/esm/stores/async.d.ts.map +1 -0
  59. package/dist/esm/stores/async.js +2 -0
  60. package/dist/esm/stores/async.js.map +1 -0
  61. package/dist/esm/stores/sync.d.ts +2 -0
  62. package/dist/esm/stores/sync.d.ts.map +1 -0
  63. package/dist/esm/stores/sync.js +2 -0
  64. package/dist/esm/stores/sync.js.map +1 -0
  65. package/dist/esm/type-utils.d.ts +12 -0
  66. package/dist/esm/type-utils.d.ts.map +1 -0
  67. package/dist/esm/type-utils.js +2 -0
  68. package/dist/esm/type-utils.js.map +1 -0
  69. package/dist/esm/types.d.ts +62 -5
  70. package/dist/esm/types.d.ts.map +1 -1
  71. package/dist/esm/types.js +18 -0
  72. package/dist/esm/types.js.map +1 -1
  73. package/index.d.ts +1 -0
  74. package/package.json +25 -4
  75. package/stores/async.d.ts +1 -0
  76. package/stores/async.js +15 -0
  77. package/stores/sync.d.ts +1 -0
  78. package/stores/sync.js +15 -0
  79. package/.turbo/turbo-build.log +0 -12
  80. package/ENTITY_STORE_DESIGN.md +0 -386
  81. package/dist/tsconfig.esm.tsbuildinfo +0 -1
  82. package/src/EntityMap.ts +0 -63
  83. package/src/QueryClient.ts +0 -482
  84. package/src/QueryStore.ts +0 -322
  85. package/src/__tests__/caching-persistence.test.ts +0 -983
  86. package/src/__tests__/entity-system.test.ts +0 -556
  87. package/src/__tests__/gc-time.test.ts +0 -327
  88. package/src/__tests__/mock-fetch.test.ts +0 -186
  89. package/src/__tests__/parse-entities.test.ts +0 -425
  90. package/src/__tests__/path-interpolation.test.ts +0 -225
  91. package/src/__tests__/reactivity.test.ts +0 -424
  92. package/src/__tests__/refetch-interval.test.ts +0 -262
  93. package/src/__tests__/rest-query-api.test.ts +0 -568
  94. package/src/__tests__/stale-time.test.ts +0 -357
  95. package/src/__tests__/type-to-string.test.ts +0 -129
  96. package/src/__tests__/utils.ts +0 -258
  97. package/src/__tests__/validation-edge-cases.test.ts +0 -821
  98. package/src/errors.ts +0 -124
  99. package/src/index.ts +0 -7
  100. package/src/parseEntities.ts +0 -213
  101. package/src/pathInterpolator.ts +0 -74
  102. package/src/proxy.ts +0 -257
  103. package/src/query.ts +0 -164
  104. package/src/react/__tests__/basic.test.tsx +0 -926
  105. package/src/react/__tests__/component.test.tsx +0 -984
  106. package/src/react/__tests__/utils.tsx +0 -71
  107. package/src/typeDefs.ts +0 -351
  108. package/src/types.ts +0 -132
  109. package/src/utils.ts +0 -66
  110. package/tsconfig.cjs.json +0 -14
  111. package/tsconfig.esm.json +0 -13
  112. package/tsconfig.json +0 -20
  113. package/vitest.config.ts +0 -65
@@ -1,983 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-unused-expressions */
2
- import { describe, it, expect, beforeEach } from 'vitest';
3
- import {
4
- SyncQueryStore,
5
- MemoryPersistentStore,
6
- valueKeyFor,
7
- refCountKeyFor,
8
- refIdsKeyFor,
9
- updatedAtKeyFor,
10
- } from '../QueryStore.js';
11
- import { QueryClient } from '../QueryClient.js';
12
- import { entity, t } from '../typeDefs.js';
13
- import { query } from '../query.js';
14
- import { hashValue } from 'signalium/utils';
15
- import { createMockFetch, testWithClient, createTestWatcher, getClientEntityMap, sleep } from './utils.js';
16
-
17
- /**
18
- * Caching and Persistence Tests
19
- *
20
- * Tests query caching, document store persistence, reference counting,
21
- * cascade deletion, and LRU cache management.
22
- */
23
-
24
- // Helper to simulate old store.set() behavior for testing
25
- function setDocument(kv: any, key: number, value: unknown, refIds?: Set<number>) {
26
- kv.setString(valueKeyFor(key), JSON.stringify(value));
27
-
28
- const prevRefIds = kv.getBuffer(refIdsKeyFor(key));
29
-
30
- if (refIds === undefined || refIds.size === 0) {
31
- kv.delete(refIdsKeyFor(key));
32
-
33
- // Decrement all previous refs
34
- if (prevRefIds) {
35
- for (const refId of prevRefIds) {
36
- if (refId === 0) continue;
37
- const refCountKey = refCountKeyFor(refId);
38
- const currentCount = kv.getNumber(refCountKey);
39
- if (currentCount === undefined) continue;
40
-
41
- const newCount = currentCount - 1;
42
- if (newCount === 0) {
43
- kv.delete(refCountKey);
44
- } else {
45
- kv.setNumber(refCountKey, newCount);
46
- }
47
- }
48
- }
49
- } else {
50
- // Convert to array for storage
51
- const newRefArray = new Uint32Array(refIds);
52
- kv.setBuffer(refIdsKeyFor(key), newRefArray);
53
-
54
- // Build sets for comparison
55
- const prevRefSet = new Set(prevRefIds || []);
56
- const newRefSet = new Set(refIds);
57
-
58
- // Decrement refs that are no longer present
59
- if (prevRefIds) {
60
- for (const refId of prevRefIds) {
61
- if (refId === 0) continue;
62
- if (!newRefSet.has(refId)) {
63
- const refCountKey = refCountKeyFor(refId);
64
- const currentCount = kv.getNumber(refCountKey);
65
- if (currentCount === undefined) continue;
66
-
67
- const newCount = currentCount - 1;
68
- if (newCount === 0) {
69
- kv.delete(refCountKey);
70
- } else {
71
- kv.setNumber(refCountKey, newCount);
72
- }
73
- }
74
- }
75
- }
76
-
77
- // Increment refs that are new
78
- for (const refId of refIds) {
79
- if (!prevRefSet.has(refId)) {
80
- const refCountKey = refCountKeyFor(refId);
81
- const currentCount = kv.getNumber(refCountKey) ?? 0;
82
- kv.setNumber(refCountKey, currentCount + 1);
83
- }
84
- }
85
- }
86
- }
87
-
88
- // Helper to simulate old store.get() behavior
89
- function getDocument(kv: any, key: number): unknown | undefined {
90
- const value = kv.getString(valueKeyFor(key));
91
- return value ? JSON.parse(value) : undefined;
92
- }
93
-
94
- // Helper to simulate old store.delete() behavior
95
- function deleteDocument(kv: any, key: number) {
96
- const refIds = kv.getBuffer(refIdsKeyFor(key));
97
-
98
- kv.delete(valueKeyFor(key));
99
- kv.delete(refIdsKeyFor(key));
100
- kv.delete(refCountKeyFor(key));
101
-
102
- // Decrement ref counts and cascade delete if needed
103
- if (refIds) {
104
- for (const refId of refIds) {
105
- if (refId === 0) continue;
106
-
107
- const refCountKey = refCountKeyFor(refId);
108
- const currentCount = kv.getNumber(refCountKey);
109
-
110
- if (currentCount === undefined) continue;
111
-
112
- const newCount = currentCount - 1;
113
-
114
- if (newCount === 0) {
115
- // Cascade delete
116
- deleteDocument(kv, refId);
117
- } else {
118
- kv.setNumber(refCountKey, newCount);
119
- }
120
- }
121
- }
122
- }
123
-
124
- // Helper to set up a query result in the store
125
- function setQuery(kv: any, [queryDefId, params]: [string, any], result: unknown, refIds?: Set<number>) {
126
- if (typeof params === 'object' && params !== null && Object.keys(params).length === 0) {
127
- params = undefined;
128
- }
129
-
130
- const queryKey = hashValue([queryDefId, params]);
131
- setDocument(kv, queryKey, result, refIds);
132
- kv.setNumber(updatedAtKeyFor(queryKey), Date.now());
133
- }
134
-
135
- describe('Caching and Persistence', () => {
136
- let client: QueryClient;
137
- let mockFetch: ReturnType<typeof createMockFetch>;
138
- let kv: any;
139
- let store: any;
140
-
141
- beforeEach(() => {
142
- client?.destroy();
143
- kv = new MemoryPersistentStore();
144
- const queryStore = new SyncQueryStore(kv);
145
- mockFetch = createMockFetch();
146
- client = new QueryClient(queryStore, { fetch: mockFetch as any });
147
- store = queryStore;
148
- });
149
-
150
- describe('Query Result Caching', () => {
151
- it('should cache query results in document store', async () => {
152
- mockFetch.get('/items/[id]', { id: 1, name: 'Test' });
153
-
154
- await testWithClient(client, async () => {
155
- const getItem = query(t => ({
156
- path: '/items/[id]',
157
- response: { id: t.number, name: t.string },
158
- }));
159
-
160
- const relay = getItem({ id: '1' });
161
- // Watcher is automatically managed
162
- await relay;
163
-
164
- // Verify data is in document store
165
- const queryKey = hashValue(['GET:/items/[id]', { id: '1' }]);
166
- const cached = getDocument(kv, queryKey);
167
-
168
- expect(cached).toEqual({ id: 1, name: 'Test' });
169
- });
170
- });
171
-
172
- it('should load query results from cache', async () => {
173
- const queryKey = hashValue(['GET:/items/[id]', { id: '1' }]);
174
- const cachedData = { id: 1, name: 'Cached Data' };
175
-
176
- // Pre-populate cache
177
- setDocument(kv, queryKey, cachedData);
178
- kv.setNumber(updatedAtKeyFor(queryKey), Date.now());
179
-
180
- mockFetch.get(
181
- '/items/[id]',
182
- { id: 1, name: 'Fresh Data' },
183
- {
184
- delay: 10,
185
- },
186
- );
187
-
188
- const getItem = query(t => ({
189
- path: '/items/[id]',
190
- response: { id: t.number, name: t.string },
191
- }));
192
-
193
- await testWithClient(client, async () => {
194
- const relay = getItem({ id: '1' });
195
- // Force a pull
196
- relay.value;
197
- await sleep();
198
-
199
- expect(relay.value).toEqual({ id: 1, name: 'Cached Data' });
200
-
201
- const result = await relay;
202
-
203
- // Immediate value should be the same as the cached value because we're
204
- // background refetching but have a value that is still valid.
205
- expect(result).toEqual({ id: 1, name: 'Cached Data' });
206
- expect(relay.value).toEqual({ id: 1, name: 'Cached Data' });
207
- expect(relay.isPending).toBe(false);
208
- expect(relay.isRefetching).toBe(true);
209
-
210
- await sleep(20);
211
-
212
- expect(relay.isRefetching).toBe(false);
213
- expect(relay.value).toEqual({ id: 1, name: 'Fresh Data' });
214
- expect(await relay).toEqual({ id: 1, name: 'Fresh Data' });
215
- });
216
- });
217
-
218
- it('should persist across QueryClient instances', async () => {
219
- mockFetch.get('/item', { id: 1, value: 'Persistent' });
220
-
221
- const getItem = query(t => ({
222
- path: '/item',
223
- response: { id: t.number, value: t.string },
224
- }));
225
-
226
- await testWithClient(client, async () => {
227
- const relay = getItem();
228
- await relay;
229
- });
230
-
231
- expect(mockFetch.calls).toHaveLength(1);
232
- mockFetch.reset();
233
-
234
- // Create new client with same stores
235
- mockFetch.get('/item', { id: 1, value: 'New Data' }, { delay: 10 });
236
- const client2 = new QueryClient(store, { fetch: mockFetch as any });
237
-
238
- await testWithClient(client2, async () => {
239
- const relay = getItem();
240
- // Force a pull
241
- relay.value;
242
- await sleep();
243
-
244
- expect(relay.value).toEqual({ id: 1, value: 'Persistent' });
245
-
246
- const result = await relay;
247
-
248
- // Immediate value should be the same as the cached value because we're
249
- // background refetching but have a value that is still valid.
250
- expect(result).toEqual({ id: 1, value: 'Persistent' });
251
- expect(relay.value).toEqual({ id: 1, value: 'Persistent' });
252
- expect(relay.isPending).toBe(false);
253
- expect(relay.isRefetching).toBe(true);
254
-
255
- await sleep(30);
256
-
257
- expect(relay.value).toEqual({ id: 1, value: 'New Data' });
258
- expect(await relay).toEqual({ id: 1, value: 'New Data' });
259
- });
260
- });
261
- });
262
-
263
- describe('Entity Persistence', () => {
264
- it('should persist entities to document store', async () => {
265
- const User = entity(() => ({
266
- __typename: t.typename('User'),
267
- id: t.id,
268
- name: t.string,
269
- }));
270
-
271
- mockFetch.get('/users/[id]', {
272
- user: { __typename: 'User', id: 1, name: 'Alice' },
273
- });
274
-
275
- await testWithClient(client, async () => {
276
- const getUser = query(t => ({
277
- path: '/users/[id]',
278
- response: { user: User },
279
- }));
280
-
281
- const relay = getUser({ id: '1' });
282
- await relay;
283
-
284
- // Verify entity is persisted
285
- const userKey = hashValue('User:1');
286
- const entityData = getDocument(kv, userKey);
287
-
288
- expect(entityData).toBeDefined();
289
- expect(entityData).toEqual({
290
- __typename: 'User',
291
- id: 1,
292
- name: 'Alice',
293
- });
294
- });
295
- });
296
-
297
- it('should load entities from persistence', async () => {
298
- const User = entity(() => ({
299
- __typename: t.typename('User'),
300
- id: t.id,
301
- name: t.string,
302
- }));
303
-
304
- const getDocument = query(t => ({
305
- path: '/document',
306
- response: { user: User },
307
- }));
308
-
309
- // Pre-populate entity
310
- const userKey = hashValue('User:1');
311
- const userData = {
312
- __typename: 'User',
313
- id: 1,
314
- name: 'Persisted User',
315
- };
316
-
317
- setDocument(kv, userKey, userData);
318
-
319
- // Set up the query result with reference to the user
320
- const queryResult = {
321
- user: { __entityRef: userKey },
322
- };
323
- setQuery(kv, ['GET:/document', {}], queryResult, new Set([userKey]));
324
-
325
- // Query returns entity reference
326
- mockFetch.get(
327
- '/document',
328
- {
329
- user: { __typename: 'User', id: 1, name: 'Fresh User' },
330
- },
331
- { delay: 10 },
332
- );
333
-
334
- await testWithClient(client, async () => {
335
- const relay = getDocument();
336
- // Force a pull
337
- relay.value;
338
- await sleep();
339
-
340
- expect(relay.value).toEqual({ user: { __typename: 'User', id: 1, name: 'Persisted User' } });
341
-
342
- const result = await relay;
343
-
344
- // Immediate value should be the same as the cached value because we're
345
- // background refetching but have a value that is still valid.
346
- expect(result).toEqual({ user: { __typename: 'User', id: 1, name: 'Persisted User' } });
347
- expect(relay.value).toEqual({ user: { __typename: 'User', id: 1, name: 'Persisted User' } });
348
- expect(relay.isPending).toBe(false);
349
- expect(relay.isRefetching).toBe(true);
350
-
351
- await sleep(20);
352
-
353
- expect(result).toEqual({ user: { __typename: 'User', id: 1, name: 'Fresh User' } });
354
- expect(relay.value).toEqual({ user: { __typename: 'User', id: 1, name: 'Fresh User' } });
355
- });
356
- });
357
- });
358
-
359
- describe('Reference Counting', () => {
360
- it('should increment ref count when entity is referenced', async () => {
361
- const Post = entity(() => ({
362
- __typename: t.typename('Post'),
363
- id: t.id,
364
- title: t.string,
365
- }));
366
-
367
- const User = entity(() => ({
368
- __typename: t.typename('User'),
369
- id: t.id,
370
- name: t.string,
371
- favoritePost: Post,
372
- }));
373
-
374
- const getUser = query(t => ({
375
- path: '/users/[id]',
376
- response: { user: User },
377
- }));
378
-
379
- mockFetch.get('/users/[id]', {
380
- user: {
381
- __typename: 'User',
382
- id: 1,
383
- name: 'Alice',
384
- favoritePost: {
385
- __typename: 'Post',
386
- id: 1,
387
- title: 'Favorite Post',
388
- },
389
- },
390
- });
391
-
392
- await testWithClient(client, async () => {
393
- const relay = getUser({ id: '1' });
394
- await relay;
395
-
396
- // Check reference count
397
- const postKey = hashValue('Post:1');
398
- const refCount = await kv.getNumber(refCountKeyFor(postKey));
399
-
400
- expect(refCount).toBe(1);
401
- });
402
- });
403
-
404
- it('should handle multiple references to same entity', async () => {
405
- const User = entity(() => ({
406
- __typename: t.typename('User'),
407
- id: t.id,
408
- name: t.string,
409
- }));
410
-
411
- mockFetch.get('/user/profile', {
412
- user: { __typename: 'User', id: 1, name: 'Alice' },
413
- });
414
- mockFetch.get('/user/details', {
415
- user: { __typename: 'User', id: 1, name: 'Alice' },
416
- });
417
-
418
- await testWithClient(client, async () => {
419
- const getUser1 = query(t => ({
420
- path: '/user/profile',
421
- response: { user: User },
422
- }));
423
-
424
- const getUser2 = query(t => ({
425
- path: '/user/details',
426
- response: { user: User },
427
- }));
428
-
429
- const relay1 = getUser1();
430
- await relay1;
431
-
432
- const relay2 = getUser2();
433
- await relay2;
434
-
435
- // Entity should have references from queries
436
- const userKey = hashValue('User:1');
437
- const refCount = await kv.getNumber(refCountKeyFor(userKey));
438
-
439
- expect(refCount).toBe(2);
440
- });
441
- });
442
- });
443
-
444
- describe('Document Store Operations', () => {
445
- it('should store query results with entity references', async () => {
446
- const User = entity(() => ({
447
- __typename: t.typename('User'),
448
- id: t.id,
449
- name: t.string,
450
- }));
451
-
452
- mockFetch.get('/users/[id]', {
453
- user: { __typename: 'User', id: 1, name: 'Alice' },
454
- });
455
-
456
- await testWithClient(client, async () => {
457
- const getUser = query(t => ({
458
- path: '/users/[id]',
459
- response: { user: User },
460
- }));
461
-
462
- const relay = getUser({ id: '1' });
463
- await relay;
464
-
465
- // Check that query and entity are stored
466
- const queryKey = hashValue(['GET:/users/[id]', { id: '1' }]);
467
- const userKey = hashValue('User:1');
468
-
469
- const queryValue = getDocument(kv, queryKey);
470
- expect(queryValue).toBeDefined();
471
-
472
- const entityValue = getDocument(kv, userKey);
473
- expect(entityValue).toEqual({
474
- __typename: 'User',
475
- id: 1,
476
- name: 'Alice',
477
- });
478
-
479
- // Check that query references the entity
480
- const refs = await kv.getBuffer(refIdsKeyFor(queryKey));
481
- expect(refs).toBeDefined();
482
- expect(Array.from(refs!)).toContain(userKey);
483
- });
484
- });
485
-
486
- it('should store nested entity references correctly', async () => {
487
- const Post = entity(() => ({
488
- __typename: t.typename('Post'),
489
- id: t.id,
490
- title: t.string,
491
- }));
492
-
493
- const User = entity(() => ({
494
- __typename: t.typename('User'),
495
- id: t.id,
496
- name: t.string,
497
- favoritePost: Post,
498
- }));
499
-
500
- const getUser = query(t => ({
501
- path: '/users/[id]',
502
- response: { user: User },
503
- }));
504
-
505
- mockFetch.get('/users/[id]', {
506
- user: {
507
- __typename: 'User',
508
- id: 1,
509
- name: 'Alice',
510
- favoritePost: {
511
- __typename: 'Post',
512
- id: 42,
513
- title: 'My Post',
514
- },
515
- },
516
- });
517
-
518
- await testWithClient(client, async () => {
519
- const relay = getUser({ id: '1' });
520
- await relay;
521
-
522
- const userKey = hashValue('User:1');
523
- const postKey = hashValue('Post:42');
524
-
525
- // User should reference Post
526
- const userRefs = await kv.getBuffer(refIdsKeyFor(userKey));
527
- expect(userRefs).toBeDefined();
528
- expect(Array.from(userRefs!)).toContain(postKey);
529
-
530
- // Post should have a reference count of 1
531
- expect(await kv.getNumber(refCountKeyFor(postKey))).toBe(1);
532
- });
533
- });
534
- });
535
-
536
- describe('Cascade Deletion', () => {
537
- it('should cascade delete entities when query is evicted from LRU', async () => {
538
- const User = entity(() => ({
539
- __typename: t.typename('User'),
540
- id: t.id,
541
- name: t.string,
542
- }));
543
-
544
- // Set up a query cache with maxCount of 2
545
- const getUser = query(t => ({
546
- path: '/users/[id]',
547
- response: { user: User },
548
- cache: { maxCount: 2 },
549
- }));
550
-
551
- mockFetch.get('/users/1', { user: { __typename: 'User', id: 1, name: 'User 1' } });
552
- mockFetch.get('/users/2', { user: { __typename: 'User', id: 2, name: 'User 2' } });
553
- mockFetch.get('/users/3', { user: { __typename: 'User', id: 3, name: 'User 3' } });
554
-
555
- await testWithClient(client, async () => {
556
- // Fetch 3 users, the third should evict the first
557
- const relay1 = getUser({ id: '1' });
558
- await relay1;
559
-
560
- const relay2 = getUser({ id: '2' });
561
- await relay2;
562
-
563
- const query1Key = hashValue(['GET:/users/[id]', { id: '1' }]);
564
- const query2Key = hashValue(['GET:/users/[id]', { id: '2' }]);
565
- const query3Key = hashValue(['GET:/users/[id]', { id: '3' }]);
566
-
567
- const user1Key = hashValue('User:1');
568
- const user2Key = hashValue('User:2');
569
- const user3Key = hashValue('User:3');
570
-
571
- // Query 1 and 2 should exist
572
- expect(getDocument(kv, query1Key)).toBeDefined();
573
- expect(getDocument(kv, query2Key)).toBeDefined();
574
-
575
- // User 1 and 2 should exist
576
- expect(getDocument(kv, user1Key)).toBeDefined();
577
- expect(getDocument(kv, user2Key)).toBeDefined();
578
-
579
- // Fetch user 3, should evict user 1's query
580
- const relay3 = getUser({ id: '3' });
581
- await relay3;
582
-
583
- // Query 1 should be evicted
584
- expect(getDocument(kv, query1Key)).toBeUndefined();
585
- expect(getDocument(kv, query2Key)).toBeDefined();
586
- expect(getDocument(kv, query3Key)).toBeDefined();
587
-
588
- // User 1 should be cascade deleted
589
- expect(getDocument(kv, user1Key)).toBeUndefined();
590
- expect(getDocument(kv, user2Key)).toBeDefined();
591
- expect(getDocument(kv, user3Key)).toBeDefined();
592
- });
593
- });
594
-
595
- it('should NOT delete entity if still referenced by another query', async () => {
596
- const User = entity(() => ({
597
- __typename: t.typename('User'),
598
- id: t.id,
599
- name: t.string,
600
- }));
601
-
602
- const getProfile = query(t => ({
603
- path: '/user/profile/[id]',
604
- response: { user: User },
605
- cache: { maxCount: 1 },
606
- }));
607
-
608
- const getDetails = query(t => ({
609
- path: '/user/details/[id]',
610
- response: { user: User },
611
- }));
612
-
613
- mockFetch.get('/user/profile/1', {
614
- user: { __typename: 'User', id: 1, name: 'Alice' },
615
- });
616
- mockFetch.get('/user/details/1', {
617
- user: { __typename: 'User', id: 1, name: 'Alice' },
618
- });
619
-
620
- await testWithClient(client, async () => {
621
- // Both queries reference the same user
622
- const relay1 = getProfile({ id: '1' });
623
- await relay1;
624
-
625
- const relay2 = getDetails({ id: '1' });
626
- await relay2;
627
-
628
- const userKey = hashValue('User:1');
629
-
630
- // User should have ref count of 2
631
- expect(await kv.getNumber(refCountKeyFor(userKey))).toBe(2);
632
-
633
- // Force eviction of first query by making another profile request
634
- mockFetch.get('/user/profile/2', {
635
- user: { __typename: 'User', id: 2, name: 'Bob' },
636
- });
637
-
638
- const relay3 = getProfile({ id: '2' });
639
- await relay3;
640
-
641
- // Original user should still exist (referenced by details query)
642
- expect(getDocument(kv, userKey)).toBeDefined();
643
- expect(await kv.getNumber(refCountKeyFor(userKey))).toBe(1);
644
- });
645
- });
646
-
647
- it('should handle deep cascade deletion through nested entities', async () => {
648
- const Tag = entity(() => ({
649
- __typename: t.typename('Tag'),
650
- id: t.id,
651
- name: t.string,
652
- }));
653
-
654
- const Post = entity(() => ({
655
- __typename: t.typename('Post'),
656
- id: t.id,
657
- title: t.string,
658
- tag: Tag,
659
- }));
660
-
661
- const User = entity(() => ({
662
- __typename: t.typename('User'),
663
- id: t.id,
664
- name: t.string,
665
- post: Post,
666
- }));
667
-
668
- mockFetch.get('/users/[id]', {
669
- user: {
670
- __typename: 'User',
671
- id: 1,
672
- name: 'Alice',
673
- post: {
674
- __typename: 'Post',
675
- id: 10,
676
- title: 'My Post',
677
- tag: {
678
- __typename: 'Tag',
679
- id: 100,
680
- name: 'Tech',
681
- },
682
- },
683
- },
684
- });
685
-
686
- const getUser = query(t => ({
687
- path: '/users/[id]',
688
- response: { user: User },
689
- cache: { maxCount: 1 },
690
- }));
691
-
692
- await testWithClient(client, async () => {
693
- const relay1 = getUser({ id: '1' });
694
- await relay1;
695
-
696
- const userKey = hashValue('User:1');
697
- const postKey = hashValue('Post:10');
698
- const tagKey = hashValue('Tag:100');
699
-
700
- // All entities should exist
701
- expect(getDocument(kv, userKey)).toBeDefined();
702
- expect(getDocument(kv, postKey)).toBeDefined();
703
- expect(getDocument(kv, tagKey)).toBeDefined();
704
-
705
- // Fetch a different user to evict the first
706
- mockFetch.get('/users/[id]', {
707
- user: {
708
- __typename: 'User',
709
- id: 2,
710
- name: 'Bob',
711
- post: {
712
- __typename: 'Post',
713
- id: 20,
714
- title: 'Other Post',
715
- tag: {
716
- __typename: 'Tag',
717
- id: 200,
718
- name: 'Other',
719
- },
720
- },
721
- },
722
- });
723
-
724
- const relay2 = getUser({ id: '2' });
725
- await relay2;
726
-
727
- // All original entities should be cascade deleted
728
- expect(getDocument(kv, userKey)).toBeUndefined();
729
- expect(getDocument(kv, postKey)).toBeUndefined();
730
- expect(getDocument(kv, tagKey)).toBeUndefined();
731
- });
732
- });
733
- });
734
-
735
- describe('Reference Updates', () => {
736
- it('should update references when query result changes', async () => {
737
- const Post = entity(() => ({
738
- __typename: t.typename('Post'),
739
- id: t.id,
740
- title: t.string,
741
- }));
742
-
743
- const User = entity(() => ({
744
- __typename: t.typename('User'),
745
- id: t.id,
746
- name: t.string,
747
- favoritePost: Post,
748
- }));
749
-
750
- // First response with post 1
751
- mockFetch.get('/users/[id]', {
752
- user: {
753
- __typename: 'User',
754
- id: 1,
755
- name: 'Alice',
756
- favoritePost: {
757
- __typename: 'Post',
758
- id: 10,
759
- title: 'Post 10',
760
- },
761
- },
762
- });
763
-
764
- await testWithClient(client, async () => {
765
- const getUser = query(t => ({
766
- path: '/users/[id]',
767
- response: { user: User },
768
- }));
769
-
770
- const relay = getUser({ id: '1' });
771
- await relay;
772
-
773
- const post10Key = hashValue('Post:10');
774
-
775
- // Post 10 should have 1 reference
776
- expect(await kv.getNumber(refCountKeyFor(post10Key))).toBe(1);
777
-
778
- // Update response to reference a different post
779
- mockFetch.get('/users/[id]', {
780
- user: {
781
- __typename: 'User',
782
- id: 1,
783
- name: 'Alice',
784
- favoritePost: {
785
- __typename: 'Post',
786
- id: 20,
787
- title: 'Post 20',
788
- },
789
- },
790
- });
791
-
792
- const result = await relay.refetch();
793
-
794
- const post20Key = hashValue('Post:20');
795
-
796
- expect(result).toEqual({
797
- user: {
798
- __typename: 'User',
799
- id: 1,
800
- name: 'Alice',
801
- favoritePost: { __typename: 'Post', id: 20, title: 'Post 20' },
802
- },
803
- });
804
-
805
- // Post 10 should have no refs (and be deleted)
806
- expect(await kv.getNumber(refCountKeyFor(post10Key))).toBeUndefined();
807
- expect(getDocument(kv, post10Key)).toBeUndefined();
808
-
809
- // Post 20 should have 1 reference
810
- expect(await kv.getNumber(refCountKeyFor(post20Key))).toBe(1);
811
- });
812
- });
813
-
814
- it('should deduplicate entity references in arrays', async () => {
815
- const Post = entity(() => ({
816
- __typename: t.typename('Post'),
817
- id: t.id,
818
- title: t.string,
819
- }));
820
-
821
- // Response with same post referenced multiple times
822
- mockFetch.get('/posts', {
823
- posts: [
824
- { __typename: 'Post', id: 1, title: 'Post 1' },
825
- { __typename: 'Post', id: 1, title: 'Post 1' }, // Same post again
826
- { __typename: 'Post', id: 1, title: 'Post 1' }, // And again
827
- ],
828
- });
829
-
830
- await testWithClient(client, async () => {
831
- const getPosts = query(t => ({
832
- path: '/posts',
833
- response: { posts: t.array(Post) },
834
- }));
835
-
836
- const relay = getPosts();
837
- const result = await relay;
838
-
839
- expect(result.posts.length).toEqual(3);
840
-
841
- const postKey = hashValue('Post:1');
842
- const queryKey = hashValue(['GET:/posts', undefined]);
843
-
844
- // Query should reference post 1
845
- const refs = await kv.getBuffer(refIdsKeyFor(queryKey));
846
- expect(refs).toBeDefined();
847
- expect(Array.from(refs!).filter(id => id === postKey).length).toBe(1);
848
-
849
- // Post should have ref count of 1 (deduplicated)
850
- expect(await kv.getNumber(refCountKeyFor(postKey))).toBe(1);
851
- });
852
- });
853
- });
854
-
855
- describe('Storage Cleanup', () => {
856
- it('should clean up all query storage keys when evicted from LRU', async () => {
857
- const Post = entity(() => ({
858
- __typename: t.typename('Post'),
859
- id: t.id,
860
- title: t.string,
861
- }));
862
-
863
- const User = entity(() => ({
864
- __typename: t.typename('User'),
865
- id: t.id,
866
- name: t.string,
867
- post: Post,
868
- }));
869
-
870
- mockFetch.get('/users/[id]', {
871
- user: {
872
- __typename: 'User',
873
- id: 1,
874
- name: 'Alice',
875
- post: { __typename: 'Post', id: 10, title: 'Post' },
876
- },
877
- });
878
-
879
- const getUser = query(t => ({
880
- path: '/users/[id]',
881
- response: { user: User },
882
- cache: { maxCount: 1 },
883
- }));
884
-
885
- await testWithClient(client, async () => {
886
- const relay1 = getUser({ id: '1' });
887
- await relay1;
888
-
889
- const queryKey = hashValue(['GET:/users/[id]', { id: '1' }]);
890
-
891
- // Verify all keys exist for the query
892
- expect(await kv.getString(valueKeyFor(queryKey))).toBeDefined();
893
- expect(await kv.getNumber(updatedAtKeyFor(queryKey))).toBeDefined();
894
- expect(await kv.getBuffer(refIdsKeyFor(queryKey))).toBeDefined();
895
-
896
- // Fetch different user to evict first query
897
- mockFetch.get('/users/[id]', {
898
- user: {
899
- __typename: 'User',
900
- id: 2,
901
- name: 'Bob',
902
- post: { __typename: 'Post', id: 20, title: 'Other' },
903
- },
904
- });
905
-
906
- const relay2 = getUser({ id: '2' });
907
- await relay2;
908
-
909
- // All query keys should be cleaned up
910
- expect(await kv.getString(valueKeyFor(queryKey))).toBeUndefined();
911
- expect(await kv.getNumber(updatedAtKeyFor(queryKey))).toBeUndefined();
912
- expect(await kv.getBuffer(refIdsKeyFor(queryKey))).toBeUndefined();
913
- expect(await kv.getNumber(refCountKeyFor(queryKey))).toBeUndefined();
914
- });
915
- });
916
-
917
- it('should clean up all entity storage keys when cascade deleted', async () => {
918
- const Post = entity(() => ({
919
- __typename: t.typename('Post'),
920
- id: t.id,
921
- title: t.string,
922
- }));
923
-
924
- const User = entity(() => ({
925
- __typename: t.typename('User'),
926
- id: t.id,
927
- name: t.string,
928
- post: Post,
929
- }));
930
-
931
- mockFetch.get('/users/[id]', {
932
- user: {
933
- __typename: 'User',
934
- id: 1,
935
- name: 'Alice',
936
- post: { __typename: 'Post', id: 10, title: 'Post' },
937
- },
938
- });
939
-
940
- const getUser = query(t => ({
941
- path: '/users/[id]',
942
- response: { user: User },
943
- cache: { maxCount: 1 },
944
- }));
945
-
946
- await testWithClient(client, async () => {
947
- const relay1 = getUser({ id: '1' });
948
- await relay1;
949
-
950
- const userKey = hashValue('User:1');
951
- const postKey = hashValue('Post:10');
952
-
953
- // Verify all keys exist for entities
954
- expect(await kv.getString(valueKeyFor(userKey))).toBeDefined();
955
- expect(await kv.getString(valueKeyFor(postKey))).toBeDefined();
956
- expect(await kv.getBuffer(refIdsKeyFor(userKey))).toBeDefined();
957
- expect(await kv.getNumber(refCountKeyFor(postKey))).toBe(1);
958
-
959
- // Fetch different user to evict first query and cascade delete entities
960
- mockFetch.get('/users/[id]', {
961
- user: {
962
- __typename: 'User',
963
- id: 2,
964
- name: 'Bob',
965
- post: { __typename: 'Post', id: 20, title: 'Other' },
966
- },
967
- });
968
-
969
- const relay2 = getUser({ id: '2' });
970
- await relay2;
971
-
972
- // All entity keys should be cleaned up
973
- expect(await kv.getString(valueKeyFor(userKey))).toBeUndefined();
974
- expect(await kv.getNumber(refCountKeyFor(userKey))).toBeUndefined();
975
- expect(await kv.getBuffer(refIdsKeyFor(userKey))).toBeUndefined();
976
-
977
- expect(await kv.getString(valueKeyFor(postKey))).toBeUndefined();
978
- expect(await kv.getNumber(refCountKeyFor(postKey))).toBeUndefined();
979
- expect(await kv.getBuffer(refIdsKeyFor(postKey))).toBeUndefined();
980
- });
981
- });
982
- });
983
- });