@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.
- package/CHANGELOG.md +21 -0
- package/dist/cjs/EntityMap.js +2 -2
- package/dist/cjs/EntityMap.js.map +1 -1
- package/dist/cjs/NetworkManager.js +105 -0
- package/dist/cjs/NetworkManager.js.map +1 -0
- package/dist/cjs/QueryClient.js +390 -76
- package/dist/cjs/QueryClient.js.map +1 -1
- package/dist/cjs/QueryStore.js +295 -3
- package/dist/cjs/QueryStore.js.map +1 -1
- package/dist/cjs/index.js +16 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/parseEntities.js +3 -0
- package/dist/cjs/parseEntities.js.map +1 -1
- package/dist/cjs/proxy.js +19 -0
- package/dist/cjs/proxy.js.map +1 -1
- package/dist/cjs/query.js +40 -2
- package/dist/cjs/query.js.map +1 -1
- package/dist/cjs/stores/async.js +6 -0
- package/dist/cjs/stores/async.js.map +1 -0
- package/dist/cjs/stores/sync.js +7 -0
- package/dist/cjs/stores/sync.js.map +1 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/cjs/type-utils.js +3 -0
- package/dist/cjs/type-utils.js.map +1 -0
- package/dist/cjs/types.js +19 -1
- package/dist/cjs/types.js.map +1 -1
- package/dist/esm/EntityMap.js +3 -3
- package/dist/esm/EntityMap.js.map +1 -1
- package/dist/esm/NetworkManager.d.ts +48 -0
- package/dist/esm/NetworkManager.d.ts.map +1 -0
- package/dist/esm/NetworkManager.js +101 -0
- package/dist/esm/NetworkManager.js.map +1 -0
- package/dist/esm/QueryClient.d.ts +81 -25
- package/dist/esm/QueryClient.d.ts.map +1 -1
- package/dist/esm/QueryClient.js +390 -76
- package/dist/esm/QueryClient.js.map +1 -1
- package/dist/esm/QueryStore.d.ts +64 -2
- package/dist/esm/QueryStore.d.ts.map +1 -1
- package/dist/esm/QueryStore.js +293 -2
- package/dist/esm/QueryStore.js.map +1 -1
- package/dist/esm/index.d.ts +5 -3
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +3 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/parseEntities.d.ts.map +1 -1
- package/dist/esm/parseEntities.js +3 -0
- package/dist/esm/parseEntities.js.map +1 -1
- package/dist/esm/proxy.d.ts +6 -0
- package/dist/esm/proxy.d.ts.map +1 -1
- package/dist/esm/proxy.js +18 -0
- package/dist/esm/proxy.js.map +1 -1
- package/dist/esm/query.d.ts +30 -29
- package/dist/esm/query.d.ts.map +1 -1
- package/dist/esm/query.js +39 -3
- package/dist/esm/query.js.map +1 -1
- package/dist/esm/stores/async.d.ts +2 -0
- package/dist/esm/stores/async.d.ts.map +1 -0
- package/dist/esm/stores/async.js +2 -0
- package/dist/esm/stores/async.js.map +1 -0
- package/dist/esm/stores/sync.d.ts +2 -0
- package/dist/esm/stores/sync.d.ts.map +1 -0
- package/dist/esm/stores/sync.js +2 -0
- package/dist/esm/stores/sync.js.map +1 -0
- package/dist/esm/type-utils.d.ts +12 -0
- package/dist/esm/type-utils.d.ts.map +1 -0
- package/dist/esm/type-utils.js +2 -0
- package/dist/esm/type-utils.js.map +1 -0
- package/dist/esm/types.d.ts +62 -5
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/esm/types.js +18 -0
- package/dist/esm/types.js.map +1 -1
- package/index.d.ts +1 -0
- package/package.json +25 -7
- package/stores/async.d.ts +1 -0
- package/stores/async.js +15 -0
- package/stores/sync.d.ts +1 -0
- package/stores/sync.js +15 -0
- package/.turbo/turbo-build.log +0 -12
- package/ENTITY_STORE_DESIGN.md +0 -386
- package/dist/tsconfig.esm.tsbuildinfo +0 -1
- package/src/EntityMap.ts +0 -63
- package/src/QueryClient.ts +0 -482
- package/src/QueryStore.ts +0 -322
- package/src/__tests__/caching-persistence.test.ts +0 -983
- package/src/__tests__/entity-system.test.ts +0 -556
- package/src/__tests__/gc-time.test.ts +0 -327
- package/src/__tests__/mock-fetch.test.ts +0 -186
- package/src/__tests__/parse-entities.test.ts +0 -425
- package/src/__tests__/path-interpolation.test.ts +0 -225
- package/src/__tests__/reactivity.test.ts +0 -424
- package/src/__tests__/refetch-interval.test.ts +0 -262
- package/src/__tests__/rest-query-api.test.ts +0 -568
- package/src/__tests__/stale-time.test.ts +0 -357
- package/src/__tests__/type-to-string.test.ts +0 -129
- package/src/__tests__/utils.ts +0 -258
- package/src/__tests__/validation-edge-cases.test.ts +0 -821
- package/src/errors.ts +0 -124
- package/src/index.ts +0 -7
- package/src/parseEntities.ts +0 -213
- package/src/pathInterpolator.ts +0 -74
- package/src/proxy.ts +0 -257
- package/src/query.ts +0 -164
- package/src/react/__tests__/basic.test.tsx +0 -926
- package/src/react/__tests__/component.test.tsx +0 -984
- package/src/react/__tests__/utils.tsx +0 -71
- package/src/typeDefs.ts +0 -351
- package/src/types.ts +0 -132
- package/src/utils.ts +0 -66
- package/tsconfig.cjs.json +0 -14
- package/tsconfig.esm.json +0 -13
- package/tsconfig.json +0 -20
- 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
|
-
});
|