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/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
+ }