@choksheak/react-utils 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +3 -1
  2. package/package.json +7 -2
  3. package/sharedQuery.d.mts +38 -7
  4. package/sharedQuery.d.ts +37 -6
  5. package/sharedQuery.js +149 -89
  6. package/sharedQuery.mjs +147 -87
  7. package/src/sharedQuery.ts +198 -117
  8. package/src/utils/useDeepMemo.ts +19 -0
  9. package/utils/useDeepMemo.d.mts +4 -0
  10. package/utils/useDeepMemo.d.ts +2 -0
  11. package/utils/useDeepMemo.js +51 -0
  12. package/utils/useDeepMemo.mjs +16 -0
  13. package/docs/.nojekyll +0 -1
  14. package/docs/assets/hierarchy.js +0 -1
  15. package/docs/assets/highlight.css +0 -71
  16. package/docs/assets/icons.js +0 -18
  17. package/docs/assets/icons.svg +0 -1
  18. package/docs/assets/main.js +0 -60
  19. package/docs/assets/navigation.js +0 -1
  20. package/docs/assets/search.js +0 -1
  21. package/docs/assets/style.css +0 -1633
  22. package/docs/classes/sharedQuery.SharedQuery.html +0 -22
  23. package/docs/classes/sharedState.SharedState.html +0 -9
  24. package/docs/classes/utils_storage.InMemoryStorageAdapter.html +0 -9
  25. package/docs/classes/utils_storage.IndexedDbAdapter.html +0 -9
  26. package/docs/classes/utils_storage.LocalStorageAdapter.html +0 -9
  27. package/docs/functions/sharedQuery.sharedQuery.html +0 -9
  28. package/docs/functions/sharedQuery.useSharedQuery.html +0 -6
  29. package/docs/functions/sharedState.sharedState.html +0 -3
  30. package/docs/functions/sharedState.useSharedState.html +0 -3
  31. package/docs/functions/sharedState.useSharedStateSetter.html +0 -2
  32. package/docs/functions/sharedState.useSharedStateValue.html +0 -2
  33. package/docs/functions/utils_storage.getStorageAdapter.html +0 -1
  34. package/docs/functions/utils_stringify.stringifyDeterministicForKeys.html +0 -7
  35. package/docs/hierarchy.html +0 -1
  36. package/docs/index.html +0 -2
  37. package/docs/modules/fetcher.html +0 -1
  38. package/docs/modules/index.html +0 -1
  39. package/docs/modules/sharedQuery.html +0 -1
  40. package/docs/modules/sharedState.html +0 -1
  41. package/docs/modules/utils_storage.html +0 -1
  42. package/docs/modules/utils_stringify.html +0 -1
  43. package/docs/modules.html +0 -1
  44. package/docs/types/sharedQuery.FetchFn.html +0 -1
  45. package/docs/types/sharedQuery.PersistTo.html +0 -1
  46. package/docs/types/sharedQuery.QueryStateValue.html +0 -9
  47. package/docs/types/sharedQuery.SharedQueryOptions.html +0 -1
  48. package/docs/types/sharedQuery.SharedQueryState.html +0 -3
  49. package/docs/types/sharedQuery.UseQueryResult.html +0 -1
  50. package/docs/types/sharedState.SharedStateOptions.html +0 -1
  51. package/docs/types/utils_storage.StorageAdapter.html +0 -5
  52. package/docs/types/utils_storage.StorageOptions.html +0 -5
  53. package/docs/variables/fetcher.fetcher.html +0 -2
  54. package/docs/variables/fetcher.fetcherWithRetries.html +0 -2
  55. package/docs/variables/sharedQuery.SharedQueryDefaults.html +0 -2
  56. package/docs/variables/sharedState.SharedStateDefaults.html +0 -6
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/
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.4",
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",
@@ -64,6 +64,7 @@
64
64
  "@eslint/compat": "^1.4.0",
65
65
  "@eslint/js": "^9.38.0",
66
66
  "@types/jest": "^30.0.0",
67
+ "@types/lodash": "^4.17.20",
67
68
  "@types/react": "^19.2.2",
68
69
  "@types/react-dom": "^19.2.2",
69
70
  "@typescript-eslint/eslint-plugin": "^8.46.2",
@@ -79,6 +80,7 @@
79
80
  "globals": "^16.4.0",
80
81
  "jiti": "^2.6.1",
81
82
  "lint-staged": "^16.2.6",
83
+ "lodash": "^4.17.21",
82
84
  "object-sizeof": "^2.6.5",
83
85
  "prettier": "^3.6.2",
84
86
  "react": "^19.2.0",
@@ -89,13 +91,16 @@
89
91
  "typedoc": "^0.28.14",
90
92
  "typescript": "^5.9.3",
91
93
  "typescript-eslint": "^8.46.2",
94
+ "use-deep-compare": "^1.3.0",
92
95
  "vitest": "^4.0.3",
93
96
  "wretch": "^3.0.2"
94
97
  },
95
98
  "peerDependencies": {
96
99
  "@choksheak/ts-utils": ">=0.2.9",
100
+ "lodash": ">=4.17.21",
97
101
  "object-sizeof": ">=2.6.5",
98
102
  "react": ">=16",
103
+ "use-deep-compare": ">=1.3.0",
99
104
  "wretch": ">=3"
100
105
  },
101
106
  "exports": {
package/sharedQuery.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { SharedStateOptions, SharedState } from './sharedState.mjs';
2
2
  import './utils/storage.mjs';
3
3
 
4
- type FetchFn<TArgs extends unknown[], TData> = (...args: TArgs) => Promise<TData> | TData;
4
+ type QueryFn<TArgs extends unknown[], TData> = (...args: TArgs) => Promise<TData> | TData;
5
5
  type QueryStateValue<TData> = {
6
6
  /**
7
7
  * When this record was last updated. Used for LRU determination. If you want
@@ -11,11 +11,27 @@ type QueryStateValue<TData> = {
11
11
  loading: boolean;
12
12
  data?: TData;
13
13
  dataUpdatedMs?: number;
14
- error?: Error;
14
+ /**
15
+ * Use unknown type for error because we can't assume what type of variable
16
+ * will be thrown from the query function. We also don't want to add this
17
+ * type to QueryStateValue<TError> as it adds complexity to the type.
18
+ */
19
+ error?: unknown;
15
20
  errorUpdatedMs?: number;
16
21
  };
17
22
  type UseQueryResult<TData> = QueryStateValue<TData> & {
18
- refetch: () => Promise<TData>;
23
+ /**
24
+ * Cancels the inflight query (if any). Returns true if canceled.
25
+ */
26
+ abortCurrentQuery: (reason?: unknown) => boolean;
27
+ /**
28
+ * Manually refresh the data by doing a new query.
29
+ */
30
+ refetch: () => void;
31
+ /**
32
+ * If you got the data from somewhere else (e.g. an in-memory data update),
33
+ * just set it directly.
34
+ */
19
35
  setData: (data: TData, dataUpdatedMs?: number) => void;
20
36
  deleteData: () => void;
21
37
  };
@@ -27,24 +43,27 @@ type PersistTo = "localStorage" | "indexedDb";
27
43
  type SharedQueryState<TData> = Record<string, QueryStateValue<TData>>;
28
44
  type SharedQueryOptions<TArgs extends unknown[], TData> = {
29
45
  queryName: string;
30
- queryFn: FetchFn<TArgs, TData>;
46
+ queryFn: QueryFn<TArgs, TData>;
31
47
  staleMs?: number;
32
48
  expiryMs?: number;
33
49
  refetchOnStale?: boolean;
34
50
  persistTo?: PersistTo;
35
51
  maxSize?: number;
36
52
  maxBytes?: number;
53
+ log?: (...args: unknown[]) => void;
37
54
  } & SharedStateOptions<SharedQueryState<TData>>;
38
55
  /** Users can override these values globally. */
39
56
  declare const SharedQueryDefaults: {
40
57
  staleMs: number;
41
58
  expiryMs: number;
59
+ refetchOnStale: boolean;
42
60
  persistTo: PersistTo | undefined;
43
61
  maxSize: number;
44
62
  maxBytes: number;
63
+ log: (...args: unknown[]) => void;
45
64
  };
46
65
  declare class SharedQuery<TArgs extends unknown[], TData> {
47
- private readonly inflightPromises;
66
+ private readonly inflightQueries;
48
67
  readonly queryState: SharedState<SharedQueryState<TData>>;
49
68
  readonly queryName: string;
50
69
  private readonly queryFn;
@@ -53,8 +72,20 @@ declare class SharedQuery<TArgs extends unknown[], TData> {
53
72
  readonly refetchOnStale: boolean;
54
73
  readonly maxSize: number;
55
74
  readonly maxBytes: number;
75
+ readonly log: (...args: unknown[]) => void;
56
76
  constructor(options: SharedQueryOptions<TArgs, TData>);
77
+ /** Returns a key to identify requests based on the given args. */
57
78
  getQueryKey(args: TArgs): string;
79
+ /**
80
+ * Get the AbortController to abort the inflight query (if any).
81
+ *
82
+ * Note that once you get the controller, you can also get the signal easily
83
+ * using `controller.signal`. So if you need both the controller and the
84
+ * signal, just use this function.
85
+ */
86
+ getAbortController(queryKey: string): AbortController | null;
87
+ /** Get the AbortSignal to check for query abortions. */
88
+ getAbortSignal(queryKey: string): AbortSignal | null;
58
89
  private isStale;
59
90
  private isExpired;
60
91
  getCachedOrFetch(...args: TArgs): Promise<TData>;
@@ -65,7 +96,7 @@ declare class SharedQuery<TArgs extends unknown[], TData> {
65
96
  /** Do a new fetch even when the data is already cached. */
66
97
  updateFromSource(...args: TArgs): Promise<TData>;
67
98
  /** Delete the cached data for one set of arguments. */
68
- deleteData(args: TArgs): void;
99
+ deleteData(queryKey: string): void;
69
100
  /** Delete all currently cached data & all inflight promises. */
70
101
  clear(): void;
71
102
  /**
@@ -102,4 +133,4 @@ declare function sharedQuery<TArgs extends unknown[], TData>(options: SharedQuer
102
133
  */
103
134
  declare function useSharedQuery<TArgs extends unknown[], TData>(query: SharedQuery<TArgs, TData>, args?: TArgs): UseQueryResult<TData>;
104
135
 
105
- export { type FetchFn, type PersistTo, type QueryStateValue, SharedQuery, SharedQueryDefaults, type SharedQueryOptions, type SharedQueryState, type UseQueryResult, sharedQuery, useSharedQuery };
136
+ export { type PersistTo, type QueryFn, type QueryStateValue, SharedQuery, SharedQueryDefaults, type SharedQueryOptions, type SharedQueryState, type UseQueryResult, sharedQuery, useSharedQuery };
package/sharedQuery.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { SharedState, SharedStateOptions } from "./sharedState";
2
- export type FetchFn<TArgs extends unknown[], TData> = (...args: TArgs) => Promise<TData> | TData;
2
+ export type QueryFn<TArgs extends unknown[], TData> = (...args: TArgs) => Promise<TData> | TData;
3
3
  export type QueryStateValue<TData> = {
4
4
  /**
5
5
  * When this record was last updated. Used for LRU determination. If you want
@@ -9,11 +9,27 @@ export type QueryStateValue<TData> = {
9
9
  loading: boolean;
10
10
  data?: TData;
11
11
  dataUpdatedMs?: number;
12
- error?: Error;
12
+ /**
13
+ * Use unknown type for error because we can't assume what type of variable
14
+ * will be thrown from the query function. We also don't want to add this
15
+ * type to QueryStateValue<TError> as it adds complexity to the type.
16
+ */
17
+ error?: unknown;
13
18
  errorUpdatedMs?: number;
14
19
  };
15
20
  export type UseQueryResult<TData> = QueryStateValue<TData> & {
16
- refetch: () => Promise<TData>;
21
+ /**
22
+ * Cancels the inflight query (if any). Returns true if canceled.
23
+ */
24
+ abortCurrentQuery: (reason?: unknown) => boolean;
25
+ /**
26
+ * Manually refresh the data by doing a new query.
27
+ */
28
+ refetch: () => void;
29
+ /**
30
+ * If you got the data from somewhere else (e.g. an in-memory data update),
31
+ * just set it directly.
32
+ */
17
33
  setData: (data: TData, dataUpdatedMs?: number) => void;
18
34
  deleteData: () => void;
19
35
  };
@@ -25,24 +41,27 @@ export type PersistTo = "localStorage" | "indexedDb";
25
41
  export type SharedQueryState<TData> = Record<string, QueryStateValue<TData>>;
26
42
  export type SharedQueryOptions<TArgs extends unknown[], TData> = {
27
43
  queryName: string;
28
- queryFn: FetchFn<TArgs, TData>;
44
+ queryFn: QueryFn<TArgs, TData>;
29
45
  staleMs?: number;
30
46
  expiryMs?: number;
31
47
  refetchOnStale?: boolean;
32
48
  persistTo?: PersistTo;
33
49
  maxSize?: number;
34
50
  maxBytes?: number;
51
+ log?: (...args: unknown[]) => void;
35
52
  } & SharedStateOptions<SharedQueryState<TData>>;
36
53
  /** Users can override these values globally. */
37
54
  export declare const SharedQueryDefaults: {
38
55
  staleMs: number;
39
56
  expiryMs: number;
57
+ refetchOnStale: boolean;
40
58
  persistTo: PersistTo | undefined;
41
59
  maxSize: number;
42
60
  maxBytes: number;
61
+ log: (...args: unknown[]) => void;
43
62
  };
44
63
  export declare class SharedQuery<TArgs extends unknown[], TData> {
45
- private readonly inflightPromises;
64
+ private readonly inflightQueries;
46
65
  readonly queryState: SharedState<SharedQueryState<TData>>;
47
66
  readonly queryName: string;
48
67
  private readonly queryFn;
@@ -51,8 +70,20 @@ export declare class SharedQuery<TArgs extends unknown[], TData> {
51
70
  readonly refetchOnStale: boolean;
52
71
  readonly maxSize: number;
53
72
  readonly maxBytes: number;
73
+ readonly log: (...args: unknown[]) => void;
54
74
  constructor(options: SharedQueryOptions<TArgs, TData>);
75
+ /** Returns a key to identify requests based on the given args. */
55
76
  getQueryKey(args: TArgs): string;
77
+ /**
78
+ * Get the AbortController to abort the inflight query (if any).
79
+ *
80
+ * Note that once you get the controller, you can also get the signal easily
81
+ * using `controller.signal`. So if you need both the controller and the
82
+ * signal, just use this function.
83
+ */
84
+ getAbortController(queryKey: string): AbortController | null;
85
+ /** Get the AbortSignal to check for query abortions. */
86
+ getAbortSignal(queryKey: string): AbortSignal | null;
56
87
  private isStale;
57
88
  private isExpired;
58
89
  getCachedOrFetch(...args: TArgs): Promise<TData>;
@@ -63,7 +94,7 @@ export declare class SharedQuery<TArgs extends unknown[], TData> {
63
94
  /** Do a new fetch even when the data is already cached. */
64
95
  updateFromSource(...args: TArgs): Promise<TData>;
65
96
  /** Delete the cached data for one set of arguments. */
66
- deleteData(args: TArgs): void;
97
+ deleteData(queryKey: string): void;
67
98
  /** Delete all currently cached data & all inflight promises. */
68
99
  clear(): void;
69
100
  /**
package/sharedQuery.js CHANGED
@@ -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,24 @@ 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(queryKey) {
387
+ return this.inflightQueries.get(queryKey)?.abortController ?? null;
388
+ }
389
+ /** Get the AbortSignal to check for query abortions. */
390
+ getAbortSignal(queryKey) {
391
+ return this.getAbortController(queryKey)?.signal ?? null;
392
+ }
352
393
  isStale(dataUpdatedMs) {
353
394
  if (this.staleMs === 0) return true;
354
395
  const ageMs = Date.now() - dataUpdatedMs;
@@ -364,42 +405,41 @@ var SharedQuery = class {
364
405
  const cached = this.queryState.getSnapshot()?.[queryKey];
365
406
  if (cached?.data && !this.isExpired(cached.dataUpdatedMs ?? 0)) {
366
407
  if (!this.isStale(cached.dataUpdatedMs ?? 0)) {
367
- console.log(
368
- `Return fresh data ${queryKey} from cache without fetching`
369
- );
408
+ this.log(`Return fresh data ${queryKey} from cache without fetching`);
370
409
  return cached.data;
371
410
  }
372
411
  if (this.refetchOnStale) {
373
- console.log(`refetchOnStale for ${queryKey}`);
412
+ this.log(`refetchOnStale for ${queryKey}`);
374
413
  void this.dedupedFetch(queryKey, ...args);
375
414
  }
376
- console.log(`Returning stale data for ${queryKey}`);
415
+ this.log(`Returning stale data for ${queryKey}`);
377
416
  return cached.data;
378
417
  }
379
418
  return await this.dedupedFetch(queryKey, ...args);
380
419
  }
381
420
  dedupedFetch(queryKey, ...args) {
382
- const inflightPromise = this.inflightPromises.get(queryKey);
383
- if (inflightPromise) {
384
- console.log(`Deduplicating inflight fetch for ${queryKey}`);
385
- return inflightPromise;
421
+ const inflightQuery = this.inflightQueries.get(queryKey);
422
+ if (inflightQuery) {
423
+ this.log(`Deduplicating inflight fetch for ${queryKey}`);
424
+ return inflightQuery.promise;
386
425
  }
387
426
  const promise = this.startFetching(queryKey, args);
388
- this.inflightPromises.set(queryKey, promise);
427
+ const abortController = new AbortController();
428
+ this.inflightQueries.set(queryKey, { promise, abortController });
389
429
  return promise;
390
430
  }
391
431
  async startFetching(queryKey, args) {
392
- console.log(`Start fetching ${queryKey}`);
432
+ this.log(`Start fetching ${queryKey}`);
393
433
  try {
394
434
  const data = await this.queryFn(...args);
395
- console.log(`Successfully fetched ${queryKey}`);
435
+ this.log(`Successfully fetched ${queryKey}`);
396
436
  this.setData(queryKey, data, Date.now());
397
437
  return data;
398
438
  } catch (e) {
399
- console.error(`Failed to fetch ${queryKey}: ${e}`);
439
+ this.log(`Failed to fetch ${queryKey}: ${e}`);
400
440
  throw e;
401
441
  } finally {
402
- this.inflightPromises.delete(queryKey);
442
+ this.inflightQueries.delete(queryKey);
403
443
  }
404
444
  }
405
445
  /** Set the data directly if the user obtained it from somewhere else. */
@@ -419,18 +459,26 @@ var SharedQuery = class {
419
459
  return await this.dedupedFetch(key, ...args);
420
460
  }
421
461
  /** Delete the cached data for one set of arguments. */
422
- deleteData(args) {
423
- const queryKey = this.getQueryKey(args);
462
+ deleteData(queryKey) {
424
463
  const record = this.queryState.getSnapshot();
425
464
  const clone = { ...record };
426
465
  delete clone[queryKey];
427
466
  this.queryState.setValue(clone);
428
- this.inflightPromises.delete(queryKey);
467
+ const inflight = this.inflightQueries.get(queryKey);
468
+ if (inflight) {
469
+ this.inflightQueries.delete(queryKey);
470
+ inflight.abortController.abort("Deleted");
471
+ }
429
472
  }
430
473
  /** Delete all currently cached data & all inflight promises. */
431
474
  clear() {
432
475
  this.queryState.delete();
433
- this.inflightPromises.clear();
476
+ for (const { abortController } of Array.from(
477
+ this.inflightQueries.values()
478
+ )) {
479
+ abortController.abort("Cleared");
480
+ }
481
+ this.inflightQueries.clear();
434
482
  }
435
483
  /**
436
484
  * Discard the oldest entries if the size exceeds the limit. The last
@@ -451,7 +499,7 @@ var SharedQuery = class {
451
499
  if (this.maxSize) {
452
500
  const numToCut = oldSize - this.maxSize;
453
501
  if (numToCut > 0) {
454
- console.log(
502
+ this.log(
455
503
  `enforceSizeLimit for ${this.queryName} needed due to size ${oldSize} > ${this.maxSize}`
456
504
  );
457
505
  newState = { ...newState };
@@ -470,7 +518,7 @@ var SharedQuery = class {
470
518
  const currentByteSize = needUpdate ? (0, import_object_sizeof.default)(newState) : oldByteSize;
471
519
  let bytesToCut = currentByteSize - this.maxBytes;
472
520
  if (bytesToCut > 0) {
473
- console.log(
521
+ this.log(
474
522
  `enforceSizeLimit for ${this.queryName} needed due to byte size ${currentByteSize.toLocaleString()} > ${this.maxBytes.toLocaleString()}`
475
523
  );
476
524
  if (!needUpdate) {
@@ -495,12 +543,12 @@ var SharedQuery = class {
495
543
  }
496
544
  if (needUpdate) {
497
545
  this.queryState.setValue(newState);
498
- console.log(
546
+ this.log(
499
547
  `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
548
  );
501
549
  return;
502
550
  }
503
- console.log(
551
+ this.log(
504
552
  `No need to trim data for ${this.queryName}: size=${oldSize} (limit=${this.maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`
505
553
  );
506
554
  }
@@ -518,77 +566,89 @@ var DEFAULT_QUERY_STATE_ENTRY = {
518
566
  loading: true
519
567
  };
520
568
  function useSharedQuery(query, args = []) {
569
+ const stableArgs = useDeepMemo(args);
521
570
  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] ?? {},
530
- lastUpdatedMs: Date.now(),
531
- 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 };
540
- const now = Date.now();
541
- clone[queryKey] = {
542
- // Don't keep old error.
543
- lastUpdatedMs: now,
544
- loading: false,
545
- data,
546
- 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 };
556
- const now = Date.now();
557
- clone[queryKey] = {
558
- // Keep old data.
559
- ...clone[queryKey] ?? {},
560
- lastUpdatedMs: now,
561
- loading: false,
562
- error,
563
- errorUpdatedMs: now
564
- };
565
- return clone;
566
- });
571
+ const isMounted = (0, import_react3.useRef)(true);
572
+ const queryKey = (0, import_react3.useMemo)(
573
+ () => query.getQueryKey(stableArgs),
574
+ [query, stableArgs]
575
+ );
576
+ const queryStateValue = queryState[queryKey];
577
+ const execute = (0, import_react3.useCallback)(
578
+ async (forceRefresh) => {
579
+ query.log(`Begin executing shared query ${queryKey}`);
580
+ setQueryState((prev) => {
581
+ const clone = { ...prev };
582
+ clone[queryKey] = {
583
+ ...clone[queryKey] ?? {},
584
+ lastUpdatedMs: Date.now(),
585
+ loading: true
586
+ };
587
+ return clone;
588
+ });
589
+ try {
590
+ const data = await (forceRefresh ? query.updateFromSource(...stableArgs) : query.getCachedOrFetch(...stableArgs));
591
+ if (isMounted.current) {
592
+ setQueryState((prev) => {
593
+ const clone = { ...prev };
594
+ const now = Date.now();
595
+ clone[queryKey] = {
596
+ // Don't keep old error.
597
+ lastUpdatedMs: now,
598
+ loading: false,
599
+ data,
600
+ dataUpdatedMs: now
601
+ };
602
+ return clone;
603
+ });
604
+ }
605
+ } catch (e) {
606
+ if (isMounted.current) {
607
+ setQueryState((prev) => {
608
+ const clone = { ...prev };
609
+ const now = Date.now();
610
+ clone[queryKey] = {
611
+ // Keep old data.
612
+ ...clone[queryKey] ?? {},
613
+ lastUpdatedMs: now,
614
+ loading: false,
615
+ error: e,
616
+ errorUpdatedMs: now
617
+ };
618
+ return clone;
619
+ });
620
+ }
567
621
  }
568
- }
569
- }, [queryKey]);
570
- (0, import_react2.useEffect)(() => {
622
+ },
623
+ [query, queryKey, setQueryState, stableArgs]
624
+ );
625
+ (0, import_react3.useEffect)(() => {
571
626
  isMounted.current = true;
572
- void execute();
627
+ void execute(false);
573
628
  return () => {
574
629
  isMounted.current = false;
575
630
  };
576
631
  }, [execute]);
577
- return (0, import_react2.useMemo)(() => {
578
- const state = queryState[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY;
632
+ return (0, import_react3.useMemo)(() => {
633
+ const state = queryStateValue ?? DEFAULT_QUERY_STATE_ENTRY;
579
634
  return {
580
635
  ...state,
636
+ abortCurrentQuery: (reason) => {
637
+ const controller = query.getAbortController(queryKey);
638
+ controller?.abort(reason);
639
+ return Boolean(controller);
640
+ },
581
641
  refetch: () => {
582
- return query.updateFromSource(...args);
642
+ void execute(true);
583
643
  },
584
644
  setData: (data, dataUpdatedMs) => {
585
645
  query.setData(queryKey, data, dataUpdatedMs ?? Date.now());
586
646
  },
587
647
  deleteData: () => {
588
- query.deleteData(args);
648
+ query.deleteData(queryKey);
589
649
  }
590
650
  };
591
- }, [query, queryKey, queryState]);
651
+ }, [execute, query, queryKey, queryStateValue]);
592
652
  }
593
653
  // Annotate the CommonJS export names for ESM import in node:
594
654
  0 && (module.exports = {