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