@choksheak/react-utils 0.3.5 → 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 -471
  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 -471
  27. package/sharedState.cjs +200 -189
  28. package/sharedState.d.mts +49 -37
  29. package/sharedState.d.ts +46 -35
  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 -189
  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,106 +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
- this.pubSubKey = String(stateKey++);
165
- this.storageAdapter = getStorageAdapter(
166
- options,
167
- SharedStateConfig.storeExpiryMs
168
- );
169
- this.setValueBounded = (next) => {
170
- this.setValue(next);
171
- };
172
- pubSubStore.setNoNotify(this.pubSubKey, defaultValue);
173
- const lazyLoad = options?.lazyLoad;
174
- const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateConfig.lazyLoad;
175
- if (!lazy) {
176
- void this.initDefaultValueOnce();
177
- }
178
- }
179
- pubSubKey;
180
- storageAdapter;
181
- setValueBounded;
182
- initStarted = false;
183
- initDone = false;
184
- /** `subscribe` function required by useSyncExternalStore. */
185
- subscribe(subscriber) {
186
- return pubSubStore.subscribe(this.pubSubKey, () => subscriber());
187
- }
188
- /** Returns true if there is a backing store. */
189
- hasStore() {
190
- return Boolean(this.storageAdapter);
191
- }
192
- /**
193
- * Set a value directly into the store. Does nothing if there is no
194
- * backing store.
195
- *
196
- * Warning: If you want to set the value properly, you should use
197
- * `setValue()` instead which also updates the in-memory copy.
198
- */
199
- async setInStore(value) {
200
- return await this.storageAdapter?.set(STORAGE_KEY, value);
201
- }
202
- /**
203
- * Get a value directly from the store. Returns undefined if there is no
204
- * backing store.
205
- */
206
- async getFromStore() {
207
- return await this.storageAdapter?.get(STORAGE_KEY);
208
- }
209
- /**
210
- * Get the value of the shared state.
211
- * `getSnapshot` function — must return same ref if value unchanged.
212
- * Note that this would return the default value if data init from store is
213
- * not completed yet.
214
- */
215
- getSnapshot() {
216
- return pubSubStore.get(this.pubSubKey);
217
- }
218
- /** Set the value of the shared state. */
219
- setValue(next) {
220
- if (typeof next === "function") {
221
- const prev = pubSubStore.get(this.pubSubKey);
222
- 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();
223
253
  }
224
- pubSubStore.set(this.pubSubKey, next);
225
- void this.setInStore(next);
226
- }
227
- /**
228
- * Remove the value of the shared state. Actually this just sets the value
229
- * back to the given default value.
230
- */
231
- delete() {
232
- pubSubStore.set(this.pubSubKey, this.defaultValue);
233
- void this.storageAdapter?.set(STORAGE_KEY, this.defaultValue);
234
- }
235
- /** Initial default handling. */
236
- async initDefaultValueOnce() {
237
- if (this.initDone || this.initStarted) return;
238
- this.initStarted = true;
239
- const stored = await this.getFromStore();
240
- const value = stored !== void 0 ? stored : this.defaultValue;
241
- pubSubStore.set(this.pubSubKey, value);
242
- this.initDone = true;
254
+ };
255
+ if (!lazy) {
256
+ void obj.initDefaultValueOnce();
243
257
  }
244
- };
245
- function sharedState(defaultValue, options) {
246
- return new SharedState(defaultValue, options);
258
+ return obj;
247
259
  }
248
260
  function useSharedState(state) {
249
- state.initDefaultValueOnce();
261
+ void state.initDefaultValueOnce();
250
262
  const value = useSyncExternalStore(
251
263
  (onStoreChange) => state.subscribe(onStoreChange),
252
264
  () => state.getSnapshot(),
253
265
  // getServerSnapshot is needed to prevent SSR dev errors in nextjs.
254
266
  () => state.getSnapshot()
255
267
  );
256
- return [value, state.setValueBounded];
268
+ return [value, state.setValue];
257
269
  }
258
270
 
259
271
  // src/stringify.ts
@@ -293,7 +305,7 @@ import { useMemo, useState } from "react";
293
305
  function useDeepMemo(value) {
294
306
  const [state, setState] = useState(value);
295
307
  return useMemo(() => {
296
- if (state === value || isEqual(state, value)) {
308
+ if (isEqual(state, value)) {
297
309
  return state;
298
310
  }
299
311
  setState(value);
@@ -321,276 +333,79 @@ var SharedQueryConfig = {
321
333
  function configureSharedQuery(config) {
322
334
  Object.assign(SharedQueryConfig, config);
323
335
  }
324
- var SharedQuery = class {
325
- inflightQueries = /* @__PURE__ */ new Map();
326
- queryState;
327
- /**
328
- * Keep track of the mounted query keys. This handles the case where an
329
- * earlier-started query returns later, and when it returns, it was already
330
- * unmounted, thus making it eligible for clean up right after fetch.
331
- */
332
- mountedKeys = /* @__PURE__ */ new Map();
333
- queryName;
334
- queryFn;
335
- refetchFn;
336
- expiryMs;
337
- staleMs;
338
- refetchOnStale;
339
- maxSize;
340
- maxBytes;
341
- keepLastNonEmptyData;
342
- log;
343
- constructor(options) {
344
- this.queryName = options.queryName;
345
- this.queryFn = options.queryFn;
346
- this.refetchFn = options.refetchFn;
347
- this.staleMs = options?.staleMs ?? SharedQueryConfig.staleMs;
348
- this.expiryMs = options?.expiryMs ?? SharedQueryConfig.expiryMs;
349
- this.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: options.queryName,
356
- expiryMs: this.expiryMs
357
- };
358
- }
359
- }
360
- this.maxSize = options?.maxSize ?? SharedQueryConfig.maxSize;
361
- this.maxBytes = options?.maxBytes ?? SharedQueryConfig.maxBytes;
362
- this.keepLastNonEmptyData = Boolean(options?.keepLastNonEmptyData);
363
- this.log = options?.log ?? SharedQueryConfig.log;
364
- this.queryState = sharedState(
365
- {},
366
- // defaultValue
367
- options
368
- );
369
- }
370
- /** Returns a key to identify requests based on the given args. */
371
- getQueryKey(args) {
372
- return this.queryName + ":" + stringifyDeterministicForKeys(args);
373
- }
374
- /**
375
- * Get the AbortController to abort the inflight query (if any).
376
- *
377
- * Note that once you get the controller, you can also get the signal easily
378
- * using `controller.signal`. So if you need both the controller and the
379
- * signal, just use this function.
380
- */
381
- getAbortController(args) {
382
- const queryKey = this.getQueryKey(args);
383
- return this.getAbortControllerByKey(queryKey);
384
- }
385
- getAbortControllerByKey(queryKey) {
386
- return this.inflightQueries.get(queryKey)?.abortController ?? null;
387
- }
388
- /**
389
- * Get the AbortSignal to check for query abortions.
390
- *
391
- * Example:
392
- * ```
393
- * const getUserQuery = sharedQuery({
394
- * queryName: "getUser",
395
- * queryFn: (userId: string) => {
396
- * // Get the signal for this current execution.
397
- * const signal = getUserQuery.getAbortSignal([userId]);
398
- *
399
- * // Pass the signal to fetch so that it can be aborted.
400
- * const response = await fetch(`/users/${userId}`, { signal });
401
- *
402
- * // Check for errors.
403
- * if (!response.ok) {
404
- * throw new Error(response.statusText);
405
- * }
406
- *
407
- * // Return the data.
408
- * return await response.json();
409
- * },
410
- * });
411
- * ```
412
- */
413
- getAbortSignal(args) {
414
- const queryKey = this.getQueryKey(args);
415
- 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}"`);
416
340
  }
417
- getAbortSignalByKey(queryKey) {
418
- 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
+ }
419
359
  }
420
- isStale(dataUpdatedMs) {
421
- 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;
422
371
  const ageMs = Date.now() - dataUpdatedMs;
423
- return ageMs > this.staleMs;
372
+ return ageMs > staleMs;
424
373
  }
425
- isExpired(dataUpdatedMs) {
426
- if (this.expiryMs === 0) return false;
374
+ function isExpired(dataUpdatedMs) {
375
+ if (expiryMs === 0) return false;
427
376
  const ageMs = Date.now() - dataUpdatedMs;
428
- return ageMs > this.expiryMs;
429
- }
430
- /** For cached data, we need to return the correct timestamps as well. */
431
- async getCachedOrFetch(...args) {
432
- const queryKey = this.getQueryKey(args);
433
- const cached = this.queryState.getSnapshot()?.[queryKey];
434
- if (cached?.data && !this.isExpired(cached.dataUpdatedMs ?? 0)) {
435
- if (!this.isStale(cached.dataUpdatedMs ?? 0)) {
436
- this.log(`Return fresh data ${queryKey} from cache without fetching`);
437
- return cached;
438
- }
439
- if (this.refetchOnStale) {
440
- this.log(`refetchOnStale for ${queryKey}`);
441
- void this.dedupedFetch(queryKey, "query", ...args);
442
- }
443
- this.log(`Returning stale data for ${queryKey}`);
444
- return cached;
445
- }
446
- const data = await this.dedupedFetch(queryKey, "query", ...args);
447
- const now = Date.now();
448
- return {
449
- // Don't keep old error.
450
- lastUpdatedMs: now,
451
- loading: false,
452
- data,
453
- dataUpdatedMs: now
454
- };
377
+ return ageMs > expiryMs;
455
378
  }
456
- dedupedFetch(queryKey, queryType, ...args) {
457
- const inflightQuery = this.inflightQueries.get(queryKey);
458
- if (inflightQuery) {
459
- this.log(
460
- `Deduplicating inflight fetch for ${queryKey}, queryType=${queryType}`
461
- );
462
- return inflightQuery.promise;
463
- }
464
- const promise = this.startFetching(queryKey, queryType, args);
465
- const abortController = new AbortController();
466
- this.inflightQueries.set(queryKey, { promise, abortController });
467
- return promise;
468
- }
469
- async startFetching(queryKey, queryType, args) {
470
- this.log(`Start fetching ${queryKey}, queryType=${queryType}`);
471
- try {
472
- const data = await (queryType === "refetch" && this.refetchFn ? this.refetchFn(...args) : this.queryFn(...args));
473
- this.log(`Successfully fetched ${queryKey}`);
474
- this.setData(queryKey, data, Date.now());
475
- return data;
476
- } catch (e) {
477
- this.log(`Failed to fetch ${queryKey} for queryType=${queryType}: ${e}`);
478
- throw e;
479
- } finally {
480
- this.inflightQueries.delete(queryKey);
481
- }
482
- }
483
- /** Get the entire stored entry for a query key. */
484
- getQueryValue(queryKey) {
485
- return this.queryState.getSnapshot()?.[queryKey];
486
- }
487
- /** Get the current stored data for a query key. */
488
- getData(queryKey) {
489
- const stateValue = this.queryState.getSnapshot()?.[queryKey];
490
- if (stateValue?.lastUpdatedMs) {
491
- if (this.isExpired(stateValue.lastUpdatedMs)) {
492
- this.deleteData(queryKey);
493
- return void 0;
494
- }
495
- }
496
- return stateValue?.data;
497
- }
498
- /** Set the data directly if the user obtained it from somewhere else. */
499
- setData(queryKey, data, dataUpdatedMs = Date.now()) {
500
- const entry = {
501
- lastUpdatedMs: Date.now(),
502
- loading: false,
503
- data,
504
- dataUpdatedMs
505
- };
506
- this.queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));
507
- this.enforceSizeLimit();
508
- }
509
- /**
510
- * Do a new fetch even when the data is already cached, but don't fetch if
511
- * another fetch is already inflight.
512
- */
513
- async updateFromSource(...args) {
514
- const key = this.getQueryKey(args);
515
- return await this.dedupedFetch(key, "query", ...args);
516
- }
517
- /**
518
- * Perform an explicit refetch. If `options.refetchFn` was not specified,
519
- * then this would be identical to calling `this.updateFromSource()`.
520
- */
521
- async refetch(...args) {
522
- const key = this.getQueryKey(args);
523
- return await this.dedupedFetch(key, "refetch", ...args);
524
- }
525
- /** Delete the cached data for one set of arguments. */
526
- deleteData(queryKey) {
527
- const record = this.queryState.getSnapshot();
528
- const clone = { ...record };
529
- delete clone[queryKey];
530
- this.queryState.setValue(clone);
531
- const inflight = this.inflightQueries.get(queryKey);
532
- if (inflight) {
533
- this.inflightQueries.delete(queryKey);
534
- inflight.abortController.abort("Deleted");
535
- }
536
- }
537
- /** Delete all currently cached data & all inflight promises. */
538
- clear() {
539
- this.queryState.delete();
540
- for (const { abortController } of Array.from(
541
- this.inflightQueries.values()
542
- )) {
543
- abortController.abort("Cleared");
544
- }
545
- this.inflightQueries.clear();
546
- }
547
- /** Keep track of mounted keys. */
548
- mount(queryKey) {
549
- this.mountedKeys.set(queryKey, (this.mountedKeys.get(queryKey) ?? 0) + 1);
550
- }
551
- /** Clean up mounted keys. */
552
- unmount(queryKey) {
553
- const count = this.mountedKeys.get(queryKey);
554
- if (count === 1) {
555
- this.mountedKeys.delete(queryKey);
556
- } else {
557
- this.mountedKeys.set(queryKey, (count ?? 0) - 1);
558
- }
559
- }
560
- /**
561
- * Discard the oldest entries if the size exceeds the limit. The last
562
- * remaining record cannot be deleted no matter what the limit is.
563
- */
564
- enforceSizeLimit() {
565
- if (!this.maxSize && !this.maxBytes) {
379
+ function enforceSizeLimit() {
380
+ if (!maxSize && !maxBytes) {
566
381
  return;
567
382
  }
568
- const oldState = this.queryState.getSnapshot();
383
+ const oldState = queryState.getSnapshot();
569
384
  const oldSize = Object.keys(oldState).length;
570
385
  if (oldSize <= 1) {
571
386
  return;
572
387
  }
573
388
  const oldByteSize = getByteSize(oldState);
574
- let numToCut = this.maxSize ? oldSize - this.maxSize : 0;
575
- let bytesToCut = this.maxBytes ? oldByteSize - this.maxBytes : 0;
389
+ let numToCut = maxSize ? oldSize - maxSize : 0;
390
+ let bytesToCut = maxBytes ? oldByteSize - maxBytes : 0;
576
391
  if (numToCut <= 0 && bytesToCut <= 0) {
577
- this.log(
578
- `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()})`
579
394
  );
580
395
  return;
581
396
  }
582
- this.log(
583
- `Need to trim ${this.queryName}: numToCut=${numToCut}, bytesToCut=${bytesToCut}`
397
+ log(
398
+ `Need to trim ${queryName}: numToCut=${numToCut}, bytesToCut=${bytesToCut}`
584
399
  );
585
400
  const newState = { ...oldState };
586
401
  let needUpdate = false;
587
402
  const deleteKeyIfNotMounted = (key, expired) => {
588
- if (this.mountedKeys.has(key)) {
589
- 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`);
590
405
  return;
591
406
  }
592
407
  const byteSize = getByteSize(key) + getByteSize(newState[key]);
593
- this.log(
408
+ log(
594
409
  `Cleaning up unmounted${expired ? ", expired" : ""} ${key} (byteSize=${byteSize.toLocaleString()})`
595
410
  );
596
411
  numToCut--;
@@ -602,7 +417,7 @@ var SharedQuery = class {
602
417
  (entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs
603
418
  );
604
419
  for (const [key] of entriesByTimeAscending) {
605
- if (!this.isExpired(newState[key].lastUpdatedMs)) {
420
+ if (!isExpired(newState[key].lastUpdatedMs)) {
606
421
  break;
607
422
  }
608
423
  deleteKeyIfNotMounted(key, true);
@@ -621,24 +436,220 @@ var SharedQuery = class {
621
436
  }
622
437
  }
623
438
  if (!needUpdate) {
624
- this.log(
625
- `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()})`
626
441
  );
627
442
  return;
628
443
  }
629
- this.queryState.setValue(newState);
630
- this.log(
631
- `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()})`
632
447
  );
633
448
  }
634
- };
635
- var seenQueryNames = /* @__PURE__ */ new Set();
636
- function sharedQuery(options) {
637
- if (seenQueryNames.has(options.queryName)) {
638
- 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();
639
458
  }
640
- seenQueryNames.add(options.queryName);
641
- 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;
642
653
  }
643
654
  var DEFAULT_QUERY_STATE_ENTRY = {
644
655
  lastUpdatedMs: 0,
@@ -735,38 +746,18 @@ function useSharedQuery(query, ...args) {
735
746
  return Boolean(controller);
736
747
  },
737
748
  refetch: () => {
738
- void execute(true);
749
+ return execute(true);
739
750
  },
740
751
  setData: (data, dataUpdatedMs) => {
741
- const now = Date.now();
742
- setQueryStateValue((prev) => ({
743
- ...prev,
744
- data,
745
- dataUpdatedMs: dataUpdatedMs ?? now,
746
- lastUpdatedMs: now,
747
- loading: false
748
- }));
752
+ query.setData(queryKey, data, dataUpdatedMs);
749
753
  },
750
754
  deleteData: () => {
751
- setQueryState((prev) => {
752
- const clone = { ...prev };
753
- delete clone[queryKey];
754
- return clone;
755
- });
755
+ query.deleteData(queryKey);
756
756
  }
757
757
  };
758
- }, [
759
- execute,
760
- lastNonEmptyData,
761
- query,
762
- queryKey,
763
- queryStateValue,
764
- setQueryState,
765
- setQueryStateValue
766
- ]);
758
+ }, [execute, lastNonEmptyData, query, queryKey, queryStateValue]);
767
759
  }
768
760
  export {
769
- SharedQuery,
770
761
  SharedQueryConfig,
771
762
  configureSharedQuery,
772
763
  sharedQuery,