@choksheak/react-utils 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/NoSSR.cjs +37 -0
  2. package/NoSSR.d.mts +24 -0
  3. package/NoSSR.d.ts +20 -0
  4. package/NoSSR.min.cjs +2 -0
  5. package/NoSSR.min.cjs.map +1 -0
  6. package/NoSSR.min.mjs +2 -0
  7. package/NoSSR.min.mjs.map +1 -0
  8. package/NoSSR.mjs +12 -0
  9. package/README.md +10 -1
  10. package/fetcher.cjs +3 -3
  11. package/fetcher.d.mts +5 -4
  12. package/fetcher.d.ts +4 -3
  13. package/fetcher.min.cjs +1 -1
  14. package/fetcher.min.cjs.map +1 -1
  15. package/fetcher.min.mjs +1 -1
  16. package/fetcher.min.mjs.map +1 -1
  17. package/fetcher.mjs +2 -2
  18. package/package.json +21 -3
  19. package/sharedQuery.cjs +461 -487
  20. package/sharedQuery.d.mts +64 -61
  21. package/sharedQuery.d.ts +62 -59
  22. package/sharedQuery.min.cjs +1 -1
  23. package/sharedQuery.min.cjs.map +1 -1
  24. package/sharedQuery.min.mjs +1 -1
  25. package/sharedQuery.min.mjs.map +1 -1
  26. package/sharedQuery.mjs +462 -487
  27. package/sharedState.cjs +200 -199
  28. package/sharedState.d.mts +48 -38
  29. package/sharedState.d.ts +45 -36
  30. package/sharedState.min.cjs +1 -1
  31. package/sharedState.min.cjs.map +1 -1
  32. package/sharedState.min.mjs +1 -1
  33. package/sharedState.min.mjs.map +1 -1
  34. package/sharedState.mjs +201 -199
  35. package/storage.cjs +78 -65
  36. package/storage.d.mts +39 -2
  37. package/storage.d.ts +37 -1
  38. package/storage.min.cjs +1 -1
  39. package/storage.min.cjs.map +1 -1
  40. package/storage.min.mjs +1 -1
  41. package/storage.min.mjs.map +1 -1
  42. package/storage.mjs +76 -65
  43. package/stringify.d.mts +9 -0
  44. package/stringify.d.ts +9 -0
  45. package/stringify.min.cjs.map +1 -1
  46. package/stringify.min.mjs.map +1 -1
  47. package/useDeepMemo.cjs +1 -1
  48. package/useDeepMemo.d.mts +11 -0
  49. package/useDeepMemo.d.ts +11 -0
  50. package/useDeepMemo.min.cjs +1 -1
  51. package/useDeepMemo.min.cjs.map +1 -1
  52. package/useDeepMemo.min.mjs +1 -1
  53. package/useDeepMemo.min.mjs.map +1 -1
  54. package/useDeepMemo.mjs +1 -1
package/sharedQuery.mjs CHANGED
@@ -15,7 +15,7 @@ import { useSyncExternalStore } from "react";
15
15
 
16
16
  // src/storage.ts
17
17
  import { kvStore } from "@choksheak/ts-utils/kvStore";
18
- import { LocalStore } from "@choksheak/ts-utils/localStore";
18
+ import { createLocalStore } from "@choksheak/ts-utils/localStore";
19
19
  var IS_NOT_BROWSER = typeof window === "undefined";
20
20
  function getStorageAdapter(options, defaultStoreExpiryMs) {
21
21
  const { store } = options ?? {};
@@ -24,70 +24,12 @@ function getStorageAdapter(options, defaultStoreExpiryMs) {
24
24
  }
25
25
  if ("persistTo" in store && typeof store.persistTo === "string" && "key" in store && typeof store.key === "string") {
26
26
  const expiryMs = store.expiryMs || defaultStoreExpiryMs;
27
- const isValid = store.isValid;
27
+ const config = { ...store, expiryMs };
28
28
  switch (store.persistTo) {
29
- case "localStorage": {
30
- const adapter = new LocalStore(store.key, {
31
- defaultExpiryMs: expiryMs
32
- });
33
- return {
34
- set: (key, value) => {
35
- if (IS_NOT_BROWSER) return;
36
- adapter.set(key, value);
37
- },
38
- get: (key) => {
39
- if (IS_NOT_BROWSER) return void 0;
40
- const value = adapter.get(key);
41
- if (isValid && !isValid(value)) {
42
- console.warn(
43
- `localStorage adapter: Auto-discard invalid value for ${key}: ${JSON.stringify(value)}`
44
- );
45
- adapter.delete(key);
46
- return void 0;
47
- }
48
- return value;
49
- },
50
- delete: (key) => {
51
- if (IS_NOT_BROWSER) return;
52
- adapter.delete(key);
53
- },
54
- clear: () => {
55
- if (IS_NOT_BROWSER) return;
56
- adapter.clear();
57
- }
58
- };
59
- }
60
- case "indexedDb": {
61
- const keyPrefix = store.key + ":";
62
- const isValid2 = store.isValid;
63
- return {
64
- set: async (key, value) => {
65
- if (IS_NOT_BROWSER) return;
66
- await kvStore.set(keyPrefix + key, value, expiryMs);
67
- },
68
- get: async (key) => {
69
- if (IS_NOT_BROWSER) return void 0;
70
- const fullKey = keyPrefix + key;
71
- const value = await kvStore.get(fullKey);
72
- if (isValid2 && !isValid2(value)) {
73
- console.warn(
74
- `indexedDb adapter: Auto-discard invalid value for ${key}: ${JSON.stringify(value)}`
75
- );
76
- await kvStore.delete(fullKey);
77
- return void 0;
78
- }
79
- return value;
80
- },
81
- delete: async (key) => {
82
- if (IS_NOT_BROWSER) return;
83
- await kvStore.delete(keyPrefix + key);
84
- },
85
- clear: async () => {
86
- if (IS_NOT_BROWSER) return;
87
- await kvStore.clear();
88
- }
89
- };
90
- }
29
+ case "localStorage":
30
+ return localStoreAdapter(config);
31
+ case "indexedDb":
32
+ return kvStoreAdapter(config);
91
33
  default:
92
34
  store.persistTo;
93
35
  throw new Error(`Unknown store.adapter ${store.persistTo}`);
@@ -95,6 +37,73 @@ function getStorageAdapter(options, defaultStoreExpiryMs) {
95
37
  }
96
38
  return store;
97
39
  }
40
+ function localStoreAdapter({
41
+ key,
42
+ expiryMs,
43
+ isValid
44
+ }) {
45
+ const adapter = createLocalStore(key, { defaultExpiryMs: expiryMs });
46
+ return {
47
+ set: (key2, value) => {
48
+ if (IS_NOT_BROWSER) return;
49
+ adapter.set(key2, value);
50
+ },
51
+ get: (key2) => {
52
+ if (IS_NOT_BROWSER) return void 0;
53
+ const value = adapter.get(key2);
54
+ if (isValid && !isValid(value)) {
55
+ console.warn(
56
+ `localStorage adapter: Auto-discard invalid value for ${key2}: ${JSON.stringify(value)}`
57
+ );
58
+ adapter.delete(key2);
59
+ return void 0;
60
+ }
61
+ return value;
62
+ },
63
+ delete: (key2) => {
64
+ if (IS_NOT_BROWSER) return;
65
+ adapter.delete(key2);
66
+ },
67
+ clear: () => {
68
+ if (IS_NOT_BROWSER) return;
69
+ adapter.clear();
70
+ }
71
+ };
72
+ }
73
+ function kvStoreAdapter({
74
+ key,
75
+ expiryMs,
76
+ isValid
77
+ }) {
78
+ const keyPrefix = key + ":";
79
+ return {
80
+ set: async (key2, value) => {
81
+ if (IS_NOT_BROWSER) return;
82
+ await kvStore.set(keyPrefix + key2, value, expiryMs);
83
+ },
84
+ get: async (key2) => {
85
+ if (IS_NOT_BROWSER) return void 0;
86
+ const fullKey = keyPrefix + key2;
87
+ const value = await kvStore.get(fullKey);
88
+ if (isValid && !isValid(value)) {
89
+ console.warn(
90
+ `indexedDb adapter: Auto-discard invalid value for ${key2}: ${JSON.stringify(value)}`
91
+ );
92
+ await kvStore.delete(fullKey);
93
+ return void 0;
94
+ }
95
+ return value;
96
+ },
97
+ delete: async (key2) => {
98
+ if (IS_NOT_BROWSER) return;
99
+ await kvStore.delete(keyPrefix + key2);
100
+ },
101
+ clear: async () => {
102
+ if (IS_NOT_BROWSER) return;
103
+ await kvStore.clear();
104
+ }
105
+ };
106
+ }
98
107
 
99
108
  // src/sharedState.ts
100
109
  var SharedStateConfig = {
@@ -109,41 +118,11 @@ var SharedStateConfig = {
109
118
  */
110
119
  lazyLoad: false
111
120
  };
112
- var PubSubStore = class {
113
- dataByKey = /* @__PURE__ */ new Map();
114
- subscribersByKey = /* @__PURE__ */ new Map();
115
- // This could be undefined if T includes undefined, else it should never
116
- // return undefined because the user has to always specify the default value.
117
- get(key) {
118
- return this.dataByKey.get(key);
119
- }
120
- set(key, value) {
121
- const prev = this.dataByKey.get(key);
122
- if (prev === value) return;
123
- this.dataByKey.set(key, value);
124
- this.notify(key, value, prev);
125
- }
126
- delete(key) {
127
- this.dataByKey.delete(key);
128
- }
129
- setNoNotify(key, value) {
130
- this.dataByKey.set(key, value);
131
- }
132
- // Subscribe to changes of a specific key
133
- subscribe(key, subscriber) {
134
- let subscribers = this.subscribersByKey.get(key);
135
- if (!subscribers) {
136
- subscribers = /* @__PURE__ */ new Set();
137
- this.subscribersByKey.set(key, subscribers);
138
- }
139
- subscribers.add(subscriber);
140
- return () => {
141
- subscribers.delete(subscriber);
142
- if (subscribers.size === 0) this.subscribersByKey.delete(key);
143
- };
144
- }
145
- notify(key, next, prev) {
146
- const subscribers = this.subscribersByKey.get(key);
121
+ function createPubSubStore() {
122
+ const dataByKey = /* @__PURE__ */ new Map();
123
+ const subscribersByKey = /* @__PURE__ */ new Map();
124
+ function notify(key, next, prev) {
125
+ const subscribers = subscribersByKey.get(key);
147
126
  if (!subscribers) return;
148
127
  const copy = Array.from(subscribers.values());
149
128
  for (const subscriber of copy) {
@@ -154,116 +133,139 @@ var PubSubStore = class {
154
133
  }
155
134
  }
156
135
  }
157
- };
158
- var pubSubStore = new PubSubStore();
136
+ return {
137
+ // This could be undefined if T includes undefined, else it should never
138
+ // return undefined because the user has to always specify the default value.
139
+ get(key) {
140
+ return dataByKey.get(key);
141
+ },
142
+ set(key, value) {
143
+ const prev = dataByKey.get(key);
144
+ if (prev === value) return;
145
+ dataByKey.set(key, value);
146
+ notify(key, value, prev);
147
+ },
148
+ delete(key) {
149
+ dataByKey.delete(key);
150
+ },
151
+ setNoNotify(key, value) {
152
+ dataByKey.set(key, value);
153
+ },
154
+ // Subscribe to changes of a specific key
155
+ subscribe(key, subscriber) {
156
+ let subscribers = subscribersByKey.get(key);
157
+ if (!subscribers) {
158
+ subscribers = /* @__PURE__ */ new Set();
159
+ subscribersByKey.set(key, subscribers);
160
+ }
161
+ subscribers.add(subscriber);
162
+ return () => {
163
+ subscribers.delete(subscriber);
164
+ if (subscribers.size === 0) subscribersByKey.delete(key);
165
+ };
166
+ }
167
+ };
168
+ }
169
+ var pubSubStore = createPubSubStore();
159
170
  var STORAGE_KEY = "state";
160
171
  var stateKey = 0;
161
- var SharedState = class {
162
- constructor(defaultValue, options) {
163
- this.defaultValue = defaultValue;
164
- let readyResolve = () => {
165
- };
166
- this.readyPromise = new Promise((resolve) => {
167
- readyResolve = resolve;
168
- });
169
- this.resolveReadyPromise = readyResolve;
170
- this.pubSubKey = String(stateKey++);
171
- this.storageAdapter = getStorageAdapter(
172
- options,
173
- SharedStateConfig.storeExpiryMs
174
- );
175
- this.setValueBounded = (next) => {
176
- this.setValue(next);
177
- };
178
- pubSubStore.setNoNotify(this.pubSubKey, defaultValue);
179
- const lazyLoad = options?.lazyLoad;
180
- const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateConfig.lazyLoad;
181
- if (!lazy) {
182
- void this.initDefaultValueOnce();
183
- }
184
- }
185
- pubSubKey;
186
- storageAdapter;
187
- setValueBounded;
188
- initStarted = false;
189
- initDone = false;
190
- // Allow users to await for the completion of data init.
191
- readyPromise;
192
- resolveReadyPromise;
193
- /** `subscribe` function required by useSyncExternalStore. */
194
- subscribe(subscriber) {
195
- return pubSubStore.subscribe(this.pubSubKey, () => subscriber());
196
- }
197
- /** Returns true if there is a backing store. */
198
- hasStore() {
199
- return Boolean(this.storageAdapter);
200
- }
201
- /**
202
- * Set a value directly into the store. Does nothing if there is no
203
- * backing store.
204
- *
205
- * Warning: If you want to set the value properly, you should use
206
- * `setValue()` instead which also updates the in-memory copy.
207
- */
208
- async setInStore(value) {
209
- return await this.storageAdapter?.set(STORAGE_KEY, value);
210
- }
211
- /**
212
- * Get a value directly from the store. Returns undefined if there is no
213
- * backing store.
214
- */
215
- async getFromStore() {
216
- return await this.storageAdapter?.get(STORAGE_KEY);
217
- }
218
- /**
219
- * Get the value of the shared state.
220
- * `getSnapshot` function — must return same ref if value unchanged.
221
- * Note that this would return the default value if data init from store is
222
- * not completed yet.
223
- */
224
- getSnapshot() {
225
- return pubSubStore.get(this.pubSubKey);
226
- }
227
- /** Set the value of the shared state. */
228
- setValue(next) {
229
- if (typeof next === "function") {
230
- const prev = pubSubStore.get(this.pubSubKey);
231
- next = next(prev);
172
+ function sharedState(defaultValue, options) {
173
+ let readyResolve = () => {
174
+ };
175
+ const readyPromise = new Promise((resolve) => {
176
+ readyResolve = resolve;
177
+ });
178
+ const resolveReadyPromise = readyResolve;
179
+ const pubSubKey = String(stateKey++);
180
+ const storageAdapter = getStorageAdapter(
181
+ options,
182
+ SharedStateConfig.storeExpiryMs
183
+ );
184
+ pubSubStore.setNoNotify(pubSubKey, defaultValue);
185
+ const lazyLoad = options?.lazyLoad;
186
+ const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateConfig.lazyLoad;
187
+ const obj = {
188
+ /** Allow users to await for the completion of data init. */
189
+ readyPromise,
190
+ lazy,
191
+ initStarted: false,
192
+ initDone: false,
193
+ /** `subscribe` function required by useSyncExternalStore. */
194
+ subscribe(subscriber) {
195
+ return pubSubStore.subscribe(pubSubKey, () => subscriber());
196
+ },
197
+ /** Returns true if there is a backing store. */
198
+ hasStore() {
199
+ return Boolean(storageAdapter);
200
+ },
201
+ /**
202
+ * Set a value directly into the store. Does nothing if there is no
203
+ * backing store.
204
+ *
205
+ * Warning: If you want to set the value properly, you should use
206
+ * `setValue()` instead which also updates the in-memory copy.
207
+ */
208
+ async setInStore(value) {
209
+ return await storageAdapter?.set(STORAGE_KEY, value);
210
+ },
211
+ /**
212
+ * Get a value directly from the store. Returns undefined if there is no
213
+ * backing store.
214
+ */
215
+ async getFromStore() {
216
+ return await storageAdapter?.get(STORAGE_KEY);
217
+ },
218
+ /**
219
+ * Get the value of the shared state.
220
+ * `getSnapshot` function — must return same ref if value unchanged.
221
+ * Note that this would return the default value if data init from store is
222
+ * not completed yet.
223
+ */
224
+ getSnapshot() {
225
+ return pubSubStore.get(pubSubKey);
226
+ },
227
+ /** Set the value of the shared state. */
228
+ setValue(next) {
229
+ if (typeof next === "function") {
230
+ const prev = pubSubStore.get(pubSubKey);
231
+ next = next(prev);
232
+ }
233
+ pubSubStore.set(pubSubKey, next);
234
+ void obj.setInStore(next);
235
+ },
236
+ /**
237
+ * Remove the value of the shared state. Actually this just sets the value
238
+ * back to the given default value.
239
+ */
240
+ delete() {
241
+ pubSubStore.set(pubSubKey, defaultValue);
242
+ void storageAdapter?.set(STORAGE_KEY, defaultValue);
243
+ },
244
+ /** Initial default handling. */
245
+ async initDefaultValueOnce() {
246
+ if (obj.initDone || obj.initStarted) return;
247
+ obj.initStarted = true;
248
+ const stored = await obj.getFromStore();
249
+ const value = stored !== void 0 ? stored : defaultValue;
250
+ pubSubStore.set(pubSubKey, value);
251
+ obj.initDone = true;
252
+ resolveReadyPromise();
232
253
  }
233
- pubSubStore.set(this.pubSubKey, next);
234
- void this.setInStore(next);
235
- }
236
- /**
237
- * Remove the value of the shared state. Actually this just sets the value
238
- * back to the given default value.
239
- */
240
- delete() {
241
- pubSubStore.set(this.pubSubKey, this.defaultValue);
242
- void this.storageAdapter?.set(STORAGE_KEY, this.defaultValue);
243
- }
244
- /** Initial default handling. */
245
- async initDefaultValueOnce() {
246
- if (this.initDone || this.initStarted) return;
247
- this.initStarted = true;
248
- const stored = await this.getFromStore();
249
- const value = stored !== void 0 ? stored : this.defaultValue;
250
- pubSubStore.set(this.pubSubKey, value);
251
- this.initDone = true;
252
- this.resolveReadyPromise();
254
+ };
255
+ if (!lazy) {
256
+ void obj.initDefaultValueOnce();
253
257
  }
254
- };
255
- function sharedState(defaultValue, options) {
256
- return new SharedState(defaultValue, options);
258
+ return obj;
257
259
  }
258
260
  function useSharedState(state) {
259
- state.initDefaultValueOnce();
261
+ void state.initDefaultValueOnce();
260
262
  const value = useSyncExternalStore(
261
263
  (onStoreChange) => state.subscribe(onStoreChange),
262
264
  () => state.getSnapshot(),
263
265
  // getServerSnapshot is needed to prevent SSR dev errors in nextjs.
264
266
  () => state.getSnapshot()
265
267
  );
266
- return [value, state.setValueBounded];
268
+ return [value, state.setValue];
267
269
  }
268
270
 
269
271
  // src/stringify.ts
@@ -303,7 +305,7 @@ import { useMemo, useState } from "react";
303
305
  function useDeepMemo(value) {
304
306
  const [state, setState] = useState(value);
305
307
  return useMemo(() => {
306
- if (state === value || isEqual(state, value)) {
308
+ if (isEqual(state, value)) {
307
309
  return state;
308
310
  }
309
311
  setState(value);
@@ -331,282 +333,79 @@ var SharedQueryConfig = {
331
333
  function configureSharedQuery(config) {
332
334
  Object.assign(SharedQueryConfig, config);
333
335
  }
334
- var SharedQuery = class {
335
- inflightQueries = /* @__PURE__ */ new Map();
336
- queryState;
337
- /**
338
- * Keep track of the mounted query keys. This handles the case where an
339
- * earlier-started query returns later, and when it returns, it was already
340
- * unmounted, thus making it eligible for clean up right after fetch.
341
- */
342
- mountedKeys = /* @__PURE__ */ new Map();
343
- queryName;
344
- queryFn;
345
- refetchFn;
346
- expiryMs;
347
- staleMs;
348
- refetchOnStale;
349
- maxSize;
350
- maxBytes;
351
- keepLastNonEmptyData;
352
- log;
353
- constructor(options) {
354
- this.queryName = options.queryName;
355
- this.queryFn = options.queryFn;
356
- this.refetchFn = options.refetchFn;
357
- this.staleMs = options?.staleMs ?? SharedQueryConfig.staleMs;
358
- this.expiryMs = options?.expiryMs ?? SharedQueryConfig.expiryMs;
359
- this.refetchOnStale = options?.refetchOnStale ?? SharedQueryConfig.refetchOnStale;
360
- if (!options.store) {
361
- const persistTo = options.persistTo ?? SharedQueryConfig.persistTo;
362
- if (persistTo) {
363
- options.store = {
364
- persistTo,
365
- key: options.queryName,
366
- expiryMs: this.expiryMs
367
- };
368
- }
369
- }
370
- this.maxSize = options?.maxSize ?? SharedQueryConfig.maxSize;
371
- this.maxBytes = options?.maxBytes ?? SharedQueryConfig.maxBytes;
372
- this.keepLastNonEmptyData = Boolean(options?.keepLastNonEmptyData);
373
- this.log = options?.log ?? SharedQueryConfig.log;
374
- this.queryState = sharedState(
375
- {},
376
- // defaultValue
377
- options
378
- );
379
- }
380
- /** Returns a key to identify requests based on the given args. */
381
- getQueryKey(args) {
382
- return this.queryName + ":" + stringifyDeterministicForKeys(args);
383
- }
384
- /**
385
- * Get the AbortController to abort the inflight query (if any).
386
- *
387
- * Note that once you get the controller, you can also get the signal easily
388
- * using `controller.signal`. So if you need both the controller and the
389
- * signal, just use this function.
390
- */
391
- getAbortController(args) {
392
- const queryKey = this.getQueryKey(args);
393
- return this.getAbortControllerByKey(queryKey);
394
- }
395
- getAbortControllerByKey(queryKey) {
396
- return this.inflightQueries.get(queryKey)?.abortController ?? null;
397
- }
398
- /**
399
- * Get the AbortSignal to check for query abortions.
400
- *
401
- * Example:
402
- * ```
403
- * const getUserQuery = sharedQuery({
404
- * queryName: "getUser",
405
- * queryFn: (userId: string) => {
406
- * // Get the signal for this current execution.
407
- * const signal = getUserQuery.getAbortSignal([userId]);
408
- *
409
- * // Pass the signal to fetch so that it can be aborted.
410
- * const response = await fetch(`/users/${userId}`, { signal });
411
- *
412
- * // Check for errors.
413
- * if (!response.ok) {
414
- * throw new Error(response.statusText);
415
- * }
416
- *
417
- * // Return the data.
418
- * return await response.json();
419
- * },
420
- * });
421
- * ```
422
- */
423
- getAbortSignal(args) {
424
- const queryKey = this.getQueryKey(args);
425
- return this.getAbortSignalByKey(queryKey);
336
+ var seenQueryNames = /* @__PURE__ */ new Set();
337
+ function sharedQuery(options) {
338
+ if (seenQueryNames.has(options.queryName)) {
339
+ throw new Error(`Duplicate shared query name "${options.queryName}"`);
426
340
  }
427
- getAbortSignalByKey(queryKey) {
428
- return this.getAbortControllerByKey(queryKey)?.signal ?? null;
341
+ seenQueryNames.add(options.queryName);
342
+ const inflightQueries = /* @__PURE__ */ new Map();
343
+ const mountedKeys = /* @__PURE__ */ new Map();
344
+ const queryName = options.queryName;
345
+ const queryFn = options.queryFn;
346
+ const refetchFn = options.refetchFn;
347
+ const staleMs = options?.staleMs ?? SharedQueryConfig.staleMs;
348
+ const expiryMs = options?.expiryMs ?? SharedQueryConfig.expiryMs;
349
+ const refetchOnStale = options?.refetchOnStale ?? SharedQueryConfig.refetchOnStale;
350
+ if (!options.store) {
351
+ const persistTo = options.persistTo ?? SharedQueryConfig.persistTo;
352
+ if (persistTo) {
353
+ options.store = {
354
+ persistTo,
355
+ key: queryName,
356
+ expiryMs
357
+ };
358
+ }
429
359
  }
430
- isStale(dataUpdatedMs) {
431
- if (this.staleMs === 0) return true;
360
+ const maxSize = options?.maxSize ?? SharedQueryConfig.maxSize;
361
+ const maxBytes = options?.maxBytes ?? SharedQueryConfig.maxBytes;
362
+ const keepLastNonEmptyData = Boolean(options?.keepLastNonEmptyData);
363
+ const log = options?.log ?? SharedQueryConfig.log;
364
+ const queryState = sharedState(
365
+ {},
366
+ // defaultValue
367
+ options
368
+ );
369
+ function isStale(dataUpdatedMs) {
370
+ if (staleMs === 0) return true;
432
371
  const ageMs = Date.now() - dataUpdatedMs;
433
- return ageMs > this.staleMs;
372
+ return ageMs > staleMs;
434
373
  }
435
- isExpired(dataUpdatedMs) {
436
- if (this.expiryMs === 0) return false;
374
+ function isExpired(dataUpdatedMs) {
375
+ if (expiryMs === 0) return false;
437
376
  const ageMs = Date.now() - dataUpdatedMs;
438
- return ageMs > this.expiryMs;
439
- }
440
- /** For cached data, we need to return the correct timestamps as well. */
441
- async getCachedOrFetch(...args) {
442
- const startMs = Date.now();
443
- await this.queryState.readyPromise;
444
- const waitedMs = Date.now() - startMs;
445
- const queryKey = this.getQueryKey(args);
446
- const cached = this.queryState.getSnapshot()?.[queryKey];
447
- if (waitedMs) {
448
- this.log(`${queryKey}: Waited for ${waitedMs}ms before data is ready`);
449
- }
450
- if (cached?.data && !this.isExpired(cached.dataUpdatedMs ?? 0)) {
451
- if (!this.isStale(cached.dataUpdatedMs ?? 0)) {
452
- this.log(`Return fresh data ${queryKey} from cache without fetching`);
453
- return cached;
454
- }
455
- if (this.refetchOnStale) {
456
- this.log(`refetchOnStale for ${queryKey}`);
457
- void this.dedupedFetch(queryKey, "query", ...args);
458
- }
459
- this.log(`Returning stale data for ${queryKey}`);
460
- return cached;
461
- }
462
- const data = await this.dedupedFetch(queryKey, "query", ...args);
463
- const now = Date.now();
464
- return {
465
- // Don't keep old error.
466
- lastUpdatedMs: now,
467
- loading: false,
468
- data,
469
- dataUpdatedMs: now
470
- };
377
+ return ageMs > expiryMs;
471
378
  }
472
- dedupedFetch(queryKey, queryType, ...args) {
473
- const inflightQuery = this.inflightQueries.get(queryKey);
474
- if (inflightQuery) {
475
- this.log(
476
- `Deduplicating inflight fetch for ${queryKey}, queryType=${queryType}`
477
- );
478
- return inflightQuery.promise;
479
- }
480
- const promise = this.startFetching(queryKey, queryType, args);
481
- const abortController = new AbortController();
482
- this.inflightQueries.set(queryKey, { promise, abortController });
483
- return promise;
484
- }
485
- async startFetching(queryKey, queryType, args) {
486
- this.log(`Start fetching ${queryKey}, queryType=${queryType}`);
487
- try {
488
- const data = await (queryType === "refetch" && this.refetchFn ? this.refetchFn(...args) : this.queryFn(...args));
489
- this.log(`Successfully fetched ${queryKey}`);
490
- this.setData(queryKey, data, Date.now());
491
- return data;
492
- } catch (e) {
493
- this.log(`Failed to fetch ${queryKey} for queryType=${queryType}: ${e}`);
494
- throw e;
495
- } finally {
496
- this.inflightQueries.delete(queryKey);
497
- }
498
- }
499
- /** Get the entire stored entry for a query key. */
500
- getQueryValue(queryKey) {
501
- return this.queryState.getSnapshot()?.[queryKey];
502
- }
503
- /** Get the current stored data for a query key. */
504
- getData(queryKey) {
505
- const stateValue = this.queryState.getSnapshot()?.[queryKey];
506
- if (stateValue?.lastUpdatedMs) {
507
- if (this.isExpired(stateValue.lastUpdatedMs)) {
508
- this.deleteData(queryKey);
509
- return void 0;
510
- }
511
- }
512
- return stateValue?.data;
513
- }
514
- /** Set the data directly if the user obtained it from somewhere else. */
515
- setData(queryKey, data, dataUpdatedMs = Date.now()) {
516
- const entry = {
517
- lastUpdatedMs: Date.now(),
518
- loading: false,
519
- data,
520
- dataUpdatedMs
521
- };
522
- this.queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));
523
- this.enforceSizeLimit();
524
- }
525
- /**
526
- * Do a new fetch even when the data is already cached, but don't fetch if
527
- * another fetch is already inflight.
528
- */
529
- async updateFromSource(...args) {
530
- const key = this.getQueryKey(args);
531
- return await this.dedupedFetch(key, "query", ...args);
532
- }
533
- /**
534
- * Perform an explicit refetch. If `options.refetchFn` was not specified,
535
- * then this would be identical to calling `this.updateFromSource()`.
536
- */
537
- async refetch(...args) {
538
- const key = this.getQueryKey(args);
539
- return await this.dedupedFetch(key, "refetch", ...args);
540
- }
541
- /** Delete the cached data for one set of arguments. */
542
- deleteData(queryKey) {
543
- const record = this.queryState.getSnapshot();
544
- const clone = { ...record };
545
- delete clone[queryKey];
546
- this.queryState.setValue(clone);
547
- const inflight = this.inflightQueries.get(queryKey);
548
- if (inflight) {
549
- this.inflightQueries.delete(queryKey);
550
- inflight.abortController.abort("Deleted");
551
- }
552
- }
553
- /** Delete all currently cached data & all inflight promises. */
554
- clear() {
555
- this.queryState.delete();
556
- for (const { abortController } of Array.from(
557
- this.inflightQueries.values()
558
- )) {
559
- abortController.abort("Cleared");
560
- }
561
- this.inflightQueries.clear();
562
- }
563
- /** Keep track of mounted keys. */
564
- mount(queryKey) {
565
- this.mountedKeys.set(queryKey, (this.mountedKeys.get(queryKey) ?? 0) + 1);
566
- }
567
- /** Clean up mounted keys. */
568
- unmount(queryKey) {
569
- const count = this.mountedKeys.get(queryKey);
570
- if (count === 1) {
571
- this.mountedKeys.delete(queryKey);
572
- } else {
573
- this.mountedKeys.set(queryKey, (count ?? 0) - 1);
574
- }
575
- }
576
- /**
577
- * Discard the oldest entries if the size exceeds the limit. The last
578
- * remaining record cannot be deleted no matter what the limit is.
579
- */
580
- enforceSizeLimit() {
581
- if (!this.maxSize && !this.maxBytes) {
379
+ function enforceSizeLimit() {
380
+ if (!maxSize && !maxBytes) {
582
381
  return;
583
382
  }
584
- const oldState = this.queryState.getSnapshot();
383
+ const oldState = queryState.getSnapshot();
585
384
  const oldSize = Object.keys(oldState).length;
586
385
  if (oldSize <= 1) {
587
386
  return;
588
387
  }
589
388
  const oldByteSize = getByteSize(oldState);
590
- let numToCut = this.maxSize ? oldSize - this.maxSize : 0;
591
- let bytesToCut = this.maxBytes ? oldByteSize - this.maxBytes : 0;
389
+ let numToCut = maxSize ? oldSize - maxSize : 0;
390
+ let bytesToCut = maxBytes ? oldByteSize - maxBytes : 0;
592
391
  if (numToCut <= 0 && bytesToCut <= 0) {
593
- this.log(
594
- `No need to trim data for ${this.queryName}: size=${oldSize} (limit=${this.maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`
392
+ log(
393
+ `No need to trim data for ${queryName}: size=${oldSize} (limit=${maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${maxBytes.toLocaleString()})`
595
394
  );
596
395
  return;
597
396
  }
598
- this.log(
599
- `Need to trim ${this.queryName}: numToCut=${numToCut}, bytesToCut=${bytesToCut}`
397
+ log(
398
+ `Need to trim ${queryName}: numToCut=${numToCut}, bytesToCut=${bytesToCut}`
600
399
  );
601
400
  const newState = { ...oldState };
602
401
  let needUpdate = false;
603
402
  const deleteKeyIfNotMounted = (key, expired) => {
604
- if (this.mountedKeys.has(key)) {
605
- this.log(`Cannot clean up ${key} as it is mounted`);
403
+ if (mountedKeys.has(key)) {
404
+ log(`Cannot clean up ${key} as it is mounted`);
606
405
  return;
607
406
  }
608
407
  const byteSize = getByteSize(key) + getByteSize(newState[key]);
609
- this.log(
408
+ log(
610
409
  `Cleaning up unmounted${expired ? ", expired" : ""} ${key} (byteSize=${byteSize.toLocaleString()})`
611
410
  );
612
411
  numToCut--;
@@ -618,7 +417,7 @@ var SharedQuery = class {
618
417
  (entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs
619
418
  );
620
419
  for (const [key] of entriesByTimeAscending) {
621
- if (!this.isExpired(newState[key].lastUpdatedMs)) {
420
+ if (!isExpired(newState[key].lastUpdatedMs)) {
622
421
  break;
623
422
  }
624
423
  deleteKeyIfNotMounted(key, true);
@@ -637,24 +436,220 @@ var SharedQuery = class {
637
436
  }
638
437
  }
639
438
  if (!needUpdate) {
640
- this.log(
641
- `No trimmable entries found for ${this.queryName}: size=${oldSize} (limit=${this.maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`
439
+ log(
440
+ `No trimmable entries found for ${queryName}: size=${oldSize} (limit=${maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${maxBytes.toLocaleString()})`
642
441
  );
643
442
  return;
644
443
  }
645
- this.queryState.setValue(newState);
646
- this.log(
647
- `Trimmed data for ${this.queryName}: size=[${oldSize} -> ${Object.keys(newState).length}] (limit=${this.maxSize}), byteSize=[${oldByteSize.toLocaleString()} -> ${getByteSize(newState).toLocaleString()}] (limit=${this.maxBytes.toLocaleString()})`
444
+ queryState.setValue(newState);
445
+ log(
446
+ `Trimmed data for ${queryName}: size=[${oldSize} -> ${Object.keys(newState).length}] (limit=${maxSize}), byteSize=[${oldByteSize.toLocaleString()} -> ${getByteSize(newState).toLocaleString()}] (limit=${maxBytes.toLocaleString()})`
648
447
  );
649
448
  }
650
- };
651
- var seenQueryNames = /* @__PURE__ */ new Set();
652
- function sharedQuery(options) {
653
- if (seenQueryNames.has(options.queryName)) {
654
- throw new Error(`Duplicate shared query "${options.queryName}"`);
449
+ function setData(queryKey, data, dataUpdatedMs = Date.now()) {
450
+ const entry = {
451
+ lastUpdatedMs: Date.now(),
452
+ loading: false,
453
+ data,
454
+ dataUpdatedMs
455
+ };
456
+ queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));
457
+ enforceSizeLimit();
655
458
  }
656
- seenQueryNames.add(options.queryName);
657
- return new SharedQuery(options);
459
+ async function startFetching(queryKey, queryType, args) {
460
+ log(`Start fetching ${queryKey}, queryType=${queryType}`);
461
+ try {
462
+ const data = await (queryType === "refetch" && refetchFn ? refetchFn(...args) : queryFn(...args));
463
+ log(`Successfully fetched ${queryKey}`);
464
+ setData(queryKey, data, Date.now());
465
+ return data;
466
+ } catch (e) {
467
+ log(`Failed to fetch ${queryKey} for queryType=${queryType}: ${e}`);
468
+ throw e;
469
+ } finally {
470
+ inflightQueries.delete(queryKey);
471
+ }
472
+ }
473
+ function dedupedFetch(queryKey, queryType, ...args) {
474
+ const inflightQuery = inflightQueries.get(queryKey);
475
+ if (inflightQuery) {
476
+ log(
477
+ `Deduplicating inflight fetch for ${queryKey}, queryType=${queryType}`
478
+ );
479
+ return inflightQuery.promise;
480
+ }
481
+ const promise = startFetching(queryKey, queryType, args);
482
+ const abortController = new AbortController();
483
+ inflightQueries.set(queryKey, { promise, abortController });
484
+ return promise;
485
+ }
486
+ const obj = {
487
+ queryName,
488
+ queryFn,
489
+ refetchFn,
490
+ expiryMs,
491
+ staleMs,
492
+ refetchOnStale,
493
+ maxSize,
494
+ maxBytes,
495
+ keepLastNonEmptyData,
496
+ log,
497
+ queryState,
498
+ /** Returns a key to identify requests based on the given args. */
499
+ getQueryKey(args) {
500
+ return queryName + ":" + stringifyDeterministicForKeys(args);
501
+ },
502
+ /**
503
+ * Get the AbortController to abort the inflight query (if any).
504
+ *
505
+ * Note that once you get the controller, you can also get the signal easily
506
+ * using `controller.signal`. So if you need both the controller and the
507
+ * signal, just use this function.
508
+ */
509
+ getAbortController(args) {
510
+ const queryKey = obj.getQueryKey(args);
511
+ return obj.getAbortControllerByKey(queryKey);
512
+ },
513
+ getAbortControllerByKey(queryKey) {
514
+ return inflightQueries.get(queryKey)?.abortController ?? null;
515
+ },
516
+ /**
517
+ * Get the AbortSignal to check for query abortions.
518
+ *
519
+ * Example:
520
+ * ```
521
+ * const getUserQuery = sharedQuery({
522
+ * queryName: "getUser",
523
+ * queryFn: (userId: string) => {
524
+ * // Get the signal for this current execution.
525
+ * const signal = getUserQuery.getAbortSignal([userId]);
526
+ *
527
+ * // Pass the signal to fetch so that it can be aborted.
528
+ * const response = await fetch(`/users/${userId}`, { signal });
529
+ *
530
+ * // Check for errors.
531
+ * if (!response.ok) {
532
+ * throw new Error(response.statusText);
533
+ * }
534
+ *
535
+ * // Return the data.
536
+ * return await response.json();
537
+ * },
538
+ * });
539
+ * ```
540
+ */
541
+ getAbortSignal(args) {
542
+ const queryKey = obj.getQueryKey(args);
543
+ return obj.getAbortSignalByKey(queryKey);
544
+ },
545
+ getAbortSignalByKey(queryKey) {
546
+ return obj.getAbortControllerByKey(queryKey)?.signal ?? null;
547
+ },
548
+ /** For cached data, we need to return the correct timestamps as well. */
549
+ async getCachedOrFetch(...args) {
550
+ const startMs = Date.now();
551
+ await queryState.readyPromise;
552
+ const waitedMs = Date.now() - startMs;
553
+ const queryKey = obj.getQueryKey(args);
554
+ const cached = queryState.getSnapshot()?.[queryKey];
555
+ if (waitedMs) {
556
+ log(`${queryKey}: Waited for ${waitedMs}ms before data is ready`);
557
+ }
558
+ if (cached?.data && !isExpired(cached.dataUpdatedMs ?? 0)) {
559
+ if (!isStale(cached.dataUpdatedMs ?? 0)) {
560
+ log(`Return fresh data ${queryKey} from cache without fetching`);
561
+ return cached;
562
+ }
563
+ if (refetchOnStale) {
564
+ log(`refetchOnStale for ${queryKey}`);
565
+ void dedupedFetch(queryKey, "query", ...args);
566
+ }
567
+ log(`Returning stale data for ${queryKey}`);
568
+ return cached;
569
+ }
570
+ const data = await dedupedFetch(queryKey, "query", ...args);
571
+ const now = Date.now();
572
+ return {
573
+ // Don't keep old error.
574
+ lastUpdatedMs: now,
575
+ loading: false,
576
+ data,
577
+ dataUpdatedMs: now
578
+ };
579
+ },
580
+ /** Get the entire stored entry for a query key. */
581
+ getQueryValue(queryKey) {
582
+ return queryState.getSnapshot()?.[queryKey];
583
+ },
584
+ /** Get the current stored data for a query key. */
585
+ getData(queryKey) {
586
+ const stateValue = queryState.getSnapshot()?.[queryKey];
587
+ if (stateValue?.lastUpdatedMs) {
588
+ if (isExpired(stateValue.lastUpdatedMs)) {
589
+ obj.deleteData(queryKey);
590
+ return void 0;
591
+ }
592
+ }
593
+ return stateValue?.data;
594
+ },
595
+ /** Set the data directly if the user obtained it from somewhere else. */
596
+ setData,
597
+ /**
598
+ * Do a new fetch even when the data is already cached, but don't fetch if
599
+ * another fetch is already inflight.
600
+ */
601
+ async updateFromSource(...args) {
602
+ const key = obj.getQueryKey(args);
603
+ return await dedupedFetch(key, "query", ...args);
604
+ },
605
+ /**
606
+ * Perform an explicit refetch. If `options.refetchFn` was not specified,
607
+ * then this would be identical to calling `obj.updateFromSource()`.
608
+ */
609
+ async refetch(...args) {
610
+ const key = obj.getQueryKey(args);
611
+ return await dedupedFetch(key, "refetch", ...args);
612
+ },
613
+ /** Delete the cached data for one set of arguments. */
614
+ deleteData(queryKey) {
615
+ const record = queryState.getSnapshot();
616
+ const clone = { ...record };
617
+ delete clone[queryKey];
618
+ queryState.setValue(clone);
619
+ const inflight = inflightQueries.get(queryKey);
620
+ if (inflight) {
621
+ inflightQueries.delete(queryKey);
622
+ inflight.abortController.abort("Deleted");
623
+ }
624
+ },
625
+ /** Delete all currently cached data & all inflight promises. */
626
+ clear() {
627
+ queryState.delete();
628
+ for (const { abortController } of Array.from(inflightQueries.values())) {
629
+ abortController.abort("Cleared");
630
+ }
631
+ inflightQueries.clear();
632
+ },
633
+ /** Keep track of mounted keys. */
634
+ mount(queryKey) {
635
+ mountedKeys.set(queryKey, (mountedKeys.get(queryKey) ?? 0) + 1);
636
+ },
637
+ /** Clean up mounted keys. */
638
+ unmount(queryKey) {
639
+ const count = mountedKeys.get(queryKey);
640
+ if (count === 1) {
641
+ mountedKeys.delete(queryKey);
642
+ } else {
643
+ mountedKeys.set(queryKey, (count ?? 0) - 1);
644
+ }
645
+ },
646
+ /**
647
+ * Discard the oldest entries if the size exceeds the limit. The last
648
+ * remaining record cannot be deleted no matter what the limit is.
649
+ */
650
+ enforceSizeLimit
651
+ };
652
+ return obj;
658
653
  }
659
654
  var DEFAULT_QUERY_STATE_ENTRY = {
660
655
  lastUpdatedMs: 0,
@@ -751,38 +746,18 @@ function useSharedQuery(query, ...args) {
751
746
  return Boolean(controller);
752
747
  },
753
748
  refetch: () => {
754
- void execute(true);
749
+ return execute(true);
755
750
  },
756
751
  setData: (data, dataUpdatedMs) => {
757
- const now = Date.now();
758
- setQueryStateValue((prev) => ({
759
- ...prev,
760
- data,
761
- dataUpdatedMs: dataUpdatedMs ?? now,
762
- lastUpdatedMs: now,
763
- loading: false
764
- }));
752
+ query.setData(queryKey, data, dataUpdatedMs);
765
753
  },
766
754
  deleteData: () => {
767
- setQueryState((prev) => {
768
- const clone = { ...prev };
769
- delete clone[queryKey];
770
- return clone;
771
- });
755
+ query.deleteData(queryKey);
772
756
  }
773
757
  };
774
- }, [
775
- execute,
776
- lastNonEmptyData,
777
- query,
778
- queryKey,
779
- queryStateValue,
780
- setQueryState,
781
- setQueryStateValue
782
- ]);
758
+ }, [execute, lastNonEmptyData, query, queryKey, queryStateValue]);
783
759
  }
784
760
  export {
785
- SharedQuery,
786
761
  SharedQueryConfig,
787
762
  configureSharedQuery,
788
763
  sharedQuery,