what-core 0.3.0 → 0.4.1
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/dist/a11y.js +22 -7
- package/dist/animation.js +9 -1
- package/dist/components.js +61 -23
- package/dist/data.js +253 -59
- package/dist/dom.js +196 -24
- package/dist/form.js +112 -44
- package/dist/helpers.js +73 -10
- package/dist/hooks.js +63 -22
- package/dist/index.js +6 -2
- package/dist/reactive.js +189 -29
- package/dist/render.js +716 -0
- package/dist/scheduler.js +10 -5
- package/dist/store.js +18 -8
- package/package.json +10 -1
- package/src/a11y.js +22 -7
- package/src/animation.js +9 -1
- package/src/components.js +61 -23
- package/src/data.js +253 -59
- package/src/dom.js +222 -24
- package/src/form.js +112 -44
- package/src/h.js +3 -0
- package/src/helpers.js +73 -10
- package/src/hooks.js +63 -22
- package/src/index.js +6 -2
- package/src/jsx-dev-runtime.js +19 -0
- package/src/jsx-runtime.js +21 -0
- package/src/reactive.js +208 -39
- package/src/render.js +716 -0
- package/src/scheduler.js +10 -5
- package/src/store.js +18 -8
package/dist/data.js
CHANGED
|
@@ -1,12 +1,75 @@
|
|
|
1
1
|
// What Framework - Data Fetching
|
|
2
2
|
// SWR-like data fetching with caching, revalidation, and optimistic updates
|
|
3
3
|
|
|
4
|
-
import { signal, effect, batch, computed } from './reactive.js';
|
|
4
|
+
import { signal, effect, batch, computed, __DEV__ } from './reactive.js';
|
|
5
5
|
import { getCurrentComponent } from './dom.js';
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
|
|
7
|
+
// --- Reactive Cache ---
|
|
8
|
+
// Each cache key maps to shared signals so all components reading the same key
|
|
9
|
+
// see updates when ANY component mutates/revalidates that key.
|
|
10
|
+
// Shared per key: data signal, error signal, isValidating signal.
|
|
11
|
+
const cacheSignals = new Map(); // key -> signal(value)
|
|
12
|
+
const errorSignals = new Map(); // key -> signal(error)
|
|
13
|
+
const validatingSignals = new Map(); // key -> signal(boolean)
|
|
14
|
+
const cacheTimestamps = new Map(); // key -> last access time (for LRU)
|
|
15
|
+
const MAX_CACHE_SIZE = 200;
|
|
16
|
+
|
|
17
|
+
function getCacheSignal(key) {
|
|
18
|
+
cacheTimestamps.set(key, Date.now());
|
|
19
|
+
if (!cacheSignals.has(key)) {
|
|
20
|
+
cacheSignals.set(key, signal(null));
|
|
21
|
+
// Evict oldest entries if cache exceeds limit
|
|
22
|
+
if (cacheSignals.size > MAX_CACHE_SIZE) {
|
|
23
|
+
evictOldest();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return cacheSignals.get(key);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getErrorSignal(key) {
|
|
30
|
+
if (!errorSignals.has(key)) errorSignals.set(key, signal(null));
|
|
31
|
+
return errorSignals.get(key);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getValidatingSignal(key) {
|
|
35
|
+
if (!validatingSignals.has(key)) validatingSignals.set(key, signal(false));
|
|
36
|
+
return validatingSignals.get(key);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function evictOldest() {
|
|
40
|
+
// Remove the 20% oldest entries
|
|
41
|
+
const entries = [...cacheTimestamps.entries()].sort((a, b) => a[1] - b[1]);
|
|
42
|
+
const toRemove = Math.floor(MAX_CACHE_SIZE * 0.2);
|
|
43
|
+
for (let i = 0; i < toRemove && i < entries.length; i++) {
|
|
44
|
+
const key = entries[i][0];
|
|
45
|
+
// Don't evict keys with active subscribers
|
|
46
|
+
if (revalidationSubscribers.has(key) && revalidationSubscribers.get(key).size > 0) continue;
|
|
47
|
+
cacheSignals.delete(key);
|
|
48
|
+
errorSignals.delete(key);
|
|
49
|
+
validatingSignals.delete(key);
|
|
50
|
+
cacheTimestamps.delete(key);
|
|
51
|
+
lastFetchTimestamps.delete(key);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Subscribers for invalidation: key -> Set<revalidateFn>
|
|
56
|
+
// When invalidateQueries is called, all subscribers re-fetch
|
|
57
|
+
const revalidationSubscribers = new Map();
|
|
58
|
+
|
|
59
|
+
function subscribeToKey(key, revalidateFn) {
|
|
60
|
+
if (!revalidationSubscribers.has(key)) revalidationSubscribers.set(key, new Set());
|
|
61
|
+
revalidationSubscribers.get(key).add(revalidateFn);
|
|
62
|
+
return () => {
|
|
63
|
+
const subs = revalidationSubscribers.get(key);
|
|
64
|
+
if (subs) {
|
|
65
|
+
subs.delete(revalidateFn);
|
|
66
|
+
if (subs.size === 0) revalidationSubscribers.delete(key);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
9
71
|
const inFlightRequests = new Map();
|
|
72
|
+
const lastFetchTimestamps = new Map(); // key -> timestamp of last completed fetch
|
|
10
73
|
|
|
11
74
|
// Create an effect scoped to the current component's lifecycle.
|
|
12
75
|
// When the component unmounts, the effect is automatically disposed.
|
|
@@ -32,8 +95,14 @@ export function useFetch(url, options = {}) {
|
|
|
32
95
|
const data = signal(initialData);
|
|
33
96
|
const error = signal(null);
|
|
34
97
|
const isLoading = signal(true);
|
|
98
|
+
let abortController = null;
|
|
35
99
|
|
|
36
100
|
async function fetchData() {
|
|
101
|
+
// Abort previous request
|
|
102
|
+
if (abortController) abortController.abort();
|
|
103
|
+
abortController = new AbortController();
|
|
104
|
+
const { signal: abortSignal } = abortController;
|
|
105
|
+
|
|
37
106
|
isLoading.set(true);
|
|
38
107
|
error.set(null);
|
|
39
108
|
|
|
@@ -45,6 +114,7 @@ export function useFetch(url, options = {}) {
|
|
|
45
114
|
...headers,
|
|
46
115
|
},
|
|
47
116
|
body: body ? JSON.stringify(body) : undefined,
|
|
117
|
+
signal: abortSignal,
|
|
48
118
|
});
|
|
49
119
|
|
|
50
120
|
if (!response.ok) {
|
|
@@ -52,17 +122,26 @@ export function useFetch(url, options = {}) {
|
|
|
52
122
|
}
|
|
53
123
|
|
|
54
124
|
const json = await response.json();
|
|
55
|
-
|
|
125
|
+
if (!abortSignal.aborted) {
|
|
126
|
+
data.set(transform(json));
|
|
127
|
+
}
|
|
56
128
|
} catch (e) {
|
|
57
|
-
|
|
129
|
+
if (!abortSignal.aborted) {
|
|
130
|
+
error.set(e);
|
|
131
|
+
}
|
|
58
132
|
} finally {
|
|
59
|
-
|
|
133
|
+
if (!abortSignal.aborted) {
|
|
134
|
+
isLoading.set(false);
|
|
135
|
+
}
|
|
60
136
|
}
|
|
61
137
|
}
|
|
62
138
|
|
|
63
|
-
// Fetch on mount
|
|
139
|
+
// Fetch on mount, abort on unmount
|
|
64
140
|
scopedEffect(() => {
|
|
65
141
|
fetchData();
|
|
142
|
+
return () => {
|
|
143
|
+
if (abortController) abortController.abort();
|
|
144
|
+
};
|
|
66
145
|
});
|
|
67
146
|
|
|
68
147
|
return {
|
|
@@ -89,49 +168,91 @@ export function useSWR(key, fetcher, options = {}) {
|
|
|
89
168
|
suspense = false,
|
|
90
169
|
} = options;
|
|
91
170
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
171
|
+
// Support null/undefined/false key for conditional/dependent fetching
|
|
172
|
+
// When key is falsy, don't fetch — return idle state
|
|
173
|
+
if (key == null || key === false) {
|
|
174
|
+
const data = signal(fallbackData || null);
|
|
175
|
+
const error = signal(null);
|
|
176
|
+
return {
|
|
177
|
+
data: () => data(),
|
|
178
|
+
error: () => error(),
|
|
179
|
+
isLoading: () => false,
|
|
180
|
+
isValidating: () => false,
|
|
181
|
+
mutate: (newData) => data.set(typeof newData === 'function' ? newData(data()) : newData),
|
|
182
|
+
revalidate: () => Promise.resolve(),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Shared reactive cache signals — all useSWR instances with the same key
|
|
187
|
+
// read from these signals, so mutating from one component updates all others.
|
|
188
|
+
const cacheS = getCacheSignal(key);
|
|
189
|
+
const error = getErrorSignal(key);
|
|
190
|
+
const isValidating = getValidatingSignal(key);
|
|
191
|
+
const data = computed(() => cacheS() ?? fallbackData ?? null);
|
|
192
|
+
const isLoading = computed(() => cacheS() == null && isValidating());
|
|
193
|
+
|
|
194
|
+
let abortController = null;
|
|
97
195
|
|
|
98
196
|
async function revalidate() {
|
|
99
|
-
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
|
|
199
|
+
// Deduplication: if there's already a request in flight, reuse it
|
|
100
200
|
if (inFlightRequests.has(key)) {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return existingPromise.promise;
|
|
201
|
+
const existing = inFlightRequests.get(key);
|
|
202
|
+
if (now - existing.timestamp < dedupingInterval) {
|
|
203
|
+
return existing.promise;
|
|
105
204
|
}
|
|
106
205
|
}
|
|
107
206
|
|
|
207
|
+
// Also deduplicate against recently completed fetches
|
|
208
|
+
const lastFetch = lastFetchTimestamps.get(key);
|
|
209
|
+
if (lastFetch && now - lastFetch < dedupingInterval && cacheS.peek() != null) {
|
|
210
|
+
return cacheS.peek();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Abort previous request
|
|
214
|
+
if (abortController) abortController.abort();
|
|
215
|
+
abortController = new AbortController();
|
|
216
|
+
const { signal: abortSignal } = abortController;
|
|
217
|
+
|
|
108
218
|
isValidating.set(true);
|
|
109
219
|
|
|
110
|
-
const promise = fetcher(key);
|
|
111
|
-
inFlightRequests.set(key, { promise, timestamp:
|
|
220
|
+
const promise = fetcher(key, { signal: abortSignal });
|
|
221
|
+
inFlightRequests.set(key, { promise, timestamp: now });
|
|
112
222
|
|
|
113
223
|
try {
|
|
114
224
|
const result = await promise;
|
|
225
|
+
if (abortSignal.aborted) return;
|
|
115
226
|
batch(() => {
|
|
116
|
-
|
|
227
|
+
cacheS.set(result); // Updates ALL components reading this key
|
|
117
228
|
error.set(null);
|
|
118
|
-
cache.set(key, result);
|
|
119
229
|
});
|
|
230
|
+
cacheTimestamps.set(key, Date.now());
|
|
231
|
+
lastFetchTimestamps.set(key, Date.now());
|
|
120
232
|
if (onSuccess) onSuccess(result, key);
|
|
121
233
|
return result;
|
|
122
234
|
} catch (e) {
|
|
235
|
+
if (abortSignal.aborted) return;
|
|
123
236
|
error.set(e);
|
|
124
237
|
if (onError) onError(e, key);
|
|
125
238
|
throw e;
|
|
126
239
|
} finally {
|
|
127
|
-
isValidating.set(false);
|
|
240
|
+
if (!abortSignal.aborted) isValidating.set(false);
|
|
128
241
|
inFlightRequests.delete(key);
|
|
129
242
|
}
|
|
130
243
|
}
|
|
131
244
|
|
|
245
|
+
// Subscribe to invalidation events for this key
|
|
246
|
+
const unsubscribe = subscribeToKey(key, () => revalidate().catch(() => {}));
|
|
247
|
+
|
|
132
248
|
// Initial fetch
|
|
133
249
|
scopedEffect(() => {
|
|
134
250
|
revalidate().catch(() => {});
|
|
251
|
+
// Cleanup: abort and unsubscribe on unmount
|
|
252
|
+
return () => {
|
|
253
|
+
if (abortController) abortController.abort();
|
|
254
|
+
unsubscribe();
|
|
255
|
+
};
|
|
135
256
|
});
|
|
136
257
|
|
|
137
258
|
// Revalidate on focus
|
|
@@ -169,11 +290,12 @@ export function useSWR(key, fetcher, options = {}) {
|
|
|
169
290
|
return {
|
|
170
291
|
data: () => data(),
|
|
171
292
|
error: () => error(),
|
|
172
|
-
isLoading,
|
|
293
|
+
isLoading: () => isLoading(),
|
|
173
294
|
isValidating: () => isValidating(),
|
|
174
295
|
mutate: (newData, shouldRevalidate = true) => {
|
|
175
|
-
|
|
176
|
-
|
|
296
|
+
const resolved = typeof newData === 'function' ? newData(cacheS.peek()) : newData;
|
|
297
|
+
cacheS.set(resolved); // Updates ALL components reading this key
|
|
298
|
+
cacheTimestamps.set(key, Date.now());
|
|
177
299
|
if (shouldRevalidate) {
|
|
178
300
|
revalidate().catch(() => {});
|
|
179
301
|
}
|
|
@@ -205,28 +327,34 @@ export function useQuery(options) {
|
|
|
205
327
|
|
|
206
328
|
const key = Array.isArray(queryKey) ? queryKey.join(':') : queryKey;
|
|
207
329
|
|
|
208
|
-
const
|
|
330
|
+
const cacheS = getCacheSignal(key);
|
|
209
331
|
const data = computed(() => {
|
|
210
|
-
const d =
|
|
332
|
+
const d = cacheS();
|
|
211
333
|
return select && d !== null ? select(d) : d;
|
|
212
334
|
});
|
|
213
|
-
const error =
|
|
214
|
-
const status = signal(
|
|
335
|
+
const error = getErrorSignal(key);
|
|
336
|
+
const status = signal(cacheS.peek() != null ? 'success' : 'loading');
|
|
215
337
|
const fetchStatus = signal('idle');
|
|
216
338
|
|
|
217
339
|
let lastFetchTime = 0;
|
|
340
|
+
let abortController = null;
|
|
218
341
|
|
|
219
|
-
async function
|
|
342
|
+
async function fetchQuery() {
|
|
220
343
|
if (!enabled) return;
|
|
221
344
|
|
|
222
345
|
// Check if data is still fresh
|
|
223
346
|
const now = Date.now();
|
|
224
|
-
if (
|
|
225
|
-
return
|
|
347
|
+
if (cacheS.peek() != null && now - lastFetchTime < staleTime) {
|
|
348
|
+
return cacheS.peek();
|
|
226
349
|
}
|
|
227
350
|
|
|
351
|
+
// Abort previous request
|
|
352
|
+
if (abortController) abortController.abort();
|
|
353
|
+
abortController = new AbortController();
|
|
354
|
+
const { signal: abortSignal } = abortController;
|
|
355
|
+
|
|
228
356
|
fetchStatus.set('fetching');
|
|
229
|
-
if (
|
|
357
|
+
if (cacheS.peek() == null) {
|
|
230
358
|
status.set('loading');
|
|
231
359
|
}
|
|
232
360
|
|
|
@@ -234,31 +362,48 @@ export function useQuery(options) {
|
|
|
234
362
|
|
|
235
363
|
async function attemptFetch() {
|
|
236
364
|
try {
|
|
237
|
-
const result = await queryFn({ queryKey: Array.isArray(queryKey) ? queryKey : [queryKey] });
|
|
365
|
+
const result = await queryFn({ queryKey: Array.isArray(queryKey) ? queryKey : [queryKey], signal: abortSignal });
|
|
366
|
+
if (abortSignal.aborted) return;
|
|
238
367
|
batch(() => {
|
|
239
|
-
|
|
368
|
+
cacheS.set(result); // Updates all components reading this key
|
|
240
369
|
error.set(null);
|
|
241
370
|
status.set('success');
|
|
242
371
|
fetchStatus.set('idle');
|
|
243
372
|
});
|
|
244
|
-
cache.set(key, result);
|
|
245
373
|
lastFetchTime = Date.now();
|
|
374
|
+
cacheTimestamps.set(key, Date.now());
|
|
246
375
|
|
|
247
376
|
if (onSuccess) onSuccess(result);
|
|
248
377
|
if (onSettled) onSettled(result, null);
|
|
249
378
|
|
|
250
|
-
// Schedule cache cleanup
|
|
379
|
+
// Schedule cache cleanup (only if no active subscribers)
|
|
251
380
|
setTimeout(() => {
|
|
252
381
|
if (Date.now() - lastFetchTime >= cacheTime) {
|
|
253
|
-
|
|
382
|
+
const subs = revalidationSubscribers.get(key);
|
|
383
|
+
if (!subs || subs.size === 0) {
|
|
384
|
+
cacheSignals.delete(key);
|
|
385
|
+
errorSignals.delete(key);
|
|
386
|
+
validatingSignals.delete(key);
|
|
387
|
+
cacheTimestamps.delete(key);
|
|
388
|
+
lastFetchTimestamps.delete(key);
|
|
389
|
+
}
|
|
254
390
|
}
|
|
255
391
|
}, cacheTime);
|
|
256
392
|
|
|
257
393
|
return result;
|
|
258
394
|
} catch (e) {
|
|
395
|
+
if (abortSignal.aborted) return;
|
|
259
396
|
attempts++;
|
|
260
397
|
if (attempts < retry) {
|
|
261
|
-
|
|
398
|
+
// Abort-aware retry delay: cancel the wait if the component unmounts
|
|
399
|
+
await new Promise((resolve, reject) => {
|
|
400
|
+
const id = setTimeout(resolve, retryDelay(attempts));
|
|
401
|
+
abortSignal.addEventListener('abort', () => {
|
|
402
|
+
clearTimeout(id);
|
|
403
|
+
reject(new DOMException('Aborted', 'AbortError'));
|
|
404
|
+
}, { once: true });
|
|
405
|
+
}).catch(e => { if (e.name === 'AbortError') return; throw e; });
|
|
406
|
+
if (abortSignal.aborted) return;
|
|
262
407
|
return attemptFetch();
|
|
263
408
|
}
|
|
264
409
|
|
|
@@ -278,11 +423,18 @@ export function useQuery(options) {
|
|
|
278
423
|
return attemptFetch();
|
|
279
424
|
}
|
|
280
425
|
|
|
426
|
+
// Subscribe to invalidation events for this key
|
|
427
|
+
const unsubscribe = subscribeToKey(key, () => fetchQuery().catch(() => {}));
|
|
428
|
+
|
|
281
429
|
// Initial fetch
|
|
282
430
|
scopedEffect(() => {
|
|
283
431
|
if (enabled) {
|
|
284
|
-
|
|
432
|
+
fetchQuery().catch(() => {});
|
|
285
433
|
}
|
|
434
|
+
return () => {
|
|
435
|
+
if (abortController) abortController.abort();
|
|
436
|
+
unsubscribe();
|
|
437
|
+
};
|
|
286
438
|
});
|
|
287
439
|
|
|
288
440
|
// Refetch on focus
|
|
@@ -290,7 +442,7 @@ export function useQuery(options) {
|
|
|
290
442
|
scopedEffect(() => {
|
|
291
443
|
const handler = () => {
|
|
292
444
|
if (document.visibilityState === 'visible') {
|
|
293
|
-
|
|
445
|
+
fetchQuery().catch(() => {});
|
|
294
446
|
}
|
|
295
447
|
};
|
|
296
448
|
document.addEventListener('visibilitychange', handler);
|
|
@@ -302,7 +454,7 @@ export function useQuery(options) {
|
|
|
302
454
|
if (refetchInterval) {
|
|
303
455
|
scopedEffect(() => {
|
|
304
456
|
const interval = setInterval(() => {
|
|
305
|
-
|
|
457
|
+
fetchQuery().catch(() => {});
|
|
306
458
|
}, refetchInterval);
|
|
307
459
|
return () => clearInterval(interval);
|
|
308
460
|
});
|
|
@@ -317,7 +469,7 @@ export function useQuery(options) {
|
|
|
317
469
|
isError: () => status() === 'error',
|
|
318
470
|
isSuccess: () => status() === 'success',
|
|
319
471
|
isFetching: () => fetchStatus() === 'fetching',
|
|
320
|
-
refetch:
|
|
472
|
+
refetch: fetchQuery,
|
|
321
473
|
};
|
|
322
474
|
}
|
|
323
475
|
|
|
@@ -342,8 +494,16 @@ export function useInfiniteQuery(options) {
|
|
|
342
494
|
const isFetchingPreviousPage = signal(false);
|
|
343
495
|
|
|
344
496
|
const key = Array.isArray(queryKey) ? queryKey.join(':') : queryKey;
|
|
497
|
+
let abortController = null;
|
|
498
|
+
|
|
499
|
+
let isRefetching = false;
|
|
345
500
|
|
|
346
501
|
async function fetchPage(pageParam, direction = 'next') {
|
|
502
|
+
// Abort previous page fetch
|
|
503
|
+
if (abortController) abortController.abort();
|
|
504
|
+
abortController = new AbortController();
|
|
505
|
+
const { signal: abortSignal } = abortController;
|
|
506
|
+
|
|
347
507
|
const loading = direction === 'next' ? isFetchingNextPage : isFetchingPreviousPage;
|
|
348
508
|
loading.set(true);
|
|
349
509
|
|
|
@@ -351,10 +511,19 @@ export function useInfiniteQuery(options) {
|
|
|
351
511
|
const result = await queryFn({
|
|
352
512
|
queryKey: Array.isArray(queryKey) ? queryKey : [queryKey],
|
|
353
513
|
pageParam,
|
|
514
|
+
signal: abortSignal,
|
|
354
515
|
});
|
|
355
516
|
|
|
517
|
+
if (abortSignal.aborted) return;
|
|
518
|
+
|
|
356
519
|
batch(() => {
|
|
357
|
-
if (
|
|
520
|
+
if (isRefetching) {
|
|
521
|
+
// Refetch: replace all pages with fresh first page (SWR pattern —
|
|
522
|
+
// old pages stayed visible during fetch, now swap atomically)
|
|
523
|
+
pages.set([result]);
|
|
524
|
+
pageParams.set([pageParam]);
|
|
525
|
+
isRefetching = false;
|
|
526
|
+
} else if (direction === 'next') {
|
|
358
527
|
pages.set([...pages.peek(), result]);
|
|
359
528
|
pageParams.set([...pageParams.peek(), pageParam]);
|
|
360
529
|
} else {
|
|
@@ -373,13 +542,16 @@ export function useInfiniteQuery(options) {
|
|
|
373
542
|
|
|
374
543
|
return result;
|
|
375
544
|
} finally {
|
|
376
|
-
loading.set(false);
|
|
545
|
+
if (!abortSignal.aborted) loading.set(false);
|
|
377
546
|
}
|
|
378
547
|
}
|
|
379
548
|
|
|
380
|
-
// Initial fetch
|
|
549
|
+
// Initial fetch, abort on unmount
|
|
381
550
|
scopedEffect(() => {
|
|
382
551
|
fetchPage(initialPageParam).catch(() => {});
|
|
552
|
+
return () => {
|
|
553
|
+
if (abortController) abortController.abort();
|
|
554
|
+
};
|
|
383
555
|
});
|
|
384
556
|
|
|
385
557
|
return {
|
|
@@ -403,8 +575,9 @@ export function useInfiniteQuery(options) {
|
|
|
403
575
|
}
|
|
404
576
|
},
|
|
405
577
|
refetch: async () => {
|
|
406
|
-
pages
|
|
407
|
-
|
|
578
|
+
// Keep old pages visible during refetch (SWR pattern).
|
|
579
|
+
// The fetchPage callback swaps them atomically when data arrives.
|
|
580
|
+
isRefetching = true;
|
|
408
581
|
return fetchPage(initialPageParam);
|
|
409
582
|
},
|
|
410
583
|
};
|
|
@@ -412,33 +585,54 @@ export function useInfiniteQuery(options) {
|
|
|
412
585
|
|
|
413
586
|
// --- Cache Management ---
|
|
414
587
|
|
|
415
|
-
export function invalidateQueries(keyOrPredicate) {
|
|
588
|
+
export function invalidateQueries(keyOrPredicate, options = {}) {
|
|
589
|
+
const { hard = false } = options;
|
|
590
|
+
const keysToInvalidate = [];
|
|
416
591
|
if (typeof keyOrPredicate === 'function') {
|
|
417
|
-
for (const [key] of
|
|
418
|
-
if (keyOrPredicate(key))
|
|
419
|
-
cache.delete(key);
|
|
420
|
-
}
|
|
592
|
+
for (const [key] of cacheSignals) {
|
|
593
|
+
if (keyOrPredicate(key)) keysToInvalidate.push(key);
|
|
421
594
|
}
|
|
422
595
|
} else {
|
|
423
|
-
|
|
596
|
+
keysToInvalidate.push(keyOrPredicate);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
for (const key of keysToInvalidate) {
|
|
600
|
+
// Hard invalidation clears data immediately (shows loading state)
|
|
601
|
+
// Soft invalidation (default) keeps stale data visible during re-fetch (SWR pattern)
|
|
602
|
+
if (hard && cacheSignals.has(key)) cacheSignals.get(key).set(null);
|
|
603
|
+
// Trigger all subscribers to re-fetch
|
|
604
|
+
const subs = revalidationSubscribers.get(key);
|
|
605
|
+
if (subs) {
|
|
606
|
+
for (const revalidate of subs) revalidate();
|
|
607
|
+
}
|
|
424
608
|
}
|
|
425
609
|
}
|
|
426
610
|
|
|
427
611
|
export function prefetchQuery(key, fetcher) {
|
|
612
|
+
const cacheS = getCacheSignal(key);
|
|
428
613
|
return fetcher(key).then(result => {
|
|
429
|
-
|
|
614
|
+
cacheS.set(result);
|
|
615
|
+
cacheTimestamps.set(key, Date.now());
|
|
430
616
|
return result;
|
|
431
617
|
});
|
|
432
618
|
}
|
|
433
619
|
|
|
434
|
-
export function setQueryData(key,
|
|
435
|
-
|
|
620
|
+
export function setQueryData(key, updater) {
|
|
621
|
+
const cacheS = getCacheSignal(key);
|
|
622
|
+
const current = cacheS.peek();
|
|
623
|
+
cacheS.set(typeof updater === 'function' ? updater(current) : updater);
|
|
624
|
+
cacheTimestamps.set(key, Date.now());
|
|
436
625
|
}
|
|
437
626
|
|
|
438
627
|
export function getQueryData(key) {
|
|
439
|
-
return
|
|
628
|
+
return cacheSignals.has(key) ? cacheSignals.get(key).peek() : undefined;
|
|
440
629
|
}
|
|
441
630
|
|
|
442
631
|
export function clearCache() {
|
|
443
|
-
|
|
632
|
+
cacheSignals.clear();
|
|
633
|
+
errorSignals.clear();
|
|
634
|
+
validatingSignals.clear();
|
|
635
|
+
cacheTimestamps.clear();
|
|
636
|
+
lastFetchTimestamps.clear();
|
|
637
|
+
inFlightRequests.clear();
|
|
444
638
|
}
|