@choksheak/react-utils 0.2.5 → 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/README.md CHANGED
@@ -2,4 +2,6 @@
2
2
 
3
3
  Miscellaneous React+TypeScript utilities with support for full tree-shaking.
4
4
 
5
+ Source code: https://github.com/choksheak/react-utils
6
+
5
7
  Online docs: https://choksheak.github.io/react-utils/
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
- fetcherWithRetries: () => fetcherWithRetries
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
- fetcherWithRetries
50
+ simpleFetcher
51
51
  });
package/fetcher.d.mts CHANGED
@@ -1,8 +1,35 @@
1
1
  import * as wretch from 'wretch';
2
2
 
3
- /** Name this as "fetcher" to make it easier to understand. */
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
- /** Fetcher with default short retries. */
6
- declare const fetcherWithRetries: wretch.Wretch<unknown, unknown, undefined, undefined>;
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, fetcherWithRetries };
35
+ export { fetcher, simpleFetcher };
package/fetcher.d.ts CHANGED
@@ -1,4 +1,31 @@
1
- /** Name this as "fetcher" to make it easier to understand. */
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
- /** Fetcher with default short retries. */
4
- export declare const fetcherWithRetries: import("wretch").Wretch<unknown, unknown, undefined, undefined>;
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 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});
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
@@ -1 +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"]}
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(),i=r().middlewares([e({maxAttempts:3,delayTimer:1e3,retryOnNetworkError:!0})]);export{m as fetcher,i as fetcherWithRetries};
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
@@ -1 +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"]}
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
- fetcherWithRetries
14
+ simpleFetcher
15
15
  };
package/package.json CHANGED
@@ -1,12 +1,9 @@
1
1
  {
2
2
  "name": "@choksheak/react-utils",
3
3
  "license": "The Unlicense",
4
- "version": "0.2.5",
4
+ "version": "0.2.7",
5
5
  "description": "Miscellaneous React+TypeScript utilities with support for full tree-shaking.",
6
6
  "private": false,
7
- "main": "index.js",
8
- "module": "index.mjs",
9
- "types": "index.d.ts",
10
7
  "files": [
11
8
  "**/*"
12
9
  ],
@@ -20,6 +17,7 @@
20
17
  "test:watch": "vitest",
21
18
  "ci": "npm run lint && npm run tsc && npm run build && npm run test",
22
19
  "ls": "lint-staged",
20
+ "clean": "rm -rf dist",
23
21
  "docs": "rm -rf docs && typedoc `find src -name '*.ts' ! -name '*.test.ts'` --out docs --name 'Chok’s React Utilities'",
24
22
  "gen-types": "tsc --emitDeclarationOnly --declaration",
25
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
- const numToCut = oldSize - this.maxSize;
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
- entry2[1].lastUpdatedMs - entry1[1].lastUpdatedMs
572
+ entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs
554
573
  )
555
- ).slice(-numToCut).forEach(([key]) => {
556
- delete newState[key];
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
- const thisSize = (0, import_object_sizeof.default)(key) + (0, import_object_sizeof.default)(newState[key]);
582
- delete newState[key];
583
- bytesToCut -= thisSize;
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
@@ -1,7 +1,93 @@
1
1
  import { SharedStateOptions, SharedState } from './sharedState.mjs';
2
2
  import './utils/storage.mjs';
3
3
 
4
+ /**
5
+ * @packageDocumentation
6
+ *
7
+ * The "shared query" is a data fetching convenience library that helps users
8
+ * to load data, check for staleness and expiration, cache in memory, local
9
+ * storage or indexed db, and be able to manipulate the data anytime.
10
+ *
11
+ * Each shared query creates a new private shared state that handles the data
12
+ * storage. This shared state contains the records for all queries, where each
13
+ * query is a unique set of arguments used to make the query. The number of
14
+ * records and the total byte size of data can be limited so as to ensure the
15
+ * query does not take up too much memory.
16
+ *
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
+ * ```
86
+ */
87
+
88
+ /** Type of the queryFn used to fetch the data from source. */
4
89
  type QueryFn<TArgs extends unknown[], TData> = (...args: TArgs) => Promise<TData> | TData;
90
+ /** An entry for a persisted state corresponding to one query key. */
5
91
  type QueryStateValue<TData> = {
6
92
  /**
7
93
  * When this record was last updated. Used for LRU determination. If you want
@@ -19,6 +105,7 @@ type QueryStateValue<TData> = {
19
105
  error?: unknown;
20
106
  errorUpdatedMs?: number;
21
107
  };
108
+ /** Return type for useSharedQuery(). */
22
109
  type UseQueryResult<TData> = QueryStateValue<TData> & {
23
110
  /**
24
111
  * Cancels the inflight query (if any). Returns true if canceled.
@@ -35,12 +122,14 @@ type UseQueryResult<TData> = QueryStateValue<TData> & {
35
122
  setData: (data: TData, dataUpdatedMs?: number) => void;
36
123
  deleteData: () => void;
37
124
  };
125
+ /** List of all available persistence options. */
38
126
  type PersistTo = "localStorage" | "indexedDb";
39
127
  /**
40
128
  * The full query state is a record of all the fetch states mapped by
41
129
  * query key to the state entry.
42
130
  */
43
131
  type SharedQueryState<TData> = Record<string, QueryStateValue<TData>>;
132
+ /** Options to configure a shared query. */
44
133
  type SharedQueryOptions<TArgs extends unknown[], TData> = {
45
134
  queryName: string;
46
135
  queryFn: QueryFn<TArgs, TData>;
@@ -62,9 +151,16 @@ declare const SharedQueryDefaults: {
62
151
  maxBytes: number;
63
152
  log: (...args: unknown[]) => void;
64
153
  };
154
+ /** Please use sharedQuery() instead. */
65
155
  declare class SharedQuery<TArgs extends unknown[], TData> {
66
156
  private readonly inflightQueries;
67
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;
68
164
  readonly queryName: string;
69
165
  private readonly queryFn;
70
166
  readonly expiryMs: number;
@@ -132,6 +228,10 @@ declare class SharedQuery<TArgs extends unknown[], TData> {
132
228
  deleteData(queryKey: string): void;
133
229
  /** Delete all currently cached data & all inflight promises. */
134
230
  clear(): void;
231
+ /** Keep track of mounted keys. */
232
+ mount(queryKey: string): void;
233
+ /** Clean up mounted keys. */
234
+ unmount(queryKey: string): void;
135
235
  /**
136
236
  * Discard the oldest entries if the size exceeds the limit. The last
137
237
  * remaining record cannot be deleted no matter what the limit is.
package/sharedQuery.d.ts CHANGED
@@ -1,5 +1,90 @@
1
+ /**
2
+ * @packageDocumentation
3
+ *
4
+ * The "shared query" is a data fetching convenience library that helps users
5
+ * to load data, check for staleness and expiration, cache in memory, local
6
+ * storage or indexed db, and be able to manipulate the data anytime.
7
+ *
8
+ * Each shared query creates a new private shared state that handles the data
9
+ * storage. This shared state contains the records for all queries, where each
10
+ * query is a unique set of arguments used to make the query. The number of
11
+ * records and the total byte size of data can be limited so as to ensure the
12
+ * query does not take up too much memory.
13
+ *
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
+ * ```
83
+ */
1
84
  import { SharedState, SharedStateOptions } from "./sharedState";
85
+ /** Type of the queryFn used to fetch the data from source. */
2
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. */
3
88
  export type QueryStateValue<TData> = {
4
89
  /**
5
90
  * When this record was last updated. Used for LRU determination. If you want
@@ -17,6 +102,7 @@ export type QueryStateValue<TData> = {
17
102
  error?: unknown;
18
103
  errorUpdatedMs?: number;
19
104
  };
105
+ /** Return type for useSharedQuery(). */
20
106
  export type UseQueryResult<TData> = QueryStateValue<TData> & {
21
107
  /**
22
108
  * Cancels the inflight query (if any). Returns true if canceled.
@@ -33,12 +119,14 @@ export type UseQueryResult<TData> = QueryStateValue<TData> & {
33
119
  setData: (data: TData, dataUpdatedMs?: number) => void;
34
120
  deleteData: () => void;
35
121
  };
122
+ /** List of all available persistence options. */
36
123
  export type PersistTo = "localStorage" | "indexedDb";
37
124
  /**
38
125
  * The full query state is a record of all the fetch states mapped by
39
126
  * query key to the state entry.
40
127
  */
41
128
  export type SharedQueryState<TData> = Record<string, QueryStateValue<TData>>;
129
+ /** Options to configure a shared query. */
42
130
  export type SharedQueryOptions<TArgs extends unknown[], TData> = {
43
131
  queryName: string;
44
132
  queryFn: QueryFn<TArgs, TData>;
@@ -60,9 +148,16 @@ export declare const SharedQueryDefaults: {
60
148
  maxBytes: number;
61
149
  log: (...args: unknown[]) => void;
62
150
  };
151
+ /** Please use sharedQuery() instead. */
63
152
  export declare class SharedQuery<TArgs extends unknown[], TData> {
64
153
  private readonly inflightQueries;
65
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;
66
161
  readonly queryName: string;
67
162
  private readonly queryFn;
68
163
  readonly expiryMs: number;
@@ -130,6 +225,10 @@ export declare class SharedQuery<TArgs extends unknown[], TData> {
130
225
  deleteData(queryKey: string): void;
131
226
  /** Delete all currently cached data & all inflight promises. */
132
227
  clear(): void;
228
+ /** Keep track of mounted keys. */
229
+ mount(queryKey: string): void;
230
+ /** Clean up mounted keys. */
231
+ unmount(queryKey: string): void;
133
232
  /**
134
233
  * Discard the oldest entries if the size exceeds the limit. The last
135
234
  * remaining record cannot be deleted no matter what the limit is.
@@ -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