@signalium/query 0.0.2 → 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 +21 -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 -7
  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,424 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { SyncQueryStore, MemoryPersistentStore } from '../QueryStore.js';
3
- import { QueryClient, QueryClientContext } from '../QueryClient.js';
4
- import { entity, t } from '../typeDefs.js';
5
- import { query } from '../query.js';
6
- import { watcher, withContexts, reactive } from 'signalium';
7
- import { createMockFetch, testWithClient } from './utils.js';
8
-
9
- /**
10
- * Signalium Reactivity Tests
11
- *
12
- * Tests relay lifecycle, reactive computations, watcher behavior,
13
- * and entity update propagation.
14
- */
15
-
16
- describe('Signalium Reactivity', () => {
17
- let client: QueryClient;
18
- let mockFetch: ReturnType<typeof createMockFetch>;
19
- let kv: any;
20
- let store: any;
21
-
22
- beforeEach(() => {
23
- kv = new MemoryPersistentStore();
24
- store = new SyncQueryStore(kv);
25
- mockFetch = createMockFetch();
26
- client = new QueryClient(store, { fetch: mockFetch as any });
27
- });
28
-
29
- afterEach(() => {
30
- client?.destroy();
31
- });
32
-
33
- describe('Relay Lifecycle', () => {
34
- it('should start relay in pending state', async () => {
35
- mockFetch.get('/item', { data: 'test' }, { delay: 100 });
36
-
37
- await testWithClient(client, async () => {
38
- const getItem = query(t => ({
39
- path: '/item',
40
- response: { data: t.string },
41
- }));
42
-
43
- const relay = getItem();
44
-
45
- // Relay should exist and be in pending state
46
- expect(relay).toBeDefined();
47
- expect(relay.isPending).toBe(true);
48
- });
49
- });
50
-
51
- it('should transition to resolved state with data', async () => {
52
- mockFetch.get('/item', { data: 'test' });
53
-
54
- await testWithClient(client, async () => {
55
- const getItem = query(t => ({
56
- path: '/item',
57
- response: { data: t.string },
58
- }));
59
-
60
- const relay = getItem();
61
- await relay;
62
-
63
- expect(relay.isResolved).toBe(true);
64
- expect(relay.isReady).toBe(true);
65
- expect(relay.isPending).toBe(false);
66
- expect(relay.value).toEqual({ data: 'test' });
67
- });
68
- });
69
-
70
- it('should transition to error state on failure', async () => {
71
- const error = new Error('Failed to fetch');
72
- mockFetch.get('/item', null, { error });
73
-
74
- await testWithClient(client, async () => {
75
- const getItem = query(t => ({
76
- path: '/item',
77
- response: { data: t.string },
78
- }));
79
-
80
- const relay = getItem();
81
- await expect(relay).rejects.toThrow('Failed to fetch');
82
-
83
- expect(relay.isRejected).toBe(true);
84
- expect(relay.error).toBe(error);
85
- });
86
- });
87
- });
88
-
89
- describe('Reactive Computations', () => {
90
- it('should support reactive functions depending on query relay', async () => {
91
- mockFetch.get('/counter', { count: 5 });
92
-
93
- await testWithClient(client, async () => {
94
- const getCounter = query(t => ({
95
- path: '/counter',
96
- response: { count: t.number },
97
- }));
98
-
99
- const relay = getCounter();
100
-
101
- // Create a reactive function that depends on the relay
102
- const doubled = reactive(() => {
103
- if (relay.isReady) {
104
- return relay.value.count * 2;
105
- }
106
- return 0;
107
- });
108
-
109
- await relay;
110
-
111
- // Wait for reactive computation
112
- const result = doubled();
113
-
114
- expect(result).toBe(10);
115
- });
116
- });
117
-
118
- it('should support nested reactive functions', async () => {
119
- mockFetch.get('/value', { value: 10 });
120
-
121
- await testWithClient(client, async () => {
122
- const getValue = query(t => ({
123
- path: '/value',
124
- response: { value: t.number },
125
- }));
126
-
127
- const relay = getValue();
128
-
129
- const doubled = reactive(() => {
130
- if (relay.isReady) {
131
- return relay.value.value * 2;
132
- }
133
- return 0;
134
- });
135
-
136
- const tripled = reactive(() => {
137
- return doubled() * 1.5;
138
- });
139
-
140
- await relay;
141
-
142
- expect(doubled()).toBe(20);
143
- expect(tripled()).toBe(30);
144
- });
145
- });
146
-
147
- it('should support conditional reactivity', async () => {
148
- mockFetch.get('/config', { value: 5, shouldDouble: true });
149
-
150
- await testWithClient(client, async () => {
151
- const getConfig = query(t => ({
152
- path: '/config',
153
- response: { value: t.number, shouldDouble: t.boolean },
154
- }));
155
-
156
- const relay = getConfig();
157
-
158
- const computed = reactive(() => {
159
- if (relay.isReady) {
160
- const config = relay.value;
161
- return config.shouldDouble ? config.value * 2 : config.value;
162
- }
163
- return 0;
164
- });
165
-
166
- await relay;
167
-
168
- expect(computed()).toBe(10);
169
- });
170
- });
171
- });
172
-
173
- describe('Promise State Tracking', () => {
174
- it('should track isPending state', async () => {
175
- mockFetch.get('/item', { data: 'test' }, { delay: 50 });
176
-
177
- await testWithClient(client, async () => {
178
- const getItem = query(t => ({
179
- path: '/item',
180
- response: { data: t.string },
181
- }));
182
-
183
- const relay = getItem();
184
-
185
- // Initially should be pending
186
- expect(relay.isPending).toBe(true);
187
-
188
- await relay;
189
-
190
- // After resolution should not be pending
191
- expect(relay.isPending).toBe(false);
192
- expect(relay.isSettled).toBe(true);
193
- });
194
- });
195
-
196
- it('should track isReady state correctly', async () => {
197
- mockFetch.get('/item', { data: 'test' });
198
-
199
- await testWithClient(client, async () => {
200
- const getItem = query(t => ({
201
- path: '/item',
202
- response: { data: t.string },
203
- }));
204
-
205
- const relay = getItem();
206
-
207
- await relay;
208
-
209
- // After resolution should be ready
210
- expect(relay.isReady).toBe(true);
211
- expect(relay.value).toEqual({ data: 'test' });
212
- });
213
- });
214
-
215
- it('should track isResolved state', async () => {
216
- mockFetch.get('/item', { success: true });
217
-
218
- await testWithClient(client, async () => {
219
- const getItem = query(t => ({
220
- path: '/item',
221
- response: { success: t.boolean },
222
- }));
223
-
224
- const relay = getItem();
225
- await relay;
226
-
227
- expect(relay.isResolved).toBe(true);
228
- expect(relay.isRejected).toBe(false);
229
- });
230
- });
231
-
232
- it('should track isRejected state', async () => {
233
- mockFetch.get('/item', null, { error: new Error('Failed') });
234
-
235
- await testWithClient(client, async () => {
236
- const getItem = query(t => ({
237
- path: '/item',
238
- response: { success: t.boolean },
239
- }));
240
-
241
- const relay = getItem();
242
- await expect(relay).rejects.toThrow('Failed');
243
-
244
- expect(relay.isRejected).toBe(true);
245
- expect(relay.isResolved).toBe(false);
246
- });
247
- });
248
- });
249
-
250
- describe('Reactive Query Patterns', () => {
251
- it('should support query results in reactive computations', async () => {
252
- mockFetch.get('/users', {
253
- users: [
254
- { id: 1, name: 'Alice' },
255
- { id: 2, name: 'Bob' },
256
- ],
257
- });
258
-
259
- await testWithClient(client, async () => {
260
- const getUsers = query(t => ({
261
- path: '/users',
262
- response: {
263
- users: t.array(t.object({ id: t.number, name: t.string })),
264
- },
265
- }));
266
-
267
- const relay = getUsers();
268
-
269
- const userCount = reactive(() => {
270
- if (relay.isReady) {
271
- return relay.value.users.length;
272
- }
273
- return 0;
274
- });
275
-
276
- const firstUserName = reactive(() => {
277
- if (relay.isReady && relay.value.users.length > 0) {
278
- return relay.value.users[0].name;
279
- }
280
- return 'Unknown';
281
- });
282
-
283
- await relay;
284
-
285
- expect(userCount()).toBe(2);
286
- expect(firstUserName()).toBe('Alice');
287
- });
288
- });
289
-
290
- it('should handle conditional query access', async () => {
291
- mockFetch.get('/config', { enabled: true, data: 'test' });
292
-
293
- await testWithClient(client, async () => {
294
- const getConfig = query(t => ({
295
- path: '/config',
296
- response: { enabled: t.boolean, data: t.string },
297
- }));
298
-
299
- const relay = getConfig();
300
-
301
- const result = reactive(() => {
302
- if (relay.isReady) {
303
- const config = relay.value;
304
- // Conditional access - only read data if enabled
305
- return config.enabled ? config.data : 'disabled';
306
- }
307
- return 'loading';
308
- });
309
-
310
- expect(result()).toBe('loading');
311
-
312
- await relay;
313
-
314
- expect(result()).toBe('test');
315
- });
316
- });
317
- });
318
-
319
- describe('Concurrent Query Handling', () => {
320
- it('should handle concurrent queries without interference', async () => {
321
- // Set up mocks for different IDs
322
- mockFetch.get('/items/1', { url: '/items/1', timestamp: Date.now() });
323
- mockFetch.get('/items/2', { url: '/items/2', timestamp: Date.now() });
324
- mockFetch.get('/items/3', { url: '/items/3', timestamp: Date.now() });
325
-
326
- await testWithClient(client, async () => {
327
- const getItem = query(t => ({
328
- path: '/items/[id]',
329
- response: { url: t.string, timestamp: t.number },
330
- }));
331
-
332
- // Start multiple concurrent queries
333
- const relay1 = getItem({ id: '1' });
334
- const relay2 = getItem({ id: '2' });
335
- const relay3 = getItem({ id: '3' });
336
-
337
- const [result1, result2, result3] = await Promise.all([relay1, relay2, relay3]);
338
-
339
- expect(result1.url).toContain('/items/1');
340
- expect(result2.url).toContain('/items/2');
341
- expect(result3.url).toContain('/items/3');
342
- });
343
- });
344
-
345
- it('should deduplicate concurrent identical requests', async () => {
346
- // Set up single mock with delay - should only be called once
347
- mockFetch.get('/item', { count: 1 }, { delay: 50 });
348
-
349
- await testWithClient(client, async () => {
350
- const getItem = query(t => ({
351
- path: '/item',
352
- response: { count: t.number },
353
- }));
354
-
355
- // Start multiple concurrent identical requests
356
- const relay1 = getItem();
357
- const relay2 = getItem();
358
- const relay3 = getItem();
359
-
360
- // Should be same relay
361
- expect(relay1).toBe(relay2);
362
- expect(relay2).toBe(relay3);
363
-
364
- const [result1, result2, result3] = await Promise.all([relay1, relay2, relay3]);
365
-
366
- // Should only fetch once
367
- expect(mockFetch.calls).toHaveLength(1);
368
-
369
- // All should have same result
370
- expect(result1).toEqual(result2);
371
- expect(result2).toEqual(result3);
372
- });
373
- });
374
- });
375
-
376
- describe('Error State Reactivity', () => {
377
- it('should notify watchers on error', async () => {
378
- const error = new Error('Network error');
379
- mockFetch.get('/item', null, { error });
380
-
381
- await testWithClient(client, async () => {
382
- const getItem = query(t => ({
383
- path: '/item',
384
- response: { data: t.string },
385
- }));
386
-
387
- const relay = getItem();
388
- let errorCaught = false;
389
-
390
- const w = watcher(() => {
391
- if (relay.isRejected) {
392
- errorCaught = true;
393
- }
394
- });
395
-
396
- const unsub = w.addListener(() => {});
397
-
398
- await expect(relay).rejects.toThrow('Network error');
399
- await new Promise(resolve => setTimeout(resolve, 10));
400
-
401
- expect(errorCaught).toBe(true);
402
-
403
- unsub();
404
- });
405
- });
406
-
407
- it('should expose error object on relay', async () => {
408
- const error = new Error('Custom error');
409
- mockFetch.get('/item', null, { error });
410
-
411
- await testWithClient(client, async () => {
412
- const getItem = query(t => ({
413
- path: '/item',
414
- response: { data: t.string },
415
- }));
416
-
417
- const relay = getItem();
418
- await expect(relay).rejects.toThrow('Custom error');
419
-
420
- expect(relay.error).toBe(error);
421
- });
422
- });
423
- });
424
- });
@@ -1,262 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-unused-expressions */
2
- import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
3
- import { SyncQueryStore, MemoryPersistentStore } from '../QueryStore.js';
4
- import { QueryClient } from '../QueryClient.js';
5
- import { query } from '../query.js';
6
- import { RefetchInterval } from '../types.js';
7
- import { createMockFetch, testWithClient, sleep } from './utils.js';
8
-
9
- /**
10
- * RefetchInterval Tests
11
- *
12
- * Tests refetchInterval with dynamic GCD-based timer management,
13
- * subscriber tracking, exponential backoff, and no overlapping fetches
14
- */
15
-
16
- describe('RefetchInterval', () => {
17
- let client: QueryClient;
18
- let mockFetch: ReturnType<typeof createMockFetch>;
19
- let kv: any;
20
- let store: any;
21
-
22
- beforeEach(() => {
23
- client?.destroy();
24
- kv = new MemoryPersistentStore();
25
- store = new SyncQueryStore(kv);
26
- mockFetch = createMockFetch();
27
- client = new QueryClient(store, { fetch: mockFetch as any, refetchMultiplier: 0.1 });
28
- });
29
-
30
- afterEach(() => {
31
- client?.destroy();
32
- });
33
-
34
- describe('Basic Refetch Interval', () => {
35
- it('should refetch at specified interval', async () => {
36
- let callCount = 0;
37
- mockFetch.get('/counter', () => ({ count: ++callCount }));
38
-
39
- const getCounter = query(t => ({
40
- path: '/counter',
41
- response: { count: t.number },
42
- cache: { refetchInterval: RefetchInterval.Every1Second },
43
- }));
44
-
45
- await testWithClient(client, async () => {
46
- const relay = getCounter();
47
- await relay;
48
- expect(relay.value).toEqual({ count: 1 });
49
-
50
- // Wait for interval to trigger (100ms with 0.1 multiplier + buffer)
51
- await sleep(120);
52
- expect(relay.value?.count).toBeGreaterThan(1);
53
-
54
- // Wait for another interval
55
- await sleep(110);
56
- expect(relay.value?.count).toBeGreaterThan(2);
57
- });
58
- });
59
-
60
- it('should stop refetching when query is no longer accessed', async () => {
61
- let callCount = 0;
62
- mockFetch.get('/item', () => ({ n: ++callCount }));
63
-
64
- const getItem = query(t => ({
65
- path: '/item',
66
- response: { n: t.number },
67
- cache: { refetchInterval: RefetchInterval.Every1Second },
68
- }));
69
-
70
- await testWithClient(client, async () => {
71
- const relay = getItem();
72
- await relay;
73
- const initialCount = relay.value!.n;
74
-
75
- // Wait a bit (250ms with 0.1 multiplier = ~2.5 intervals)
76
- await sleep(250);
77
- const afterCount = relay.value!.n;
78
-
79
- expect(afterCount).toBeGreaterThan(initialCount);
80
-
81
- await sleep(250);
82
- });
83
-
84
- // After context ends, wait and check that no more calls happen
85
- const countBeforeWait = callCount;
86
- await sleep(200);
87
-
88
- // Note: This test is simplified - in a real implementation,
89
- // subscriber tracking would need proper cleanup
90
- // For now we just verify the basic interval works
91
- });
92
- });
93
-
94
- describe('Multiple Intervals with GCD', () => {
95
- it('should handle multiple queries with different intervals efficiently', async () => {
96
- let count1 = 0;
97
- let count5 = 0;
98
-
99
- mockFetch.get('/every1s', () => ({ count: ++count1 }));
100
- mockFetch.get('/every5s', () => ({ count: ++count5 }));
101
-
102
- const getEvery1s = query(t => ({
103
- path: '/every1s',
104
- response: { count: t.number },
105
- cache: { refetchInterval: RefetchInterval.Every1Second },
106
- }));
107
-
108
- const getEvery5s = query(t => ({
109
- path: '/every5s',
110
- response: { count: t.number },
111
- cache: { refetchInterval: RefetchInterval.Every5Seconds },
112
- }));
113
-
114
- await testWithClient(client, async () => {
115
- const relay1s = getEvery1s();
116
- const relay5s = getEvery5s();
117
-
118
- await relay1s;
119
- await relay5s;
120
-
121
- // Wait and verify different refetch rates (350ms = 3.5 intervals of 1s)
122
- await sleep(350);
123
-
124
- // 1s query should have refetched ~3 times
125
- expect(count1).toBeGreaterThanOrEqual(3);
126
- expect(count1).toBeLessThanOrEqual(5);
127
-
128
- // 5s query should have refetched 0-1 times
129
- expect(count5).toBeGreaterThanOrEqual(1);
130
- expect(count5).toBeLessThanOrEqual(2);
131
-
132
- // Wait for 5s interval (200ms more)
133
- await sleep(200);
134
-
135
- // 5s query should now have refetched
136
- expect(count5).toBeGreaterThanOrEqual(2);
137
- });
138
- });
139
-
140
- it('should use GCD for multiple queries with compatible intervals', async () => {
141
- // Every5Seconds and Every10Seconds should use GCD of 5s
142
- let count5 = 0;
143
- let count10 = 0;
144
-
145
- mockFetch.get('/5s', () => ({ n: ++count5 }));
146
- mockFetch.get('/10s', () => ({ n: ++count10 }));
147
-
148
- const get5s = query(t => ({
149
- path: '/5s',
150
- response: { n: t.number },
151
- cache: { refetchInterval: RefetchInterval.Every5Seconds },
152
- }));
153
-
154
- const get10s = query(t => ({
155
- path: '/10s',
156
- response: { n: t.number },
157
- cache: { refetchInterval: RefetchInterval.Every10Seconds },
158
- }));
159
-
160
- await testWithClient(client, async () => {
161
- const relay5 = get5s();
162
- const relay10 = get10s();
163
-
164
- await Promise.all([relay5, relay10]);
165
-
166
- // Wait 1100ms (11 seconds at 0.1x = 1.1s)
167
- await sleep(1100);
168
-
169
- // 5s should refetch ~2 times
170
- expect(count5).toBeGreaterThanOrEqual(2);
171
-
172
- // 10s should refetch ~1 time
173
- expect(count10).toBeGreaterThanOrEqual(1);
174
- expect(count10).toBeLessThanOrEqual(2);
175
- });
176
- });
177
- });
178
-
179
- describe('No Overlapping Fetches', () => {
180
- it('should wait for previous fetch to complete before next refetch', async () => {
181
- let activeFetches = 0;
182
- let maxConcurrent = 0;
183
- let fetchCount = 0;
184
-
185
- mockFetch.get('/slow', async () => {
186
- activeFetches++;
187
- maxConcurrent = Math.max(maxConcurrent, activeFetches);
188
- fetchCount++;
189
- await sleep(80); // Slow fetch (80ms = 800ms at 0.1x)
190
- activeFetches--;
191
- return { count: fetchCount };
192
- });
193
-
194
- const getSlow = query(t => ({
195
- path: '/slow',
196
- response: { count: t.number },
197
- cache: { refetchInterval: RefetchInterval.Every1Second },
198
- }));
199
-
200
- await testWithClient(client, async () => {
201
- const relay = getSlow();
202
- await relay;
203
-
204
- // Wait for several intervals (350ms = 3.5 intervals)
205
- await sleep(350);
206
-
207
- // Should never have overlapping fetches
208
- expect(maxConcurrent).toBe(1);
209
-
210
- // Should have attempted multiple fetches but not overlapping
211
- expect(fetchCount).toBeGreaterThan(1);
212
- });
213
- });
214
- });
215
-
216
- describe('Edge Cases', () => {
217
- it('should handle query without refetchInterval', async () => {
218
- let callCount = 0;
219
- mockFetch.get('/no-interval', () => ({ n: ++callCount }));
220
-
221
- const getItem = query(t => ({
222
- path: '/no-interval',
223
- response: { n: t.number },
224
- // No refetchInterval
225
- }));
226
-
227
- await testWithClient(client, async () => {
228
- const relay = getItem();
229
- await relay;
230
- expect(relay.value).toEqual({ n: 1 });
231
-
232
- // Wait a bit (200ms = 2s at 0.1x)
233
- await sleep(200);
234
-
235
- // Should not have refetched
236
- expect(callCount).toBe(1);
237
- });
238
- });
239
-
240
- it('should handle very fast intervals', async () => {
241
- let callCount = 0;
242
- mockFetch.get('/fast', () => ({ count: ++callCount }));
243
-
244
- const getFast = query(t => ({
245
- path: '/fast',
246
- response: { count: t.number },
247
- cache: { refetchInterval: RefetchInterval.Every1Second },
248
- }));
249
-
250
- await testWithClient(client, async () => {
251
- const relay = getFast();
252
- await relay;
253
-
254
- // Wait 250ms (2.5 intervals at 0.1x)
255
- await sleep(250);
256
-
257
- // Should have refetched at least twice
258
- expect(callCount).toBeGreaterThanOrEqual(2);
259
- });
260
- });
261
- });
262
- });