@choksheak/react-utils 0.2.4 → 0.2.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.
- package/fetcher.min.cjs +2 -0
- package/fetcher.min.cjs.map +1 -0
- package/fetcher.min.mjs +2 -0
- package/fetcher.min.mjs.map +1 -0
- package/index.min.cjs +2 -0
- package/index.min.cjs.map +1 -0
- package/index.min.mjs +2 -0
- package/index.min.mjs.map +1 -0
- package/package.json +32 -4
- package/{sharedQuery.js → sharedQuery.cjs} +106 -43
- package/sharedQuery.d.mts +38 -5
- package/sharedQuery.d.ts +38 -5
- package/sharedQuery.min.cjs +2 -0
- package/sharedQuery.min.cjs.map +1 -0
- package/sharedQuery.min.mjs +2 -0
- package/sharedQuery.min.mjs.map +1 -0
- package/sharedQuery.mjs +106 -43
- package/sharedState.min.cjs +2 -0
- package/sharedState.min.cjs.map +1 -0
- package/sharedState.min.mjs +2 -0
- package/sharedState.min.mjs.map +1 -0
- package/utils/storage.min.cjs +2 -0
- package/utils/storage.min.cjs.map +1 -0
- package/utils/storage.min.mjs +2 -0
- package/utils/storage.min.mjs.map +1 -0
- package/utils/stringify.min.cjs +2 -0
- package/utils/stringify.min.cjs.map +1 -0
- package/utils/stringify.min.mjs +2 -0
- package/utils/stringify.min.mjs.map +1 -0
- package/utils/useDeepMemo.min.cjs +2 -0
- package/utils/useDeepMemo.min.cjs.map +1 -0
- package/utils/useDeepMemo.min.mjs +2 -0
- package/utils/useDeepMemo.min.mjs.map +1 -0
- package/src/fetcher.ts +0 -14
- package/src/index.ts +0 -4
- package/src/sharedQuery.ts +0 -588
- package/src/sharedState.ts +0 -232
- package/src/utils/storage.ts +0 -197
- package/src/utils/stringify.ts +0 -49
- package/src/utils/useDeepMemo.ts +0 -19
- /package/{fetcher.js → fetcher.cjs} +0 -0
- /package/{index.js → index.cjs} +0 -0
- /package/{sharedState.js → sharedState.cjs} +0 -0
- /package/utils/{storage.js → storage.cjs} +0 -0
- /package/utils/{stringify.js → stringify.cjs} +0 -0
- /package/utils/{useDeepMemo.js → useDeepMemo.cjs} +0 -0
package/fetcher.min.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var f=Object.create;var m=Object.defineProperty;var h=Object.getOwnPropertyDescriptor;var a=Object.getOwnPropertyNames;var d=Object.getPrototypeOf,n=Object.prototype.hasOwnProperty;var w=(r,e)=>{for(var t in e)m(r,t,{get:e[t],enumerable:!0})},p=(r,e,t,c)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of a(e))!n.call(r,o)&&o!==t&&m(r,o,{get:()=>e[o],enumerable:!(c=h(e,o))||c.enumerable});return r};var x=(r,e,t)=>(t=r!=null?f(d(r)):{},p(e||!r||!r.__esModule?m(t,"default",{value:r,enumerable:!0}):t,r)),y=r=>p(m({},"__esModule",{value:!0}),r);var u={};w(u,{fetcher:()=>l,fetcherWithRetries:()=>k});module.exports=y(u);var i=x(require("wretch")),s=require("wretch/middlewares"),l=(0,i.default)(),k=(0,i.default)().middlewares([(0,s.retry)({maxAttempts:3,delayTimer:1e3,retryOnNetworkError:!0})]);0&&(module.exports={fetcher,fetcherWithRetries});
|
|
2
|
+
//# sourceMappingURL=fetcher.min.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fetcher.ts"],"sourcesContent":["import wretch from \"wretch\";\nimport { retry } from \"wretch/middlewares\";\n\n/** Name this as \"fetcher\" to make it easier to understand. */\nexport const fetcher = wretch();\n\n/** Fetcher with default short retries. */\nexport const fetcherWithRetries = wretch().middlewares([\n retry({\n maxAttempts: 3,\n delayTimer: 1000,\n retryOnNetworkError: true,\n }),\n]);\n"],"mappings":"0jBAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,aAAAE,EAAA,uBAAAC,IAAA,eAAAC,EAAAJ,GAAA,IAAAK,EAAmB,qBACnBC,EAAsB,8BAGTJ,KAAU,EAAAK,SAAO,EAGjBJ,KAAqB,EAAAI,SAAO,EAAE,YAAY,IACrD,SAAM,CACJ,YAAa,EACb,WAAY,IACZ,oBAAqB,EACvB,CAAC,CACH,CAAC","names":["fetcher_exports","__export","fetcher","fetcherWithRetries","__toCommonJS","import_wretch","import_middlewares","wretch"]}
|
package/fetcher.min.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fetcher.ts"],"sourcesContent":["import wretch from \"wretch\";\nimport { retry } from \"wretch/middlewares\";\n\n/** Name this as \"fetcher\" to make it easier to understand. */\nexport const fetcher = wretch();\n\n/** Fetcher with default short retries. */\nexport const fetcherWithRetries = wretch().middlewares([\n retry({\n maxAttempts: 3,\n delayTimer: 1000,\n retryOnNetworkError: true,\n }),\n]);\n"],"mappings":"AAAA,OAAOA,MAAY,SACnB,OAAS,SAAAC,MAAa,qBAGf,IAAMC,EAAUF,EAAO,EAGjBG,EAAqBH,EAAO,EAAE,YAAY,CACrDC,EAAM,CACJ,YAAa,EACb,WAAY,IACZ,oBAAqB,EACvB,CAAC,CACH,CAAC","names":["wretch","retry","fetcher","fetcherWithRetries"]}
|
package/index.min.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Unused file. To support full tree-shaking, please import from the individual\n// files directly.\n\nconsole.log(`Hello from Chok's React Utils`);\n"],"mappings":"aAGA,QAAQ,IAAI,+BAA+B","names":[]}
|
package/index.min.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Unused file. To support full tree-shaking, please import from the individual\n// files directly.\n\nconsole.log(`Hello from Chok's React Utils`);\n"],"mappings":"8DAAA,IAAAA,EAAAC,EAAA,KAGA,QAAQ,IAAI,+BAA+B","names":["require_index","__commonJSMin"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@choksheak/react-utils",
|
|
3
3
|
"license": "The Unlicense",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.5",
|
|
5
5
|
"description": "Miscellaneous React+TypeScript utilities with support for full tree-shaking.",
|
|
6
6
|
"private": false,
|
|
7
7
|
"main": "index.js",
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@eslint/compat": "^1.4.0",
|
|
65
65
|
"@eslint/js": "^9.38.0",
|
|
66
|
+
"@swc/core": "^1.14.0",
|
|
66
67
|
"@types/jest": "^30.0.0",
|
|
67
68
|
"@types/lodash": "^4.17.20",
|
|
68
69
|
"@types/react": "^19.2.2",
|
|
@@ -107,15 +108,42 @@
|
|
|
107
108
|
"./package.json": "./package.json",
|
|
108
109
|
"./fetcher": {
|
|
109
110
|
"types": "./fetcher.d.ts",
|
|
110
|
-
"import":
|
|
111
|
+
"import": {
|
|
112
|
+
"production": "./fetcher.min.mjs",
|
|
113
|
+
"development": "./fetcher.mjs",
|
|
114
|
+
"default": "./fetcher.mjs"
|
|
115
|
+
},
|
|
116
|
+
"require": {
|
|
117
|
+
"production": "./fetcher.min.cjs",
|
|
118
|
+
"development": "./fetcher.cjs",
|
|
119
|
+
"default": "./fetcher.cjs"
|
|
120
|
+
}
|
|
111
121
|
},
|
|
112
122
|
"./sharedQuery": {
|
|
113
123
|
"types": "./sharedQuery.d.ts",
|
|
114
|
-
"import":
|
|
124
|
+
"import": {
|
|
125
|
+
"production": "./sharedQuery.min.mjs",
|
|
126
|
+
"development": "./sharedQuery.mjs",
|
|
127
|
+
"default": "./sharedQuery.mjs"
|
|
128
|
+
},
|
|
129
|
+
"require": {
|
|
130
|
+
"production": "./sharedQuery.min.cjs",
|
|
131
|
+
"development": "./sharedQuery.cjs",
|
|
132
|
+
"default": "./sharedQuery.cjs"
|
|
133
|
+
}
|
|
115
134
|
},
|
|
116
135
|
"./sharedState": {
|
|
117
136
|
"types": "./sharedState.d.ts",
|
|
118
|
-
"import":
|
|
137
|
+
"import": {
|
|
138
|
+
"production": "./sharedState.min.mjs",
|
|
139
|
+
"development": "./sharedState.mjs",
|
|
140
|
+
"default": "./sharedState.mjs"
|
|
141
|
+
},
|
|
142
|
+
"require": {
|
|
143
|
+
"production": "./sharedState.min.cjs",
|
|
144
|
+
"development": "./sharedState.cjs",
|
|
145
|
+
"default": "./sharedState.cjs"
|
|
146
|
+
}
|
|
119
147
|
}
|
|
120
148
|
},
|
|
121
149
|
"typesVersions": {
|
|
@@ -383,12 +383,44 @@ var SharedQuery = class {
|
|
|
383
383
|
* using `controller.signal`. So if you need both the controller and the
|
|
384
384
|
* signal, just use this function.
|
|
385
385
|
*/
|
|
386
|
-
getAbortController(
|
|
386
|
+
getAbortController(args) {
|
|
387
|
+
const queryKey = this.getQueryKey(args);
|
|
388
|
+
return this.getAbortControllerByKey(queryKey);
|
|
389
|
+
}
|
|
390
|
+
getAbortControllerByKey(queryKey) {
|
|
387
391
|
return this.inflightQueries.get(queryKey)?.abortController ?? null;
|
|
388
392
|
}
|
|
389
|
-
/**
|
|
390
|
-
|
|
391
|
-
|
|
393
|
+
/**
|
|
394
|
+
* Get the AbortSignal to check for query abortions.
|
|
395
|
+
*
|
|
396
|
+
* Example:
|
|
397
|
+
* ```
|
|
398
|
+
* const getUserQuery = sharedQuery({
|
|
399
|
+
* queryName: "getUser",
|
|
400
|
+
* queryFn: (userId: string) => {
|
|
401
|
+
* // Get the signal for this current execution.
|
|
402
|
+
* const signal = getUserQuery.getAbortSignal([userId]);
|
|
403
|
+
*
|
|
404
|
+
* // Pass the signal to fetch so that it can be aborted.
|
|
405
|
+
* const response = await fetch(`/users/${userId}`, { signal });
|
|
406
|
+
*
|
|
407
|
+
* // Check for errors.
|
|
408
|
+
* if (!response.ok) {
|
|
409
|
+
* throw new Error(response.statusText);
|
|
410
|
+
* }
|
|
411
|
+
*
|
|
412
|
+
* // Return the data.
|
|
413
|
+
* return await response.json();
|
|
414
|
+
* },
|
|
415
|
+
* });
|
|
416
|
+
* ```
|
|
417
|
+
*/
|
|
418
|
+
getAbortSignal(args) {
|
|
419
|
+
const queryKey = this.getQueryKey(args);
|
|
420
|
+
return this.getAbortSignalByKey(queryKey);
|
|
421
|
+
}
|
|
422
|
+
getAbortSignalByKey(queryKey) {
|
|
423
|
+
return this.getAbortControllerByKey(queryKey)?.signal ?? null;
|
|
392
424
|
}
|
|
393
425
|
isStale(dataUpdatedMs) {
|
|
394
426
|
if (this.staleMs === 0) return true;
|
|
@@ -442,8 +474,16 @@ var SharedQuery = class {
|
|
|
442
474
|
this.inflightQueries.delete(queryKey);
|
|
443
475
|
}
|
|
444
476
|
}
|
|
477
|
+
/** Get the entire stored entry for a query key. */
|
|
478
|
+
getQueryValue(queryKey) {
|
|
479
|
+
return this.queryState.getSnapshot()?.[queryKey];
|
|
480
|
+
}
|
|
481
|
+
/** Get the current stored data for a query key. */
|
|
482
|
+
getData(queryKey) {
|
|
483
|
+
return this.queryState.getSnapshot()?.[queryKey]?.data;
|
|
484
|
+
}
|
|
445
485
|
/** Set the data directly if the user obtained it from somewhere else. */
|
|
446
|
-
|
|
486
|
+
setData(queryKey, data, dataUpdatedMs = Date.now()) {
|
|
447
487
|
const entry = {
|
|
448
488
|
lastUpdatedMs: Date.now(),
|
|
449
489
|
loading: false,
|
|
@@ -453,7 +493,10 @@ var SharedQuery = class {
|
|
|
453
493
|
this.queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));
|
|
454
494
|
this.enforceSizeLimit();
|
|
455
495
|
}
|
|
456
|
-
/**
|
|
496
|
+
/**
|
|
497
|
+
* Do a new fetch even when the data is already cached, but don't fetch if
|
|
498
|
+
* another fetch is already inflight.
|
|
499
|
+
*/
|
|
457
500
|
async updateFromSource(...args) {
|
|
458
501
|
const key = this.getQueryKey(args);
|
|
459
502
|
return await this.dedupedFetch(key, ...args);
|
|
@@ -574,53 +617,52 @@ function useSharedQuery(query, args = []) {
|
|
|
574
617
|
[query, stableArgs]
|
|
575
618
|
);
|
|
576
619
|
const queryStateValue = queryState[queryKey];
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
query.log(`Begin executing shared query ${queryKey}`);
|
|
620
|
+
const setQueryStateValue = (0, import_react3.useCallback)(
|
|
621
|
+
(arg) => {
|
|
580
622
|
setQueryState((prev) => {
|
|
581
623
|
const clone = { ...prev };
|
|
582
|
-
clone[queryKey] =
|
|
583
|
-
...clone[queryKey] ?? {},
|
|
584
|
-
lastUpdatedMs: Date.now(),
|
|
585
|
-
loading: true
|
|
586
|
-
};
|
|
624
|
+
clone[queryKey] = typeof arg === "function" ? arg(clone[queryKey]) : arg;
|
|
587
625
|
return clone;
|
|
588
626
|
});
|
|
627
|
+
},
|
|
628
|
+
[queryKey, setQueryState]
|
|
629
|
+
);
|
|
630
|
+
const execute = (0, import_react3.useCallback)(
|
|
631
|
+
async (forceRefresh) => {
|
|
632
|
+
query.log(`Begin executing shared query ${queryKey}`);
|
|
633
|
+
setQueryStateValue((prev) => ({
|
|
634
|
+
// Keep old data and error.
|
|
635
|
+
...prev ?? {},
|
|
636
|
+
lastUpdatedMs: Date.now(),
|
|
637
|
+
loading: true
|
|
638
|
+
}));
|
|
589
639
|
try {
|
|
590
640
|
const data = await (forceRefresh ? query.updateFromSource(...stableArgs) : query.getCachedOrFetch(...stableArgs));
|
|
591
641
|
if (isMounted.current) {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
data,
|
|
600
|
-
dataUpdatedMs: now
|
|
601
|
-
};
|
|
602
|
-
return clone;
|
|
642
|
+
const now = Date.now();
|
|
643
|
+
setQueryStateValue({
|
|
644
|
+
// Don't keep old error.
|
|
645
|
+
lastUpdatedMs: now,
|
|
646
|
+
loading: false,
|
|
647
|
+
data,
|
|
648
|
+
dataUpdatedMs: now
|
|
603
649
|
});
|
|
604
650
|
}
|
|
605
651
|
} catch (e) {
|
|
606
652
|
if (isMounted.current) {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
errorUpdatedMs: now
|
|
617
|
-
};
|
|
618
|
-
return clone;
|
|
619
|
-
});
|
|
653
|
+
const now = Date.now();
|
|
654
|
+
setQueryStateValue((prev) => ({
|
|
655
|
+
// Keep old data.
|
|
656
|
+
...prev ?? {},
|
|
657
|
+
lastUpdatedMs: now,
|
|
658
|
+
loading: false,
|
|
659
|
+
error: e,
|
|
660
|
+
errorUpdatedMs: now
|
|
661
|
+
}));
|
|
620
662
|
}
|
|
621
663
|
}
|
|
622
664
|
},
|
|
623
|
-
[query, queryKey,
|
|
665
|
+
[query, queryKey, setQueryStateValue, stableArgs]
|
|
624
666
|
);
|
|
625
667
|
(0, import_react3.useEffect)(() => {
|
|
626
668
|
isMounted.current = true;
|
|
@@ -633,8 +675,11 @@ function useSharedQuery(query, args = []) {
|
|
|
633
675
|
const state = queryStateValue ?? DEFAULT_QUERY_STATE_ENTRY;
|
|
634
676
|
return {
|
|
635
677
|
...state,
|
|
678
|
+
// This works only if the user-given queryFn supports abort. If not,
|
|
679
|
+
// this function doesn't do anything, since we don't have any means to
|
|
680
|
+
// abort the running queryFn.
|
|
636
681
|
abortCurrentQuery: (reason) => {
|
|
637
|
-
const controller = query.
|
|
682
|
+
const controller = query.getAbortControllerByKey(queryKey);
|
|
638
683
|
controller?.abort(reason);
|
|
639
684
|
return Boolean(controller);
|
|
640
685
|
},
|
|
@@ -642,13 +687,31 @@ function useSharedQuery(query, args = []) {
|
|
|
642
687
|
void execute(true);
|
|
643
688
|
},
|
|
644
689
|
setData: (data, dataUpdatedMs) => {
|
|
645
|
-
|
|
690
|
+
const now = Date.now();
|
|
691
|
+
setQueryStateValue((prev) => ({
|
|
692
|
+
...prev ?? {},
|
|
693
|
+
data,
|
|
694
|
+
dataUpdatedMs: dataUpdatedMs ?? now,
|
|
695
|
+
lastUpdatedMs: now,
|
|
696
|
+
loading: false
|
|
697
|
+
}));
|
|
646
698
|
},
|
|
647
699
|
deleteData: () => {
|
|
648
|
-
|
|
700
|
+
setQueryState((prev) => {
|
|
701
|
+
const clone = { ...prev };
|
|
702
|
+
delete clone[queryKey];
|
|
703
|
+
return clone;
|
|
704
|
+
});
|
|
649
705
|
}
|
|
650
706
|
};
|
|
651
|
-
}, [
|
|
707
|
+
}, [
|
|
708
|
+
execute,
|
|
709
|
+
query,
|
|
710
|
+
queryKey,
|
|
711
|
+
queryStateValue,
|
|
712
|
+
setQueryState,
|
|
713
|
+
setQueryStateValue
|
|
714
|
+
]);
|
|
652
715
|
}
|
|
653
716
|
// Annotate the CommonJS export names for ESM import in node:
|
|
654
717
|
0 && (module.exports = {
|
package/sharedQuery.d.mts
CHANGED
|
@@ -83,17 +83,50 @@ declare class SharedQuery<TArgs extends unknown[], TData> {
|
|
|
83
83
|
* using `controller.signal`. So if you need both the controller and the
|
|
84
84
|
* signal, just use this function.
|
|
85
85
|
*/
|
|
86
|
-
getAbortController(
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
getAbortController(args: TArgs): AbortController | null;
|
|
87
|
+
getAbortControllerByKey(queryKey: string): AbortController | null;
|
|
88
|
+
/**
|
|
89
|
+
* Get the AbortSignal to check for query abortions.
|
|
90
|
+
*
|
|
91
|
+
* Example:
|
|
92
|
+
* ```
|
|
93
|
+
* const getUserQuery = sharedQuery({
|
|
94
|
+
* queryName: "getUser",
|
|
95
|
+
* queryFn: (userId: string) => {
|
|
96
|
+
* // Get the signal for this current execution.
|
|
97
|
+
* const signal = getUserQuery.getAbortSignal([userId]);
|
|
98
|
+
*
|
|
99
|
+
* // Pass the signal to fetch so that it can be aborted.
|
|
100
|
+
* const response = await fetch(`/users/${userId}`, { signal });
|
|
101
|
+
*
|
|
102
|
+
* // Check for errors.
|
|
103
|
+
* if (!response.ok) {
|
|
104
|
+
* throw new Error(response.statusText);
|
|
105
|
+
* }
|
|
106
|
+
*
|
|
107
|
+
* // Return the data.
|
|
108
|
+
* return await response.json();
|
|
109
|
+
* },
|
|
110
|
+
* });
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
getAbortSignal(args: TArgs): AbortSignal | null;
|
|
114
|
+
getAbortSignalByKey(queryKey: string): AbortSignal | null;
|
|
89
115
|
private isStale;
|
|
90
116
|
private isExpired;
|
|
91
117
|
getCachedOrFetch(...args: TArgs): Promise<TData>;
|
|
92
118
|
private dedupedFetch;
|
|
93
119
|
private startFetching;
|
|
120
|
+
/** Get the entire stored entry for a query key. */
|
|
121
|
+
getQueryValue(queryKey: string): QueryStateValue<TData> | undefined;
|
|
122
|
+
/** Get the current stored data for a query key. */
|
|
123
|
+
getData(queryKey: string): TData | undefined;
|
|
94
124
|
/** Set the data directly if the user obtained it from somewhere else. */
|
|
95
|
-
setData(queryKey: string, data: TData, dataUpdatedMs?: number):
|
|
96
|
-
/**
|
|
125
|
+
setData(queryKey: string, data: TData, dataUpdatedMs?: number): void;
|
|
126
|
+
/**
|
|
127
|
+
* Do a new fetch even when the data is already cached, but don't fetch if
|
|
128
|
+
* another fetch is already inflight.
|
|
129
|
+
*/
|
|
97
130
|
updateFromSource(...args: TArgs): Promise<TData>;
|
|
98
131
|
/** Delete the cached data for one set of arguments. */
|
|
99
132
|
deleteData(queryKey: string): void;
|
package/sharedQuery.d.ts
CHANGED
|
@@ -81,17 +81,50 @@ export declare class SharedQuery<TArgs extends unknown[], TData> {
|
|
|
81
81
|
* using `controller.signal`. So if you need both the controller and the
|
|
82
82
|
* signal, just use this function.
|
|
83
83
|
*/
|
|
84
|
-
getAbortController(
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
getAbortController(args: TArgs): AbortController | null;
|
|
85
|
+
getAbortControllerByKey(queryKey: string): AbortController | null;
|
|
86
|
+
/**
|
|
87
|
+
* Get the AbortSignal to check for query abortions.
|
|
88
|
+
*
|
|
89
|
+
* Example:
|
|
90
|
+
* ```
|
|
91
|
+
* const getUserQuery = sharedQuery({
|
|
92
|
+
* queryName: "getUser",
|
|
93
|
+
* queryFn: (userId: string) => {
|
|
94
|
+
* // Get the signal for this current execution.
|
|
95
|
+
* const signal = getUserQuery.getAbortSignal([userId]);
|
|
96
|
+
*
|
|
97
|
+
* // Pass the signal to fetch so that it can be aborted.
|
|
98
|
+
* const response = await fetch(`/users/${userId}`, { signal });
|
|
99
|
+
*
|
|
100
|
+
* // Check for errors.
|
|
101
|
+
* if (!response.ok) {
|
|
102
|
+
* throw new Error(response.statusText);
|
|
103
|
+
* }
|
|
104
|
+
*
|
|
105
|
+
* // Return the data.
|
|
106
|
+
* return await response.json();
|
|
107
|
+
* },
|
|
108
|
+
* });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
getAbortSignal(args: TArgs): AbortSignal | null;
|
|
112
|
+
getAbortSignalByKey(queryKey: string): AbortSignal | null;
|
|
87
113
|
private isStale;
|
|
88
114
|
private isExpired;
|
|
89
115
|
getCachedOrFetch(...args: TArgs): Promise<TData>;
|
|
90
116
|
private dedupedFetch;
|
|
91
117
|
private startFetching;
|
|
118
|
+
/** Get the entire stored entry for a query key. */
|
|
119
|
+
getQueryValue(queryKey: string): QueryStateValue<TData> | undefined;
|
|
120
|
+
/** Get the current stored data for a query key. */
|
|
121
|
+
getData(queryKey: string): TData | undefined;
|
|
92
122
|
/** Set the data directly if the user obtained it from somewhere else. */
|
|
93
|
-
setData(queryKey: string, data: TData, dataUpdatedMs?: number):
|
|
94
|
-
/**
|
|
123
|
+
setData(queryKey: string, data: TData, dataUpdatedMs?: number): void;
|
|
124
|
+
/**
|
|
125
|
+
* Do a new fetch even when the data is already cached, but don't fetch if
|
|
126
|
+
* another fetch is already inflight.
|
|
127
|
+
*/
|
|
95
128
|
updateFromSource(...args: TArgs): Promise<TData>;
|
|
96
129
|
/** Delete the cached data for one set of arguments. */
|
|
97
130
|
deleteData(queryKey: string): void;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var F=Object.create;var T=Object.defineProperty;var L=Object.getOwnPropertyDescriptor;var R=Object.getOwnPropertyNames;var _=Object.getPrototypeOf,j=Object.prototype.hasOwnProperty;var J=(i,e)=>{for(var t in e)T(i,t,{get:e[t],enumerable:!0})},Q=(i,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of R(e))!j.call(i,a)&&a!==t&&T(i,a,{get:()=>e[a],enumerable:!(r=L(e,a))||r.enumerable});return i};var K=(i,e,t)=>(t=i!=null?F(_(i)):{},Q(e||!i||!i.__esModule?T(t,"default",{value:i,enumerable:!0}):t,i)),W=i=>Q(T({},"__esModule",{value:!0}),i);var X={};J(X,{SharedQuery:()=>x,SharedQueryDefaults:()=>h,sharedQuery:()=>I,useSharedQuery:()=>H});module.exports=W(X);var q=require("@choksheak/ts-utils/timeConstants"),S=K(require("object-sizeof")),p=require("react");var B=require("@choksheak/ts-utils/timeConstants"),z=require("react");var f=require("@choksheak/ts-utils/kvStore");function k(i,e){let{localStorageKey:t,indexedDbKey:r,storeExpiryMs:a,store:s}=i??{};if(s)return s;let n=a||e;return t?new w(t,n):r?new D(r,n):null}var w=class{constructor(e,t){this.keyPrefix=e;this.expiryMs=t}save(e,t){if(typeof window>"u")return;let r=this.keyPrefix+":"+e;window.localStorage.setItem(r,JSON.stringify({value:t,expiresAt:this.expiryMs?Date.now()+this.expiryMs:0}))}load(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e,r=window.localStorage.getItem(t);if(r)try{let a=JSON.parse(r);if(!a||a.expiresAt&&Date.now()>=a.expiresAt){window.localStorage.removeItem(t);return}return a.value}catch{window.localStorage.removeItem(t);return}}delete(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e;window.localStorage.removeItem(t)}clear(){if(!(typeof window>"u"))for(let e of Object.keys(window.localStorage))e.startsWith(this.keyPrefix)&&window.localStorage.removeItem(e)}},D=class{constructor(e,t){this.keyPrefix=e;this.expiryMs=t}async save(e,t){if(typeof window>"u")return;let r=this.keyPrefix+":"+e;await f.kvStore.set(r,JSON.stringify(t),this.expiryMs)}async load(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e,r=await f.kvStore.get(t);if(r!==void 0)try{return JSON.parse(r)}catch{await f.kvStore.delete(t);return}}async delete(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e;await f.kvStore.delete(t)}async clear(){typeof window>"u"||await f.kvStore.forEach(async e=>{e.startsWith(this.keyPrefix)&&await f.kvStore.delete(e)})}};var O={storeExpiryMs:30*B.MS_PER_DAY,lazyLoad:!1},A=class{dataByKey=new Map;subscribersByKey=new Map;get(e){return this.dataByKey.get(e)}set(e,t){let r=this.dataByKey.get(e);r!==t&&(this.dataByKey.set(e,t),this.notify(e,t,r))}delete(e){this.dataByKey.delete(e)}setNoNotify(e,t){this.dataByKey.set(e,t)}subscribe(e,t){let r=this.subscribersByKey.get(e);return r||(r=new Set,this.subscribersByKey.set(e,r)),r.add(t),()=>{r.delete(t),r.size===0&&this.subscribersByKey.delete(e)}}notify(e,t,r){let a=this.subscribersByKey.get(e);if(!a)return;let s=Array.from(a.values());for(let n of s)try{n(t,r)}catch(l){console.error("Error invoking subscriber:",l)}}},g=new A,v="state",Y=0,M=class{constructor(e,t){this.defaultValue=e;this.pubSubKey=String(Y++),this.storageAdapter=k(t,O.storeExpiryMs),this.setValueBounded=s=>this.setValue(s),g.setNoNotify(this.pubSubKey,e);let r=t?.lazyLoad;(r!==void 0?r:O.lazyLoad)||this.initDefaultValueOnce()}pubSubKey;storageAdapter;setValueBounded;initDone=!1;subscribe(e){return g.subscribe(this.pubSubKey,()=>e())}getSnapshot(){return g.get(this.pubSubKey)}setValue(e){if(typeof e=="function"){let t=g.get(this.pubSubKey);e=e(t)}g.set(this.pubSubKey,e),this.storageAdapter?.save(v,e)}delete(){g.set(this.pubSubKey,this.defaultValue),this.storageAdapter?.save(v,this.defaultValue)}async initDefaultValueOnce(){if(this.initDone)return;this.initDone=!0;let e=await this.storageAdapter?.load(v);g.set(this.pubSubKey,e!==void 0?e:this.defaultValue)}};function V(i,e){return new M(i,e)}function P(i){return i.initDefaultValueOnce(),[(0,z.useSyncExternalStore)(t=>i.subscribe(t),()=>i.getSnapshot(),()=>i.getSnapshot()),i.setValueBounded]}function E(i){let e=new Set;return JSON.stringify(i,(t,r)=>{if(typeof r=="function")throw new Error("Functions should not be passed into query parameters");if(!r||typeof r!="object"||Array.isArray(r))return r;if(e.has(r))return"[Circular]";if(e.add(r),r!==i)try{JSON.stringify(r)}catch{return"[Unserializable]"}return Object.keys(r).sort().reduce((a,s)=>(a[s]=r[s],a),{})})}var $=K(require("lodash/isEqual")),m=require("react");function N(i){let[e,t]=(0,m.useState)(i);return(0,m.useMemo)(()=>e===i||(0,$.default)(e,i)?e:(t(i),i),[e,i])}var h={staleMs:0,expiryMs:30*q.MS_PER_DAY,refetchOnStale:!0,persistTo:void 0,maxSize:100,maxBytes:1e5,log:(...i)=>console.log("[sharedQuery]",...i)},x=class{inflightQueries=new Map;queryState;queryName;queryFn;expiryMs;staleMs;refetchOnStale;maxSize;maxBytes;log;constructor(e){if(this.queryName=e.queryName,this.queryFn=e.queryFn,this.staleMs=e?.staleMs??h.staleMs,this.expiryMs=e?.expiryMs??h.expiryMs,this.refetchOnStale=e?.refetchOnStale??h.refetchOnStale,!e.localStorageKey&&!e.indexedDbKey){let t=e.persistTo??h.persistTo;t==="localStorage"?e.localStorageKey=e.queryName:t==="indexedDb"&&(e.indexedDbKey=e.queryName)}this.maxSize=e?.maxSize??h.maxSize,this.maxBytes=e?.maxBytes??h.maxBytes,this.log=e?.log??h.log,this.queryState=V({},{...e,storeExpiryMs:e?.storeExpiryMs||this.expiryMs})}getQueryKey(e){return this.queryName+":"+E(e)}getAbortController(e){let t=this.getQueryKey(e);return this.getAbortControllerByKey(t)}getAbortControllerByKey(e){return this.inflightQueries.get(e)?.abortController??null}getAbortSignal(e){let t=this.getQueryKey(e);return this.getAbortSignalByKey(t)}getAbortSignalByKey(e){return this.getAbortControllerByKey(e)?.signal??null}isStale(e){return this.staleMs===0?!0:Date.now()-e>this.staleMs}isExpired(e){return this.expiryMs===0?!1:Date.now()-e>this.expiryMs}async getCachedOrFetch(...e){let t=this.getQueryKey(e),r=this.queryState.getSnapshot()?.[t];return r?.data&&!this.isExpired(r.dataUpdatedMs??0)?this.isStale(r.dataUpdatedMs??0)?(this.refetchOnStale&&(this.log(`refetchOnStale for ${t}`),this.dedupedFetch(t,...e)),this.log(`Returning stale data for ${t}`),r.data):(this.log(`Return fresh data ${t} from cache without fetching`),r.data):await this.dedupedFetch(t,...e)}dedupedFetch(e,...t){let r=this.inflightQueries.get(e);if(r)return this.log(`Deduplicating inflight fetch for ${e}`),r.promise;let a=this.startFetching(e,t),s=new AbortController;return this.inflightQueries.set(e,{promise:a,abortController:s}),a}async startFetching(e,t){this.log(`Start fetching ${e}`);try{let r=await this.queryFn(...t);return this.log(`Successfully fetched ${e}`),this.setData(e,r,Date.now()),r}catch(r){throw this.log(`Failed to fetch ${e}: ${r}`),r}finally{this.inflightQueries.delete(e)}}getQueryValue(e){return this.queryState.getSnapshot()?.[e]}getData(e){return this.queryState.getSnapshot()?.[e]?.data}setData(e,t,r=Date.now()){let a={lastUpdatedMs:Date.now(),loading:!1,data:t,dataUpdatedMs:r};this.queryState.setValue(s=>({...s,[e]:a})),this.enforceSizeLimit()}async updateFromSource(...e){let t=this.getQueryKey(e);return await this.dedupedFetch(t,...e)}deleteData(e){let r={...this.queryState.getSnapshot()};delete r[e],this.queryState.setValue(r);let a=this.inflightQueries.get(e);a&&(this.inflightQueries.delete(e),a.abortController.abort("Deleted"))}clear(){this.queryState.delete();for(let{abortController:e}of Array.from(this.inflightQueries.values()))e.abort("Cleared");this.inflightQueries.clear()}enforceSizeLimit(){if(!this.maxSize&&!this.maxBytes)return;let e=this.queryState.getSnapshot(),t=Object.keys(e).length;if(t<=1)return;let r=(0,S.default)(e),a=e,s=!1;if(this.maxSize){let n=t-this.maxSize;n>0&&(this.log(`enforceSizeLimit for ${this.queryName} needed due to size ${t} > ${this.maxSize}`),a={...a},s=!0,Object.entries(a).sort((l,c)=>c[1].lastUpdatedMs-l[1].lastUpdatedMs).slice(-n).forEach(([l])=>{delete a[l]}))}if(this.maxBytes&&Object.keys(a).length>1){let n=s?(0,S.default)(a):r,l=n-this.maxBytes;if(l>0){this.log(`enforceSizeLimit for ${this.queryName} needed due to byte size ${n.toLocaleString()} > ${this.maxBytes.toLocaleString()}`),s||(a={...a},s=!0);let c=Object.entries(a).sort((y,d)=>y[1].lastUpdatedMs-d[1].lastUpdatedMs);for(let y=0;y<c.length-1&&l>0;y++){let d=c[y][0],u=(0,S.default)(d)+(0,S.default)(a[d]);delete a[d],l-=u}}}if(s){this.queryState.setValue(a),this.log(`Trimmed data for ${this.queryName}: size=(${t} -> ${Object.keys(a).length}) (limit=${this.maxSize}), byteSize=(${r.toLocaleString()} -> ${(0,S.default)(a).toLocaleString()}) (limit=${this.maxBytes.toLocaleString()})`);return}this.log(`No need to trim data for ${this.queryName}: size=${t} (limit=${this.maxSize}), byteSize=${r.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`)}},U=new Set;function I(i){if(U.has(i.queryName))throw new Error(`Duplicate shared query "${i.queryName}"`);return U.add(i.queryName),new x(i)}var G={lastUpdatedMs:0,loading:!0};function H(i,e=[]){let t=N(e),[r,a]=P(i.queryState),s=(0,p.useRef)(!0),n=(0,p.useMemo)(()=>i.getQueryKey(t),[i,t]),l=r[n],c=(0,p.useCallback)(d=>{a(u=>{let o={...u};return o[n]=typeof d=="function"?d(o[n]):d,o})},[n,a]),y=(0,p.useCallback)(async d=>{i.log(`Begin executing shared query ${n}`),c(u=>({...u??{},lastUpdatedMs:Date.now(),loading:!0}));try{let u=await(d?i.updateFromSource(...t):i.getCachedOrFetch(...t));if(s.current){let o=Date.now();c({lastUpdatedMs:o,loading:!1,data:u,dataUpdatedMs:o})}}catch(u){if(s.current){let o=Date.now();c(b=>({...b??{},lastUpdatedMs:o,loading:!1,error:u,errorUpdatedMs:o}))}}},[i,n,c,t]);return(0,p.useEffect)(()=>(s.current=!0,y(!1),()=>{s.current=!1}),[y]),(0,p.useMemo)(()=>({...l??G,abortCurrentQuery:u=>{let o=i.getAbortControllerByKey(n);return o?.abort(u),!!o},refetch:()=>{y(!0)},setData:(u,o)=>{let b=Date.now();c(C=>({...C??{},data:u,dataUpdatedMs:o??b,lastUpdatedMs:b,loading:!1}))},deleteData:()=>{a(u=>{let o={...u};return delete o[n],o})}}),[y,i,n,l,a,c])}0&&(module.exports={SharedQuery,SharedQueryDefaults,sharedQuery,useSharedQuery});
|
|
2
|
+
//# sourceMappingURL=sharedQuery.min.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sharedQuery.ts","../src/sharedState.ts","../src/utils/storage.ts","../src/utils/stringify.ts","../src/utils/useDeepMemo.ts"],"sourcesContent":["import { MS_PER_DAY } from \"@choksheak/ts-utils/timeConstants\";\nimport getByteSize from \"object-sizeof\";\nimport { useCallback, useEffect, useMemo, useRef } from \"react\";\n\nimport {\n SharedState,\n sharedState,\n SharedStateOptions,\n useSharedState,\n} from \"./sharedState\";\nimport { stringifyDeterministicForKeys } from \"./utils/stringify\";\nimport { useDeepMemo } from \"./utils/useDeepMemo\";\n\nexport type QueryFn<TArgs extends unknown[], TData> = (\n ...args: TArgs\n) => Promise<TData> | TData;\n\nexport type QueryStateValue<TData> = {\n /**\n * When this record was last updated. Used for LRU determination. If you want\n * the data age, please use `dataUpdatedMs` instead.\n */\n lastUpdatedMs: number;\n\n loading: boolean;\n data?: TData;\n dataUpdatedMs?: number;\n\n /**\n * Use unknown type for error because we can't assume what type of variable\n * will be thrown from the query function. We also don't want to add this\n * type to QueryStateValue<TError> as it adds complexity to the type.\n */\n error?: unknown;\n\n errorUpdatedMs?: number;\n};\n\nexport type UseQueryResult<TData> = QueryStateValue<TData> & {\n /**\n * Cancels the inflight query (if any). Returns true if canceled.\n */\n abortCurrentQuery: (reason?: unknown) => boolean;\n\n /**\n * Manually refresh the data by doing a new query.\n */\n refetch: () => void;\n\n /**\n * If you got the data from somewhere else (e.g. an in-memory data update),\n * just set it directly.\n */\n setData: (data: TData, dataUpdatedMs?: number) => void;\n\n deleteData: () => void;\n};\n\nexport type PersistTo = \"localStorage\" | \"indexedDb\";\n\n/**\n * The full query state is a record of all the fetch states mapped by\n * query key to the state entry.\n */\nexport type SharedQueryState<TData> = Record<string, QueryStateValue<TData>>;\n\nexport type SharedQueryOptions<TArgs extends unknown[], TData> = {\n // The name could have been auto-generated, but we let the user give us a\n // human-readable name that can be identified quickly in the logs.\n queryName: string;\n\n // Function to fetch data.\n queryFn: QueryFn<TArgs, TData>;\n\n // ms before data considered stale; 0 means always stale.\n staleMs?: number;\n\n // ms before data is removed from cache; 0 means never expire.\n expiryMs?: number;\n\n // Trigger background re-fetch if stale.\n refetchOnStale?: boolean;\n\n // Shortcut to set the localStorageKey or indexedDbKey automatically from\n // queryName. This is important because it avoids requiring the user to\n // specify the same queryName twice everytime.\n persistTo?: PersistTo;\n\n // Max number of entries to keep. The oldest entries will be discarded.\n // The last record cannot be discarded. 0 means no limit.\n maxSize?: number;\n\n // Max number of bytes to keep. The oldest entries will be discarded.\n // The last record cannot be discarded. 0 means no limit.\n maxBytes?: number;\n\n // Customize the logger.\n log?: (...args: unknown[]) => void;\n} & SharedStateOptions<SharedQueryState<TData>>;\n\n/** Users can override these values globally. */\nexport const SharedQueryDefaults = {\n // Refetch on every page load.\n staleMs: 0,\n\n // Keep in cache for a long time.\n expiryMs: 30 * MS_PER_DAY,\n\n // Always refetch automatically if the data is stale.\n refetchOnStale: true,\n\n // No default persistence configured.\n persistTo: undefined as PersistTo | undefined,\n\n // 100 is not too large, but please tweak accordingly.\n maxSize: 100,\n\n // 100kb limit before discarding old records.\n maxBytes: 100_000,\n\n // Default to log to console.\n log: (...args: unknown[]) => console.log(\"[sharedQuery]\", ...args),\n};\n\nexport class SharedQuery<TArgs extends unknown[], TData> {\n private readonly inflightQueries = new Map<\n string,\n { promise: Promise<TData>; abortController: AbortController }\n >();\n\n public readonly queryState: SharedState<SharedQueryState<TData>>;\n\n public readonly queryName: string;\n private readonly queryFn: QueryFn<TArgs, TData>;\n public readonly expiryMs: number;\n public readonly staleMs: number;\n public readonly refetchOnStale: boolean;\n public readonly maxSize: number;\n public readonly maxBytes: number;\n public readonly log: (...args: unknown[]) => void;\n\n public constructor(options: SharedQueryOptions<TArgs, TData>) {\n this.queryName = options.queryName;\n this.queryFn = options.queryFn;\n\n this.staleMs = options?.staleMs ?? SharedQueryDefaults.staleMs;\n this.expiryMs = options?.expiryMs ?? SharedQueryDefaults.expiryMs;\n\n this.refetchOnStale =\n options?.refetchOnStale ?? SharedQueryDefaults.refetchOnStale;\n\n // Apply shortcut when using `persistTo`.\n // Specifying the storage keys will take precedence over `persistTo`.\n if (!options.localStorageKey && !options.indexedDbKey) {\n const persistTo = options.persistTo ?? SharedQueryDefaults.persistTo;\n\n if (persistTo === \"localStorage\") {\n options.localStorageKey = options.queryName;\n } else if (persistTo === \"indexedDb\") {\n options.indexedDbKey = options.queryName;\n }\n }\n\n this.maxSize = options?.maxSize ?? SharedQueryDefaults.maxSize;\n this.maxBytes = options?.maxBytes ?? SharedQueryDefaults.maxBytes;\n\n this.log = options?.log ?? SharedQueryDefaults.log;\n\n this.queryState = sharedState<SharedQueryState<TData>>(\n {}, // defaultValue\n {\n ...options,\n // Fallback to use expiryMs for the storeExpiryMs.\n storeExpiryMs: options?.storeExpiryMs || this.expiryMs,\n },\n );\n }\n\n /** Returns a key to identify requests based on the given args. */\n public getQueryKey(args: TArgs): string {\n return this.queryName + \":\" + stringifyDeterministicForKeys(args);\n }\n\n /**\n * Get the AbortController to abort the inflight query (if any).\n *\n * Note that once you get the controller, you can also get the signal easily\n * using `controller.signal`. So if you need both the controller and the\n * signal, just use this function.\n */\n public getAbortController(args: TArgs): AbortController | null {\n const queryKey = this.getQueryKey(args);\n return this.getAbortControllerByKey(queryKey);\n }\n\n public getAbortControllerByKey(queryKey: string): AbortController | null {\n return this.inflightQueries.get(queryKey)?.abortController ?? null;\n }\n\n /**\n * Get the AbortSignal to check for query abortions.\n *\n * Example:\n * ```\n * const getUserQuery = sharedQuery({\n * queryName: \"getUser\",\n * queryFn: (userId: string) => {\n * // Get the signal for this current execution.\n * const signal = getUserQuery.getAbortSignal([userId]);\n *\n * // Pass the signal to fetch so that it can be aborted.\n * const response = await fetch(`/users/${userId}`, { signal });\n *\n * // Check for errors.\n * if (!response.ok) {\n * throw new Error(response.statusText);\n * }\n *\n * // Return the data.\n * return await response.json();\n * },\n * });\n * ```\n */\n public getAbortSignal(args: TArgs): AbortSignal | null {\n const queryKey = this.getQueryKey(args);\n return this.getAbortSignalByKey(queryKey);\n }\n\n public getAbortSignalByKey(queryKey: string): AbortSignal | null {\n return this.getAbortControllerByKey(queryKey)?.signal ?? null;\n }\n\n private isStale(dataUpdatedMs: number): boolean {\n if (this.staleMs === 0) return true; // always stale\n\n const ageMs = Date.now() - dataUpdatedMs;\n return ageMs > this.staleMs;\n }\n\n private isExpired(dataUpdatedMs: number): boolean {\n if (this.expiryMs === 0) return false; // never expire\n\n const ageMs = Date.now() - dataUpdatedMs;\n return ageMs > this.expiryMs;\n }\n\n public async getCachedOrFetch(...args: TArgs): Promise<TData> {\n const queryKey = this.getQueryKey(args);\n const cached = this.queryState.getSnapshot()?.[queryKey];\n\n if (cached?.data && !this.isExpired(cached.dataUpdatedMs ?? 0)) {\n // Return cached value if not stale.\n if (!this.isStale(cached.dataUpdatedMs ?? 0)) {\n this.log(`Return fresh data ${queryKey} from cache without fetching`);\n return cached.data;\n }\n\n // If stale, optionally update the data in the background.\n if (this.refetchOnStale) {\n this.log(`refetchOnStale for ${queryKey}`);\n void this.dedupedFetch(queryKey, ...args);\n }\n\n // Still return stale data immediately.\n this.log(`Returning stale data for ${queryKey}`);\n return cached.data;\n }\n\n return await this.dedupedFetch(queryKey, ...args);\n }\n\n private dedupedFetch(queryKey: string, ...args: TArgs): Promise<TData> {\n const inflightQuery = this.inflightQueries.get(queryKey);\n\n // De-duplicate in-flight requests\n if (inflightQuery) {\n this.log(`Deduplicating inflight fetch for ${queryKey}`);\n return inflightQuery.promise;\n }\n\n // Fetch new data\n const promise = this.startFetching(queryKey, args);\n const abortController = new AbortController();\n\n this.inflightQueries.set(queryKey, { promise, abortController });\n\n return promise;\n }\n\n private async startFetching(queryKey: string, args: TArgs) {\n this.log(`Start fetching ${queryKey}`);\n\n try {\n const data = await this.queryFn(...args);\n\n this.log(`Successfully fetched ${queryKey}`);\n this.setData(queryKey, data, Date.now());\n return data;\n } catch (e) {\n this.log(`Failed to fetch ${queryKey}: ${e}`);\n throw e;\n } finally {\n this.inflightQueries.delete(queryKey);\n }\n }\n\n /** Get the entire stored entry for a query key. */\n public getQueryValue(queryKey: string): QueryStateValue<TData> | undefined {\n return this.queryState.getSnapshot()?.[queryKey];\n }\n\n /** Get the current stored data for a query key. */\n public getData(queryKey: string): TData | undefined {\n return this.queryState.getSnapshot()?.[queryKey]?.data;\n }\n\n /** Set the data directly if the user obtained it from somewhere else. */\n public setData(\n queryKey: string,\n data: TData,\n dataUpdatedMs = Date.now(),\n ): void {\n const entry: QueryStateValue<TData> = {\n lastUpdatedMs: Date.now(),\n loading: false,\n data,\n dataUpdatedMs,\n };\n\n this.queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));\n\n this.enforceSizeLimit();\n }\n\n /**\n * Do a new fetch even when the data is already cached, but don't fetch if\n * another fetch is already inflight.\n */\n public async updateFromSource(...args: TArgs): Promise<TData> {\n const key = this.getQueryKey(args);\n return await this.dedupedFetch(key, ...args);\n }\n\n /** Delete the cached data for one set of arguments. */\n public deleteData(queryKey: string): void {\n const record = this.queryState.getSnapshot();\n const clone = { ...record };\n delete clone[queryKey];\n this.queryState.setValue(clone);\n\n const inflight = this.inflightQueries.get(queryKey);\n if (inflight) {\n this.inflightQueries.delete(queryKey);\n inflight.abortController.abort(\"Deleted\");\n }\n }\n\n /** Delete all currently cached data & all inflight promises. */\n public clear(): void {\n this.queryState.delete();\n\n for (const { abortController } of Array.from(\n this.inflightQueries.values(),\n )) {\n abortController.abort(\"Cleared\");\n }\n\n this.inflightQueries.clear();\n }\n\n /**\n * Discard the oldest entries if the size exceeds the limit. The last\n * remaining record cannot be deleted no matter what the limit is.\n */\n public enforceSizeLimit(): void {\n // Skip if there are no limits.\n if (!this.maxSize && !this.maxBytes) {\n return;\n }\n\n const oldState = this.queryState.getSnapshot();\n const oldSize = Object.keys(oldState).length;\n\n // Cannot do GC if size is 1 or less.\n if (oldSize <= 1) {\n return;\n }\n\n const oldByteSize = getByteSize(oldState);\n\n let newState = oldState;\n let needUpdate = false;\n\n // Limit by number of records.\n if (this.maxSize) {\n const numToCut = oldSize - this.maxSize;\n\n if (numToCut > 0) {\n this.log(\n `enforceSizeLimit for ${this.queryName} needed due to size ${oldSize} > ${this.maxSize}`,\n );\n\n newState = { ...newState }; // shallow clone\n needUpdate = true;\n\n Object.entries(newState)\n .sort(\n (entry1, entry2) =>\n // Sort descending.\n entry2[1].lastUpdatedMs - entry1[1].lastUpdatedMs,\n )\n .slice(-numToCut)\n .forEach(([key]) => {\n delete newState[key];\n });\n }\n }\n\n // Limit by byte size.\n if (this.maxBytes && Object.keys(newState).length > 1) {\n // Recompute only if changed by above code.\n const currentByteSize = needUpdate ? getByteSize(newState) : oldByteSize;\n\n let bytesToCut = currentByteSize - this.maxBytes;\n\n if (bytesToCut > 0) {\n this.log(\n `enforceSizeLimit for ${this.queryName} needed due to byte size ${currentByteSize.toLocaleString()} > ${this.maxBytes.toLocaleString()}`,\n );\n\n // Shallow clone only if not already cloned above.\n if (!needUpdate) {\n newState = { ...newState };\n needUpdate = true;\n }\n\n const entriesByTimeAscending = Object.entries(newState).sort(\n (entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs,\n );\n\n for (\n let i = 0;\n // Leave the last record entriesByTimeAscending[len-1] untouched.\n i < entriesByTimeAscending.length - 1 && bytesToCut > 0;\n i++\n ) {\n const key = entriesByTimeAscending[i][0];\n const thisSize = getByteSize(key) + getByteSize(newState[key]);\n delete newState[key];\n bytesToCut -= thisSize;\n }\n }\n }\n\n // Update the state.\n if (needUpdate) {\n this.queryState.setValue(newState);\n\n // Log to inform user that the data was trimmed.\n this.log(\n `Trimmed data for ${this.queryName}: size=(${oldSize} -> ${Object.keys(newState).length}) (limit=${this.maxSize}), byteSize=(${oldByteSize.toLocaleString()} -> ${getByteSize(newState).toLocaleString()}) (limit=${this.maxBytes.toLocaleString()})`,\n );\n\n return;\n }\n\n // Log to indicate why trimming was not needed.\n this.log(\n `No need to trim data for ${this.queryName}: size=${oldSize} (limit=${this.maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`,\n );\n }\n}\n\nconst seenQueryNames = new Set<string>();\n\n/**\n * Create a reusable shared query object that can be used to auto de-duplicate\n * and cache all data fetches, with auto-expiration and staleness checks.\n *\n * Each `queryName` must be unique, and can only be declared once, most often\n * in the top level scope. The queryName is used for logging only.\n *\n * Example:\n * ```\n * export const usersQuery = sharedQuery({\n * queryName: \"users\",\n * queryFn: listUsers,\n * persistTo: \"indexedDb\",\n * staleMs: MS_PER_DAY,\n * });\n * ```\n */\nexport function sharedQuery<TArgs extends unknown[], TData>(\n // The name could have been auto-generated, but we let the user give us a\n // human-readable name that can be identified quickly in the logs.\n options: SharedQueryOptions<TArgs, TData>,\n): SharedQuery<TArgs, TData> {\n if (seenQueryNames.has(options.queryName)) {\n throw new Error(`Duplicate shared query \"${options.queryName}\"`);\n }\n seenQueryNames.add(options.queryName);\n\n return new SharedQuery(options);\n}\n\nconst DEFAULT_QUERY_STATE_ENTRY: QueryStateValue<unknown> = {\n lastUpdatedMs: 0,\n loading: true,\n};\n\n/**\n * React hook to make use of a shared query inside any React component.\n *\n * Example:\n * ```\n * const users = useSharedQuery(usersQuery, [\"123\"]);\n * ```\n */\nexport function useSharedQuery<TArgs extends unknown[], TData>(\n query: SharedQuery<TArgs, TData>,\n // Default to use no arguments.\n args: TArgs = [] as unknown as TArgs,\n): UseQueryResult<TData> {\n // We don't expect users to provide a stable `args`, so stabilize it here.\n const stableArgs = useDeepMemo(args);\n\n const [queryState, setQueryState] = useSharedState(query.queryState);\n\n const isMounted = useRef(true);\n\n const queryKey = useMemo(\n () => query.getQueryKey(stableArgs),\n [query, stableArgs],\n );\n\n // Make sure typechecker knows this could be undefined.\n const queryStateValue = queryState[queryKey] as\n | QueryStateValue<TData>\n | undefined;\n\n const setQueryStateValue = useCallback(\n (\n arg:\n | QueryStateValue<TData>\n | ((\n prev: QueryStateValue<TData> | undefined,\n ) => QueryStateValue<TData>),\n ) => {\n setQueryState((prev) => {\n const clone = { ...prev };\n clone[queryKey] =\n typeof arg === \"function\" ? arg(clone[queryKey]) : arg;\n return clone;\n });\n },\n [queryKey, setQueryState],\n );\n\n // The fetch logic wrapped in useCallback to be stable for useEffect\n const execute = useCallback(\n async (forceRefresh: boolean) => {\n query.log(`Begin executing shared query ${queryKey}`);\n\n setQueryStateValue((prev) => ({\n // Keep old data and error.\n ...(prev ?? {}),\n lastUpdatedMs: Date.now(),\n loading: true,\n }));\n\n try {\n const data = await (forceRefresh\n ? query.updateFromSource(...stableArgs)\n : query.getCachedOrFetch(...stableArgs));\n\n if (isMounted.current) {\n const now = Date.now();\n\n setQueryStateValue({\n // Don't keep old error.\n lastUpdatedMs: now,\n loading: false,\n data,\n dataUpdatedMs: now,\n });\n }\n } catch (e) {\n if (isMounted.current) {\n const now = Date.now();\n\n setQueryStateValue((prev) => ({\n // Keep old data.\n ...(prev ?? {}),\n lastUpdatedMs: now,\n loading: false,\n error: e,\n errorUpdatedMs: now,\n }));\n }\n }\n },\n [query, queryKey, setQueryStateValue, stableArgs],\n );\n\n useEffect(() => {\n isMounted.current = true;\n\n void execute(false);\n\n return () => {\n isMounted.current = false;\n };\n }, [execute]);\n\n return useMemo(() => {\n const state =\n queryStateValue ?? (DEFAULT_QUERY_STATE_ENTRY as QueryStateValue<TData>);\n\n return {\n ...state,\n // This works only if the user-given queryFn supports abort. If not,\n // this function doesn't do anything, since we don't have any means to\n // abort the running queryFn.\n abortCurrentQuery: (reason?: unknown) => {\n const controller = query.getAbortControllerByKey(queryKey);\n controller?.abort(reason);\n return Boolean(controller);\n },\n refetch: () => {\n void execute(true);\n },\n setData: (data: TData, dataUpdatedMs?: number) => {\n const now = Date.now();\n setQueryStateValue((prev) => ({\n ...(prev ?? {}),\n data,\n dataUpdatedMs: dataUpdatedMs ?? now,\n lastUpdatedMs: now,\n loading: false,\n }));\n },\n deleteData: () => {\n setQueryState((prev) => {\n const clone = { ...prev };\n delete clone[queryKey];\n return clone;\n });\n },\n };\n }, [\n execute,\n query,\n queryKey,\n queryStateValue,\n setQueryState,\n setQueryStateValue,\n ]);\n}\n","import { MS_PER_DAY } from \"@choksheak/ts-utils/timeConstants\";\nimport { useSyncExternalStore } from \"react\";\n\nimport {\n getStorageAdapter,\n StorageAdapter,\n StorageOptions,\n} from \"./utils/storage\";\n\n/**\n * The \"shared state\" is a React state that shares the same value across the\n * entire app. It is largely similar to the jotai library, but is much smaller\n * and much easier to work with when used for persisting in local storage or\n * indexed db, with an automatic expiration for persisted values.\n *\n * This library aims to be the shortest, simplest, easiest to use, and most\n * efficient implementation of shared global React states with seamless support\n * for client persistence.\n */\n\n/************************************************************************/\n/* Global config */\n/************************************************************************/\n\n/**\n * These values are configurable by the user at any time.\n */\nexport const SharedStateDefaults = {\n /**\n * Default duration for storage expiration in milliseconds. Initial value\n * is 30 days, but can be changed to anything.\n */\n storeExpiryMs: 30 * MS_PER_DAY,\n\n /**\n * True to load from storage only on mount, false (default) to load in\n * the top level scope when the shared state is defined.\n */\n lazyLoad: false,\n};\n\n/************************************************************************/\n/* Shared state implementation */\n/************************************************************************/\n\ntype Subscriber<T> = (next: T, prev: T) => void;\n\nclass PubSubStore {\n private dataByKey = new Map<string, unknown>();\n private subscribersByKey = new Map<string, Set<Subscriber<unknown>>>();\n\n // This could be undefined if T includes undefined, else it should never\n // return undefined because the user has to always specify the default value.\n public get<T>(key: string): T {\n return this.dataByKey.get(key) as T;\n }\n\n public set<T>(key: string, value: T): void {\n const prev = this.dataByKey.get(key) as T;\n\n // Do nothing if no change.\n if (prev === value) return;\n\n this.dataByKey.set(key, value);\n this.notify(key, value, prev);\n }\n\n public delete(key: string): void {\n this.dataByKey.delete(key);\n }\n\n public setNoNotify<T>(key: string, value: T): void {\n this.dataByKey.set(key, value);\n }\n\n // Subscribe to changes of a specific key\n public subscribe<T>(key: string, subscriber: Subscriber<T>): () => void {\n let subscribers = this.subscribersByKey.get(key);\n if (!subscribers) {\n subscribers = new Set();\n this.subscribersByKey.set(key, subscribers);\n }\n\n // Type coercion: we store Subscriber<unknown>\n subscribers.add(subscriber as Subscriber<unknown>);\n\n // Return unsubscribe function\n return () => {\n subscribers.delete(subscriber as Subscriber<unknown>);\n if (subscribers.size === 0) this.subscribersByKey.delete(key);\n };\n }\n\n private notify<T>(key: string, next: T, prev: T): void {\n const subscribers = this.subscribersByKey.get(key);\n if (!subscribers) return;\n\n // Make copy to protect against mutation during iteration.\n const copy = Array.from(subscribers.values());\n for (const subscriber of copy) {\n try {\n subscriber(next, prev);\n } catch (e) {\n console.error(`Error invoking subscriber:`, e);\n }\n }\n }\n}\n\nconst pubSubStore = new PubSubStore();\n\nconst STORAGE_KEY = \"state\";\n\nexport type SharedStateOptions<T> = {\n lazyLoad?: boolean;\n} & StorageOptions<T>;\n\nlet stateKey = 0;\n\nexport class SharedState<T> {\n private readonly pubSubKey: string;\n private readonly storageAdapter: StorageAdapter<T> | null;\n public readonly setValueBounded: (next: T | ((prev: T) => T)) => void;\n public initDone = false;\n\n public constructor(\n private readonly defaultValue: T,\n options?: SharedStateOptions<T>,\n ) {\n this.pubSubKey = String(stateKey++);\n\n // Use max of 1 storage adapter per shared state.\n this.storageAdapter = getStorageAdapter(\n options,\n SharedStateDefaults.storeExpiryMs,\n );\n\n // Same as this.setValue, but binding the `this` reference.\n this.setValueBounded = (arg) => this.setValue(arg);\n\n // Always set the default value first to avoid returning undefineds if that\n // is not part of T.\n pubSubStore.setNoNotify(this.pubSubKey, defaultValue);\n\n const lazyLoad = options?.lazyLoad;\n const lazy =\n lazyLoad !== undefined ? lazyLoad : SharedStateDefaults.lazyLoad;\n\n if (!lazy) {\n void this.initDefaultValueOnce();\n }\n }\n\n // `subscribe` function required by useSyncExternalStore\n public subscribe(subscriber: () => void): () => void {\n return pubSubStore.subscribe(this.pubSubKey, () => subscriber());\n }\n\n // Get the value of the shared state.\n // `getSnapshot` function — must return same ref if value unchanged\n public getSnapshot(): T {\n return pubSubStore.get(this.pubSubKey);\n }\n\n // Set the value of the shared state.\n public setValue(next: T | ((prev: T) => T)): void {\n if (typeof next === \"function\") {\n const prev = pubSubStore.get<T>(this.pubSubKey);\n next = (next as (p: T) => T)(prev);\n }\n\n pubSubStore.set(this.pubSubKey, next);\n void this.storageAdapter?.save(STORAGE_KEY, next);\n }\n\n // Remove the value of the shared state. Actually this just sets the value\n // back to the given default value.\n public delete() {\n pubSubStore.set(this.pubSubKey, this.defaultValue);\n void this.storageAdapter?.save(STORAGE_KEY, this.defaultValue);\n }\n\n // Initial default handling\n public async initDefaultValueOnce() {\n if (this.initDone) return;\n this.initDone = true;\n\n const v = await this.storageAdapter?.load(STORAGE_KEY);\n pubSubStore.set(this.pubSubKey, v !== undefined ? v : this.defaultValue);\n }\n}\n\n/**\n * Create a new shared state object. Put this code at the top level scope:\n *\n * const myState = sharedState<number>(0);\n */\nexport function sharedState<T>(\n defaultValue: T,\n options?: SharedStateOptions<T>,\n): SharedState<T> {\n return new SharedState(defaultValue, options);\n}\n\n/**\n * React hook for subscribing to a key in the global store.\n * const [my, setMy] = useSharedState(myState);\n */\nexport function useSharedState<T>(\n state: SharedState<T>,\n): [T, (next: T | ((prev: T) => T)) => void] {\n state.initDefaultValueOnce();\n\n const value = useSyncExternalStore(\n (onStoreChange) => state.subscribe(onStoreChange),\n () => state.getSnapshot(),\n // getServerSnapshot is needed to prevent SSR dev errors in nextjs.\n () => state.getSnapshot(),\n );\n\n return [value, state.setValueBounded];\n}\n\n/** E.g. const my = useSharedStateValue(myState) */\nexport function useSharedStateValue<T>(state: SharedState<T>) {\n return useSharedState(state)[0];\n}\n\n/** E.g. const setMy = useSharedStateSetter(myState) */\nexport function useSharedStateSetter<T>(state: SharedState<T>) {\n return useSharedState(state)[1];\n}\n","import { kvStore } from \"@choksheak/ts-utils/kvStore\";\n\nexport type StorageAdapter<T> = {\n save: (key: string, value: T) => Promise<void> | void;\n load: (key: string) => Promise<T | undefined> | T | undefined;\n delete: (key: string) => Promise<void> | void;\n clear: () => Promise<void> | void;\n};\n\nexport type StorageOptions<T> = {\n // If specified, this will be used and all other fields will be ignored.\n store?: StorageAdapter<T>;\n\n // Storage key in local storage for this data.\n localStorageKey?: string;\n\n // Storage key in indexed db for this data.\n indexedDbKey?: string;\n\n // How long to wait before the value is considered expired.\n storeExpiryMs?: number;\n};\n\nexport function getStorageAdapter<T>(\n options: StorageOptions<T> | undefined,\n defaultStoreExpiryMs: number,\n): StorageAdapter<T> | null {\n const { localStorageKey, indexedDbKey, storeExpiryMs, store } = options ?? {};\n\n if (store) {\n return store;\n }\n\n const expiryMs = storeExpiryMs || defaultStoreExpiryMs;\n\n if (localStorageKey) {\n return new LocalStorageAdapter<T>(localStorageKey, expiryMs);\n }\n\n if (indexedDbKey) {\n return new IndexedDbAdapter<T>(indexedDbKey, expiryMs);\n }\n\n return null;\n}\n\n/************************************************************************/\n/* Storage adapters */\n/************************************************************************/\n\ntype ValueWithExpiration<T> = { value: T; expiresAt: number };\n\n/** Save state to memory only - not persisted. */\nexport class InMemoryStorageAdapter<T> implements StorageAdapter<T> {\n private readonly cache = new Map<string, ValueWithExpiration<T>>();\n\n public constructor(\n public readonly keyPrefix: string,\n public readonly expiryMs: number,\n ) {}\n\n public save(key: string, value: T): void {\n const fullKey = this.keyPrefix + \":\" + key;\n this.cache.set(fullKey, {\n value,\n expiresAt: Date.now() + this.expiryMs,\n });\n }\n\n public load(key: string): T | undefined {\n const fullKey = this.keyPrefix + \":\" + key;\n const cached = this.cache.get(fullKey);\n\n if (cached && Date.now() < cached.expiresAt) {\n return cached.value;\n }\n\n return undefined;\n }\n\n public delete(key: string): void {\n const fullKey = this.keyPrefix + \":\" + key;\n this.cache.delete(fullKey);\n }\n\n public clear(): void {\n this.cache.clear();\n }\n}\n\n/** Save state to local storage. */\nexport class LocalStorageAdapter<T> implements StorageAdapter<T> {\n public constructor(\n public readonly keyPrefix: string,\n public readonly expiryMs: number,\n ) {}\n\n public save(key: string, value: T): void {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n window.localStorage.setItem(\n fullKey,\n JSON.stringify({\n value,\n expiresAt: this.expiryMs ? Date.now() + this.expiryMs : 0,\n } satisfies ValueWithExpiration<T>),\n );\n }\n\n public load(key: string): T | undefined {\n if (typeof window === \"undefined\") return undefined;\n\n const fullKey = this.keyPrefix + \":\" + key;\n const raw = window.localStorage.getItem(fullKey);\n if (!raw) return undefined;\n\n try {\n const box = JSON.parse(raw) as ValueWithExpiration<T>;\n if (!box || (box.expiresAt && Date.now() >= box.expiresAt)) {\n window.localStorage.removeItem(fullKey);\n return undefined;\n }\n\n return box.value;\n } catch {\n // Ignore error.\n window.localStorage.removeItem(fullKey);\n return undefined;\n }\n }\n\n public delete(key: string): void {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n window.localStorage.removeItem(fullKey);\n }\n\n public clear(): void {\n if (typeof window === \"undefined\") return;\n\n for (const key of Object.keys(window.localStorage)) {\n if (key.startsWith(this.keyPrefix)) {\n window.localStorage.removeItem(key);\n }\n }\n }\n}\n\n/** Save state to indexed db. */\nexport class IndexedDbAdapter<T> implements StorageAdapter<T> {\n public constructor(\n public readonly keyPrefix: string,\n public readonly expiryMs: number,\n ) {}\n\n public async save(key: string, value: T): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n await kvStore.set(fullKey, JSON.stringify(value), this.expiryMs);\n }\n\n public async load(key: string): Promise<T | undefined> {\n if (typeof window === \"undefined\") return undefined;\n\n const fullKey = this.keyPrefix + \":\" + key;\n const raw = await kvStore.get<string>(fullKey);\n if (raw === undefined) return undefined;\n\n try {\n return JSON.parse(raw);\n } catch {\n // Ignore error.\n await kvStore.delete(fullKey);\n return undefined;\n }\n }\n\n public async delete(key: string): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n await kvStore.delete(fullKey);\n }\n\n public async clear(): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n await kvStore.forEach(async (k) => {\n if (k.startsWith(this.keyPrefix)) {\n await kvStore.delete(k);\n }\n });\n }\n}\n","/**\n * JSON stringify a value of any type, and returns an unsafe string. The string\n * is \"unsafe\" because you might not be able to restore the original object\n * using the serialized JSON. This is specifically used for query key generation\n * and is not suitable for more general uses.\n *\n * Note: The given value should not contain any functions because functions\n * cannot be serialized and deserialized properly.\n */\nexport function stringifyDeterministicForKeys(params: unknown): string {\n const cache = new Set();\n\n return JSON.stringify(params, (_, value) => {\n if (typeof value === \"function\") {\n throw new Error(`Functions should not be passed into query parameters`);\n }\n\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return value;\n }\n\n if (cache.has(value)) {\n // Circular reference detected! Return a placeholder string.\n return \"[Circular]\";\n }\n\n cache.add(value);\n\n // Don't crash on circular references, but don't mark the top level object\n // as circular because we want to return as much string as possible.\n if (value !== params) {\n try {\n JSON.stringify(value);\n } catch {\n return \"[Unserializable]\";\n }\n }\n\n return Object.keys(value)\n .sort()\n .reduce(\n (result: Record<string, unknown>, key: string) => {\n result[key] = value[key];\n return result;\n },\n {} as Record<string, unknown>,\n );\n });\n}\n","import isEqual from \"lodash/isEqual\";\nimport { useMemo, useState } from \"react\";\n\n/** Provides a stable value for T based on deep equality. */\nexport function useDeepMemo<T>(value: T): T {\n const [state, setState] = useState<T>(value);\n\n return useMemo(() => {\n if (state === value || isEqual(state, value)) {\n return state;\n }\n\n // We check equality, so this should be safe.\n // eslint-disable-next-line react-hooks/set-state-in-render\n setState(value);\n\n return value;\n }, [state, value]);\n}\n"],"mappings":"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","names":["sharedQuery_exports","__export","SharedQuery","SharedQueryDefaults","sharedQuery","useSharedQuery","__toCommonJS","import_timeConstants","import_object_sizeof","import_react","import_timeConstants","import_react","import_kvStore","getStorageAdapter","options","defaultStoreExpiryMs","localStorageKey","indexedDbKey","storeExpiryMs","store","expiryMs","LocalStorageAdapter","IndexedDbAdapter","LocalStorageAdapter","keyPrefix","expiryMs","key","value","fullKey","raw","box","IndexedDbAdapter","k","SharedStateDefaults","PubSubStore","key","value","prev","subscriber","subscribers","next","copy","e","pubSubStore","STORAGE_KEY","stateKey","SharedState","defaultValue","options","getStorageAdapter","arg","lazyLoad","v","sharedState","useSharedState","state","onStoreChange","stringifyDeterministicForKeys","params","cache","_","value","result","key","import_isEqual","import_react","useDeepMemo","value","state","setState","isEqual","SharedQueryDefaults","args","SharedQuery","options","persistTo","sharedState","stringifyDeterministicForKeys","queryKey","dataUpdatedMs","cached","inflightQuery","promise","abortController","data","e","entry","prev","key","clone","inflight","oldState","oldSize","oldByteSize","getByteSize","newState","needUpdate","numToCut","entry1","entry2","currentByteSize","bytesToCut","entriesByTimeAscending","i","thisSize","seenQueryNames","sharedQuery","DEFAULT_QUERY_STATE_ENTRY","useSharedQuery","query","stableArgs","useDeepMemo","queryState","setQueryState","useSharedState","isMounted","queryStateValue","setQueryStateValue","arg","execute","forceRefresh","now","reason","controller"]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{MS_PER_DAY as C}from"@choksheak/ts-utils/timeConstants";import g from"object-sizeof";import{useCallback as O,useEffect as F,useMemo as B,useRef as L}from"react";import{MS_PER_DAY as P}from"@choksheak/ts-utils/timeConstants";import{useSyncExternalStore as E}from"react";import{kvStore as f}from"@choksheak/ts-utils/kvStore";function v(i,e){let{localStorageKey:t,indexedDbKey:r,storeExpiryMs:a,store:s}=i??{};if(s)return s;let n=a||e;return t?new b(t,n):r?new T(r,n):null}var b=class{constructor(e,t){this.keyPrefix=e;this.expiryMs=t}save(e,t){if(typeof window>"u")return;let r=this.keyPrefix+":"+e;window.localStorage.setItem(r,JSON.stringify({value:t,expiresAt:this.expiryMs?Date.now()+this.expiryMs:0}))}load(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e,r=window.localStorage.getItem(t);if(r)try{let a=JSON.parse(r);if(!a||a.expiresAt&&Date.now()>=a.expiresAt){window.localStorage.removeItem(t);return}return a.value}catch{window.localStorage.removeItem(t);return}}delete(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e;window.localStorage.removeItem(t)}clear(){if(!(typeof window>"u"))for(let e of Object.keys(window.localStorage))e.startsWith(this.keyPrefix)&&window.localStorage.removeItem(e)}},T=class{constructor(e,t){this.keyPrefix=e;this.expiryMs=t}async save(e,t){if(typeof window>"u")return;let r=this.keyPrefix+":"+e;await f.set(r,JSON.stringify(t),this.expiryMs)}async load(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e,r=await f.get(t);if(r!==void 0)try{return JSON.parse(r)}catch{await f.delete(t);return}}async delete(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e;await f.delete(t)}async clear(){typeof window>"u"||await f.forEach(async e=>{e.startsWith(this.keyPrefix)&&await f.delete(e)})}};var A={storeExpiryMs:30*P,lazyLoad:!1},x=class{dataByKey=new Map;subscribersByKey=new Map;get(e){return this.dataByKey.get(e)}set(e,t){let r=this.dataByKey.get(e);r!==t&&(this.dataByKey.set(e,t),this.notify(e,t,r))}delete(e){this.dataByKey.delete(e)}setNoNotify(e,t){this.dataByKey.set(e,t)}subscribe(e,t){let r=this.subscribersByKey.get(e);return r||(r=new Set,this.subscribersByKey.set(e,r)),r.add(t),()=>{r.delete(t),r.size===0&&this.subscribersByKey.delete(e)}}notify(e,t,r){let a=this.subscribersByKey.get(e);if(!a)return;let s=Array.from(a.values());for(let n of s)try{n(t,r)}catch(l){console.error("Error invoking subscriber:",l)}}},p=new x,m="state",$=0,w=class{constructor(e,t){this.defaultValue=e;this.pubSubKey=String($++),this.storageAdapter=v(t,A.storeExpiryMs),this.setValueBounded=s=>this.setValue(s),p.setNoNotify(this.pubSubKey,e);let r=t?.lazyLoad;(r!==void 0?r:A.lazyLoad)||this.initDefaultValueOnce()}pubSubKey;storageAdapter;setValueBounded;initDone=!1;subscribe(e){return p.subscribe(this.pubSubKey,()=>e())}getSnapshot(){return p.get(this.pubSubKey)}setValue(e){if(typeof e=="function"){let t=p.get(this.pubSubKey);e=e(t)}p.set(this.pubSubKey,e),this.storageAdapter?.save(m,e)}delete(){p.set(this.pubSubKey,this.defaultValue),this.storageAdapter?.save(m,this.defaultValue)}async initDefaultValueOnce(){if(this.initDone)return;this.initDone=!0;let e=await this.storageAdapter?.load(m);p.set(this.pubSubKey,e!==void 0?e:this.defaultValue)}};function M(i,e){return new w(i,e)}function Q(i){return i.initDefaultValueOnce(),[E(t=>i.subscribe(t),()=>i.getSnapshot(),()=>i.getSnapshot()),i.setValueBounded]}function K(i){let e=new Set;return JSON.stringify(i,(t,r)=>{if(typeof r=="function")throw new Error("Functions should not be passed into query parameters");if(!r||typeof r!="object"||Array.isArray(r))return r;if(e.has(r))return"[Circular]";if(e.add(r),r!==i)try{JSON.stringify(r)}catch{return"[Unserializable]"}return Object.keys(r).sort().reduce((a,s)=>(a[s]=r[s],a),{})})}import N from"lodash/isEqual";import{useMemo as U,useState as q}from"react";function k(i){let[e,t]=q(i);return U(()=>e===i||N(e,i)?e:(t(i),i),[e,i])}var h={staleMs:0,expiryMs:30*C,refetchOnStale:!0,persistTo:void 0,maxSize:100,maxBytes:1e5,log:(...i)=>console.log("[sharedQuery]",...i)},D=class{inflightQueries=new Map;queryState;queryName;queryFn;expiryMs;staleMs;refetchOnStale;maxSize;maxBytes;log;constructor(e){if(this.queryName=e.queryName,this.queryFn=e.queryFn,this.staleMs=e?.staleMs??h.staleMs,this.expiryMs=e?.expiryMs??h.expiryMs,this.refetchOnStale=e?.refetchOnStale??h.refetchOnStale,!e.localStorageKey&&!e.indexedDbKey){let t=e.persistTo??h.persistTo;t==="localStorage"?e.localStorageKey=e.queryName:t==="indexedDb"&&(e.indexedDbKey=e.queryName)}this.maxSize=e?.maxSize??h.maxSize,this.maxBytes=e?.maxBytes??h.maxBytes,this.log=e?.log??h.log,this.queryState=M({},{...e,storeExpiryMs:e?.storeExpiryMs||this.expiryMs})}getQueryKey(e){return this.queryName+":"+K(e)}getAbortController(e){let t=this.getQueryKey(e);return this.getAbortControllerByKey(t)}getAbortControllerByKey(e){return this.inflightQueries.get(e)?.abortController??null}getAbortSignal(e){let t=this.getQueryKey(e);return this.getAbortSignalByKey(t)}getAbortSignalByKey(e){return this.getAbortControllerByKey(e)?.signal??null}isStale(e){return this.staleMs===0?!0:Date.now()-e>this.staleMs}isExpired(e){return this.expiryMs===0?!1:Date.now()-e>this.expiryMs}async getCachedOrFetch(...e){let t=this.getQueryKey(e),r=this.queryState.getSnapshot()?.[t];return r?.data&&!this.isExpired(r.dataUpdatedMs??0)?this.isStale(r.dataUpdatedMs??0)?(this.refetchOnStale&&(this.log(`refetchOnStale for ${t}`),this.dedupedFetch(t,...e)),this.log(`Returning stale data for ${t}`),r.data):(this.log(`Return fresh data ${t} from cache without fetching`),r.data):await this.dedupedFetch(t,...e)}dedupedFetch(e,...t){let r=this.inflightQueries.get(e);if(r)return this.log(`Deduplicating inflight fetch for ${e}`),r.promise;let a=this.startFetching(e,t),s=new AbortController;return this.inflightQueries.set(e,{promise:a,abortController:s}),a}async startFetching(e,t){this.log(`Start fetching ${e}`);try{let r=await this.queryFn(...t);return this.log(`Successfully fetched ${e}`),this.setData(e,r,Date.now()),r}catch(r){throw this.log(`Failed to fetch ${e}: ${r}`),r}finally{this.inflightQueries.delete(e)}}getQueryValue(e){return this.queryState.getSnapshot()?.[e]}getData(e){return this.queryState.getSnapshot()?.[e]?.data}setData(e,t,r=Date.now()){let a={lastUpdatedMs:Date.now(),loading:!1,data:t,dataUpdatedMs:r};this.queryState.setValue(s=>({...s,[e]:a})),this.enforceSizeLimit()}async updateFromSource(...e){let t=this.getQueryKey(e);return await this.dedupedFetch(t,...e)}deleteData(e){let r={...this.queryState.getSnapshot()};delete r[e],this.queryState.setValue(r);let a=this.inflightQueries.get(e);a&&(this.inflightQueries.delete(e),a.abortController.abort("Deleted"))}clear(){this.queryState.delete();for(let{abortController:e}of Array.from(this.inflightQueries.values()))e.abort("Cleared");this.inflightQueries.clear()}enforceSizeLimit(){if(!this.maxSize&&!this.maxBytes)return;let e=this.queryState.getSnapshot(),t=Object.keys(e).length;if(t<=1)return;let r=g(e),a=e,s=!1;if(this.maxSize){let n=t-this.maxSize;n>0&&(this.log(`enforceSizeLimit for ${this.queryName} needed due to size ${t} > ${this.maxSize}`),a={...a},s=!0,Object.entries(a).sort((l,c)=>c[1].lastUpdatedMs-l[1].lastUpdatedMs).slice(-n).forEach(([l])=>{delete a[l]}))}if(this.maxBytes&&Object.keys(a).length>1){let n=s?g(a):r,l=n-this.maxBytes;if(l>0){this.log(`enforceSizeLimit for ${this.queryName} needed due to byte size ${n.toLocaleString()} > ${this.maxBytes.toLocaleString()}`),s||(a={...a},s=!0);let c=Object.entries(a).sort((y,d)=>y[1].lastUpdatedMs-d[1].lastUpdatedMs);for(let y=0;y<c.length-1&&l>0;y++){let d=c[y][0],u=g(d)+g(a[d]);delete a[d],l-=u}}}if(s){this.queryState.setValue(a),this.log(`Trimmed data for ${this.queryName}: size=(${t} -> ${Object.keys(a).length}) (limit=${this.maxSize}), byteSize=(${r.toLocaleString()} -> ${g(a).toLocaleString()}) (limit=${this.maxBytes.toLocaleString()})`);return}this.log(`No need to trim data for ${this.queryName}: size=${t} (limit=${this.maxSize}), byteSize=${r.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`)}},z=new Set;function de(i){if(z.has(i.queryName))throw new Error(`Duplicate shared query "${i.queryName}"`);return z.add(i.queryName),new D(i)}var R={lastUpdatedMs:0,loading:!0};function ce(i,e=[]){let t=k(e),[r,a]=Q(i.queryState),s=L(!0),n=B(()=>i.getQueryKey(t),[i,t]),l=r[n],c=O(d=>{a(u=>{let o={...u};return o[n]=typeof d=="function"?d(o[n]):d,o})},[n,a]),y=O(async d=>{i.log(`Begin executing shared query ${n}`),c(u=>({...u??{},lastUpdatedMs:Date.now(),loading:!0}));try{let u=await(d?i.updateFromSource(...t):i.getCachedOrFetch(...t));if(s.current){let o=Date.now();c({lastUpdatedMs:o,loading:!1,data:u,dataUpdatedMs:o})}}catch(u){if(s.current){let o=Date.now();c(S=>({...S??{},lastUpdatedMs:o,loading:!1,error:u,errorUpdatedMs:o}))}}},[i,n,c,t]);return F(()=>(s.current=!0,y(!1),()=>{s.current=!1}),[y]),B(()=>({...l??R,abortCurrentQuery:u=>{let o=i.getAbortControllerByKey(n);return o?.abort(u),!!o},refetch:()=>{y(!0)},setData:(u,o)=>{let S=Date.now();c(V=>({...V??{},data:u,dataUpdatedMs:o??S,lastUpdatedMs:S,loading:!1}))},deleteData:()=>{a(u=>{let o={...u};return delete o[n],o})}}),[y,i,n,l,a,c])}export{D as SharedQuery,h as SharedQueryDefaults,de as sharedQuery,ce as useSharedQuery};
|
|
2
|
+
//# sourceMappingURL=sharedQuery.min.mjs.map
|