@choksheak/react-utils 0.2.0 → 0.2.2

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 (50) hide show
  1. package/docs/assets/navigation.js +1 -1
  2. package/docs/assets/search.js +1 -1
  3. package/docs/classes/sharedQuery.SharedQuery.html +10 -8
  4. package/docs/classes/sharedState.SharedState.html +2 -2
  5. package/docs/classes/utils_storage.InMemoryStorageAdapter.html +2 -2
  6. package/docs/classes/utils_storage.IndexedDbAdapter.html +2 -2
  7. package/docs/classes/utils_storage.LocalStorageAdapter.html +2 -2
  8. package/docs/functions/sharedQuery.sharedQuery.html +2 -2
  9. package/docs/functions/sharedQuery.useSharedQuery.html +4 -0
  10. package/docs/functions/sharedState.sharedState.html +1 -1
  11. package/docs/functions/sharedState.useSharedState.html +1 -1
  12. package/docs/functions/sharedState.useSharedStateSetter.html +1 -1
  13. package/docs/functions/sharedState.useSharedStateValue.html +1 -1
  14. package/docs/functions/utils_storage.getStorageAdapter.html +1 -1
  15. package/docs/functions/utils_stringify.stringifyDeterministicForKeys.html +1 -1
  16. package/docs/modules/sharedQuery.html +1 -1
  17. package/docs/modules/sharedState.html +1 -1
  18. package/docs/types/sharedQuery.FetchFn.html +1 -1
  19. package/docs/types/sharedQuery.PersistTo.html +1 -0
  20. package/docs/types/sharedQuery.QueryStateValue.html +5 -3
  21. package/docs/types/sharedQuery.SharedQueryOptions.html +1 -1
  22. package/docs/types/sharedQuery.SharedQueryState.html +2 -2
  23. package/docs/types/sharedQuery.UseQueryResult.html +1 -0
  24. package/docs/types/sharedState.SharedStateOptions.html +1 -1
  25. package/docs/types/utils_storage.StorageAdapter.html +2 -2
  26. package/docs/types/utils_storage.StorageOptions.html +2 -2
  27. package/docs/variables/fetcher.fetcher.html +1 -1
  28. package/docs/variables/fetcher.fetcherWithRetries.html +1 -1
  29. package/docs/variables/sharedQuery.SharedQueryDefaults.html +2 -0
  30. package/docs/variables/sharedState.SharedStateDefaults.html +6 -0
  31. package/package.json +1 -1
  32. package/sharedQuery.d.mts +48 -33
  33. package/sharedQuery.d.ts +46 -32
  34. package/sharedQuery.js +121 -96
  35. package/sharedQuery.mjs +120 -96
  36. package/sharedState.d.mts +5 -5
  37. package/sharedState.d.ts +4 -4
  38. package/sharedState.js +10 -8
  39. package/sharedState.mjs +9 -7
  40. package/src/sharedQuery.ts +181 -131
  41. package/src/sharedState.ts +10 -7
  42. package/src/utils/stringify.test.ts +8 -0
  43. package/src/utils/stringify.ts +1 -1
  44. package/utils/stringify.js +1 -1
  45. package/utils/stringify.mjs +1 -1
  46. package/utils/stringify.test.js +6 -1
  47. package/utils/stringify.test.mjs +6 -1
  48. package/docs/classes/sharedState.SharedStateConfig.html +0 -9
  49. package/docs/functions/sharedQuery.useQuery.html +0 -4
  50. package/docs/types/sharedQuery.FetchedData.html +0 -3
package/sharedQuery.js CHANGED
@@ -21,15 +21,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var sharedQuery_exports = {};
22
22
  __export(sharedQuery_exports, {
23
23
  SharedQuery: () => SharedQuery,
24
+ SharedQueryDefaults: () => SharedQueryDefaults,
24
25
  sharedQuery: () => sharedQuery,
25
- useQuery: () => useQuery
26
+ useSharedQuery: () => useSharedQuery
26
27
  });
27
28
  module.exports = __toCommonJS(sharedQuery_exports);
28
- var import_kvStore3 = require("@choksheak/ts-utils/kvStore");
29
+ var import_timeConstants2 = require("@choksheak/ts-utils/timeConstants");
29
30
  var import_react2 = require("react");
30
31
 
31
32
  // src/sharedState.ts
32
- var import_kvStore2 = require("@choksheak/ts-utils/kvStore");
33
33
  var import_timeConstants = require("@choksheak/ts-utils/timeConstants");
34
34
  var import_react = require("react");
35
35
 
@@ -134,17 +134,17 @@ var IndexedDbAdapter = class {
134
134
  };
135
135
 
136
136
  // src/sharedState.ts
137
- var SharedStateConfig = class {
137
+ var SharedStateDefaults = {
138
138
  /**
139
139
  * Default duration for storage expiration in milliseconds. Initial value
140
140
  * is 30 days, but can be changed to anything.
141
141
  */
142
- static storeExpiryMs = import_timeConstants.MS_PER_DAY * 30;
142
+ storeExpiryMs: 30 * import_timeConstants.MS_PER_DAY,
143
143
  /**
144
144
  * True to load from storage only on mount, false (default) to load in
145
145
  * the top level scope when the shared state is defined.
146
146
  */
147
- static lazyLoad = false;
147
+ lazyLoad: false
148
148
  };
149
149
  var PubSubStore = class {
150
150
  dataByKey = /* @__PURE__ */ new Map();
@@ -199,11 +199,14 @@ var SharedState = class {
199
199
  constructor(defaultValue, options) {
200
200
  this.defaultValue = defaultValue;
201
201
  this.pubSubKey = String(stateKey++);
202
- this.storageAdapter = getStorageAdapter(options, import_kvStore2.DEFAULT_EXPIRY_DELTA_MS);
202
+ this.storageAdapter = getStorageAdapter(
203
+ options,
204
+ SharedStateDefaults.storeExpiryMs
205
+ );
203
206
  this.setValueBounded = (arg) => this.setValue(arg);
204
207
  pubSubStore.setNoNotify(this.pubSubKey, defaultValue);
205
208
  const lazyLoad = options?.lazyLoad;
206
- const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateConfig.lazyLoad;
209
+ const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateDefaults.lazyLoad;
207
210
  if (!lazy) {
208
211
  void this.initDefaultValueOnce();
209
212
  }
@@ -265,7 +268,7 @@ function stringifyDeterministicForKeys(params) {
265
268
  if (typeof value === "function") {
266
269
  throw new Error(`Functions should not be passed into query parameters`);
267
270
  }
268
- if (!value || typeof value !== "object") {
271
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
269
272
  return value;
270
273
  }
271
274
  if (cache.has(value)) {
@@ -290,14 +293,31 @@ function stringifyDeterministicForKeys(params) {
290
293
  }
291
294
 
292
295
  // src/sharedQuery.ts
293
- var DEFAULT_STALE_MS = 0;
296
+ var SharedQueryDefaults = {
297
+ staleMs: 0,
298
+ expiryMs: 30 * import_timeConstants2.MS_PER_DAY,
299
+ persistTo: void 0
300
+ };
294
301
  var SharedQuery = class {
295
- constructor(queryName, fetchFn, options) {
296
- this.queryName = queryName;
297
- this.fetchFn = fetchFn;
298
- this.expiryMs = options?.expiryMs !== void 0 ? options?.expiryMs : import_kvStore3.DEFAULT_EXPIRY_DELTA_MS;
299
- this.staleMs = options?.staleMs !== void 0 ? options?.staleMs : DEFAULT_STALE_MS;
300
- this.revalidateOnStale = Boolean(options?.revalidateOnStale);
302
+ inflightPromises = /* @__PURE__ */ new Map();
303
+ queryState;
304
+ queryName;
305
+ queryFn;
306
+ expiryMs;
307
+ staleMs;
308
+ refetchOnStale;
309
+ constructor(options) {
310
+ this.queryName = options.queryName;
311
+ this.queryFn = options.queryFn;
312
+ this.expiryMs = options?.expiryMs !== void 0 ? options?.expiryMs : SharedQueryDefaults.expiryMs;
313
+ this.staleMs = options?.staleMs !== void 0 ? options?.staleMs : SharedQueryDefaults.staleMs;
314
+ this.refetchOnStale = Boolean(options?.refetchOnStale);
315
+ const persistTo = options.persistTo ?? SharedQueryDefaults.persistTo;
316
+ if (persistTo === "localStorage") {
317
+ options.localStorageKey = options.queryName;
318
+ } else if (persistTo === "indexedDb") {
319
+ options.indexedDbKey = options.queryName;
320
+ }
301
321
  this.queryState = sharedState(
302
322
  {},
303
323
  {
@@ -307,109 +327,101 @@ var SharedQuery = class {
307
327
  }
308
328
  );
309
329
  }
310
- inflightPromises = /* @__PURE__ */ new Map();
311
- queryState;
312
- expiryMs;
313
- staleMs;
314
- revalidateOnStale;
315
330
  getQueryKey(args) {
316
331
  return this.queryName + ":" + stringifyDeterministicForKeys(args);
317
332
  }
318
- isStale(entry) {
333
+ isStale(dataUpdatedMs) {
319
334
  if (this.staleMs === 0) return true;
320
- const ageMs = Date.now() - entry.updatedMs;
335
+ const ageMs = Date.now() - dataUpdatedMs;
321
336
  return ageMs > this.staleMs;
322
337
  }
323
- isExpired(entry) {
338
+ isExpired(dataUpdatedMs) {
324
339
  if (this.expiryMs === 0) return false;
325
- const ageMs = Date.now() - entry.updatedMs;
340
+ const ageMs = Date.now() - dataUpdatedMs;
326
341
  return ageMs > this.expiryMs;
327
342
  }
328
- fetchNoCaching(queryKey, ...args) {
343
+ async getCachedOrFetch(...args) {
344
+ const queryKey = this.getQueryKey(args);
345
+ const cached = this.queryState.getSnapshot()?.[queryKey];
346
+ if (cached?.data && !this.isExpired(cached.dataUpdatedMs ?? 0)) {
347
+ if (!this.isStale(cached.dataUpdatedMs ?? 0)) {
348
+ console.log(
349
+ `Return fresh data ${queryKey} from cache without fetching`
350
+ );
351
+ return cached.data;
352
+ }
353
+ if (this.refetchOnStale) {
354
+ console.log(`refetchOnStale for ${queryKey}`);
355
+ void this.dedupedFetch(queryKey, ...args);
356
+ }
357
+ console.log(`Returning stale data for ${queryKey}`);
358
+ return cached.data;
359
+ }
360
+ return await this.dedupedFetch(queryKey, ...args);
361
+ }
362
+ dedupedFetch(queryKey, ...args) {
329
363
  const inflightPromise = this.inflightPromises.get(queryKey);
330
364
  if (inflightPromise) {
331
365
  console.log(`Deduplicating inflight fetch for ${queryKey}`);
332
366
  return inflightPromise;
333
367
  }
368
+ const promise = this.startFetching(queryKey, args);
369
+ this.inflightPromises.set(queryKey, promise);
370
+ return promise;
371
+ }
372
+ async startFetching(queryKey, args) {
334
373
  console.log(`Start fetching ${queryKey}`);
335
- const promise = this.fetchFn(...args).then((result) => {
374
+ try {
375
+ const data = await this.queryFn(...args);
336
376
  console.log(`Successfully fetched ${queryKey}`);
337
- this.setCache(queryKey, result);
338
- return result;
339
- }).catch((e) => {
377
+ this.setData(queryKey, data, Date.now());
378
+ return data;
379
+ } catch (e) {
340
380
  console.error(`Failed to fetch ${queryKey}: ${e}`);
341
381
  throw e;
342
- }).finally(() => {
382
+ } finally {
343
383
  this.inflightPromises.delete(queryKey);
344
- });
345
- this.inflightPromises.set(queryKey, promise);
346
- return promise;
347
- }
348
- async getCachedOrFetch(...args) {
349
- const queryKey = this.getQueryKey(args);
350
- const cached = this.queryState.getSnapshot()?.[queryKey];
351
- if (cached?.data && !this.isExpired(cached.data)) {
352
- if (!this.isStale(cached.data)) {
353
- console.log(
354
- `Return fresh data ${queryKey} from cache without fetching`
355
- );
356
- return cached.data.value;
357
- }
358
- if (this.revalidateOnStale) {
359
- console.log(`revalidateOnStale for ${queryKey}`);
360
- void this.fetchNoCaching(queryKey, ...args);
361
- }
362
- console.log(`Returning stale data for ${queryKey}`);
363
- return cached.data.value;
364
384
  }
365
- return await this.fetchNoCaching(queryKey, ...args);
366
385
  }
367
- async setCache(queryKey, value) {
386
+ /** Set the data directly if the user obtained it from somewhere else. */
387
+ async setData(queryKey, data, dataUpdatedMs = Date.now()) {
368
388
  const entry = {
369
- data: { value, updatedMs: Date.now() },
370
389
  loading: false,
371
- error: null
390
+ data,
391
+ dataUpdatedMs
372
392
  };
373
393
  this.queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));
374
394
  }
375
395
  /** Do a new fetch even when the data is already cached. */
376
396
  async updateFromSource(...args) {
377
397
  const key = this.getQueryKey(args);
378
- return await this.fetchNoCaching(key, ...args);
379
- }
380
- /** Delete the currently cached data. */
381
- invalidate(args) {
382
- if (args) {
383
- const queryKey = this.getQueryKey(args);
384
- const record = this.queryState.getSnapshot();
385
- const clone = { ...record };
386
- delete clone[queryKey];
387
- this.queryState.setValue(clone);
388
- this.inflightPromises.delete(queryKey);
389
- } else {
390
- this.clearAll();
391
- }
398
+ return await this.dedupedFetch(key, ...args);
399
+ }
400
+ /** Delete the cached data for one set of arguments. */
401
+ deleteData(args) {
402
+ const queryKey = this.getQueryKey(args);
403
+ const record = this.queryState.getSnapshot();
404
+ const clone = { ...record };
405
+ delete clone[queryKey];
406
+ this.queryState.setValue(clone);
407
+ this.inflightPromises.delete(queryKey);
392
408
  }
393
- /** Delete the currently cached data & all inflight promises. */
394
- clearAll() {
409
+ /** Delete all currently cached data & all inflight promises. */
410
+ clear() {
395
411
  this.queryState.delete();
396
412
  this.inflightPromises.clear();
397
413
  }
398
414
  };
399
415
  var seenQueryNames = /* @__PURE__ */ new Set();
400
- function sharedQuery(queryName, fetchFn, options) {
401
- if (seenQueryNames.has(queryName)) {
402
- throw new Error(`Duplicate shared query "${queryName}"`);
416
+ function sharedQuery(options) {
417
+ if (seenQueryNames.has(options.queryName)) {
418
+ throw new Error(`Duplicate shared query "${options.queryName}"`);
403
419
  }
404
- seenQueryNames.add(queryName);
405
- return new SharedQuery(queryName, fetchFn, options);
420
+ seenQueryNames.add(options.queryName);
421
+ return new SharedQuery(options);
406
422
  }
407
- var DEFAULT_QUERY_STATE_ENTRY = {
408
- data: null,
409
- loading: true,
410
- error: null
411
- };
412
- function useQuery(query, args) {
423
+ var DEFAULT_QUERY_STATE_ENTRY = { loading: true };
424
+ function useSharedQuery(query, args = []) {
413
425
  const [queryState, setQueryState] = useSharedState(query.queryState);
414
426
  const isMounted = (0, import_react2.useRef)(true);
415
427
  const queryKey = query.getQueryKey(args);
@@ -418,25 +430,21 @@ function useQuery(query, args) {
418
430
  setQueryState((prev) => {
419
431
  const clone = { ...prev };
420
432
  clone[queryKey] = {
421
- data: clone[queryKey]?.data ?? null,
422
- loading: true,
423
- error: null
433
+ ...clone[queryKey] ?? {},
434
+ loading: true
424
435
  };
425
436
  return clone;
426
437
  });
427
438
  try {
428
- const value = await query.getCachedOrFetch(...args);
439
+ const data = await query.getCachedOrFetch(...args);
429
440
  if (isMounted.current) {
430
441
  setQueryState((prev) => {
431
442
  const clone = { ...prev };
432
- const now = Date.now();
433
443
  clone[queryKey] = {
434
- data: {
435
- value,
436
- updatedMs: now
437
- },
444
+ // Don't keep old error.
438
445
  loading: false,
439
- error: null
446
+ data,
447
+ dataUpdatedMs: Date.now()
440
448
  };
441
449
  return clone;
442
450
  });
@@ -447,9 +455,11 @@ function useQuery(query, args) {
447
455
  setQueryState((prev) => {
448
456
  const clone = { ...prev };
449
457
  clone[queryKey] = {
450
- data: clone[queryKey]?.data ?? null,
458
+ // Keep old data.
459
+ ...clone[queryKey] ?? {},
451
460
  loading: false,
452
- error
461
+ error,
462
+ errorUpdatedMs: Date.now()
453
463
  };
454
464
  return clone;
455
465
  });
@@ -463,11 +473,26 @@ function useQuery(query, args) {
463
473
  isMounted.current = false;
464
474
  };
465
475
  }, [execute]);
466
- return queryState[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY;
476
+ return (0, import_react2.useMemo)(() => {
477
+ const state = queryState[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY;
478
+ return {
479
+ ...state,
480
+ refetch: () => {
481
+ return query.updateFromSource(...args);
482
+ },
483
+ setData: (data, dataUpdatedMs) => {
484
+ query.setData(queryKey, data, dataUpdatedMs ?? Date.now());
485
+ },
486
+ deleteData: () => {
487
+ query.deleteData(args);
488
+ }
489
+ };
490
+ }, [query, queryKey, queryState]);
467
491
  }
468
492
  // Annotate the CommonJS export names for ESM import in node:
469
493
  0 && (module.exports = {
470
494
  SharedQuery,
495
+ SharedQueryDefaults,
471
496
  sharedQuery,
472
- useQuery
497
+ useSharedQuery
473
498
  });
package/sharedQuery.mjs CHANGED
@@ -1,9 +1,8 @@
1
1
  // src/sharedQuery.ts
2
- import { DEFAULT_EXPIRY_DELTA_MS as DEFAULT_EXPIRY_DELTA_MS2 } from "@choksheak/ts-utils/kvStore";
3
- import { useCallback, useEffect, useRef } from "react";
2
+ import { MS_PER_DAY as MS_PER_DAY2 } from "@choksheak/ts-utils/timeConstants";
3
+ import { useCallback, useEffect, useMemo, useRef } from "react";
4
4
 
5
5
  // src/sharedState.ts
6
- import { DEFAULT_EXPIRY_DELTA_MS } from "@choksheak/ts-utils/kvStore";
7
6
  import { MS_PER_DAY } from "@choksheak/ts-utils/timeConstants";
8
7
  import { useSyncExternalStore } from "react";
9
8
 
@@ -108,17 +107,17 @@ var IndexedDbAdapter = class {
108
107
  };
109
108
 
110
109
  // src/sharedState.ts
111
- var SharedStateConfig = class {
110
+ var SharedStateDefaults = {
112
111
  /**
113
112
  * Default duration for storage expiration in milliseconds. Initial value
114
113
  * is 30 days, but can be changed to anything.
115
114
  */
116
- static storeExpiryMs = MS_PER_DAY * 30;
115
+ storeExpiryMs: 30 * MS_PER_DAY,
117
116
  /**
118
117
  * True to load from storage only on mount, false (default) to load in
119
118
  * the top level scope when the shared state is defined.
120
119
  */
121
- static lazyLoad = false;
120
+ lazyLoad: false
122
121
  };
123
122
  var PubSubStore = class {
124
123
  dataByKey = /* @__PURE__ */ new Map();
@@ -173,11 +172,14 @@ var SharedState = class {
173
172
  constructor(defaultValue, options) {
174
173
  this.defaultValue = defaultValue;
175
174
  this.pubSubKey = String(stateKey++);
176
- this.storageAdapter = getStorageAdapter(options, DEFAULT_EXPIRY_DELTA_MS);
175
+ this.storageAdapter = getStorageAdapter(
176
+ options,
177
+ SharedStateDefaults.storeExpiryMs
178
+ );
177
179
  this.setValueBounded = (arg) => this.setValue(arg);
178
180
  pubSubStore.setNoNotify(this.pubSubKey, defaultValue);
179
181
  const lazyLoad = options?.lazyLoad;
180
- const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateConfig.lazyLoad;
182
+ const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateDefaults.lazyLoad;
181
183
  if (!lazy) {
182
184
  void this.initDefaultValueOnce();
183
185
  }
@@ -239,7 +241,7 @@ function stringifyDeterministicForKeys(params) {
239
241
  if (typeof value === "function") {
240
242
  throw new Error(`Functions should not be passed into query parameters`);
241
243
  }
242
- if (!value || typeof value !== "object") {
244
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
243
245
  return value;
244
246
  }
245
247
  if (cache.has(value)) {
@@ -264,14 +266,31 @@ function stringifyDeterministicForKeys(params) {
264
266
  }
265
267
 
266
268
  // src/sharedQuery.ts
267
- var DEFAULT_STALE_MS = 0;
269
+ var SharedQueryDefaults = {
270
+ staleMs: 0,
271
+ expiryMs: 30 * MS_PER_DAY2,
272
+ persistTo: void 0
273
+ };
268
274
  var SharedQuery = class {
269
- constructor(queryName, fetchFn, options) {
270
- this.queryName = queryName;
271
- this.fetchFn = fetchFn;
272
- this.expiryMs = options?.expiryMs !== void 0 ? options?.expiryMs : DEFAULT_EXPIRY_DELTA_MS2;
273
- this.staleMs = options?.staleMs !== void 0 ? options?.staleMs : DEFAULT_STALE_MS;
274
- this.revalidateOnStale = Boolean(options?.revalidateOnStale);
275
+ inflightPromises = /* @__PURE__ */ new Map();
276
+ queryState;
277
+ queryName;
278
+ queryFn;
279
+ expiryMs;
280
+ staleMs;
281
+ refetchOnStale;
282
+ constructor(options) {
283
+ this.queryName = options.queryName;
284
+ this.queryFn = options.queryFn;
285
+ this.expiryMs = options?.expiryMs !== void 0 ? options?.expiryMs : SharedQueryDefaults.expiryMs;
286
+ this.staleMs = options?.staleMs !== void 0 ? options?.staleMs : SharedQueryDefaults.staleMs;
287
+ this.refetchOnStale = Boolean(options?.refetchOnStale);
288
+ const persistTo = options.persistTo ?? SharedQueryDefaults.persistTo;
289
+ if (persistTo === "localStorage") {
290
+ options.localStorageKey = options.queryName;
291
+ } else if (persistTo === "indexedDb") {
292
+ options.indexedDbKey = options.queryName;
293
+ }
275
294
  this.queryState = sharedState(
276
295
  {},
277
296
  {
@@ -281,109 +300,101 @@ var SharedQuery = class {
281
300
  }
282
301
  );
283
302
  }
284
- inflightPromises = /* @__PURE__ */ new Map();
285
- queryState;
286
- expiryMs;
287
- staleMs;
288
- revalidateOnStale;
289
303
  getQueryKey(args) {
290
304
  return this.queryName + ":" + stringifyDeterministicForKeys(args);
291
305
  }
292
- isStale(entry) {
306
+ isStale(dataUpdatedMs) {
293
307
  if (this.staleMs === 0) return true;
294
- const ageMs = Date.now() - entry.updatedMs;
308
+ const ageMs = Date.now() - dataUpdatedMs;
295
309
  return ageMs > this.staleMs;
296
310
  }
297
- isExpired(entry) {
311
+ isExpired(dataUpdatedMs) {
298
312
  if (this.expiryMs === 0) return false;
299
- const ageMs = Date.now() - entry.updatedMs;
313
+ const ageMs = Date.now() - dataUpdatedMs;
300
314
  return ageMs > this.expiryMs;
301
315
  }
302
- fetchNoCaching(queryKey, ...args) {
316
+ async getCachedOrFetch(...args) {
317
+ const queryKey = this.getQueryKey(args);
318
+ const cached = this.queryState.getSnapshot()?.[queryKey];
319
+ if (cached?.data && !this.isExpired(cached.dataUpdatedMs ?? 0)) {
320
+ if (!this.isStale(cached.dataUpdatedMs ?? 0)) {
321
+ console.log(
322
+ `Return fresh data ${queryKey} from cache without fetching`
323
+ );
324
+ return cached.data;
325
+ }
326
+ if (this.refetchOnStale) {
327
+ console.log(`refetchOnStale for ${queryKey}`);
328
+ void this.dedupedFetch(queryKey, ...args);
329
+ }
330
+ console.log(`Returning stale data for ${queryKey}`);
331
+ return cached.data;
332
+ }
333
+ return await this.dedupedFetch(queryKey, ...args);
334
+ }
335
+ dedupedFetch(queryKey, ...args) {
303
336
  const inflightPromise = this.inflightPromises.get(queryKey);
304
337
  if (inflightPromise) {
305
338
  console.log(`Deduplicating inflight fetch for ${queryKey}`);
306
339
  return inflightPromise;
307
340
  }
341
+ const promise = this.startFetching(queryKey, args);
342
+ this.inflightPromises.set(queryKey, promise);
343
+ return promise;
344
+ }
345
+ async startFetching(queryKey, args) {
308
346
  console.log(`Start fetching ${queryKey}`);
309
- const promise = this.fetchFn(...args).then((result) => {
347
+ try {
348
+ const data = await this.queryFn(...args);
310
349
  console.log(`Successfully fetched ${queryKey}`);
311
- this.setCache(queryKey, result);
312
- return result;
313
- }).catch((e) => {
350
+ this.setData(queryKey, data, Date.now());
351
+ return data;
352
+ } catch (e) {
314
353
  console.error(`Failed to fetch ${queryKey}: ${e}`);
315
354
  throw e;
316
- }).finally(() => {
355
+ } finally {
317
356
  this.inflightPromises.delete(queryKey);
318
- });
319
- this.inflightPromises.set(queryKey, promise);
320
- return promise;
321
- }
322
- async getCachedOrFetch(...args) {
323
- const queryKey = this.getQueryKey(args);
324
- const cached = this.queryState.getSnapshot()?.[queryKey];
325
- if (cached?.data && !this.isExpired(cached.data)) {
326
- if (!this.isStale(cached.data)) {
327
- console.log(
328
- `Return fresh data ${queryKey} from cache without fetching`
329
- );
330
- return cached.data.value;
331
- }
332
- if (this.revalidateOnStale) {
333
- console.log(`revalidateOnStale for ${queryKey}`);
334
- void this.fetchNoCaching(queryKey, ...args);
335
- }
336
- console.log(`Returning stale data for ${queryKey}`);
337
- return cached.data.value;
338
357
  }
339
- return await this.fetchNoCaching(queryKey, ...args);
340
358
  }
341
- async setCache(queryKey, value) {
359
+ /** Set the data directly if the user obtained it from somewhere else. */
360
+ async setData(queryKey, data, dataUpdatedMs = Date.now()) {
342
361
  const entry = {
343
- data: { value, updatedMs: Date.now() },
344
362
  loading: false,
345
- error: null
363
+ data,
364
+ dataUpdatedMs
346
365
  };
347
366
  this.queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));
348
367
  }
349
368
  /** Do a new fetch even when the data is already cached. */
350
369
  async updateFromSource(...args) {
351
370
  const key = this.getQueryKey(args);
352
- return await this.fetchNoCaching(key, ...args);
353
- }
354
- /** Delete the currently cached data. */
355
- invalidate(args) {
356
- if (args) {
357
- const queryKey = this.getQueryKey(args);
358
- const record = this.queryState.getSnapshot();
359
- const clone = { ...record };
360
- delete clone[queryKey];
361
- this.queryState.setValue(clone);
362
- this.inflightPromises.delete(queryKey);
363
- } else {
364
- this.clearAll();
365
- }
371
+ return await this.dedupedFetch(key, ...args);
372
+ }
373
+ /** Delete the cached data for one set of arguments. */
374
+ deleteData(args) {
375
+ const queryKey = this.getQueryKey(args);
376
+ const record = this.queryState.getSnapshot();
377
+ const clone = { ...record };
378
+ delete clone[queryKey];
379
+ this.queryState.setValue(clone);
380
+ this.inflightPromises.delete(queryKey);
366
381
  }
367
- /** Delete the currently cached data & all inflight promises. */
368
- clearAll() {
382
+ /** Delete all currently cached data & all inflight promises. */
383
+ clear() {
369
384
  this.queryState.delete();
370
385
  this.inflightPromises.clear();
371
386
  }
372
387
  };
373
388
  var seenQueryNames = /* @__PURE__ */ new Set();
374
- function sharedQuery(queryName, fetchFn, options) {
375
- if (seenQueryNames.has(queryName)) {
376
- throw new Error(`Duplicate shared query "${queryName}"`);
389
+ function sharedQuery(options) {
390
+ if (seenQueryNames.has(options.queryName)) {
391
+ throw new Error(`Duplicate shared query "${options.queryName}"`);
377
392
  }
378
- seenQueryNames.add(queryName);
379
- return new SharedQuery(queryName, fetchFn, options);
393
+ seenQueryNames.add(options.queryName);
394
+ return new SharedQuery(options);
380
395
  }
381
- var DEFAULT_QUERY_STATE_ENTRY = {
382
- data: null,
383
- loading: true,
384
- error: null
385
- };
386
- function useQuery(query, args) {
396
+ var DEFAULT_QUERY_STATE_ENTRY = { loading: true };
397
+ function useSharedQuery(query, args = []) {
387
398
  const [queryState, setQueryState] = useSharedState(query.queryState);
388
399
  const isMounted = useRef(true);
389
400
  const queryKey = query.getQueryKey(args);
@@ -392,25 +403,21 @@ function useQuery(query, args) {
392
403
  setQueryState((prev) => {
393
404
  const clone = { ...prev };
394
405
  clone[queryKey] = {
395
- data: clone[queryKey]?.data ?? null,
396
- loading: true,
397
- error: null
406
+ ...clone[queryKey] ?? {},
407
+ loading: true
398
408
  };
399
409
  return clone;
400
410
  });
401
411
  try {
402
- const value = await query.getCachedOrFetch(...args);
412
+ const data = await query.getCachedOrFetch(...args);
403
413
  if (isMounted.current) {
404
414
  setQueryState((prev) => {
405
415
  const clone = { ...prev };
406
- const now = Date.now();
407
416
  clone[queryKey] = {
408
- data: {
409
- value,
410
- updatedMs: now
411
- },
417
+ // Don't keep old error.
412
418
  loading: false,
413
- error: null
419
+ data,
420
+ dataUpdatedMs: Date.now()
414
421
  };
415
422
  return clone;
416
423
  });
@@ -421,9 +428,11 @@ function useQuery(query, args) {
421
428
  setQueryState((prev) => {
422
429
  const clone = { ...prev };
423
430
  clone[queryKey] = {
424
- data: clone[queryKey]?.data ?? null,
431
+ // Keep old data.
432
+ ...clone[queryKey] ?? {},
425
433
  loading: false,
426
- error
434
+ error,
435
+ errorUpdatedMs: Date.now()
427
436
  };
428
437
  return clone;
429
438
  });
@@ -437,10 +446,25 @@ function useQuery(query, args) {
437
446
  isMounted.current = false;
438
447
  };
439
448
  }, [execute]);
440
- return queryState[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY;
449
+ return useMemo(() => {
450
+ const state = queryState[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY;
451
+ return {
452
+ ...state,
453
+ refetch: () => {
454
+ return query.updateFromSource(...args);
455
+ },
456
+ setData: (data, dataUpdatedMs) => {
457
+ query.setData(queryKey, data, dataUpdatedMs ?? Date.now());
458
+ },
459
+ deleteData: () => {
460
+ query.deleteData(args);
461
+ }
462
+ };
463
+ }, [query, queryKey, queryState]);
441
464
  }
442
465
  export {
443
466
  SharedQuery,
467
+ SharedQueryDefaults,
444
468
  sharedQuery,
445
- useQuery
469
+ useSharedQuery
446
470
  };