what-core 0.2.0 → 0.4.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/dist/a11y.js +22 -7
- package/dist/animation.js +20 -3
- package/dist/components.js +61 -23
- package/dist/data.js +272 -68
- package/dist/dom.js +325 -89
- package/dist/form.js +112 -44
- package/dist/helpers.js +73 -10
- package/dist/hooks.js +75 -22
- package/dist/index.js +6 -2
- package/dist/reactive.js +202 -28
- package/dist/render.js +716 -0
- package/dist/scheduler.js +10 -5
- package/dist/store.js +19 -8
- package/package.json +4 -1
- package/src/a11y.js +22 -7
- package/src/animation.js +20 -3
- package/src/components.js +61 -23
- package/src/data.js +272 -68
- package/src/dom.js +325 -89
- package/src/form.js +112 -44
- package/src/helpers.js +73 -10
- package/src/hooks.js +75 -22
- package/src/index.js +6 -2
- package/src/reactive.js +202 -28
- package/src/render.js +716 -0
- package/src/scheduler.js +10 -5
- package/src/store.js +19 -8
package/dist/data.js
CHANGED
|
@@ -1,11 +1,84 @@
|
|
|
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
|
+
import { getCurrentComponent } from './dom.js';
|
|
6
|
+
|
|
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
|
+
}
|
|
5
70
|
|
|
6
|
-
// Global cache for requests
|
|
7
|
-
const cache = new Map();
|
|
8
71
|
const inFlightRequests = new Map();
|
|
72
|
+
const lastFetchTimestamps = new Map(); // key -> timestamp of last completed fetch
|
|
73
|
+
|
|
74
|
+
// Create an effect scoped to the current component's lifecycle.
|
|
75
|
+
// When the component unmounts, the effect is automatically disposed.
|
|
76
|
+
function scopedEffect(fn) {
|
|
77
|
+
const ctx = getCurrentComponent?.();
|
|
78
|
+
const dispose = effect(fn);
|
|
79
|
+
if (ctx) ctx.effects.push(dispose);
|
|
80
|
+
return dispose;
|
|
81
|
+
}
|
|
9
82
|
|
|
10
83
|
// --- useFetch Hook ---
|
|
11
84
|
// Simple fetch with automatic JSON parsing and error handling
|
|
@@ -22,8 +95,14 @@ export function useFetch(url, options = {}) {
|
|
|
22
95
|
const data = signal(initialData);
|
|
23
96
|
const error = signal(null);
|
|
24
97
|
const isLoading = signal(true);
|
|
98
|
+
let abortController = null;
|
|
25
99
|
|
|
26
100
|
async function fetchData() {
|
|
101
|
+
// Abort previous request
|
|
102
|
+
if (abortController) abortController.abort();
|
|
103
|
+
abortController = new AbortController();
|
|
104
|
+
const { signal: abortSignal } = abortController;
|
|
105
|
+
|
|
27
106
|
isLoading.set(true);
|
|
28
107
|
error.set(null);
|
|
29
108
|
|
|
@@ -35,6 +114,7 @@ export function useFetch(url, options = {}) {
|
|
|
35
114
|
...headers,
|
|
36
115
|
},
|
|
37
116
|
body: body ? JSON.stringify(body) : undefined,
|
|
117
|
+
signal: abortSignal,
|
|
38
118
|
});
|
|
39
119
|
|
|
40
120
|
if (!response.ok) {
|
|
@@ -42,17 +122,26 @@ export function useFetch(url, options = {}) {
|
|
|
42
122
|
}
|
|
43
123
|
|
|
44
124
|
const json = await response.json();
|
|
45
|
-
|
|
125
|
+
if (!abortSignal.aborted) {
|
|
126
|
+
data.set(transform(json));
|
|
127
|
+
}
|
|
46
128
|
} catch (e) {
|
|
47
|
-
|
|
129
|
+
if (!abortSignal.aborted) {
|
|
130
|
+
error.set(e);
|
|
131
|
+
}
|
|
48
132
|
} finally {
|
|
49
|
-
|
|
133
|
+
if (!abortSignal.aborted) {
|
|
134
|
+
isLoading.set(false);
|
|
135
|
+
}
|
|
50
136
|
}
|
|
51
137
|
}
|
|
52
138
|
|
|
53
|
-
// Fetch on mount
|
|
54
|
-
|
|
139
|
+
// Fetch on mount, abort on unmount
|
|
140
|
+
scopedEffect(() => {
|
|
55
141
|
fetchData();
|
|
142
|
+
return () => {
|
|
143
|
+
if (abortController) abortController.abort();
|
|
144
|
+
};
|
|
56
145
|
});
|
|
57
146
|
|
|
58
147
|
return {
|
|
@@ -79,54 +168,96 @@ export function useSWR(key, fetcher, options = {}) {
|
|
|
79
168
|
suspense = false,
|
|
80
169
|
} = options;
|
|
81
170
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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;
|
|
87
195
|
|
|
88
196
|
async function revalidate() {
|
|
89
|
-
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
|
|
199
|
+
// Deduplication: if there's already a request in flight, reuse it
|
|
90
200
|
if (inFlightRequests.has(key)) {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return existingPromise.promise;
|
|
201
|
+
const existing = inFlightRequests.get(key);
|
|
202
|
+
if (now - existing.timestamp < dedupingInterval) {
|
|
203
|
+
return existing.promise;
|
|
95
204
|
}
|
|
96
205
|
}
|
|
97
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
|
+
|
|
98
218
|
isValidating.set(true);
|
|
99
219
|
|
|
100
|
-
const promise = fetcher(key);
|
|
101
|
-
inFlightRequests.set(key, { promise, timestamp:
|
|
220
|
+
const promise = fetcher(key, { signal: abortSignal });
|
|
221
|
+
inFlightRequests.set(key, { promise, timestamp: now });
|
|
102
222
|
|
|
103
223
|
try {
|
|
104
224
|
const result = await promise;
|
|
225
|
+
if (abortSignal.aborted) return;
|
|
105
226
|
batch(() => {
|
|
106
|
-
|
|
227
|
+
cacheS.set(result); // Updates ALL components reading this key
|
|
107
228
|
error.set(null);
|
|
108
|
-
cache.set(key, result);
|
|
109
229
|
});
|
|
230
|
+
cacheTimestamps.set(key, Date.now());
|
|
231
|
+
lastFetchTimestamps.set(key, Date.now());
|
|
110
232
|
if (onSuccess) onSuccess(result, key);
|
|
111
233
|
return result;
|
|
112
234
|
} catch (e) {
|
|
235
|
+
if (abortSignal.aborted) return;
|
|
113
236
|
error.set(e);
|
|
114
237
|
if (onError) onError(e, key);
|
|
115
238
|
throw e;
|
|
116
239
|
} finally {
|
|
117
|
-
isValidating.set(false);
|
|
240
|
+
if (!abortSignal.aborted) isValidating.set(false);
|
|
118
241
|
inFlightRequests.delete(key);
|
|
119
242
|
}
|
|
120
243
|
}
|
|
121
244
|
|
|
245
|
+
// Subscribe to invalidation events for this key
|
|
246
|
+
const unsubscribe = subscribeToKey(key, () => revalidate().catch(() => {}));
|
|
247
|
+
|
|
122
248
|
// Initial fetch
|
|
123
|
-
|
|
249
|
+
scopedEffect(() => {
|
|
124
250
|
revalidate().catch(() => {});
|
|
251
|
+
// Cleanup: abort and unsubscribe on unmount
|
|
252
|
+
return () => {
|
|
253
|
+
if (abortController) abortController.abort();
|
|
254
|
+
unsubscribe();
|
|
255
|
+
};
|
|
125
256
|
});
|
|
126
257
|
|
|
127
258
|
// Revalidate on focus
|
|
128
259
|
if (revalidateOnFocus && typeof window !== 'undefined') {
|
|
129
|
-
|
|
260
|
+
scopedEffect(() => {
|
|
130
261
|
const handler = () => {
|
|
131
262
|
if (document.visibilityState === 'visible') {
|
|
132
263
|
revalidate().catch(() => {});
|
|
@@ -139,7 +270,7 @@ export function useSWR(key, fetcher, options = {}) {
|
|
|
139
270
|
|
|
140
271
|
// Revalidate on reconnect
|
|
141
272
|
if (revalidateOnReconnect && typeof window !== 'undefined') {
|
|
142
|
-
|
|
273
|
+
scopedEffect(() => {
|
|
143
274
|
const handler = () => revalidate().catch(() => {});
|
|
144
275
|
window.addEventListener('online', handler);
|
|
145
276
|
return () => window.removeEventListener('online', handler);
|
|
@@ -148,7 +279,7 @@ export function useSWR(key, fetcher, options = {}) {
|
|
|
148
279
|
|
|
149
280
|
// Polling
|
|
150
281
|
if (refreshInterval > 0) {
|
|
151
|
-
|
|
282
|
+
scopedEffect(() => {
|
|
152
283
|
const interval = setInterval(() => {
|
|
153
284
|
revalidate().catch(() => {});
|
|
154
285
|
}, refreshInterval);
|
|
@@ -159,11 +290,12 @@ export function useSWR(key, fetcher, options = {}) {
|
|
|
159
290
|
return {
|
|
160
291
|
data: () => data(),
|
|
161
292
|
error: () => error(),
|
|
162
|
-
isLoading,
|
|
293
|
+
isLoading: () => isLoading(),
|
|
163
294
|
isValidating: () => isValidating(),
|
|
164
295
|
mutate: (newData, shouldRevalidate = true) => {
|
|
165
|
-
|
|
166
|
-
|
|
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());
|
|
167
299
|
if (shouldRevalidate) {
|
|
168
300
|
revalidate().catch(() => {});
|
|
169
301
|
}
|
|
@@ -195,28 +327,34 @@ export function useQuery(options) {
|
|
|
195
327
|
|
|
196
328
|
const key = Array.isArray(queryKey) ? queryKey.join(':') : queryKey;
|
|
197
329
|
|
|
198
|
-
const
|
|
330
|
+
const cacheS = getCacheSignal(key);
|
|
199
331
|
const data = computed(() => {
|
|
200
|
-
const d =
|
|
332
|
+
const d = cacheS();
|
|
201
333
|
return select && d !== null ? select(d) : d;
|
|
202
334
|
});
|
|
203
|
-
const error =
|
|
204
|
-
const status = signal(
|
|
335
|
+
const error = getErrorSignal(key);
|
|
336
|
+
const status = signal(cacheS.peek() != null ? 'success' : 'loading');
|
|
205
337
|
const fetchStatus = signal('idle');
|
|
206
338
|
|
|
207
339
|
let lastFetchTime = 0;
|
|
340
|
+
let abortController = null;
|
|
208
341
|
|
|
209
|
-
async function
|
|
342
|
+
async function fetchQuery() {
|
|
210
343
|
if (!enabled) return;
|
|
211
344
|
|
|
212
345
|
// Check if data is still fresh
|
|
213
346
|
const now = Date.now();
|
|
214
|
-
if (
|
|
215
|
-
return
|
|
347
|
+
if (cacheS.peek() != null && now - lastFetchTime < staleTime) {
|
|
348
|
+
return cacheS.peek();
|
|
216
349
|
}
|
|
217
350
|
|
|
351
|
+
// Abort previous request
|
|
352
|
+
if (abortController) abortController.abort();
|
|
353
|
+
abortController = new AbortController();
|
|
354
|
+
const { signal: abortSignal } = abortController;
|
|
355
|
+
|
|
218
356
|
fetchStatus.set('fetching');
|
|
219
|
-
if (
|
|
357
|
+
if (cacheS.peek() == null) {
|
|
220
358
|
status.set('loading');
|
|
221
359
|
}
|
|
222
360
|
|
|
@@ -224,31 +362,48 @@ export function useQuery(options) {
|
|
|
224
362
|
|
|
225
363
|
async function attemptFetch() {
|
|
226
364
|
try {
|
|
227
|
-
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;
|
|
228
367
|
batch(() => {
|
|
229
|
-
|
|
368
|
+
cacheS.set(result); // Updates all components reading this key
|
|
230
369
|
error.set(null);
|
|
231
370
|
status.set('success');
|
|
232
371
|
fetchStatus.set('idle');
|
|
233
372
|
});
|
|
234
|
-
cache.set(key, result);
|
|
235
373
|
lastFetchTime = Date.now();
|
|
374
|
+
cacheTimestamps.set(key, Date.now());
|
|
236
375
|
|
|
237
376
|
if (onSuccess) onSuccess(result);
|
|
238
377
|
if (onSettled) onSettled(result, null);
|
|
239
378
|
|
|
240
|
-
// Schedule cache cleanup
|
|
379
|
+
// Schedule cache cleanup (only if no active subscribers)
|
|
241
380
|
setTimeout(() => {
|
|
242
381
|
if (Date.now() - lastFetchTime >= cacheTime) {
|
|
243
|
-
|
|
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
|
+
}
|
|
244
390
|
}
|
|
245
391
|
}, cacheTime);
|
|
246
392
|
|
|
247
393
|
return result;
|
|
248
394
|
} catch (e) {
|
|
395
|
+
if (abortSignal.aborted) return;
|
|
249
396
|
attempts++;
|
|
250
397
|
if (attempts < retry) {
|
|
251
|
-
|
|
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;
|
|
252
407
|
return attemptFetch();
|
|
253
408
|
}
|
|
254
409
|
|
|
@@ -268,19 +423,26 @@ export function useQuery(options) {
|
|
|
268
423
|
return attemptFetch();
|
|
269
424
|
}
|
|
270
425
|
|
|
426
|
+
// Subscribe to invalidation events for this key
|
|
427
|
+
const unsubscribe = subscribeToKey(key, () => fetchQuery().catch(() => {}));
|
|
428
|
+
|
|
271
429
|
// Initial fetch
|
|
272
|
-
|
|
430
|
+
scopedEffect(() => {
|
|
273
431
|
if (enabled) {
|
|
274
|
-
|
|
432
|
+
fetchQuery().catch(() => {});
|
|
275
433
|
}
|
|
434
|
+
return () => {
|
|
435
|
+
if (abortController) abortController.abort();
|
|
436
|
+
unsubscribe();
|
|
437
|
+
};
|
|
276
438
|
});
|
|
277
439
|
|
|
278
440
|
// Refetch on focus
|
|
279
441
|
if (refetchOnWindowFocus && typeof window !== 'undefined') {
|
|
280
|
-
|
|
442
|
+
scopedEffect(() => {
|
|
281
443
|
const handler = () => {
|
|
282
444
|
if (document.visibilityState === 'visible') {
|
|
283
|
-
|
|
445
|
+
fetchQuery().catch(() => {});
|
|
284
446
|
}
|
|
285
447
|
};
|
|
286
448
|
document.addEventListener('visibilitychange', handler);
|
|
@@ -290,9 +452,9 @@ export function useQuery(options) {
|
|
|
290
452
|
|
|
291
453
|
// Polling
|
|
292
454
|
if (refetchInterval) {
|
|
293
|
-
|
|
455
|
+
scopedEffect(() => {
|
|
294
456
|
const interval = setInterval(() => {
|
|
295
|
-
|
|
457
|
+
fetchQuery().catch(() => {});
|
|
296
458
|
}, refetchInterval);
|
|
297
459
|
return () => clearInterval(interval);
|
|
298
460
|
});
|
|
@@ -307,7 +469,7 @@ export function useQuery(options) {
|
|
|
307
469
|
isError: () => status() === 'error',
|
|
308
470
|
isSuccess: () => status() === 'success',
|
|
309
471
|
isFetching: () => fetchStatus() === 'fetching',
|
|
310
|
-
refetch:
|
|
472
|
+
refetch: fetchQuery,
|
|
311
473
|
};
|
|
312
474
|
}
|
|
313
475
|
|
|
@@ -332,8 +494,16 @@ export function useInfiniteQuery(options) {
|
|
|
332
494
|
const isFetchingPreviousPage = signal(false);
|
|
333
495
|
|
|
334
496
|
const key = Array.isArray(queryKey) ? queryKey.join(':') : queryKey;
|
|
497
|
+
let abortController = null;
|
|
498
|
+
|
|
499
|
+
let isRefetching = false;
|
|
335
500
|
|
|
336
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
|
+
|
|
337
507
|
const loading = direction === 'next' ? isFetchingNextPage : isFetchingPreviousPage;
|
|
338
508
|
loading.set(true);
|
|
339
509
|
|
|
@@ -341,10 +511,19 @@ export function useInfiniteQuery(options) {
|
|
|
341
511
|
const result = await queryFn({
|
|
342
512
|
queryKey: Array.isArray(queryKey) ? queryKey : [queryKey],
|
|
343
513
|
pageParam,
|
|
514
|
+
signal: abortSignal,
|
|
344
515
|
});
|
|
345
516
|
|
|
517
|
+
if (abortSignal.aborted) return;
|
|
518
|
+
|
|
346
519
|
batch(() => {
|
|
347
|
-
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') {
|
|
348
527
|
pages.set([...pages.peek(), result]);
|
|
349
528
|
pageParams.set([...pageParams.peek(), pageParam]);
|
|
350
529
|
} else {
|
|
@@ -363,13 +542,16 @@ export function useInfiniteQuery(options) {
|
|
|
363
542
|
|
|
364
543
|
return result;
|
|
365
544
|
} finally {
|
|
366
|
-
loading.set(false);
|
|
545
|
+
if (!abortSignal.aborted) loading.set(false);
|
|
367
546
|
}
|
|
368
547
|
}
|
|
369
548
|
|
|
370
|
-
// Initial fetch
|
|
371
|
-
|
|
549
|
+
// Initial fetch, abort on unmount
|
|
550
|
+
scopedEffect(() => {
|
|
372
551
|
fetchPage(initialPageParam).catch(() => {});
|
|
552
|
+
return () => {
|
|
553
|
+
if (abortController) abortController.abort();
|
|
554
|
+
};
|
|
373
555
|
});
|
|
374
556
|
|
|
375
557
|
return {
|
|
@@ -393,8 +575,9 @@ export function useInfiniteQuery(options) {
|
|
|
393
575
|
}
|
|
394
576
|
},
|
|
395
577
|
refetch: async () => {
|
|
396
|
-
pages
|
|
397
|
-
|
|
578
|
+
// Keep old pages visible during refetch (SWR pattern).
|
|
579
|
+
// The fetchPage callback swaps them atomically when data arrives.
|
|
580
|
+
isRefetching = true;
|
|
398
581
|
return fetchPage(initialPageParam);
|
|
399
582
|
},
|
|
400
583
|
};
|
|
@@ -402,33 +585,54 @@ export function useInfiniteQuery(options) {
|
|
|
402
585
|
|
|
403
586
|
// --- Cache Management ---
|
|
404
587
|
|
|
405
|
-
export function invalidateQueries(keyOrPredicate) {
|
|
588
|
+
export function invalidateQueries(keyOrPredicate, options = {}) {
|
|
589
|
+
const { hard = false } = options;
|
|
590
|
+
const keysToInvalidate = [];
|
|
406
591
|
if (typeof keyOrPredicate === 'function') {
|
|
407
|
-
for (const [key] of
|
|
408
|
-
if (keyOrPredicate(key))
|
|
409
|
-
cache.delete(key);
|
|
410
|
-
}
|
|
592
|
+
for (const [key] of cacheSignals) {
|
|
593
|
+
if (keyOrPredicate(key)) keysToInvalidate.push(key);
|
|
411
594
|
}
|
|
412
595
|
} else {
|
|
413
|
-
|
|
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
|
+
}
|
|
414
608
|
}
|
|
415
609
|
}
|
|
416
610
|
|
|
417
611
|
export function prefetchQuery(key, fetcher) {
|
|
612
|
+
const cacheS = getCacheSignal(key);
|
|
418
613
|
return fetcher(key).then(result => {
|
|
419
|
-
|
|
614
|
+
cacheS.set(result);
|
|
615
|
+
cacheTimestamps.set(key, Date.now());
|
|
420
616
|
return result;
|
|
421
617
|
});
|
|
422
618
|
}
|
|
423
619
|
|
|
424
|
-
export function setQueryData(key,
|
|
425
|
-
|
|
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());
|
|
426
625
|
}
|
|
427
626
|
|
|
428
627
|
export function getQueryData(key) {
|
|
429
|
-
return
|
|
628
|
+
return cacheSignals.has(key) ? cacheSignals.get(key).peek() : undefined;
|
|
430
629
|
}
|
|
431
630
|
|
|
432
631
|
export function clearCache() {
|
|
433
|
-
|
|
632
|
+
cacheSignals.clear();
|
|
633
|
+
errorSignals.clear();
|
|
634
|
+
validatingSignals.clear();
|
|
635
|
+
cacheTimestamps.clear();
|
|
636
|
+
lastFetchTimestamps.clear();
|
|
637
|
+
inFlightRequests.clear();
|
|
434
638
|
}
|