@signalium/query 0.0.1 → 0.1.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/.turbo/turbo-build.log +3 -3
- package/CHANGELOG.md +14 -0
- package/dist/cjs/QueryClient.js +254 -66
- package/dist/cjs/QueryClient.js.map +1 -1
- package/dist/cjs/QueryStore.js +8 -5
- package/dist/cjs/QueryStore.js.map +1 -1
- package/dist/cjs/query.js +1 -1
- package/dist/cjs/query.js.map +1 -1
- package/dist/cjs/types.js +10 -1
- package/dist/cjs/types.js.map +1 -1
- package/dist/esm/QueryClient.d.ts +58 -17
- package/dist/esm/QueryClient.d.ts.map +1 -1
- package/dist/esm/QueryClient.js +255 -67
- package/dist/esm/QueryClient.js.map +1 -1
- package/dist/esm/QueryStore.d.ts +6 -2
- package/dist/esm/QueryStore.d.ts.map +1 -1
- package/dist/esm/QueryStore.js +8 -5
- package/dist/esm/QueryStore.js.map +1 -1
- package/dist/esm/query.d.ts +1 -0
- package/dist/esm/query.d.ts.map +1 -1
- package/dist/esm/query.js +1 -1
- package/dist/esm/query.js.map +1 -1
- package/dist/esm/types.d.ts +10 -0
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/esm/types.js +9 -0
- package/dist/esm/types.js.map +1 -1
- package/package.json +3 -6
- package/src/QueryClient.ts +321 -105
- package/src/QueryStore.ts +15 -7
- package/src/__tests__/caching-persistence.test.ts +31 -2
- package/src/__tests__/entity-system.test.ts +5 -1
- package/src/__tests__/gc-time.test.ts +327 -0
- package/src/__tests__/mock-fetch.test.ts +8 -4
- package/src/__tests__/parse-entities.test.ts +5 -1
- package/src/__tests__/reactivity.test.ts +5 -1
- package/src/__tests__/refetch-interval.test.ts +262 -0
- package/src/__tests__/rest-query-api.test.ts +5 -1
- package/src/__tests__/stale-time.test.ts +357 -0
- package/src/__tests__/utils.ts +28 -12
- package/src/__tests__/validation-edge-cases.test.ts +1 -0
- package/src/query.ts +2 -1
- package/src/react/__tests__/basic.test.tsx +9 -4
- package/src/react/__tests__/component.test.tsx +10 -3
- package/src/types.ts +11 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
|
2
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
3
|
+
import { SyncQueryStore, MemoryPersistentStore, updatedAtKeyFor } from '../QueryStore.js';
|
|
4
|
+
import { QueryClient } from '../QueryClient.js';
|
|
5
|
+
import { query } from '../query.js';
|
|
6
|
+
import { createMockFetch, testWithClient, sleep } from './utils.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* StaleTime Tests
|
|
10
|
+
*
|
|
11
|
+
* Tests staleTime behavior: serving cached data while refetching in background
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
describe('StaleTime', () => {
|
|
15
|
+
let client: QueryClient;
|
|
16
|
+
let mockFetch: ReturnType<typeof createMockFetch>;
|
|
17
|
+
let kv: any;
|
|
18
|
+
let store: any;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
client?.destroy();
|
|
22
|
+
kv = new MemoryPersistentStore();
|
|
23
|
+
store = new SyncQueryStore(kv);
|
|
24
|
+
mockFetch = createMockFetch();
|
|
25
|
+
client = new QueryClient(store, { fetch: mockFetch as any });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('Fresh Data', () => {
|
|
29
|
+
it('should not refetch when data is fresh (within staleTime)', async () => {
|
|
30
|
+
// Set up query with 10 second staleTime
|
|
31
|
+
const getItem = query(t => ({
|
|
32
|
+
path: '/item',
|
|
33
|
+
response: { value: t.string },
|
|
34
|
+
cache: { staleTime: 10000 }, // 10 seconds
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
mockFetch.get('/item', { value: 'first' });
|
|
38
|
+
|
|
39
|
+
await testWithClient(client, async () => {
|
|
40
|
+
// First fetch
|
|
41
|
+
const relay1 = getItem();
|
|
42
|
+
await relay1;
|
|
43
|
+
expect(relay1.value).toEqual({ value: 'first' });
|
|
44
|
+
expect(mockFetch.calls).toHaveLength(1);
|
|
45
|
+
|
|
46
|
+
// Second access immediately (data is fresh)
|
|
47
|
+
mockFetch.get('/item', { value: 'second' });
|
|
48
|
+
const relay2 = getItem();
|
|
49
|
+
|
|
50
|
+
// Force evaluation
|
|
51
|
+
relay2.value;
|
|
52
|
+
await sleep(50);
|
|
53
|
+
|
|
54
|
+
// Should use cached data without refetch
|
|
55
|
+
expect(relay2.value).toEqual({ value: 'first' });
|
|
56
|
+
expect(mockFetch.calls).toHaveLength(1); // Still only one call
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should use fresh data from disk cache without refetch', async () => {
|
|
61
|
+
const getItem = query(t => ({
|
|
62
|
+
path: '/item',
|
|
63
|
+
response: { data: t.number },
|
|
64
|
+
cache: { staleTime: 5000 },
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
mockFetch.get('/item', { data: 42 });
|
|
68
|
+
|
|
69
|
+
await testWithClient(client, async () => {
|
|
70
|
+
const relay1 = getItem();
|
|
71
|
+
await relay1;
|
|
72
|
+
expect(mockFetch.calls).toHaveLength(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Create new client with same store (simulating app restart)
|
|
76
|
+
mockFetch.reset();
|
|
77
|
+
mockFetch.get('/item', { data: 99 }, { delay: 50 });
|
|
78
|
+
const client2 = new QueryClient(store, { fetch: mockFetch as any });
|
|
79
|
+
|
|
80
|
+
await testWithClient(client2, async () => {
|
|
81
|
+
const relay = getItem();
|
|
82
|
+
|
|
83
|
+
// Should immediately have cached value
|
|
84
|
+
relay.value;
|
|
85
|
+
await sleep(10);
|
|
86
|
+
expect(relay.value).toEqual({ data: 42 });
|
|
87
|
+
|
|
88
|
+
// Should not refetch since data is still fresh
|
|
89
|
+
await sleep(100);
|
|
90
|
+
expect(mockFetch.calls).toHaveLength(0);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('Stale Data', () => {
|
|
96
|
+
it('should serve stale data immediately while refetching in background', async () => {
|
|
97
|
+
const getItem = query(t => ({
|
|
98
|
+
path: '/item',
|
|
99
|
+
response: { count: t.number },
|
|
100
|
+
staleTime: 100, // 100ms
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
mockFetch.get('/item', { count: 1 });
|
|
104
|
+
|
|
105
|
+
await testWithClient(client, async () => {
|
|
106
|
+
// Initial fetch
|
|
107
|
+
const relay1 = getItem();
|
|
108
|
+
await relay1;
|
|
109
|
+
expect(relay1.value).toEqual({ count: 1 });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Wait for data to become stale, and unwatch query entirely
|
|
113
|
+
await sleep(200);
|
|
114
|
+
|
|
115
|
+
await testWithClient(client, async () => {
|
|
116
|
+
// Set up new response
|
|
117
|
+
mockFetch.get('/item', { count: 2 }, { delay: 50 });
|
|
118
|
+
|
|
119
|
+
// Access again - should serve stale data immediately
|
|
120
|
+
const relay2 = getItem();
|
|
121
|
+
relay2.value;
|
|
122
|
+
await sleep(10);
|
|
123
|
+
|
|
124
|
+
// Should have stale data immediately
|
|
125
|
+
expect(relay2.value).toEqual({ count: 1 });
|
|
126
|
+
|
|
127
|
+
// Wait for background refetch to complete
|
|
128
|
+
await sleep(100);
|
|
129
|
+
|
|
130
|
+
// Should now have fresh data
|
|
131
|
+
expect(relay2.value).toEqual({ count: 2 });
|
|
132
|
+
expect(mockFetch.calls).toHaveLength(2);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should refetch stale data from disk cache', async () => {
|
|
137
|
+
const getItem = query(t => ({
|
|
138
|
+
path: '/data',
|
|
139
|
+
response: { version: t.number },
|
|
140
|
+
staleTime: 100,
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
mockFetch.get('/data', { version: 1 });
|
|
144
|
+
|
|
145
|
+
await testWithClient(client, async () => {
|
|
146
|
+
const relay = getItem();
|
|
147
|
+
await relay;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Wait for data to become stale
|
|
151
|
+
await sleep(150);
|
|
152
|
+
|
|
153
|
+
// Create new client
|
|
154
|
+
mockFetch.reset();
|
|
155
|
+
mockFetch.get('/data', { version: 2 }, { delay: 50 });
|
|
156
|
+
const client2 = new QueryClient(store, { fetch: mockFetch as any });
|
|
157
|
+
|
|
158
|
+
await testWithClient(client2, async () => {
|
|
159
|
+
const relay = getItem();
|
|
160
|
+
|
|
161
|
+
// Should have cached value immediately
|
|
162
|
+
relay.value;
|
|
163
|
+
await sleep(10);
|
|
164
|
+
expect(relay.value).toEqual({ version: 1 });
|
|
165
|
+
|
|
166
|
+
// Should trigger background refetch
|
|
167
|
+
await sleep(100);
|
|
168
|
+
|
|
169
|
+
expect(relay.value).toEqual({ version: 2 });
|
|
170
|
+
expect(mockFetch.calls).toHaveLength(1);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should handle no staleTime (always refetch)', async () => {
|
|
175
|
+
const getItem = query(t => ({
|
|
176
|
+
path: '/item',
|
|
177
|
+
response: { value: t.string },
|
|
178
|
+
// No staleTime configured
|
|
179
|
+
}));
|
|
180
|
+
|
|
181
|
+
mockFetch.get('/item', { value: 'first' });
|
|
182
|
+
|
|
183
|
+
await testWithClient(client, async () => {
|
|
184
|
+
const relay1 = getItem();
|
|
185
|
+
await relay1;
|
|
186
|
+
expect(mockFetch.calls).toHaveLength(1);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Access again immediately
|
|
190
|
+
await testWithClient(client, async () => {
|
|
191
|
+
mockFetch.get('/item', { value: 'second' }, { delay: 50 });
|
|
192
|
+
const relay2 = getItem();
|
|
193
|
+
|
|
194
|
+
relay2.value;
|
|
195
|
+
await sleep(10);
|
|
196
|
+
|
|
197
|
+
// Should have cached value
|
|
198
|
+
expect(relay2.value).toEqual({ value: 'first' });
|
|
199
|
+
|
|
200
|
+
// But should refetch in background
|
|
201
|
+
await sleep(100);
|
|
202
|
+
|
|
203
|
+
expect(relay2.value).toEqual({ value: 'second' });
|
|
204
|
+
expect(mockFetch.calls).toHaveLength(2);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('Edge Cases', () => {
|
|
210
|
+
it('should handle staleTime of 0 (always stale)', async () => {
|
|
211
|
+
const getItem = query(t => ({
|
|
212
|
+
path: '/item',
|
|
213
|
+
response: { n: t.number },
|
|
214
|
+
cache: { staleTime: 0 },
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
mockFetch.get('/item', { n: 1 });
|
|
218
|
+
|
|
219
|
+
await testWithClient(client, async () => {
|
|
220
|
+
const relay1 = getItem();
|
|
221
|
+
await relay1;
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await testWithClient(client, async () => {
|
|
225
|
+
mockFetch.get('/item', { n: 2 }, { delay: 50 });
|
|
226
|
+
const relay2 = getItem();
|
|
227
|
+
|
|
228
|
+
// Should serve cached but refetch immediately
|
|
229
|
+
relay2.value;
|
|
230
|
+
await sleep(10);
|
|
231
|
+
expect(relay2.value).toEqual({ n: 1 });
|
|
232
|
+
|
|
233
|
+
await sleep(100);
|
|
234
|
+
expect(relay2.value).toEqual({ n: 2 });
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should handle very long staleTime', async () => {
|
|
239
|
+
vi.useFakeTimers();
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const getItem = query(t => ({
|
|
243
|
+
path: '/item',
|
|
244
|
+
response: { data: t.string },
|
|
245
|
+
cache: { staleTime: 1000 * 60 * 60 }, // 1 hour
|
|
246
|
+
}));
|
|
247
|
+
|
|
248
|
+
mockFetch.get('/item', { data: 'cached' });
|
|
249
|
+
|
|
250
|
+
// First subscription - fetch initial data
|
|
251
|
+
await testWithClient(client, async () => {
|
|
252
|
+
const relay1 = getItem();
|
|
253
|
+
await relay1;
|
|
254
|
+
expect(relay1.value).toEqual({ data: 'cached' });
|
|
255
|
+
expect(mockFetch.calls).toHaveLength(1);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Unsubscribed now (testWithClient ended)
|
|
259
|
+
|
|
260
|
+
// Second subscription - should still use cache (data is fresh)
|
|
261
|
+
mockFetch.reset();
|
|
262
|
+
mockFetch.get('/item', { data: 'fresh1' });
|
|
263
|
+
await testWithClient(client, async () => {
|
|
264
|
+
const relay2 = getItem();
|
|
265
|
+
relay2.value;
|
|
266
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
267
|
+
|
|
268
|
+
// Should use cached data without refetch (still fresh)
|
|
269
|
+
expect(relay2.value).toEqual({ data: 'cached' });
|
|
270
|
+
expect(mockFetch.calls).toHaveLength(0);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Advance time by 30 minutes - still within 1 hour staleTime
|
|
274
|
+
await vi.advanceTimersByTimeAsync(30 * 60 * 1000);
|
|
275
|
+
|
|
276
|
+
// Third subscription - data should still be fresh
|
|
277
|
+
mockFetch.reset();
|
|
278
|
+
mockFetch.get('/item', { data: 'fresh2' });
|
|
279
|
+
await testWithClient(client, async () => {
|
|
280
|
+
const relay3 = getItem();
|
|
281
|
+
relay3.value;
|
|
282
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
283
|
+
|
|
284
|
+
// Should still use cached data (within 1 hour)
|
|
285
|
+
expect(relay3.value).toEqual({ data: 'cached' });
|
|
286
|
+
expect(mockFetch.calls).toHaveLength(0);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Advance time past the 1 hour mark (31 more minutes = 61 minutes total)
|
|
290
|
+
await vi.advanceTimersByTimeAsync(31 * 60 * 1000);
|
|
291
|
+
|
|
292
|
+
// Fourth subscription - data should now be stale and trigger refetch
|
|
293
|
+
mockFetch.reset();
|
|
294
|
+
mockFetch.get('/item', { data: 'fresh-after-hour' }, { delay: 100 });
|
|
295
|
+
await testWithClient(client, async () => {
|
|
296
|
+
const relay4 = getItem();
|
|
297
|
+
|
|
298
|
+
// Should serve stale data immediately
|
|
299
|
+
relay4.value;
|
|
300
|
+
await vi.advanceTimersByTimeAsync(10);
|
|
301
|
+
expect(relay4.value).toEqual({ data: 'cached' });
|
|
302
|
+
|
|
303
|
+
// Wait for background refetch to complete
|
|
304
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
305
|
+
|
|
306
|
+
// Should now have fresh data
|
|
307
|
+
expect(relay4.value).toEqual({ data: 'fresh-after-hour' });
|
|
308
|
+
expect(mockFetch.calls).toHaveLength(1);
|
|
309
|
+
});
|
|
310
|
+
} finally {
|
|
311
|
+
vi.useRealTimers();
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should handle concurrent access to stale data', async () => {
|
|
316
|
+
const getItem = query(t => ({
|
|
317
|
+
path: '/item',
|
|
318
|
+
response: { id: t.number },
|
|
319
|
+
cache: { staleTime: 50 },
|
|
320
|
+
}));
|
|
321
|
+
|
|
322
|
+
mockFetch.get('/item', { id: 1 });
|
|
323
|
+
|
|
324
|
+
await testWithClient(client, async () => {
|
|
325
|
+
const relay1 = getItem();
|
|
326
|
+
await relay1;
|
|
327
|
+
|
|
328
|
+
await sleep(100); // Make stale
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
await testWithClient(client, async () => {
|
|
332
|
+
mockFetch.get('/item', { id: 2 }, { delay: 100 });
|
|
333
|
+
|
|
334
|
+
// Multiple concurrent accesses
|
|
335
|
+
const relay2 = getItem();
|
|
336
|
+
const relay3 = getItem();
|
|
337
|
+
const relay4 = getItem();
|
|
338
|
+
|
|
339
|
+
// All should be the same relay
|
|
340
|
+
expect(relay2).toBe(relay3);
|
|
341
|
+
expect(relay3).toBe(relay4);
|
|
342
|
+
|
|
343
|
+
// Should serve stale data immediately
|
|
344
|
+
relay2.value;
|
|
345
|
+
await sleep(10);
|
|
346
|
+
expect(relay2.value).toEqual({ id: 1 });
|
|
347
|
+
|
|
348
|
+
// Wait for refetch
|
|
349
|
+
await sleep(100);
|
|
350
|
+
expect(relay2.value).toEqual({ id: 2 });
|
|
351
|
+
|
|
352
|
+
// Should only refetch once
|
|
353
|
+
expect(mockFetch.calls).toHaveLength(2);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|
package/src/__tests__/utils.ts
CHANGED
|
@@ -51,13 +51,8 @@ export function createMockFetch(): MockFetch {
|
|
|
51
51
|
const calls: Array<{ url: string; options: RequestInit }> = [];
|
|
52
52
|
|
|
53
53
|
const matchRoute = (url: string, method: string): MockRoute | undefined => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (exactMatch) return exactMatch;
|
|
57
|
-
|
|
58
|
-
// Then try pattern matching (for URLs with query params or path segments)
|
|
59
|
-
return routes.find(r => {
|
|
60
|
-
if (r.method !== method || r.used) return false;
|
|
54
|
+
const isMatch = (r: MockRoute): boolean => {
|
|
55
|
+
if (r.method !== method) return false;
|
|
61
56
|
|
|
62
57
|
// Simple pattern: check if the route URL is a prefix or matches the base path
|
|
63
58
|
const routeBase = r.url.split('?')[0];
|
|
@@ -80,7 +75,20 @@ export function createMockFetch(): MockFetch {
|
|
|
80
75
|
}
|
|
81
76
|
|
|
82
77
|
return false;
|
|
83
|
-
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// First try to find an unused match
|
|
81
|
+
const unusedMatch = routes.find(r => !r.used && isMatch(r));
|
|
82
|
+
if (unusedMatch) return unusedMatch;
|
|
83
|
+
|
|
84
|
+
// If no unused matches, reuse the last matching route
|
|
85
|
+
for (let i = routes.length - 1; i >= 0; i--) {
|
|
86
|
+
if (isMatch(routes[i])) {
|
|
87
|
+
return routes[i];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return undefined;
|
|
84
92
|
};
|
|
85
93
|
|
|
86
94
|
const mockFetch = async (url: string, options: RequestInit = {}): Promise<Response> => {
|
|
@@ -110,6 +118,14 @@ export function createMockFetch(): MockFetch {
|
|
|
110
118
|
const status = route.options.status ?? 200;
|
|
111
119
|
const headers = route.options.headers ?? {};
|
|
112
120
|
|
|
121
|
+
// Resolve response if it's a function
|
|
122
|
+
const resolveResponse = async () => {
|
|
123
|
+
if (typeof route.response === 'function') {
|
|
124
|
+
return await route.response();
|
|
125
|
+
}
|
|
126
|
+
return route.response;
|
|
127
|
+
};
|
|
128
|
+
|
|
113
129
|
// Create a mock Response object
|
|
114
130
|
const response = {
|
|
115
131
|
ok: status >= 200 && status < 300,
|
|
@@ -120,11 +136,11 @@ export function createMockFetch(): MockFetch {
|
|
|
120
136
|
if (route.options.jsonError) {
|
|
121
137
|
throw route.options.jsonError;
|
|
122
138
|
}
|
|
123
|
-
return
|
|
139
|
+
return await resolveResponse();
|
|
124
140
|
},
|
|
125
|
-
text: async () => JSON.stringify(
|
|
126
|
-
blob: async () => new Blob([JSON.stringify(
|
|
127
|
-
arrayBuffer: async () => new TextEncoder().encode(JSON.stringify(
|
|
141
|
+
text: async () => JSON.stringify(await resolveResponse()),
|
|
142
|
+
blob: async () => new Blob([JSON.stringify(await resolveResponse())]),
|
|
143
|
+
arrayBuffer: async () => new TextEncoder().encode(JSON.stringify(await resolveResponse())).buffer,
|
|
128
144
|
clone: () => response,
|
|
129
145
|
} as Response;
|
|
130
146
|
|
|
@@ -19,6 +19,7 @@ describe('Type Validation and Edge Cases', () => {
|
|
|
19
19
|
let mockFetch: ReturnType<typeof createMockFetch>;
|
|
20
20
|
|
|
21
21
|
beforeEach(() => {
|
|
22
|
+
client?.destroy();
|
|
22
23
|
const store = new SyncQueryStore(new MemoryPersistentStore());
|
|
23
24
|
mockFetch = createMockFetch();
|
|
24
25
|
client = new QueryClient(store, { fetch: mockFetch as any });
|
package/src/query.ts
CHANGED
|
@@ -67,6 +67,7 @@ interface RESTQueryDefinition {
|
|
|
67
67
|
response: Record<string, ObjectFieldTypeDef> | ObjectFieldTypeDef;
|
|
68
68
|
|
|
69
69
|
cache?: QueryCacheOptions;
|
|
70
|
+
refetchInterval?: number;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
type ExtractTypesFromObjectOrTypeDef<S extends Record<string, ObjectFieldTypeDef> | ObjectFieldTypeDef | undefined> =
|
|
@@ -123,7 +124,7 @@ export function query<const QDef extends RESTQueryDefinition>(
|
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
if (queryDefinition === undefined) {
|
|
126
|
-
const { path, method = 'GET', response, cache } = queryDefinitionBuilder(t);
|
|
127
|
+
const { path, method = 'GET', response, cache, refetchInterval } = queryDefinitionBuilder(t);
|
|
127
128
|
|
|
128
129
|
const id = `${method}:${path}`;
|
|
129
130
|
|
|
@@ -23,6 +23,7 @@ describe('React Query Integration', () => {
|
|
|
23
23
|
let mockFetch: ReturnType<typeof createMockFetch>;
|
|
24
24
|
|
|
25
25
|
beforeEach(() => {
|
|
26
|
+
client?.destroy();
|
|
26
27
|
const store = new SyncQueryStore(new MemoryPersistentStore());
|
|
27
28
|
mockFetch = createMockFetch();
|
|
28
29
|
client = new QueryClient(store, { fetch: mockFetch as any });
|
|
@@ -216,7 +217,7 @@ describe('React Query Integration', () => {
|
|
|
216
217
|
|
|
217
218
|
await expect.element(getByText('Alice Updated')).toBeInTheDocument();
|
|
218
219
|
expect(getByTestId(String(Counter.testId))).toBeDefined();
|
|
219
|
-
expect(Counter.renderCount).toBe(
|
|
220
|
+
expect(Counter.renderCount).toBe(2);
|
|
220
221
|
});
|
|
221
222
|
|
|
222
223
|
it('should keep multiple components in sync when sharing entity data', async () => {
|
|
@@ -906,16 +907,20 @@ describe('React Query Integration', () => {
|
|
|
906
907
|
// Trigger refetch with delay
|
|
907
908
|
await getByText('Refetch').click();
|
|
908
909
|
|
|
909
|
-
// During refetch, should show
|
|
910
|
+
// During refetch, should show fetching AND still have previous value
|
|
910
911
|
await sleep(10);
|
|
911
|
-
// Note: The value should still be accessible even during
|
|
912
|
+
// Note: The value should still be accessible even during refetch state
|
|
912
913
|
expect(itemQuery!.value?.data).toBe('first');
|
|
913
|
-
expect(itemQuery!.isPending).toBe(
|
|
914
|
+
expect(itemQuery!.isPending).toBe(false); // Not pending - we have data!
|
|
915
|
+
expect(itemQuery!.isRefetching).toBe(true); // But we are refetching
|
|
916
|
+
expect(itemQuery!.isFetching).toBe(true); // isFetching = isPending || isRefetching
|
|
914
917
|
|
|
915
918
|
await sleep(100);
|
|
916
919
|
|
|
917
920
|
// After refetch completes
|
|
918
921
|
expect(getByTestId('data').element().textContent).toBe('second');
|
|
922
|
+
expect(itemQuery!.isRefetching).toBe(false);
|
|
923
|
+
expect(itemQuery!.isFetching).toBe(false);
|
|
919
924
|
});
|
|
920
925
|
});
|
|
921
926
|
});
|
|
@@ -24,6 +24,7 @@ describe('React Query Integration with component()', () => {
|
|
|
24
24
|
let mockFetch: ReturnType<typeof createMockFetch>;
|
|
25
25
|
|
|
26
26
|
beforeEach(() => {
|
|
27
|
+
client?.destroy();
|
|
27
28
|
const store = new SyncQueryStore(new MemoryPersistentStore());
|
|
28
29
|
mockFetch = createMockFetch();
|
|
29
30
|
client = new QueryClient(store, { fetch: mockFetch as any });
|
|
@@ -724,6 +725,7 @@ describe('React Query Integration with component()', () => {
|
|
|
724
725
|
return (
|
|
725
726
|
<div>
|
|
726
727
|
<div data-testid="count">{result.isReady ? result.value!.count : 'Loading'}</div>
|
|
728
|
+
<div data-testid="refetching">{result.isRefetching ? 'Refetching' : 'Idle'}</div>
|
|
727
729
|
<button
|
|
728
730
|
onClick={async () => {
|
|
729
731
|
mockFetch.get('/counter', { count: (result.value?.count ?? 0) + 1 });
|
|
@@ -754,6 +756,7 @@ describe('React Query Integration with component()', () => {
|
|
|
754
756
|
await sleep(10);
|
|
755
757
|
|
|
756
758
|
expect(getByTestId('count').element().textContent).toBe('1');
|
|
759
|
+
expect(getByTestId('refetching').element().textContent).toBe('Idle');
|
|
757
760
|
});
|
|
758
761
|
|
|
759
762
|
it('should work with nested components', async () => {
|
|
@@ -962,16 +965,20 @@ describe('React Query Integration with component()', () => {
|
|
|
962
965
|
// Trigger refetch with delay
|
|
963
966
|
await getByText('Refetch').click();
|
|
964
967
|
|
|
965
|
-
// During refetch, should show
|
|
968
|
+
// During refetch, should show fetching AND still have previous value
|
|
966
969
|
await sleep(10);
|
|
967
|
-
// Note: The value should still be accessible even during
|
|
970
|
+
// Note: The value should still be accessible even during refetch state
|
|
968
971
|
expect(itemQuery!.value?.data).toBe('first');
|
|
969
|
-
expect(itemQuery!.isPending).toBe(
|
|
972
|
+
expect(itemQuery!.isPending).toBe(false); // Not pending - we have data!
|
|
973
|
+
expect(itemQuery!.isRefetching).toBe(true); // But we are refetching
|
|
974
|
+
expect(itemQuery!.isFetching).toBe(true); // isFetching = isPending || isRefetching
|
|
970
975
|
|
|
971
976
|
await sleep(100);
|
|
972
977
|
|
|
973
978
|
// After refetch completes
|
|
974
979
|
expect(getByTestId('data').element().textContent).toBe('second');
|
|
980
|
+
expect(itemQuery!.isRefetching).toBe(false);
|
|
981
|
+
expect(itemQuery!.isFetching).toBe(false);
|
|
975
982
|
});
|
|
976
983
|
});
|
|
977
984
|
});
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { PendingReactivePromise, ReadyReactivePromise } from 'signalium';
|
|
2
2
|
import { ReactivePromise } from 'signalium';
|
|
3
3
|
|
|
4
|
+
export enum RefetchInterval {
|
|
5
|
+
Every1Second = 1000,
|
|
6
|
+
Every5Seconds = 5000,
|
|
7
|
+
Every10Seconds = 10000,
|
|
8
|
+
Every30Seconds = 30000,
|
|
9
|
+
Every1Minute = 60000,
|
|
10
|
+
Every5Minutes = 300000,
|
|
11
|
+
}
|
|
12
|
+
|
|
4
13
|
export const enum Mask {
|
|
5
14
|
// Fundamental types
|
|
6
15
|
UNDEFINED = 1 << 0,
|
|
@@ -110,6 +119,8 @@ export interface APITypes {
|
|
|
110
119
|
|
|
111
120
|
type QueryResultExtensions<T> = {
|
|
112
121
|
refetch: () => Promise<T>;
|
|
122
|
+
readonly isRefetching: boolean;
|
|
123
|
+
readonly isFetching: boolean;
|
|
113
124
|
};
|
|
114
125
|
|
|
115
126
|
export type QueryResult<T> = ReactivePromise<T> & QueryResultExtensions<T>;
|