@choksheak/react-utils 0.3.6 → 0.3.8

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 +486 -486
  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 +487 -486
  27. package/sharedState.cjs +201 -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 +202 -199
  35. package/storage.cjs +79 -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 +77 -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.cjs CHANGED
@@ -30,7 +30,6 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/sharedQuery.ts
31
31
  var sharedQuery_exports = {};
32
32
  __export(sharedQuery_exports, {
33
- SharedQuery: () => SharedQuery,
34
33
  SharedQueryConfig: () => SharedQueryConfig,
35
34
  configureSharedQuery: () => configureSharedQuery,
36
35
  sharedQuery: () => sharedQuery,
@@ -56,70 +55,12 @@ function getStorageAdapter(options, defaultStoreExpiryMs) {
56
55
  }
57
56
  if ("persistTo" in store && typeof store.persistTo === "string" && "key" in store && typeof store.key === "string") {
58
57
  const expiryMs = store.expiryMs || defaultStoreExpiryMs;
59
- const isValid = store.isValid;
58
+ const config = { ...store, expiryMs };
60
59
  switch (store.persistTo) {
61
- case "localStorage": {
62
- const adapter = new import_localStore.LocalStore(store.key, {
63
- defaultExpiryMs: expiryMs
64
- });
65
- return {
66
- set: (key, value) => {
67
- if (IS_NOT_BROWSER) return;
68
- adapter.set(key, value);
69
- },
70
- get: (key) => {
71
- if (IS_NOT_BROWSER) return void 0;
72
- const value = adapter.get(key);
73
- if (isValid && !isValid(value)) {
74
- console.warn(
75
- `localStorage adapter: Auto-discard invalid value for ${key}: ${JSON.stringify(value)}`
76
- );
77
- adapter.delete(key);
78
- return void 0;
79
- }
80
- return value;
81
- },
82
- delete: (key) => {
83
- if (IS_NOT_BROWSER) return;
84
- adapter.delete(key);
85
- },
86
- clear: () => {
87
- if (IS_NOT_BROWSER) return;
88
- adapter.clear();
89
- }
90
- };
91
- }
92
- case "indexedDb": {
93
- const keyPrefix = store.key + ":";
94
- const isValid2 = store.isValid;
95
- return {
96
- set: async (key, value) => {
97
- if (IS_NOT_BROWSER) return;
98
- await import_kvStore.kvStore.set(keyPrefix + key, value, expiryMs);
99
- },
100
- get: async (key) => {
101
- if (IS_NOT_BROWSER) return void 0;
102
- const fullKey = keyPrefix + key;
103
- const value = await import_kvStore.kvStore.get(fullKey);
104
- if (isValid2 && !isValid2(value)) {
105
- console.warn(
106
- `indexedDb adapter: Auto-discard invalid value for ${key}: ${JSON.stringify(value)}`
107
- );
108
- await import_kvStore.kvStore.delete(fullKey);
109
- return void 0;
110
- }
111
- return value;
112
- },
113
- delete: async (key) => {
114
- if (IS_NOT_BROWSER) return;
115
- await import_kvStore.kvStore.delete(keyPrefix + key);
116
- },
117
- clear: async () => {
118
- if (IS_NOT_BROWSER) return;
119
- await import_kvStore.kvStore.clear();
120
- }
121
- };
122
- }
60
+ case "localStorage":
61
+ return localStoreAdapter(config);
62
+ case "indexedDb":
63
+ return kvStoreAdapter(config);
123
64
  default:
124
65
  store.persistTo;
125
66
  throw new Error(`Unknown store.adapter ${store.persistTo}`);
@@ -127,6 +68,74 @@ function getStorageAdapter(options, defaultStoreExpiryMs) {
127
68
  }
128
69
  return store;
129
70
  }
71
+ function localStoreAdapter({
72
+ key,
73
+ expiryMs,
74
+ isValid
75
+ }) {
76
+ const adapter = (0, import_localStore.createLocalStore)(key, { defaultExpiryMs: expiryMs });
77
+ return {
78
+ set: (key2, value) => {
79
+ if (IS_NOT_BROWSER) return;
80
+ adapter.set(key2, value);
81
+ },
82
+ get: (key2) => {
83
+ if (IS_NOT_BROWSER) return void 0;
84
+ const value = adapter.get(key2);
85
+ if (isValid && !isValid(value)) {
86
+ console.warn(
87
+ `localStorage adapter: Auto-discard invalid value for ${key2}: ${JSON.stringify(value)}`
88
+ );
89
+ adapter.delete(key2);
90
+ return void 0;
91
+ }
92
+ return value;
93
+ },
94
+ delete: (key2) => {
95
+ if (IS_NOT_BROWSER) return;
96
+ adapter.delete(key2);
97
+ },
98
+ clear: () => {
99
+ if (IS_NOT_BROWSER) return;
100
+ adapter.clear();
101
+ }
102
+ };
103
+ }
104
+ function kvStoreAdapter({
105
+ key,
106
+ expiryMs,
107
+ isValid
108
+ }) {
109
+ const keyPrefix = key + ":";
110
+ return {
111
+ set: async (key2, value) => {
112
+ if (IS_NOT_BROWSER) return;
113
+ await import_kvStore.kvStore.set(keyPrefix + key2, value, expiryMs);
114
+ },
115
+ get: async (key2) => {
116
+ if (IS_NOT_BROWSER) return void 0;
117
+ const fullKey = keyPrefix + key2;
118
+ const value = await import_kvStore.kvStore.get(fullKey);
119
+ if (value !== void 0 && isValid && !isValid(value)) {
120
+ console.warn(
121
+ `indexedDb adapter: Auto-discard invalid value for ${key2}:`,
122
+ value
123
+ );
124
+ await import_kvStore.kvStore.delete(fullKey);
125
+ return void 0;
126
+ }
127
+ return value;
128
+ },
129
+ delete: async (key2) => {
130
+ if (IS_NOT_BROWSER) return;
131
+ await import_kvStore.kvStore.delete(keyPrefix + key2);
132
+ },
133
+ clear: async () => {
134
+ if (IS_NOT_BROWSER) return;
135
+ await import_kvStore.kvStore.clear();
136
+ }
137
+ };
138
+ }
130
139
 
131
140
  // src/sharedState.ts
132
141
  var SharedStateConfig = {
@@ -141,41 +150,11 @@ var SharedStateConfig = {
141
150
  */
142
151
  lazyLoad: false
143
152
  };
144
- var PubSubStore = class {
145
- dataByKey = /* @__PURE__ */ new Map();
146
- subscribersByKey = /* @__PURE__ */ new Map();
147
- // This could be undefined if T includes undefined, else it should never
148
- // return undefined because the user has to always specify the default value.
149
- get(key) {
150
- return this.dataByKey.get(key);
151
- }
152
- set(key, value) {
153
- const prev = this.dataByKey.get(key);
154
- if (prev === value) return;
155
- this.dataByKey.set(key, value);
156
- this.notify(key, value, prev);
157
- }
158
- delete(key) {
159
- this.dataByKey.delete(key);
160
- }
161
- setNoNotify(key, value) {
162
- this.dataByKey.set(key, value);
163
- }
164
- // Subscribe to changes of a specific key
165
- subscribe(key, subscriber) {
166
- let subscribers = this.subscribersByKey.get(key);
167
- if (!subscribers) {
168
- subscribers = /* @__PURE__ */ new Set();
169
- this.subscribersByKey.set(key, subscribers);
170
- }
171
- subscribers.add(subscriber);
172
- return () => {
173
- subscribers.delete(subscriber);
174
- if (subscribers.size === 0) this.subscribersByKey.delete(key);
175
- };
176
- }
177
- notify(key, next, prev) {
178
- const subscribers = this.subscribersByKey.get(key);
153
+ function createPubSubStore() {
154
+ const dataByKey = /* @__PURE__ */ new Map();
155
+ const subscribersByKey = /* @__PURE__ */ new Map();
156
+ function notify(key, next, prev) {
157
+ const subscribers = subscribersByKey.get(key);
179
158
  if (!subscribers) return;
180
159
  const copy = Array.from(subscribers.values());
181
160
  for (const subscriber of copy) {
@@ -186,116 +165,139 @@ var PubSubStore = class {
186
165
  }
187
166
  }
188
167
  }
189
- };
190
- var pubSubStore = new PubSubStore();
168
+ return {
169
+ // This could be undefined if T includes undefined, else it should never
170
+ // return undefined because the user has to always specify the default value.
171
+ get(key) {
172
+ return dataByKey.get(key);
173
+ },
174
+ set(key, value) {
175
+ const prev = dataByKey.get(key);
176
+ if (prev === value) return;
177
+ dataByKey.set(key, value);
178
+ notify(key, value, prev);
179
+ },
180
+ delete(key) {
181
+ dataByKey.delete(key);
182
+ },
183
+ setNoNotify(key, value) {
184
+ dataByKey.set(key, value);
185
+ },
186
+ // Subscribe to changes of a specific key
187
+ subscribe(key, subscriber) {
188
+ let subscribers = subscribersByKey.get(key);
189
+ if (!subscribers) {
190
+ subscribers = /* @__PURE__ */ new Set();
191
+ subscribersByKey.set(key, subscribers);
192
+ }
193
+ subscribers.add(subscriber);
194
+ return () => {
195
+ subscribers.delete(subscriber);
196
+ if (subscribers.size === 0) subscribersByKey.delete(key);
197
+ };
198
+ }
199
+ };
200
+ }
201
+ var pubSubStore = createPubSubStore();
191
202
  var STORAGE_KEY = "state";
192
203
  var stateKey = 0;
193
- var SharedState = class {
194
- constructor(defaultValue, options) {
195
- this.defaultValue = defaultValue;
196
- let readyResolve = () => {
197
- };
198
- this.readyPromise = new Promise((resolve) => {
199
- readyResolve = resolve;
200
- });
201
- this.resolveReadyPromise = readyResolve;
202
- this.pubSubKey = String(stateKey++);
203
- this.storageAdapter = getStorageAdapter(
204
- options,
205
- SharedStateConfig.storeExpiryMs
206
- );
207
- this.setValueBounded = (next) => {
208
- this.setValue(next);
209
- };
210
- pubSubStore.setNoNotify(this.pubSubKey, defaultValue);
211
- const lazyLoad = options?.lazyLoad;
212
- const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateConfig.lazyLoad;
213
- if (!lazy) {
214
- void this.initDefaultValueOnce();
215
- }
216
- }
217
- pubSubKey;
218
- storageAdapter;
219
- setValueBounded;
220
- initStarted = false;
221
- initDone = false;
222
- // Allow users to await for the completion of data init.
223
- readyPromise;
224
- resolveReadyPromise;
225
- /** `subscribe` function required by useSyncExternalStore. */
226
- subscribe(subscriber) {
227
- return pubSubStore.subscribe(this.pubSubKey, () => subscriber());
228
- }
229
- /** Returns true if there is a backing store. */
230
- hasStore() {
231
- return Boolean(this.storageAdapter);
232
- }
233
- /**
234
- * Set a value directly into the store. Does nothing if there is no
235
- * backing store.
236
- *
237
- * Warning: If you want to set the value properly, you should use
238
- * `setValue()` instead which also updates the in-memory copy.
239
- */
240
- async setInStore(value) {
241
- return await this.storageAdapter?.set(STORAGE_KEY, value);
242
- }
243
- /**
244
- * Get a value directly from the store. Returns undefined if there is no
245
- * backing store.
246
- */
247
- async getFromStore() {
248
- return await this.storageAdapter?.get(STORAGE_KEY);
249
- }
250
- /**
251
- * Get the value of the shared state.
252
- * `getSnapshot` function — must return same ref if value unchanged.
253
- * Note that this would return the default value if data init from store is
254
- * not completed yet.
255
- */
256
- getSnapshot() {
257
- return pubSubStore.get(this.pubSubKey);
258
- }
259
- /** Set the value of the shared state. */
260
- setValue(next) {
261
- if (typeof next === "function") {
262
- const prev = pubSubStore.get(this.pubSubKey);
263
- next = next(prev);
204
+ function sharedState(defaultValue, options) {
205
+ let readyResolve = () => {
206
+ };
207
+ const readyPromise = new Promise((resolve) => {
208
+ readyResolve = resolve;
209
+ });
210
+ const resolveReadyPromise = readyResolve;
211
+ const pubSubKey = String(stateKey++);
212
+ const storageAdapter = getStorageAdapter(
213
+ options,
214
+ SharedStateConfig.storeExpiryMs
215
+ );
216
+ pubSubStore.setNoNotify(pubSubKey, defaultValue);
217
+ const lazyLoad = options?.lazyLoad;
218
+ const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateConfig.lazyLoad;
219
+ const obj = {
220
+ /** Allow users to await for the completion of data init. */
221
+ readyPromise,
222
+ lazy,
223
+ initStarted: false,
224
+ initDone: false,
225
+ /** `subscribe` function required by useSyncExternalStore. */
226
+ subscribe(subscriber) {
227
+ return pubSubStore.subscribe(pubSubKey, () => subscriber());
228
+ },
229
+ /** Returns true if there is a backing store. */
230
+ hasStore() {
231
+ return Boolean(storageAdapter);
232
+ },
233
+ /**
234
+ * Set a value directly into the store. Does nothing if there is no
235
+ * backing store.
236
+ *
237
+ * Warning: If you want to set the value properly, you should use
238
+ * `setValue()` instead which also updates the in-memory copy.
239
+ */
240
+ async setInStore(value) {
241
+ return await storageAdapter?.set(STORAGE_KEY, value);
242
+ },
243
+ /**
244
+ * Get a value directly from the store. Returns undefined if there is no
245
+ * backing store.
246
+ */
247
+ async getFromStore() {
248
+ return await storageAdapter?.get(STORAGE_KEY);
249
+ },
250
+ /**
251
+ * Get the value of the shared state.
252
+ * `getSnapshot` function — must return same ref if value unchanged.
253
+ * Note that this would return the default value if data init from store is
254
+ * not completed yet.
255
+ */
256
+ getSnapshot() {
257
+ return pubSubStore.get(pubSubKey);
258
+ },
259
+ /** Set the value of the shared state. */
260
+ setValue(next) {
261
+ if (typeof next === "function") {
262
+ const prev = pubSubStore.get(pubSubKey);
263
+ next = next(prev);
264
+ }
265
+ pubSubStore.set(pubSubKey, next);
266
+ void obj.setInStore(next);
267
+ },
268
+ /**
269
+ * Remove the value of the shared state. Actually this just sets the value
270
+ * back to the given default value.
271
+ */
272
+ delete() {
273
+ pubSubStore.set(pubSubKey, defaultValue);
274
+ void storageAdapter?.set(STORAGE_KEY, defaultValue);
275
+ },
276
+ /** Initial default handling. */
277
+ async initDefaultValueOnce() {
278
+ if (obj.initDone || obj.initStarted) return;
279
+ obj.initStarted = true;
280
+ const stored = await obj.getFromStore();
281
+ const value = stored !== void 0 ? stored : defaultValue;
282
+ pubSubStore.set(pubSubKey, value);
283
+ obj.initDone = true;
284
+ resolveReadyPromise();
264
285
  }
265
- pubSubStore.set(this.pubSubKey, next);
266
- void this.setInStore(next);
286
+ };
287
+ if (!lazy) {
288
+ void obj.initDefaultValueOnce();
267
289
  }
268
- /**
269
- * Remove the value of the shared state. Actually this just sets the value
270
- * back to the given default value.
271
- */
272
- delete() {
273
- pubSubStore.set(this.pubSubKey, this.defaultValue);
274
- void this.storageAdapter?.set(STORAGE_KEY, this.defaultValue);
275
- }
276
- /** Initial default handling. */
277
- async initDefaultValueOnce() {
278
- if (this.initDone || this.initStarted) return;
279
- this.initStarted = true;
280
- const stored = await this.getFromStore();
281
- const value = stored !== void 0 ? stored : this.defaultValue;
282
- pubSubStore.set(this.pubSubKey, value);
283
- this.initDone = true;
284
- this.resolveReadyPromise();
285
- }
286
- };
287
- function sharedState(defaultValue, options) {
288
- return new SharedState(defaultValue, options);
290
+ return obj;
289
291
  }
290
292
  function useSharedState(state) {
291
- state.initDefaultValueOnce();
293
+ void state.initDefaultValueOnce();
292
294
  const value = (0, import_react.useSyncExternalStore)(
293
295
  (onStoreChange) => state.subscribe(onStoreChange),
294
296
  () => state.getSnapshot(),
295
297
  // getServerSnapshot is needed to prevent SSR dev errors in nextjs.
296
298
  () => state.getSnapshot()
297
299
  );
298
- return [value, state.setValueBounded];
300
+ return [value, state.setValue];
299
301
  }
300
302
 
301
303
  // src/stringify.ts
@@ -335,7 +337,7 @@ var import_react2 = require("react");
335
337
  function useDeepMemo(value) {
336
338
  const [state, setState] = (0, import_react2.useState)(value);
337
339
  return (0, import_react2.useMemo)(() => {
338
- if (state === value || (0, import_isEqual.default)(state, value)) {
340
+ if ((0, import_isEqual.default)(state, value)) {
339
341
  return state;
340
342
  }
341
343
  setState(value);
@@ -363,282 +365,104 @@ var SharedQueryConfig = {
363
365
  function configureSharedQuery(config) {
364
366
  Object.assign(SharedQueryConfig, config);
365
367
  }
366
- var SharedQuery = class {
367
- inflightQueries = /* @__PURE__ */ new Map();
368
- queryState;
369
- /**
370
- * Keep track of the mounted query keys. This handles the case where an
371
- * earlier-started query returns later, and when it returns, it was already
372
- * unmounted, thus making it eligible for clean up right after fetch.
373
- */
374
- mountedKeys = /* @__PURE__ */ new Map();
375
- queryName;
376
- queryFn;
377
- refetchFn;
378
- expiryMs;
379
- staleMs;
380
- refetchOnStale;
381
- maxSize;
382
- maxBytes;
383
- keepLastNonEmptyData;
384
- log;
385
- constructor(options) {
386
- this.queryName = options.queryName;
387
- this.queryFn = options.queryFn;
388
- this.refetchFn = options.refetchFn;
389
- this.staleMs = options?.staleMs ?? SharedQueryConfig.staleMs;
390
- this.expiryMs = options?.expiryMs ?? SharedQueryConfig.expiryMs;
391
- this.refetchOnStale = options?.refetchOnStale ?? SharedQueryConfig.refetchOnStale;
392
- if (!options.store) {
393
- const persistTo = options.persistTo ?? SharedQueryConfig.persistTo;
394
- if (persistTo) {
395
- options.store = {
368
+ var seenQueryNames = /* @__PURE__ */ new Set();
369
+ function sharedQuery(options) {
370
+ if (seenQueryNames.has(options.queryName)) {
371
+ throw new Error(`Duplicate shared query name "${options.queryName}"`);
372
+ }
373
+ seenQueryNames.add(options.queryName);
374
+ const inflightQueries = /* @__PURE__ */ new Map();
375
+ const mountedKeys = /* @__PURE__ */ new Map();
376
+ const queryName = options.queryName;
377
+ const queryFn = options.queryFn;
378
+ const refetchFn = options.refetchFn;
379
+ const staleMs = options?.staleMs ?? SharedQueryConfig.staleMs;
380
+ const expiryMs = options?.expiryMs ?? SharedQueryConfig.expiryMs;
381
+ const refetchOnStale = options?.refetchOnStale ?? SharedQueryConfig.refetchOnStale;
382
+ const store = options.store;
383
+ if (store) {
384
+ if ("persistTo" in store && store.isValid) {
385
+ const isValid = store.isValid;
386
+ options = {
387
+ ...options,
388
+ store: {
389
+ ...store,
390
+ isValid: (u) => {
391
+ if (!u || typeof u !== "object") {
392
+ return false;
393
+ }
394
+ for (const v of Object.values(u)) {
395
+ if (!v || typeof v !== "object" || !("data" in v) || !("dataUpdatedMs" in v) || !("lastUpdatedMs" in v) || typeof v.dataUpdatedMs !== "number" || typeof v.lastUpdatedMs !== "number" || !isValid(v.data)) {
396
+ return false;
397
+ }
398
+ }
399
+ return true;
400
+ }
401
+ }
402
+ };
403
+ }
404
+ } else {
405
+ const persistTo = options.persistTo ?? SharedQueryConfig.persistTo;
406
+ if (persistTo) {
407
+ options = {
408
+ ...options,
409
+ store: {
396
410
  persistTo,
397
- key: options.queryName,
398
- expiryMs: this.expiryMs
399
- };
400
- }
411
+ key: queryName,
412
+ expiryMs
413
+ }
414
+ };
401
415
  }
402
- this.maxSize = options?.maxSize ?? SharedQueryConfig.maxSize;
403
- this.maxBytes = options?.maxBytes ?? SharedQueryConfig.maxBytes;
404
- this.keepLastNonEmptyData = Boolean(options?.keepLastNonEmptyData);
405
- this.log = options?.log ?? SharedQueryConfig.log;
406
- this.queryState = sharedState(
407
- {},
408
- // defaultValue
409
- options
410
- );
411
416
  }
412
- /** Returns a key to identify requests based on the given args. */
413
- getQueryKey(args) {
414
- return this.queryName + ":" + stringifyDeterministicForKeys(args);
415
- }
416
- /**
417
- * Get the AbortController to abort the inflight query (if any).
418
- *
419
- * Note that once you get the controller, you can also get the signal easily
420
- * using `controller.signal`. So if you need both the controller and the
421
- * signal, just use this function.
422
- */
423
- getAbortController(args) {
424
- const queryKey = this.getQueryKey(args);
425
- return this.getAbortControllerByKey(queryKey);
426
- }
427
- getAbortControllerByKey(queryKey) {
428
- return this.inflightQueries.get(queryKey)?.abortController ?? null;
429
- }
430
- /**
431
- * Get the AbortSignal to check for query abortions.
432
- *
433
- * Example:
434
- * ```
435
- * const getUserQuery = sharedQuery({
436
- * queryName: "getUser",
437
- * queryFn: (userId: string) => {
438
- * // Get the signal for this current execution.
439
- * const signal = getUserQuery.getAbortSignal([userId]);
440
- *
441
- * // Pass the signal to fetch so that it can be aborted.
442
- * const response = await fetch(`/users/${userId}`, { signal });
443
- *
444
- * // Check for errors.
445
- * if (!response.ok) {
446
- * throw new Error(response.statusText);
447
- * }
448
- *
449
- * // Return the data.
450
- * return await response.json();
451
- * },
452
- * });
453
- * ```
454
- */
455
- getAbortSignal(args) {
456
- const queryKey = this.getQueryKey(args);
457
- return this.getAbortSignalByKey(queryKey);
458
- }
459
- getAbortSignalByKey(queryKey) {
460
- return this.getAbortControllerByKey(queryKey)?.signal ?? null;
461
- }
462
- isStale(dataUpdatedMs) {
463
- if (this.staleMs === 0) return true;
417
+ const maxSize = options?.maxSize ?? SharedQueryConfig.maxSize;
418
+ const maxBytes = options?.maxBytes ?? SharedQueryConfig.maxBytes;
419
+ const keepLastNonEmptyData = Boolean(options?.keepLastNonEmptyData);
420
+ const log = options?.log ?? SharedQueryConfig.log;
421
+ const queryState = sharedState(
422
+ {},
423
+ // defaultValue
424
+ options
425
+ );
426
+ function isStale(dataUpdatedMs) {
427
+ if (staleMs === 0) return true;
464
428
  const ageMs = Date.now() - dataUpdatedMs;
465
- return ageMs > this.staleMs;
429
+ return ageMs > staleMs;
466
430
  }
467
- isExpired(dataUpdatedMs) {
468
- if (this.expiryMs === 0) return false;
431
+ function isExpired(dataUpdatedMs) {
432
+ if (expiryMs === 0) return false;
469
433
  const ageMs = Date.now() - dataUpdatedMs;
470
- return ageMs > this.expiryMs;
434
+ return ageMs > expiryMs;
471
435
  }
472
- /** For cached data, we need to return the correct timestamps as well. */
473
- async getCachedOrFetch(...args) {
474
- const startMs = Date.now();
475
- await this.queryState.readyPromise;
476
- const waitedMs = Date.now() - startMs;
477
- const queryKey = this.getQueryKey(args);
478
- const cached = this.queryState.getSnapshot()?.[queryKey];
479
- if (waitedMs) {
480
- this.log(`${queryKey}: Waited for ${waitedMs}ms before data is ready`);
481
- }
482
- if (cached?.data && !this.isExpired(cached.dataUpdatedMs ?? 0)) {
483
- if (!this.isStale(cached.dataUpdatedMs ?? 0)) {
484
- this.log(`Return fresh data ${queryKey} from cache without fetching`);
485
- return cached;
486
- }
487
- if (this.refetchOnStale) {
488
- this.log(`refetchOnStale for ${queryKey}`);
489
- void this.dedupedFetch(queryKey, "query", ...args);
490
- }
491
- this.log(`Returning stale data for ${queryKey}`);
492
- return cached;
493
- }
494
- const data = await this.dedupedFetch(queryKey, "query", ...args);
495
- const now = Date.now();
496
- return {
497
- // Don't keep old error.
498
- lastUpdatedMs: now,
499
- loading: false,
500
- data,
501
- dataUpdatedMs: now
502
- };
503
- }
504
- dedupedFetch(queryKey, queryType, ...args) {
505
- const inflightQuery = this.inflightQueries.get(queryKey);
506
- if (inflightQuery) {
507
- this.log(
508
- `Deduplicating inflight fetch for ${queryKey}, queryType=${queryType}`
509
- );
510
- return inflightQuery.promise;
511
- }
512
- const promise = this.startFetching(queryKey, queryType, args);
513
- const abortController = new AbortController();
514
- this.inflightQueries.set(queryKey, { promise, abortController });
515
- return promise;
516
- }
517
- async startFetching(queryKey, queryType, args) {
518
- this.log(`Start fetching ${queryKey}, queryType=${queryType}`);
519
- try {
520
- const data = await (queryType === "refetch" && this.refetchFn ? this.refetchFn(...args) : this.queryFn(...args));
521
- this.log(`Successfully fetched ${queryKey}`);
522
- this.setData(queryKey, data, Date.now());
523
- return data;
524
- } catch (e) {
525
- this.log(`Failed to fetch ${queryKey} for queryType=${queryType}: ${e}`);
526
- throw e;
527
- } finally {
528
- this.inflightQueries.delete(queryKey);
529
- }
530
- }
531
- /** Get the entire stored entry for a query key. */
532
- getQueryValue(queryKey) {
533
- return this.queryState.getSnapshot()?.[queryKey];
534
- }
535
- /** Get the current stored data for a query key. */
536
- getData(queryKey) {
537
- const stateValue = this.queryState.getSnapshot()?.[queryKey];
538
- if (stateValue?.lastUpdatedMs) {
539
- if (this.isExpired(stateValue.lastUpdatedMs)) {
540
- this.deleteData(queryKey);
541
- return void 0;
542
- }
543
- }
544
- return stateValue?.data;
545
- }
546
- /** Set the data directly if the user obtained it from somewhere else. */
547
- setData(queryKey, data, dataUpdatedMs = Date.now()) {
548
- const entry = {
549
- lastUpdatedMs: Date.now(),
550
- loading: false,
551
- data,
552
- dataUpdatedMs
553
- };
554
- this.queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));
555
- this.enforceSizeLimit();
556
- }
557
- /**
558
- * Do a new fetch even when the data is already cached, but don't fetch if
559
- * another fetch is already inflight.
560
- */
561
- async updateFromSource(...args) {
562
- const key = this.getQueryKey(args);
563
- return await this.dedupedFetch(key, "query", ...args);
564
- }
565
- /**
566
- * Perform an explicit refetch. If `options.refetchFn` was not specified,
567
- * then this would be identical to calling `this.updateFromSource()`.
568
- */
569
- async refetch(...args) {
570
- const key = this.getQueryKey(args);
571
- return await this.dedupedFetch(key, "refetch", ...args);
572
- }
573
- /** Delete the cached data for one set of arguments. */
574
- deleteData(queryKey) {
575
- const record = this.queryState.getSnapshot();
576
- const clone = { ...record };
577
- delete clone[queryKey];
578
- this.queryState.setValue(clone);
579
- const inflight = this.inflightQueries.get(queryKey);
580
- if (inflight) {
581
- this.inflightQueries.delete(queryKey);
582
- inflight.abortController.abort("Deleted");
583
- }
584
- }
585
- /** Delete all currently cached data & all inflight promises. */
586
- clear() {
587
- this.queryState.delete();
588
- for (const { abortController } of Array.from(
589
- this.inflightQueries.values()
590
- )) {
591
- abortController.abort("Cleared");
592
- }
593
- this.inflightQueries.clear();
594
- }
595
- /** Keep track of mounted keys. */
596
- mount(queryKey) {
597
- this.mountedKeys.set(queryKey, (this.mountedKeys.get(queryKey) ?? 0) + 1);
598
- }
599
- /** Clean up mounted keys. */
600
- unmount(queryKey) {
601
- const count = this.mountedKeys.get(queryKey);
602
- if (count === 1) {
603
- this.mountedKeys.delete(queryKey);
604
- } else {
605
- this.mountedKeys.set(queryKey, (count ?? 0) - 1);
606
- }
607
- }
608
- /**
609
- * Discard the oldest entries if the size exceeds the limit. The last
610
- * remaining record cannot be deleted no matter what the limit is.
611
- */
612
- enforceSizeLimit() {
613
- if (!this.maxSize && !this.maxBytes) {
436
+ function enforceSizeLimit() {
437
+ if (!maxSize && !maxBytes) {
614
438
  return;
615
439
  }
616
- const oldState = this.queryState.getSnapshot();
440
+ const oldState = queryState.getSnapshot();
617
441
  const oldSize = Object.keys(oldState).length;
618
442
  if (oldSize <= 1) {
619
443
  return;
620
444
  }
621
445
  const oldByteSize = (0, import_object_sizeof.default)(oldState);
622
- let numToCut = this.maxSize ? oldSize - this.maxSize : 0;
623
- let bytesToCut = this.maxBytes ? oldByteSize - this.maxBytes : 0;
446
+ let numToCut = maxSize ? oldSize - maxSize : 0;
447
+ let bytesToCut = maxBytes ? oldByteSize - maxBytes : 0;
624
448
  if (numToCut <= 0 && bytesToCut <= 0) {
625
- this.log(
626
- `No need to trim data for ${this.queryName}: size=${oldSize} (limit=${this.maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`
449
+ log(
450
+ `No need to trim data for ${queryName}: size=${oldSize} (limit=${maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${maxBytes.toLocaleString()})`
627
451
  );
628
452
  return;
629
453
  }
630
- this.log(
631
- `Need to trim ${this.queryName}: numToCut=${numToCut}, bytesToCut=${bytesToCut}`
454
+ log(
455
+ `Need to trim ${queryName}: numToCut=${numToCut}, bytesToCut=${bytesToCut}`
632
456
  );
633
457
  const newState = { ...oldState };
634
458
  let needUpdate = false;
635
459
  const deleteKeyIfNotMounted = (key, expired) => {
636
- if (this.mountedKeys.has(key)) {
637
- this.log(`Cannot clean up ${key} as it is mounted`);
460
+ if (mountedKeys.has(key)) {
461
+ log(`Cannot clean up ${key} as it is mounted`);
638
462
  return;
639
463
  }
640
464
  const byteSize = (0, import_object_sizeof.default)(key) + (0, import_object_sizeof.default)(newState[key]);
641
- this.log(
465
+ log(
642
466
  `Cleaning up unmounted${expired ? ", expired" : ""} ${key} (byteSize=${byteSize.toLocaleString()})`
643
467
  );
644
468
  numToCut--;
@@ -650,7 +474,7 @@ var SharedQuery = class {
650
474
  (entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs
651
475
  );
652
476
  for (const [key] of entriesByTimeAscending) {
653
- if (!this.isExpired(newState[key].lastUpdatedMs)) {
477
+ if (!isExpired(newState[key].lastUpdatedMs)) {
654
478
  break;
655
479
  }
656
480
  deleteKeyIfNotMounted(key, true);
@@ -669,24 +493,220 @@ var SharedQuery = class {
669
493
  }
670
494
  }
671
495
  if (!needUpdate) {
672
- this.log(
673
- `No trimmable entries found for ${this.queryName}: size=${oldSize} (limit=${this.maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`
496
+ log(
497
+ `No trimmable entries found for ${queryName}: size=${oldSize} (limit=${maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${maxBytes.toLocaleString()})`
674
498
  );
675
499
  return;
676
500
  }
677
- this.queryState.setValue(newState);
678
- this.log(
679
- `Trimmed data for ${this.queryName}: size=[${oldSize} -> ${Object.keys(newState).length}] (limit=${this.maxSize}), byteSize=[${oldByteSize.toLocaleString()} -> ${(0, import_object_sizeof.default)(newState).toLocaleString()}] (limit=${this.maxBytes.toLocaleString()})`
501
+ queryState.setValue(newState);
502
+ log(
503
+ `Trimmed data for ${queryName}: size=[${oldSize} -> ${Object.keys(newState).length}] (limit=${maxSize}), byteSize=[${oldByteSize.toLocaleString()} -> ${(0, import_object_sizeof.default)(newState).toLocaleString()}] (limit=${maxBytes.toLocaleString()})`
680
504
  );
681
505
  }
682
- };
683
- var seenQueryNames = /* @__PURE__ */ new Set();
684
- function sharedQuery(options) {
685
- if (seenQueryNames.has(options.queryName)) {
686
- throw new Error(`Duplicate shared query "${options.queryName}"`);
506
+ function setData(queryKey, data, dataUpdatedMs = Date.now()) {
507
+ const entry = {
508
+ lastUpdatedMs: Date.now(),
509
+ loading: false,
510
+ data,
511
+ dataUpdatedMs
512
+ };
513
+ queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));
514
+ enforceSizeLimit();
687
515
  }
688
- seenQueryNames.add(options.queryName);
689
- return new SharedQuery(options);
516
+ async function startFetching(queryKey, queryType, args) {
517
+ log(`Start fetching ${queryKey}, queryType=${queryType}`);
518
+ try {
519
+ const data = await (queryType === "refetch" && refetchFn ? refetchFn(...args) : queryFn(...args));
520
+ log(`Successfully fetched ${queryKey}`);
521
+ setData(queryKey, data, Date.now());
522
+ return data;
523
+ } catch (e) {
524
+ log(`Failed to fetch ${queryKey} for queryType=${queryType}: ${e}`);
525
+ throw e;
526
+ } finally {
527
+ inflightQueries.delete(queryKey);
528
+ }
529
+ }
530
+ function dedupedFetch(queryKey, queryType, ...args) {
531
+ const inflightQuery = inflightQueries.get(queryKey);
532
+ if (inflightQuery) {
533
+ log(
534
+ `Deduplicating inflight fetch for ${queryKey}, queryType=${queryType}`
535
+ );
536
+ return inflightQuery.promise;
537
+ }
538
+ const promise = startFetching(queryKey, queryType, args);
539
+ const abortController = new AbortController();
540
+ inflightQueries.set(queryKey, { promise, abortController });
541
+ return promise;
542
+ }
543
+ const obj = {
544
+ queryName,
545
+ queryFn,
546
+ refetchFn,
547
+ expiryMs,
548
+ staleMs,
549
+ refetchOnStale,
550
+ maxSize,
551
+ maxBytes,
552
+ keepLastNonEmptyData,
553
+ log,
554
+ queryState,
555
+ /** Returns a key to identify requests based on the given args. */
556
+ getQueryKey(args) {
557
+ return queryName + ":" + stringifyDeterministicForKeys(args);
558
+ },
559
+ /**
560
+ * Get the AbortController to abort the inflight query (if any).
561
+ *
562
+ * Note that once you get the controller, you can also get the signal easily
563
+ * using `controller.signal`. So if you need both the controller and the
564
+ * signal, just use this function.
565
+ */
566
+ getAbortController(args) {
567
+ const queryKey = obj.getQueryKey(args);
568
+ return obj.getAbortControllerByKey(queryKey);
569
+ },
570
+ getAbortControllerByKey(queryKey) {
571
+ return inflightQueries.get(queryKey)?.abortController ?? null;
572
+ },
573
+ /**
574
+ * Get the AbortSignal to check for query abortions.
575
+ *
576
+ * Example:
577
+ * ```
578
+ * const getUserQuery = sharedQuery({
579
+ * queryName: "getUser",
580
+ * queryFn: (userId: string) => {
581
+ * // Get the signal for this current execution.
582
+ * const signal = getUserQuery.getAbortSignal([userId]);
583
+ *
584
+ * // Pass the signal to fetch so that it can be aborted.
585
+ * const response = await fetch(`/users/${userId}`, { signal });
586
+ *
587
+ * // Check for errors.
588
+ * if (!response.ok) {
589
+ * throw new Error(response.statusText);
590
+ * }
591
+ *
592
+ * // Return the data.
593
+ * return await response.json();
594
+ * },
595
+ * });
596
+ * ```
597
+ */
598
+ getAbortSignal(args) {
599
+ const queryKey = obj.getQueryKey(args);
600
+ return obj.getAbortSignalByKey(queryKey);
601
+ },
602
+ getAbortSignalByKey(queryKey) {
603
+ return obj.getAbortControllerByKey(queryKey)?.signal ?? null;
604
+ },
605
+ /** For cached data, we need to return the correct timestamps as well. */
606
+ async getCachedOrFetch(...args) {
607
+ const startMs = Date.now();
608
+ await queryState.readyPromise;
609
+ const waitedMs = Date.now() - startMs;
610
+ const queryKey = obj.getQueryKey(args);
611
+ const cached = queryState.getSnapshot()?.[queryKey];
612
+ if (waitedMs) {
613
+ log(`${queryKey}: Waited for ${waitedMs}ms before data is ready`);
614
+ }
615
+ if (cached?.data && !isExpired(cached.dataUpdatedMs ?? 0)) {
616
+ if (!isStale(cached.dataUpdatedMs ?? 0)) {
617
+ log(`Return fresh data ${queryKey} from cache without fetching`);
618
+ return cached;
619
+ }
620
+ if (refetchOnStale) {
621
+ log(`refetchOnStale for ${queryKey}`);
622
+ void dedupedFetch(queryKey, "query", ...args);
623
+ }
624
+ log(`Returning stale data for ${queryKey}`);
625
+ return cached;
626
+ }
627
+ const data = await dedupedFetch(queryKey, "query", ...args);
628
+ const now = Date.now();
629
+ return {
630
+ // Don't keep old error.
631
+ lastUpdatedMs: now,
632
+ loading: false,
633
+ data,
634
+ dataUpdatedMs: now
635
+ };
636
+ },
637
+ /** Get the entire stored entry for a query key. */
638
+ getQueryValue(queryKey) {
639
+ return queryState.getSnapshot()?.[queryKey];
640
+ },
641
+ /** Get the current stored data for a query key. */
642
+ getData(queryKey) {
643
+ const stateValue = queryState.getSnapshot()?.[queryKey];
644
+ if (stateValue?.lastUpdatedMs) {
645
+ if (isExpired(stateValue.lastUpdatedMs)) {
646
+ obj.deleteData(queryKey);
647
+ return void 0;
648
+ }
649
+ }
650
+ return stateValue?.data;
651
+ },
652
+ /** Set the data directly if the user obtained it from somewhere else. */
653
+ setData,
654
+ /**
655
+ * Do a new fetch even when the data is already cached, but don't fetch if
656
+ * another fetch is already inflight.
657
+ */
658
+ async updateFromSource(...args) {
659
+ const key = obj.getQueryKey(args);
660
+ return await dedupedFetch(key, "query", ...args);
661
+ },
662
+ /**
663
+ * Perform an explicit refetch. If `options.refetchFn` was not specified,
664
+ * then this would be identical to calling `obj.updateFromSource()`.
665
+ */
666
+ async refetch(...args) {
667
+ const key = obj.getQueryKey(args);
668
+ return await dedupedFetch(key, "refetch", ...args);
669
+ },
670
+ /** Delete the cached data for one set of arguments. */
671
+ deleteData(queryKey) {
672
+ const record = queryState.getSnapshot();
673
+ const clone = { ...record };
674
+ delete clone[queryKey];
675
+ queryState.setValue(clone);
676
+ const inflight = inflightQueries.get(queryKey);
677
+ if (inflight) {
678
+ inflightQueries.delete(queryKey);
679
+ inflight.abortController.abort("Deleted");
680
+ }
681
+ },
682
+ /** Delete all currently cached data & all inflight promises. */
683
+ clear() {
684
+ queryState.delete();
685
+ for (const { abortController } of Array.from(inflightQueries.values())) {
686
+ abortController.abort("Cleared");
687
+ }
688
+ inflightQueries.clear();
689
+ },
690
+ /** Keep track of mounted keys. */
691
+ mount(queryKey) {
692
+ mountedKeys.set(queryKey, (mountedKeys.get(queryKey) ?? 0) + 1);
693
+ },
694
+ /** Clean up mounted keys. */
695
+ unmount(queryKey) {
696
+ const count = mountedKeys.get(queryKey);
697
+ if (count === 1) {
698
+ mountedKeys.delete(queryKey);
699
+ } else {
700
+ mountedKeys.set(queryKey, (count ?? 0) - 1);
701
+ }
702
+ },
703
+ /**
704
+ * Discard the oldest entries if the size exceeds the limit. The last
705
+ * remaining record cannot be deleted no matter what the limit is.
706
+ */
707
+ enforceSizeLimit
708
+ };
709
+ return obj;
690
710
  }
691
711
  var DEFAULT_QUERY_STATE_ENTRY = {
692
712
  lastUpdatedMs: 0,
@@ -783,39 +803,19 @@ function useSharedQuery(query, ...args) {
783
803
  return Boolean(controller);
784
804
  },
785
805
  refetch: () => {
786
- void execute(true);
806
+ return execute(true);
787
807
  },
788
808
  setData: (data, dataUpdatedMs) => {
789
- const now = Date.now();
790
- setQueryStateValue((prev) => ({
791
- ...prev,
792
- data,
793
- dataUpdatedMs: dataUpdatedMs ?? now,
794
- lastUpdatedMs: now,
795
- loading: false
796
- }));
809
+ query.setData(queryKey, data, dataUpdatedMs);
797
810
  },
798
811
  deleteData: () => {
799
- setQueryState((prev) => {
800
- const clone = { ...prev };
801
- delete clone[queryKey];
802
- return clone;
803
- });
812
+ query.deleteData(queryKey);
804
813
  }
805
814
  };
806
- }, [
807
- execute,
808
- lastNonEmptyData,
809
- query,
810
- queryKey,
811
- queryStateValue,
812
- setQueryState,
813
- setQueryStateValue
814
- ]);
815
+ }, [execute, lastNonEmptyData, query, queryKey, queryStateValue]);
815
816
  }
816
817
  // Annotate the CommonJS export names for ESM import in node:
817
818
  0 && (module.exports = {
818
- SharedQuery,
819
819
  SharedQueryConfig,
820
820
  configureSharedQuery,
821
821
  sharedQuery,