@choksheak/react-utils 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +3 -1
  2. package/fetcher.min.cjs +2 -0
  3. package/fetcher.min.cjs.map +1 -0
  4. package/fetcher.min.mjs +2 -0
  5. package/fetcher.min.mjs.map +1 -0
  6. package/index.min.cjs +2 -0
  7. package/index.min.cjs.map +1 -0
  8. package/index.min.mjs +2 -0
  9. package/index.min.mjs.map +1 -0
  10. package/package.json +38 -5
  11. package/{sharedQuery.js → sharedQuery.cjs} +202 -79
  12. package/sharedQuery.d.mts +73 -9
  13. package/sharedQuery.d.ts +72 -8
  14. package/sharedQuery.min.cjs +2 -0
  15. package/sharedQuery.min.cjs.map +1 -0
  16. package/sharedQuery.min.mjs +2 -0
  17. package/sharedQuery.min.mjs.map +1 -0
  18. package/sharedQuery.mjs +200 -77
  19. package/sharedState.min.cjs +2 -0
  20. package/sharedState.min.cjs.map +1 -0
  21. package/sharedState.min.mjs +2 -0
  22. package/sharedState.min.mjs.map +1 -0
  23. package/utils/storage.min.cjs +2 -0
  24. package/utils/storage.min.cjs.map +1 -0
  25. package/utils/storage.min.mjs +2 -0
  26. package/utils/storage.min.mjs.map +1 -0
  27. package/utils/stringify.min.cjs +2 -0
  28. package/utils/stringify.min.cjs.map +1 -0
  29. package/utils/stringify.min.mjs +2 -0
  30. package/utils/stringify.min.mjs.map +1 -0
  31. package/utils/useDeepMemo.cjs +51 -0
  32. package/utils/useDeepMemo.d.mts +4 -0
  33. package/utils/useDeepMemo.d.ts +2 -0
  34. package/utils/useDeepMemo.min.cjs +2 -0
  35. package/utils/useDeepMemo.min.cjs.map +1 -0
  36. package/utils/useDeepMemo.min.mjs +2 -0
  37. package/utils/useDeepMemo.min.mjs.map +1 -0
  38. package/utils/useDeepMemo.mjs +16 -0
  39. package/docs/.nojekyll +0 -1
  40. package/docs/assets/hierarchy.js +0 -1
  41. package/docs/assets/highlight.css +0 -71
  42. package/docs/assets/icons.js +0 -18
  43. package/docs/assets/icons.svg +0 -1
  44. package/docs/assets/main.js +0 -60
  45. package/docs/assets/navigation.js +0 -1
  46. package/docs/assets/search.js +0 -1
  47. package/docs/assets/style.css +0 -1633
  48. package/docs/classes/sharedQuery.SharedQuery.html +0 -22
  49. package/docs/classes/sharedState.SharedState.html +0 -9
  50. package/docs/classes/utils_storage.InMemoryStorageAdapter.html +0 -9
  51. package/docs/classes/utils_storage.IndexedDbAdapter.html +0 -9
  52. package/docs/classes/utils_storage.LocalStorageAdapter.html +0 -9
  53. package/docs/functions/sharedQuery.sharedQuery.html +0 -9
  54. package/docs/functions/sharedQuery.useSharedQuery.html +0 -6
  55. package/docs/functions/sharedState.sharedState.html +0 -3
  56. package/docs/functions/sharedState.useSharedState.html +0 -3
  57. package/docs/functions/sharedState.useSharedStateSetter.html +0 -2
  58. package/docs/functions/sharedState.useSharedStateValue.html +0 -2
  59. package/docs/functions/utils_storage.getStorageAdapter.html +0 -1
  60. package/docs/functions/utils_stringify.stringifyDeterministicForKeys.html +0 -7
  61. package/docs/hierarchy.html +0 -1
  62. package/docs/index.html +0 -2
  63. package/docs/modules/fetcher.html +0 -1
  64. package/docs/modules/index.html +0 -1
  65. package/docs/modules/sharedQuery.html +0 -1
  66. package/docs/modules/sharedState.html +0 -1
  67. package/docs/modules/utils_storage.html +0 -1
  68. package/docs/modules/utils_stringify.html +0 -1
  69. package/docs/modules.html +0 -1
  70. package/docs/types/sharedQuery.FetchFn.html +0 -1
  71. package/docs/types/sharedQuery.PersistTo.html +0 -1
  72. package/docs/types/sharedQuery.QueryStateValue.html +0 -9
  73. package/docs/types/sharedQuery.SharedQueryOptions.html +0 -1
  74. package/docs/types/sharedQuery.SharedQueryState.html +0 -3
  75. package/docs/types/sharedQuery.UseQueryResult.html +0 -1
  76. package/docs/types/sharedState.SharedStateOptions.html +0 -1
  77. package/docs/types/utils_storage.StorageAdapter.html +0 -5
  78. package/docs/types/utils_storage.StorageOptions.html +0 -5
  79. package/docs/variables/fetcher.fetcher.html +0 -2
  80. package/docs/variables/fetcher.fetcherWithRetries.html +0 -2
  81. package/docs/variables/sharedQuery.SharedQueryDefaults.html +0 -2
  82. package/docs/variables/sharedState.SharedStateDefaults.html +0 -6
  83. package/src/fetcher.ts +0 -14
  84. package/src/index.ts +0 -4
  85. package/src/sharedQuery.ts +0 -507
  86. package/src/sharedState.ts +0 -232
  87. package/src/utils/storage.ts +0 -197
  88. package/src/utils/stringify.ts +0 -49
  89. /package/{fetcher.js → fetcher.cjs} +0 -0
  90. /package/{index.js → index.cjs} +0 -0
  91. /package/{sharedState.js → sharedState.cjs} +0 -0
  92. /package/utils/{storage.js → storage.cjs} +0 -0
  93. /package/utils/{stringify.js → stringify.cjs} +0 -0
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
- # Chok's React Utilities
1
+ # @choksheak/react-utils
2
2
 
3
3
  Miscellaneous React+TypeScript utilities with support for full tree-shaking.
4
+
5
+ Online docs: https://choksheak.github.io/react-utils/
@@ -0,0 +1,2 @@
1
+ "use strict";var f=Object.create;var m=Object.defineProperty;var h=Object.getOwnPropertyDescriptor;var a=Object.getOwnPropertyNames;var d=Object.getPrototypeOf,n=Object.prototype.hasOwnProperty;var w=(r,e)=>{for(var t in e)m(r,t,{get:e[t],enumerable:!0})},p=(r,e,t,c)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of a(e))!n.call(r,o)&&o!==t&&m(r,o,{get:()=>e[o],enumerable:!(c=h(e,o))||c.enumerable});return r};var x=(r,e,t)=>(t=r!=null?f(d(r)):{},p(e||!r||!r.__esModule?m(t,"default",{value:r,enumerable:!0}):t,r)),y=r=>p(m({},"__esModule",{value:!0}),r);var u={};w(u,{fetcher:()=>l,fetcherWithRetries:()=>k});module.exports=y(u);var i=x(require("wretch")),s=require("wretch/middlewares"),l=(0,i.default)(),k=(0,i.default)().middlewares([(0,s.retry)({maxAttempts:3,delayTimer:1e3,retryOnNetworkError:!0})]);0&&(module.exports={fetcher,fetcherWithRetries});
2
+ //# sourceMappingURL=fetcher.min.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/fetcher.ts"],"sourcesContent":["import wretch from \"wretch\";\nimport { retry } from \"wretch/middlewares\";\n\n/** Name this as \"fetcher\" to make it easier to understand. */\nexport const fetcher = wretch();\n\n/** Fetcher with default short retries. */\nexport const fetcherWithRetries = wretch().middlewares([\n retry({\n maxAttempts: 3,\n delayTimer: 1000,\n retryOnNetworkError: true,\n }),\n]);\n"],"mappings":"0jBAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,aAAAE,EAAA,uBAAAC,IAAA,eAAAC,EAAAJ,GAAA,IAAAK,EAAmB,qBACnBC,EAAsB,8BAGTJ,KAAU,EAAAK,SAAO,EAGjBJ,KAAqB,EAAAI,SAAO,EAAE,YAAY,IACrD,SAAM,CACJ,YAAa,EACb,WAAY,IACZ,oBAAqB,EACvB,CAAC,CACH,CAAC","names":["fetcher_exports","__export","fetcher","fetcherWithRetries","__toCommonJS","import_wretch","import_middlewares","wretch"]}
@@ -0,0 +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};
2
+ //# sourceMappingURL=fetcher.min.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/fetcher.ts"],"sourcesContent":["import wretch from \"wretch\";\nimport { retry } from \"wretch/middlewares\";\n\n/** Name this as \"fetcher\" to make it easier to understand. */\nexport const fetcher = wretch();\n\n/** Fetcher with default short retries. */\nexport const fetcherWithRetries = wretch().middlewares([\n retry({\n maxAttempts: 3,\n delayTimer: 1000,\n retryOnNetworkError: true,\n }),\n]);\n"],"mappings":"AAAA,OAAOA,MAAY,SACnB,OAAS,SAAAC,MAAa,qBAGf,IAAMC,EAAUF,EAAO,EAGjBG,EAAqBH,EAAO,EAAE,YAAY,CACrDC,EAAM,CACJ,YAAa,EACb,WAAY,IACZ,oBAAqB,EACvB,CAAC,CACH,CAAC","names":["wretch","retry","fetcher","fetcherWithRetries"]}
package/index.min.cjs ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";console.log("Hello from Chok's React Utils");
2
+ //# sourceMappingURL=index.min.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Unused file. To support full tree-shaking, please import from the individual\n// files directly.\n\nconsole.log(`Hello from Chok's React Utils`);\n"],"mappings":"aAGA,QAAQ,IAAI,+BAA+B","names":[]}
package/index.min.mjs ADDED
@@ -0,0 +1,2 @@
1
+ var e=(l,o)=>()=>(o||l((o={exports:{}}).exports,o),o.exports);var s=e(()=>{console.log("Hello from Chok's React Utils")});export default s();
2
+ //# sourceMappingURL=index.min.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Unused file. To support full tree-shaking, please import from the individual\n// files directly.\n\nconsole.log(`Hello from Chok's React Utils`);\n"],"mappings":"8DAAA,IAAAA,EAAAC,EAAA,KAGA,QAAQ,IAAI,+BAA+B","names":["require_index","__commonJSMin"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@choksheak/react-utils",
3
3
  "license": "The Unlicense",
4
- "version": "0.2.3",
4
+ "version": "0.2.5",
5
5
  "description": "Miscellaneous React+TypeScript utilities with support for full tree-shaking.",
6
6
  "private": false,
7
7
  "main": "index.js",
@@ -13,7 +13,7 @@
13
13
  "scripts": {
14
14
  "lint": "eslint .",
15
15
  "fix": "eslint . --fix",
16
- "tsc": "tsc --noEmit",
16
+ "tsc": "tsc --noEmit --allowSyntheticDefaultImports",
17
17
  "ft": "npm run fix && npm run tsc",
18
18
  "build": "tsup src/*",
19
19
  "test": "vitest run",
@@ -63,7 +63,9 @@
63
63
  "devDependencies": {
64
64
  "@eslint/compat": "^1.4.0",
65
65
  "@eslint/js": "^9.38.0",
66
+ "@swc/core": "^1.14.0",
66
67
  "@types/jest": "^30.0.0",
68
+ "@types/lodash": "^4.17.20",
67
69
  "@types/react": "^19.2.2",
68
70
  "@types/react-dom": "^19.2.2",
69
71
  "@typescript-eslint/eslint-plugin": "^8.46.2",
@@ -79,6 +81,7 @@
79
81
  "globals": "^16.4.0",
80
82
  "jiti": "^2.6.1",
81
83
  "lint-staged": "^16.2.6",
84
+ "lodash": "^4.17.21",
82
85
  "object-sizeof": "^2.6.5",
83
86
  "prettier": "^3.6.2",
84
87
  "react": "^19.2.0",
@@ -89,28 +92,58 @@
89
92
  "typedoc": "^0.28.14",
90
93
  "typescript": "^5.9.3",
91
94
  "typescript-eslint": "^8.46.2",
95
+ "use-deep-compare": "^1.3.0",
92
96
  "vitest": "^4.0.3",
93
97
  "wretch": "^3.0.2"
94
98
  },
95
99
  "peerDependencies": {
96
100
  "@choksheak/ts-utils": ">=0.2.9",
101
+ "lodash": ">=4.17.21",
97
102
  "object-sizeof": ">=2.6.5",
98
103
  "react": ">=16",
104
+ "use-deep-compare": ">=1.3.0",
99
105
  "wretch": ">=3"
100
106
  },
101
107
  "exports": {
102
108
  "./package.json": "./package.json",
103
109
  "./fetcher": {
104
110
  "types": "./fetcher.d.ts",
105
- "import": "./fetcher.js"
111
+ "import": {
112
+ "production": "./fetcher.min.mjs",
113
+ "development": "./fetcher.mjs",
114
+ "default": "./fetcher.mjs"
115
+ },
116
+ "require": {
117
+ "production": "./fetcher.min.cjs",
118
+ "development": "./fetcher.cjs",
119
+ "default": "./fetcher.cjs"
120
+ }
106
121
  },
107
122
  "./sharedQuery": {
108
123
  "types": "./sharedQuery.d.ts",
109
- "import": "./sharedQuery.js"
124
+ "import": {
125
+ "production": "./sharedQuery.min.mjs",
126
+ "development": "./sharedQuery.mjs",
127
+ "default": "./sharedQuery.mjs"
128
+ },
129
+ "require": {
130
+ "production": "./sharedQuery.min.cjs",
131
+ "development": "./sharedQuery.cjs",
132
+ "default": "./sharedQuery.cjs"
133
+ }
110
134
  },
111
135
  "./sharedState": {
112
136
  "types": "./sharedState.d.ts",
113
- "import": "./sharedState.js"
137
+ "import": {
138
+ "production": "./sharedState.min.mjs",
139
+ "development": "./sharedState.mjs",
140
+ "default": "./sharedState.mjs"
141
+ },
142
+ "require": {
143
+ "production": "./sharedState.min.cjs",
144
+ "development": "./sharedState.cjs",
145
+ "default": "./sharedState.cjs"
146
+ }
114
147
  }
115
148
  },
116
149
  "typesVersions": {
@@ -38,7 +38,7 @@ __export(sharedQuery_exports, {
38
38
  module.exports = __toCommonJS(sharedQuery_exports);
39
39
  var import_timeConstants2 = require("@choksheak/ts-utils/timeConstants");
40
40
  var import_object_sizeof = __toESM(require("object-sizeof"));
41
- var import_react2 = require("react");
41
+ var import_react3 = require("react");
42
42
 
43
43
  // src/sharedState.ts
44
44
  var import_timeConstants = require("@choksheak/ts-utils/timeConstants");
@@ -303,18 +303,39 @@ function stringifyDeterministicForKeys(params) {
303
303
  });
304
304
  }
305
305
 
306
+ // src/utils/useDeepMemo.ts
307
+ var import_isEqual = __toESM(require("lodash/isEqual"));
308
+ var import_react2 = require("react");
309
+ function useDeepMemo(value) {
310
+ const [state, setState] = (0, import_react2.useState)(value);
311
+ return (0, import_react2.useMemo)(() => {
312
+ if (state === value || (0, import_isEqual.default)(state, value)) {
313
+ return state;
314
+ }
315
+ setState(value);
316
+ return value;
317
+ }, [state, value]);
318
+ }
319
+
306
320
  // src/sharedQuery.ts
307
321
  var SharedQueryDefaults = {
322
+ // Refetch on every page load.
308
323
  staleMs: 0,
324
+ // Keep in cache for a long time.
309
325
  expiryMs: 30 * import_timeConstants2.MS_PER_DAY,
326
+ // Always refetch automatically if the data is stale.
327
+ refetchOnStale: true,
328
+ // No default persistence configured.
310
329
  persistTo: void 0,
330
+ // 100 is not too large, but please tweak accordingly.
311
331
  maxSize: 100,
312
- // 100 is not too large, but please tweak accordingly
313
- maxBytes: 1e5
314
- // 100kb before GC
332
+ // 100kb limit before discarding old records.
333
+ maxBytes: 1e5,
334
+ // Default to log to console.
335
+ log: (...args) => console.log("[sharedQuery]", ...args)
315
336
  };
316
337
  var SharedQuery = class {
317
- inflightPromises = /* @__PURE__ */ new Map();
338
+ inflightQueries = /* @__PURE__ */ new Map();
318
339
  queryState;
319
340
  queryName;
320
341
  queryFn;
@@ -323,22 +344,27 @@ var SharedQuery = class {
323
344
  refetchOnStale;
324
345
  maxSize;
325
346
  maxBytes;
347
+ log;
326
348
  constructor(options) {
327
349
  this.queryName = options.queryName;
328
350
  this.queryFn = options.queryFn;
329
- this.expiryMs = options?.expiryMs !== void 0 ? options?.expiryMs : SharedQueryDefaults.expiryMs;
330
- this.staleMs = options?.staleMs !== void 0 ? options?.staleMs : SharedQueryDefaults.staleMs;
331
- this.refetchOnStale = Boolean(options?.refetchOnStale);
332
- const persistTo = options.persistTo ?? SharedQueryDefaults.persistTo;
333
- if (persistTo === "localStorage") {
334
- options.localStorageKey = options.queryName;
335
- } else if (persistTo === "indexedDb") {
336
- options.indexedDbKey = options.queryName;
351
+ this.staleMs = options?.staleMs ?? SharedQueryDefaults.staleMs;
352
+ this.expiryMs = options?.expiryMs ?? SharedQueryDefaults.expiryMs;
353
+ this.refetchOnStale = options?.refetchOnStale ?? SharedQueryDefaults.refetchOnStale;
354
+ if (!options.localStorageKey && !options.indexedDbKey) {
355
+ const persistTo = options.persistTo ?? SharedQueryDefaults.persistTo;
356
+ if (persistTo === "localStorage") {
357
+ options.localStorageKey = options.queryName;
358
+ } else if (persistTo === "indexedDb") {
359
+ options.indexedDbKey = options.queryName;
360
+ }
337
361
  }
338
362
  this.maxSize = options?.maxSize ?? SharedQueryDefaults.maxSize;
339
363
  this.maxBytes = options?.maxBytes ?? SharedQueryDefaults.maxBytes;
364
+ this.log = options?.log ?? SharedQueryDefaults.log;
340
365
  this.queryState = sharedState(
341
366
  {},
367
+ // defaultValue
342
368
  {
343
369
  ...options,
344
370
  // Fallback to use expiryMs for the storeExpiryMs.
@@ -346,9 +372,56 @@ var SharedQuery = class {
346
372
  }
347
373
  );
348
374
  }
375
+ /** Returns a key to identify requests based on the given args. */
349
376
  getQueryKey(args) {
350
377
  return this.queryName + ":" + stringifyDeterministicForKeys(args);
351
378
  }
379
+ /**
380
+ * Get the AbortController to abort the inflight query (if any).
381
+ *
382
+ * Note that once you get the controller, you can also get the signal easily
383
+ * using `controller.signal`. So if you need both the controller and the
384
+ * signal, just use this function.
385
+ */
386
+ getAbortController(args) {
387
+ const queryKey = this.getQueryKey(args);
388
+ return this.getAbortControllerByKey(queryKey);
389
+ }
390
+ getAbortControllerByKey(queryKey) {
391
+ return this.inflightQueries.get(queryKey)?.abortController ?? null;
392
+ }
393
+ /**
394
+ * Get the AbortSignal to check for query abortions.
395
+ *
396
+ * Example:
397
+ * ```
398
+ * const getUserQuery = sharedQuery({
399
+ * queryName: "getUser",
400
+ * queryFn: (userId: string) => {
401
+ * // Get the signal for this current execution.
402
+ * const signal = getUserQuery.getAbortSignal([userId]);
403
+ *
404
+ * // Pass the signal to fetch so that it can be aborted.
405
+ * const response = await fetch(`/users/${userId}`, { signal });
406
+ *
407
+ * // Check for errors.
408
+ * if (!response.ok) {
409
+ * throw new Error(response.statusText);
410
+ * }
411
+ *
412
+ * // Return the data.
413
+ * return await response.json();
414
+ * },
415
+ * });
416
+ * ```
417
+ */
418
+ getAbortSignal(args) {
419
+ const queryKey = this.getQueryKey(args);
420
+ return this.getAbortSignalByKey(queryKey);
421
+ }
422
+ getAbortSignalByKey(queryKey) {
423
+ return this.getAbortControllerByKey(queryKey)?.signal ?? null;
424
+ }
352
425
  isStale(dataUpdatedMs) {
353
426
  if (this.staleMs === 0) return true;
354
427
  const ageMs = Date.now() - dataUpdatedMs;
@@ -364,46 +437,53 @@ var SharedQuery = class {
364
437
  const cached = this.queryState.getSnapshot()?.[queryKey];
365
438
  if (cached?.data && !this.isExpired(cached.dataUpdatedMs ?? 0)) {
366
439
  if (!this.isStale(cached.dataUpdatedMs ?? 0)) {
367
- console.log(
368
- `Return fresh data ${queryKey} from cache without fetching`
369
- );
440
+ this.log(`Return fresh data ${queryKey} from cache without fetching`);
370
441
  return cached.data;
371
442
  }
372
443
  if (this.refetchOnStale) {
373
- console.log(`refetchOnStale for ${queryKey}`);
444
+ this.log(`refetchOnStale for ${queryKey}`);
374
445
  void this.dedupedFetch(queryKey, ...args);
375
446
  }
376
- console.log(`Returning stale data for ${queryKey}`);
447
+ this.log(`Returning stale data for ${queryKey}`);
377
448
  return cached.data;
378
449
  }
379
450
  return await this.dedupedFetch(queryKey, ...args);
380
451
  }
381
452
  dedupedFetch(queryKey, ...args) {
382
- const inflightPromise = this.inflightPromises.get(queryKey);
383
- if (inflightPromise) {
384
- console.log(`Deduplicating inflight fetch for ${queryKey}`);
385
- return inflightPromise;
453
+ const inflightQuery = this.inflightQueries.get(queryKey);
454
+ if (inflightQuery) {
455
+ this.log(`Deduplicating inflight fetch for ${queryKey}`);
456
+ return inflightQuery.promise;
386
457
  }
387
458
  const promise = this.startFetching(queryKey, args);
388
- this.inflightPromises.set(queryKey, promise);
459
+ const abortController = new AbortController();
460
+ this.inflightQueries.set(queryKey, { promise, abortController });
389
461
  return promise;
390
462
  }
391
463
  async startFetching(queryKey, args) {
392
- console.log(`Start fetching ${queryKey}`);
464
+ this.log(`Start fetching ${queryKey}`);
393
465
  try {
394
466
  const data = await this.queryFn(...args);
395
- console.log(`Successfully fetched ${queryKey}`);
467
+ this.log(`Successfully fetched ${queryKey}`);
396
468
  this.setData(queryKey, data, Date.now());
397
469
  return data;
398
470
  } catch (e) {
399
- console.error(`Failed to fetch ${queryKey}: ${e}`);
471
+ this.log(`Failed to fetch ${queryKey}: ${e}`);
400
472
  throw e;
401
473
  } finally {
402
- this.inflightPromises.delete(queryKey);
474
+ this.inflightQueries.delete(queryKey);
403
475
  }
404
476
  }
477
+ /** Get the entire stored entry for a query key. */
478
+ getQueryValue(queryKey) {
479
+ return this.queryState.getSnapshot()?.[queryKey];
480
+ }
481
+ /** Get the current stored data for a query key. */
482
+ getData(queryKey) {
483
+ return this.queryState.getSnapshot()?.[queryKey]?.data;
484
+ }
405
485
  /** Set the data directly if the user obtained it from somewhere else. */
406
- async setData(queryKey, data, dataUpdatedMs = Date.now()) {
486
+ setData(queryKey, data, dataUpdatedMs = Date.now()) {
407
487
  const entry = {
408
488
  lastUpdatedMs: Date.now(),
409
489
  loading: false,
@@ -413,24 +493,35 @@ var SharedQuery = class {
413
493
  this.queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));
414
494
  this.enforceSizeLimit();
415
495
  }
416
- /** Do a new fetch even when the data is already cached. */
496
+ /**
497
+ * Do a new fetch even when the data is already cached, but don't fetch if
498
+ * another fetch is already inflight.
499
+ */
417
500
  async updateFromSource(...args) {
418
501
  const key = this.getQueryKey(args);
419
502
  return await this.dedupedFetch(key, ...args);
420
503
  }
421
504
  /** Delete the cached data for one set of arguments. */
422
- deleteData(args) {
423
- const queryKey = this.getQueryKey(args);
505
+ deleteData(queryKey) {
424
506
  const record = this.queryState.getSnapshot();
425
507
  const clone = { ...record };
426
508
  delete clone[queryKey];
427
509
  this.queryState.setValue(clone);
428
- this.inflightPromises.delete(queryKey);
510
+ const inflight = this.inflightQueries.get(queryKey);
511
+ if (inflight) {
512
+ this.inflightQueries.delete(queryKey);
513
+ inflight.abortController.abort("Deleted");
514
+ }
429
515
  }
430
516
  /** Delete all currently cached data & all inflight promises. */
431
517
  clear() {
432
518
  this.queryState.delete();
433
- this.inflightPromises.clear();
519
+ for (const { abortController } of Array.from(
520
+ this.inflightQueries.values()
521
+ )) {
522
+ abortController.abort("Cleared");
523
+ }
524
+ this.inflightQueries.clear();
434
525
  }
435
526
  /**
436
527
  * Discard the oldest entries if the size exceeds the limit. The last
@@ -451,7 +542,7 @@ var SharedQuery = class {
451
542
  if (this.maxSize) {
452
543
  const numToCut = oldSize - this.maxSize;
453
544
  if (numToCut > 0) {
454
- console.log(
545
+ this.log(
455
546
  `enforceSizeLimit for ${this.queryName} needed due to size ${oldSize} > ${this.maxSize}`
456
547
  );
457
548
  newState = { ...newState };
@@ -470,7 +561,7 @@ var SharedQuery = class {
470
561
  const currentByteSize = needUpdate ? (0, import_object_sizeof.default)(newState) : oldByteSize;
471
562
  let bytesToCut = currentByteSize - this.maxBytes;
472
563
  if (bytesToCut > 0) {
473
- console.log(
564
+ this.log(
474
565
  `enforceSizeLimit for ${this.queryName} needed due to byte size ${currentByteSize.toLocaleString()} > ${this.maxBytes.toLocaleString()}`
475
566
  );
476
567
  if (!needUpdate) {
@@ -495,12 +586,12 @@ var SharedQuery = class {
495
586
  }
496
587
  if (needUpdate) {
497
588
  this.queryState.setValue(newState);
498
- console.log(
589
+ this.log(
499
590
  `Trimmed data for ${this.queryName}: size=(${oldSize} -> ${Object.keys(newState).length}) (limit=${this.maxSize}), byteSize=(${oldByteSize.toLocaleString()} -> ${(0, import_object_sizeof.default)(newState).toLocaleString()}) (limit=${this.maxBytes.toLocaleString()})`
500
591
  );
501
592
  return;
502
593
  }
503
- console.log(
594
+ this.log(
504
595
  `No need to trim data for ${this.queryName}: size=${oldSize} (limit=${this.maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`
505
596
  );
506
597
  }
@@ -518,77 +609,109 @@ var DEFAULT_QUERY_STATE_ENTRY = {
518
609
  loading: true
519
610
  };
520
611
  function useSharedQuery(query, args = []) {
612
+ const stableArgs = useDeepMemo(args);
521
613
  const [queryState, setQueryState] = useSharedState(query.queryState);
522
- const isMounted = (0, import_react2.useRef)(true);
523
- const queryKey = query.getQueryKey(args);
524
- const execute = (0, import_react2.useCallback)(async () => {
525
- console.log(`Begin executing shared query ${queryKey}`);
526
- setQueryState((prev) => {
527
- const clone = { ...prev };
528
- clone[queryKey] = {
529
- ...clone[queryKey] ?? {},
614
+ const isMounted = (0, import_react3.useRef)(true);
615
+ const queryKey = (0, import_react3.useMemo)(
616
+ () => query.getQueryKey(stableArgs),
617
+ [query, stableArgs]
618
+ );
619
+ const queryStateValue = queryState[queryKey];
620
+ const setQueryStateValue = (0, import_react3.useCallback)(
621
+ (arg) => {
622
+ setQueryState((prev) => {
623
+ const clone = { ...prev };
624
+ clone[queryKey] = typeof arg === "function" ? arg(clone[queryKey]) : arg;
625
+ return clone;
626
+ });
627
+ },
628
+ [queryKey, setQueryState]
629
+ );
630
+ const execute = (0, import_react3.useCallback)(
631
+ async (forceRefresh) => {
632
+ query.log(`Begin executing shared query ${queryKey}`);
633
+ setQueryStateValue((prev) => ({
634
+ // Keep old data and error.
635
+ ...prev ?? {},
530
636
  lastUpdatedMs: Date.now(),
531
637
  loading: true
532
- };
533
- return clone;
534
- });
535
- try {
536
- const data = await query.getCachedOrFetch(...args);
537
- if (isMounted.current) {
538
- setQueryState((prev) => {
539
- const clone = { ...prev };
638
+ }));
639
+ try {
640
+ const data = await (forceRefresh ? query.updateFromSource(...stableArgs) : query.getCachedOrFetch(...stableArgs));
641
+ if (isMounted.current) {
540
642
  const now = Date.now();
541
- clone[queryKey] = {
643
+ setQueryStateValue({
542
644
  // Don't keep old error.
543
645
  lastUpdatedMs: now,
544
646
  loading: false,
545
647
  data,
546
648
  dataUpdatedMs: now
547
- };
548
- return clone;
549
- });
550
- }
551
- } catch (e) {
552
- const error = e instanceof Error ? e : new Error(`An unknown error occurred during fetch: ${e}`);
553
- if (isMounted.current) {
554
- setQueryState((prev) => {
555
- const clone = { ...prev };
649
+ });
650
+ }
651
+ } catch (e) {
652
+ if (isMounted.current) {
556
653
  const now = Date.now();
557
- clone[queryKey] = {
654
+ setQueryStateValue((prev) => ({
558
655
  // Keep old data.
559
- ...clone[queryKey] ?? {},
656
+ ...prev ?? {},
560
657
  lastUpdatedMs: now,
561
658
  loading: false,
562
- error,
659
+ error: e,
563
660
  errorUpdatedMs: now
564
- };
565
- return clone;
566
- });
661
+ }));
662
+ }
567
663
  }
568
- }
569
- }, [queryKey]);
570
- (0, import_react2.useEffect)(() => {
664
+ },
665
+ [query, queryKey, setQueryStateValue, stableArgs]
666
+ );
667
+ (0, import_react3.useEffect)(() => {
571
668
  isMounted.current = true;
572
- void execute();
669
+ void execute(false);
573
670
  return () => {
574
671
  isMounted.current = false;
575
672
  };
576
673
  }, [execute]);
577
- return (0, import_react2.useMemo)(() => {
578
- const state = queryState[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY;
674
+ return (0, import_react3.useMemo)(() => {
675
+ const state = queryStateValue ?? DEFAULT_QUERY_STATE_ENTRY;
579
676
  return {
580
677
  ...state,
678
+ // This works only if the user-given queryFn supports abort. If not,
679
+ // this function doesn't do anything, since we don't have any means to
680
+ // abort the running queryFn.
681
+ abortCurrentQuery: (reason) => {
682
+ const controller = query.getAbortControllerByKey(queryKey);
683
+ controller?.abort(reason);
684
+ return Boolean(controller);
685
+ },
581
686
  refetch: () => {
582
- return query.updateFromSource(...args);
687
+ void execute(true);
583
688
  },
584
689
  setData: (data, dataUpdatedMs) => {
585
- query.setData(queryKey, data, dataUpdatedMs ?? Date.now());
690
+ const now = Date.now();
691
+ setQueryStateValue((prev) => ({
692
+ ...prev ?? {},
693
+ data,
694
+ dataUpdatedMs: dataUpdatedMs ?? now,
695
+ lastUpdatedMs: now,
696
+ loading: false
697
+ }));
586
698
  },
587
699
  deleteData: () => {
588
- query.deleteData(args);
700
+ setQueryState((prev) => {
701
+ const clone = { ...prev };
702
+ delete clone[queryKey];
703
+ return clone;
704
+ });
589
705
  }
590
706
  };
591
- }, [query, queryKey, queryState]);
707
+ }, [
708
+ execute,
709
+ query,
710
+ queryKey,
711
+ queryStateValue,
712
+ setQueryState,
713
+ setQueryStateValue
714
+ ]);
592
715
  }
593
716
  // Annotate the CommonJS export names for ESM import in node:
594
717
  0 && (module.exports = {