@choksheak/react-utils 0.2.6 → 0.2.7
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.cjs +4 -4
- package/fetcher.d.mts +31 -4
- package/fetcher.d.ts +30 -3
- package/fetcher.min.cjs +1 -1
- package/fetcher.min.cjs.map +1 -1
- package/fetcher.min.mjs +1 -1
- package/fetcher.min.mjs.map +1 -1
- package/fetcher.mjs +3 -3
- package/package.json +2 -1
- package/sharedQuery.cjs +45 -16
- package/sharedQuery.d.mts +87 -1
- package/sharedQuery.d.ts +87 -1
- package/sharedQuery.min.cjs +1 -1
- package/sharedQuery.min.cjs.map +1 -1
- package/sharedQuery.min.mjs +1 -1
- package/sharedQuery.min.mjs.map +1 -1
- package/sharedQuery.mjs +45 -16
- package/sharedState.d.mts +57 -5
- package/sharedState.d.ts +57 -5
- package/sharedState.min.cjs.map +1 -1
- package/sharedState.min.mjs.map +1 -1
package/fetcher.cjs
CHANGED
|
@@ -31,21 +31,21 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var fetcher_exports = {};
|
|
32
32
|
__export(fetcher_exports, {
|
|
33
33
|
fetcher: () => fetcher,
|
|
34
|
-
|
|
34
|
+
simpleFetcher: () => simpleFetcher
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(fetcher_exports);
|
|
37
37
|
var import_wretch = __toESM(require("wretch"));
|
|
38
38
|
var import_middlewares = require("wretch/middlewares");
|
|
39
|
-
var fetcher = (0, import_wretch.default)()
|
|
40
|
-
var fetcherWithRetries = (0, import_wretch.default)().middlewares([
|
|
39
|
+
var fetcher = (0, import_wretch.default)().middlewares([
|
|
41
40
|
(0, import_middlewares.retry)({
|
|
42
41
|
maxAttempts: 3,
|
|
43
42
|
delayTimer: 1e3,
|
|
44
43
|
retryOnNetworkError: true
|
|
45
44
|
})
|
|
46
45
|
]);
|
|
46
|
+
var simpleFetcher = (0, import_wretch.default)();
|
|
47
47
|
// Annotate the CommonJS export names for ESM import in node:
|
|
48
48
|
0 && (module.exports = {
|
|
49
49
|
fetcher,
|
|
50
|
-
|
|
50
|
+
simpleFetcher
|
|
51
51
|
});
|
package/fetcher.d.mts
CHANGED
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
import * as wretch from 'wretch';
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*
|
|
6
|
+
* An alternate syntax to fetch() that allows function chaining to compose the
|
|
7
|
+
* entire fetch request. One can also compose partial fetcher objects to be
|
|
8
|
+
* reused in future requests.
|
|
9
|
+
*
|
|
10
|
+
* Examples:
|
|
11
|
+
* ```
|
|
12
|
+
* const userFetcher = fetcher
|
|
13
|
+
* .auth("Bearer TOPSECRET")
|
|
14
|
+
* .content("application/json")
|
|
15
|
+
* .url("/api/user");
|
|
16
|
+
*
|
|
17
|
+
* const user = await userFetcher.get();
|
|
18
|
+
*
|
|
19
|
+
* const updateResult = await userFetcher
|
|
20
|
+
* .body({ id: user.id, name: "New name"})
|
|
21
|
+
* .post();
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Fetcher with default short retries. Use this to make your queries more
|
|
26
|
+
* robust without any additional work.
|
|
27
|
+
*/
|
|
4
28
|
declare const fetcher: wretch.Wretch<unknown, unknown, undefined, undefined>;
|
|
5
|
-
/**
|
|
6
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Name this as `simpleFetcher` to make it easier to understand than `wretch`.
|
|
31
|
+
* Use this if you don't want retries. Use `fetcher` if you want retries.
|
|
32
|
+
*/
|
|
33
|
+
declare const simpleFetcher: wretch.Wretch<unknown, unknown, undefined, undefined>;
|
|
7
34
|
|
|
8
|
-
export { fetcher,
|
|
35
|
+
export { fetcher, simpleFetcher };
|
package/fetcher.d.ts
CHANGED
|
@@ -1,4 +1,31 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
*
|
|
4
|
+
* An alternate syntax to fetch() that allows function chaining to compose the
|
|
5
|
+
* entire fetch request. One can also compose partial fetcher objects to be
|
|
6
|
+
* reused in future requests.
|
|
7
|
+
*
|
|
8
|
+
* Examples:
|
|
9
|
+
* ```
|
|
10
|
+
* const userFetcher = fetcher
|
|
11
|
+
* .auth("Bearer TOPSECRET")
|
|
12
|
+
* .content("application/json")
|
|
13
|
+
* .url("/api/user");
|
|
14
|
+
*
|
|
15
|
+
* const user = await userFetcher.get();
|
|
16
|
+
*
|
|
17
|
+
* const updateResult = await userFetcher
|
|
18
|
+
* .body({ id: user.id, name: "New name"})
|
|
19
|
+
* .post();
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Fetcher with default short retries. Use this to make your queries more
|
|
24
|
+
* robust without any additional work.
|
|
25
|
+
*/
|
|
2
26
|
export declare const fetcher: import("wretch").Wretch<unknown, unknown, undefined, undefined>;
|
|
3
|
-
/**
|
|
4
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Name this as `simpleFetcher` to make it easier to understand than `wretch`.
|
|
29
|
+
* Use this if you don't want retries. Use `fetcher` if you want retries.
|
|
30
|
+
*/
|
|
31
|
+
export declare const simpleFetcher: import("wretch").Wretch<unknown, unknown, undefined, undefined>;
|
package/fetcher.min.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var a=Object.create;var m=Object.defineProperty;var d=Object.getOwnPropertyDescriptor;var f=Object.getOwnPropertyNames;var h=Object.getPrototypeOf,l=Object.prototype.hasOwnProperty;var n=(r,e)=>{for(var t in e)m(r,t,{get:e[t],enumerable:!0})},i=(r,e,t,c)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of f(e))!l.call(r,o)&&o!==t&&m(r,o,{get:()=>e[o],enumerable:!(c=d(e,o))||c.enumerable});return r};var w=(r,e,t)=>(t=r!=null?a(h(r)):{},i(e||!r||!r.__esModule?m(t,"default",{value:r,enumerable:!0}):t,r)),x=r=>i(m({},"__esModule",{value:!0}),r);var u={};n(u,{fetcher:()=>y,simpleFetcher:()=>k});module.exports=x(u);var p=w(require("wretch")),s=require("wretch/middlewares"),y=(0,p.default)().middlewares([(0,s.retry)({maxAttempts:3,delayTimer:1e3,retryOnNetworkError:!0})]),k=(0,p.default)();0&&(module.exports={fetcher,simpleFetcher});
|
|
2
2
|
//# sourceMappingURL=fetcher.min.cjs.map
|
package/fetcher.min.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/fetcher.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"sources":["../src/fetcher.ts"],"sourcesContent":["/**\n * @packageDocumentation\n *\n * An alternate syntax to fetch() that allows function chaining to compose the\n * entire fetch request. One can also compose partial fetcher objects to be\n * reused in future requests.\n *\n * Examples:\n * ```\n * const userFetcher = fetcher\n * .auth(\"Bearer TOPSECRET\")\n * .content(\"application/json\")\n * .url(\"/api/user\");\n *\n * const user = await userFetcher.get();\n *\n * const updateResult = await userFetcher\n * .body({ id: user.id, name: \"New name\"})\n * .post();\n * ```\n */\n\nimport wretch from \"wretch\";\nimport { retry } from \"wretch/middlewares\";\n\n/**\n * Fetcher with default short retries. Use this to make your queries more\n * robust without any additional work.\n */\nexport const fetcher = wretch().middlewares([\n retry({\n maxAttempts: 3,\n delayTimer: 1000,\n retryOnNetworkError: true,\n }),\n]);\n\n/**\n * Name this as `simpleFetcher` to make it easier to understand than `wretch`.\n * Use this if you don't want retries. Use `fetcher` if you want retries.\n */\nexport const simpleFetcher = wretch();\n"],"mappings":"0jBAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,aAAAE,EAAA,kBAAAC,IAAA,eAAAC,EAAAJ,GAsBA,IAAAK,EAAmB,qBACnBC,EAAsB,8BAMTJ,KAAU,EAAAK,SAAO,EAAE,YAAY,IAC1C,SAAM,CACJ,YAAa,EACb,WAAY,IACZ,oBAAqB,EACvB,CAAC,CACH,CAAC,EAMYJ,KAAgB,EAAAI,SAAO","names":["fetcher_exports","__export","fetcher","simpleFetcher","__toCommonJS","import_wretch","import_middlewares","wretch"]}
|
package/fetcher.min.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import r from"wretch";import{retry as e}from"wretch/middlewares";var m=r()
|
|
1
|
+
import r from"wretch";import{retry as e}from"wretch/middlewares";var m=r().middlewares([e({maxAttempts:3,delayTimer:1e3,retryOnNetworkError:!0})]),p=r();export{m as fetcher,p as simpleFetcher};
|
|
2
2
|
//# sourceMappingURL=fetcher.min.mjs.map
|
package/fetcher.min.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/fetcher.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"sources":["../src/fetcher.ts"],"sourcesContent":["/**\n * @packageDocumentation\n *\n * An alternate syntax to fetch() that allows function chaining to compose the\n * entire fetch request. One can also compose partial fetcher objects to be\n * reused in future requests.\n *\n * Examples:\n * ```\n * const userFetcher = fetcher\n * .auth(\"Bearer TOPSECRET\")\n * .content(\"application/json\")\n * .url(\"/api/user\");\n *\n * const user = await userFetcher.get();\n *\n * const updateResult = await userFetcher\n * .body({ id: user.id, name: \"New name\"})\n * .post();\n * ```\n */\n\nimport wretch from \"wretch\";\nimport { retry } from \"wretch/middlewares\";\n\n/**\n * Fetcher with default short retries. Use this to make your queries more\n * robust without any additional work.\n */\nexport const fetcher = wretch().middlewares([\n retry({\n maxAttempts: 3,\n delayTimer: 1000,\n retryOnNetworkError: true,\n }),\n]);\n\n/**\n * Name this as `simpleFetcher` to make it easier to understand than `wretch`.\n * Use this if you don't want retries. Use `fetcher` if you want retries.\n */\nexport const simpleFetcher = wretch();\n"],"mappings":"AAsBA,OAAOA,MAAY,SACnB,OAAS,SAAAC,MAAa,qBAMf,IAAMC,EAAUF,EAAO,EAAE,YAAY,CAC1CC,EAAM,CACJ,YAAa,EACb,WAAY,IACZ,oBAAqB,EACvB,CAAC,CACH,CAAC,EAMYE,EAAgBH,EAAO","names":["wretch","retry","fetcher","simpleFetcher"]}
|
package/fetcher.mjs
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// src/fetcher.ts
|
|
2
2
|
import wretch from "wretch";
|
|
3
3
|
import { retry } from "wretch/middlewares";
|
|
4
|
-
var fetcher = wretch()
|
|
5
|
-
var fetcherWithRetries = wretch().middlewares([
|
|
4
|
+
var fetcher = wretch().middlewares([
|
|
6
5
|
retry({
|
|
7
6
|
maxAttempts: 3,
|
|
8
7
|
delayTimer: 1e3,
|
|
9
8
|
retryOnNetworkError: true
|
|
10
9
|
})
|
|
11
10
|
]);
|
|
11
|
+
var simpleFetcher = wretch();
|
|
12
12
|
export {
|
|
13
13
|
fetcher,
|
|
14
|
-
|
|
14
|
+
simpleFetcher
|
|
15
15
|
};
|
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.7",
|
|
5
5
|
"description": "Miscellaneous React+TypeScript utilities with support for full tree-shaking.",
|
|
6
6
|
"private": false,
|
|
7
7
|
"files": [
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"test:watch": "vitest",
|
|
18
18
|
"ci": "npm run lint && npm run tsc && npm run build && npm run test",
|
|
19
19
|
"ls": "lint-staged",
|
|
20
|
+
"clean": "rm -rf dist",
|
|
20
21
|
"docs": "rm -rf docs && typedoc `find src -name '*.ts' ! -name '*.test.ts'` --out docs --name 'Chok’s React Utilities'",
|
|
21
22
|
"gen-types": "tsc --emitDeclarationOnly --declaration",
|
|
22
23
|
"gen-exports": "tsx scripts/gen-exports.ts",
|
package/sharedQuery.cjs
CHANGED
|
@@ -337,6 +337,12 @@ var SharedQueryDefaults = {
|
|
|
337
337
|
var SharedQuery = class {
|
|
338
338
|
inflightQueries = /* @__PURE__ */ new Map();
|
|
339
339
|
queryState;
|
|
340
|
+
/**
|
|
341
|
+
* Keep track of the mounted query keys. This handles the case where an
|
|
342
|
+
* earlier-started query returns later, and when it returns, it was already
|
|
343
|
+
* unmounted, thus making it eligible for clean up right after fetch.
|
|
344
|
+
*/
|
|
345
|
+
mountedKeys = /* @__PURE__ */ new Map();
|
|
340
346
|
queryName;
|
|
341
347
|
queryFn;
|
|
342
348
|
expiryMs;
|
|
@@ -523,6 +529,19 @@ var SharedQuery = class {
|
|
|
523
529
|
}
|
|
524
530
|
this.inflightQueries.clear();
|
|
525
531
|
}
|
|
532
|
+
/** Keep track of mounted keys. */
|
|
533
|
+
mount(queryKey) {
|
|
534
|
+
this.mountedKeys.set(queryKey, (this.mountedKeys.get(queryKey) ?? 0) + 1);
|
|
535
|
+
}
|
|
536
|
+
/** Clean up mounted keys. */
|
|
537
|
+
unmount(queryKey) {
|
|
538
|
+
const count = this.mountedKeys.get(queryKey);
|
|
539
|
+
if (count === 1) {
|
|
540
|
+
this.mountedKeys.delete(queryKey);
|
|
541
|
+
} else {
|
|
542
|
+
this.mountedKeys.set(queryKey, (count ?? 0) - 1);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
526
545
|
/**
|
|
527
546
|
* Discard the oldest entries if the size exceeds the limit. The last
|
|
528
547
|
* remaining record cannot be deleted no matter what the limit is.
|
|
@@ -540,21 +559,29 @@ var SharedQuery = class {
|
|
|
540
559
|
let newState = oldState;
|
|
541
560
|
let needUpdate = false;
|
|
542
561
|
if (this.maxSize) {
|
|
543
|
-
|
|
562
|
+
let numToCut = oldSize - this.maxSize;
|
|
544
563
|
if (numToCut > 0) {
|
|
545
564
|
this.log(
|
|
546
565
|
`enforceSizeLimit for ${this.queryName} needed due to size ${oldSize} > ${this.maxSize}`
|
|
547
566
|
);
|
|
548
567
|
newState = { ...newState };
|
|
549
568
|
needUpdate = true;
|
|
550
|
-
Object.entries(newState).sort(
|
|
569
|
+
const entriesByTimeAscending = Object.entries(newState).sort(
|
|
551
570
|
(entry1, entry2) => (
|
|
552
571
|
// Sort descending.
|
|
553
|
-
|
|
572
|
+
entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs
|
|
554
573
|
)
|
|
555
|
-
)
|
|
556
|
-
|
|
557
|
-
|
|
574
|
+
);
|
|
575
|
+
for (let i = 0; i < entriesByTimeAscending.length && numToCut > 0; i++) {
|
|
576
|
+
const key = entriesByTimeAscending[i][0];
|
|
577
|
+
if (this.mountedKeys.has(key)) {
|
|
578
|
+
this.log(`Cannot clean up ${key} as it is mounted`);
|
|
579
|
+
} else {
|
|
580
|
+
this.log(`Cleaning up unmounted ${key}`);
|
|
581
|
+
delete newState[key];
|
|
582
|
+
numToCut--;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
558
585
|
}
|
|
559
586
|
}
|
|
560
587
|
if (this.maxBytes && Object.keys(newState).length > 1) {
|
|
@@ -571,16 +598,16 @@ var SharedQuery = class {
|
|
|
571
598
|
const entriesByTimeAscending = Object.entries(newState).sort(
|
|
572
599
|
(entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs
|
|
573
600
|
);
|
|
574
|
-
for (
|
|
575
|
-
let i = 0;
|
|
576
|
-
// Leave the last record entriesByTimeAscending[len-1] untouched.
|
|
577
|
-
i < entriesByTimeAscending.length - 1 && bytesToCut > 0;
|
|
578
|
-
i++
|
|
579
|
-
) {
|
|
601
|
+
for (let i = 0; i < entriesByTimeAscending.length && bytesToCut > 0; i++) {
|
|
580
602
|
const key = entriesByTimeAscending[i][0];
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
603
|
+
if (this.mountedKeys.has(key)) {
|
|
604
|
+
this.log(`Cannot clean up ${key} as it is mounted`);
|
|
605
|
+
} else {
|
|
606
|
+
this.log(`Cleaning up unmounted ${key}`);
|
|
607
|
+
const thisSize = (0, import_object_sizeof.default)(key) + (0, import_object_sizeof.default)(newState[key]);
|
|
608
|
+
delete newState[key];
|
|
609
|
+
bytesToCut -= thisSize;
|
|
610
|
+
}
|
|
584
611
|
}
|
|
585
612
|
}
|
|
586
613
|
}
|
|
@@ -666,11 +693,13 @@ function useSharedQuery(query, args = []) {
|
|
|
666
693
|
);
|
|
667
694
|
(0, import_react3.useEffect)(() => {
|
|
668
695
|
isMounted.current = true;
|
|
696
|
+
query.mount(queryKey);
|
|
669
697
|
void execute(false);
|
|
670
698
|
return () => {
|
|
671
699
|
isMounted.current = false;
|
|
700
|
+
query.unmount(queryKey);
|
|
672
701
|
};
|
|
673
|
-
}, [execute]);
|
|
702
|
+
}, [execute, query, queryKey]);
|
|
674
703
|
return (0, import_react3.useMemo)(() => {
|
|
675
704
|
const state = queryStateValue ?? DEFAULT_QUERY_STATE_ENTRY;
|
|
676
705
|
return {
|
package/sharedQuery.d.mts
CHANGED
|
@@ -2,6 +2,8 @@ import { SharedStateOptions, SharedState } from './sharedState.mjs';
|
|
|
2
2
|
import './utils/storage.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*
|
|
5
7
|
* The "shared query" is a data fetching convenience library that helps users
|
|
6
8
|
* to load data, check for staleness and expiration, cache in memory, local
|
|
7
9
|
* storage or indexed db, and be able to manipulate the data anytime.
|
|
@@ -12,10 +14,80 @@ import './utils/storage.mjs';
|
|
|
12
14
|
* records and the total byte size of data can be limited so as to ensure the
|
|
13
15
|
* query does not take up too much memory.
|
|
14
16
|
*
|
|
15
|
-
*
|
|
17
|
+
* Example:
|
|
18
|
+
* ```
|
|
19
|
+
* import { sharedQuery, useSharedQuery } from "@choksheak/react-utils/sharedQuery";
|
|
20
|
+
*
|
|
21
|
+
* // Create a new shared query in the top level scope.
|
|
22
|
+
* // The query function `queryFn` here does not take any arguments.
|
|
23
|
+
* //
|
|
24
|
+
* // Only the queryName and queryFn are required and everything else is
|
|
25
|
+
* // optional.
|
|
26
|
+
* //
|
|
27
|
+
* // The queryName must be unique, otherwise we will throw an
|
|
28
|
+
* // error. This is because defining the same queryName for two different
|
|
29
|
+
* // queries implies that the queries are the same, thus violating the idea
|
|
30
|
+
* // of a "shared" query, and almost certainly points to a bug in the code.
|
|
31
|
+
* export const usersQuery = sharedQuery({
|
|
32
|
+
* queryName: "users",
|
|
33
|
+
* queryFn: listUsers,
|
|
34
|
+
* persistTo: "indexedDb",
|
|
35
|
+
* staleMs: MS_PER_DAY,
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* // Note that the query will only start fetching on mount in React.
|
|
39
|
+
* // But you can do `usersQuery.getCachedOrFetch()` here to prefetch the
|
|
40
|
+
* // data if necessary.
|
|
41
|
+
* usersQuery.getCachedOrFetch();
|
|
42
|
+
*
|
|
43
|
+
* // Example of using a query function with arguments.
|
|
44
|
+
* export const getUserQuery = sharedQuery({
|
|
45
|
+
* queryName: "getUser",
|
|
46
|
+
* queryFn: getUser, // getUser(userId: string) => User
|
|
47
|
+
* persistTo: "indexedDb",
|
|
48
|
+
* staleMs: MS_PER_DAY,
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* export const UsersComponent: React.FC = () => {
|
|
52
|
+
* // Second argument is optional, and is an array of arguments to be
|
|
53
|
+
* // passed to the `queryFn`.
|
|
54
|
+
* const users = useSharedQuery(usersQuery);
|
|
55
|
+
*
|
|
56
|
+
* return (
|
|
57
|
+
* <>
|
|
58
|
+
* <h1>List of Users</h1>
|
|
59
|
+
*
|
|
60
|
+
* <button onclick={users.refetch}>Refresh</button>
|
|
61
|
+
*
|
|
62
|
+
* {users.loading && <Loader />}
|
|
63
|
+
*
|
|
64
|
+
* {users.error && !users.data && <p>Error: {String(users.error)}</p>}
|
|
65
|
+
*
|
|
66
|
+
* <p>Data: {users.data ? JSON.stringify(users.data) : "-"}</p>
|
|
67
|
+
* </>
|
|
68
|
+
* );
|
|
69
|
+
* };
|
|
70
|
+
*
|
|
71
|
+
* export const AnotherComponent: React.FC = () => {
|
|
72
|
+
* // This query will be deduped with the one above, sharing the same
|
|
73
|
+
* // data and the same fetches.
|
|
74
|
+
* const users = useSharedQuery(usersQuery);
|
|
75
|
+
* ...
|
|
76
|
+
* };
|
|
77
|
+
*
|
|
78
|
+
* export const DisplayOneUser: React.FC<{userId: string}> = ({userId}) => {
|
|
79
|
+
* // Example of using the query with an argument. Whenever the argument
|
|
80
|
+
* // changes, the data will be fetched and updated automatically.
|
|
81
|
+
* const user = useSharedQuery(getUserQuery, [userId]);
|
|
82
|
+
*
|
|
83
|
+
* return <>User: {user.data ? JSON.stringify(user.data) : "-"}</>;
|
|
84
|
+
* };
|
|
85
|
+
* ```
|
|
16
86
|
*/
|
|
17
87
|
|
|
88
|
+
/** Type of the queryFn used to fetch the data from source. */
|
|
18
89
|
type QueryFn<TArgs extends unknown[], TData> = (...args: TArgs) => Promise<TData> | TData;
|
|
90
|
+
/** An entry for a persisted state corresponding to one query key. */
|
|
19
91
|
type QueryStateValue<TData> = {
|
|
20
92
|
/**
|
|
21
93
|
* When this record was last updated. Used for LRU determination. If you want
|
|
@@ -33,6 +105,7 @@ type QueryStateValue<TData> = {
|
|
|
33
105
|
error?: unknown;
|
|
34
106
|
errorUpdatedMs?: number;
|
|
35
107
|
};
|
|
108
|
+
/** Return type for useSharedQuery(). */
|
|
36
109
|
type UseQueryResult<TData> = QueryStateValue<TData> & {
|
|
37
110
|
/**
|
|
38
111
|
* Cancels the inflight query (if any). Returns true if canceled.
|
|
@@ -49,12 +122,14 @@ type UseQueryResult<TData> = QueryStateValue<TData> & {
|
|
|
49
122
|
setData: (data: TData, dataUpdatedMs?: number) => void;
|
|
50
123
|
deleteData: () => void;
|
|
51
124
|
};
|
|
125
|
+
/** List of all available persistence options. */
|
|
52
126
|
type PersistTo = "localStorage" | "indexedDb";
|
|
53
127
|
/**
|
|
54
128
|
* The full query state is a record of all the fetch states mapped by
|
|
55
129
|
* query key to the state entry.
|
|
56
130
|
*/
|
|
57
131
|
type SharedQueryState<TData> = Record<string, QueryStateValue<TData>>;
|
|
132
|
+
/** Options to configure a shared query. */
|
|
58
133
|
type SharedQueryOptions<TArgs extends unknown[], TData> = {
|
|
59
134
|
queryName: string;
|
|
60
135
|
queryFn: QueryFn<TArgs, TData>;
|
|
@@ -76,9 +151,16 @@ declare const SharedQueryDefaults: {
|
|
|
76
151
|
maxBytes: number;
|
|
77
152
|
log: (...args: unknown[]) => void;
|
|
78
153
|
};
|
|
154
|
+
/** Please use sharedQuery() instead. */
|
|
79
155
|
declare class SharedQuery<TArgs extends unknown[], TData> {
|
|
80
156
|
private readonly inflightQueries;
|
|
81
157
|
readonly queryState: SharedState<SharedQueryState<TData>>;
|
|
158
|
+
/**
|
|
159
|
+
* Keep track of the mounted query keys. This handles the case where an
|
|
160
|
+
* earlier-started query returns later, and when it returns, it was already
|
|
161
|
+
* unmounted, thus making it eligible for clean up right after fetch.
|
|
162
|
+
*/
|
|
163
|
+
private readonly mountedKeys;
|
|
82
164
|
readonly queryName: string;
|
|
83
165
|
private readonly queryFn;
|
|
84
166
|
readonly expiryMs: number;
|
|
@@ -146,6 +228,10 @@ declare class SharedQuery<TArgs extends unknown[], TData> {
|
|
|
146
228
|
deleteData(queryKey: string): void;
|
|
147
229
|
/** Delete all currently cached data & all inflight promises. */
|
|
148
230
|
clear(): void;
|
|
231
|
+
/** Keep track of mounted keys. */
|
|
232
|
+
mount(queryKey: string): void;
|
|
233
|
+
/** Clean up mounted keys. */
|
|
234
|
+
unmount(queryKey: string): void;
|
|
149
235
|
/**
|
|
150
236
|
* Discard the oldest entries if the size exceeds the limit. The last
|
|
151
237
|
* remaining record cannot be deleted no matter what the limit is.
|
package/sharedQuery.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
*
|
|
2
4
|
* The "shared query" is a data fetching convenience library that helps users
|
|
3
5
|
* to load data, check for staleness and expiration, cache in memory, local
|
|
4
6
|
* storage or indexed db, and be able to manipulate the data anytime.
|
|
@@ -9,10 +11,80 @@
|
|
|
9
11
|
* records and the total byte size of data can be limited so as to ensure the
|
|
10
12
|
* query does not take up too much memory.
|
|
11
13
|
*
|
|
12
|
-
*
|
|
14
|
+
* Example:
|
|
15
|
+
* ```
|
|
16
|
+
* import { sharedQuery, useSharedQuery } from "@choksheak/react-utils/sharedQuery";
|
|
17
|
+
*
|
|
18
|
+
* // Create a new shared query in the top level scope.
|
|
19
|
+
* // The query function `queryFn` here does not take any arguments.
|
|
20
|
+
* //
|
|
21
|
+
* // Only the queryName and queryFn are required and everything else is
|
|
22
|
+
* // optional.
|
|
23
|
+
* //
|
|
24
|
+
* // The queryName must be unique, otherwise we will throw an
|
|
25
|
+
* // error. This is because defining the same queryName for two different
|
|
26
|
+
* // queries implies that the queries are the same, thus violating the idea
|
|
27
|
+
* // of a "shared" query, and almost certainly points to a bug in the code.
|
|
28
|
+
* export const usersQuery = sharedQuery({
|
|
29
|
+
* queryName: "users",
|
|
30
|
+
* queryFn: listUsers,
|
|
31
|
+
* persistTo: "indexedDb",
|
|
32
|
+
* staleMs: MS_PER_DAY,
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* // Note that the query will only start fetching on mount in React.
|
|
36
|
+
* // But you can do `usersQuery.getCachedOrFetch()` here to prefetch the
|
|
37
|
+
* // data if necessary.
|
|
38
|
+
* usersQuery.getCachedOrFetch();
|
|
39
|
+
*
|
|
40
|
+
* // Example of using a query function with arguments.
|
|
41
|
+
* export const getUserQuery = sharedQuery({
|
|
42
|
+
* queryName: "getUser",
|
|
43
|
+
* queryFn: getUser, // getUser(userId: string) => User
|
|
44
|
+
* persistTo: "indexedDb",
|
|
45
|
+
* staleMs: MS_PER_DAY,
|
|
46
|
+
* });
|
|
47
|
+
*
|
|
48
|
+
* export const UsersComponent: React.FC = () => {
|
|
49
|
+
* // Second argument is optional, and is an array of arguments to be
|
|
50
|
+
* // passed to the `queryFn`.
|
|
51
|
+
* const users = useSharedQuery(usersQuery);
|
|
52
|
+
*
|
|
53
|
+
* return (
|
|
54
|
+
* <>
|
|
55
|
+
* <h1>List of Users</h1>
|
|
56
|
+
*
|
|
57
|
+
* <button onclick={users.refetch}>Refresh</button>
|
|
58
|
+
*
|
|
59
|
+
* {users.loading && <Loader />}
|
|
60
|
+
*
|
|
61
|
+
* {users.error && !users.data && <p>Error: {String(users.error)}</p>}
|
|
62
|
+
*
|
|
63
|
+
* <p>Data: {users.data ? JSON.stringify(users.data) : "-"}</p>
|
|
64
|
+
* </>
|
|
65
|
+
* );
|
|
66
|
+
* };
|
|
67
|
+
*
|
|
68
|
+
* export const AnotherComponent: React.FC = () => {
|
|
69
|
+
* // This query will be deduped with the one above, sharing the same
|
|
70
|
+
* // data and the same fetches.
|
|
71
|
+
* const users = useSharedQuery(usersQuery);
|
|
72
|
+
* ...
|
|
73
|
+
* };
|
|
74
|
+
*
|
|
75
|
+
* export const DisplayOneUser: React.FC<{userId: string}> = ({userId}) => {
|
|
76
|
+
* // Example of using the query with an argument. Whenever the argument
|
|
77
|
+
* // changes, the data will be fetched and updated automatically.
|
|
78
|
+
* const user = useSharedQuery(getUserQuery, [userId]);
|
|
79
|
+
*
|
|
80
|
+
* return <>User: {user.data ? JSON.stringify(user.data) : "-"}</>;
|
|
81
|
+
* };
|
|
82
|
+
* ```
|
|
13
83
|
*/
|
|
14
84
|
import { SharedState, SharedStateOptions } from "./sharedState";
|
|
85
|
+
/** Type of the queryFn used to fetch the data from source. */
|
|
15
86
|
export type QueryFn<TArgs extends unknown[], TData> = (...args: TArgs) => Promise<TData> | TData;
|
|
87
|
+
/** An entry for a persisted state corresponding to one query key. */
|
|
16
88
|
export type QueryStateValue<TData> = {
|
|
17
89
|
/**
|
|
18
90
|
* When this record was last updated. Used for LRU determination. If you want
|
|
@@ -30,6 +102,7 @@ export type QueryStateValue<TData> = {
|
|
|
30
102
|
error?: unknown;
|
|
31
103
|
errorUpdatedMs?: number;
|
|
32
104
|
};
|
|
105
|
+
/** Return type for useSharedQuery(). */
|
|
33
106
|
export type UseQueryResult<TData> = QueryStateValue<TData> & {
|
|
34
107
|
/**
|
|
35
108
|
* Cancels the inflight query (if any). Returns true if canceled.
|
|
@@ -46,12 +119,14 @@ export type UseQueryResult<TData> = QueryStateValue<TData> & {
|
|
|
46
119
|
setData: (data: TData, dataUpdatedMs?: number) => void;
|
|
47
120
|
deleteData: () => void;
|
|
48
121
|
};
|
|
122
|
+
/** List of all available persistence options. */
|
|
49
123
|
export type PersistTo = "localStorage" | "indexedDb";
|
|
50
124
|
/**
|
|
51
125
|
* The full query state is a record of all the fetch states mapped by
|
|
52
126
|
* query key to the state entry.
|
|
53
127
|
*/
|
|
54
128
|
export type SharedQueryState<TData> = Record<string, QueryStateValue<TData>>;
|
|
129
|
+
/** Options to configure a shared query. */
|
|
55
130
|
export type SharedQueryOptions<TArgs extends unknown[], TData> = {
|
|
56
131
|
queryName: string;
|
|
57
132
|
queryFn: QueryFn<TArgs, TData>;
|
|
@@ -73,9 +148,16 @@ export declare const SharedQueryDefaults: {
|
|
|
73
148
|
maxBytes: number;
|
|
74
149
|
log: (...args: unknown[]) => void;
|
|
75
150
|
};
|
|
151
|
+
/** Please use sharedQuery() instead. */
|
|
76
152
|
export declare class SharedQuery<TArgs extends unknown[], TData> {
|
|
77
153
|
private readonly inflightQueries;
|
|
78
154
|
readonly queryState: SharedState<SharedQueryState<TData>>;
|
|
155
|
+
/**
|
|
156
|
+
* Keep track of the mounted query keys. This handles the case where an
|
|
157
|
+
* earlier-started query returns later, and when it returns, it was already
|
|
158
|
+
* unmounted, thus making it eligible for clean up right after fetch.
|
|
159
|
+
*/
|
|
160
|
+
private readonly mountedKeys;
|
|
79
161
|
readonly queryName: string;
|
|
80
162
|
private readonly queryFn;
|
|
81
163
|
readonly expiryMs: number;
|
|
@@ -143,6 +225,10 @@ export declare class SharedQuery<TArgs extends unknown[], TData> {
|
|
|
143
225
|
deleteData(queryKey: string): void;
|
|
144
226
|
/** Delete all currently cached data & all inflight promises. */
|
|
145
227
|
clear(): void;
|
|
228
|
+
/** Keep track of mounted keys. */
|
|
229
|
+
mount(queryKey: string): void;
|
|
230
|
+
/** Clean up mounted keys. */
|
|
231
|
+
unmount(queryKey: string): void;
|
|
146
232
|
/**
|
|
147
233
|
* Discard the oldest entries if the size exceeds the limit. The last
|
|
148
234
|
* remaining record cannot be deleted no matter what the limit is.
|
package/sharedQuery.min.cjs
CHANGED
|
@@ -1,2 +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});
|
|
1
|
+
"use strict";var q=Object.create;var m=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)m(i,t,{get:e[t],enumerable:!0})},K=(i,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of R(e))!j.call(i,s)&&s!==t&&m(i,s,{get:()=>e[s],enumerable:!(r=L(e,s))||r.enumerable});return i};var Q=(i,e,t)=>(t=i!=null?q(_(i)):{},K(e||!i||!i.__esModule?m(t,"default",{value:i,enumerable:!0}):t,i)),W=i=>K(m({},"__esModule",{value:!0}),i);var X={};J(X,{SharedQuery:()=>x,SharedQueryDefaults:()=>h,sharedQuery:()=>I,useSharedQuery:()=>H});module.exports=W(X);var U=require("@choksheak/ts-utils/timeConstants"),S=Q(require("object-sizeof")),p=require("react");var O=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:s,store:a}=i??{};if(a)return a;let n=s||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 s=JSON.parse(r);if(!s||s.expiresAt&&Date.now()>=s.expiresAt){window.localStorage.removeItem(t);return}return s.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 B={storeExpiryMs:30*O.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 s=this.subscribersByKey.get(e);if(!s)return;let a=Array.from(s.values());for(let n of a)try{n(t,r)}catch(y){console.error("Error invoking subscriber:",y)}}},g=new A,v="state",Y=0,M=class{constructor(e,t){this.defaultValue=e;this.pubSubKey=String(Y++),this.storageAdapter=k(t,B.storeExpiryMs),this.setValueBounded=a=>this.setValue(a),g.setNoNotify(this.pubSubKey,e);let r=t?.lazyLoad;(r!==void 0?r:B.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 $(i){return i.initDefaultValueOnce(),[(0,z.useSyncExternalStore)(t=>i.subscribe(t),()=>i.getSnapshot(),()=>i.getSnapshot()),i.setValueBounded]}function P(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((s,a)=>(s[a]=r[a],s),{})})}var C=Q(require("lodash/isEqual")),T=require("react");function E(i){let[e,t]=(0,T.useState)(i);return(0,T.useMemo)(()=>e===i||(0,C.default)(e,i)?e:(t(i),i),[e,i])}var h={staleMs:0,expiryMs:30*U.MS_PER_DAY,refetchOnStale:!0,persistTo:void 0,maxSize:100,maxBytes:1e5,log:(...i)=>console.log("[sharedQuery]",...i)},x=class{inflightQueries=new Map;queryState;mountedKeys=new Map;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+":"+P(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 s=this.startFetching(e,t),a=new AbortController;return this.inflightQueries.set(e,{promise:s,abortController:a}),s}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 s={lastUpdatedMs:Date.now(),loading:!1,data:t,dataUpdatedMs:r};this.queryState.setValue(a=>({...a,[e]:s})),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 s=this.inflightQueries.get(e);s&&(this.inflightQueries.delete(e),s.abortController.abort("Deleted"))}clear(){this.queryState.delete();for(let{abortController:e}of Array.from(this.inflightQueries.values()))e.abort("Cleared");this.inflightQueries.clear()}mount(e){this.mountedKeys.set(e,(this.mountedKeys.get(e)??0)+1)}unmount(e){let t=this.mountedKeys.get(e);t===1?this.mountedKeys.delete(e):this.mountedKeys.set(e,(t??0)-1)}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),s=e,a=!1;if(this.maxSize){let n=t-this.maxSize;if(n>0){this.log(`enforceSizeLimit for ${this.queryName} needed due to size ${t} > ${this.maxSize}`),s={...s},a=!0;let y=Object.entries(s).sort((l,o)=>l[1].lastUpdatedMs-o[1].lastUpdatedMs);for(let l=0;l<y.length&&n>0;l++){let o=y[l][0];this.mountedKeys.has(o)?this.log(`Cannot clean up ${o} as it is mounted`):(this.log(`Cleaning up unmounted ${o}`),delete s[o],n--)}}}if(this.maxBytes&&Object.keys(s).length>1){let n=a?(0,S.default)(s):r,y=n-this.maxBytes;if(y>0){this.log(`enforceSizeLimit for ${this.queryName} needed due to byte size ${n.toLocaleString()} > ${this.maxBytes.toLocaleString()}`),a||(s={...s},a=!0);let l=Object.entries(s).sort((o,d)=>o[1].lastUpdatedMs-d[1].lastUpdatedMs);for(let o=0;o<l.length&&y>0;o++){let d=l[o][0];if(this.mountedKeys.has(d))this.log(`Cannot clean up ${d} as it is mounted`);else{this.log(`Cleaning up unmounted ${d}`);let c=(0,S.default)(d)+(0,S.default)(s[d]);delete s[d],y-=c}}}}if(a){this.queryState.setValue(s),this.log(`Trimmed data for ${this.queryName}: size=(${t} -> ${Object.keys(s).length}) (limit=${this.maxSize}), byteSize=(${r.toLocaleString()} -> ${(0,S.default)(s).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()})`)}},N=new Set;function I(i){if(N.has(i.queryName))throw new Error(`Duplicate shared query "${i.queryName}"`);return N.add(i.queryName),new x(i)}var G={lastUpdatedMs:0,loading:!0};function H(i,e=[]){let t=E(e),[r,s]=$(i.queryState),a=(0,p.useRef)(!0),n=(0,p.useMemo)(()=>i.getQueryKey(t),[i,t]),y=r[n],l=(0,p.useCallback)(d=>{s(c=>{let u={...c};return u[n]=typeof d=="function"?d(u[n]):d,u})},[n,s]),o=(0,p.useCallback)(async d=>{i.log(`Begin executing shared query ${n}`),l(c=>({...c??{},lastUpdatedMs:Date.now(),loading:!0}));try{let c=await(d?i.updateFromSource(...t):i.getCachedOrFetch(...t));if(a.current){let u=Date.now();l({lastUpdatedMs:u,loading:!1,data:c,dataUpdatedMs:u})}}catch(c){if(a.current){let u=Date.now();l(b=>({...b??{},lastUpdatedMs:u,loading:!1,error:c,errorUpdatedMs:u}))}}},[i,n,l,t]);return(0,p.useEffect)(()=>(a.current=!0,i.mount(n),o(!1),()=>{a.current=!1,i.unmount(n)}),[o,i,n]),(0,p.useMemo)(()=>({...y??G,abortCurrentQuery:c=>{let u=i.getAbortControllerByKey(n);return u?.abort(c),!!u},refetch:()=>{o(!0)},setData:(c,u)=>{let b=Date.now();l(F=>({...F??{},data:c,dataUpdatedMs:u??b,lastUpdatedMs:b,loading:!1}))},deleteData:()=>{s(c=>{let u={...c};return delete u[n],u})}}),[o,i,n,y,s,l])}0&&(module.exports={SharedQuery,SharedQueryDefaults,sharedQuery,useSharedQuery});
|
|
2
2
|
//# sourceMappingURL=sharedQuery.min.cjs.map
|