@choksheak/react-utils 0.2.3 → 0.2.4
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/README.md +3 -1
- package/package.json +7 -2
- package/sharedQuery.d.mts +38 -7
- package/sharedQuery.d.ts +37 -6
- package/sharedQuery.js +149 -89
- package/sharedQuery.mjs +147 -87
- package/src/sharedQuery.ts +198 -117
- package/src/utils/useDeepMemo.ts +19 -0
- package/utils/useDeepMemo.d.mts +4 -0
- package/utils/useDeepMemo.d.ts +2 -0
- package/utils/useDeepMemo.js +51 -0
- package/utils/useDeepMemo.mjs +16 -0
- package/docs/.nojekyll +0 -1
- package/docs/assets/hierarchy.js +0 -1
- package/docs/assets/highlight.css +0 -71
- package/docs/assets/icons.js +0 -18
- package/docs/assets/icons.svg +0 -1
- package/docs/assets/main.js +0 -60
- package/docs/assets/navigation.js +0 -1
- package/docs/assets/search.js +0 -1
- package/docs/assets/style.css +0 -1633
- package/docs/classes/sharedQuery.SharedQuery.html +0 -22
- package/docs/classes/sharedState.SharedState.html +0 -9
- package/docs/classes/utils_storage.InMemoryStorageAdapter.html +0 -9
- package/docs/classes/utils_storage.IndexedDbAdapter.html +0 -9
- package/docs/classes/utils_storage.LocalStorageAdapter.html +0 -9
- package/docs/functions/sharedQuery.sharedQuery.html +0 -9
- package/docs/functions/sharedQuery.useSharedQuery.html +0 -6
- package/docs/functions/sharedState.sharedState.html +0 -3
- package/docs/functions/sharedState.useSharedState.html +0 -3
- package/docs/functions/sharedState.useSharedStateSetter.html +0 -2
- package/docs/functions/sharedState.useSharedStateValue.html +0 -2
- package/docs/functions/utils_storage.getStorageAdapter.html +0 -1
- package/docs/functions/utils_stringify.stringifyDeterministicForKeys.html +0 -7
- package/docs/hierarchy.html +0 -1
- package/docs/index.html +0 -2
- package/docs/modules/fetcher.html +0 -1
- package/docs/modules/index.html +0 -1
- package/docs/modules/sharedQuery.html +0 -1
- package/docs/modules/sharedState.html +0 -1
- package/docs/modules/utils_storage.html +0 -1
- package/docs/modules/utils_stringify.html +0 -1
- package/docs/modules.html +0 -1
- package/docs/types/sharedQuery.FetchFn.html +0 -1
- package/docs/types/sharedQuery.PersistTo.html +0 -1
- package/docs/types/sharedQuery.QueryStateValue.html +0 -9
- package/docs/types/sharedQuery.SharedQueryOptions.html +0 -1
- package/docs/types/sharedQuery.SharedQueryState.html +0 -3
- package/docs/types/sharedQuery.UseQueryResult.html +0 -1
- package/docs/types/sharedState.SharedStateOptions.html +0 -1
- package/docs/types/utils_storage.StorageAdapter.html +0 -5
- package/docs/types/utils_storage.StorageOptions.html +0 -5
- package/docs/variables/fetcher.fetcher.html +0 -2
- package/docs/variables/fetcher.fetcherWithRetries.html +0 -2
- package/docs/variables/sharedQuery.SharedQueryDefaults.html +0 -2
- package/docs/variables/sharedState.SharedStateDefaults.html +0 -6
package/sharedQuery.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/sharedQuery.ts
|
|
2
2
|
import { MS_PER_DAY as MS_PER_DAY2 } from "@choksheak/ts-utils/timeConstants";
|
|
3
3
|
import getByteSize from "object-sizeof";
|
|
4
|
-
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
4
|
+
import { useCallback, useEffect, useMemo as useMemo2, useRef } from "react";
|
|
5
5
|
|
|
6
6
|
// src/sharedState.ts
|
|
7
7
|
import { MS_PER_DAY } from "@choksheak/ts-utils/timeConstants";
|
|
@@ -266,18 +266,39 @@ function stringifyDeterministicForKeys(params) {
|
|
|
266
266
|
});
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
// src/utils/useDeepMemo.ts
|
|
270
|
+
import isEqual from "lodash/isEqual";
|
|
271
|
+
import { useMemo, useState } from "react";
|
|
272
|
+
function useDeepMemo(value) {
|
|
273
|
+
const [state, setState] = useState(value);
|
|
274
|
+
return useMemo(() => {
|
|
275
|
+
if (state === value || isEqual(state, value)) {
|
|
276
|
+
return state;
|
|
277
|
+
}
|
|
278
|
+
setState(value);
|
|
279
|
+
return value;
|
|
280
|
+
}, [state, value]);
|
|
281
|
+
}
|
|
282
|
+
|
|
269
283
|
// src/sharedQuery.ts
|
|
270
284
|
var SharedQueryDefaults = {
|
|
285
|
+
// Refetch on every page load.
|
|
271
286
|
staleMs: 0,
|
|
287
|
+
// Keep in cache for a long time.
|
|
272
288
|
expiryMs: 30 * MS_PER_DAY2,
|
|
289
|
+
// Always refetch automatically if the data is stale.
|
|
290
|
+
refetchOnStale: true,
|
|
291
|
+
// No default persistence configured.
|
|
273
292
|
persistTo: void 0,
|
|
293
|
+
// 100 is not too large, but please tweak accordingly.
|
|
274
294
|
maxSize: 100,
|
|
275
|
-
//
|
|
276
|
-
maxBytes: 1e5
|
|
277
|
-
//
|
|
295
|
+
// 100kb limit before discarding old records.
|
|
296
|
+
maxBytes: 1e5,
|
|
297
|
+
// Default to log to console.
|
|
298
|
+
log: (...args) => console.log("[sharedQuery]", ...args)
|
|
278
299
|
};
|
|
279
300
|
var SharedQuery = class {
|
|
280
|
-
|
|
301
|
+
inflightQueries = /* @__PURE__ */ new Map();
|
|
281
302
|
queryState;
|
|
282
303
|
queryName;
|
|
283
304
|
queryFn;
|
|
@@ -286,22 +307,27 @@ var SharedQuery = class {
|
|
|
286
307
|
refetchOnStale;
|
|
287
308
|
maxSize;
|
|
288
309
|
maxBytes;
|
|
310
|
+
log;
|
|
289
311
|
constructor(options) {
|
|
290
312
|
this.queryName = options.queryName;
|
|
291
313
|
this.queryFn = options.queryFn;
|
|
292
|
-
this.
|
|
293
|
-
this.
|
|
294
|
-
this.refetchOnStale =
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
314
|
+
this.staleMs = options?.staleMs ?? SharedQueryDefaults.staleMs;
|
|
315
|
+
this.expiryMs = options?.expiryMs ?? SharedQueryDefaults.expiryMs;
|
|
316
|
+
this.refetchOnStale = options?.refetchOnStale ?? SharedQueryDefaults.refetchOnStale;
|
|
317
|
+
if (!options.localStorageKey && !options.indexedDbKey) {
|
|
318
|
+
const persistTo = options.persistTo ?? SharedQueryDefaults.persistTo;
|
|
319
|
+
if (persistTo === "localStorage") {
|
|
320
|
+
options.localStorageKey = options.queryName;
|
|
321
|
+
} else if (persistTo === "indexedDb") {
|
|
322
|
+
options.indexedDbKey = options.queryName;
|
|
323
|
+
}
|
|
300
324
|
}
|
|
301
325
|
this.maxSize = options?.maxSize ?? SharedQueryDefaults.maxSize;
|
|
302
326
|
this.maxBytes = options?.maxBytes ?? SharedQueryDefaults.maxBytes;
|
|
327
|
+
this.log = options?.log ?? SharedQueryDefaults.log;
|
|
303
328
|
this.queryState = sharedState(
|
|
304
329
|
{},
|
|
330
|
+
// defaultValue
|
|
305
331
|
{
|
|
306
332
|
...options,
|
|
307
333
|
// Fallback to use expiryMs for the storeExpiryMs.
|
|
@@ -309,9 +335,24 @@ var SharedQuery = class {
|
|
|
309
335
|
}
|
|
310
336
|
);
|
|
311
337
|
}
|
|
338
|
+
/** Returns a key to identify requests based on the given args. */
|
|
312
339
|
getQueryKey(args) {
|
|
313
340
|
return this.queryName + ":" + stringifyDeterministicForKeys(args);
|
|
314
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Get the AbortController to abort the inflight query (if any).
|
|
344
|
+
*
|
|
345
|
+
* Note that once you get the controller, you can also get the signal easily
|
|
346
|
+
* using `controller.signal`. So if you need both the controller and the
|
|
347
|
+
* signal, just use this function.
|
|
348
|
+
*/
|
|
349
|
+
getAbortController(queryKey) {
|
|
350
|
+
return this.inflightQueries.get(queryKey)?.abortController ?? null;
|
|
351
|
+
}
|
|
352
|
+
/** Get the AbortSignal to check for query abortions. */
|
|
353
|
+
getAbortSignal(queryKey) {
|
|
354
|
+
return this.getAbortController(queryKey)?.signal ?? null;
|
|
355
|
+
}
|
|
315
356
|
isStale(dataUpdatedMs) {
|
|
316
357
|
if (this.staleMs === 0) return true;
|
|
317
358
|
const ageMs = Date.now() - dataUpdatedMs;
|
|
@@ -327,42 +368,41 @@ var SharedQuery = class {
|
|
|
327
368
|
const cached = this.queryState.getSnapshot()?.[queryKey];
|
|
328
369
|
if (cached?.data && !this.isExpired(cached.dataUpdatedMs ?? 0)) {
|
|
329
370
|
if (!this.isStale(cached.dataUpdatedMs ?? 0)) {
|
|
330
|
-
|
|
331
|
-
`Return fresh data ${queryKey} from cache without fetching`
|
|
332
|
-
);
|
|
371
|
+
this.log(`Return fresh data ${queryKey} from cache without fetching`);
|
|
333
372
|
return cached.data;
|
|
334
373
|
}
|
|
335
374
|
if (this.refetchOnStale) {
|
|
336
|
-
|
|
375
|
+
this.log(`refetchOnStale for ${queryKey}`);
|
|
337
376
|
void this.dedupedFetch(queryKey, ...args);
|
|
338
377
|
}
|
|
339
|
-
|
|
378
|
+
this.log(`Returning stale data for ${queryKey}`);
|
|
340
379
|
return cached.data;
|
|
341
380
|
}
|
|
342
381
|
return await this.dedupedFetch(queryKey, ...args);
|
|
343
382
|
}
|
|
344
383
|
dedupedFetch(queryKey, ...args) {
|
|
345
|
-
const
|
|
346
|
-
if (
|
|
347
|
-
|
|
348
|
-
return
|
|
384
|
+
const inflightQuery = this.inflightQueries.get(queryKey);
|
|
385
|
+
if (inflightQuery) {
|
|
386
|
+
this.log(`Deduplicating inflight fetch for ${queryKey}`);
|
|
387
|
+
return inflightQuery.promise;
|
|
349
388
|
}
|
|
350
389
|
const promise = this.startFetching(queryKey, args);
|
|
351
|
-
|
|
390
|
+
const abortController = new AbortController();
|
|
391
|
+
this.inflightQueries.set(queryKey, { promise, abortController });
|
|
352
392
|
return promise;
|
|
353
393
|
}
|
|
354
394
|
async startFetching(queryKey, args) {
|
|
355
|
-
|
|
395
|
+
this.log(`Start fetching ${queryKey}`);
|
|
356
396
|
try {
|
|
357
397
|
const data = await this.queryFn(...args);
|
|
358
|
-
|
|
398
|
+
this.log(`Successfully fetched ${queryKey}`);
|
|
359
399
|
this.setData(queryKey, data, Date.now());
|
|
360
400
|
return data;
|
|
361
401
|
} catch (e) {
|
|
362
|
-
|
|
402
|
+
this.log(`Failed to fetch ${queryKey}: ${e}`);
|
|
363
403
|
throw e;
|
|
364
404
|
} finally {
|
|
365
|
-
this.
|
|
405
|
+
this.inflightQueries.delete(queryKey);
|
|
366
406
|
}
|
|
367
407
|
}
|
|
368
408
|
/** Set the data directly if the user obtained it from somewhere else. */
|
|
@@ -382,18 +422,26 @@ var SharedQuery = class {
|
|
|
382
422
|
return await this.dedupedFetch(key, ...args);
|
|
383
423
|
}
|
|
384
424
|
/** Delete the cached data for one set of arguments. */
|
|
385
|
-
deleteData(
|
|
386
|
-
const queryKey = this.getQueryKey(args);
|
|
425
|
+
deleteData(queryKey) {
|
|
387
426
|
const record = this.queryState.getSnapshot();
|
|
388
427
|
const clone = { ...record };
|
|
389
428
|
delete clone[queryKey];
|
|
390
429
|
this.queryState.setValue(clone);
|
|
391
|
-
this.
|
|
430
|
+
const inflight = this.inflightQueries.get(queryKey);
|
|
431
|
+
if (inflight) {
|
|
432
|
+
this.inflightQueries.delete(queryKey);
|
|
433
|
+
inflight.abortController.abort("Deleted");
|
|
434
|
+
}
|
|
392
435
|
}
|
|
393
436
|
/** Delete all currently cached data & all inflight promises. */
|
|
394
437
|
clear() {
|
|
395
438
|
this.queryState.delete();
|
|
396
|
-
|
|
439
|
+
for (const { abortController } of Array.from(
|
|
440
|
+
this.inflightQueries.values()
|
|
441
|
+
)) {
|
|
442
|
+
abortController.abort("Cleared");
|
|
443
|
+
}
|
|
444
|
+
this.inflightQueries.clear();
|
|
397
445
|
}
|
|
398
446
|
/**
|
|
399
447
|
* Discard the oldest entries if the size exceeds the limit. The last
|
|
@@ -414,7 +462,7 @@ var SharedQuery = class {
|
|
|
414
462
|
if (this.maxSize) {
|
|
415
463
|
const numToCut = oldSize - this.maxSize;
|
|
416
464
|
if (numToCut > 0) {
|
|
417
|
-
|
|
465
|
+
this.log(
|
|
418
466
|
`enforceSizeLimit for ${this.queryName} needed due to size ${oldSize} > ${this.maxSize}`
|
|
419
467
|
);
|
|
420
468
|
newState = { ...newState };
|
|
@@ -433,7 +481,7 @@ var SharedQuery = class {
|
|
|
433
481
|
const currentByteSize = needUpdate ? getByteSize(newState) : oldByteSize;
|
|
434
482
|
let bytesToCut = currentByteSize - this.maxBytes;
|
|
435
483
|
if (bytesToCut > 0) {
|
|
436
|
-
|
|
484
|
+
this.log(
|
|
437
485
|
`enforceSizeLimit for ${this.queryName} needed due to byte size ${currentByteSize.toLocaleString()} > ${this.maxBytes.toLocaleString()}`
|
|
438
486
|
);
|
|
439
487
|
if (!needUpdate) {
|
|
@@ -458,12 +506,12 @@ var SharedQuery = class {
|
|
|
458
506
|
}
|
|
459
507
|
if (needUpdate) {
|
|
460
508
|
this.queryState.setValue(newState);
|
|
461
|
-
|
|
509
|
+
this.log(
|
|
462
510
|
`Trimmed data for ${this.queryName}: size=(${oldSize} -> ${Object.keys(newState).length}) (limit=${this.maxSize}), byteSize=(${oldByteSize.toLocaleString()} -> ${getByteSize(newState).toLocaleString()}) (limit=${this.maxBytes.toLocaleString()})`
|
|
463
511
|
);
|
|
464
512
|
return;
|
|
465
513
|
}
|
|
466
|
-
|
|
514
|
+
this.log(
|
|
467
515
|
`No need to trim data for ${this.queryName}: size=${oldSize} (limit=${this.maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`
|
|
468
516
|
);
|
|
469
517
|
}
|
|
@@ -481,77 +529,89 @@ var DEFAULT_QUERY_STATE_ENTRY = {
|
|
|
481
529
|
loading: true
|
|
482
530
|
};
|
|
483
531
|
function useSharedQuery(query, args = []) {
|
|
532
|
+
const stableArgs = useDeepMemo(args);
|
|
484
533
|
const [queryState, setQueryState] = useSharedState(query.queryState);
|
|
485
534
|
const isMounted = useRef(true);
|
|
486
|
-
const queryKey =
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
535
|
+
const queryKey = useMemo2(
|
|
536
|
+
() => query.getQueryKey(stableArgs),
|
|
537
|
+
[query, stableArgs]
|
|
538
|
+
);
|
|
539
|
+
const queryStateValue = queryState[queryKey];
|
|
540
|
+
const execute = useCallback(
|
|
541
|
+
async (forceRefresh) => {
|
|
542
|
+
query.log(`Begin executing shared query ${queryKey}`);
|
|
543
|
+
setQueryState((prev) => {
|
|
544
|
+
const clone = { ...prev };
|
|
545
|
+
clone[queryKey] = {
|
|
546
|
+
...clone[queryKey] ?? {},
|
|
547
|
+
lastUpdatedMs: Date.now(),
|
|
548
|
+
loading: true
|
|
549
|
+
};
|
|
550
|
+
return clone;
|
|
551
|
+
});
|
|
552
|
+
try {
|
|
553
|
+
const data = await (forceRefresh ? query.updateFromSource(...stableArgs) : query.getCachedOrFetch(...stableArgs));
|
|
554
|
+
if (isMounted.current) {
|
|
555
|
+
setQueryState((prev) => {
|
|
556
|
+
const clone = { ...prev };
|
|
557
|
+
const now = Date.now();
|
|
558
|
+
clone[queryKey] = {
|
|
559
|
+
// Don't keep old error.
|
|
560
|
+
lastUpdatedMs: now,
|
|
561
|
+
loading: false,
|
|
562
|
+
data,
|
|
563
|
+
dataUpdatedMs: now
|
|
564
|
+
};
|
|
565
|
+
return clone;
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
} catch (e) {
|
|
569
|
+
if (isMounted.current) {
|
|
570
|
+
setQueryState((prev) => {
|
|
571
|
+
const clone = { ...prev };
|
|
572
|
+
const now = Date.now();
|
|
573
|
+
clone[queryKey] = {
|
|
574
|
+
// Keep old data.
|
|
575
|
+
...clone[queryKey] ?? {},
|
|
576
|
+
lastUpdatedMs: now,
|
|
577
|
+
loading: false,
|
|
578
|
+
error: e,
|
|
579
|
+
errorUpdatedMs: now
|
|
580
|
+
};
|
|
581
|
+
return clone;
|
|
582
|
+
});
|
|
583
|
+
}
|
|
530
584
|
}
|
|
531
|
-
}
|
|
532
|
-
|
|
585
|
+
},
|
|
586
|
+
[query, queryKey, setQueryState, stableArgs]
|
|
587
|
+
);
|
|
533
588
|
useEffect(() => {
|
|
534
589
|
isMounted.current = true;
|
|
535
|
-
void execute();
|
|
590
|
+
void execute(false);
|
|
536
591
|
return () => {
|
|
537
592
|
isMounted.current = false;
|
|
538
593
|
};
|
|
539
594
|
}, [execute]);
|
|
540
|
-
return
|
|
541
|
-
const state =
|
|
595
|
+
return useMemo2(() => {
|
|
596
|
+
const state = queryStateValue ?? DEFAULT_QUERY_STATE_ENTRY;
|
|
542
597
|
return {
|
|
543
598
|
...state,
|
|
599
|
+
abortCurrentQuery: (reason) => {
|
|
600
|
+
const controller = query.getAbortController(queryKey);
|
|
601
|
+
controller?.abort(reason);
|
|
602
|
+
return Boolean(controller);
|
|
603
|
+
},
|
|
544
604
|
refetch: () => {
|
|
545
|
-
|
|
605
|
+
void execute(true);
|
|
546
606
|
},
|
|
547
607
|
setData: (data, dataUpdatedMs) => {
|
|
548
608
|
query.setData(queryKey, data, dataUpdatedMs ?? Date.now());
|
|
549
609
|
},
|
|
550
610
|
deleteData: () => {
|
|
551
|
-
query.deleteData(
|
|
611
|
+
query.deleteData(queryKey);
|
|
552
612
|
}
|
|
553
613
|
};
|
|
554
|
-
}, [query, queryKey,
|
|
614
|
+
}, [execute, query, queryKey, queryStateValue]);
|
|
555
615
|
}
|
|
556
616
|
export {
|
|
557
617
|
SharedQuery,
|