@choksheak/react-utils 0.2.7 → 0.2.9

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 (55) hide show
  1. package/package.json +50 -2
  2. package/sharedQuery.cjs +146 -191
  3. package/sharedQuery.d.mts +72 -12
  4. package/sharedQuery.d.ts +69 -10
  5. package/sharedQuery.min.cjs +1 -1
  6. package/sharedQuery.min.cjs.map +1 -1
  7. package/sharedQuery.min.mjs +1 -1
  8. package/sharedQuery.min.mjs.map +1 -1
  9. package/sharedQuery.mjs +151 -191
  10. package/sharedState.cjs +50 -103
  11. package/sharedState.d.mts +12 -7
  12. package/sharedState.d.ts +10 -6
  13. package/sharedState.min.cjs +1 -1
  14. package/sharedState.min.cjs.map +1 -1
  15. package/sharedState.min.mjs +1 -1
  16. package/sharedState.min.mjs.map +1 -1
  17. package/sharedState.mjs +48 -102
  18. package/storage.cjs +67 -0
  19. package/storage.d.mts +19 -0
  20. package/storage.d.ts +16 -0
  21. package/storage.min.cjs +2 -0
  22. package/storage.min.cjs.map +1 -0
  23. package/storage.min.mjs +2 -0
  24. package/storage.min.mjs.map +1 -0
  25. package/storage.mjs +42 -0
  26. package/{utils/stringify.cjs → stringify.cjs} +1 -1
  27. package/stringify.min.cjs.map +1 -0
  28. package/stringify.min.mjs.map +1 -0
  29. package/{utils/stringify.mjs → stringify.mjs} +1 -1
  30. package/{utils/useDeepMemo.cjs → useDeepMemo.cjs} +1 -1
  31. package/useDeepMemo.d.mts +7 -0
  32. package/useDeepMemo.d.ts +5 -0
  33. package/useDeepMemo.min.cjs.map +1 -0
  34. package/useDeepMemo.min.mjs.map +1 -0
  35. package/{utils/useDeepMemo.mjs → useDeepMemo.mjs} +1 -1
  36. package/utils/storage.cjs +0 -162
  37. package/utils/storage.d.mts +0 -46
  38. package/utils/storage.d.ts +0 -44
  39. package/utils/storage.min.cjs +0 -2
  40. package/utils/storage.min.cjs.map +0 -1
  41. package/utils/storage.min.mjs +0 -2
  42. package/utils/storage.min.mjs.map +0 -1
  43. package/utils/storage.mjs +0 -134
  44. package/utils/stringify.min.cjs.map +0 -1
  45. package/utils/stringify.min.mjs.map +0 -1
  46. package/utils/useDeepMemo.d.mts +0 -4
  47. package/utils/useDeepMemo.d.ts +0 -2
  48. package/utils/useDeepMemo.min.cjs.map +0 -1
  49. package/utils/useDeepMemo.min.mjs.map +0 -1
  50. /package/{utils/stringify.d.mts → stringify.d.mts} +0 -0
  51. /package/{utils/stringify.d.ts → stringify.d.ts} +0 -0
  52. /package/{utils/stringify.min.cjs → stringify.min.cjs} +0 -0
  53. /package/{utils/stringify.min.mjs → stringify.min.mjs} +0 -0
  54. /package/{utils/useDeepMemo.min.cjs → useDeepMemo.min.cjs} +0 -0
  55. /package/{utils/useDeepMemo.min.mjs → useDeepMemo.min.mjs} +0 -0
package/sharedQuery.mjs CHANGED
@@ -1,114 +1,60 @@
1
1
  // src/sharedQuery.ts
2
2
  import { MS_PER_DAY as MS_PER_DAY2 } from "@choksheak/ts-utils/timeConstants";
3
3
  import getByteSize from "object-sizeof";
4
- import { useCallback, useEffect, useMemo as useMemo2, useRef } from "react";
4
+ import {
5
+ useCallback,
6
+ useEffect,
7
+ useMemo as useMemo2,
8
+ useRef,
9
+ useState as useState2
10
+ } from "react";
5
11
 
6
12
  // src/sharedState.ts
7
13
  import { MS_PER_DAY } from "@choksheak/ts-utils/timeConstants";
8
14
  import { useSyncExternalStore } from "react";
9
15
 
10
- // src/utils/storage.ts
16
+ // src/storage.ts
11
17
  import { kvStore } from "@choksheak/ts-utils/kvStore";
18
+ import { LocalStore } from "@choksheak/ts-utils/localStore";
12
19
  function getStorageAdapter(options, defaultStoreExpiryMs) {
13
- const { localStorageKey, indexedDbKey, storeExpiryMs, store } = options ?? {};
14
- if (store) {
15
- return store;
16
- }
17
- const expiryMs = storeExpiryMs || defaultStoreExpiryMs;
18
- if (localStorageKey) {
19
- return new LocalStorageAdapter(localStorageKey, expiryMs);
20
- }
21
- if (indexedDbKey) {
22
- return new IndexedDbAdapter(indexedDbKey, expiryMs);
23
- }
24
- return null;
25
- }
26
- var LocalStorageAdapter = class {
27
- constructor(keyPrefix, expiryMs) {
28
- this.keyPrefix = keyPrefix;
29
- this.expiryMs = expiryMs;
30
- }
31
- save(key, value) {
32
- if (typeof window === "undefined") return;
33
- const fullKey = this.keyPrefix + ":" + key;
34
- window.localStorage.setItem(
35
- fullKey,
36
- JSON.stringify({
37
- value,
38
- expiresAt: this.expiryMs ? Date.now() + this.expiryMs : 0
39
- })
40
- );
41
- }
42
- load(key) {
43
- if (typeof window === "undefined") return void 0;
44
- const fullKey = this.keyPrefix + ":" + key;
45
- const raw = window.localStorage.getItem(fullKey);
46
- if (!raw) return void 0;
47
- try {
48
- const box = JSON.parse(raw);
49
- if (!box || box.expiresAt && Date.now() >= box.expiresAt) {
50
- window.localStorage.removeItem(fullKey);
51
- return void 0;
52
- }
53
- return box.value;
54
- } catch {
55
- window.localStorage.removeItem(fullKey);
56
- return void 0;
57
- }
58
- }
59
- delete(key) {
60
- if (typeof window === "undefined") return;
61
- const fullKey = this.keyPrefix + ":" + key;
62
- window.localStorage.removeItem(fullKey);
63
- }
64
- clear() {
65
- if (typeof window === "undefined") return;
66
- for (const key of Object.keys(window.localStorage)) {
67
- if (key.startsWith(this.keyPrefix)) {
68
- window.localStorage.removeItem(key);
20
+ const { store } = options ?? {};
21
+ if (!store) {
22
+ return null;
23
+ }
24
+ if ("persistTo" in store && typeof store.persistTo === "string" && "key" in store && typeof store.key === "string") {
25
+ const expiryMs = store.expiryMs || defaultStoreExpiryMs;
26
+ switch (store.persistTo) {
27
+ case "localStorage":
28
+ return new LocalStore(store.key, {
29
+ defaultExpiryMs: expiryMs
30
+ }).asStorageAdapter();
31
+ case "indexedDb": {
32
+ const keyPrefix = store.key + ":";
33
+ return {
34
+ set: async (key, value) => {
35
+ await kvStore.set(keyPrefix + key, value, expiryMs);
36
+ },
37
+ get: async (key) => {
38
+ return await kvStore.get(keyPrefix + key);
39
+ },
40
+ delete: async (key) => {
41
+ await kvStore.delete(keyPrefix + key);
42
+ },
43
+ clear: async () => {
44
+ await kvStore.clear();
45
+ }
46
+ };
69
47
  }
48
+ default:
49
+ store.persistTo;
50
+ throw new Error(`Unknown store.adapter ${store.persistTo}`);
70
51
  }
71
52
  }
72
- };
73
- var IndexedDbAdapter = class {
74
- constructor(keyPrefix, expiryMs) {
75
- this.keyPrefix = keyPrefix;
76
- this.expiryMs = expiryMs;
77
- }
78
- async save(key, value) {
79
- if (typeof window === "undefined") return;
80
- const fullKey = this.keyPrefix + ":" + key;
81
- await kvStore.set(fullKey, JSON.stringify(value), this.expiryMs);
82
- }
83
- async load(key) {
84
- if (typeof window === "undefined") return void 0;
85
- const fullKey = this.keyPrefix + ":" + key;
86
- const raw = await kvStore.get(fullKey);
87
- if (raw === void 0) return void 0;
88
- try {
89
- return JSON.parse(raw);
90
- } catch {
91
- await kvStore.delete(fullKey);
92
- return void 0;
93
- }
94
- }
95
- async delete(key) {
96
- if (typeof window === "undefined") return;
97
- const fullKey = this.keyPrefix + ":" + key;
98
- await kvStore.delete(fullKey);
99
- }
100
- async clear() {
101
- if (typeof window === "undefined") return;
102
- await kvStore.forEach(async (k) => {
103
- if (k.startsWith(this.keyPrefix)) {
104
- await kvStore.delete(k);
105
- }
106
- });
107
- }
108
- };
53
+ return store;
54
+ }
109
55
 
110
56
  // src/sharedState.ts
111
- var SharedStateDefaults = {
57
+ var SharedStateConfig = {
112
58
  /**
113
59
  * Default duration for storage expiration in milliseconds. Initial value
114
60
  * is 30 days, but can be changed to anything.
@@ -175,12 +121,14 @@ var SharedState = class {
175
121
  this.pubSubKey = String(stateKey++);
176
122
  this.storageAdapter = getStorageAdapter(
177
123
  options,
178
- SharedStateDefaults.storeExpiryMs
124
+ SharedStateConfig.storeExpiryMs
179
125
  );
180
- this.setValueBounded = (arg) => this.setValue(arg);
126
+ this.setValueBounded = (next) => {
127
+ this.setValue(next);
128
+ };
181
129
  pubSubStore.setNoNotify(this.pubSubKey, defaultValue);
182
130
  const lazyLoad = options?.lazyLoad;
183
- const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateDefaults.lazyLoad;
131
+ const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateConfig.lazyLoad;
184
132
  if (!lazy) {
185
133
  void this.initDefaultValueOnce();
186
134
  }
@@ -205,19 +153,19 @@ var SharedState = class {
205
153
  next = next(prev);
206
154
  }
207
155
  pubSubStore.set(this.pubSubKey, next);
208
- void this.storageAdapter?.save(STORAGE_KEY, next);
156
+ void this.storageAdapter?.set(STORAGE_KEY, next);
209
157
  }
210
158
  // Remove the value of the shared state. Actually this just sets the value
211
159
  // back to the given default value.
212
160
  delete() {
213
161
  pubSubStore.set(this.pubSubKey, this.defaultValue);
214
- void this.storageAdapter?.save(STORAGE_KEY, this.defaultValue);
162
+ void this.storageAdapter?.set(STORAGE_KEY, this.defaultValue);
215
163
  }
216
164
  // Initial default handling
217
165
  async initDefaultValueOnce() {
218
166
  if (this.initDone) return;
219
167
  this.initDone = true;
220
- const v = await this.storageAdapter?.load(STORAGE_KEY);
168
+ const v = await this.storageAdapter?.get(STORAGE_KEY);
221
169
  pubSubStore.set(this.pubSubKey, v !== void 0 ? v : this.defaultValue);
222
170
  }
223
171
  };
@@ -235,7 +183,7 @@ function useSharedState(state) {
235
183
  return [value, state.setValueBounded];
236
184
  }
237
185
 
238
- // src/utils/stringify.ts
186
+ // src/stringify.ts
239
187
  function stringifyDeterministicForKeys(params) {
240
188
  const cache = /* @__PURE__ */ new Set();
241
189
  return JSON.stringify(params, (_, value) => {
@@ -266,7 +214,7 @@ function stringifyDeterministicForKeys(params) {
266
214
  });
267
215
  }
268
216
 
269
- // src/utils/useDeepMemo.ts
217
+ // src/useDeepMemo.ts
270
218
  import isEqual from "lodash/isEqual";
271
219
  import { useMemo, useState } from "react";
272
220
  function useDeepMemo(value) {
@@ -281,22 +229,25 @@ function useDeepMemo(value) {
281
229
  }
282
230
 
283
231
  // src/sharedQuery.ts
284
- var SharedQueryDefaults = {
285
- // Refetch on every page load.
232
+ var SharedQueryConfig = {
233
+ /** Refetch on every page load. */
286
234
  staleMs: 0,
287
- // Keep in cache for a long time.
235
+ /** Keep in cache for a long time. */
288
236
  expiryMs: 30 * MS_PER_DAY2,
289
- // Always refetch automatically if the data is stale.
237
+ /** Always refetch automatically if the data is stale. */
290
238
  refetchOnStale: true,
291
- // No default persistence configured.
239
+ /** No default persistence configured. */
292
240
  persistTo: void 0,
293
- // 100 is not too large, but please tweak accordingly.
241
+ /** 100 is not too large, but please tweak accordingly. */
294
242
  maxSize: 100,
295
- // 100kb limit before discarding old records.
243
+ /** 100kb limit before discarding old records. */
296
244
  maxBytes: 1e5,
297
- // Default to log to console.
245
+ /** Default to log to console. */
298
246
  log: (...args) => console.log("[sharedQuery]", ...args)
299
247
  };
248
+ function configureSharedQuery(config) {
249
+ Object.assign(SharedQueryConfig, config);
250
+ }
300
251
  var SharedQuery = class {
301
252
  inflightQueries = /* @__PURE__ */ new Map();
302
253
  queryState;
@@ -313,32 +264,32 @@ var SharedQuery = class {
313
264
  refetchOnStale;
314
265
  maxSize;
315
266
  maxBytes;
267
+ keepLastNonEmptyData;
316
268
  log;
317
269
  constructor(options) {
318
270
  this.queryName = options.queryName;
319
271
  this.queryFn = options.queryFn;
320
- this.staleMs = options?.staleMs ?? SharedQueryDefaults.staleMs;
321
- this.expiryMs = options?.expiryMs ?? SharedQueryDefaults.expiryMs;
322
- this.refetchOnStale = options?.refetchOnStale ?? SharedQueryDefaults.refetchOnStale;
323
- if (!options.localStorageKey && !options.indexedDbKey) {
324
- const persistTo = options.persistTo ?? SharedQueryDefaults.persistTo;
325
- if (persistTo === "localStorage") {
326
- options.localStorageKey = options.queryName;
327
- } else if (persistTo === "indexedDb") {
328
- options.indexedDbKey = options.queryName;
272
+ this.staleMs = options?.staleMs ?? SharedQueryConfig.staleMs;
273
+ this.expiryMs = options?.expiryMs ?? SharedQueryConfig.expiryMs;
274
+ this.refetchOnStale = options?.refetchOnStale ?? SharedQueryConfig.refetchOnStale;
275
+ if (!options.store) {
276
+ const persistTo = options.persistTo ?? SharedQueryConfig.persistTo;
277
+ if (persistTo) {
278
+ options.store = {
279
+ persistTo,
280
+ key: options.queryName,
281
+ expiryMs: this.expiryMs
282
+ };
329
283
  }
330
284
  }
331
- this.maxSize = options?.maxSize ?? SharedQueryDefaults.maxSize;
332
- this.maxBytes = options?.maxBytes ?? SharedQueryDefaults.maxBytes;
333
- this.log = options?.log ?? SharedQueryDefaults.log;
285
+ this.maxSize = options?.maxSize ?? SharedQueryConfig.maxSize;
286
+ this.maxBytes = options?.maxBytes ?? SharedQueryConfig.maxBytes;
287
+ this.keepLastNonEmptyData = Boolean(options?.keepLastNonEmptyData);
288
+ this.log = options?.log ?? SharedQueryConfig.log;
334
289
  this.queryState = sharedState(
335
290
  {},
336
291
  // defaultValue
337
- {
338
- ...options,
339
- // Fallback to use expiryMs for the storeExpiryMs.
340
- storeExpiryMs: options?.storeExpiryMs || this.expiryMs
341
- }
292
+ options
342
293
  );
343
294
  }
344
295
  /** Returns a key to identify requests based on the given args. */
@@ -449,7 +400,14 @@ var SharedQuery = class {
449
400
  }
450
401
  /** Get the current stored data for a query key. */
451
402
  getData(queryKey) {
452
- return this.queryState.getSnapshot()?.[queryKey]?.data;
403
+ const stateValue = this.queryState.getSnapshot()?.[queryKey];
404
+ if (stateValue?.lastUpdatedMs) {
405
+ if (this.isExpired(stateValue.lastUpdatedMs)) {
406
+ this.deleteData(queryKey);
407
+ return void 0;
408
+ }
409
+ }
410
+ return stateValue?.data;
453
411
  }
454
412
  /** Set the data directly if the user obtained it from somewhere else. */
455
413
  setData(queryKey, data, dataUpdatedMs = Date.now()) {
@@ -519,70 +477,64 @@ var SharedQuery = class {
519
477
  return;
520
478
  }
521
479
  const oldByteSize = getByteSize(oldState);
522
- let newState = oldState;
480
+ let numToCut = this.maxSize ? oldSize - this.maxSize : 0;
481
+ let bytesToCut = this.maxBytes ? oldByteSize - this.maxBytes : 0;
482
+ if (numToCut <= 0 && bytesToCut <= 0) {
483
+ this.log(
484
+ `No need to trim data for ${this.queryName}: size=${oldSize} (limit=${this.maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`
485
+ );
486
+ return;
487
+ }
488
+ this.log(
489
+ `Need to trim ${this.queryName}: numToCut=${numToCut}, bytesToCut=${bytesToCut}`
490
+ );
491
+ const newState = { ...oldState };
523
492
  let needUpdate = false;
524
- if (this.maxSize) {
525
- let numToCut = oldSize - this.maxSize;
526
- if (numToCut > 0) {
527
- this.log(
528
- `enforceSizeLimit for ${this.queryName} needed due to size ${oldSize} > ${this.maxSize}`
529
- );
530
- newState = { ...newState };
531
- needUpdate = true;
532
- const entriesByTimeAscending = Object.entries(newState).sort(
533
- (entry1, entry2) => (
534
- // Sort descending.
535
- entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs
536
- )
537
- );
538
- for (let i = 0; i < entriesByTimeAscending.length && numToCut > 0; i++) {
539
- const key = entriesByTimeAscending[i][0];
540
- if (this.mountedKeys.has(key)) {
541
- this.log(`Cannot clean up ${key} as it is mounted`);
542
- } else {
543
- this.log(`Cleaning up unmounted ${key}`);
544
- delete newState[key];
545
- numToCut--;
546
- }
547
- }
493
+ const deleteKeyIfNotMounted = (key, expired) => {
494
+ if (this.mountedKeys.has(key)) {
495
+ this.log(`Cannot clean up ${key} as it is mounted`);
496
+ return;
497
+ }
498
+ const byteSize = getByteSize(key) + getByteSize(newState[key]);
499
+ this.log(
500
+ `Cleaning up unmounted${expired ? ", expired" : ""} ${key} (byteSize=${byteSize.toLocaleString()})`
501
+ );
502
+ numToCut--;
503
+ bytesToCut -= byteSize;
504
+ delete newState[key];
505
+ needUpdate = true;
506
+ };
507
+ let entriesByTimeAscending = Object.entries(newState).sort(
508
+ (entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs
509
+ );
510
+ for (const [key] of entriesByTimeAscending) {
511
+ if (!this.isExpired(newState[key].lastUpdatedMs)) {
512
+ break;
548
513
  }
514
+ deleteKeyIfNotMounted(key, true);
549
515
  }
550
- if (this.maxBytes && Object.keys(newState).length > 1) {
551
- const currentByteSize = needUpdate ? getByteSize(newState) : oldByteSize;
552
- let bytesToCut = currentByteSize - this.maxBytes;
553
- if (bytesToCut > 0) {
554
- this.log(
555
- `enforceSizeLimit for ${this.queryName} needed due to byte size ${currentByteSize.toLocaleString()} > ${this.maxBytes.toLocaleString()}`
556
- );
557
- if (!needUpdate) {
558
- newState = { ...newState };
559
- needUpdate = true;
560
- }
561
- const entriesByTimeAscending = Object.entries(newState).sort(
516
+ if (numToCut > 0 || bytesToCut > 0) {
517
+ if (needUpdate) {
518
+ entriesByTimeAscending = Object.entries(newState).sort(
562
519
  (entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs
563
520
  );
564
- for (let i = 0; i < entriesByTimeAscending.length && bytesToCut > 0; i++) {
565
- const key = entriesByTimeAscending[i][0];
566
- if (this.mountedKeys.has(key)) {
567
- this.log(`Cannot clean up ${key} as it is mounted`);
568
- } else {
569
- this.log(`Cleaning up unmounted ${key}`);
570
- const thisSize = getByteSize(key) + getByteSize(newState[key]);
571
- delete newState[key];
572
- bytesToCut -= thisSize;
573
- }
521
+ }
522
+ for (const [key] of entriesByTimeAscending) {
523
+ deleteKeyIfNotMounted(key, false);
524
+ if (numToCut <= 0 && bytesToCut <= 0) {
525
+ break;
574
526
  }
575
527
  }
576
528
  }
577
- if (needUpdate) {
578
- this.queryState.setValue(newState);
529
+ if (!needUpdate) {
579
530
  this.log(
580
- `Trimmed data for ${this.queryName}: size=(${oldSize} -> ${Object.keys(newState).length}) (limit=${this.maxSize}), byteSize=(${oldByteSize.toLocaleString()} -> ${getByteSize(newState).toLocaleString()}) (limit=${this.maxBytes.toLocaleString()})`
531
+ `No trimmable entries found for ${this.queryName}: size=${oldSize} (limit=${this.maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`
581
532
  );
582
533
  return;
583
534
  }
535
+ this.queryState.setValue(newState);
584
536
  this.log(
585
- `No need to trim data for ${this.queryName}: size=${oldSize} (limit=${this.maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`
537
+ `Trimmed data for ${this.queryName}: size=[${oldSize} -> ${Object.keys(newState).length}] (limit=${this.maxSize}), byteSize=[${oldByteSize.toLocaleString()} -> ${getByteSize(newState).toLocaleString()}] (limit=${this.maxBytes.toLocaleString()})`
586
538
  );
587
539
  }
588
540
  };
@@ -598,7 +550,7 @@ var DEFAULT_QUERY_STATE_ENTRY = {
598
550
  lastUpdatedMs: 0,
599
551
  loading: true
600
552
  };
601
- function useSharedQuery(query, args = []) {
553
+ function useSharedQuery(query, ...args) {
602
554
  const stableArgs = useDeepMemo(args);
603
555
  const [queryState, setQueryState] = useSharedState(query.queryState);
604
556
  const isMounted = useRef(true);
@@ -606,23 +558,29 @@ function useSharedQuery(query, args = []) {
606
558
  () => query.getQueryKey(stableArgs),
607
559
  [query, stableArgs]
608
560
  );
609
- const queryStateValue = queryState[queryKey];
561
+ const queryStateValue = queryState[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY;
610
562
  const setQueryStateValue = useCallback(
611
- (arg) => {
563
+ (next) => {
612
564
  setQueryState((prev) => {
613
565
  const clone = { ...prev };
614
- clone[queryKey] = typeof arg === "function" ? arg(clone[queryKey]) : arg;
566
+ clone[queryKey] = typeof next === "function" ? next(clone[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY) : next;
615
567
  return clone;
616
568
  });
617
569
  },
618
570
  [queryKey, setQueryState]
619
571
  );
572
+ const [lastNonEmptyData, setLastNonEmptyData] = useState2();
573
+ useEffect(() => {
574
+ if (query.keepLastNonEmptyData && queryStateValue.data !== void 0) {
575
+ setLastNonEmptyData(queryStateValue.data);
576
+ }
577
+ }, [query.keepLastNonEmptyData, queryStateValue.data]);
620
578
  const execute = useCallback(
621
579
  async (forceRefresh) => {
622
580
  query.log(`Begin executing shared query ${queryKey}`);
623
581
  setQueryStateValue((prev) => ({
624
582
  // Keep old data and error.
625
- ...prev ?? {},
583
+ ...prev,
626
584
  lastUpdatedMs: Date.now(),
627
585
  loading: true
628
586
  }));
@@ -643,7 +601,7 @@ function useSharedQuery(query, args = []) {
643
601
  const now = Date.now();
644
602
  setQueryStateValue((prev) => ({
645
603
  // Keep old data.
646
- ...prev ?? {},
604
+ ...prev,
647
605
  lastUpdatedMs: now,
648
606
  loading: false,
649
607
  error: e,
@@ -664,9 +622,9 @@ function useSharedQuery(query, args = []) {
664
622
  };
665
623
  }, [execute, query, queryKey]);
666
624
  return useMemo2(() => {
667
- const state = queryStateValue ?? DEFAULT_QUERY_STATE_ENTRY;
668
625
  return {
669
- ...state,
626
+ ...queryStateValue,
627
+ lastNonEmptyData,
670
628
  // This works only if the user-given queryFn supports abort. If not,
671
629
  // this function doesn't do anything, since we don't have any means to
672
630
  // abort the running queryFn.
@@ -681,7 +639,7 @@ function useSharedQuery(query, args = []) {
681
639
  setData: (data, dataUpdatedMs) => {
682
640
  const now = Date.now();
683
641
  setQueryStateValue((prev) => ({
684
- ...prev ?? {},
642
+ ...prev,
685
643
  data,
686
644
  dataUpdatedMs: dataUpdatedMs ?? now,
687
645
  lastUpdatedMs: now,
@@ -698,6 +656,7 @@ function useSharedQuery(query, args = []) {
698
656
  };
699
657
  }, [
700
658
  execute,
659
+ lastNonEmptyData,
701
660
  query,
702
661
  queryKey,
703
662
  queryStateValue,
@@ -707,7 +666,8 @@ function useSharedQuery(query, args = []) {
707
666
  }
708
667
  export {
709
668
  SharedQuery,
710
- SharedQueryDefaults,
669
+ SharedQueryConfig,
670
+ configureSharedQuery,
711
671
  sharedQuery,
712
672
  useSharedQuery
713
673
  };