@choksheak/react-utils 0.3.1 → 0.3.3

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@choksheak/react-utils",
3
3
  "license": "The Unlicense",
4
- "version": "0.3.1",
4
+ "version": "0.3.3",
5
5
  "description": "Miscellaneous React+TypeScript utilities with support for full tree-shaking.",
6
6
  "private": false,
7
7
  "files": [
@@ -95,7 +95,7 @@
95
95
  "wretch": "^3.0.2"
96
96
  },
97
97
  "peerDependencies": {
98
- "@choksheak/ts-utils": ">=0.3.1",
98
+ "@choksheak/ts-utils": ">=0.3.2",
99
99
  "lodash": ">=4.17.21",
100
100
  "object-sizeof": ">=2.6.5",
101
101
  "react": ">=16",
package/sharedQuery.cjs CHANGED
@@ -48,6 +48,7 @@ var import_react = require("react");
48
48
  // src/storage.ts
49
49
  var import_kvStore = require("@choksheak/ts-utils/kvStore");
50
50
  var import_localStore = require("@choksheak/ts-utils/localStore");
51
+ var IS_NOT_BROWSER = typeof window === "undefined";
51
52
  function getStorageAdapter(options, defaultStoreExpiryMs) {
52
53
  const { store } = options ?? {};
53
54
  if (!store) {
@@ -55,6 +56,7 @@ function getStorageAdapter(options, defaultStoreExpiryMs) {
55
56
  }
56
57
  if ("persistTo" in store && typeof store.persistTo === "string" && "key" in store && typeof store.key === "string") {
57
58
  const expiryMs = store.expiryMs || defaultStoreExpiryMs;
59
+ const isValid = store.isValid;
58
60
  switch (store.persistTo) {
59
61
  case "localStorage": {
60
62
  const adapter = new import_localStore.LocalStore(store.key, {
@@ -62,40 +64,58 @@ function getStorageAdapter(options, defaultStoreExpiryMs) {
62
64
  });
63
65
  return {
64
66
  set: (key, value) => {
65
- if (typeof window === "undefined") return;
67
+ if (IS_NOT_BROWSER) return;
66
68
  adapter.set(key, value);
67
69
  },
68
70
  get: (key) => {
69
- if (typeof window === "undefined") return void 0;
70
- return adapter.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;
71
81
  },
72
82
  delete: (key) => {
73
- if (typeof window === "undefined") return;
83
+ if (IS_NOT_BROWSER) return;
74
84
  adapter.delete(key);
75
85
  },
76
86
  clear: () => {
77
- if (typeof window === "undefined") return;
87
+ if (IS_NOT_BROWSER) return;
78
88
  adapter.clear();
79
89
  }
80
90
  };
81
91
  }
82
92
  case "indexedDb": {
83
93
  const keyPrefix = store.key + ":";
94
+ const isValid2 = store.isValid;
84
95
  return {
85
96
  set: async (key, value) => {
86
- if (typeof window === "undefined") return;
97
+ if (IS_NOT_BROWSER) return;
87
98
  await import_kvStore.kvStore.set(keyPrefix + key, value, expiryMs);
88
99
  },
89
100
  get: async (key) => {
90
- if (typeof window === "undefined") return void 0;
91
- return await import_kvStore.kvStore.get(keyPrefix + 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;
92
112
  },
93
113
  delete: async (key) => {
94
- if (typeof window === "undefined") return;
114
+ if (IS_NOT_BROWSER) return;
95
115
  await import_kvStore.kvStore.delete(keyPrefix + key);
96
116
  },
97
117
  clear: async () => {
98
- if (typeof window === "undefined") return;
118
+ if (IS_NOT_BROWSER) return;
99
119
  await import_kvStore.kvStore.clear();
100
120
  }
101
121
  };
@@ -193,39 +213,65 @@ var SharedState = class {
193
213
  setValueBounded;
194
214
  initStarted = false;
195
215
  initDone = false;
196
- // `subscribe` function required by useSyncExternalStore
216
+ /** `subscribe` function required by useSyncExternalStore. */
197
217
  subscribe(subscriber) {
198
218
  return pubSubStore.subscribe(this.pubSubKey, () => subscriber());
199
219
  }
200
- // Get the value of the shared state.
201
- // `getSnapshot` function — must return same ref if value unchanged
220
+ /** Returns true if there is a backing store. */
221
+ hasStore() {
222
+ return Boolean(this.storageAdapter);
223
+ }
224
+ /**
225
+ * Set a value directly into the store. Does nothing if there is no
226
+ * backing store.
227
+ *
228
+ * Warning: If you want to set the value properly, you should use
229
+ * `setValue()` instead which also updates the in-memory copy.
230
+ */
231
+ async setInStore(value) {
232
+ return await this.storageAdapter?.set(STORAGE_KEY, value);
233
+ }
234
+ /**
235
+ * Get a value directly from the store. Returns undefined if there is no
236
+ * backing store.
237
+ */
238
+ async getFromStore() {
239
+ return await this.storageAdapter?.get(STORAGE_KEY);
240
+ }
241
+ /**
242
+ * Get the value of the shared state.
243
+ * `getSnapshot` function — must return same ref if value unchanged.
244
+ * Note that this would return the default value if data init from store is
245
+ * not completed yet.
246
+ */
202
247
  getSnapshot() {
203
248
  return pubSubStore.get(this.pubSubKey);
204
249
  }
205
- // Set the value of the shared state.
250
+ /** Set the value of the shared state. */
206
251
  setValue(next) {
207
252
  if (typeof next === "function") {
208
253
  const prev = pubSubStore.get(this.pubSubKey);
209
254
  next = next(prev);
210
255
  }
211
256
  pubSubStore.set(this.pubSubKey, next);
212
- void this.storageAdapter?.set(STORAGE_KEY, next);
257
+ void this.setInStore(next);
213
258
  }
214
- // Remove the value of the shared state. Actually this just sets the value
215
- // back to the given default value.
259
+ /**
260
+ * Remove the value of the shared state. Actually this just sets the value
261
+ * back to the given default value.
262
+ */
216
263
  delete() {
217
264
  pubSubStore.set(this.pubSubKey, this.defaultValue);
218
265
  void this.storageAdapter?.set(STORAGE_KEY, this.defaultValue);
219
266
  }
220
- // Initial default handling
267
+ /** Initial default handling. */
221
268
  async initDefaultValueOnce() {
222
269
  if (this.initDone || this.initStarted) return;
223
270
  this.initStarted = true;
224
- const v = await this.storageAdapter?.get(STORAGE_KEY);
225
- const value = v !== void 0 ? v : this.defaultValue;
271
+ const stored = await this.getFromStore();
272
+ const value = stored !== void 0 ? stored : this.defaultValue;
226
273
  pubSubStore.set(this.pubSubKey, value);
227
274
  this.initDone = true;
228
- pubSubStore.set(this.pubSubKey, value);
229
275
  }
230
276
  };
231
277
  function sharedState(defaultValue, options) {
@@ -318,6 +364,7 @@ var SharedQuery = class {
318
364
  mountedKeys = /* @__PURE__ */ new Map();
319
365
  queryName;
320
366
  queryFn;
367
+ refetchFn;
321
368
  expiryMs;
322
369
  staleMs;
323
370
  refetchOnStale;
@@ -328,6 +375,7 @@ var SharedQuery = class {
328
375
  constructor(options) {
329
376
  this.queryName = options.queryName;
330
377
  this.queryFn = options.queryFn;
378
+ this.refetchFn = options.refetchFn;
331
379
  this.staleMs = options?.staleMs ?? SharedQueryConfig.staleMs;
332
380
  this.expiryMs = options?.expiryMs ?? SharedQueryConfig.expiryMs;
333
381
  this.refetchOnStale = options?.refetchOnStale ?? SharedQueryConfig.refetchOnStale;
@@ -421,33 +469,35 @@ var SharedQuery = class {
421
469
  }
422
470
  if (this.refetchOnStale) {
423
471
  this.log(`refetchOnStale for ${queryKey}`);
424
- void this.dedupedFetch(queryKey, ...args);
472
+ void this.dedupedFetch(queryKey, "query", ...args);
425
473
  }
426
474
  this.log(`Returning stale data for ${queryKey}`);
427
475
  return cached.data;
428
476
  }
429
- return await this.dedupedFetch(queryKey, ...args);
477
+ return await this.dedupedFetch(queryKey, "query", ...args);
430
478
  }
431
- dedupedFetch(queryKey, ...args) {
479
+ dedupedFetch(queryKey, queryType, ...args) {
432
480
  const inflightQuery = this.inflightQueries.get(queryKey);
433
481
  if (inflightQuery) {
434
- this.log(`Deduplicating inflight fetch for ${queryKey}`);
482
+ this.log(
483
+ `Deduplicating inflight fetch for ${queryKey}, queryType=${queryType}`
484
+ );
435
485
  return inflightQuery.promise;
436
486
  }
437
- const promise = this.startFetching(queryKey, args);
487
+ const promise = this.startFetching(queryKey, queryType, args);
438
488
  const abortController = new AbortController();
439
489
  this.inflightQueries.set(queryKey, { promise, abortController });
440
490
  return promise;
441
491
  }
442
- async startFetching(queryKey, args) {
443
- this.log(`Start fetching ${queryKey}`);
492
+ async startFetching(queryKey, queryType, args) {
493
+ this.log(`Start fetching ${queryKey}, queryType=${queryType}`);
444
494
  try {
445
- const data = await this.queryFn(...args);
495
+ const data = await (queryType === "refetch" && this.refetchFn ? this.refetchFn(...args) : this.queryFn(...args));
446
496
  this.log(`Successfully fetched ${queryKey}`);
447
497
  this.setData(queryKey, data, Date.now());
448
498
  return data;
449
499
  } catch (e) {
450
- this.log(`Failed to fetch ${queryKey}: ${e}`);
500
+ this.log(`Failed to fetch ${queryKey} for queryType=${queryType}: ${e}`);
451
501
  throw e;
452
502
  } finally {
453
503
  this.inflightQueries.delete(queryKey);
@@ -485,7 +535,15 @@ var SharedQuery = class {
485
535
  */
486
536
  async updateFromSource(...args) {
487
537
  const key = this.getQueryKey(args);
488
- return await this.dedupedFetch(key, ...args);
538
+ return await this.dedupedFetch(key, "query", ...args);
539
+ }
540
+ /**
541
+ * Perform an explicit refetch. If `options.refetchFn` was not specified,
542
+ * then this would be identical to calling `this.updateFromSource()`.
543
+ */
544
+ async refetch(...args) {
545
+ const key = this.getQueryKey(args);
546
+ return await this.dedupedFetch(key, "refetch", ...args);
489
547
  }
490
548
  /** Delete the cached data for one set of arguments. */
491
549
  deleteData(queryKey) {
@@ -635,8 +693,10 @@ function useSharedQuery(query, ...args) {
635
693
  }
636
694
  }, [query.keepLastNonEmptyData, queryStateValue.data]);
637
695
  const execute = (0, import_react3.useCallback)(
638
- async (forceRefresh) => {
639
- query.log(`Begin executing shared query ${queryKey}`);
696
+ async (isRefetch) => {
697
+ query.log(
698
+ `Begin executing shared query ${queryKey}, isRefetch=${isRefetch}`
699
+ );
640
700
  setQueryStateValue((prev) => ({
641
701
  // Keep old data and error.
642
702
  ...prev,
@@ -644,7 +704,7 @@ function useSharedQuery(query, ...args) {
644
704
  loading: true
645
705
  }));
646
706
  try {
647
- const data = await (forceRefresh ? query.updateFromSource(...stableArgs) : query.getCachedOrFetch(...stableArgs));
707
+ const data = await (isRefetch ? query.refetch(...stableArgs) : query.getCachedOrFetch(...stableArgs));
648
708
  if (isMounted.current) {
649
709
  const now = Date.now();
650
710
  setQueryStateValue({
package/sharedQuery.d.mts CHANGED
@@ -147,6 +147,11 @@ type SharedQueryOptions<TArgs extends unknown[], TData> = {
147
147
  * Function to fetch data.
148
148
  */
149
149
  queryFn: QueryFn<TArgs, TData>;
150
+ /**
151
+ * Function to re-fetch data. Usually this would just be the `queryFn`, but
152
+ * if you need a different behavior for refetching, then specify this.
153
+ */
154
+ refetchFn?: QueryFn<TArgs, TData>;
150
155
  /**
151
156
  * ms before data considered stale; 0 means always stale.
152
157
  */
@@ -217,7 +222,8 @@ declare class SharedQuery<TArgs extends unknown[], TData> {
217
222
  */
218
223
  private readonly mountedKeys;
219
224
  readonly queryName: string;
220
- private readonly queryFn;
225
+ readonly queryFn: QueryFn<TArgs, TData>;
226
+ readonly refetchFn: QueryFn<TArgs, TData> | undefined;
221
227
  readonly expiryMs: number;
222
228
  readonly staleMs: number;
223
229
  readonly refetchOnStale: boolean;
@@ -280,6 +286,11 @@ declare class SharedQuery<TArgs extends unknown[], TData> {
280
286
  * another fetch is already inflight.
281
287
  */
282
288
  updateFromSource(...args: TArgs): Promise<TData>;
289
+ /**
290
+ * Perform an explicit refetch. If `options.refetchFn` was not specified,
291
+ * then this would be identical to calling `this.updateFromSource()`.
292
+ */
293
+ refetch(...args: TArgs): Promise<TData>;
283
294
  /** Delete the cached data for one set of arguments. */
284
295
  deleteData(queryKey: string): void;
285
296
  /** Delete all currently cached data & all inflight promises. */
package/sharedQuery.d.ts CHANGED
@@ -143,6 +143,11 @@ export type SharedQueryOptions<TArgs extends unknown[], TData> = {
143
143
  * Function to fetch data.
144
144
  */
145
145
  queryFn: QueryFn<TArgs, TData>;
146
+ /**
147
+ * Function to re-fetch data. Usually this would just be the `queryFn`, but
148
+ * if you need a different behavior for refetching, then specify this.
149
+ */
150
+ refetchFn?: QueryFn<TArgs, TData>;
146
151
  /**
147
152
  * ms before data considered stale; 0 means always stale.
148
153
  */
@@ -213,7 +218,8 @@ export declare class SharedQuery<TArgs extends unknown[], TData> {
213
218
  */
214
219
  private readonly mountedKeys;
215
220
  readonly queryName: string;
216
- private readonly queryFn;
221
+ readonly queryFn: QueryFn<TArgs, TData>;
222
+ readonly refetchFn: QueryFn<TArgs, TData> | undefined;
217
223
  readonly expiryMs: number;
218
224
  readonly staleMs: number;
219
225
  readonly refetchOnStale: boolean;
@@ -276,6 +282,11 @@ export declare class SharedQuery<TArgs extends unknown[], TData> {
276
282
  * another fetch is already inflight.
277
283
  */
278
284
  updateFromSource(...args: TArgs): Promise<TData>;
285
+ /**
286
+ * Perform an explicit refetch. If `options.refetchFn` was not specified,
287
+ * then this would be identical to calling `this.updateFromSource()`.
288
+ */
289
+ refetch(...args: TArgs): Promise<TData>;
279
290
  /** Delete the cached data for one set of arguments. */
280
291
  deleteData(queryKey: string): void;
281
292
  /** Delete all currently cached data & all inflight promises. */
@@ -1,2 +1,2 @@
1
- "use strict";var R=Object.create;var T=Object.defineProperty;var q=Object.getOwnPropertyDescriptor;var _=Object.getOwnPropertyNames;var j=Object.getPrototypeOf,Y=Object.prototype.hasOwnProperty;var J=(r,e)=>{for(var t in e)T(r,t,{get:e[t],enumerable:!0})},v=(r,e,t,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of _(e))!Y.call(r,i)&&i!==t&&T(r,i,{get:()=>e[i],enumerable:!(a=q(e,i))||a.enumerable});return r};var B=(r,e,t)=>(t=r!=null?R(j(r)):{},v(e||!r||!r.__esModule?T(t,"default",{value:r,enumerable:!0}):t,r)),G=r=>v(T({},"__esModule",{value:!0}),r);var Z={};J(Z,{SharedQuery:()=>w,SharedQueryConfig:()=>g,configureSharedQuery:()=>H,sharedQuery:()=>W,useSharedQuery:()=>X});module.exports=G(Z);var P=require("@choksheak/ts-utils/timeConstants"),m=B(require("object-sizeof")),l=require("react");var $=require("@choksheak/ts-utils/timeConstants"),z=require("react");var b=require("@choksheak/ts-utils/kvStore"),O=require("@choksheak/ts-utils/localStore");function K(r,e){let{store:t}=r??{};if(!t)return null;if("persistTo"in t&&typeof t.persistTo=="string"&&"key"in t&&typeof t.key=="string"){let a=t.expiryMs||e;switch(t.persistTo){case"localStorage":{let i=new O.LocalStore(t.key,{defaultExpiryMs:a});return{set:(s,n)=>{typeof window>"u"||i.set(s,n)},get:s=>{if(!(typeof window>"u"))return i.get(s)},delete:s=>{typeof window>"u"||i.delete(s)},clear:()=>{typeof window>"u"||i.clear()}}}case"indexedDb":{let i=t.key+":";return{set:async(s,n)=>{typeof window>"u"||await b.kvStore.set(i+s,n,a)},get:async s=>{if(!(typeof window>"u"))return await b.kvStore.get(i+s)},delete:async s=>{typeof window>"u"||await b.kvStore.delete(i+s)},clear:async()=>{typeof window>"u"||await b.kvStore.clear()}}}default:throw t.persistTo,new Error(`Unknown store.adapter ${t.persistTo}`)}}return t}var C={storeExpiryMs:30*$.MS_PER_DAY,lazyLoad:!1};var M=class{dataByKey=new Map;subscribersByKey=new Map;get(e){return this.dataByKey.get(e)}set(e,t){let a=this.dataByKey.get(e);a!==t&&(this.dataByKey.set(e,t),this.notify(e,t,a))}delete(e){this.dataByKey.delete(e)}setNoNotify(e,t){this.dataByKey.set(e,t)}subscribe(e,t){let a=this.subscribersByKey.get(e);return a||(a=new Set,this.subscribersByKey.set(e,a)),a.add(t),()=>{a.delete(t),a.size===0&&this.subscribersByKey.delete(e)}}notify(e,t,a){let i=this.subscribersByKey.get(e);if(!i)return;let s=Array.from(i.values());for(let n of s)try{n(t,a)}catch(p){console.error("Error invoking subscriber:",p)}}},f=new M,A="state",I=0,Q=class{constructor(e,t){this.defaultValue=e;this.pubSubKey=String(I++),this.storageAdapter=K(t,C.storeExpiryMs),this.setValueBounded=s=>{this.setValue(s)},f.setNoNotify(this.pubSubKey,e);let a=t?.lazyLoad;(a!==void 0?a:C.lazyLoad)||this.initDefaultValueOnce()}pubSubKey;storageAdapter;setValueBounded;initStarted=!1;initDone=!1;subscribe(e){return f.subscribe(this.pubSubKey,()=>e())}getSnapshot(){return f.get(this.pubSubKey)}setValue(e){if(typeof e=="function"){let t=f.get(this.pubSubKey);e=e(t)}f.set(this.pubSubKey,e),this.storageAdapter?.set(A,e)}delete(){f.set(this.pubSubKey,this.defaultValue),this.storageAdapter?.set(A,this.defaultValue)}async initDefaultValueOnce(){if(this.initDone||this.initStarted)return;this.initStarted=!0;let e=await this.storageAdapter?.get(A),t=e!==void 0?e:this.defaultValue;f.set(this.pubSubKey,t),this.initDone=!0,f.set(this.pubSubKey,t)}};function N(r,e){return new Q(r,e)}function k(r){return r.initDefaultValueOnce(),[(0,z.useSyncExternalStore)(t=>r.subscribe(t),()=>r.getSnapshot(),()=>r.getSnapshot()),r.setValueBounded]}function E(r){let e=new Set;return JSON.stringify(r,(t,a)=>{if(typeof a=="function")throw new Error("Functions should not be passed into query parameters");if(!a||typeof a!="object"||Array.isArray(a))return a;if(e.has(a))return"[Circular]";if(e.add(a),a!==r)try{JSON.stringify(a)}catch{return"[Unserializable]"}return Object.keys(a).sort().reduce((i,s)=>(i[s]=a[s],i),{})})}var U=B(require("lodash/isEqual")),D=require("react");function V(r){let[e,t]=(0,D.useState)(r);return(0,D.useMemo)(()=>e===r||(0,U.default)(e,r)?e:(t(r),r),[e,r])}var g={staleMs:0,expiryMs:30*P.MS_PER_DAY,refetchOnStale:!0,persistTo:void 0,maxSize:100,maxBytes:1e5,log:(...r)=>console.log("[sharedQuery]",...r)};function H(r){Object.assign(g,r)}var w=class{inflightQueries=new Map;queryState;mountedKeys=new Map;queryName;queryFn;expiryMs;staleMs;refetchOnStale;maxSize;maxBytes;keepLastNonEmptyData;log;constructor(e){if(this.queryName=e.queryName,this.queryFn=e.queryFn,this.staleMs=e?.staleMs??g.staleMs,this.expiryMs=e?.expiryMs??g.expiryMs,this.refetchOnStale=e?.refetchOnStale??g.refetchOnStale,!e.store){let t=e.persistTo??g.persistTo;t&&(e.store={persistTo:t,key:e.queryName,expiryMs:this.expiryMs})}this.maxSize=e?.maxSize??g.maxSize,this.maxBytes=e?.maxBytes??g.maxBytes,this.keepLastNonEmptyData=!!e?.keepLastNonEmptyData,this.log=e?.log??g.log,this.queryState=N({},e)}getQueryKey(e){return this.queryName+":"+E(e)}getAbortController(e){let t=this.getQueryKey(e);return this.getAbortControllerByKey(t)}getAbortControllerByKey(e){return this.inflightQueries.get(e)?.abortController??null}getAbortSignal(e){let t=this.getQueryKey(e);return this.getAbortSignalByKey(t)}getAbortSignalByKey(e){return this.getAbortControllerByKey(e)?.signal??null}isStale(e){return this.staleMs===0?!0:Date.now()-e>this.staleMs}isExpired(e){return this.expiryMs===0?!1:Date.now()-e>this.expiryMs}async getCachedOrFetch(...e){let t=this.getQueryKey(e),a=this.queryState.getSnapshot()?.[t];return a?.data&&!this.isExpired(a.dataUpdatedMs??0)?this.isStale(a.dataUpdatedMs??0)?(this.refetchOnStale&&(this.log(`refetchOnStale for ${t}`),this.dedupedFetch(t,...e)),this.log(`Returning stale data for ${t}`),a.data):(this.log(`Return fresh data ${t} from cache without fetching`),a.data):await this.dedupedFetch(t,...e)}dedupedFetch(e,...t){let a=this.inflightQueries.get(e);if(a)return this.log(`Deduplicating inflight fetch for ${e}`),a.promise;let i=this.startFetching(e,t),s=new AbortController;return this.inflightQueries.set(e,{promise:i,abortController:s}),i}async startFetching(e,t){this.log(`Start fetching ${e}`);try{let a=await this.queryFn(...t);return this.log(`Successfully fetched ${e}`),this.setData(e,a,Date.now()),a}catch(a){throw this.log(`Failed to fetch ${e}: ${a}`),a}finally{this.inflightQueries.delete(e)}}getQueryValue(e){return this.queryState.getSnapshot()?.[e]}getData(e){let t=this.queryState.getSnapshot()?.[e];if(t?.lastUpdatedMs&&this.isExpired(t.lastUpdatedMs)){this.deleteData(e);return}return t?.data}setData(e,t,a=Date.now()){let i={lastUpdatedMs:Date.now(),loading:!1,data:t,dataUpdatedMs:a};this.queryState.setValue(s=>({...s,[e]:i})),this.enforceSizeLimit()}async updateFromSource(...e){let t=this.getQueryKey(e);return await this.dedupedFetch(t,...e)}deleteData(e){let a={...this.queryState.getSnapshot()};delete a[e],this.queryState.setValue(a);let i=this.inflightQueries.get(e);i&&(this.inflightQueries.delete(e),i.abortController.abort("Deleted"))}clear(){this.queryState.delete();for(let{abortController:e}of Array.from(this.inflightQueries.values()))e.abort("Cleared");this.inflightQueries.clear()}mount(e){this.mountedKeys.set(e,(this.mountedKeys.get(e)??0)+1)}unmount(e){let t=this.mountedKeys.get(e);t===1?this.mountedKeys.delete(e):this.mountedKeys.set(e,(t??0)-1)}enforceSizeLimit(){if(!this.maxSize&&!this.maxBytes)return;let e=this.queryState.getSnapshot(),t=Object.keys(e).length;if(t<=1)return;let a=(0,m.default)(e),i=this.maxSize?t-this.maxSize:0,s=this.maxBytes?a-this.maxBytes:0;if(i<=0&&s<=0){this.log(`No need to trim data for ${this.queryName}: size=${t} (limit=${this.maxSize}), byteSize=${a.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`);return}this.log(`Need to trim ${this.queryName}: numToCut=${i}, bytesToCut=${s}`);let n={...e},p=!1,h=(o,y)=>{if(this.mountedKeys.has(o)){this.log(`Cannot clean up ${o} as it is mounted`);return}let d=(0,m.default)(o)+(0,m.default)(n[o]);this.log(`Cleaning up unmounted${y?", expired":""} ${o} (byteSize=${d.toLocaleString()})`),i--,s-=d,delete n[o],p=!0},S=Object.entries(n).sort((o,y)=>o[1].lastUpdatedMs-y[1].lastUpdatedMs);for(let[o]of S){if(!this.isExpired(n[o].lastUpdatedMs))break;h(o,!0)}if(i>0||s>0){p&&(S=Object.entries(n).sort((o,y)=>o[1].lastUpdatedMs-y[1].lastUpdatedMs));for(let[o]of S)if(h(o,!1),i<=0&&s<=0)break}if(!p){this.log(`No trimmable entries found for ${this.queryName}: size=${t} (limit=${this.maxSize}), byteSize=${a.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`);return}this.queryState.setValue(n),this.log(`Trimmed data for ${this.queryName}: size=[${t} -> ${Object.keys(n).length}] (limit=${this.maxSize}), byteSize=[${a.toLocaleString()} -> ${(0,m.default)(n).toLocaleString()}] (limit=${this.maxBytes.toLocaleString()})`)}},L=new Set;function W(r){if(L.has(r.queryName))throw new Error(`Duplicate shared query "${r.queryName}"`);return L.add(r.queryName),new w(r)}var F={lastUpdatedMs:0,loading:!0};function X(r,...e){let t=V(e),[a,i]=k(r.queryState),s=(0,l.useRef)(!0),n=(0,l.useMemo)(()=>r.getQueryKey(t),[r,t]),p=a[n]??F,h=(0,l.useCallback)(d=>{i(u=>{let c={...u};return c[n]=typeof d=="function"?d(c[n]??F):d,c})},[n,i]),[S,o]=(0,l.useState)();(0,l.useEffect)(()=>{r.keepLastNonEmptyData&&p.data!==void 0&&o(p.data)},[r.keepLastNonEmptyData,p.data]);let y=(0,l.useCallback)(async d=>{r.log(`Begin executing shared query ${n}`),h(u=>({...u,lastUpdatedMs:Date.now(),loading:!0}));try{let u=await(d?r.updateFromSource(...t):r.getCachedOrFetch(...t));if(s.current){let c=Date.now();h({lastUpdatedMs:c,loading:!1,data:u,dataUpdatedMs:c})}}catch(u){if(s.current){let c=Date.now();h(x=>({...x,lastUpdatedMs:c,loading:!1,error:u,errorUpdatedMs:c}))}}},[r,n,h,t]);return(0,l.useEffect)(()=>(s.current=!0,r.mount(n),y(!1),()=>{s.current=!1,r.unmount(n)}),[y,r,n]),(0,l.useMemo)(()=>({...p,lastNonEmptyData:S,abortCurrentQuery:d=>{let u=r.getAbortControllerByKey(n);return u?.abort(d),!!u},refetch:()=>{y(!0)},setData:(d,u)=>{let c=Date.now();h(x=>({...x,data:d,dataUpdatedMs:u??c,lastUpdatedMs:c,loading:!1}))},deleteData:()=>{i(d=>{let u={...d};return delete u[n],u})}}),[y,S,r,n,p,i,h])}0&&(module.exports={SharedQuery,SharedQueryConfig,configureSharedQuery,sharedQuery,useSharedQuery});
1
+ "use strict";var q=Object.create;var D=Object.defineProperty;var _=Object.getOwnPropertyDescriptor;var j=Object.getOwnPropertyNames;var Y=Object.getPrototypeOf,I=Object.prototype.hasOwnProperty;var J=(r,e)=>{for(var t in e)D(r,t,{get:e[t],enumerable:!0})},$=(r,e,t,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of j(e))!I.call(r,i)&&i!==t&&D(r,i,{get:()=>e[i],enumerable:!(a=_(e,i))||a.enumerable});return r};var O=(r,e,t)=>(t=r!=null?q(Y(r)):{},$(e||!r||!r.__esModule?D(t,"default",{value:r,enumerable:!0}):t,r)),G=r=>$(D({},"__esModule",{value:!0}),r);var ee={};J(ee,{SharedQuery:()=>w,SharedQueryConfig:()=>f,configureSharedQuery:()=>H,sharedQuery:()=>X,useSharedQuery:()=>Z});module.exports=G(ee);var R=require("@choksheak/ts-utils/timeConstants"),m=O(require("object-sizeof")),y=require("react");var C=require("@choksheak/ts-utils/timeConstants"),z=require("react");var T=require("@choksheak/ts-utils/kvStore"),B=require("@choksheak/ts-utils/localStore"),S=typeof window>"u";function K(r,e){let{store:t}=r??{};if(!t)return null;if("persistTo"in t&&typeof t.persistTo=="string"&&"key"in t&&typeof t.key=="string"){let a=t.expiryMs||e,i=t.isValid;switch(t.persistTo){case"localStorage":{let n=new B.LocalStore(t.key,{defaultExpiryMs:a});return{set:(s,o)=>{S||n.set(s,o)},get:s=>{if(S)return;let o=n.get(s);if(i&&!i(o)){console.warn(`localStorage adapter: Auto-discard invalid value for ${s}: ${JSON.stringify(o)}`),n.delete(s);return}return o},delete:s=>{S||n.delete(s)},clear:()=>{S||n.clear()}}}case"indexedDb":{let n=t.key+":",s=t.isValid;return{set:async(o,c)=>{S||await T.kvStore.set(n+o,c,a)},get:async o=>{if(S)return;let c=n+o,h=await T.kvStore.get(c);if(s&&!s(h)){console.warn(`indexedDb adapter: Auto-discard invalid value for ${o}: ${JSON.stringify(h)}`),await T.kvStore.delete(c);return}return h},delete:async o=>{S||await T.kvStore.delete(n+o)},clear:async()=>{S||await T.kvStore.clear()}}}default:throw t.persistTo,new Error(`Unknown store.adapter ${t.persistTo}`)}}return t}var N={storeExpiryMs:30*C.MS_PER_DAY,lazyLoad:!1};var Q=class{dataByKey=new Map;subscribersByKey=new Map;get(e){return this.dataByKey.get(e)}set(e,t){let a=this.dataByKey.get(e);a!==t&&(this.dataByKey.set(e,t),this.notify(e,t,a))}delete(e){this.dataByKey.delete(e)}setNoNotify(e,t){this.dataByKey.set(e,t)}subscribe(e,t){let a=this.subscribersByKey.get(e);return a||(a=new Set,this.subscribersByKey.set(e,a)),a.add(t),()=>{a.delete(t),a.size===0&&this.subscribersByKey.delete(e)}}notify(e,t,a){let i=this.subscribersByKey.get(e);if(!i)return;let n=Array.from(i.values());for(let s of n)try{s(t,a)}catch(o){console.error("Error invoking subscriber:",o)}}},b=new Q,M="state",W=0,v=class{constructor(e,t){this.defaultValue=e;this.pubSubKey=String(W++),this.storageAdapter=K(t,N.storeExpiryMs),this.setValueBounded=n=>{this.setValue(n)},b.setNoNotify(this.pubSubKey,e);let a=t?.lazyLoad;(a!==void 0?a:N.lazyLoad)||this.initDefaultValueOnce()}pubSubKey;storageAdapter;setValueBounded;initStarted=!1;initDone=!1;subscribe(e){return b.subscribe(this.pubSubKey,()=>e())}hasStore(){return!!this.storageAdapter}async setInStore(e){return await this.storageAdapter?.set(M,e)}async getFromStore(){return await this.storageAdapter?.get(M)}getSnapshot(){return b.get(this.pubSubKey)}setValue(e){if(typeof e=="function"){let t=b.get(this.pubSubKey);e=e(t)}b.set(this.pubSubKey,e),this.setInStore(e)}delete(){b.set(this.pubSubKey,this.defaultValue),this.storageAdapter?.set(M,this.defaultValue)}async initDefaultValueOnce(){if(this.initDone||this.initStarted)return;this.initStarted=!0;let e=await this.getFromStore(),t=e!==void 0?e:this.defaultValue;b.set(this.pubSubKey,t),this.initDone=!0}};function F(r,e){return new v(r,e)}function V(r){return r.initDefaultValueOnce(),[(0,z.useSyncExternalStore)(t=>r.subscribe(t),()=>r.getSnapshot(),()=>r.getSnapshot()),r.setValueBounded]}function k(r){let e=new Set;return JSON.stringify(r,(t,a)=>{if(typeof a=="function")throw new Error("Functions should not be passed into query parameters");if(!a||typeof a!="object"||Array.isArray(a))return a;if(e.has(a))return"[Circular]";if(e.add(a),a!==r)try{JSON.stringify(a)}catch{return"[Unserializable]"}return Object.keys(a).sort().reduce((i,n)=>(i[n]=a[n],i),{})})}var E=O(require("lodash/isEqual")),x=require("react");function U(r){let[e,t]=(0,x.useState)(r);return(0,x.useMemo)(()=>e===r||(0,E.default)(e,r)?e:(t(r),r),[e,r])}var f={staleMs:0,expiryMs:30*R.MS_PER_DAY,refetchOnStale:!0,persistTo:void 0,maxSize:100,maxBytes:1e5,log:(...r)=>console.log("[sharedQuery]",...r)};function H(r){Object.assign(f,r)}var w=class{inflightQueries=new Map;queryState;mountedKeys=new Map;queryName;queryFn;refetchFn;expiryMs;staleMs;refetchOnStale;maxSize;maxBytes;keepLastNonEmptyData;log;constructor(e){if(this.queryName=e.queryName,this.queryFn=e.queryFn,this.refetchFn=e.refetchFn,this.staleMs=e?.staleMs??f.staleMs,this.expiryMs=e?.expiryMs??f.expiryMs,this.refetchOnStale=e?.refetchOnStale??f.refetchOnStale,!e.store){let t=e.persistTo??f.persistTo;t&&(e.store={persistTo:t,key:e.queryName,expiryMs:this.expiryMs})}this.maxSize=e?.maxSize??f.maxSize,this.maxBytes=e?.maxBytes??f.maxBytes,this.keepLastNonEmptyData=!!e?.keepLastNonEmptyData,this.log=e?.log??f.log,this.queryState=F({},e)}getQueryKey(e){return this.queryName+":"+k(e)}getAbortController(e){let t=this.getQueryKey(e);return this.getAbortControllerByKey(t)}getAbortControllerByKey(e){return this.inflightQueries.get(e)?.abortController??null}getAbortSignal(e){let t=this.getQueryKey(e);return this.getAbortSignalByKey(t)}getAbortSignalByKey(e){return this.getAbortControllerByKey(e)?.signal??null}isStale(e){return this.staleMs===0?!0:Date.now()-e>this.staleMs}isExpired(e){return this.expiryMs===0?!1:Date.now()-e>this.expiryMs}async getCachedOrFetch(...e){let t=this.getQueryKey(e),a=this.queryState.getSnapshot()?.[t];return a?.data&&!this.isExpired(a.dataUpdatedMs??0)?this.isStale(a.dataUpdatedMs??0)?(this.refetchOnStale&&(this.log(`refetchOnStale for ${t}`),this.dedupedFetch(t,"query",...e)),this.log(`Returning stale data for ${t}`),a.data):(this.log(`Return fresh data ${t} from cache without fetching`),a.data):await this.dedupedFetch(t,"query",...e)}dedupedFetch(e,t,...a){let i=this.inflightQueries.get(e);if(i)return this.log(`Deduplicating inflight fetch for ${e}, queryType=${t}`),i.promise;let n=this.startFetching(e,t,a),s=new AbortController;return this.inflightQueries.set(e,{promise:n,abortController:s}),n}async startFetching(e,t,a){this.log(`Start fetching ${e}, queryType=${t}`);try{let i=await(t==="refetch"&&this.refetchFn?this.refetchFn(...a):this.queryFn(...a));return this.log(`Successfully fetched ${e}`),this.setData(e,i,Date.now()),i}catch(i){throw this.log(`Failed to fetch ${e} for queryType=${t}: ${i}`),i}finally{this.inflightQueries.delete(e)}}getQueryValue(e){return this.queryState.getSnapshot()?.[e]}getData(e){let t=this.queryState.getSnapshot()?.[e];if(t?.lastUpdatedMs&&this.isExpired(t.lastUpdatedMs)){this.deleteData(e);return}return t?.data}setData(e,t,a=Date.now()){let i={lastUpdatedMs:Date.now(),loading:!1,data:t,dataUpdatedMs:a};this.queryState.setValue(n=>({...n,[e]:i})),this.enforceSizeLimit()}async updateFromSource(...e){let t=this.getQueryKey(e);return await this.dedupedFetch(t,"query",...e)}async refetch(...e){let t=this.getQueryKey(e);return await this.dedupedFetch(t,"refetch",...e)}deleteData(e){let a={...this.queryState.getSnapshot()};delete a[e],this.queryState.setValue(a);let i=this.inflightQueries.get(e);i&&(this.inflightQueries.delete(e),i.abortController.abort("Deleted"))}clear(){this.queryState.delete();for(let{abortController:e}of Array.from(this.inflightQueries.values()))e.abort("Cleared");this.inflightQueries.clear()}mount(e){this.mountedKeys.set(e,(this.mountedKeys.get(e)??0)+1)}unmount(e){let t=this.mountedKeys.get(e);t===1?this.mountedKeys.delete(e):this.mountedKeys.set(e,(t??0)-1)}enforceSizeLimit(){if(!this.maxSize&&!this.maxBytes)return;let e=this.queryState.getSnapshot(),t=Object.keys(e).length;if(t<=1)return;let a=(0,m.default)(e),i=this.maxSize?t-this.maxSize:0,n=this.maxBytes?a-this.maxBytes:0;if(i<=0&&n<=0){this.log(`No need to trim data for ${this.queryName}: size=${t} (limit=${this.maxSize}), byteSize=${a.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`);return}this.log(`Need to trim ${this.queryName}: numToCut=${i}, bytesToCut=${n}`);let s={...e},o=!1,c=(u,g)=>{if(this.mountedKeys.has(u)){this.log(`Cannot clean up ${u} as it is mounted`);return}let l=(0,m.default)(u)+(0,m.default)(s[u]);this.log(`Cleaning up unmounted${g?", expired":""} ${u} (byteSize=${l.toLocaleString()})`),i--,n-=l,delete s[u],o=!0},h=Object.entries(s).sort((u,g)=>u[1].lastUpdatedMs-g[1].lastUpdatedMs);for(let[u]of h){if(!this.isExpired(s[u].lastUpdatedMs))break;c(u,!0)}if(i>0||n>0){o&&(h=Object.entries(s).sort((u,g)=>u[1].lastUpdatedMs-g[1].lastUpdatedMs));for(let[u]of h)if(c(u,!1),i<=0&&n<=0)break}if(!o){this.log(`No trimmable entries found for ${this.queryName}: size=${t} (limit=${this.maxSize}), byteSize=${a.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`);return}this.queryState.setValue(s),this.log(`Trimmed data for ${this.queryName}: size=[${t} -> ${Object.keys(s).length}] (limit=${this.maxSize}), byteSize=[${a.toLocaleString()} -> ${(0,m.default)(s).toLocaleString()}] (limit=${this.maxBytes.toLocaleString()})`)}},L=new Set;function X(r){if(L.has(r.queryName))throw new Error(`Duplicate shared query "${r.queryName}"`);return L.add(r.queryName),new w(r)}var P={lastUpdatedMs:0,loading:!0};function Z(r,...e){let t=U(e),[a,i]=V(r.queryState),n=(0,y.useRef)(!0),s=(0,y.useMemo)(()=>r.getQueryKey(t),[r,t]),o=a[s]??P,c=(0,y.useCallback)(l=>{i(d=>{let p={...d};return p[s]=typeof l=="function"?l(p[s]??P):l,p})},[s,i]),[h,u]=(0,y.useState)();(0,y.useEffect)(()=>{r.keepLastNonEmptyData&&o.data!==void 0&&u(o.data)},[r.keepLastNonEmptyData,o.data]);let g=(0,y.useCallback)(async l=>{r.log(`Begin executing shared query ${s}, isRefetch=${l}`),c(d=>({...d,lastUpdatedMs:Date.now(),loading:!0}));try{let d=await(l?r.refetch(...t):r.getCachedOrFetch(...t));if(n.current){let p=Date.now();c({lastUpdatedMs:p,loading:!1,data:d,dataUpdatedMs:p})}}catch(d){if(n.current){let p=Date.now();c(A=>({...A,lastUpdatedMs:p,loading:!1,error:d,errorUpdatedMs:p}))}}},[r,s,c,t]);return(0,y.useEffect)(()=>(n.current=!0,r.mount(s),g(!1),()=>{n.current=!1,r.unmount(s)}),[g,r,s]),(0,y.useMemo)(()=>({...o,lastNonEmptyData:h,abortCurrentQuery:l=>{let d=r.getAbortControllerByKey(s);return d?.abort(l),!!d},refetch:()=>{g(!0)},setData:(l,d)=>{let p=Date.now();c(A=>({...A,data:l,dataUpdatedMs:d??p,lastUpdatedMs:p,loading:!1}))},deleteData:()=>{i(l=>{let d={...l};return delete d[s],d})}}),[g,h,r,s,o,i,c])}0&&(module.exports={SharedQuery,SharedQueryConfig,configureSharedQuery,sharedQuery,useSharedQuery});
2
2
  //# sourceMappingURL=sharedQuery.min.cjs.map