@signalium/query 0.0.0 → 0.0.2

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 (95) hide show
  1. package/.turbo/turbo-build.log +12 -0
  2. package/CHANGELOG.md +17 -0
  3. package/dist/cjs/EntityMap.js +46 -0
  4. package/dist/cjs/EntityMap.js.map +1 -0
  5. package/dist/cjs/QueryClient.js +368 -0
  6. package/dist/cjs/QueryClient.js.map +1 -0
  7. package/dist/cjs/QueryStore.js +222 -0
  8. package/dist/cjs/QueryStore.js.map +1 -0
  9. package/dist/cjs/errors.js +105 -0
  10. package/dist/cjs/errors.js.map +1 -0
  11. package/dist/cjs/index.js +24 -0
  12. package/dist/cjs/index.js.map +1 -0
  13. package/dist/cjs/parseEntities.js +127 -0
  14. package/dist/cjs/parseEntities.js.map +1 -0
  15. package/dist/cjs/pathInterpolator.js +69 -0
  16. package/dist/cjs/pathInterpolator.js.map +1 -0
  17. package/dist/cjs/proxy.js +187 -0
  18. package/dist/cjs/proxy.js.map +1 -0
  19. package/dist/cjs/query.js +41 -0
  20. package/dist/cjs/query.js.map +1 -0
  21. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -0
  22. package/dist/cjs/typeDefs.js +249 -0
  23. package/dist/cjs/typeDefs.js.map +1 -0
  24. package/dist/cjs/types.js +15 -0
  25. package/dist/cjs/types.js.map +1 -0
  26. package/dist/cjs/utils.js +65 -0
  27. package/dist/cjs/utils.js.map +1 -0
  28. package/dist/esm/EntityMap.d.ts +18 -0
  29. package/dist/esm/EntityMap.d.ts.map +1 -0
  30. package/dist/esm/EntityMap.js +42 -0
  31. package/dist/esm/EntityMap.js.map +1 -0
  32. package/dist/esm/QueryClient.d.ts +123 -0
  33. package/dist/esm/QueryClient.d.ts.map +1 -0
  34. package/dist/esm/QueryClient.js +363 -0
  35. package/dist/esm/QueryClient.js.map +1 -0
  36. package/dist/esm/QueryStore.d.ts +77 -0
  37. package/dist/esm/QueryStore.d.ts.map +1 -0
  38. package/dist/esm/QueryStore.js +212 -0
  39. package/dist/esm/QueryStore.js.map +1 -0
  40. package/dist/esm/errors.d.ts +4 -0
  41. package/dist/esm/errors.d.ts.map +1 -0
  42. package/dist/esm/errors.js +101 -0
  43. package/dist/esm/errors.js.map +1 -0
  44. package/dist/esm/index.d.ts +7 -0
  45. package/dist/esm/index.d.ts.map +1 -0
  46. package/dist/esm/index.js +4 -0
  47. package/dist/esm/index.js.map +1 -0
  48. package/dist/esm/parseEntities.d.ts +8 -0
  49. package/dist/esm/parseEntities.d.ts.map +1 -0
  50. package/dist/esm/parseEntities.js +120 -0
  51. package/dist/esm/parseEntities.js.map +1 -0
  52. package/dist/esm/pathInterpolator.d.ts +29 -0
  53. package/dist/esm/pathInterpolator.d.ts.map +1 -0
  54. package/dist/esm/pathInterpolator.js +66 -0
  55. package/dist/esm/pathInterpolator.js.map +1 -0
  56. package/dist/esm/proxy.d.ts +8 -0
  57. package/dist/esm/proxy.d.ts.map +1 -0
  58. package/dist/esm/proxy.js +180 -0
  59. package/dist/esm/proxy.js.map +1 -0
  60. package/dist/esm/query.d.ts +41 -0
  61. package/dist/esm/query.d.ts.map +1 -0
  62. package/dist/esm/query.js +38 -0
  63. package/dist/esm/query.js.map +1 -0
  64. package/dist/esm/typeDefs.d.ts +25 -0
  65. package/dist/esm/typeDefs.d.ts.map +1 -0
  66. package/dist/esm/typeDefs.js +239 -0
  67. package/dist/esm/typeDefs.js.map +1 -0
  68. package/dist/esm/types.d.ts +96 -0
  69. package/dist/esm/types.d.ts.map +1 -0
  70. package/dist/esm/types.js +12 -0
  71. package/dist/esm/types.js.map +1 -0
  72. package/dist/esm/utils.d.ts +6 -0
  73. package/dist/esm/utils.d.ts.map +1 -0
  74. package/dist/esm/utils.js +60 -0
  75. package/dist/esm/utils.js.map +1 -0
  76. package/dist/tsconfig.esm.tsbuildinfo +1 -0
  77. package/package.json +3 -3
  78. package/src/QueryClient.ts +321 -105
  79. package/src/QueryStore.ts +15 -7
  80. package/src/__tests__/caching-persistence.test.ts +31 -2
  81. package/src/__tests__/entity-system.test.ts +5 -1
  82. package/src/__tests__/gc-time.test.ts +327 -0
  83. package/src/__tests__/mock-fetch.test.ts +8 -4
  84. package/src/__tests__/parse-entities.test.ts +5 -1
  85. package/src/__tests__/reactivity.test.ts +5 -1
  86. package/src/__tests__/refetch-interval.test.ts +262 -0
  87. package/src/__tests__/rest-query-api.test.ts +5 -1
  88. package/src/__tests__/stale-time.test.ts +357 -0
  89. package/src/__tests__/utils.ts +28 -12
  90. package/src/__tests__/validation-edge-cases.test.ts +1 -0
  91. package/src/query.ts +2 -1
  92. package/src/react/__tests__/basic.test.tsx +9 -4
  93. package/src/react/__tests__/component.test.tsx +10 -3
  94. package/src/types.ts +11 -0
  95. package/vitest.config.ts +4 -10
@@ -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
+ });
@@ -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
- // First try exact match
55
- const exactMatch = routes.find(r => r.url === url && r.method === method && !r.used);
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 route.response;
139
+ return await resolveResponse();
124
140
  },
125
- text: async () => JSON.stringify(route.response),
126
- blob: async () => new Blob([JSON.stringify(route.response)]),
127
- arrayBuffer: async () => new TextEncoder().encode(JSON.stringify(route.response)).buffer,
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(3);
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 loading AND still have previous value
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 pending state
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(true);
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 loading AND still have previous value
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 pending state
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(true);
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>;
package/vitest.config.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /// <reference types="@vitest/browser/providers/playwright" />
2
2
 
3
3
  import { defineConfig } from 'vitest/config';
4
+ import react from '@vitejs/plugin-react';
4
5
  import babel from 'vite-plugin-babel';
5
6
  import { signaliumPreset } from 'signalium/transform';
6
7
  import tsconfigPaths from 'vite-tsconfig-paths';
@@ -42,16 +43,9 @@ export default defineConfig({
42
43
  {
43
44
  extends: true,
44
45
  plugins: [
45
- (babel as any)({
46
- filter: /\.(j|t)sx?$/,
47
- babelConfig: {
48
- babelrc: false,
49
- configFile: false,
50
- sourceMaps: true,
51
- presets: [['@babel/preset-react', { runtime: 'automatic' }], signaliumPreset()],
52
- parserOpts: {
53
- plugins: ['typescript', 'jsx'],
54
- },
46
+ react({
47
+ babel: {
48
+ presets: [signaliumPreset()],
55
49
  },
56
50
  }),
57
51
  ],