what-core 0.6.2 → 0.6.5

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.
Files changed (52) hide show
  1. package/README.md +2 -0
  2. package/compiler.d.ts +30 -0
  3. package/devtools.d.ts +2 -0
  4. package/dist/compiler.js +1787 -0
  5. package/dist/compiler.js.map +7 -0
  6. package/dist/compiler.min.js +2 -0
  7. package/dist/compiler.min.js.map +7 -0
  8. package/dist/devtools.js +10 -0
  9. package/dist/devtools.js.map +7 -0
  10. package/dist/devtools.min.js +2 -0
  11. package/dist/devtools.min.js.map +7 -0
  12. package/dist/index.js +330 -382
  13. package/dist/index.js.map +4 -4
  14. package/dist/index.min.js +62 -62
  15. package/dist/index.min.js.map +4 -4
  16. package/dist/render.js +262 -21
  17. package/dist/render.js.map +4 -4
  18. package/dist/render.min.js +58 -1
  19. package/dist/render.min.js.map +4 -4
  20. package/dist/testing.js +3 -0
  21. package/dist/testing.js.map +2 -2
  22. package/dist/testing.min.js +1 -1
  23. package/dist/testing.min.js.map +2 -2
  24. package/index.d.ts +176 -1
  25. package/jsx-runtime.d.ts +622 -0
  26. package/package.json +20 -2
  27. package/render.d.ts +1 -1
  28. package/src/agent-context.js +1 -1
  29. package/src/compiler.js +18 -0
  30. package/src/components.js +73 -27
  31. package/src/devtools.js +4 -0
  32. package/src/dom.js +7 -0
  33. package/src/guardrails.js +3 -4
  34. package/src/hooks.js +0 -11
  35. package/src/index.js +5 -9
  36. package/src/render.js +91 -24
  37. package/testing.d.ts +1 -1
  38. package/dist/a11y.js +0 -440
  39. package/dist/animation.js +0 -548
  40. package/dist/components.js +0 -229
  41. package/dist/data.js +0 -638
  42. package/dist/dom.js +0 -439
  43. package/dist/form.js +0 -509
  44. package/dist/h.js +0 -152
  45. package/dist/head.js +0 -51
  46. package/dist/helpers.js +0 -140
  47. package/dist/hooks.js +0 -210
  48. package/dist/reactive.js +0 -432
  49. package/dist/scheduler.js +0 -246
  50. package/dist/skeleton.js +0 -363
  51. package/dist/store.js +0 -83
  52. 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
- }