what-core 0.1.1 → 0.3.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 +425 -0
- package/dist/animation.js +540 -0
- package/dist/components.js +272 -115
- package/dist/data.js +444 -0
- package/dist/dom.js +702 -427
- package/dist/form.js +441 -0
- package/dist/h.js +191 -138
- package/dist/head.js +59 -42
- package/dist/helpers.js +125 -83
- package/dist/hooks.js +226 -124
- package/dist/index.js +2 -2
- package/dist/reactive.js +165 -108
- package/dist/scheduler.js +241 -0
- package/dist/skeleton.js +363 -0
- package/dist/store.js +114 -55
- package/dist/testing.js +367 -0
- package/dist/what.js +2 -2
- package/index.d.ts +15 -0
- package/package.json +1 -1
- package/src/animation.js +11 -2
- package/src/components.js +93 -0
- package/src/data.js +19 -9
- package/src/dom.js +181 -85
- package/src/hooks.js +22 -10
- package/src/index.js +2 -2
- package/src/reactive.js +15 -1
- package/src/store.js +24 -5
package/dist/data.js
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
// What Framework - Data Fetching
|
|
2
|
+
// SWR-like data fetching with caching, revalidation, and optimistic updates
|
|
3
|
+
|
|
4
|
+
import { signal, effect, batch, computed } from './reactive.js';
|
|
5
|
+
import { getCurrentComponent } from './dom.js';
|
|
6
|
+
|
|
7
|
+
// Global cache for requests
|
|
8
|
+
const cache = new Map();
|
|
9
|
+
const inFlightRequests = new Map();
|
|
10
|
+
|
|
11
|
+
// Create an effect scoped to the current component's lifecycle.
|
|
12
|
+
// When the component unmounts, the effect is automatically disposed.
|
|
13
|
+
function scopedEffect(fn) {
|
|
14
|
+
const ctx = getCurrentComponent?.();
|
|
15
|
+
const dispose = effect(fn);
|
|
16
|
+
if (ctx) ctx.effects.push(dispose);
|
|
17
|
+
return dispose;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// --- useFetch Hook ---
|
|
21
|
+
// Simple fetch with automatic JSON parsing and error handling
|
|
22
|
+
|
|
23
|
+
export function useFetch(url, options = {}) {
|
|
24
|
+
const {
|
|
25
|
+
method = 'GET',
|
|
26
|
+
body,
|
|
27
|
+
headers = {},
|
|
28
|
+
transform = (data) => data,
|
|
29
|
+
initialData = null,
|
|
30
|
+
} = options;
|
|
31
|
+
|
|
32
|
+
const data = signal(initialData);
|
|
33
|
+
const error = signal(null);
|
|
34
|
+
const isLoading = signal(true);
|
|
35
|
+
|
|
36
|
+
async function fetchData() {
|
|
37
|
+
isLoading.set(true);
|
|
38
|
+
error.set(null);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch(url, {
|
|
42
|
+
method,
|
|
43
|
+
headers: {
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
...headers,
|
|
46
|
+
},
|
|
47
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const json = await response.json();
|
|
55
|
+
data.set(transform(json));
|
|
56
|
+
} catch (e) {
|
|
57
|
+
error.set(e);
|
|
58
|
+
} finally {
|
|
59
|
+
isLoading.set(false);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Fetch on mount
|
|
64
|
+
scopedEffect(() => {
|
|
65
|
+
fetchData();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
data: () => data(),
|
|
70
|
+
error: () => error(),
|
|
71
|
+
isLoading: () => isLoading(),
|
|
72
|
+
refetch: fetchData,
|
|
73
|
+
mutate: (newData) => data.set(newData),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --- useSWR Hook ---
|
|
78
|
+
// Stale-while-revalidate pattern with caching
|
|
79
|
+
|
|
80
|
+
export function useSWR(key, fetcher, options = {}) {
|
|
81
|
+
const {
|
|
82
|
+
revalidateOnFocus = true,
|
|
83
|
+
revalidateOnReconnect = true,
|
|
84
|
+
refreshInterval = 0,
|
|
85
|
+
dedupingInterval = 2000,
|
|
86
|
+
fallbackData,
|
|
87
|
+
onSuccess,
|
|
88
|
+
onError,
|
|
89
|
+
suspense = false,
|
|
90
|
+
} = options;
|
|
91
|
+
|
|
92
|
+
// Reactive state
|
|
93
|
+
const data = signal(cache.get(key) || fallbackData || null);
|
|
94
|
+
const error = signal(null);
|
|
95
|
+
const isValidating = signal(false);
|
|
96
|
+
const isLoading = computed(() => !data() && isValidating());
|
|
97
|
+
|
|
98
|
+
async function revalidate() {
|
|
99
|
+
// Deduplication: if there's already a request in flight, wait for it
|
|
100
|
+
if (inFlightRequests.has(key)) {
|
|
101
|
+
const existingPromise = inFlightRequests.get(key);
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
if (now - existingPromise.timestamp < dedupingInterval) {
|
|
104
|
+
return existingPromise.promise;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
isValidating.set(true);
|
|
109
|
+
|
|
110
|
+
const promise = fetcher(key);
|
|
111
|
+
inFlightRequests.set(key, { promise, timestamp: Date.now() });
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const result = await promise;
|
|
115
|
+
batch(() => {
|
|
116
|
+
data.set(result);
|
|
117
|
+
error.set(null);
|
|
118
|
+
cache.set(key, result);
|
|
119
|
+
});
|
|
120
|
+
if (onSuccess) onSuccess(result, key);
|
|
121
|
+
return result;
|
|
122
|
+
} catch (e) {
|
|
123
|
+
error.set(e);
|
|
124
|
+
if (onError) onError(e, key);
|
|
125
|
+
throw e;
|
|
126
|
+
} finally {
|
|
127
|
+
isValidating.set(false);
|
|
128
|
+
inFlightRequests.delete(key);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Initial fetch
|
|
133
|
+
scopedEffect(() => {
|
|
134
|
+
revalidate().catch(() => {});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Revalidate on focus
|
|
138
|
+
if (revalidateOnFocus && typeof window !== 'undefined') {
|
|
139
|
+
scopedEffect(() => {
|
|
140
|
+
const handler = () => {
|
|
141
|
+
if (document.visibilityState === 'visible') {
|
|
142
|
+
revalidate().catch(() => {});
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
document.addEventListener('visibilitychange', handler);
|
|
146
|
+
return () => document.removeEventListener('visibilitychange', handler);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Revalidate on reconnect
|
|
151
|
+
if (revalidateOnReconnect && typeof window !== 'undefined') {
|
|
152
|
+
scopedEffect(() => {
|
|
153
|
+
const handler = () => revalidate().catch(() => {});
|
|
154
|
+
window.addEventListener('online', handler);
|
|
155
|
+
return () => window.removeEventListener('online', handler);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Polling
|
|
160
|
+
if (refreshInterval > 0) {
|
|
161
|
+
scopedEffect(() => {
|
|
162
|
+
const interval = setInterval(() => {
|
|
163
|
+
revalidate().catch(() => {});
|
|
164
|
+
}, refreshInterval);
|
|
165
|
+
return () => clearInterval(interval);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
data: () => data(),
|
|
171
|
+
error: () => error(),
|
|
172
|
+
isLoading,
|
|
173
|
+
isValidating: () => isValidating(),
|
|
174
|
+
mutate: (newData, shouldRevalidate = true) => {
|
|
175
|
+
data.set(typeof newData === 'function' ? newData(data()) : newData);
|
|
176
|
+
cache.set(key, data());
|
|
177
|
+
if (shouldRevalidate) {
|
|
178
|
+
revalidate().catch(() => {});
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
revalidate,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// --- useQuery Hook ---
|
|
186
|
+
// TanStack Query-like API
|
|
187
|
+
|
|
188
|
+
export function useQuery(options) {
|
|
189
|
+
const {
|
|
190
|
+
queryKey,
|
|
191
|
+
queryFn,
|
|
192
|
+
enabled = true,
|
|
193
|
+
staleTime = 0,
|
|
194
|
+
cacheTime = 5 * 60 * 1000,
|
|
195
|
+
refetchOnWindowFocus = true,
|
|
196
|
+
refetchInterval = false,
|
|
197
|
+
retry = 3,
|
|
198
|
+
retryDelay = (attempt) => Math.min(1000 * 2 ** attempt, 30000),
|
|
199
|
+
onSuccess,
|
|
200
|
+
onError,
|
|
201
|
+
onSettled,
|
|
202
|
+
select,
|
|
203
|
+
placeholderData,
|
|
204
|
+
} = options;
|
|
205
|
+
|
|
206
|
+
const key = Array.isArray(queryKey) ? queryKey.join(':') : queryKey;
|
|
207
|
+
|
|
208
|
+
const rawData = signal(cache.get(key) || null);
|
|
209
|
+
const data = computed(() => {
|
|
210
|
+
const d = rawData();
|
|
211
|
+
return select && d !== null ? select(d) : d;
|
|
212
|
+
});
|
|
213
|
+
const error = signal(null);
|
|
214
|
+
const status = signal(cache.has(key) ? 'success' : 'loading');
|
|
215
|
+
const fetchStatus = signal('idle');
|
|
216
|
+
|
|
217
|
+
let lastFetchTime = 0;
|
|
218
|
+
|
|
219
|
+
async function fetch() {
|
|
220
|
+
if (!enabled) return;
|
|
221
|
+
|
|
222
|
+
// Check if data is still fresh
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
if (cache.has(key) && now - lastFetchTime < staleTime) {
|
|
225
|
+
return cache.get(key);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fetchStatus.set('fetching');
|
|
229
|
+
if (!cache.has(key)) {
|
|
230
|
+
status.set('loading');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let attempts = 0;
|
|
234
|
+
|
|
235
|
+
async function attemptFetch() {
|
|
236
|
+
try {
|
|
237
|
+
const result = await queryFn({ queryKey: Array.isArray(queryKey) ? queryKey : [queryKey] });
|
|
238
|
+
batch(() => {
|
|
239
|
+
rawData.set(result);
|
|
240
|
+
error.set(null);
|
|
241
|
+
status.set('success');
|
|
242
|
+
fetchStatus.set('idle');
|
|
243
|
+
});
|
|
244
|
+
cache.set(key, result);
|
|
245
|
+
lastFetchTime = Date.now();
|
|
246
|
+
|
|
247
|
+
if (onSuccess) onSuccess(result);
|
|
248
|
+
if (onSettled) onSettled(result, null);
|
|
249
|
+
|
|
250
|
+
// Schedule cache cleanup
|
|
251
|
+
setTimeout(() => {
|
|
252
|
+
if (Date.now() - lastFetchTime >= cacheTime) {
|
|
253
|
+
cache.delete(key);
|
|
254
|
+
}
|
|
255
|
+
}, cacheTime);
|
|
256
|
+
|
|
257
|
+
return result;
|
|
258
|
+
} catch (e) {
|
|
259
|
+
attempts++;
|
|
260
|
+
if (attempts < retry) {
|
|
261
|
+
await new Promise(r => setTimeout(r, retryDelay(attempts)));
|
|
262
|
+
return attemptFetch();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
batch(() => {
|
|
266
|
+
error.set(e);
|
|
267
|
+
status.set('error');
|
|
268
|
+
fetchStatus.set('idle');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (onError) onError(e);
|
|
272
|
+
if (onSettled) onSettled(null, e);
|
|
273
|
+
|
|
274
|
+
throw e;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return attemptFetch();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Initial fetch
|
|
282
|
+
scopedEffect(() => {
|
|
283
|
+
if (enabled) {
|
|
284
|
+
fetch().catch(() => {});
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Refetch on focus
|
|
289
|
+
if (refetchOnWindowFocus && typeof window !== 'undefined') {
|
|
290
|
+
scopedEffect(() => {
|
|
291
|
+
const handler = () => {
|
|
292
|
+
if (document.visibilityState === 'visible') {
|
|
293
|
+
fetch().catch(() => {});
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
document.addEventListener('visibilitychange', handler);
|
|
297
|
+
return () => document.removeEventListener('visibilitychange', handler);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Polling
|
|
302
|
+
if (refetchInterval) {
|
|
303
|
+
scopedEffect(() => {
|
|
304
|
+
const interval = setInterval(() => {
|
|
305
|
+
fetch().catch(() => {});
|
|
306
|
+
}, refetchInterval);
|
|
307
|
+
return () => clearInterval(interval);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
data: () => data() ?? placeholderData,
|
|
313
|
+
error: () => error(),
|
|
314
|
+
status: () => status(),
|
|
315
|
+
fetchStatus: () => fetchStatus(),
|
|
316
|
+
isLoading: () => status() === 'loading',
|
|
317
|
+
isError: () => status() === 'error',
|
|
318
|
+
isSuccess: () => status() === 'success',
|
|
319
|
+
isFetching: () => fetchStatus() === 'fetching',
|
|
320
|
+
refetch: fetch,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// --- useInfiniteQuery Hook ---
|
|
325
|
+
// For paginated/infinite scroll data
|
|
326
|
+
|
|
327
|
+
export function useInfiniteQuery(options) {
|
|
328
|
+
const {
|
|
329
|
+
queryKey,
|
|
330
|
+
queryFn,
|
|
331
|
+
getNextPageParam,
|
|
332
|
+
getPreviousPageParam,
|
|
333
|
+
initialPageParam,
|
|
334
|
+
...rest
|
|
335
|
+
} = options;
|
|
336
|
+
|
|
337
|
+
const pages = signal([]);
|
|
338
|
+
const pageParams = signal([initialPageParam]);
|
|
339
|
+
const hasNextPage = signal(true);
|
|
340
|
+
const hasPreviousPage = signal(false);
|
|
341
|
+
const isFetchingNextPage = signal(false);
|
|
342
|
+
const isFetchingPreviousPage = signal(false);
|
|
343
|
+
|
|
344
|
+
const key = Array.isArray(queryKey) ? queryKey.join(':') : queryKey;
|
|
345
|
+
|
|
346
|
+
async function fetchPage(pageParam, direction = 'next') {
|
|
347
|
+
const loading = direction === 'next' ? isFetchingNextPage : isFetchingPreviousPage;
|
|
348
|
+
loading.set(true);
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
const result = await queryFn({
|
|
352
|
+
queryKey: Array.isArray(queryKey) ? queryKey : [queryKey],
|
|
353
|
+
pageParam,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
batch(() => {
|
|
357
|
+
if (direction === 'next') {
|
|
358
|
+
pages.set([...pages.peek(), result]);
|
|
359
|
+
pageParams.set([...pageParams.peek(), pageParam]);
|
|
360
|
+
} else {
|
|
361
|
+
pages.set([result, ...pages.peek()]);
|
|
362
|
+
pageParams.set([pageParam, ...pageParams.peek()]);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const nextParam = getNextPageParam?.(result, pages.peek());
|
|
366
|
+
hasNextPage.set(nextParam !== undefined);
|
|
367
|
+
|
|
368
|
+
if (getPreviousPageParam) {
|
|
369
|
+
const prevParam = getPreviousPageParam(result, pages.peek());
|
|
370
|
+
hasPreviousPage.set(prevParam !== undefined);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
return result;
|
|
375
|
+
} finally {
|
|
376
|
+
loading.set(false);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Initial fetch
|
|
381
|
+
scopedEffect(() => {
|
|
382
|
+
fetchPage(initialPageParam).catch(() => {});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
data: () => ({ pages: pages(), pageParams: pageParams() }),
|
|
387
|
+
hasNextPage: () => hasNextPage(),
|
|
388
|
+
hasPreviousPage: () => hasPreviousPage(),
|
|
389
|
+
isFetchingNextPage: () => isFetchingNextPage(),
|
|
390
|
+
isFetchingPreviousPage: () => isFetchingPreviousPage(),
|
|
391
|
+
fetchNextPage: async () => {
|
|
392
|
+
const lastPage = pages.peek()[pages.peek().length - 1];
|
|
393
|
+
const nextParam = getNextPageParam?.(lastPage, pages.peek());
|
|
394
|
+
if (nextParam !== undefined) {
|
|
395
|
+
return fetchPage(nextParam, 'next');
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
fetchPreviousPage: async () => {
|
|
399
|
+
const firstPage = pages.peek()[0];
|
|
400
|
+
const prevParam = getPreviousPageParam?.(firstPage, pages.peek());
|
|
401
|
+
if (prevParam !== undefined) {
|
|
402
|
+
return fetchPage(prevParam, 'previous');
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
refetch: async () => {
|
|
406
|
+
pages.set([]);
|
|
407
|
+
pageParams.set([initialPageParam]);
|
|
408
|
+
return fetchPage(initialPageParam);
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// --- Cache Management ---
|
|
414
|
+
|
|
415
|
+
export function invalidateQueries(keyOrPredicate) {
|
|
416
|
+
if (typeof keyOrPredicate === 'function') {
|
|
417
|
+
for (const [key] of cache) {
|
|
418
|
+
if (keyOrPredicate(key)) {
|
|
419
|
+
cache.delete(key);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
cache.delete(keyOrPredicate);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function prefetchQuery(key, fetcher) {
|
|
428
|
+
return fetcher(key).then(result => {
|
|
429
|
+
cache.set(key, result);
|
|
430
|
+
return result;
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function setQueryData(key, data) {
|
|
435
|
+
cache.set(key, typeof data === 'function' ? data(cache.get(key)) : data);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function getQueryData(key) {
|
|
439
|
+
return cache.get(key);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function clearCache() {
|
|
443
|
+
cache.clear();
|
|
444
|
+
}
|