what-core 0.1.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/components.js +140 -0
- package/dist/dom.js +443 -0
- package/dist/h.js +150 -0
- package/dist/head.js +51 -0
- package/dist/helpers.js +98 -0
- package/dist/hooks.js +156 -0
- package/dist/index.js +147 -0
- package/dist/reactive.js +124 -0
- package/dist/store.js +59 -0
- package/dist/what.js +114 -0
- package/index.d.ts +252 -0
- package/package.json +47 -0
- package/src/a11y.js +425 -0
- package/src/animation.js +531 -0
- package/src/components.js +204 -0
- package/src/data.js +434 -0
- package/src/dom.js +619 -0
- package/src/form.js +441 -0
- package/src/h.js +203 -0
- package/src/head.js +68 -0
- package/src/helpers.js +140 -0
- package/src/hooks.js +246 -0
- package/src/index.js +147 -0
- package/src/reactive.js +167 -0
- package/src/scheduler.js +241 -0
- package/src/skeleton.js +363 -0
- package/src/store.js +99 -0
- package/src/testing.js +367 -0
- package/testing.d.ts +103 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// What Framework - Component Utilities
|
|
2
|
+
// memo, lazy, Suspense, ErrorBoundary
|
|
3
|
+
|
|
4
|
+
import { h } from './h.js';
|
|
5
|
+
import { signal, effect, untrack } from './reactive.js';
|
|
6
|
+
|
|
7
|
+
// Error boundary context - components can register their error handlers here
|
|
8
|
+
export const errorBoundaryStack = [];
|
|
9
|
+
|
|
10
|
+
// --- memo ---
|
|
11
|
+
// Skip re-render if props haven't changed (shallow compare by default).
|
|
12
|
+
|
|
13
|
+
export function memo(Component, areEqual) {
|
|
14
|
+
const compare = areEqual || shallowEqual;
|
|
15
|
+
let prevProps = null;
|
|
16
|
+
let prevResult = null;
|
|
17
|
+
|
|
18
|
+
function MemoWrapper(props) {
|
|
19
|
+
if (prevProps && compare(prevProps, props)) {
|
|
20
|
+
return prevResult;
|
|
21
|
+
}
|
|
22
|
+
prevProps = { ...props };
|
|
23
|
+
prevResult = Component(props);
|
|
24
|
+
return prevResult;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
MemoWrapper.displayName = `Memo(${Component.name || 'Anonymous'})`;
|
|
28
|
+
return MemoWrapper;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function shallowEqual(a, b) {
|
|
32
|
+
if (a === b) return true;
|
|
33
|
+
const keysA = Object.keys(a);
|
|
34
|
+
const keysB = Object.keys(b);
|
|
35
|
+
if (keysA.length !== keysB.length) return false;
|
|
36
|
+
for (const key of keysA) {
|
|
37
|
+
if (!Object.is(a[key], b[key])) return false;
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --- lazy ---
|
|
43
|
+
// Code-split a component. Returns a wrapper that loads on first render.
|
|
44
|
+
|
|
45
|
+
export function lazy(loader) {
|
|
46
|
+
let Component = null;
|
|
47
|
+
let loadPromise = null;
|
|
48
|
+
let loadError = null;
|
|
49
|
+
const listeners = new Set();
|
|
50
|
+
|
|
51
|
+
function LazyWrapper(props) {
|
|
52
|
+
if (loadError) throw loadError;
|
|
53
|
+
if (Component) return h(Component, props);
|
|
54
|
+
|
|
55
|
+
if (!loadPromise) {
|
|
56
|
+
loadPromise = loader()
|
|
57
|
+
.then(mod => {
|
|
58
|
+
Component = mod.default || mod;
|
|
59
|
+
// Notify all waiting instances
|
|
60
|
+
listeners.forEach(fn => fn());
|
|
61
|
+
listeners.clear();
|
|
62
|
+
})
|
|
63
|
+
.catch(err => { loadError = err; });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Throw promise for Suspense to catch
|
|
67
|
+
throw loadPromise;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
LazyWrapper.displayName = 'Lazy';
|
|
71
|
+
LazyWrapper._lazy = true;
|
|
72
|
+
LazyWrapper._onLoad = (fn) => {
|
|
73
|
+
if (Component) fn();
|
|
74
|
+
else listeners.add(fn);
|
|
75
|
+
};
|
|
76
|
+
return LazyWrapper;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --- Suspense ---
|
|
80
|
+
// Show fallback while children are loading (lazy components).
|
|
81
|
+
// Works with lazy() and async components.
|
|
82
|
+
|
|
83
|
+
export function Suspense({ fallback, children }) {
|
|
84
|
+
const loading = signal(false);
|
|
85
|
+
const pendingPromises = new Set();
|
|
86
|
+
|
|
87
|
+
// Suspense boundary marker
|
|
88
|
+
const boundary = {
|
|
89
|
+
_suspense: true,
|
|
90
|
+
onSuspend(promise) {
|
|
91
|
+
loading.set(true);
|
|
92
|
+
pendingPromises.add(promise);
|
|
93
|
+
promise.finally(() => {
|
|
94
|
+
pendingPromises.delete(promise);
|
|
95
|
+
if (pendingPromises.size === 0) {
|
|
96
|
+
loading.set(false);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
tag: '__suspense',
|
|
104
|
+
props: { boundary, fallback, loading },
|
|
105
|
+
children: Array.isArray(children) ? children : [children],
|
|
106
|
+
_vnode: true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- ErrorBoundary ---
|
|
111
|
+
// Catch errors in children and show fallback.
|
|
112
|
+
// Uses a signal to track error state so it works with reactive rendering.
|
|
113
|
+
|
|
114
|
+
export function ErrorBoundary({ fallback, children, onError }) {
|
|
115
|
+
const errorState = signal(null);
|
|
116
|
+
|
|
117
|
+
// Error handler that will be registered with the component tree
|
|
118
|
+
const handleError = (error) => {
|
|
119
|
+
errorState.set(error);
|
|
120
|
+
if (onError) {
|
|
121
|
+
try {
|
|
122
|
+
onError(error);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
console.error('Error in onError handler:', e);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Reset function to recover from error
|
|
130
|
+
const reset = () => errorState.set(null);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
tag: '__errorBoundary',
|
|
134
|
+
props: { errorState, handleError, fallback, reset },
|
|
135
|
+
children: Array.isArray(children) ? children : [children],
|
|
136
|
+
_vnode: true,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Helper to get current error boundary
|
|
141
|
+
export function getCurrentErrorBoundary() {
|
|
142
|
+
return errorBoundaryStack[errorBoundaryStack.length - 1] || null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Helper to report error to nearest boundary
|
|
146
|
+
export function reportError(error) {
|
|
147
|
+
const boundary = getCurrentErrorBoundary();
|
|
148
|
+
if (boundary) {
|
|
149
|
+
boundary.handleError(error);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// --- Show ---
|
|
156
|
+
// Conditional rendering component. Cleaner than ternaries.
|
|
157
|
+
|
|
158
|
+
export function Show({ when, fallback = null, children }) {
|
|
159
|
+
// when can be a signal or a value
|
|
160
|
+
const condition = typeof when === 'function' ? when() : when;
|
|
161
|
+
return condition ? children : fallback;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- For ---
|
|
165
|
+
// Efficient list rendering with keyed reconciliation.
|
|
166
|
+
|
|
167
|
+
export function For({ each, fallback = null, children }) {
|
|
168
|
+
const list = typeof each === 'function' ? each() : each;
|
|
169
|
+
if (!list || list.length === 0) return fallback;
|
|
170
|
+
|
|
171
|
+
// children should be a function (item, index) => vnode
|
|
172
|
+
const renderFn = Array.isArray(children) ? children[0] : children;
|
|
173
|
+
if (typeof renderFn !== 'function') {
|
|
174
|
+
console.warn('For: children must be a function');
|
|
175
|
+
return fallback;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return list.map((item, index) => renderFn(item, index));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// --- Switch / Match ---
|
|
182
|
+
// Multi-condition rendering (like switch statement).
|
|
183
|
+
|
|
184
|
+
export function Switch({ fallback = null, children }) {
|
|
185
|
+
const kids = Array.isArray(children) ? children : [children];
|
|
186
|
+
|
|
187
|
+
for (const child of kids) {
|
|
188
|
+
if (child && child.tag === Match) {
|
|
189
|
+
const condition = typeof child.props.when === 'function'
|
|
190
|
+
? child.props.when()
|
|
191
|
+
: child.props.when;
|
|
192
|
+
if (condition) {
|
|
193
|
+
return child.children;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return fallback;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function Match({ when, children }) {
|
|
202
|
+
// Match is just a marker component, Switch handles the logic
|
|
203
|
+
return { tag: Match, props: { when }, children, _vnode: true };
|
|
204
|
+
}
|
package/src/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
|
+
}
|