pulse-js-framework 1.7.1 → 1.7.3
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/cli/dev.js +55 -39
- package/cli/release.js +2 -3
- package/package.json +8 -2
- package/runtime/async.js +619 -0
- package/runtime/devtools.js +619 -0
- package/runtime/dom.js +254 -40
- package/runtime/form.js +659 -0
- package/runtime/pulse.js +36 -3
- package/runtime/router.js +51 -5
- package/runtime/store.js +45 -0
package/runtime/async.js
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Async Primitives
|
|
3
|
+
* @module pulse-js-framework/runtime/async
|
|
4
|
+
*
|
|
5
|
+
* Reactive primitives for handling asynchronous operations like
|
|
6
|
+
* data fetching, polling, and async state management.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { pulse, effect, batch, onCleanup } from './pulse.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} AsyncState
|
|
13
|
+
* @property {T|null} data - The resolved data
|
|
14
|
+
* @property {Error|null} error - The error if rejected
|
|
15
|
+
* @property {boolean} loading - Whether the async operation is in progress
|
|
16
|
+
* @property {'idle'|'loading'|'success'|'error'} status - Current status
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} UseAsyncOptions
|
|
21
|
+
* @property {boolean} [immediate=true] - Execute immediately on creation
|
|
22
|
+
* @property {T} [initialData=null] - Initial data value
|
|
23
|
+
* @property {function(Error): void} [onError] - Error callback
|
|
24
|
+
* @property {function(T): void} [onSuccess] - Success callback
|
|
25
|
+
* @property {number} [retries=0] - Number of retry attempts on failure
|
|
26
|
+
* @property {number} [retryDelay=1000] - Delay between retries in ms
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a reactive async operation handler.
|
|
31
|
+
* Manages loading, error, and success states automatically.
|
|
32
|
+
*
|
|
33
|
+
* @template T
|
|
34
|
+
* @param {function(): Promise<T>} asyncFn - Async function to execute
|
|
35
|
+
* @param {UseAsyncOptions<T>} [options={}] - Configuration options
|
|
36
|
+
* @returns {Object} Reactive async state and controls
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // Basic usage
|
|
40
|
+
* const { data, loading, error, execute } = useAsync(
|
|
41
|
+
* () => fetch('/api/users').then(r => r.json())
|
|
42
|
+
* );
|
|
43
|
+
*
|
|
44
|
+
* effect(() => {
|
|
45
|
+
* if (loading.get()) console.log('Loading...');
|
|
46
|
+
* if (data.get()) console.log('Users:', data.get());
|
|
47
|
+
* if (error.get()) console.log('Error:', error.get().message);
|
|
48
|
+
* });
|
|
49
|
+
*
|
|
50
|
+
* // With options
|
|
51
|
+
* const { data } = useAsync(
|
|
52
|
+
* () => fetchData(),
|
|
53
|
+
* {
|
|
54
|
+
* immediate: false,
|
|
55
|
+
* retries: 3,
|
|
56
|
+
* onSuccess: (data) => console.log('Got data:', data),
|
|
57
|
+
* onError: (err) => console.error('Failed:', err)
|
|
58
|
+
* }
|
|
59
|
+
* );
|
|
60
|
+
*/
|
|
61
|
+
export function useAsync(asyncFn, options = {}) {
|
|
62
|
+
const {
|
|
63
|
+
immediate = true,
|
|
64
|
+
initialData = null,
|
|
65
|
+
onError,
|
|
66
|
+
onSuccess,
|
|
67
|
+
retries = 0,
|
|
68
|
+
retryDelay = 1000
|
|
69
|
+
} = options;
|
|
70
|
+
|
|
71
|
+
const data = pulse(initialData);
|
|
72
|
+
const error = pulse(null);
|
|
73
|
+
const loading = pulse(false);
|
|
74
|
+
const status = pulse('idle');
|
|
75
|
+
|
|
76
|
+
// Track current execution version to handle race conditions
|
|
77
|
+
let executionVersion = 0;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Execute the async function
|
|
81
|
+
* @param {...any} args - Arguments to pass to asyncFn
|
|
82
|
+
* @returns {Promise<T|null>} The resolved data or null on error
|
|
83
|
+
*/
|
|
84
|
+
async function execute(...args) {
|
|
85
|
+
const currentVersion = ++executionVersion;
|
|
86
|
+
let attempt = 0;
|
|
87
|
+
|
|
88
|
+
batch(() => {
|
|
89
|
+
loading.set(true);
|
|
90
|
+
error.set(null);
|
|
91
|
+
status.set('loading');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
while (attempt <= retries) {
|
|
95
|
+
try {
|
|
96
|
+
const result = await asyncFn(...args);
|
|
97
|
+
|
|
98
|
+
// Check if this execution is still current (not stale)
|
|
99
|
+
if (currentVersion !== executionVersion) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
batch(() => {
|
|
104
|
+
data.set(result);
|
|
105
|
+
loading.set(false);
|
|
106
|
+
status.set('success');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (onSuccess) onSuccess(result);
|
|
110
|
+
return result;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
attempt++;
|
|
113
|
+
|
|
114
|
+
// Only retry if we haven't exceeded retries and execution is still current
|
|
115
|
+
if (attempt <= retries && currentVersion === executionVersion) {
|
|
116
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check if this execution is still current
|
|
121
|
+
if (currentVersion !== executionVersion) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
batch(() => {
|
|
126
|
+
error.set(err);
|
|
127
|
+
loading.set(false);
|
|
128
|
+
status.set('error');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (onError) onError(err);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Reset state to initial values
|
|
141
|
+
*/
|
|
142
|
+
function reset() {
|
|
143
|
+
executionVersion++;
|
|
144
|
+
batch(() => {
|
|
145
|
+
data.set(initialData);
|
|
146
|
+
error.set(null);
|
|
147
|
+
loading.set(false);
|
|
148
|
+
status.set('idle');
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Abort current execution (marks it as stale)
|
|
154
|
+
*/
|
|
155
|
+
function abort() {
|
|
156
|
+
executionVersion++;
|
|
157
|
+
if (loading.get()) {
|
|
158
|
+
batch(() => {
|
|
159
|
+
loading.set(false);
|
|
160
|
+
status.set('idle');
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Execute immediately if requested
|
|
166
|
+
if (immediate) {
|
|
167
|
+
execute();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
data,
|
|
172
|
+
error,
|
|
173
|
+
loading,
|
|
174
|
+
status,
|
|
175
|
+
execute,
|
|
176
|
+
reset,
|
|
177
|
+
abort
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @typedef {Object} ResourceOptions
|
|
183
|
+
* @property {number} [refreshInterval] - Auto-refresh interval in ms
|
|
184
|
+
* @property {boolean} [refreshOnFocus=false] - Refresh when window regains focus
|
|
185
|
+
* @property {boolean} [refreshOnReconnect=false] - Refresh when network reconnects
|
|
186
|
+
* @property {T} [initialData=null] - Initial data value
|
|
187
|
+
* @property {function(Error): void} [onError] - Error callback
|
|
188
|
+
* @property {number} [staleTime=0] - Time in ms before data is considered stale
|
|
189
|
+
* @property {number} [cacheTime=300000] - Time in ms to keep data in cache (5 min default)
|
|
190
|
+
*/
|
|
191
|
+
|
|
192
|
+
// Global resource cache
|
|
193
|
+
const resourceCache = new Map();
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Create a reactive resource with caching, auto-refresh, and stale-while-revalidate.
|
|
197
|
+
* Similar to React Query or SWR patterns.
|
|
198
|
+
*
|
|
199
|
+
* @template T
|
|
200
|
+
* @param {string|function(): string} key - Cache key or function returning key
|
|
201
|
+
* @param {function(): Promise<T>} fetcher - Async function to fetch data
|
|
202
|
+
* @param {ResourceOptions<T>} [options={}] - Configuration options
|
|
203
|
+
* @returns {Object} Reactive resource state and controls
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* // Basic usage with caching
|
|
207
|
+
* const users = useResource('users', () => fetch('/api/users').then(r => r.json()));
|
|
208
|
+
*
|
|
209
|
+
* // With auto-refresh
|
|
210
|
+
* const liveData = useResource(
|
|
211
|
+
* 'live-data',
|
|
212
|
+
* () => fetchLiveData(),
|
|
213
|
+
* { refreshInterval: 5000 }
|
|
214
|
+
* );
|
|
215
|
+
*
|
|
216
|
+
* // Dynamic key based on reactive value
|
|
217
|
+
* const userId = pulse(1);
|
|
218
|
+
* const user = useResource(
|
|
219
|
+
* () => `user-${userId.get()}`,
|
|
220
|
+
* () => fetch(`/api/users/${userId.get()}`).then(r => r.json())
|
|
221
|
+
* );
|
|
222
|
+
*/
|
|
223
|
+
export function useResource(key, fetcher, options = {}) {
|
|
224
|
+
const {
|
|
225
|
+
refreshInterval,
|
|
226
|
+
refreshOnFocus = false,
|
|
227
|
+
refreshOnReconnect = false,
|
|
228
|
+
initialData = null,
|
|
229
|
+
onError,
|
|
230
|
+
staleTime = 0,
|
|
231
|
+
cacheTime = 300000
|
|
232
|
+
} = options;
|
|
233
|
+
|
|
234
|
+
const data = pulse(initialData);
|
|
235
|
+
const error = pulse(null);
|
|
236
|
+
const loading = pulse(false);
|
|
237
|
+
const isStale = pulse(false);
|
|
238
|
+
const isValidating = pulse(false);
|
|
239
|
+
const lastFetchTime = pulse(0);
|
|
240
|
+
|
|
241
|
+
let intervalId = null;
|
|
242
|
+
let currentKey = null;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get the current cache key
|
|
246
|
+
*/
|
|
247
|
+
function getCacheKey() {
|
|
248
|
+
return typeof key === 'function' ? key() : key;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get cached data if available and not expired
|
|
253
|
+
*/
|
|
254
|
+
function getCachedData() {
|
|
255
|
+
const cacheKey = getCacheKey();
|
|
256
|
+
const cached = resourceCache.get(cacheKey);
|
|
257
|
+
|
|
258
|
+
if (cached) {
|
|
259
|
+
const age = Date.now() - cached.timestamp;
|
|
260
|
+
if (age < cacheTime) {
|
|
261
|
+
return {
|
|
262
|
+
data: cached.data,
|
|
263
|
+
isStale: age > staleTime
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
// Expired, remove from cache
|
|
267
|
+
resourceCache.delete(cacheKey);
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Update cache with new data
|
|
274
|
+
*/
|
|
275
|
+
function updateCache(newData) {
|
|
276
|
+
const cacheKey = getCacheKey();
|
|
277
|
+
resourceCache.set(cacheKey, {
|
|
278
|
+
data: newData,
|
|
279
|
+
timestamp: Date.now()
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Fetch fresh data
|
|
285
|
+
*/
|
|
286
|
+
async function fetch() {
|
|
287
|
+
const cacheKey = getCacheKey();
|
|
288
|
+
currentKey = cacheKey;
|
|
289
|
+
|
|
290
|
+
// Check cache first
|
|
291
|
+
const cached = getCachedData();
|
|
292
|
+
if (cached && !cached.isStale) {
|
|
293
|
+
batch(() => {
|
|
294
|
+
data.set(cached.data);
|
|
295
|
+
isStale.set(false);
|
|
296
|
+
lastFetchTime.set(resourceCache.get(cacheKey)?.timestamp || 0);
|
|
297
|
+
});
|
|
298
|
+
return cached.data;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Show cached data immediately if stale (stale-while-revalidate)
|
|
302
|
+
if (cached && cached.isStale) {
|
|
303
|
+
batch(() => {
|
|
304
|
+
data.set(cached.data);
|
|
305
|
+
isStale.set(true);
|
|
306
|
+
isValidating.set(true);
|
|
307
|
+
});
|
|
308
|
+
} else {
|
|
309
|
+
loading.set(true);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
const result = await fetcher();
|
|
314
|
+
|
|
315
|
+
// Check if key changed during fetch
|
|
316
|
+
if (cacheKey !== currentKey) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
updateCache(result);
|
|
321
|
+
|
|
322
|
+
batch(() => {
|
|
323
|
+
data.set(result);
|
|
324
|
+
error.set(null);
|
|
325
|
+
loading.set(false);
|
|
326
|
+
isStale.set(false);
|
|
327
|
+
isValidating.set(false);
|
|
328
|
+
lastFetchTime.set(Date.now());
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
return result;
|
|
332
|
+
} catch (err) {
|
|
333
|
+
if (cacheKey !== currentKey) {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
batch(() => {
|
|
338
|
+
error.set(err);
|
|
339
|
+
loading.set(false);
|
|
340
|
+
isValidating.set(false);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
if (onError) onError(err);
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Force refresh, ignoring cache
|
|
350
|
+
*/
|
|
351
|
+
async function refresh() {
|
|
352
|
+
const cacheKey = getCacheKey();
|
|
353
|
+
resourceCache.delete(cacheKey);
|
|
354
|
+
return fetch();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Mutate data optimistically
|
|
359
|
+
*/
|
|
360
|
+
function mutate(newData, shouldRevalidate = false) {
|
|
361
|
+
const cacheKey = getCacheKey();
|
|
362
|
+
const resolvedData = typeof newData === 'function' ? newData(data.get()) : newData;
|
|
363
|
+
|
|
364
|
+
updateCache(resolvedData);
|
|
365
|
+
data.set(resolvedData);
|
|
366
|
+
|
|
367
|
+
if (shouldRevalidate) {
|
|
368
|
+
isStale.set(true);
|
|
369
|
+
fetch();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Clear cache for this resource
|
|
375
|
+
*/
|
|
376
|
+
function invalidate() {
|
|
377
|
+
const cacheKey = getCacheKey();
|
|
378
|
+
resourceCache.delete(cacheKey);
|
|
379
|
+
isStale.set(true);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Setup auto-refresh interval
|
|
383
|
+
if (refreshInterval && refreshInterval > 0) {
|
|
384
|
+
intervalId = setInterval(() => {
|
|
385
|
+
if (!loading.get() && !isValidating.get()) {
|
|
386
|
+
fetch();
|
|
387
|
+
}
|
|
388
|
+
}, refreshInterval);
|
|
389
|
+
|
|
390
|
+
onCleanup(() => {
|
|
391
|
+
if (intervalId) clearInterval(intervalId);
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Setup window focus listener
|
|
396
|
+
if (refreshOnFocus && typeof window !== 'undefined') {
|
|
397
|
+
const handleFocus = () => {
|
|
398
|
+
const cached = getCachedData();
|
|
399
|
+
if (!cached || cached.isStale) {
|
|
400
|
+
fetch();
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
window.addEventListener('focus', handleFocus);
|
|
405
|
+
onCleanup(() => window.removeEventListener('focus', handleFocus));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Setup online listener
|
|
409
|
+
if (refreshOnReconnect && typeof window !== 'undefined') {
|
|
410
|
+
const handleOnline = () => {
|
|
411
|
+
fetch();
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
window.addEventListener('online', handleOnline);
|
|
415
|
+
onCleanup(() => window.removeEventListener('online', handleOnline));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Watch for key changes if key is a function
|
|
419
|
+
if (typeof key === 'function') {
|
|
420
|
+
effect(() => {
|
|
421
|
+
const newKey = key();
|
|
422
|
+
if (newKey !== currentKey) {
|
|
423
|
+
fetch();
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
} else {
|
|
427
|
+
// Initial fetch
|
|
428
|
+
fetch();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
data,
|
|
433
|
+
error,
|
|
434
|
+
loading,
|
|
435
|
+
isStale,
|
|
436
|
+
isValidating,
|
|
437
|
+
lastFetchTime,
|
|
438
|
+
fetch,
|
|
439
|
+
refresh,
|
|
440
|
+
mutate,
|
|
441
|
+
invalidate
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* @typedef {Object} PollingOptions
|
|
447
|
+
* @property {number} interval - Polling interval in ms
|
|
448
|
+
* @property {boolean} [immediate=true] - Execute immediately on start
|
|
449
|
+
* @property {boolean} [pauseOnHidden=true] - Pause when page is hidden
|
|
450
|
+
* @property {boolean} [pauseOnOffline=true] - Pause when offline
|
|
451
|
+
* @property {number} [maxErrors=3] - Max consecutive errors before stopping
|
|
452
|
+
* @property {function(Error): void} [onError] - Error callback
|
|
453
|
+
*/
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Create a polling mechanism for repeated async operations.
|
|
457
|
+
*
|
|
458
|
+
* @template T
|
|
459
|
+
* @param {function(): Promise<T>} asyncFn - Async function to poll
|
|
460
|
+
* @param {PollingOptions} options - Polling configuration
|
|
461
|
+
* @returns {Object} Polling controls and state
|
|
462
|
+
*
|
|
463
|
+
* @example
|
|
464
|
+
* const { data, start, stop, isPolling } = usePolling(
|
|
465
|
+
* () => fetch('/api/status').then(r => r.json()),
|
|
466
|
+
* { interval: 5000, pauseOnHidden: true }
|
|
467
|
+
* );
|
|
468
|
+
*
|
|
469
|
+
* // Start polling
|
|
470
|
+
* start();
|
|
471
|
+
*
|
|
472
|
+
* // Stop when done
|
|
473
|
+
* stop();
|
|
474
|
+
*/
|
|
475
|
+
export function usePolling(asyncFn, options) {
|
|
476
|
+
const {
|
|
477
|
+
interval,
|
|
478
|
+
immediate = true,
|
|
479
|
+
pauseOnHidden = true,
|
|
480
|
+
pauseOnOffline = true,
|
|
481
|
+
maxErrors = 3,
|
|
482
|
+
onError
|
|
483
|
+
} = options;
|
|
484
|
+
|
|
485
|
+
const data = pulse(null);
|
|
486
|
+
const error = pulse(null);
|
|
487
|
+
const isPolling = pulse(false);
|
|
488
|
+
const errorCount = pulse(0);
|
|
489
|
+
|
|
490
|
+
let intervalId = null;
|
|
491
|
+
let isPaused = false;
|
|
492
|
+
|
|
493
|
+
async function poll() {
|
|
494
|
+
if (isPaused || !isPolling.get()) return;
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
const result = await asyncFn();
|
|
498
|
+
batch(() => {
|
|
499
|
+
data.set(result);
|
|
500
|
+
error.set(null);
|
|
501
|
+
errorCount.set(0);
|
|
502
|
+
});
|
|
503
|
+
} catch (err) {
|
|
504
|
+
const newCount = errorCount.get() + 1;
|
|
505
|
+
batch(() => {
|
|
506
|
+
error.set(err);
|
|
507
|
+
errorCount.set(newCount);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
if (onError) onError(err);
|
|
511
|
+
|
|
512
|
+
// Stop polling after max consecutive errors
|
|
513
|
+
if (newCount >= maxErrors) {
|
|
514
|
+
stop();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function start() {
|
|
520
|
+
if (intervalId) return;
|
|
521
|
+
|
|
522
|
+
isPolling.set(true);
|
|
523
|
+
errorCount.set(0);
|
|
524
|
+
|
|
525
|
+
if (immediate) {
|
|
526
|
+
poll();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
intervalId = setInterval(poll, interval);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function stop() {
|
|
533
|
+
if (intervalId) {
|
|
534
|
+
clearInterval(intervalId);
|
|
535
|
+
intervalId = null;
|
|
536
|
+
}
|
|
537
|
+
isPolling.set(false);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function pause() {
|
|
541
|
+
isPaused = true;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function resume() {
|
|
545
|
+
isPaused = false;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Page visibility handling
|
|
549
|
+
if (pauseOnHidden && typeof document !== 'undefined') {
|
|
550
|
+
const handleVisibility = () => {
|
|
551
|
+
if (document.hidden) {
|
|
552
|
+
pause();
|
|
553
|
+
} else {
|
|
554
|
+
resume();
|
|
555
|
+
// Immediately poll when becoming visible
|
|
556
|
+
if (isPolling.get()) poll();
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
document.addEventListener('visibilitychange', handleVisibility);
|
|
561
|
+
onCleanup(() => document.removeEventListener('visibilitychange', handleVisibility));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Online/offline handling
|
|
565
|
+
if (pauseOnOffline && typeof window !== 'undefined') {
|
|
566
|
+
const handleOffline = () => pause();
|
|
567
|
+
const handleOnline = () => {
|
|
568
|
+
resume();
|
|
569
|
+
if (isPolling.get()) poll();
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
window.addEventListener('offline', handleOffline);
|
|
573
|
+
window.addEventListener('online', handleOnline);
|
|
574
|
+
onCleanup(() => {
|
|
575
|
+
window.removeEventListener('offline', handleOffline);
|
|
576
|
+
window.removeEventListener('online', handleOnline);
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Cleanup on unmount
|
|
581
|
+
onCleanup(stop);
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
data,
|
|
585
|
+
error,
|
|
586
|
+
isPolling,
|
|
587
|
+
errorCount,
|
|
588
|
+
start,
|
|
589
|
+
stop,
|
|
590
|
+
pause,
|
|
591
|
+
resume
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Clear the entire resource cache
|
|
597
|
+
*/
|
|
598
|
+
export function clearResourceCache() {
|
|
599
|
+
resourceCache.clear();
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Get resource cache statistics
|
|
604
|
+
* @returns {{size: number, keys: string[]}}
|
|
605
|
+
*/
|
|
606
|
+
export function getResourceCacheStats() {
|
|
607
|
+
return {
|
|
608
|
+
size: resourceCache.size,
|
|
609
|
+
keys: [...resourceCache.keys()]
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export default {
|
|
614
|
+
useAsync,
|
|
615
|
+
useResource,
|
|
616
|
+
usePolling,
|
|
617
|
+
clearResourceCache,
|
|
618
|
+
getResourceCacheStats
|
|
619
|
+
};
|