what-core 0.6.1 → 0.6.3
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/README.md +2 -0
- package/compiler.d.ts +30 -0
- package/devtools.d.ts +2 -0
- package/dist/compiler.js +1787 -0
- package/dist/compiler.js.map +7 -0
- package/dist/compiler.min.js +2 -0
- package/dist/compiler.min.js.map +7 -0
- package/dist/devtools.js +10 -0
- package/dist/devtools.js.map +7 -0
- package/dist/devtools.min.js +2 -0
- package/dist/devtools.min.js.map +7 -0
- package/dist/index.js +331 -382
- package/dist/index.js.map +4 -4
- package/dist/index.min.js +62 -62
- package/dist/index.min.js.map +4 -4
- package/dist/render.js +263 -21
- package/dist/render.js.map +4 -4
- package/dist/render.min.js +58 -1
- package/dist/render.min.js.map +4 -4
- package/dist/testing.js +3 -0
- package/dist/testing.js.map +2 -2
- package/dist/testing.min.js +1 -1
- package/dist/testing.min.js.map +2 -2
- package/index.d.ts +176 -1
- package/jsx-runtime.d.ts +622 -0
- package/package.json +20 -2
- package/src/agent-context.js +1 -1
- package/src/compiler.js +18 -0
- package/src/components.js +73 -27
- package/src/devtools.js +4 -0
- package/src/dom.js +7 -0
- package/src/guardrails.js +3 -4
- package/src/hooks.js +0 -11
- package/src/index.js +5 -9
- package/src/render.js +94 -24
- package/dist/a11y.js +0 -440
- package/dist/animation.js +0 -548
- package/dist/components.js +0 -229
- package/dist/data.js +0 -638
- package/dist/dom.js +0 -439
- package/dist/form.js +0 -509
- package/dist/h.js +0 -152
- package/dist/head.js +0 -51
- package/dist/helpers.js +0 -140
- package/dist/hooks.js +0 -210
- package/dist/reactive.js +0 -432
- package/dist/scheduler.js +0 -246
- package/dist/skeleton.js +0 -363
- package/dist/store.js +0 -83
- package/dist/what.js +0 -117
package/dist/data.js
DELETED
|
@@ -1,638 +0,0 @@
|
|
|
1
|
-
// What Framework - Data Fetching
|
|
2
|
-
// SWR-like data fetching with caching, revalidation, and optimistic updates
|
|
3
|
-
|
|
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
|
-
}
|
|
70
|
-
|
|
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
|
-
}
|
|
82
|
-
|
|
83
|
-
// --- useFetch Hook ---
|
|
84
|
-
// Simple fetch with automatic JSON parsing and error handling
|
|
85
|
-
|
|
86
|
-
export function useFetch(url, options = {}) {
|
|
87
|
-
const {
|
|
88
|
-
method = 'GET',
|
|
89
|
-
body,
|
|
90
|
-
headers = {},
|
|
91
|
-
transform = (data) => data,
|
|
92
|
-
initialData = null,
|
|
93
|
-
} = options;
|
|
94
|
-
|
|
95
|
-
const data = signal(initialData);
|
|
96
|
-
const error = signal(null);
|
|
97
|
-
const isLoading = signal(true);
|
|
98
|
-
let abortController = null;
|
|
99
|
-
|
|
100
|
-
async function fetchData() {
|
|
101
|
-
// Abort previous request
|
|
102
|
-
if (abortController) abortController.abort();
|
|
103
|
-
abortController = new AbortController();
|
|
104
|
-
const { signal: abortSignal } = abortController;
|
|
105
|
-
|
|
106
|
-
isLoading.set(true);
|
|
107
|
-
error.set(null);
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
const response = await fetch(url, {
|
|
111
|
-
method,
|
|
112
|
-
headers: {
|
|
113
|
-
'Content-Type': 'application/json',
|
|
114
|
-
...headers,
|
|
115
|
-
},
|
|
116
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
117
|
-
signal: abortSignal,
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
if (!response.ok) {
|
|
121
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const json = await response.json();
|
|
125
|
-
if (!abortSignal.aborted) {
|
|
126
|
-
data.set(transform(json));
|
|
127
|
-
}
|
|
128
|
-
} catch (e) {
|
|
129
|
-
if (!abortSignal.aborted) {
|
|
130
|
-
error.set(e);
|
|
131
|
-
}
|
|
132
|
-
} finally {
|
|
133
|
-
if (!abortSignal.aborted) {
|
|
134
|
-
isLoading.set(false);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Fetch on mount, abort on unmount
|
|
140
|
-
scopedEffect(() => {
|
|
141
|
-
fetchData();
|
|
142
|
-
return () => {
|
|
143
|
-
if (abortController) abortController.abort();
|
|
144
|
-
};
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
return {
|
|
148
|
-
data: () => data(),
|
|
149
|
-
error: () => error(),
|
|
150
|
-
isLoading: () => isLoading(),
|
|
151
|
-
refetch: fetchData,
|
|
152
|
-
mutate: (newData) => data.set(newData),
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// --- useSWR Hook ---
|
|
157
|
-
// Stale-while-revalidate pattern with caching
|
|
158
|
-
|
|
159
|
-
export function useSWR(key, fetcher, options = {}) {
|
|
160
|
-
const {
|
|
161
|
-
revalidateOnFocus = true,
|
|
162
|
-
revalidateOnReconnect = true,
|
|
163
|
-
refreshInterval = 0,
|
|
164
|
-
dedupingInterval = 2000,
|
|
165
|
-
fallbackData,
|
|
166
|
-
onSuccess,
|
|
167
|
-
onError,
|
|
168
|
-
suspense = false,
|
|
169
|
-
} = options;
|
|
170
|
-
|
|
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;
|
|
195
|
-
|
|
196
|
-
async function revalidate() {
|
|
197
|
-
const now = Date.now();
|
|
198
|
-
|
|
199
|
-
// Deduplication: if there's already a request in flight, reuse it
|
|
200
|
-
if (inFlightRequests.has(key)) {
|
|
201
|
-
const existing = inFlightRequests.get(key);
|
|
202
|
-
if (now - existing.timestamp < dedupingInterval) {
|
|
203
|
-
return existing.promise;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
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
|
-
|
|
218
|
-
isValidating.set(true);
|
|
219
|
-
|
|
220
|
-
const promise = fetcher(key, { signal: abortSignal });
|
|
221
|
-
inFlightRequests.set(key, { promise, timestamp: now });
|
|
222
|
-
|
|
223
|
-
try {
|
|
224
|
-
const result = await promise;
|
|
225
|
-
if (abortSignal.aborted) return;
|
|
226
|
-
batch(() => {
|
|
227
|
-
cacheS.set(result); // Updates ALL components reading this key
|
|
228
|
-
error.set(null);
|
|
229
|
-
});
|
|
230
|
-
cacheTimestamps.set(key, Date.now());
|
|
231
|
-
lastFetchTimestamps.set(key, Date.now());
|
|
232
|
-
if (onSuccess) onSuccess(result, key);
|
|
233
|
-
return result;
|
|
234
|
-
} catch (e) {
|
|
235
|
-
if (abortSignal.aborted) return;
|
|
236
|
-
error.set(e);
|
|
237
|
-
if (onError) onError(e, key);
|
|
238
|
-
throw e;
|
|
239
|
-
} finally {
|
|
240
|
-
if (!abortSignal.aborted) isValidating.set(false);
|
|
241
|
-
inFlightRequests.delete(key);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Subscribe to invalidation events for this key
|
|
246
|
-
const unsubscribe = subscribeToKey(key, () => revalidate().catch(() => {}));
|
|
247
|
-
|
|
248
|
-
// Initial fetch
|
|
249
|
-
scopedEffect(() => {
|
|
250
|
-
revalidate().catch(() => {});
|
|
251
|
-
// Cleanup: abort and unsubscribe on unmount
|
|
252
|
-
return () => {
|
|
253
|
-
if (abortController) abortController.abort();
|
|
254
|
-
unsubscribe();
|
|
255
|
-
};
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
// Revalidate on focus
|
|
259
|
-
if (revalidateOnFocus && typeof window !== 'undefined') {
|
|
260
|
-
scopedEffect(() => {
|
|
261
|
-
const handler = () => {
|
|
262
|
-
if (document.visibilityState === 'visible') {
|
|
263
|
-
revalidate().catch(() => {});
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
document.addEventListener('visibilitychange', handler);
|
|
267
|
-
return () => document.removeEventListener('visibilitychange', handler);
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Revalidate on reconnect
|
|
272
|
-
if (revalidateOnReconnect && typeof window !== 'undefined') {
|
|
273
|
-
scopedEffect(() => {
|
|
274
|
-
const handler = () => revalidate().catch(() => {});
|
|
275
|
-
window.addEventListener('online', handler);
|
|
276
|
-
return () => window.removeEventListener('online', handler);
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Polling
|
|
281
|
-
if (refreshInterval > 0) {
|
|
282
|
-
scopedEffect(() => {
|
|
283
|
-
const interval = setInterval(() => {
|
|
284
|
-
revalidate().catch(() => {});
|
|
285
|
-
}, refreshInterval);
|
|
286
|
-
return () => clearInterval(interval);
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
return {
|
|
291
|
-
data: () => data(),
|
|
292
|
-
error: () => error(),
|
|
293
|
-
isLoading: () => isLoading(),
|
|
294
|
-
isValidating: () => isValidating(),
|
|
295
|
-
mutate: (newData, shouldRevalidate = true) => {
|
|
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());
|
|
299
|
-
if (shouldRevalidate) {
|
|
300
|
-
revalidate().catch(() => {});
|
|
301
|
-
}
|
|
302
|
-
},
|
|
303
|
-
revalidate,
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// --- useQuery Hook ---
|
|
308
|
-
// TanStack Query-like API
|
|
309
|
-
|
|
310
|
-
export function useQuery(options) {
|
|
311
|
-
const {
|
|
312
|
-
queryKey,
|
|
313
|
-
queryFn,
|
|
314
|
-
enabled = true,
|
|
315
|
-
staleTime = 0,
|
|
316
|
-
cacheTime = 5 * 60 * 1000,
|
|
317
|
-
refetchOnWindowFocus = true,
|
|
318
|
-
refetchInterval = false,
|
|
319
|
-
retry = 3,
|
|
320
|
-
retryDelay = (attempt) => Math.min(1000 * 2 ** attempt, 30000),
|
|
321
|
-
onSuccess,
|
|
322
|
-
onError,
|
|
323
|
-
onSettled,
|
|
324
|
-
select,
|
|
325
|
-
placeholderData,
|
|
326
|
-
} = options;
|
|
327
|
-
|
|
328
|
-
const key = Array.isArray(queryKey) ? queryKey.join(':') : queryKey;
|
|
329
|
-
|
|
330
|
-
const cacheS = getCacheSignal(key);
|
|
331
|
-
const data = computed(() => {
|
|
332
|
-
const d = cacheS();
|
|
333
|
-
return select && d !== null ? select(d) : d;
|
|
334
|
-
});
|
|
335
|
-
const error = getErrorSignal(key);
|
|
336
|
-
const status = signal(cacheS.peek() != null ? 'success' : 'loading');
|
|
337
|
-
const fetchStatus = signal('idle');
|
|
338
|
-
|
|
339
|
-
let lastFetchTime = 0;
|
|
340
|
-
let abortController = null;
|
|
341
|
-
|
|
342
|
-
async function fetchQuery() {
|
|
343
|
-
if (!enabled) return;
|
|
344
|
-
|
|
345
|
-
// Check if data is still fresh
|
|
346
|
-
const now = Date.now();
|
|
347
|
-
if (cacheS.peek() != null && now - lastFetchTime < staleTime) {
|
|
348
|
-
return cacheS.peek();
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Abort previous request
|
|
352
|
-
if (abortController) abortController.abort();
|
|
353
|
-
abortController = new AbortController();
|
|
354
|
-
const { signal: abortSignal } = abortController;
|
|
355
|
-
|
|
356
|
-
fetchStatus.set('fetching');
|
|
357
|
-
if (cacheS.peek() == null) {
|
|
358
|
-
status.set('loading');
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
let attempts = 0;
|
|
362
|
-
|
|
363
|
-
async function attemptFetch() {
|
|
364
|
-
try {
|
|
365
|
-
const result = await queryFn({ queryKey: Array.isArray(queryKey) ? queryKey : [queryKey], signal: abortSignal });
|
|
366
|
-
if (abortSignal.aborted) return;
|
|
367
|
-
batch(() => {
|
|
368
|
-
cacheS.set(result); // Updates all components reading this key
|
|
369
|
-
error.set(null);
|
|
370
|
-
status.set('success');
|
|
371
|
-
fetchStatus.set('idle');
|
|
372
|
-
});
|
|
373
|
-
lastFetchTime = Date.now();
|
|
374
|
-
cacheTimestamps.set(key, Date.now());
|
|
375
|
-
|
|
376
|
-
if (onSuccess) onSuccess(result);
|
|
377
|
-
if (onSettled) onSettled(result, null);
|
|
378
|
-
|
|
379
|
-
// Schedule cache cleanup (only if no active subscribers)
|
|
380
|
-
setTimeout(() => {
|
|
381
|
-
if (Date.now() - lastFetchTime >= cacheTime) {
|
|
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
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}, cacheTime);
|
|
392
|
-
|
|
393
|
-
return result;
|
|
394
|
-
} catch (e) {
|
|
395
|
-
if (abortSignal.aborted) return;
|
|
396
|
-
attempts++;
|
|
397
|
-
if (attempts < retry) {
|
|
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;
|
|
407
|
-
return attemptFetch();
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
batch(() => {
|
|
411
|
-
error.set(e);
|
|
412
|
-
status.set('error');
|
|
413
|
-
fetchStatus.set('idle');
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
if (onError) onError(e);
|
|
417
|
-
if (onSettled) onSettled(null, e);
|
|
418
|
-
|
|
419
|
-
throw e;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
return attemptFetch();
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// Subscribe to invalidation events for this key
|
|
427
|
-
const unsubscribe = subscribeToKey(key, () => fetchQuery().catch(() => {}));
|
|
428
|
-
|
|
429
|
-
// Initial fetch
|
|
430
|
-
scopedEffect(() => {
|
|
431
|
-
if (enabled) {
|
|
432
|
-
fetchQuery().catch(() => {});
|
|
433
|
-
}
|
|
434
|
-
return () => {
|
|
435
|
-
if (abortController) abortController.abort();
|
|
436
|
-
unsubscribe();
|
|
437
|
-
};
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
// Refetch on focus
|
|
441
|
-
if (refetchOnWindowFocus && typeof window !== 'undefined') {
|
|
442
|
-
scopedEffect(() => {
|
|
443
|
-
const handler = () => {
|
|
444
|
-
if (document.visibilityState === 'visible') {
|
|
445
|
-
fetchQuery().catch(() => {});
|
|
446
|
-
}
|
|
447
|
-
};
|
|
448
|
-
document.addEventListener('visibilitychange', handler);
|
|
449
|
-
return () => document.removeEventListener('visibilitychange', handler);
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Polling
|
|
454
|
-
if (refetchInterval) {
|
|
455
|
-
scopedEffect(() => {
|
|
456
|
-
const interval = setInterval(() => {
|
|
457
|
-
fetchQuery().catch(() => {});
|
|
458
|
-
}, refetchInterval);
|
|
459
|
-
return () => clearInterval(interval);
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
return {
|
|
464
|
-
data: () => data() ?? placeholderData,
|
|
465
|
-
error: () => error(),
|
|
466
|
-
status: () => status(),
|
|
467
|
-
fetchStatus: () => fetchStatus(),
|
|
468
|
-
isLoading: () => status() === 'loading',
|
|
469
|
-
isError: () => status() === 'error',
|
|
470
|
-
isSuccess: () => status() === 'success',
|
|
471
|
-
isFetching: () => fetchStatus() === 'fetching',
|
|
472
|
-
refetch: fetchQuery,
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// --- useInfiniteQuery Hook ---
|
|
477
|
-
// For paginated/infinite scroll data
|
|
478
|
-
|
|
479
|
-
export function useInfiniteQuery(options) {
|
|
480
|
-
const {
|
|
481
|
-
queryKey,
|
|
482
|
-
queryFn,
|
|
483
|
-
getNextPageParam,
|
|
484
|
-
getPreviousPageParam,
|
|
485
|
-
initialPageParam,
|
|
486
|
-
...rest
|
|
487
|
-
} = options;
|
|
488
|
-
|
|
489
|
-
const pages = signal([]);
|
|
490
|
-
const pageParams = signal([initialPageParam]);
|
|
491
|
-
const hasNextPage = signal(true);
|
|
492
|
-
const hasPreviousPage = signal(false);
|
|
493
|
-
const isFetchingNextPage = signal(false);
|
|
494
|
-
const isFetchingPreviousPage = signal(false);
|
|
495
|
-
|
|
496
|
-
const key = Array.isArray(queryKey) ? queryKey.join(':') : queryKey;
|
|
497
|
-
let abortController = null;
|
|
498
|
-
|
|
499
|
-
let isRefetching = false;
|
|
500
|
-
|
|
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
|
-
|
|
507
|
-
const loading = direction === 'next' ? isFetchingNextPage : isFetchingPreviousPage;
|
|
508
|
-
loading.set(true);
|
|
509
|
-
|
|
510
|
-
try {
|
|
511
|
-
const result = await queryFn({
|
|
512
|
-
queryKey: Array.isArray(queryKey) ? queryKey : [queryKey],
|
|
513
|
-
pageParam,
|
|
514
|
-
signal: abortSignal,
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
if (abortSignal.aborted) return;
|
|
518
|
-
|
|
519
|
-
batch(() => {
|
|
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') {
|
|
527
|
-
pages.set([...pages.peek(), result]);
|
|
528
|
-
pageParams.set([...pageParams.peek(), pageParam]);
|
|
529
|
-
} else {
|
|
530
|
-
pages.set([result, ...pages.peek()]);
|
|
531
|
-
pageParams.set([pageParam, ...pageParams.peek()]);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
const nextParam = getNextPageParam?.(result, pages.peek());
|
|
535
|
-
hasNextPage.set(nextParam !== undefined);
|
|
536
|
-
|
|
537
|
-
if (getPreviousPageParam) {
|
|
538
|
-
const prevParam = getPreviousPageParam(result, pages.peek());
|
|
539
|
-
hasPreviousPage.set(prevParam !== undefined);
|
|
540
|
-
}
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
return result;
|
|
544
|
-
} finally {
|
|
545
|
-
if (!abortSignal.aborted) loading.set(false);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// Initial fetch, abort on unmount
|
|
550
|
-
scopedEffect(() => {
|
|
551
|
-
fetchPage(initialPageParam).catch(() => {});
|
|
552
|
-
return () => {
|
|
553
|
-
if (abortController) abortController.abort();
|
|
554
|
-
};
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
return {
|
|
558
|
-
data: () => ({ pages: pages(), pageParams: pageParams() }),
|
|
559
|
-
hasNextPage: () => hasNextPage(),
|
|
560
|
-
hasPreviousPage: () => hasPreviousPage(),
|
|
561
|
-
isFetchingNextPage: () => isFetchingNextPage(),
|
|
562
|
-
isFetchingPreviousPage: () => isFetchingPreviousPage(),
|
|
563
|
-
fetchNextPage: async () => {
|
|
564
|
-
const lastPage = pages.peek()[pages.peek().length - 1];
|
|
565
|
-
const nextParam = getNextPageParam?.(lastPage, pages.peek());
|
|
566
|
-
if (nextParam !== undefined) {
|
|
567
|
-
return fetchPage(nextParam, 'next');
|
|
568
|
-
}
|
|
569
|
-
},
|
|
570
|
-
fetchPreviousPage: async () => {
|
|
571
|
-
const firstPage = pages.peek()[0];
|
|
572
|
-
const prevParam = getPreviousPageParam?.(firstPage, pages.peek());
|
|
573
|
-
if (prevParam !== undefined) {
|
|
574
|
-
return fetchPage(prevParam, 'previous');
|
|
575
|
-
}
|
|
576
|
-
},
|
|
577
|
-
refetch: async () => {
|
|
578
|
-
// Keep old pages visible during refetch (SWR pattern).
|
|
579
|
-
// The fetchPage callback swaps them atomically when data arrives.
|
|
580
|
-
isRefetching = true;
|
|
581
|
-
return fetchPage(initialPageParam);
|
|
582
|
-
},
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// --- Cache Management ---
|
|
587
|
-
|
|
588
|
-
export function invalidateQueries(keyOrPredicate, options = {}) {
|
|
589
|
-
const { hard = false } = options;
|
|
590
|
-
const keysToInvalidate = [];
|
|
591
|
-
if (typeof keyOrPredicate === 'function') {
|
|
592
|
-
for (const [key] of cacheSignals) {
|
|
593
|
-
if (keyOrPredicate(key)) keysToInvalidate.push(key);
|
|
594
|
-
}
|
|
595
|
-
} else {
|
|
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
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
export function prefetchQuery(key, fetcher) {
|
|
612
|
-
const cacheS = getCacheSignal(key);
|
|
613
|
-
return fetcher(key).then(result => {
|
|
614
|
-
cacheS.set(result);
|
|
615
|
-
cacheTimestamps.set(key, Date.now());
|
|
616
|
-
return result;
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
|
|
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());
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
export function getQueryData(key) {
|
|
628
|
-
return cacheSignals.has(key) ? cacheSignals.get(key).peek() : undefined;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
export function clearCache() {
|
|
632
|
-
cacheSignals.clear();
|
|
633
|
-
errorSignals.clear();
|
|
634
|
-
validatingSignals.clear();
|
|
635
|
-
cacheTimestamps.clear();
|
|
636
|
-
lastFetchTimestamps.clear();
|
|
637
|
-
inFlightRequests.clear();
|
|
638
|
-
}
|