@choksheak/react-utils 0.4.0 → 0.4.1

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.4.0",
4
+ "version": "0.4.1",
5
5
  "description": "Miscellaneous React+TypeScript utilities with support for full tree-shaking.",
6
6
  "private": false,
7
7
  "sideEffects": false,
@@ -37,7 +37,7 @@
37
37
  "homepage": "https://github.com/choksheak/react-utils#readme",
38
38
  "devDependencies": {
39
39
  "@choksheak/bump-version": "^0.1.2",
40
- "@choksheak/ts-utils": "^0.3.5",
40
+ "@choksheak/ts-utils": "^0.3.8",
41
41
  "@eslint/compat": "^1.4.0",
42
42
  "@eslint/js": "^9.38.0",
43
43
  "@swc/core": "^1.14.0",
@@ -71,16 +71,14 @@
71
71
  "typescript": "^5.9.3",
72
72
  "typescript-eslint": "^8.46.2",
73
73
  "use-deep-compare": "^1.3.0",
74
- "vitest": "^4.0.3",
75
- "wretch": "^3.0.2"
74
+ "vitest": "^4.0.3"
76
75
  },
77
76
  "peerDependencies": {
78
- "@choksheak/ts-utils": ">=0.3.5",
77
+ "@choksheak/ts-utils": ">=0.3.8",
79
78
  "lodash": ">=4.17.21",
80
79
  "object-sizeof": ">=2.6.5",
81
80
  "react": ">=16",
82
81
  "use-deep-compare": ">=1.3.0",
83
- "wretch": ">=3",
84
82
  "xxhashjs": ">=0.2.2"
85
83
  },
86
84
  "exports": {
@@ -150,19 +148,6 @@
150
148
  "default": "./chunk-74BS2FAO.cjs"
151
149
  }
152
150
  },
153
- "./chunk-UFJNJ3WV": {
154
- "types": "./chunk-UFJNJ3WV.d.ts",
155
- "import": {
156
- "production": "./chunk-UFJNJ3WV.min.mjs",
157
- "development": "./chunk-UFJNJ3WV.mjs",
158
- "default": "./chunk-UFJNJ3WV.mjs"
159
- },
160
- "require": {
161
- "production": "./chunk-UFJNJ3WV.min.cjs",
162
- "development": "./chunk-UFJNJ3WV.cjs",
163
- "default": "./chunk-UFJNJ3WV.cjs"
164
- }
165
- },
166
151
  "./chunk-WVLPJPWG": {
167
152
  "types": "./chunk-WVLPJPWG.d.ts",
168
153
  "import": {
@@ -176,19 +161,6 @@
176
161
  "default": "./chunk-WVLPJPWG.cjs"
177
162
  }
178
163
  },
179
- "./fetcher": {
180
- "types": "./fetcher.d.ts",
181
- "import": {
182
- "production": "./fetcher.min.mjs",
183
- "development": "./fetcher.mjs",
184
- "default": "./fetcher.mjs"
185
- },
186
- "require": {
187
- "production": "./fetcher.min.cjs",
188
- "development": "./fetcher.cjs",
189
- "default": "./fetcher.cjs"
190
- }
191
- },
192
164
  "./sharedQuery": {
193
165
  "types": "./sharedQuery.d.ts",
194
166
  "import": {
@@ -272,15 +244,9 @@
272
244
  "chunk-74BS2FAO": [
273
245
  "./chunk-74BS2FAO.d.ts"
274
246
  ],
275
- "chunk-UFJNJ3WV": [
276
- "./chunk-UFJNJ3WV.d.ts"
277
- ],
278
247
  "chunk-WVLPJPWG": [
279
248
  "./chunk-WVLPJPWG.d.ts"
280
249
  ],
281
- "fetcher": [
282
- "./fetcher.d.ts"
283
- ],
284
250
  "sharedQuery": [
285
251
  "./sharedQuery.d.ts"
286
252
  ],
@@ -313,9 +279,9 @@
313
279
  "gen-types": "tsc --emitDeclarationOnly --declaration",
314
280
  "gen-exports": "tsx scripts/gen-exports.ts",
315
281
  "pkg": "scripts/package.sh",
316
- "pub": "pnpm --dir dist publish --access=public --no-git-checks",
282
+ "pub": "pnpm publish dist/ --access=public",
317
283
  "commit": "git add . && git commit -m 'Release '`bump-version get`",
318
- "release": "pnpm run clean && bump-version && pnpm install && pnpm run ci && pnpm run pkg && pnpm run pub && pnpm run commit",
284
+ "release": "pnpm run clean && bump-version && pnpm install && pnpm run ci && pnpm run pkg && pnpm run commit && pnpm run pub",
319
285
  "postinstall": "simple-git-hooks"
320
286
  }
321
287
  }
package/sharedQuery.cjs CHANGED
@@ -1,8 +1,5 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
2
 
3
- var _chunkUFJNJ3WVcjs = require('./chunk-UFJNJ3WV.cjs');
4
-
5
-
6
3
 
7
4
  var _chunk3X25NN6Xcjs = require('./chunk-3X25NN6X.cjs');
8
5
  require('./chunk-5PDGU37E.cjs');
@@ -16,6 +13,7 @@ var _chunkWVLPJPWGcjs = require('./chunk-WVLPJPWG.cjs');
16
13
  require('./chunk-74BS2FAO.cjs');
17
14
 
18
15
  // src/sharedQuery.ts
16
+ var _http = require('@choksheak/ts-utils/http');
19
17
  var _objectsizeof = require('object-sizeof'); var _objectsizeof2 = _interopRequireDefault(_objectsizeof);
20
18
 
21
19
 
@@ -62,7 +60,8 @@ function sharedQuery(options) {
62
60
  if (options.queryFn) {
63
61
  queryFn = options.queryFn;
64
62
  } else if (url) {
65
- queryFn = (...args) => {
63
+ const fetcher = _http.http.use(_http.retries.call(void 0, ));
64
+ queryFn = async (...args) => {
66
65
  let params = "";
67
66
  const firstArg = _optionalChain([args, 'optionalAccess', _ => _[0]]);
68
67
  if (firstArg) {
@@ -91,7 +90,8 @@ function sharedQuery(options) {
91
90
  }
92
91
  }
93
92
  }
94
- return _chunkUFJNJ3WVcjs.fetcher.url(url + params).get().json();
93
+ const response = await fetcher.get(url + params);
94
+ return await response.json();
95
95
  };
96
96
  } else {
97
97
  throw new Error(`Either queryFn or url must be specified`);
@@ -320,7 +320,7 @@ function sharedQuery(options) {
320
320
  * });
321
321
  * ```
322
322
  *
323
- * Example using `fetcher.ts`:
323
+ * Example using `http.ts`:
324
324
  * ```
325
325
  * const getUserQuery = sharedQuery({
326
326
  * queryName: "getUser",
@@ -329,7 +329,8 @@ function sharedQuery(options) {
329
329
  * const signal = getUserQuery.getAbortSignal([userId]);
330
330
  *
331
331
  * // Pass the signal to fetch so that it can be aborted.
332
- * return fetcher.url(`/users/${userId}`).options({ signal }).get().json();
332
+ * const response = await http.get(`/users/${userId}`, { signal });
333
+ * return await response.json();
333
334
  * },
334
335
  * });
335
336
  * ```
package/sharedQuery.d.mts CHANGED
@@ -323,7 +323,7 @@ declare function sharedQuery<TArgs extends unknown[], TData>(options: SharedQuer
323
323
  * });
324
324
  * ```
325
325
  *
326
- * Example using `fetcher.ts`:
326
+ * Example using `http.ts`:
327
327
  * ```
328
328
  * const getUserQuery = sharedQuery({
329
329
  * queryName: "getUser",
@@ -332,7 +332,8 @@ declare function sharedQuery<TArgs extends unknown[], TData>(options: SharedQuer
332
332
  * const signal = getUserQuery.getAbortSignal([userId]);
333
333
  *
334
334
  * // Pass the signal to fetch so that it can be aborted.
335
- * return fetcher.url(`/users/${userId}`).options({ signal }).get().json();
335
+ * const response = await http.get(`/users/${userId}`, { signal });
336
+ * return await response.json();
336
337
  * },
337
338
  * });
338
339
  * ```
package/sharedQuery.d.ts CHANGED
@@ -319,7 +319,7 @@ export declare function sharedQuery<TArgs extends unknown[], TData>(options: Sha
319
319
  * });
320
320
  * ```
321
321
  *
322
- * Example using `fetcher.ts`:
322
+ * Example using `http.ts`:
323
323
  * ```
324
324
  * const getUserQuery = sharedQuery({
325
325
  * queryName: "getUser",
@@ -328,7 +328,8 @@ export declare function sharedQuery<TArgs extends unknown[], TData>(options: Sha
328
328
  * const signal = getUserQuery.getAbortSignal([userId]);
329
329
  *
330
330
  * // Pass the signal to fetch so that it can be aborted.
331
- * return fetcher.url(`/users/${userId}`).options({ signal }).get().json();
331
+ * const response = await http.get(`/users/${userId}`, { signal });
332
+ * return await response.json();
332
333
  * },
333
334
  * });
334
335
  * ```
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _chunkO3RBYG3Wmincjs = require('./chunk-O3RBYG3W.min.cjs');var _chunkNTFFK7ZHmincjs = require('./chunk-NTFFK7ZH.min.cjs');require('./chunk-UFJQLQFI.min.cjs');var _chunkTEZPF6BGmincjs = require('./chunk-TEZPF6BG.min.cjs');var _chunkF5L32IUCmincjs = require('./chunk-F5L32IUC.min.cjs');require('./chunk-KE3RXVMS.min.cjs');var _objectsizeof = require('object-sizeof'); var _objectsizeof2 = _interopRequireDefault(_objectsizeof);var _react = require('react');var _xxhashjs = require('xxhashjs'); var _xxhashjs2 = _interopRequireDefault(_xxhashjs);_chunkTEZPF6BGmincjs.b.call(void 0, );var Q={staleMs:0,expiryMs:30*864e5,refetchOnStale:!0,persistTo:void 0,maxSize:100,maxBytes:1e5,log:(...r)=>console.log("[sharedQuery]",...r)};function se(r){Object.assign(Q,r)}var W=new Set;function ie(r){let w=r.url||"",u=r.queryName||w;if(!u)throw new Error("Either queryName or url must be specified.");if(W.has(u))throw new Error(`Duplicate shared query name "${u}"`);W.add(u);let p=new Map,h=new Map,m;if(r.queryFn)m=r.queryFn;else if(w)m=(...e)=>{let t="",a=_optionalChain([e, 'optionalAccess', _2 => _2[0]]);if(a){if(typeof a=="string")t=a;else if(typeof a=="object"){let n=[];for(let[s,d]of Object.entries(a))if(d!==void 0)if(Array.isArray(d))for(let b of d)b!==void 0&&n.push([s,String(b)]);else n.push([s,String(d)]);t=new URLSearchParams(n).toString()}t&&(w.includes("?")?t="&"+t:t="?"+t)}return _chunkO3RBYG3Wmincjs.a.url(w+t).get().json()};else throw new Error("Either queryFn or url must be specified");let l=r.refetchFn,y=_nullishCoalesce(_optionalChain([r, 'optionalAccess', _3 => _3.staleMs]), () => (Q.staleMs)),D=_nullishCoalesce(_optionalChain([r, 'optionalAccess', _4 => _4.expiryMs]), () => (Q.expiryMs)),M=_nullishCoalesce(_optionalChain([r, 'optionalAccess', _5 => _5.refetchOnStale]), () => (Q.refetchOnStale)),A=r.store;if(A){if("persistTo"in A&&A.isValid){let e=A.isValid;r={...r,store:{...A,isValid:t=>{if(!t||typeof t!="object")return!1;for(let a of Object.values(t))if(!a||typeof a!="object"||!("data"in a)||!("dataUpdatedMs"in a)||!("lastUpdatedMs"in a)||typeof a.dataUpdatedMs!="number"||typeof a.lastUpdatedMs!="number"||!e(a.data))return!1;return!0}}}}}else{let e=_nullishCoalesce(r.persistTo, () => (Q.persistTo));e&&(r={...r,store:{persistTo:e,key:u,expiryMs:D}})}let T=_nullishCoalesce(_optionalChain([r, 'optionalAccess', _6 => _6.maxSize]), () => (Q.maxSize)),i=_nullishCoalesce(_optionalChain([r, 'optionalAccess', _7 => _7.maxBytes]), () => (Q.maxBytes)),c=!!_optionalChain([r, 'optionalAccess', _8 => _8.keepLastNonEmptyData]),o=_nullishCoalesce(_optionalChain([r, 'optionalAccess', _9 => _9.log]), () => (Q.log)),f=_chunkNTFFK7ZHmincjs.c.call(void 0, {},r);function _(e){return y===0?!0:Date.now()-e>y}function C(e){return D===0?!1:Date.now()-e>D}function k(){if(!T&&!i)return;let e=f.getSnapshot(),t=Object.keys(e).length;if(t<=1)return;let a=_objectsizeof2.default.call(void 0, e),n=T?t-T:0,s=i?a-i:0;if(n<=0&&s<=0){o(`No need to trim data for ${u}: size=${t} (limit=${T}), byteSize=${a.toLocaleString()} (limit=${i.toLocaleString()})`);return}o(`Need to trim ${u}: numToCut=${n}, bytesToCut=${s}`);let d={...e},b=!1,v=(g,$)=>{if(h.has(g)){o(`Cannot clean up ${g} as it is mounted`);return}let O=_objectsizeof2.default.call(void 0, g)+_objectsizeof2.default.call(void 0, d[g]);o(`Cleaning up unmounted${$?", expired":""} ${g} (byteSize=${O.toLocaleString()})`),n--,s-=O,delete d[g],b=!0},F=Object.entries(d).sort((g,$)=>g[1].lastUpdatedMs-$[1].lastUpdatedMs);for(let[g]of F){if(!C(d[g].lastUpdatedMs))break;v(g,!0)}if(n>0||s>0){b&&(F=Object.entries(d).sort((g,$)=>g[1].lastUpdatedMs-$[1].lastUpdatedMs));for(let[g]of F)if(v(g,!1),n<=0&&s<=0)break}if(!b){o(`No trimmable entries found for ${u}: size=${t} (limit=${T}), byteSize=${a.toLocaleString()} (limit=${i.toLocaleString()})`);return}f.setValue(d),o(`Trimmed data for ${u}: size=[${t} -> ${Object.keys(d).length}] (limit=${T}), byteSize=[${a.toLocaleString()} -> ${_objectsizeof2.default.call(void 0, d).toLocaleString()}] (limit=${i.toLocaleString()})`)}function V(e,t,a=Date.now()){let n={lastUpdatedMs:Date.now(),loading:!1,data:t,dataUpdatedMs:a};f.setValue(s=>({...s,[e]:n})),k()}async function X(e,t,a){o(`Start fetching ${e}, queryType=${t}`);try{let n=await(t==="refetch"&&l?l(...a):m(...a));return o(`Successfully fetched ${e}`),V(e,n,Date.now()),n}catch(n){throw o(`Failed to fetch ${e} for queryType=${t}: ${n}`),n}finally{p.delete(e)}}function x(e,t,...a){let n=p.get(e);if(n)return o(`Deduplicating inflight fetch for ${e}, queryType=${t}`),n.promise;let s=X(e,t,a),d=new AbortController;return p.set(e,{promise:s,abortController:d}),s}let S={queryName:u,queryFn:m,refetchFn:l,expiryMs:D,staleMs:y,refetchOnStale:M,maxSize:T,maxBytes:i,keepLastNonEmptyData:c,log:o,queryState:f,getQueryKey(e){let t=_chunkTEZPF6BGmincjs.a.call(void 0, e);return t.length>30&&(t=_xxhashjs2.default.h32(t,39406).toString(16).slice(2)),u+":"+t},getAbortController(e){let t=S.getQueryKey(e);return S.getAbortControllerByKey(t)},getAbortControllerByKey(e){return _nullishCoalesce(_optionalChain([p, 'access', _10 => _10.get, 'call', _11 => _11(e), 'optionalAccess', _12 => _12.abortController]), () => (null))},getAbortSignal(e){let t=S.getQueryKey(e);return S.getAbortSignalByKey(t)},getAbortSignalByKey(e){return _nullishCoalesce(_optionalChain([S, 'access', _13 => _13.getAbortControllerByKey, 'call', _14 => _14(e), 'optionalAccess', _15 => _15.signal]), () => (null))},async getCachedOrFetch(...e){let t=Date.now();await f.readyPromise;let a=Date.now()-t,n=S.getQueryKey(e),s=_optionalChain([f, 'access', _16 => _16.getSnapshot, 'call', _17 => _17(), 'optionalAccess', _18 => _18[n]]);if(a&&o(`${n}: Waited for ${a}ms before data is ready`),_optionalChain([s, 'optionalAccess', _19 => _19.data])&&!C(_nullishCoalesce(s.dataUpdatedMs, () => (0))))return _(_nullishCoalesce(s.dataUpdatedMs, () => (0)))?(M&&(o(`refetchOnStale for ${n}`),x(n,"query",...e)),o(`Returning stale data for ${n}`),s):(o(`Return fresh data ${n} from cache without fetching`),s);let d=await x(n,"query",...e),b=Date.now();return{lastUpdatedMs:b,loading:!1,data:d,dataUpdatedMs:b}},getQueryValue(e){return _optionalChain([f, 'access', _20 => _20.getSnapshot, 'call', _21 => _21(), 'optionalAccess', _22 => _22[e]])},getData(e){let t=_optionalChain([f, 'access', _23 => _23.getSnapshot, 'call', _24 => _24(), 'optionalAccess', _25 => _25[e]]);if(_optionalChain([t, 'optionalAccess', _26 => _26.lastUpdatedMs])&&C(t.lastUpdatedMs)){S.deleteData(e);return}return _optionalChain([t, 'optionalAccess', _27 => _27.data])},setData:V,async updateFromSource(...e){let t=S.getQueryKey(e);return await x(t,"query",...e)},async refetch(...e){let t=S.getQueryKey(e);return await x(t,"refetch",...e)},deleteData(e){let a={...f.getSnapshot()};delete a[e],f.setValue(a);let n=p.get(e);n&&(p.delete(e),n.abortController.abort("Deleted"))},clear(){f.delete();for(let{abortController:e}of Array.from(p.values()))e.abort("Cleared");p.clear()},mount(e){h.set(e,(_nullishCoalesce(h.get(e), () => (0)))+1)},unmount(e){let t=h.get(e);t===1?h.delete(e):h.set(e,(_nullishCoalesce(t, () => (0)))-1)},enforceSizeLimit:k};return S}var K={lastUpdatedMs:0,loading:!0};function ue(r,...w){let u=_chunkF5L32IUCmincjs.a.call(void 0, w),[p,h]=_chunkNTFFK7ZHmincjs.d.call(void 0, r.queryState),m=_react.useRef.call(void 0, !0),l=_react.useMemo.call(void 0, ()=>r.getQueryKey(u),[r,u]),y=_nullishCoalesce(p[l], () => (K)),D=_react.useCallback.call(void 0, i=>{h(c=>{let o={...c};return o[l]=typeof i=="function"?i(_nullishCoalesce(o[l], () => (K))):i,o})},[l,h]),[M,A]=_react.useState.call(void 0, );_react.useEffect.call(void 0, ()=>{r.keepLastNonEmptyData&&y.data!==void 0&&A(y.data)},[r.keepLastNonEmptyData,y.data]);let T=_react.useCallback.call(void 0, async i=>{if(r.log(`Begin executing shared query ${l}, isRefetch=${i}`),!!m.current){D(c=>({...c,lastUpdatedMs:Date.now(),loading:!0}));try{let c;if(i){let o=await r.refetch(...u),f=Date.now();c={lastUpdatedMs:f,loading:!1,data:o,dataUpdatedMs:f}}else c=await r.getCachedOrFetch(...u);if(!m.current)return;D({...c,loading:!1})}catch(c){if(!m.current)return;let o=Date.now();D(f=>({...f,lastUpdatedMs:o,loading:!1,error:c,errorUpdatedMs:o}))}}},[r,l,D,u]);return _react.useEffect.call(void 0, ()=>(m.current=!0,r.mount(l),T(!1),()=>{m.current=!1,r.unmount(l)}),[T,r,l]),_react.useMemo.call(void 0, ()=>({...y,data:y.data!==void 0?y.data:M,latestData:y.data,abortCurrentQuery:i=>{let c=r.getAbortControllerByKey(l);return _optionalChain([c, 'optionalAccess', _28 => _28.abort, 'call', _29 => _29(i)]),!!c},refetch:()=>T(!0),setData:(i,c)=>{r.setData(l,i,c)},deleteData:()=>{r.deleteData(l)}}),[T,M,r,l,y])}exports.SharedQueryConfig = Q; exports.configureSharedQuery = se; exports.sharedQuery = ie; exports.useSharedQuery = ue;
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _chunkNTFFK7ZHmincjs = require('./chunk-NTFFK7ZH.min.cjs');require('./chunk-UFJQLQFI.min.cjs');var _chunkTEZPF6BGmincjs = require('./chunk-TEZPF6BG.min.cjs');var _chunkF5L32IUCmincjs = require('./chunk-F5L32IUC.min.cjs');require('./chunk-KE3RXVMS.min.cjs');var _http = require('@choksheak/ts-utils/http');var _objectsizeof = require('object-sizeof'); var _objectsizeof2 = _interopRequireDefault(_objectsizeof);var _react = require('react');var _xxhashjs = require('xxhashjs'); var _xxhashjs2 = _interopRequireDefault(_xxhashjs);_chunkTEZPF6BGmincjs.b.call(void 0, );var Q={staleMs:0,expiryMs:30*864e5,refetchOnStale:!0,persistTo:void 0,maxSize:100,maxBytes:1e5,log:(...a)=>console.log("[sharedQuery]",...a)};function ie(a){Object.assign(Q,a)}var j=new Set;function ue(a){let M=a.url||"",i=a.queryName||M;if(!i)throw new Error("Either queryName or url must be specified.");if(j.has(i))throw new Error(`Duplicate shared query name "${i}"`);j.add(i);let T=new Map,h=new Map,m;if(a.queryFn)m=a.queryFn;else if(M){let e=_http.http.use(_http.retries.call(void 0, ));m=async(...t)=>{let r="",n=_optionalChain([t, 'optionalAccess', _2 => _2[0]]);if(n){if(typeof n=="string")r=n;else if(typeof n=="object"){let f=[];for(let[b,w]of Object.entries(n))if(w!==void 0)if(Array.isArray(w))for(let $ of w)$!==void 0&&f.push([b,String($)]);else f.push([b,String(w)]);r=new URLSearchParams(f).toString()}r&&(M.includes("?")?r="&"+r:r="?"+r)}return await(await e.get(M+r)).json()}}else throw new Error("Either queryFn or url must be specified");let u=a.refetchFn,y=_nullishCoalesce(_optionalChain([a, 'optionalAccess', _3 => _3.staleMs]), () => (Q.staleMs)),D=_nullishCoalesce(_optionalChain([a, 'optionalAccess', _4 => _4.expiryMs]), () => (Q.expiryMs)),x=_nullishCoalesce(_optionalChain([a, 'optionalAccess', _5 => _5.refetchOnStale]), () => (Q.refetchOnStale)),A=a.store;if(A){if("persistTo"in A&&A.isValid){let e=A.isValid;a={...a,store:{...A,isValid:t=>{if(!t||typeof t!="object")return!1;for(let r of Object.values(t))if(!r||typeof r!="object"||!("data"in r)||!("dataUpdatedMs"in r)||!("lastUpdatedMs"in r)||typeof r.dataUpdatedMs!="number"||typeof r.lastUpdatedMs!="number"||!e(r.data))return!1;return!0}}}}}else{let e=_nullishCoalesce(a.persistTo, () => (Q.persistTo));e&&(a={...a,store:{persistTo:e,key:i,expiryMs:D}})}let p=_nullishCoalesce(_optionalChain([a, 'optionalAccess', _6 => _6.maxSize]), () => (Q.maxSize)),s=_nullishCoalesce(_optionalChain([a, 'optionalAccess', _7 => _7.maxBytes]), () => (Q.maxBytes)),d=!!_optionalChain([a, 'optionalAccess', _8 => _8.keepLastNonEmptyData]),o=_nullishCoalesce(_optionalChain([a, 'optionalAccess', _9 => _9.log]), () => (Q.log)),c=_chunkNTFFK7ZHmincjs.c.call(void 0, {},a);function K(e){return y===0?!0:Date.now()-e>y}function k(e){return D===0?!1:Date.now()-e>D}function V(){if(!p&&!s)return;let e=c.getSnapshot(),t=Object.keys(e).length;if(t<=1)return;let r=_objectsizeof2.default.call(void 0, e),n=p?t-p:0,l=s?r-s:0;if(n<=0&&l<=0){o(`No need to trim data for ${i}: size=${t} (limit=${p}), byteSize=${r.toLocaleString()} (limit=${s.toLocaleString()})`);return}o(`Need to trim ${i}: numToCut=${n}, bytesToCut=${l}`);let f={...e},b=!1,w=(g,U)=>{if(h.has(g)){o(`Cannot clean up ${g} as it is mounted`);return}let O=_objectsizeof2.default.call(void 0, g)+_objectsizeof2.default.call(void 0, f[g]);o(`Cleaning up unmounted${U?", expired":""} ${g} (byteSize=${O.toLocaleString()})`),n--,l-=O,delete f[g],b=!0},$=Object.entries(f).sort((g,U)=>g[1].lastUpdatedMs-U[1].lastUpdatedMs);for(let[g]of $){if(!k(f[g].lastUpdatedMs))break;w(g,!0)}if(n>0||l>0){b&&($=Object.entries(f).sort((g,U)=>g[1].lastUpdatedMs-U[1].lastUpdatedMs));for(let[g]of $)if(w(g,!1),n<=0&&l<=0)break}if(!b){o(`No trimmable entries found for ${i}: size=${t} (limit=${p}), byteSize=${r.toLocaleString()} (limit=${s.toLocaleString()})`);return}c.setValue(f),o(`Trimmed data for ${i}: size=[${t} -> ${Object.keys(f).length}] (limit=${p}), byteSize=[${r.toLocaleString()} -> ${_objectsizeof2.default.call(void 0, f).toLocaleString()}] (limit=${s.toLocaleString()})`)}function v(e,t,r=Date.now()){let n={lastUpdatedMs:Date.now(),loading:!1,data:t,dataUpdatedMs:r};c.setValue(l=>({...l,[e]:n})),V()}async function _(e,t,r){o(`Start fetching ${e}, queryType=${t}`);try{let n=await(t==="refetch"&&u?u(...r):m(...r));return o(`Successfully fetched ${e}`),v(e,n,Date.now()),n}catch(n){throw o(`Failed to fetch ${e} for queryType=${t}: ${n}`),n}finally{T.delete(e)}}function C(e,t,...r){let n=T.get(e);if(n)return o(`Deduplicating inflight fetch for ${e}, queryType=${t}`),n.promise;let l=_(e,t,r),f=new AbortController;return T.set(e,{promise:l,abortController:f}),l}let S={queryName:i,queryFn:m,refetchFn:u,expiryMs:D,staleMs:y,refetchOnStale:x,maxSize:p,maxBytes:s,keepLastNonEmptyData:d,log:o,queryState:c,getQueryKey(e){let t=_chunkTEZPF6BGmincjs.a.call(void 0, e);return t.length>30&&(t=_xxhashjs2.default.h32(t,39406).toString(16).slice(2)),i+":"+t},getAbortController(e){let t=S.getQueryKey(e);return S.getAbortControllerByKey(t)},getAbortControllerByKey(e){return _nullishCoalesce(_optionalChain([T, 'access', _10 => _10.get, 'call', _11 => _11(e), 'optionalAccess', _12 => _12.abortController]), () => (null))},getAbortSignal(e){let t=S.getQueryKey(e);return S.getAbortSignalByKey(t)},getAbortSignalByKey(e){return _nullishCoalesce(_optionalChain([S, 'access', _13 => _13.getAbortControllerByKey, 'call', _14 => _14(e), 'optionalAccess', _15 => _15.signal]), () => (null))},async getCachedOrFetch(...e){let t=Date.now();await c.readyPromise;let r=Date.now()-t,n=S.getQueryKey(e),l=_optionalChain([c, 'access', _16 => _16.getSnapshot, 'call', _17 => _17(), 'optionalAccess', _18 => _18[n]]);if(r&&o(`${n}: Waited for ${r}ms before data is ready`),_optionalChain([l, 'optionalAccess', _19 => _19.data])&&!k(_nullishCoalesce(l.dataUpdatedMs, () => (0))))return K(_nullishCoalesce(l.dataUpdatedMs, () => (0)))?(x&&(o(`refetchOnStale for ${n}`),C(n,"query",...e)),o(`Returning stale data for ${n}`),l):(o(`Return fresh data ${n} from cache without fetching`),l);let f=await C(n,"query",...e),b=Date.now();return{lastUpdatedMs:b,loading:!1,data:f,dataUpdatedMs:b}},getQueryValue(e){return _optionalChain([c, 'access', _20 => _20.getSnapshot, 'call', _21 => _21(), 'optionalAccess', _22 => _22[e]])},getData(e){let t=_optionalChain([c, 'access', _23 => _23.getSnapshot, 'call', _24 => _24(), 'optionalAccess', _25 => _25[e]]);if(_optionalChain([t, 'optionalAccess', _26 => _26.lastUpdatedMs])&&k(t.lastUpdatedMs)){S.deleteData(e);return}return _optionalChain([t, 'optionalAccess', _27 => _27.data])},setData:v,async updateFromSource(...e){let t=S.getQueryKey(e);return await C(t,"query",...e)},async refetch(...e){let t=S.getQueryKey(e);return await C(t,"refetch",...e)},deleteData(e){let r={...c.getSnapshot()};delete r[e],c.setValue(r);let n=T.get(e);n&&(T.delete(e),n.abortController.abort("Deleted"))},clear(){c.delete();for(let{abortController:e}of Array.from(T.values()))e.abort("Cleared");T.clear()},mount(e){h.set(e,(_nullishCoalesce(h.get(e), () => (0)))+1)},unmount(e){let t=h.get(e);t===1?h.delete(e):h.set(e,(_nullishCoalesce(t, () => (0)))-1)},enforceSizeLimit:V};return S}var W={lastUpdatedMs:0,loading:!0};function le(a,...M){let i=_chunkF5L32IUCmincjs.a.call(void 0, M),[T,h]=_chunkNTFFK7ZHmincjs.d.call(void 0, a.queryState),m=_react.useRef.call(void 0, !0),u=_react.useMemo.call(void 0, ()=>a.getQueryKey(i),[a,i]),y=_nullishCoalesce(T[u], () => (W)),D=_react.useCallback.call(void 0, s=>{h(d=>{let o={...d};return o[u]=typeof s=="function"?s(_nullishCoalesce(o[u], () => (W))):s,o})},[u,h]),[x,A]=_react.useState.call(void 0, );_react.useEffect.call(void 0, ()=>{a.keepLastNonEmptyData&&y.data!==void 0&&A(y.data)},[a.keepLastNonEmptyData,y.data]);let p=_react.useCallback.call(void 0, async s=>{if(a.log(`Begin executing shared query ${u}, isRefetch=${s}`),!!m.current){D(d=>({...d,lastUpdatedMs:Date.now(),loading:!0}));try{let d;if(s){let o=await a.refetch(...i),c=Date.now();d={lastUpdatedMs:c,loading:!1,data:o,dataUpdatedMs:c}}else d=await a.getCachedOrFetch(...i);if(!m.current)return;D({...d,loading:!1})}catch(d){if(!m.current)return;let o=Date.now();D(c=>({...c,lastUpdatedMs:o,loading:!1,error:d,errorUpdatedMs:o}))}}},[a,u,D,i]);return _react.useEffect.call(void 0, ()=>(m.current=!0,a.mount(u),p(!1),()=>{m.current=!1,a.unmount(u)}),[p,a,u]),_react.useMemo.call(void 0, ()=>({...y,data:y.data!==void 0?y.data:x,latestData:y.data,abortCurrentQuery:s=>{let d=a.getAbortControllerByKey(u);return _optionalChain([d, 'optionalAccess', _28 => _28.abort, 'call', _29 => _29(s)]),!!d},refetch:()=>p(!0),setData:(s,d)=>{a.setData(u,s,d)},deleteData:()=>{a.deleteData(u)}}),[p,x,a,u,y])}exports.SharedQueryConfig = Q; exports.configureSharedQuery = ie; exports.sharedQuery = ue; exports.useSharedQuery = le;
2
2
  //# sourceMappingURL=sharedQuery.min.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/choksheaklau/src/react-utils/dist/sharedQuery.min.cjs","../src/sharedQuery.ts"],"names":["init_stringify","SharedQueryConfig","args","configureSharedQuery","config","seenQueryNames","sharedQuery","options","url","queryName","inflightQueries","mountedKeys","queryFn","params","firstArg","entries","k","v","item","fetcher","refetchFn","staleMs","expiryMs","refetchOnStale","store","isValid","u","persistTo","maxSize","maxBytes","keepLastNonEmptyData","log","queryState","sharedState","isStale","dataUpdatedMs","isExpired","enforceSizeLimit","oldState","oldSize","oldByteSize","getByteSize","numToCut","bytesToCut"],"mappings":"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","file":"/Users/choksheaklau/src/react-utils/dist/sharedQuery.min.cjs","sourcesContent":[null,"/**\n * @packageDocumentation\n *\n * The \"shared query\" is a data fetching convenience library that helps users\n * to load data, check for staleness and expiration, cache in memory, local\n * storage or indexed db, and be able to manipulate the data anytime.\n *\n * Each shared query creates a new private shared state that handles the data\n * storage. This shared state contains the records for all queries, where each\n * query is a unique set of arguments used to make the query. The number of\n * records and the total byte size of data can be limited so as to ensure the\n * query does not take up too much memory.\n *\n * Think of shared queries as like the open source npm package \"react-query\",\n * but with strong type-checking and built-in support for persistence.\n *\n * Example:\n * ```\n * import { sharedQuery, useSharedQuery } from \"@choksheak/react-utils/sharedQuery\";\n * import { MS_PER_DAY } from \"@choksheak/ts-utils/timeConstants\";\n *\n * // Create a new shared query in the top level scope.\n * // The query function `queryFn` here does not take any arguments.\n * //\n * // Only the queryName and queryFn are required and everything else is\n * // optional.\n * //\n * // The queryName must be unique, otherwise we will throw an\n * // error. This is because defining the same queryName for two different\n * // queries implies that the queries are the same, thus violating the idea\n * // of a \"shared\" query, and almost certainly points to a bug in the code.\n * export const usersQuery = sharedQuery({\n * queryName: \"users\",\n * queryFn: listUsers,\n * persistTo: \"indexedDb\",\n * staleMs: MS_PER_DAY,\n * });\n *\n * // Example of using a query function with arguments.\n * export const getUserQuery = sharedQuery({\n * queryName: \"getUser\",\n * queryFn: getUser, // getUser(userId: string) => User\n * persistTo: \"indexedDb\",\n * staleMs: MS_PER_DAY,\n * });\n *\n * export const UsersComponent: React.FC = () => {\n * // Second argument is optional, and is an array of arguments to be\n * // passed to the `queryFn`.\n * const users = useSharedQuery(usersQuery);\n *\n * return (\n * <>\n * <h1>List of Users</h1>\n *\n * <button onclick={users.refetch}>Refresh</button>\n *\n * {users.loading && <Loader />}\n *\n * {users.error && !users.data && <p>Error: {String(users.error)}</p>}\n *\n * <p>Data: {users.data ? JSON.stringify(users.data) : \"-\"}</p>\n * </>\n * );\n * };\n *\n * export const AnotherComponent: React.FC = () => {\n * // This query will be deduped with the one above, sharing the same\n * // data and the same fetches.\n * const users = useSharedQuery(usersQuery);\n * ...\n * };\n *\n * export const DisplayOneUser: React.FC<{userId: string}> = ({userId}) => {\n * // Example of using the query with an argument. Whenever the argument\n * // changes, the data will be fetched and updated automatically.\n * const user = useSharedQuery(getUserQuery, userId);\n *\n * // You can check for user.loading and user.error if needed.\n *\n * return <>User: {user.data ? JSON.stringify(user.data) : \"-\"}</>;\n * };\n * ```\n */\n\nimport getByteSize from \"object-sizeof\";\nimport {\n SetStateAction,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport XXH from \"xxhashjs\";\n\nimport { fetcher } from \"./fetcher\";\nimport { sharedState, SharedStateOptions, useSharedState } from \"./sharedState\";\nimport { PersistTo } from \"./storage\";\nimport { stringifyDeterministicForKeys } from \"./stringify\";\nimport { useDeepMemo } from \"./useDeepMemo\";\n\n/** Type of the queryFn used to fetch the data from source. */\nexport type QueryFn<TArgs extends unknown[], TData> = (\n ...args: TArgs\n) => Promise<TData> | TData;\n\n/** An entry for a persisted state corresponding to one query key. */\nexport type QueryStateValue<TData> = Readonly<{\n /**\n * When this record was last updated. Used for LRU determination. If you want\n * the data age, please use `dataUpdatedMs` instead.\n */\n lastUpdatedMs: number;\n\n loading: boolean;\n data?: TData;\n dataUpdatedMs?: number;\n\n /**\n * Use unknown type for error because we can't assume what type of variable\n * will be thrown from the query function. We also don't want to add this\n * type to QueryStateValue<TError> as it adds complexity to the type.\n */\n error?: unknown;\n\n errorUpdatedMs?: number;\n}>;\n\n/** Return type for useSharedQuery(). */\nexport type UseQueryResult<TData> = QueryStateValue<TData> &\n Readonly<{\n /**\n * Usually same as `data`, but if you specify `keepLastNonEmptyData: true`,\n * then this will contain the latest data (undefined when args just changed)\n * and data will contain the last non empty data.\n */\n latestData: TData | undefined;\n\n /**\n * Cancels the inflight query (if any). Returns true if canceled.\n */\n abortCurrentQuery: (reason?: unknown) => boolean;\n\n /**\n * Manually refresh the data by doing a new query.\n */\n refetch: () => Promise<void>;\n\n /**\n * If you got the data from somewhere else (e.g. an in-memory data update),\n * just set it directly.\n */\n setData: (data: TData, dataUpdatedMs?: number) => void;\n\n deleteData: () => void;\n }>;\n\n/**\n * The full query state is a record of all the fetch states mapped by\n * query key to the state entry.\n */\nexport type SharedQueryState<TData> = Record<string, QueryStateValue<TData>>;\n\nexport type QueryTargetWithFn<TArgs extends unknown[], TData> = {\n /**\n * The name could have been auto-generated, but we let the user give us a\n * human-readable name that can be identified quickly in the logs.\n * Note that the queryKey is a string formed by the queryName plus the query\n * arguments.\n */\n queryName: string;\n\n /** Function to fetch data. */\n queryFn: QueryFn<TArgs, TData>;\n};\n\nexport type QueryTargetWithUrl = {\n /**\n * The queryName can optionally be specified, else we will fallback to use\n * the url as the queryName.\n */\n queryName?: string;\n\n /**\n * URL to fetch data using HTTP GET. Note that using this field will cause\n * both TArgs and TData to be undefined (unknown). Therefore you can make up\n * for this by specifying types with useSharedQuery, like this:\n * ```\n * const usersQuery = useSharedQuery<[{ userId?: string[] }], User[]>({\n * url: \"/api/list-users\",\n * });\n * ```\n *\n * If `userId` were specified as [\"u1\", \"u2\"], the query url would be\n * \"/api/list-users?userId=u1&userId=u2\".\n *\n * You can also pass the params as a string like \"userId=u1&userId=u2\".\n * ```\n * const usersQuery = useSharedQuery<[string], User[]>({\n * url: \"/api/list-users\",\n * });\n * ```\n */\n url: string;\n};\n\n/** Specifies how to fetch the data. */\nexport type QueryTarget<TArgs extends unknown[], TData> =\n | QueryTargetWithFn<TArgs, TData>\n | QueryTargetWithUrl;\n\n/** Options to configure a shared query. */\nexport type SharedQueryOptions<TArgs extends unknown[], TData> = QueryTarget<\n TArgs,\n TData\n> & {\n /**\n * Function to re-fetch data. Usually this would just be the `queryFn`, but\n * if you need a different behavior for refetching, then specify this.\n */\n refetchFn?: QueryFn<TArgs, TData>;\n\n /**\n * ms before data considered stale; 0 means always stale.\n */\n staleMs?: number;\n\n /**\n * ms before data is removed from cache; 0 means never expire.\n */\n expiryMs?: number;\n\n /**\n * Trigger background re-fetch if stale.\n */\n refetchOnStale?: boolean;\n\n /**\n * Shortcut to set the localStorageKey or indexedDbKey automatically from\n * queryName. This is important because it avoids requiring the user to\n * specify the same queryName twice everytime.\n */\n persistTo?: PersistTo;\n\n /**\n * Max number of entries to keep. The oldest entries will be discarded.\n * The last record cannot be discarded. 0 means no limit.\n */\n maxSize?: number;\n\n /**\n * Max number of bytes to keep. The oldest entries will be discarded.\n * The last record cannot be discarded. 0 means no limit.\n */\n maxBytes?: number;\n\n /**\n * Keep a copy of the last non-empty data so that when you change the\n * query arguments, you can still display the old data while waiting for the\n * new data to load.\n */\n keepLastNonEmptyData?: boolean;\n\n /**\n * Customize the logger.\n */\n log?: (...args: unknown[]) => void;\n} & SharedStateOptions<SharedQueryState<TData>>;\n\n/** Users can override these values globally. */\nexport const SharedQueryConfig = {\n /** Refetch on every page load. */\n staleMs: 0,\n\n /** Keep in cache for a long time. */\n expiryMs: 30 * 86_400_000 /* MS_PER_DAY */,\n\n /** Always refetch automatically if the data is stale. */\n refetchOnStale: true,\n\n /** No default persistence configured. */\n persistTo: undefined as PersistTo | undefined,\n\n /** 100 is not too large, but please tweak accordingly. */\n maxSize: 100,\n\n /** 100kb limit before discarding old records. */\n maxBytes: 100_000,\n\n /** Default to log to console. */\n log: (...args: unknown[]) => console.log(\"[sharedQuery]\", ...args),\n};\n\nexport type SharedQueryConfig = typeof SharedQueryConfig;\n\n/** Convenience function to update global defaults. */\nexport function configureSharedQuery(config: Partial<SharedQueryConfig>) {\n Object.assign(SharedQueryConfig, config);\n}\n\n/** Whether this is a normal query or an explicit refetch. */\ntype QueryType = \"query\" | \"refetch\";\n\n// Disallow duplicate query names.\nconst seenQueryNames = new Set<string>();\n\n/**\n * Create a reusable shared query object that can be used to auto de-duplicate\n * and cache all data fetches, with auto-expiration and staleness checks.\n *\n * Each `queryName` must be unique, and can only be declared once, most often\n * in the top level scope. The queryName is mainly used for logging and also\n * used as the storage key when you use the top-level `persistTo`.\n *\n * Example:\n * ```\n * export const usersQuery = sharedQuery({\n * queryName: \"users\",\n * queryFn: listUsers,\n * persistTo: \"indexedDb\",\n * staleMs: MS_PER_DAY,\n * });\n * ```\n */\nexport function sharedQuery<TArgs extends unknown[], TData>(\n // The name could have been auto-generated, but we let the user give us a\n // human-readable name that can be identified quickly in the logs.\n options: SharedQueryOptions<TArgs, TData>,\n) {\n // If queryName is not specified, fallback to use url as the queryName.\n const url = (options as QueryTargetWithUrl).url || \"\";\n const queryName = options.queryName || url;\n\n if (!queryName) {\n throw new Error(`Either queryName or url must be specified.`);\n }\n\n if (seenQueryNames.has(queryName)) {\n throw new Error(`Duplicate shared query name \"${queryName}\"`);\n }\n seenQueryNames.add(queryName);\n\n const inflightQueries = new Map<\n string,\n { promise: Promise<TData>; abortController: AbortController }\n >();\n\n /**\n * Keep track of the mounted query keys. This handles the case where an\n * earlier-started query returns later, and when it returns, it was already\n * unmounted, thus making it eligible for clean up right after fetch.\n */\n const mountedKeys = new Map<string, number>();\n\n let queryFn: QueryFn<TArgs, TData>;\n\n if ((options as QueryTargetWithFn<TArgs, TData>).queryFn) {\n queryFn = (options as QueryTargetWithFn<TArgs, TData>).queryFn;\n } else if (url) {\n // Auto-generate the queryFn as a HTTP GET request using the url.\n queryFn = (...args: TArgs): Promise<TData> => {\n // If args are given, treat the first argument as a search param.\n let params = \"\";\n const firstArg = args?.[0];\n\n if (firstArg) {\n if (typeof firstArg === \"string\") {\n params = firstArg;\n } else if (typeof firstArg === \"object\") {\n const entries: [string, string][] = [];\n\n for (const [k, v] of Object.entries(firstArg)) {\n if (v === undefined) continue;\n\n // Allow the same param to be specified multiple times.\n if (Array.isArray(v)) {\n for (const item of v) {\n if (item === undefined) continue;\n\n // The field values can only be primitives, not objects.\n entries.push([k, String(item)]);\n }\n } else {\n // The field values can only be primitives, not objects.\n entries.push([k, String(v)]);\n }\n }\n\n params = new URLSearchParams(entries).toString();\n }\n\n if (params) {\n if (url.includes(\"?\")) {\n // Append more search params to existing ones.\n params = \"&\" + params;\n } else {\n // Set the full search params string.\n params = \"?\" + params;\n }\n }\n }\n\n return fetcher\n .url(url + params)\n .get()\n .json();\n };\n } else {\n throw new Error(`Either queryFn or url must be specified`);\n }\n\n const refetchFn = options.refetchFn;\n\n const staleMs = options?.staleMs ?? SharedQueryConfig.staleMs;\n const expiryMs = options?.expiryMs ?? SharedQueryConfig.expiryMs;\n\n const refetchOnStale =\n options?.refetchOnStale ?? SharedQueryConfig.refetchOnStale;\n\n // Apply shortcut when using `persistTo`.\n // Specifying the `store` will take precedence over `persistTo`.\n const store = options.store;\n\n if (store) {\n // Wrap the isValid to account for the query state wrapper.\n if (\"persistTo\" in store && store.isValid) {\n const isValid = store.isValid;\n\n options = {\n ...options,\n store: {\n ...store,\n isValid: (u) => {\n // Supposed type of u: SharedQueryState<TData>\n if (!u || typeof u !== \"object\") {\n return false;\n }\n\n // Since each stored key could contain multiple values, and isValid\n // checks for each value, we need to check every value to make sure\n // they are valid.\n // Supposed type of v: QueryStateValue<TData>\n for (const v of Object.values(u)) {\n if (\n !v ||\n typeof v !== \"object\" ||\n !(\"data\" in v) ||\n !(\"dataUpdatedMs\" in v) ||\n !(\"lastUpdatedMs\" in v) ||\n typeof v.dataUpdatedMs !== \"number\" ||\n typeof v.lastUpdatedMs !== \"number\" ||\n !isValid(v.data)\n ) {\n return false;\n }\n }\n\n return true;\n },\n },\n };\n }\n } else {\n const persistTo = options.persistTo ?? SharedQueryConfig.persistTo;\n\n if (persistTo) {\n options = {\n ...options,\n store: {\n persistTo,\n key: queryName,\n expiryMs,\n },\n };\n }\n }\n\n const maxSize = options?.maxSize ?? SharedQueryConfig.maxSize;\n const maxBytes = options?.maxBytes ?? SharedQueryConfig.maxBytes;\n\n const keepLastNonEmptyData = Boolean(options?.keepLastNonEmptyData);\n\n const log = options?.log ?? SharedQueryConfig.log;\n\n const queryState = sharedState<SharedQueryState<TData>>(\n {}, // defaultValue\n options,\n );\n\n function isStale(dataUpdatedMs: number): boolean {\n if (staleMs === 0) return true; // always stale\n\n const ageMs = Date.now() - dataUpdatedMs;\n return ageMs > staleMs;\n }\n\n function isExpired(dataUpdatedMs: number): boolean {\n if (expiryMs === 0) return false; // never expire\n\n const ageMs = Date.now() - dataUpdatedMs;\n return ageMs > expiryMs;\n }\n\n function enforceSizeLimit(): void {\n // Skip if there are no limits.\n if (!maxSize && !maxBytes) {\n return;\n }\n\n const oldState = queryState.getSnapshot();\n const oldSize = Object.keys(oldState).length;\n\n // Cannot do GC if size is 1 or less.\n if (oldSize <= 1) {\n return;\n }\n\n const oldByteSize = getByteSize(oldState);\n\n let numToCut = maxSize ? oldSize - maxSize : 0;\n let bytesToCut = maxBytes ? oldByteSize - maxBytes : 0;\n\n // Log to indicate why trimming was not needed.\n if (numToCut <= 0 && bytesToCut <= 0) {\n log(\n `No need to trim data for ${queryName}: size=${oldSize} (limit=${maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${maxBytes.toLocaleString()})`,\n );\n return;\n }\n\n // Log to inform user that trimming is needed.\n log(\n `Need to trim ${queryName}: numToCut=${numToCut}, bytesToCut=${bytesToCut}`,\n );\n\n const newState = { ...oldState }; // shallow clone\n let needUpdate = false;\n\n const deleteKeyIfNotMounted = (key: string, expired: boolean) => {\n // Mounted keys cannot be cleaned up as they are visible in the UI.\n if (mountedKeys.has(key)) {\n log(`Cannot clean up ${key} as it is mounted`);\n return;\n }\n\n const byteSize = getByteSize(key) + getByteSize(newState[key]);\n log(\n `Cleaning up unmounted${expired ? \", expired\" : \"\"} ${key} (byteSize=${byteSize.toLocaleString()})`,\n );\n\n numToCut--;\n bytesToCut -= byteSize;\n\n delete newState[key];\n needUpdate = true;\n };\n\n let entriesByTimeAscending = Object.entries(newState).sort(\n (entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs,\n );\n\n // Clean up all expired entries. Note that the auto-expiration in the\n // storage adapter only applies to the entire state, which contains all\n // the different query keys and their values. Each query key has its own\n // timestamp and we need to apply our own expiration here.\n for (const [key] of entriesByTimeAscending) {\n // Anything after this is not expired yet.\n if (!isExpired(newState[key].lastUpdatedMs)) {\n break;\n }\n\n deleteKeyIfNotMounted(key, true);\n }\n\n if (numToCut > 0 || bytesToCut > 0) {\n if (needUpdate) {\n entriesByTimeAscending = Object.entries(newState).sort(\n (entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs,\n );\n }\n\n // Clean up starting from oldest entry first.\n for (const [key] of entriesByTimeAscending) {\n deleteKeyIfNotMounted(key, false);\n\n if (numToCut <= 0 && bytesToCut <= 0) {\n break;\n }\n }\n }\n\n // Log to indicate why trimming was not needed.\n if (!needUpdate) {\n log(\n `No trimmable entries found for ${queryName}: size=${oldSize} (limit=${maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${maxBytes.toLocaleString()})`,\n );\n return;\n }\n\n // Update the state.\n queryState.setValue(newState);\n\n // Log to inform user that the data was trimmed.\n log(\n `Trimmed data for ${queryName}: size=[${oldSize} -> ${Object.keys(newState).length}] (limit=${maxSize}), byteSize=[${oldByteSize.toLocaleString()} -> ${getByteSize(newState).toLocaleString()}] (limit=${maxBytes.toLocaleString()})`,\n );\n }\n\n function setData(\n queryKey: string,\n data: TData,\n dataUpdatedMs = Date.now(),\n ): void {\n const entry: QueryStateValue<TData> = {\n lastUpdatedMs: Date.now(),\n loading: false,\n data,\n dataUpdatedMs,\n };\n\n queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));\n\n enforceSizeLimit();\n }\n\n async function startFetching(\n queryKey: string,\n queryType: QueryType,\n args: TArgs,\n ) {\n log(`Start fetching ${queryKey}, queryType=${queryType}`);\n\n try {\n const data = await (queryType === \"refetch\" && refetchFn\n ? refetchFn(...args)\n : queryFn(...args));\n\n log(`Successfully fetched ${queryKey}`);\n setData(queryKey, data, Date.now());\n return data;\n } catch (e) {\n log(`Failed to fetch ${queryKey} for queryType=${queryType}: ${e}`);\n throw e;\n } finally {\n inflightQueries.delete(queryKey);\n }\n }\n\n function dedupedFetch(\n queryKey: string,\n queryType: QueryType,\n ...args: TArgs\n ): Promise<TData> {\n const inflightQuery = inflightQueries.get(queryKey);\n\n // De-duplicate in-flight requests. Note that queries and refetches are\n // also deduped because they should technically be trying to fetch the same\n // data.\n if (inflightQuery) {\n log(\n `Deduplicating inflight fetch for ${queryKey}, queryType=${queryType}`,\n );\n return inflightQuery.promise;\n }\n\n // Fetch new data\n const promise = startFetching(queryKey, queryType, args);\n const abortController = new AbortController();\n\n inflightQueries.set(queryKey, { promise, abortController });\n\n return promise;\n }\n\n const obj = {\n queryName,\n queryFn,\n refetchFn,\n expiryMs,\n staleMs,\n refetchOnStale,\n maxSize,\n maxBytes,\n keepLastNonEmptyData,\n log,\n queryState,\n\n /** Returns a key to identify requests based on the given args. */\n getQueryKey(args: TArgs): string {\n let s = stringifyDeterministicForKeys(args);\n\n // Ensure the query key is not too long.\n if (s.length > 30) {\n s = XXH.h32(s, 0x99ee).toString(16).slice(2); // remove \"0x\"\n }\n\n return queryName + \":\" + s;\n },\n\n /**\n * Get the AbortController to abort the inflight query (if any).\n *\n * Note that once you get the controller, you can also get the signal easily\n * using `controller.signal`. So if you need both the controller and the\n * signal, just use this function.\n */\n getAbortController(args: TArgs): AbortController | null {\n const queryKey = obj.getQueryKey(args);\n return obj.getAbortControllerByKey(queryKey);\n },\n\n getAbortControllerByKey(queryKey: string): AbortController | null {\n return inflightQueries.get(queryKey)?.abortController ?? null;\n },\n\n /**\n * Get the AbortSignal to check for query abortions.\n *\n * Example using fetch:\n * ```\n * const getUserQuery = sharedQuery({\n * queryName: \"getUser\",\n * queryFn: (userId: string) => {\n * // Get the signal for this current execution.\n * const signal = getUserQuery.getAbortSignal([userId]);\n *\n * // Pass the signal to fetch so that it can be aborted.\n * const response = await fetch(`/users/${userId}`, { signal });\n *\n * // Check for errors.\n * if (!response.ok) {\n * throw new Error(response.statusText);\n * }\n *\n * // Return the data.\n * return await response.json();\n * },\n * });\n * ```\n *\n * Example using `fetcher.ts`:\n * ```\n * const getUserQuery = sharedQuery({\n * queryName: \"getUser\",\n * queryFn: (userId: string) => {\n * // Get the signal for this current execution.\n * const signal = getUserQuery.getAbortSignal([userId]);\n *\n * // Pass the signal to fetch so that it can be aborted.\n * return fetcher.url(`/users/${userId}`).options({ signal }).get().json();\n * },\n * });\n * ```\n */\n getAbortSignal(args: TArgs): AbortSignal | null {\n const queryKey = obj.getQueryKey(args);\n return obj.getAbortSignalByKey(queryKey);\n },\n\n getAbortSignalByKey(queryKey: string): AbortSignal | null {\n return obj.getAbortControllerByKey(queryKey)?.signal ?? null;\n },\n\n /** For cached data, we need to return the correct timestamps as well. */\n async getCachedOrFetch(...args: TArgs): Promise<QueryStateValue<TData>> {\n // Important: wait for data to be loaded from cache first before trying to\n // fetch from server. This fixes a bad bug where the data is big, and the\n // code here starts a server fetch before the cache data loaded.\n const startMs = Date.now();\n await queryState.readyPromise;\n const waitedMs = Date.now() - startMs;\n\n const queryKey = obj.getQueryKey(args);\n const cached = queryState.getSnapshot()?.[queryKey];\n\n if (waitedMs) {\n log(`${queryKey}: Waited for ${waitedMs}ms before data is ready`);\n }\n\n if (cached?.data && !isExpired(cached.dataUpdatedMs ?? 0)) {\n // Return cached value if not stale.\n if (!isStale(cached.dataUpdatedMs ?? 0)) {\n log(`Return fresh data ${queryKey} from cache without fetching`);\n return cached;\n }\n\n // If stale, optionally update the data in the background.\n if (refetchOnStale) {\n log(`refetchOnStale for ${queryKey}`);\n void dedupedFetch(queryKey, \"query\", ...args);\n }\n\n // Still return stale data immediately.\n log(`Returning stale data for ${queryKey}`);\n return cached;\n }\n\n const data = await dedupedFetch(queryKey, \"query\", ...args);\n const now = Date.now();\n\n return {\n // Don't keep old error.\n lastUpdatedMs: now,\n loading: false,\n data,\n dataUpdatedMs: now,\n };\n },\n\n /** Get the entire stored entry for a query key. */\n getQueryValue(queryKey: string): QueryStateValue<TData> | undefined {\n return queryState.getSnapshot()?.[queryKey];\n },\n\n /** Get the current stored data for a query key. */\n getData(queryKey: string): TData | undefined {\n const stateValue: QueryStateValue<TData> | undefined =\n queryState.getSnapshot()?.[queryKey];\n\n // Check for expiration.\n if (stateValue?.lastUpdatedMs) {\n if (isExpired(stateValue.lastUpdatedMs)) {\n obj.deleteData(queryKey);\n return undefined;\n }\n }\n\n return stateValue?.data;\n },\n\n /** Set the data directly if the user obtained it from somewhere else. */\n setData,\n\n /**\n * Do a new fetch even when the data is already cached, but don't fetch if\n * another fetch is already inflight.\n */\n async updateFromSource(...args: TArgs): Promise<TData> {\n const key = obj.getQueryKey(args);\n return await dedupedFetch(key, \"query\", ...args);\n },\n\n /**\n * Perform an explicit refetch. If `options.refetchFn` was not specified,\n * then this would be identical to calling `obj.updateFromSource()`.\n */\n async refetch(...args: TArgs): Promise<TData> {\n const key = obj.getQueryKey(args);\n return await dedupedFetch(key, \"refetch\", ...args);\n },\n\n /** Delete the cached data for one set of arguments. */\n deleteData(queryKey: string): void {\n const record = queryState.getSnapshot();\n const clone = { ...record };\n delete clone[queryKey];\n queryState.setValue(clone);\n\n const inflight = inflightQueries.get(queryKey);\n if (inflight) {\n inflightQueries.delete(queryKey);\n inflight.abortController.abort(\"Deleted\");\n }\n },\n\n /** Delete all currently cached data & all inflight promises. */\n clear(): void {\n queryState.delete();\n\n for (const { abortController } of Array.from(inflightQueries.values())) {\n abortController.abort(\"Cleared\");\n }\n\n inflightQueries.clear();\n },\n\n /** Keep track of mounted keys. */\n mount(queryKey: string): void {\n mountedKeys.set(queryKey, (mountedKeys.get(queryKey) ?? 0) + 1);\n },\n\n /** Clean up mounted keys. */\n unmount(queryKey: string): void {\n const count = mountedKeys.get(queryKey);\n if (count === 1) {\n mountedKeys.delete(queryKey);\n } else {\n mountedKeys.set(queryKey, (count ?? 0) - 1);\n }\n },\n\n /**\n * Discard the oldest entries if the size exceeds the limit. The last\n * remaining record cannot be deleted no matter what the limit is.\n */\n enforceSizeLimit,\n } as const;\n\n return obj;\n}\n\nexport type SharedQuery<TArgs extends unknown[], TData> = ReturnType<\n typeof sharedQuery<TArgs, TData>\n>;\n\n// Using any so that we don't need to typecast later.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst DEFAULT_QUERY_STATE_ENTRY: Readonly<QueryStateValue<any>> = {\n lastUpdatedMs: 0,\n loading: true,\n};\n\n/**\n * React hook to make use of a shared query inside any React component. The\n * args do not need to be specified if no args are needed, i.e. this would work\n * ```\n * const allUsers = useSharedQuery(listUsersQuery);\n * ```\n *\n * Example:\n * ```\n * const users = useSharedQuery(usersQuery, \"userid123\");\n * ```\n */\nexport function useSharedQuery<TArgs extends unknown[], TData>(\n query: SharedQuery<TArgs, TData>,\n // Optional: Default to use no arguments.\n ...args: TArgs\n): UseQueryResult<TData> {\n // We don't expect users to provide a stable `args`, so stabilize it here.\n const stableArgs = useDeepMemo(args);\n\n const [queryState, setQueryState] = useSharedState(query.queryState);\n\n const isMounted = useRef(true);\n\n const queryKey = useMemo(\n () => query.getQueryKey(stableArgs),\n [query, stableArgs],\n );\n\n // Init with empty object.\n const queryStateValue: QueryStateValue<TData> =\n queryState[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY;\n\n const setQueryStateValue = useCallback(\n (next: SetStateAction<QueryStateValue<TData>>) => {\n setQueryState((prev) => {\n const clone = { ...prev };\n clone[queryKey] =\n typeof next === \"function\"\n ? next(clone[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY)\n : next;\n return clone;\n });\n },\n [queryKey, setQueryState],\n );\n\n // Keep the last non-empty data loaded in case you want to show a placeholder\n // during new loads.\n const [lastNonEmptyData, setLastNonEmptyData] = useState<TData | undefined>();\n\n useEffect(() => {\n if (query.keepLastNonEmptyData && queryStateValue.data !== undefined) {\n setLastNonEmptyData(queryStateValue.data);\n }\n }, [query.keepLastNonEmptyData, queryStateValue.data]);\n\n // The fetch logic wrapped in useCallback to be stable for useEffect\n const execute = useCallback(\n async (isRefetch: boolean) => {\n query.log(\n `Begin executing shared query ${queryKey}, isRefetch=${isRefetch}`,\n );\n\n if (!isMounted.current) return;\n\n setQueryStateValue((prev) => ({\n // Keep old data and error.\n ...prev,\n lastUpdatedMs: Date.now(),\n loading: true,\n }));\n\n try {\n let state: QueryStateValue<TData>;\n\n if (isRefetch) {\n const data = await query.refetch(...stableArgs);\n const now = Date.now();\n\n state = {\n // Don't keep old error.\n lastUpdatedMs: now,\n loading: false,\n data,\n dataUpdatedMs: now,\n };\n } else {\n state = await query.getCachedOrFetch(...stableArgs);\n }\n\n if (!isMounted.current) return;\n\n // Make sure to reset loading to false.\n setQueryStateValue({ ...state, loading: false });\n } catch (e) {\n if (!isMounted.current) return;\n\n const now = Date.now();\n\n setQueryStateValue((prev) => ({\n // Keep old data.\n ...prev,\n lastUpdatedMs: now,\n loading: false,\n error: e,\n errorUpdatedMs: now,\n }));\n }\n },\n [query, queryKey, setQueryStateValue, stableArgs],\n );\n\n useEffect(() => {\n isMounted.current = true;\n query.mount(queryKey);\n\n void execute(false);\n\n return () => {\n isMounted.current = false;\n query.unmount(queryKey);\n };\n }, [execute, query, queryKey]);\n\n return useMemo(() => {\n return {\n ...queryStateValue,\n\n // Fill with lastNonEmptyData when data is undefined.\n data:\n queryStateValue.data !== undefined\n ? queryStateValue.data\n : lastNonEmptyData,\n latestData: queryStateValue.data,\n\n // This works only if the user-given queryFn supports abort. If not,\n // this function doesn't do anything, since we don't have any means to\n // abort the running queryFn.\n abortCurrentQuery: (reason?: unknown): boolean => {\n const controller = query.getAbortControllerByKey(queryKey);\n controller?.abort(reason);\n return Boolean(controller);\n },\n refetch: (): Promise<void> => {\n return execute(true);\n },\n setData: (data: TData, dataUpdatedMs?: number): void => {\n query.setData(queryKey, data, dataUpdatedMs);\n },\n deleteData: (): void => {\n query.deleteData(queryKey);\n },\n } satisfies UseQueryResult<TData>;\n }, [execute, lastNonEmptyData, query, queryKey, queryStateValue]);\n}\n"]}
1
+ {"version":3,"sources":["/Users/choksheaklau/src/react-utils/dist/sharedQuery.min.cjs","../src/sharedQuery.ts"],"names":["init_stringify","SharedQueryConfig","args","configureSharedQuery","config","seenQueryNames","sharedQuery","options","url","queryName","inflightQueries","mountedKeys","queryFn","fetcher","http","retries","params","firstArg","entries","k","v","item","refetchFn","staleMs","expiryMs","refetchOnStale","store","isValid","u","persistTo","maxSize","maxBytes","keepLastNonEmptyData","log","queryState","sharedState","isStale","dataUpdatedMs","isExpired","enforceSizeLimit","oldState","oldSize","oldByteSize","getByteSize","numToCut","bytesToCut"],"mappings":"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","file":"/Users/choksheaklau/src/react-utils/dist/sharedQuery.min.cjs","sourcesContent":[null,"/**\n * @packageDocumentation\n *\n * The \"shared query\" is a data fetching convenience library that helps users\n * to load data, check for staleness and expiration, cache in memory, local\n * storage or indexed db, and be able to manipulate the data anytime.\n *\n * Each shared query creates a new private shared state that handles the data\n * storage. This shared state contains the records for all queries, where each\n * query is a unique set of arguments used to make the query. The number of\n * records and the total byte size of data can be limited so as to ensure the\n * query does not take up too much memory.\n *\n * Think of shared queries as like the open source npm package \"react-query\",\n * but with strong type-checking and built-in support for persistence.\n *\n * Example:\n * ```\n * import { sharedQuery, useSharedQuery } from \"@choksheak/react-utils/sharedQuery\";\n * import { MS_PER_DAY } from \"@choksheak/ts-utils/timeConstants\";\n *\n * // Create a new shared query in the top level scope.\n * // The query function `queryFn` here does not take any arguments.\n * //\n * // Only the queryName and queryFn are required and everything else is\n * // optional.\n * //\n * // The queryName must be unique, otherwise we will throw an\n * // error. This is because defining the same queryName for two different\n * // queries implies that the queries are the same, thus violating the idea\n * // of a \"shared\" query, and almost certainly points to a bug in the code.\n * export const usersQuery = sharedQuery({\n * queryName: \"users\",\n * queryFn: listUsers,\n * persistTo: \"indexedDb\",\n * staleMs: MS_PER_DAY,\n * });\n *\n * // Example of using a query function with arguments.\n * export const getUserQuery = sharedQuery({\n * queryName: \"getUser\",\n * queryFn: getUser, // getUser(userId: string) => User\n * persistTo: \"indexedDb\",\n * staleMs: MS_PER_DAY,\n * });\n *\n * export const UsersComponent: React.FC = () => {\n * // Second argument is optional, and is an array of arguments to be\n * // passed to the `queryFn`.\n * const users = useSharedQuery(usersQuery);\n *\n * return (\n * <>\n * <h1>List of Users</h1>\n *\n * <button onclick={users.refetch}>Refresh</button>\n *\n * {users.loading && <Loader />}\n *\n * {users.error && !users.data && <p>Error: {String(users.error)}</p>}\n *\n * <p>Data: {users.data ? JSON.stringify(users.data) : \"-\"}</p>\n * </>\n * );\n * };\n *\n * export const AnotherComponent: React.FC = () => {\n * // This query will be deduped with the one above, sharing the same\n * // data and the same fetches.\n * const users = useSharedQuery(usersQuery);\n * ...\n * };\n *\n * export const DisplayOneUser: React.FC<{userId: string}> = ({userId}) => {\n * // Example of using the query with an argument. Whenever the argument\n * // changes, the data will be fetched and updated automatically.\n * const user = useSharedQuery(getUserQuery, userId);\n *\n * // You can check for user.loading and user.error if needed.\n *\n * return <>User: {user.data ? JSON.stringify(user.data) : \"-\"}</>;\n * };\n * ```\n */\n\nimport { http, retries } from \"@choksheak/ts-utils/http\";\nimport getByteSize from \"object-sizeof\";\nimport {\n SetStateAction,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport XXH from \"xxhashjs\";\n\nimport { sharedState, SharedStateOptions, useSharedState } from \"./sharedState\";\nimport { PersistTo } from \"./storage\";\nimport { stringifyDeterministicForKeys } from \"./stringify\";\nimport { useDeepMemo } from \"./useDeepMemo\";\n\n/** Type of the queryFn used to fetch the data from source. */\nexport type QueryFn<TArgs extends unknown[], TData> = (\n ...args: TArgs\n) => Promise<TData> | TData;\n\n/** An entry for a persisted state corresponding to one query key. */\nexport type QueryStateValue<TData> = Readonly<{\n /**\n * When this record was last updated. Used for LRU determination. If you want\n * the data age, please use `dataUpdatedMs` instead.\n */\n lastUpdatedMs: number;\n\n loading: boolean;\n data?: TData;\n dataUpdatedMs?: number;\n\n /**\n * Use unknown type for error because we can't assume what type of variable\n * will be thrown from the query function. We also don't want to add this\n * type to QueryStateValue<TError> as it adds complexity to the type.\n */\n error?: unknown;\n\n errorUpdatedMs?: number;\n}>;\n\n/** Return type for useSharedQuery(). */\nexport type UseQueryResult<TData> = QueryStateValue<TData> &\n Readonly<{\n /**\n * Usually same as `data`, but if you specify `keepLastNonEmptyData: true`,\n * then this will contain the latest data (undefined when args just changed)\n * and data will contain the last non empty data.\n */\n latestData: TData | undefined;\n\n /**\n * Cancels the inflight query (if any). Returns true if canceled.\n */\n abortCurrentQuery: (reason?: unknown) => boolean;\n\n /**\n * Manually refresh the data by doing a new query.\n */\n refetch: () => Promise<void>;\n\n /**\n * If you got the data from somewhere else (e.g. an in-memory data update),\n * just set it directly.\n */\n setData: (data: TData, dataUpdatedMs?: number) => void;\n\n deleteData: () => void;\n }>;\n\n/**\n * The full query state is a record of all the fetch states mapped by\n * query key to the state entry.\n */\nexport type SharedQueryState<TData> = Record<string, QueryStateValue<TData>>;\n\nexport type QueryTargetWithFn<TArgs extends unknown[], TData> = {\n /**\n * The name could have been auto-generated, but we let the user give us a\n * human-readable name that can be identified quickly in the logs.\n * Note that the queryKey is a string formed by the queryName plus the query\n * arguments.\n */\n queryName: string;\n\n /** Function to fetch data. */\n queryFn: QueryFn<TArgs, TData>;\n};\n\nexport type QueryTargetWithUrl = {\n /**\n * The queryName can optionally be specified, else we will fallback to use\n * the url as the queryName.\n */\n queryName?: string;\n\n /**\n * URL to fetch data using HTTP GET. Note that using this field will cause\n * both TArgs and TData to be undefined (unknown). Therefore you can make up\n * for this by specifying types with useSharedQuery, like this:\n * ```\n * const usersQuery = useSharedQuery<[{ userId?: string[] }], User[]>({\n * url: \"/api/list-users\",\n * });\n * ```\n *\n * If `userId` were specified as [\"u1\", \"u2\"], the query url would be\n * \"/api/list-users?userId=u1&userId=u2\".\n *\n * You can also pass the params as a string like \"userId=u1&userId=u2\".\n * ```\n * const usersQuery = useSharedQuery<[string], User[]>({\n * url: \"/api/list-users\",\n * });\n * ```\n */\n url: string;\n};\n\n/** Specifies how to fetch the data. */\nexport type QueryTarget<TArgs extends unknown[], TData> =\n | QueryTargetWithFn<TArgs, TData>\n | QueryTargetWithUrl;\n\n/** Options to configure a shared query. */\nexport type SharedQueryOptions<TArgs extends unknown[], TData> = QueryTarget<\n TArgs,\n TData\n> & {\n /**\n * Function to re-fetch data. Usually this would just be the `queryFn`, but\n * if you need a different behavior for refetching, then specify this.\n */\n refetchFn?: QueryFn<TArgs, TData>;\n\n /**\n * ms before data considered stale; 0 means always stale.\n */\n staleMs?: number;\n\n /**\n * ms before data is removed from cache; 0 means never expire.\n */\n expiryMs?: number;\n\n /**\n * Trigger background re-fetch if stale.\n */\n refetchOnStale?: boolean;\n\n /**\n * Shortcut to set the localStorageKey or indexedDbKey automatically from\n * queryName. This is important because it avoids requiring the user to\n * specify the same queryName twice everytime.\n */\n persistTo?: PersistTo;\n\n /**\n * Max number of entries to keep. The oldest entries will be discarded.\n * The last record cannot be discarded. 0 means no limit.\n */\n maxSize?: number;\n\n /**\n * Max number of bytes to keep. The oldest entries will be discarded.\n * The last record cannot be discarded. 0 means no limit.\n */\n maxBytes?: number;\n\n /**\n * Keep a copy of the last non-empty data so that when you change the\n * query arguments, you can still display the old data while waiting for the\n * new data to load.\n */\n keepLastNonEmptyData?: boolean;\n\n /**\n * Customize the logger.\n */\n log?: (...args: unknown[]) => void;\n} & SharedStateOptions<SharedQueryState<TData>>;\n\n/** Users can override these values globally. */\nexport const SharedQueryConfig = {\n /** Refetch on every page load. */\n staleMs: 0,\n\n /** Keep in cache for a long time. */\n expiryMs: 30 * 86_400_000 /* MS_PER_DAY */,\n\n /** Always refetch automatically if the data is stale. */\n refetchOnStale: true,\n\n /** No default persistence configured. */\n persistTo: undefined as PersistTo | undefined,\n\n /** 100 is not too large, but please tweak accordingly. */\n maxSize: 100,\n\n /** 100kb limit before discarding old records. */\n maxBytes: 100_000,\n\n /** Default to log to console. */\n log: (...args: unknown[]) => console.log(\"[sharedQuery]\", ...args),\n};\n\nexport type SharedQueryConfig = typeof SharedQueryConfig;\n\n/** Convenience function to update global defaults. */\nexport function configureSharedQuery(config: Partial<SharedQueryConfig>) {\n Object.assign(SharedQueryConfig, config);\n}\n\n/** Whether this is a normal query or an explicit refetch. */\ntype QueryType = \"query\" | \"refetch\";\n\n// Disallow duplicate query names.\nconst seenQueryNames = new Set<string>();\n\n/**\n * Create a reusable shared query object that can be used to auto de-duplicate\n * and cache all data fetches, with auto-expiration and staleness checks.\n *\n * Each `queryName` must be unique, and can only be declared once, most often\n * in the top level scope. The queryName is mainly used for logging and also\n * used as the storage key when you use the top-level `persistTo`.\n *\n * Example:\n * ```\n * export const usersQuery = sharedQuery({\n * queryName: \"users\",\n * queryFn: listUsers,\n * persistTo: \"indexedDb\",\n * staleMs: MS_PER_DAY,\n * });\n * ```\n */\nexport function sharedQuery<TArgs extends unknown[], TData>(\n // The name could have been auto-generated, but we let the user give us a\n // human-readable name that can be identified quickly in the logs.\n options: SharedQueryOptions<TArgs, TData>,\n) {\n // If queryName is not specified, fallback to use url as the queryName.\n const url = (options as QueryTargetWithUrl).url || \"\";\n const queryName = options.queryName || url;\n\n if (!queryName) {\n throw new Error(`Either queryName or url must be specified.`);\n }\n\n if (seenQueryNames.has(queryName)) {\n throw new Error(`Duplicate shared query name \"${queryName}\"`);\n }\n seenQueryNames.add(queryName);\n\n const inflightQueries = new Map<\n string,\n { promise: Promise<TData>; abortController: AbortController }\n >();\n\n /**\n * Keep track of the mounted query keys. This handles the case where an\n * earlier-started query returns later, and when it returns, it was already\n * unmounted, thus making it eligible for clean up right after fetch.\n */\n const mountedKeys = new Map<string, number>();\n\n let queryFn: QueryFn<TArgs, TData>;\n\n if ((options as QueryTargetWithFn<TArgs, TData>).queryFn) {\n queryFn = (options as QueryTargetWithFn<TArgs, TData>).queryFn;\n } else if (url) {\n const fetcher = http.use(retries());\n\n // Auto-generate the queryFn as a HTTP GET request using the url.\n queryFn = async (...args: TArgs): Promise<TData> => {\n // If args are given, treat the first argument as a search param.\n let params = \"\";\n const firstArg = args?.[0];\n\n if (firstArg) {\n if (typeof firstArg === \"string\") {\n params = firstArg;\n } else if (typeof firstArg === \"object\") {\n const entries: [string, string][] = [];\n\n for (const [k, v] of Object.entries(firstArg)) {\n if (v === undefined) continue;\n\n // Allow the same param to be specified multiple times.\n if (Array.isArray(v)) {\n for (const item of v) {\n if (item === undefined) continue;\n\n // The field values can only be primitives, not objects.\n entries.push([k, String(item)]);\n }\n } else {\n // The field values can only be primitives, not objects.\n entries.push([k, String(v)]);\n }\n }\n\n params = new URLSearchParams(entries).toString();\n }\n\n if (params) {\n if (url.includes(\"?\")) {\n // Append more search params to existing ones.\n params = \"&\" + params;\n } else {\n // Set the full search params string.\n params = \"?\" + params;\n }\n }\n }\n\n const response = await fetcher.get(url + params);\n\n return await response.json();\n };\n } else {\n throw new Error(`Either queryFn or url must be specified`);\n }\n\n const refetchFn = options.refetchFn;\n\n const staleMs = options?.staleMs ?? SharedQueryConfig.staleMs;\n const expiryMs = options?.expiryMs ?? SharedQueryConfig.expiryMs;\n\n const refetchOnStale =\n options?.refetchOnStale ?? SharedQueryConfig.refetchOnStale;\n\n // Apply shortcut when using `persistTo`.\n // Specifying the `store` will take precedence over `persistTo`.\n const store = options.store;\n\n if (store) {\n // Wrap the isValid to account for the query state wrapper.\n if (\"persistTo\" in store && store.isValid) {\n const isValid = store.isValid;\n\n options = {\n ...options,\n store: {\n ...store,\n isValid: (u) => {\n // Supposed type of u: SharedQueryState<TData>\n if (!u || typeof u !== \"object\") {\n return false;\n }\n\n // Since each stored key could contain multiple values, and isValid\n // checks for each value, we need to check every value to make sure\n // they are valid.\n // Supposed type of v: QueryStateValue<TData>\n for (const v of Object.values(u)) {\n if (\n !v ||\n typeof v !== \"object\" ||\n !(\"data\" in v) ||\n !(\"dataUpdatedMs\" in v) ||\n !(\"lastUpdatedMs\" in v) ||\n typeof v.dataUpdatedMs !== \"number\" ||\n typeof v.lastUpdatedMs !== \"number\" ||\n !isValid(v.data)\n ) {\n return false;\n }\n }\n\n return true;\n },\n },\n };\n }\n } else {\n const persistTo = options.persistTo ?? SharedQueryConfig.persistTo;\n\n if (persistTo) {\n options = {\n ...options,\n store: {\n persistTo,\n key: queryName,\n expiryMs,\n },\n };\n }\n }\n\n const maxSize = options?.maxSize ?? SharedQueryConfig.maxSize;\n const maxBytes = options?.maxBytes ?? SharedQueryConfig.maxBytes;\n\n const keepLastNonEmptyData = Boolean(options?.keepLastNonEmptyData);\n\n const log = options?.log ?? SharedQueryConfig.log;\n\n const queryState = sharedState<SharedQueryState<TData>>(\n {}, // defaultValue\n options,\n );\n\n function isStale(dataUpdatedMs: number): boolean {\n if (staleMs === 0) return true; // always stale\n\n const ageMs = Date.now() - dataUpdatedMs;\n return ageMs > staleMs;\n }\n\n function isExpired(dataUpdatedMs: number): boolean {\n if (expiryMs === 0) return false; // never expire\n\n const ageMs = Date.now() - dataUpdatedMs;\n return ageMs > expiryMs;\n }\n\n function enforceSizeLimit(): void {\n // Skip if there are no limits.\n if (!maxSize && !maxBytes) {\n return;\n }\n\n const oldState = queryState.getSnapshot();\n const oldSize = Object.keys(oldState).length;\n\n // Cannot do GC if size is 1 or less.\n if (oldSize <= 1) {\n return;\n }\n\n const oldByteSize = getByteSize(oldState);\n\n let numToCut = maxSize ? oldSize - maxSize : 0;\n let bytesToCut = maxBytes ? oldByteSize - maxBytes : 0;\n\n // Log to indicate why trimming was not needed.\n if (numToCut <= 0 && bytesToCut <= 0) {\n log(\n `No need to trim data for ${queryName}: size=${oldSize} (limit=${maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${maxBytes.toLocaleString()})`,\n );\n return;\n }\n\n // Log to inform user that trimming is needed.\n log(\n `Need to trim ${queryName}: numToCut=${numToCut}, bytesToCut=${bytesToCut}`,\n );\n\n const newState = { ...oldState }; // shallow clone\n let needUpdate = false;\n\n const deleteKeyIfNotMounted = (key: string, expired: boolean) => {\n // Mounted keys cannot be cleaned up as they are visible in the UI.\n if (mountedKeys.has(key)) {\n log(`Cannot clean up ${key} as it is mounted`);\n return;\n }\n\n const byteSize = getByteSize(key) + getByteSize(newState[key]);\n log(\n `Cleaning up unmounted${expired ? \", expired\" : \"\"} ${key} (byteSize=${byteSize.toLocaleString()})`,\n );\n\n numToCut--;\n bytesToCut -= byteSize;\n\n delete newState[key];\n needUpdate = true;\n };\n\n let entriesByTimeAscending = Object.entries(newState).sort(\n (entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs,\n );\n\n // Clean up all expired entries. Note that the auto-expiration in the\n // storage adapter only applies to the entire state, which contains all\n // the different query keys and their values. Each query key has its own\n // timestamp and we need to apply our own expiration here.\n for (const [key] of entriesByTimeAscending) {\n // Anything after this is not expired yet.\n if (!isExpired(newState[key].lastUpdatedMs)) {\n break;\n }\n\n deleteKeyIfNotMounted(key, true);\n }\n\n if (numToCut > 0 || bytesToCut > 0) {\n if (needUpdate) {\n entriesByTimeAscending = Object.entries(newState).sort(\n (entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs,\n );\n }\n\n // Clean up starting from oldest entry first.\n for (const [key] of entriesByTimeAscending) {\n deleteKeyIfNotMounted(key, false);\n\n if (numToCut <= 0 && bytesToCut <= 0) {\n break;\n }\n }\n }\n\n // Log to indicate why trimming was not needed.\n if (!needUpdate) {\n log(\n `No trimmable entries found for ${queryName}: size=${oldSize} (limit=${maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${maxBytes.toLocaleString()})`,\n );\n return;\n }\n\n // Update the state.\n queryState.setValue(newState);\n\n // Log to inform user that the data was trimmed.\n log(\n `Trimmed data for ${queryName}: size=[${oldSize} -> ${Object.keys(newState).length}] (limit=${maxSize}), byteSize=[${oldByteSize.toLocaleString()} -> ${getByteSize(newState).toLocaleString()}] (limit=${maxBytes.toLocaleString()})`,\n );\n }\n\n function setData(\n queryKey: string,\n data: TData,\n dataUpdatedMs = Date.now(),\n ): void {\n const entry: QueryStateValue<TData> = {\n lastUpdatedMs: Date.now(),\n loading: false,\n data,\n dataUpdatedMs,\n };\n\n queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));\n\n enforceSizeLimit();\n }\n\n async function startFetching(\n queryKey: string,\n queryType: QueryType,\n args: TArgs,\n ) {\n log(`Start fetching ${queryKey}, queryType=${queryType}`);\n\n try {\n const data = await (queryType === \"refetch\" && refetchFn\n ? refetchFn(...args)\n : queryFn(...args));\n\n log(`Successfully fetched ${queryKey}`);\n setData(queryKey, data, Date.now());\n return data;\n } catch (e) {\n log(`Failed to fetch ${queryKey} for queryType=${queryType}: ${e}`);\n throw e;\n } finally {\n inflightQueries.delete(queryKey);\n }\n }\n\n function dedupedFetch(\n queryKey: string,\n queryType: QueryType,\n ...args: TArgs\n ): Promise<TData> {\n const inflightQuery = inflightQueries.get(queryKey);\n\n // De-duplicate in-flight requests. Note that queries and refetches are\n // also deduped because they should technically be trying to fetch the same\n // data.\n if (inflightQuery) {\n log(\n `Deduplicating inflight fetch for ${queryKey}, queryType=${queryType}`,\n );\n return inflightQuery.promise;\n }\n\n // Fetch new data\n const promise = startFetching(queryKey, queryType, args);\n const abortController = new AbortController();\n\n inflightQueries.set(queryKey, { promise, abortController });\n\n return promise;\n }\n\n const obj = {\n queryName,\n queryFn,\n refetchFn,\n expiryMs,\n staleMs,\n refetchOnStale,\n maxSize,\n maxBytes,\n keepLastNonEmptyData,\n log,\n queryState,\n\n /** Returns a key to identify requests based on the given args. */\n getQueryKey(args: TArgs): string {\n let s = stringifyDeterministicForKeys(args);\n\n // Ensure the query key is not too long.\n if (s.length > 30) {\n s = XXH.h32(s, 0x99ee).toString(16).slice(2); // remove \"0x\"\n }\n\n return queryName + \":\" + s;\n },\n\n /**\n * Get the AbortController to abort the inflight query (if any).\n *\n * Note that once you get the controller, you can also get the signal easily\n * using `controller.signal`. So if you need both the controller and the\n * signal, just use this function.\n */\n getAbortController(args: TArgs): AbortController | null {\n const queryKey = obj.getQueryKey(args);\n return obj.getAbortControllerByKey(queryKey);\n },\n\n getAbortControllerByKey(queryKey: string): AbortController | null {\n return inflightQueries.get(queryKey)?.abortController ?? null;\n },\n\n /**\n * Get the AbortSignal to check for query abortions.\n *\n * Example using fetch:\n * ```\n * const getUserQuery = sharedQuery({\n * queryName: \"getUser\",\n * queryFn: (userId: string) => {\n * // Get the signal for this current execution.\n * const signal = getUserQuery.getAbortSignal([userId]);\n *\n * // Pass the signal to fetch so that it can be aborted.\n * const response = await fetch(`/users/${userId}`, { signal });\n *\n * // Check for errors.\n * if (!response.ok) {\n * throw new Error(response.statusText);\n * }\n *\n * // Return the data.\n * return await response.json();\n * },\n * });\n * ```\n *\n * Example using `http.ts`:\n * ```\n * const getUserQuery = sharedQuery({\n * queryName: \"getUser\",\n * queryFn: (userId: string) => {\n * // Get the signal for this current execution.\n * const signal = getUserQuery.getAbortSignal([userId]);\n *\n * // Pass the signal to fetch so that it can be aborted.\n * const response = await http.get(`/users/${userId}`, { signal });\n * return await response.json();\n * },\n * });\n * ```\n */\n getAbortSignal(args: TArgs): AbortSignal | null {\n const queryKey = obj.getQueryKey(args);\n return obj.getAbortSignalByKey(queryKey);\n },\n\n getAbortSignalByKey(queryKey: string): AbortSignal | null {\n return obj.getAbortControllerByKey(queryKey)?.signal ?? null;\n },\n\n /** For cached data, we need to return the correct timestamps as well. */\n async getCachedOrFetch(...args: TArgs): Promise<QueryStateValue<TData>> {\n // Important: wait for data to be loaded from cache first before trying to\n // fetch from server. This fixes a bad bug where the data is big, and the\n // code here starts a server fetch before the cache data loaded.\n const startMs = Date.now();\n await queryState.readyPromise;\n const waitedMs = Date.now() - startMs;\n\n const queryKey = obj.getQueryKey(args);\n const cached = queryState.getSnapshot()?.[queryKey];\n\n if (waitedMs) {\n log(`${queryKey}: Waited for ${waitedMs}ms before data is ready`);\n }\n\n if (cached?.data && !isExpired(cached.dataUpdatedMs ?? 0)) {\n // Return cached value if not stale.\n if (!isStale(cached.dataUpdatedMs ?? 0)) {\n log(`Return fresh data ${queryKey} from cache without fetching`);\n return cached;\n }\n\n // If stale, optionally update the data in the background.\n if (refetchOnStale) {\n log(`refetchOnStale for ${queryKey}`);\n void dedupedFetch(queryKey, \"query\", ...args);\n }\n\n // Still return stale data immediately.\n log(`Returning stale data for ${queryKey}`);\n return cached;\n }\n\n const data = await dedupedFetch(queryKey, \"query\", ...args);\n const now = Date.now();\n\n return {\n // Don't keep old error.\n lastUpdatedMs: now,\n loading: false,\n data,\n dataUpdatedMs: now,\n };\n },\n\n /** Get the entire stored entry for a query key. */\n getQueryValue(queryKey: string): QueryStateValue<TData> | undefined {\n return queryState.getSnapshot()?.[queryKey];\n },\n\n /** Get the current stored data for a query key. */\n getData(queryKey: string): TData | undefined {\n const stateValue: QueryStateValue<TData> | undefined =\n queryState.getSnapshot()?.[queryKey];\n\n // Check for expiration.\n if (stateValue?.lastUpdatedMs) {\n if (isExpired(stateValue.lastUpdatedMs)) {\n obj.deleteData(queryKey);\n return undefined;\n }\n }\n\n return stateValue?.data;\n },\n\n /** Set the data directly if the user obtained it from somewhere else. */\n setData,\n\n /**\n * Do a new fetch even when the data is already cached, but don't fetch if\n * another fetch is already inflight.\n */\n async updateFromSource(...args: TArgs): Promise<TData> {\n const key = obj.getQueryKey(args);\n return await dedupedFetch(key, \"query\", ...args);\n },\n\n /**\n * Perform an explicit refetch. If `options.refetchFn` was not specified,\n * then this would be identical to calling `obj.updateFromSource()`.\n */\n async refetch(...args: TArgs): Promise<TData> {\n const key = obj.getQueryKey(args);\n return await dedupedFetch(key, \"refetch\", ...args);\n },\n\n /** Delete the cached data for one set of arguments. */\n deleteData(queryKey: string): void {\n const record = queryState.getSnapshot();\n const clone = { ...record };\n delete clone[queryKey];\n queryState.setValue(clone);\n\n const inflight = inflightQueries.get(queryKey);\n if (inflight) {\n inflightQueries.delete(queryKey);\n inflight.abortController.abort(\"Deleted\");\n }\n },\n\n /** Delete all currently cached data & all inflight promises. */\n clear(): void {\n queryState.delete();\n\n for (const { abortController } of Array.from(inflightQueries.values())) {\n abortController.abort(\"Cleared\");\n }\n\n inflightQueries.clear();\n },\n\n /** Keep track of mounted keys. */\n mount(queryKey: string): void {\n mountedKeys.set(queryKey, (mountedKeys.get(queryKey) ?? 0) + 1);\n },\n\n /** Clean up mounted keys. */\n unmount(queryKey: string): void {\n const count = mountedKeys.get(queryKey);\n if (count === 1) {\n mountedKeys.delete(queryKey);\n } else {\n mountedKeys.set(queryKey, (count ?? 0) - 1);\n }\n },\n\n /**\n * Discard the oldest entries if the size exceeds the limit. The last\n * remaining record cannot be deleted no matter what the limit is.\n */\n enforceSizeLimit,\n } as const;\n\n return obj;\n}\n\nexport type SharedQuery<TArgs extends unknown[], TData> = ReturnType<\n typeof sharedQuery<TArgs, TData>\n>;\n\n// Using any so that we don't need to typecast later.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst DEFAULT_QUERY_STATE_ENTRY: Readonly<QueryStateValue<any>> = {\n lastUpdatedMs: 0,\n loading: true,\n};\n\n/**\n * React hook to make use of a shared query inside any React component. The\n * args do not need to be specified if no args are needed, i.e. this would work\n * ```\n * const allUsers = useSharedQuery(listUsersQuery);\n * ```\n *\n * Example:\n * ```\n * const users = useSharedQuery(usersQuery, \"userid123\");\n * ```\n */\nexport function useSharedQuery<TArgs extends unknown[], TData>(\n query: SharedQuery<TArgs, TData>,\n // Optional: Default to use no arguments.\n ...args: TArgs\n): UseQueryResult<TData> {\n // We don't expect users to provide a stable `args`, so stabilize it here.\n const stableArgs = useDeepMemo(args);\n\n const [queryState, setQueryState] = useSharedState(query.queryState);\n\n const isMounted = useRef(true);\n\n const queryKey = useMemo(\n () => query.getQueryKey(stableArgs),\n [query, stableArgs],\n );\n\n // Init with empty object.\n const queryStateValue: QueryStateValue<TData> =\n queryState[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY;\n\n const setQueryStateValue = useCallback(\n (next: SetStateAction<QueryStateValue<TData>>) => {\n setQueryState((prev) => {\n const clone = { ...prev };\n clone[queryKey] =\n typeof next === \"function\"\n ? next(clone[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY)\n : next;\n return clone;\n });\n },\n [queryKey, setQueryState],\n );\n\n // Keep the last non-empty data loaded in case you want to show a placeholder\n // during new loads.\n const [lastNonEmptyData, setLastNonEmptyData] = useState<TData | undefined>();\n\n useEffect(() => {\n if (query.keepLastNonEmptyData && queryStateValue.data !== undefined) {\n setLastNonEmptyData(queryStateValue.data);\n }\n }, [query.keepLastNonEmptyData, queryStateValue.data]);\n\n // The fetch logic wrapped in useCallback to be stable for useEffect\n const execute = useCallback(\n async (isRefetch: boolean) => {\n query.log(\n `Begin executing shared query ${queryKey}, isRefetch=${isRefetch}`,\n );\n\n if (!isMounted.current) return;\n\n setQueryStateValue((prev) => ({\n // Keep old data and error.\n ...prev,\n lastUpdatedMs: Date.now(),\n loading: true,\n }));\n\n try {\n let state: QueryStateValue<TData>;\n\n if (isRefetch) {\n const data = await query.refetch(...stableArgs);\n const now = Date.now();\n\n state = {\n // Don't keep old error.\n lastUpdatedMs: now,\n loading: false,\n data,\n dataUpdatedMs: now,\n };\n } else {\n state = await query.getCachedOrFetch(...stableArgs);\n }\n\n if (!isMounted.current) return;\n\n // Make sure to reset loading to false.\n setQueryStateValue({ ...state, loading: false });\n } catch (e) {\n if (!isMounted.current) return;\n\n const now = Date.now();\n\n setQueryStateValue((prev) => ({\n // Keep old data.\n ...prev,\n lastUpdatedMs: now,\n loading: false,\n error: e,\n errorUpdatedMs: now,\n }));\n }\n },\n [query, queryKey, setQueryStateValue, stableArgs],\n );\n\n useEffect(() => {\n isMounted.current = true;\n query.mount(queryKey);\n\n void execute(false);\n\n return () => {\n isMounted.current = false;\n query.unmount(queryKey);\n };\n }, [execute, query, queryKey]);\n\n return useMemo(() => {\n return {\n ...queryStateValue,\n\n // Fill with lastNonEmptyData when data is undefined.\n data:\n queryStateValue.data !== undefined\n ? queryStateValue.data\n : lastNonEmptyData,\n latestData: queryStateValue.data,\n\n // This works only if the user-given queryFn supports abort. If not,\n // this function doesn't do anything, since we don't have any means to\n // abort the running queryFn.\n abortCurrentQuery: (reason?: unknown): boolean => {\n const controller = query.getAbortControllerByKey(queryKey);\n controller?.abort(reason);\n return Boolean(controller);\n },\n refetch: (): Promise<void> => {\n return execute(true);\n },\n setData: (data: TData, dataUpdatedMs?: number): void => {\n query.setData(queryKey, data, dataUpdatedMs);\n },\n deleteData: (): void => {\n query.deleteData(queryKey);\n },\n } satisfies UseQueryResult<TData>;\n }, [execute, lastNonEmptyData, query, queryKey, queryStateValue]);\n}\n"]}
@@ -1,2 +1,2 @@
1
- import{a as E}from"./chunk-6ZZ54XM5.min.mjs";import{c as N,d as z}from"./chunk-KILGR7YA.min.mjs";import"./chunk-VHMO75EY.min.mjs";import{a as B,b as Y}from"./chunk-RM4ZGGZ2.min.mjs";import{a as L}from"./chunk-V7L3UICC.min.mjs";import"./chunk-MWDH7YON.min.mjs";import U from"object-sizeof";import{useCallback as P,useEffect as R,useMemo as j,useRef as H,useState as I}from"react";import G from"xxhashjs";Y();var Q={staleMs:0,expiryMs:30*864e5,refetchOnStale:!0,persistTo:void 0,maxSize:100,maxBytes:1e5,log:(...r)=>console.log("[sharedQuery]",...r)};function se(r){Object.assign(Q,r)}var W=new Set;function ie(r){let w=r.url||"",u=r.queryName||w;if(!u)throw new Error("Either queryName or url must be specified.");if(W.has(u))throw new Error(`Duplicate shared query name "${u}"`);W.add(u);let p=new Map,h=new Map,m;if(r.queryFn)m=r.queryFn;else if(w)m=(...e)=>{let t="",a=e?.[0];if(a){if(typeof a=="string")t=a;else if(typeof a=="object"){let n=[];for(let[s,d]of Object.entries(a))if(d!==void 0)if(Array.isArray(d))for(let b of d)b!==void 0&&n.push([s,String(b)]);else n.push([s,String(d)]);t=new URLSearchParams(n).toString()}t&&(w.includes("?")?t="&"+t:t="?"+t)}return E.url(w+t).get().json()};else throw new Error("Either queryFn or url must be specified");let l=r.refetchFn,y=r?.staleMs??Q.staleMs,D=r?.expiryMs??Q.expiryMs,M=r?.refetchOnStale??Q.refetchOnStale,A=r.store;if(A){if("persistTo"in A&&A.isValid){let e=A.isValid;r={...r,store:{...A,isValid:t=>{if(!t||typeof t!="object")return!1;for(let a of Object.values(t))if(!a||typeof a!="object"||!("data"in a)||!("dataUpdatedMs"in a)||!("lastUpdatedMs"in a)||typeof a.dataUpdatedMs!="number"||typeof a.lastUpdatedMs!="number"||!e(a.data))return!1;return!0}}}}}else{let e=r.persistTo??Q.persistTo;e&&(r={...r,store:{persistTo:e,key:u,expiryMs:D}})}let T=r?.maxSize??Q.maxSize,i=r?.maxBytes??Q.maxBytes,c=!!r?.keepLastNonEmptyData,o=r?.log??Q.log,f=N({},r);function _(e){return y===0?!0:Date.now()-e>y}function C(e){return D===0?!1:Date.now()-e>D}function k(){if(!T&&!i)return;let e=f.getSnapshot(),t=Object.keys(e).length;if(t<=1)return;let a=U(e),n=T?t-T:0,s=i?a-i:0;if(n<=0&&s<=0){o(`No need to trim data for ${u}: size=${t} (limit=${T}), byteSize=${a.toLocaleString()} (limit=${i.toLocaleString()})`);return}o(`Need to trim ${u}: numToCut=${n}, bytesToCut=${s}`);let d={...e},b=!1,v=(g,$)=>{if(h.has(g)){o(`Cannot clean up ${g} as it is mounted`);return}let O=U(g)+U(d[g]);o(`Cleaning up unmounted${$?", expired":""} ${g} (byteSize=${O.toLocaleString()})`),n--,s-=O,delete d[g],b=!0},F=Object.entries(d).sort((g,$)=>g[1].lastUpdatedMs-$[1].lastUpdatedMs);for(let[g]of F){if(!C(d[g].lastUpdatedMs))break;v(g,!0)}if(n>0||s>0){b&&(F=Object.entries(d).sort((g,$)=>g[1].lastUpdatedMs-$[1].lastUpdatedMs));for(let[g]of F)if(v(g,!1),n<=0&&s<=0)break}if(!b){o(`No trimmable entries found for ${u}: size=${t} (limit=${T}), byteSize=${a.toLocaleString()} (limit=${i.toLocaleString()})`);return}f.setValue(d),o(`Trimmed data for ${u}: size=[${t} -> ${Object.keys(d).length}] (limit=${T}), byteSize=[${a.toLocaleString()} -> ${U(d).toLocaleString()}] (limit=${i.toLocaleString()})`)}function V(e,t,a=Date.now()){let n={lastUpdatedMs:Date.now(),loading:!1,data:t,dataUpdatedMs:a};f.setValue(s=>({...s,[e]:n})),k()}async function X(e,t,a){o(`Start fetching ${e}, queryType=${t}`);try{let n=await(t==="refetch"&&l?l(...a):m(...a));return o(`Successfully fetched ${e}`),V(e,n,Date.now()),n}catch(n){throw o(`Failed to fetch ${e} for queryType=${t}: ${n}`),n}finally{p.delete(e)}}function x(e,t,...a){let n=p.get(e);if(n)return o(`Deduplicating inflight fetch for ${e}, queryType=${t}`),n.promise;let s=X(e,t,a),d=new AbortController;return p.set(e,{promise:s,abortController:d}),s}let S={queryName:u,queryFn:m,refetchFn:l,expiryMs:D,staleMs:y,refetchOnStale:M,maxSize:T,maxBytes:i,keepLastNonEmptyData:c,log:o,queryState:f,getQueryKey(e){let t=B(e);return t.length>30&&(t=G.h32(t,39406).toString(16).slice(2)),u+":"+t},getAbortController(e){let t=S.getQueryKey(e);return S.getAbortControllerByKey(t)},getAbortControllerByKey(e){return p.get(e)?.abortController??null},getAbortSignal(e){let t=S.getQueryKey(e);return S.getAbortSignalByKey(t)},getAbortSignalByKey(e){return S.getAbortControllerByKey(e)?.signal??null},async getCachedOrFetch(...e){let t=Date.now();await f.readyPromise;let a=Date.now()-t,n=S.getQueryKey(e),s=f.getSnapshot()?.[n];if(a&&o(`${n}: Waited for ${a}ms before data is ready`),s?.data&&!C(s.dataUpdatedMs??0))return _(s.dataUpdatedMs??0)?(M&&(o(`refetchOnStale for ${n}`),x(n,"query",...e)),o(`Returning stale data for ${n}`),s):(o(`Return fresh data ${n} from cache without fetching`),s);let d=await x(n,"query",...e),b=Date.now();return{lastUpdatedMs:b,loading:!1,data:d,dataUpdatedMs:b}},getQueryValue(e){return f.getSnapshot()?.[e]},getData(e){let t=f.getSnapshot()?.[e];if(t?.lastUpdatedMs&&C(t.lastUpdatedMs)){S.deleteData(e);return}return t?.data},setData:V,async updateFromSource(...e){let t=S.getQueryKey(e);return await x(t,"query",...e)},async refetch(...e){let t=S.getQueryKey(e);return await x(t,"refetch",...e)},deleteData(e){let a={...f.getSnapshot()};delete a[e],f.setValue(a);let n=p.get(e);n&&(p.delete(e),n.abortController.abort("Deleted"))},clear(){f.delete();for(let{abortController:e}of Array.from(p.values()))e.abort("Cleared");p.clear()},mount(e){h.set(e,(h.get(e)??0)+1)},unmount(e){let t=h.get(e);t===1?h.delete(e):h.set(e,(t??0)-1)},enforceSizeLimit:k};return S}var K={lastUpdatedMs:0,loading:!0};function ue(r,...w){let u=L(w),[p,h]=z(r.queryState),m=H(!0),l=j(()=>r.getQueryKey(u),[r,u]),y=p[l]??K,D=P(i=>{h(c=>{let o={...c};return o[l]=typeof i=="function"?i(o[l]??K):i,o})},[l,h]),[M,A]=I();R(()=>{r.keepLastNonEmptyData&&y.data!==void 0&&A(y.data)},[r.keepLastNonEmptyData,y.data]);let T=P(async i=>{if(r.log(`Begin executing shared query ${l}, isRefetch=${i}`),!!m.current){D(c=>({...c,lastUpdatedMs:Date.now(),loading:!0}));try{let c;if(i){let o=await r.refetch(...u),f=Date.now();c={lastUpdatedMs:f,loading:!1,data:o,dataUpdatedMs:f}}else c=await r.getCachedOrFetch(...u);if(!m.current)return;D({...c,loading:!1})}catch(c){if(!m.current)return;let o=Date.now();D(f=>({...f,lastUpdatedMs:o,loading:!1,error:c,errorUpdatedMs:o}))}}},[r,l,D,u]);return R(()=>(m.current=!0,r.mount(l),T(!1),()=>{m.current=!1,r.unmount(l)}),[T,r,l]),j(()=>({...y,data:y.data!==void 0?y.data:M,latestData:y.data,abortCurrentQuery:i=>{let c=r.getAbortControllerByKey(l);return c?.abort(i),!!c},refetch:()=>T(!0),setData:(i,c)=>{r.setData(l,i,c)},deleteData:()=>{r.deleteData(l)}}),[T,M,r,l,y])}export{Q as SharedQueryConfig,se as configureSharedQuery,ie as sharedQuery,ue as useSharedQuery};
1
+ import{c as E,d as N}from"./chunk-KILGR7YA.min.mjs";import"./chunk-VHMO75EY.min.mjs";import{a as z,b as X}from"./chunk-RM4ZGGZ2.min.mjs";import{a as B}from"./chunk-V7L3UICC.min.mjs";import"./chunk-MWDH7YON.min.mjs";import{http as Y,retries as H}from"@choksheak/ts-utils/http";import F from"object-sizeof";import{useCallback as L,useEffect as P,useMemo as R,useRef as I,useState as G}from"react";import J from"xxhashjs";X();var Q={staleMs:0,expiryMs:30*864e5,refetchOnStale:!0,persistTo:void 0,maxSize:100,maxBytes:1e5,log:(...a)=>console.log("[sharedQuery]",...a)};function ie(a){Object.assign(Q,a)}var j=new Set;function ue(a){let M=a.url||"",i=a.queryName||M;if(!i)throw new Error("Either queryName or url must be specified.");if(j.has(i))throw new Error(`Duplicate shared query name "${i}"`);j.add(i);let T=new Map,h=new Map,m;if(a.queryFn)m=a.queryFn;else if(M){let e=Y.use(H());m=async(...t)=>{let r="",n=t?.[0];if(n){if(typeof n=="string")r=n;else if(typeof n=="object"){let f=[];for(let[b,w]of Object.entries(n))if(w!==void 0)if(Array.isArray(w))for(let $ of w)$!==void 0&&f.push([b,String($)]);else f.push([b,String(w)]);r=new URLSearchParams(f).toString()}r&&(M.includes("?")?r="&"+r:r="?"+r)}return await(await e.get(M+r)).json()}}else throw new Error("Either queryFn or url must be specified");let u=a.refetchFn,y=a?.staleMs??Q.staleMs,D=a?.expiryMs??Q.expiryMs,x=a?.refetchOnStale??Q.refetchOnStale,A=a.store;if(A){if("persistTo"in A&&A.isValid){let e=A.isValid;a={...a,store:{...A,isValid:t=>{if(!t||typeof t!="object")return!1;for(let r of Object.values(t))if(!r||typeof r!="object"||!("data"in r)||!("dataUpdatedMs"in r)||!("lastUpdatedMs"in r)||typeof r.dataUpdatedMs!="number"||typeof r.lastUpdatedMs!="number"||!e(r.data))return!1;return!0}}}}}else{let e=a.persistTo??Q.persistTo;e&&(a={...a,store:{persistTo:e,key:i,expiryMs:D}})}let p=a?.maxSize??Q.maxSize,s=a?.maxBytes??Q.maxBytes,d=!!a?.keepLastNonEmptyData,o=a?.log??Q.log,c=E({},a);function K(e){return y===0?!0:Date.now()-e>y}function k(e){return D===0?!1:Date.now()-e>D}function V(){if(!p&&!s)return;let e=c.getSnapshot(),t=Object.keys(e).length;if(t<=1)return;let r=F(e),n=p?t-p:0,l=s?r-s:0;if(n<=0&&l<=0){o(`No need to trim data for ${i}: size=${t} (limit=${p}), byteSize=${r.toLocaleString()} (limit=${s.toLocaleString()})`);return}o(`Need to trim ${i}: numToCut=${n}, bytesToCut=${l}`);let f={...e},b=!1,w=(g,U)=>{if(h.has(g)){o(`Cannot clean up ${g} as it is mounted`);return}let O=F(g)+F(f[g]);o(`Cleaning up unmounted${U?", expired":""} ${g} (byteSize=${O.toLocaleString()})`),n--,l-=O,delete f[g],b=!0},$=Object.entries(f).sort((g,U)=>g[1].lastUpdatedMs-U[1].lastUpdatedMs);for(let[g]of $){if(!k(f[g].lastUpdatedMs))break;w(g,!0)}if(n>0||l>0){b&&($=Object.entries(f).sort((g,U)=>g[1].lastUpdatedMs-U[1].lastUpdatedMs));for(let[g]of $)if(w(g,!1),n<=0&&l<=0)break}if(!b){o(`No trimmable entries found for ${i}: size=${t} (limit=${p}), byteSize=${r.toLocaleString()} (limit=${s.toLocaleString()})`);return}c.setValue(f),o(`Trimmed data for ${i}: size=[${t} -> ${Object.keys(f).length}] (limit=${p}), byteSize=[${r.toLocaleString()} -> ${F(f).toLocaleString()}] (limit=${s.toLocaleString()})`)}function v(e,t,r=Date.now()){let n={lastUpdatedMs:Date.now(),loading:!1,data:t,dataUpdatedMs:r};c.setValue(l=>({...l,[e]:n})),V()}async function _(e,t,r){o(`Start fetching ${e}, queryType=${t}`);try{let n=await(t==="refetch"&&u?u(...r):m(...r));return o(`Successfully fetched ${e}`),v(e,n,Date.now()),n}catch(n){throw o(`Failed to fetch ${e} for queryType=${t}: ${n}`),n}finally{T.delete(e)}}function C(e,t,...r){let n=T.get(e);if(n)return o(`Deduplicating inflight fetch for ${e}, queryType=${t}`),n.promise;let l=_(e,t,r),f=new AbortController;return T.set(e,{promise:l,abortController:f}),l}let S={queryName:i,queryFn:m,refetchFn:u,expiryMs:D,staleMs:y,refetchOnStale:x,maxSize:p,maxBytes:s,keepLastNonEmptyData:d,log:o,queryState:c,getQueryKey(e){let t=z(e);return t.length>30&&(t=J.h32(t,39406).toString(16).slice(2)),i+":"+t},getAbortController(e){let t=S.getQueryKey(e);return S.getAbortControllerByKey(t)},getAbortControllerByKey(e){return T.get(e)?.abortController??null},getAbortSignal(e){let t=S.getQueryKey(e);return S.getAbortSignalByKey(t)},getAbortSignalByKey(e){return S.getAbortControllerByKey(e)?.signal??null},async getCachedOrFetch(...e){let t=Date.now();await c.readyPromise;let r=Date.now()-t,n=S.getQueryKey(e),l=c.getSnapshot()?.[n];if(r&&o(`${n}: Waited for ${r}ms before data is ready`),l?.data&&!k(l.dataUpdatedMs??0))return K(l.dataUpdatedMs??0)?(x&&(o(`refetchOnStale for ${n}`),C(n,"query",...e)),o(`Returning stale data for ${n}`),l):(o(`Return fresh data ${n} from cache without fetching`),l);let f=await C(n,"query",...e),b=Date.now();return{lastUpdatedMs:b,loading:!1,data:f,dataUpdatedMs:b}},getQueryValue(e){return c.getSnapshot()?.[e]},getData(e){let t=c.getSnapshot()?.[e];if(t?.lastUpdatedMs&&k(t.lastUpdatedMs)){S.deleteData(e);return}return t?.data},setData:v,async updateFromSource(...e){let t=S.getQueryKey(e);return await C(t,"query",...e)},async refetch(...e){let t=S.getQueryKey(e);return await C(t,"refetch",...e)},deleteData(e){let r={...c.getSnapshot()};delete r[e],c.setValue(r);let n=T.get(e);n&&(T.delete(e),n.abortController.abort("Deleted"))},clear(){c.delete();for(let{abortController:e}of Array.from(T.values()))e.abort("Cleared");T.clear()},mount(e){h.set(e,(h.get(e)??0)+1)},unmount(e){let t=h.get(e);t===1?h.delete(e):h.set(e,(t??0)-1)},enforceSizeLimit:V};return S}var W={lastUpdatedMs:0,loading:!0};function le(a,...M){let i=B(M),[T,h]=N(a.queryState),m=I(!0),u=R(()=>a.getQueryKey(i),[a,i]),y=T[u]??W,D=L(s=>{h(d=>{let o={...d};return o[u]=typeof s=="function"?s(o[u]??W):s,o})},[u,h]),[x,A]=G();P(()=>{a.keepLastNonEmptyData&&y.data!==void 0&&A(y.data)},[a.keepLastNonEmptyData,y.data]);let p=L(async s=>{if(a.log(`Begin executing shared query ${u}, isRefetch=${s}`),!!m.current){D(d=>({...d,lastUpdatedMs:Date.now(),loading:!0}));try{let d;if(s){let o=await a.refetch(...i),c=Date.now();d={lastUpdatedMs:c,loading:!1,data:o,dataUpdatedMs:c}}else d=await a.getCachedOrFetch(...i);if(!m.current)return;D({...d,loading:!1})}catch(d){if(!m.current)return;let o=Date.now();D(c=>({...c,lastUpdatedMs:o,loading:!1,error:d,errorUpdatedMs:o}))}}},[a,u,D,i]);return P(()=>(m.current=!0,a.mount(u),p(!1),()=>{m.current=!1,a.unmount(u)}),[p,a,u]),R(()=>({...y,data:y.data!==void 0?y.data:x,latestData:y.data,abortCurrentQuery:s=>{let d=a.getAbortControllerByKey(u);return d?.abort(s),!!d},refetch:()=>p(!0),setData:(s,d)=>{a.setData(u,s,d)},deleteData:()=>{a.deleteData(u)}}),[p,x,a,u,y])}export{Q as SharedQueryConfig,ie as configureSharedQuery,ue as sharedQuery,le as useSharedQuery};
2
2
  //# sourceMappingURL=sharedQuery.min.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sharedQuery.ts"],"sourcesContent":["/**\n * @packageDocumentation\n *\n * The \"shared query\" is a data fetching convenience library that helps users\n * to load data, check for staleness and expiration, cache in memory, local\n * storage or indexed db, and be able to manipulate the data anytime.\n *\n * Each shared query creates a new private shared state that handles the data\n * storage. This shared state contains the records for all queries, where each\n * query is a unique set of arguments used to make the query. The number of\n * records and the total byte size of data can be limited so as to ensure the\n * query does not take up too much memory.\n *\n * Think of shared queries as like the open source npm package \"react-query\",\n * but with strong type-checking and built-in support for persistence.\n *\n * Example:\n * ```\n * import { sharedQuery, useSharedQuery } from \"@choksheak/react-utils/sharedQuery\";\n * import { MS_PER_DAY } from \"@choksheak/ts-utils/timeConstants\";\n *\n * // Create a new shared query in the top level scope.\n * // The query function `queryFn` here does not take any arguments.\n * //\n * // Only the queryName and queryFn are required and everything else is\n * // optional.\n * //\n * // The queryName must be unique, otherwise we will throw an\n * // error. This is because defining the same queryName for two different\n * // queries implies that the queries are the same, thus violating the idea\n * // of a \"shared\" query, and almost certainly points to a bug in the code.\n * export const usersQuery = sharedQuery({\n * queryName: \"users\",\n * queryFn: listUsers,\n * persistTo: \"indexedDb\",\n * staleMs: MS_PER_DAY,\n * });\n *\n * // Example of using a query function with arguments.\n * export const getUserQuery = sharedQuery({\n * queryName: \"getUser\",\n * queryFn: getUser, // getUser(userId: string) => User\n * persistTo: \"indexedDb\",\n * staleMs: MS_PER_DAY,\n * });\n *\n * export const UsersComponent: React.FC = () => {\n * // Second argument is optional, and is an array of arguments to be\n * // passed to the `queryFn`.\n * const users = useSharedQuery(usersQuery);\n *\n * return (\n * <>\n * <h1>List of Users</h1>\n *\n * <button onclick={users.refetch}>Refresh</button>\n *\n * {users.loading && <Loader />}\n *\n * {users.error && !users.data && <p>Error: {String(users.error)}</p>}\n *\n * <p>Data: {users.data ? JSON.stringify(users.data) : \"-\"}</p>\n * </>\n * );\n * };\n *\n * export const AnotherComponent: React.FC = () => {\n * // This query will be deduped with the one above, sharing the same\n * // data and the same fetches.\n * const users = useSharedQuery(usersQuery);\n * ...\n * };\n *\n * export const DisplayOneUser: React.FC<{userId: string}> = ({userId}) => {\n * // Example of using the query with an argument. Whenever the argument\n * // changes, the data will be fetched and updated automatically.\n * const user = useSharedQuery(getUserQuery, userId);\n *\n * // You can check for user.loading and user.error if needed.\n *\n * return <>User: {user.data ? JSON.stringify(user.data) : \"-\"}</>;\n * };\n * ```\n */\n\nimport getByteSize from \"object-sizeof\";\nimport {\n SetStateAction,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport XXH from \"xxhashjs\";\n\nimport { fetcher } from \"./fetcher\";\nimport { sharedState, SharedStateOptions, useSharedState } from \"./sharedState\";\nimport { PersistTo } from \"./storage\";\nimport { stringifyDeterministicForKeys } from \"./stringify\";\nimport { useDeepMemo } from \"./useDeepMemo\";\n\n/** Type of the queryFn used to fetch the data from source. */\nexport type QueryFn<TArgs extends unknown[], TData> = (\n ...args: TArgs\n) => Promise<TData> | TData;\n\n/** An entry for a persisted state corresponding to one query key. */\nexport type QueryStateValue<TData> = Readonly<{\n /**\n * When this record was last updated. Used for LRU determination. If you want\n * the data age, please use `dataUpdatedMs` instead.\n */\n lastUpdatedMs: number;\n\n loading: boolean;\n data?: TData;\n dataUpdatedMs?: number;\n\n /**\n * Use unknown type for error because we can't assume what type of variable\n * will be thrown from the query function. We also don't want to add this\n * type to QueryStateValue<TError> as it adds complexity to the type.\n */\n error?: unknown;\n\n errorUpdatedMs?: number;\n}>;\n\n/** Return type for useSharedQuery(). */\nexport type UseQueryResult<TData> = QueryStateValue<TData> &\n Readonly<{\n /**\n * Usually same as `data`, but if you specify `keepLastNonEmptyData: true`,\n * then this will contain the latest data (undefined when args just changed)\n * and data will contain the last non empty data.\n */\n latestData: TData | undefined;\n\n /**\n * Cancels the inflight query (if any). Returns true if canceled.\n */\n abortCurrentQuery: (reason?: unknown) => boolean;\n\n /**\n * Manually refresh the data by doing a new query.\n */\n refetch: () => Promise<void>;\n\n /**\n * If you got the data from somewhere else (e.g. an in-memory data update),\n * just set it directly.\n */\n setData: (data: TData, dataUpdatedMs?: number) => void;\n\n deleteData: () => void;\n }>;\n\n/**\n * The full query state is a record of all the fetch states mapped by\n * query key to the state entry.\n */\nexport type SharedQueryState<TData> = Record<string, QueryStateValue<TData>>;\n\nexport type QueryTargetWithFn<TArgs extends unknown[], TData> = {\n /**\n * The name could have been auto-generated, but we let the user give us a\n * human-readable name that can be identified quickly in the logs.\n * Note that the queryKey is a string formed by the queryName plus the query\n * arguments.\n */\n queryName: string;\n\n /** Function to fetch data. */\n queryFn: QueryFn<TArgs, TData>;\n};\n\nexport type QueryTargetWithUrl = {\n /**\n * The queryName can optionally be specified, else we will fallback to use\n * the url as the queryName.\n */\n queryName?: string;\n\n /**\n * URL to fetch data using HTTP GET. Note that using this field will cause\n * both TArgs and TData to be undefined (unknown). Therefore you can make up\n * for this by specifying types with useSharedQuery, like this:\n * ```\n * const usersQuery = useSharedQuery<[{ userId?: string[] }], User[]>({\n * url: \"/api/list-users\",\n * });\n * ```\n *\n * If `userId` were specified as [\"u1\", \"u2\"], the query url would be\n * \"/api/list-users?userId=u1&userId=u2\".\n *\n * You can also pass the params as a string like \"userId=u1&userId=u2\".\n * ```\n * const usersQuery = useSharedQuery<[string], User[]>({\n * url: \"/api/list-users\",\n * });\n * ```\n */\n url: string;\n};\n\n/** Specifies how to fetch the data. */\nexport type QueryTarget<TArgs extends unknown[], TData> =\n | QueryTargetWithFn<TArgs, TData>\n | QueryTargetWithUrl;\n\n/** Options to configure a shared query. */\nexport type SharedQueryOptions<TArgs extends unknown[], TData> = QueryTarget<\n TArgs,\n TData\n> & {\n /**\n * Function to re-fetch data. Usually this would just be the `queryFn`, but\n * if you need a different behavior for refetching, then specify this.\n */\n refetchFn?: QueryFn<TArgs, TData>;\n\n /**\n * ms before data considered stale; 0 means always stale.\n */\n staleMs?: number;\n\n /**\n * ms before data is removed from cache; 0 means never expire.\n */\n expiryMs?: number;\n\n /**\n * Trigger background re-fetch if stale.\n */\n refetchOnStale?: boolean;\n\n /**\n * Shortcut to set the localStorageKey or indexedDbKey automatically from\n * queryName. This is important because it avoids requiring the user to\n * specify the same queryName twice everytime.\n */\n persistTo?: PersistTo;\n\n /**\n * Max number of entries to keep. The oldest entries will be discarded.\n * The last record cannot be discarded. 0 means no limit.\n */\n maxSize?: number;\n\n /**\n * Max number of bytes to keep. The oldest entries will be discarded.\n * The last record cannot be discarded. 0 means no limit.\n */\n maxBytes?: number;\n\n /**\n * Keep a copy of the last non-empty data so that when you change the\n * query arguments, you can still display the old data while waiting for the\n * new data to load.\n */\n keepLastNonEmptyData?: boolean;\n\n /**\n * Customize the logger.\n */\n log?: (...args: unknown[]) => void;\n} & SharedStateOptions<SharedQueryState<TData>>;\n\n/** Users can override these values globally. */\nexport const SharedQueryConfig = {\n /** Refetch on every page load. */\n staleMs: 0,\n\n /** Keep in cache for a long time. */\n expiryMs: 30 * 86_400_000 /* MS_PER_DAY */,\n\n /** Always refetch automatically if the data is stale. */\n refetchOnStale: true,\n\n /** No default persistence configured. */\n persistTo: undefined as PersistTo | undefined,\n\n /** 100 is not too large, but please tweak accordingly. */\n maxSize: 100,\n\n /** 100kb limit before discarding old records. */\n maxBytes: 100_000,\n\n /** Default to log to console. */\n log: (...args: unknown[]) => console.log(\"[sharedQuery]\", ...args),\n};\n\nexport type SharedQueryConfig = typeof SharedQueryConfig;\n\n/** Convenience function to update global defaults. */\nexport function configureSharedQuery(config: Partial<SharedQueryConfig>) {\n Object.assign(SharedQueryConfig, config);\n}\n\n/** Whether this is a normal query or an explicit refetch. */\ntype QueryType = \"query\" | \"refetch\";\n\n// Disallow duplicate query names.\nconst seenQueryNames = new Set<string>();\n\n/**\n * Create a reusable shared query object that can be used to auto de-duplicate\n * and cache all data fetches, with auto-expiration and staleness checks.\n *\n * Each `queryName` must be unique, and can only be declared once, most often\n * in the top level scope. The queryName is mainly used for logging and also\n * used as the storage key when you use the top-level `persistTo`.\n *\n * Example:\n * ```\n * export const usersQuery = sharedQuery({\n * queryName: \"users\",\n * queryFn: listUsers,\n * persistTo: \"indexedDb\",\n * staleMs: MS_PER_DAY,\n * });\n * ```\n */\nexport function sharedQuery<TArgs extends unknown[], TData>(\n // The name could have been auto-generated, but we let the user give us a\n // human-readable name that can be identified quickly in the logs.\n options: SharedQueryOptions<TArgs, TData>,\n) {\n // If queryName is not specified, fallback to use url as the queryName.\n const url = (options as QueryTargetWithUrl).url || \"\";\n const queryName = options.queryName || url;\n\n if (!queryName) {\n throw new Error(`Either queryName or url must be specified.`);\n }\n\n if (seenQueryNames.has(queryName)) {\n throw new Error(`Duplicate shared query name \"${queryName}\"`);\n }\n seenQueryNames.add(queryName);\n\n const inflightQueries = new Map<\n string,\n { promise: Promise<TData>; abortController: AbortController }\n >();\n\n /**\n * Keep track of the mounted query keys. This handles the case where an\n * earlier-started query returns later, and when it returns, it was already\n * unmounted, thus making it eligible for clean up right after fetch.\n */\n const mountedKeys = new Map<string, number>();\n\n let queryFn: QueryFn<TArgs, TData>;\n\n if ((options as QueryTargetWithFn<TArgs, TData>).queryFn) {\n queryFn = (options as QueryTargetWithFn<TArgs, TData>).queryFn;\n } else if (url) {\n // Auto-generate the queryFn as a HTTP GET request using the url.\n queryFn = (...args: TArgs): Promise<TData> => {\n // If args are given, treat the first argument as a search param.\n let params = \"\";\n const firstArg = args?.[0];\n\n if (firstArg) {\n if (typeof firstArg === \"string\") {\n params = firstArg;\n } else if (typeof firstArg === \"object\") {\n const entries: [string, string][] = [];\n\n for (const [k, v] of Object.entries(firstArg)) {\n if (v === undefined) continue;\n\n // Allow the same param to be specified multiple times.\n if (Array.isArray(v)) {\n for (const item of v) {\n if (item === undefined) continue;\n\n // The field values can only be primitives, not objects.\n entries.push([k, String(item)]);\n }\n } else {\n // The field values can only be primitives, not objects.\n entries.push([k, String(v)]);\n }\n }\n\n params = new URLSearchParams(entries).toString();\n }\n\n if (params) {\n if (url.includes(\"?\")) {\n // Append more search params to existing ones.\n params = \"&\" + params;\n } else {\n // Set the full search params string.\n params = \"?\" + params;\n }\n }\n }\n\n return fetcher\n .url(url + params)\n .get()\n .json();\n };\n } else {\n throw new Error(`Either queryFn or url must be specified`);\n }\n\n const refetchFn = options.refetchFn;\n\n const staleMs = options?.staleMs ?? SharedQueryConfig.staleMs;\n const expiryMs = options?.expiryMs ?? SharedQueryConfig.expiryMs;\n\n const refetchOnStale =\n options?.refetchOnStale ?? SharedQueryConfig.refetchOnStale;\n\n // Apply shortcut when using `persistTo`.\n // Specifying the `store` will take precedence over `persistTo`.\n const store = options.store;\n\n if (store) {\n // Wrap the isValid to account for the query state wrapper.\n if (\"persistTo\" in store && store.isValid) {\n const isValid = store.isValid;\n\n options = {\n ...options,\n store: {\n ...store,\n isValid: (u) => {\n // Supposed type of u: SharedQueryState<TData>\n if (!u || typeof u !== \"object\") {\n return false;\n }\n\n // Since each stored key could contain multiple values, and isValid\n // checks for each value, we need to check every value to make sure\n // they are valid.\n // Supposed type of v: QueryStateValue<TData>\n for (const v of Object.values(u)) {\n if (\n !v ||\n typeof v !== \"object\" ||\n !(\"data\" in v) ||\n !(\"dataUpdatedMs\" in v) ||\n !(\"lastUpdatedMs\" in v) ||\n typeof v.dataUpdatedMs !== \"number\" ||\n typeof v.lastUpdatedMs !== \"number\" ||\n !isValid(v.data)\n ) {\n return false;\n }\n }\n\n return true;\n },\n },\n };\n }\n } else {\n const persistTo = options.persistTo ?? SharedQueryConfig.persistTo;\n\n if (persistTo) {\n options = {\n ...options,\n store: {\n persistTo,\n key: queryName,\n expiryMs,\n },\n };\n }\n }\n\n const maxSize = options?.maxSize ?? SharedQueryConfig.maxSize;\n const maxBytes = options?.maxBytes ?? SharedQueryConfig.maxBytes;\n\n const keepLastNonEmptyData = Boolean(options?.keepLastNonEmptyData);\n\n const log = options?.log ?? SharedQueryConfig.log;\n\n const queryState = sharedState<SharedQueryState<TData>>(\n {}, // defaultValue\n options,\n );\n\n function isStale(dataUpdatedMs: number): boolean {\n if (staleMs === 0) return true; // always stale\n\n const ageMs = Date.now() - dataUpdatedMs;\n return ageMs > staleMs;\n }\n\n function isExpired(dataUpdatedMs: number): boolean {\n if (expiryMs === 0) return false; // never expire\n\n const ageMs = Date.now() - dataUpdatedMs;\n return ageMs > expiryMs;\n }\n\n function enforceSizeLimit(): void {\n // Skip if there are no limits.\n if (!maxSize && !maxBytes) {\n return;\n }\n\n const oldState = queryState.getSnapshot();\n const oldSize = Object.keys(oldState).length;\n\n // Cannot do GC if size is 1 or less.\n if (oldSize <= 1) {\n return;\n }\n\n const oldByteSize = getByteSize(oldState);\n\n let numToCut = maxSize ? oldSize - maxSize : 0;\n let bytesToCut = maxBytes ? oldByteSize - maxBytes : 0;\n\n // Log to indicate why trimming was not needed.\n if (numToCut <= 0 && bytesToCut <= 0) {\n log(\n `No need to trim data for ${queryName}: size=${oldSize} (limit=${maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${maxBytes.toLocaleString()})`,\n );\n return;\n }\n\n // Log to inform user that trimming is needed.\n log(\n `Need to trim ${queryName}: numToCut=${numToCut}, bytesToCut=${bytesToCut}`,\n );\n\n const newState = { ...oldState }; // shallow clone\n let needUpdate = false;\n\n const deleteKeyIfNotMounted = (key: string, expired: boolean) => {\n // Mounted keys cannot be cleaned up as they are visible in the UI.\n if (mountedKeys.has(key)) {\n log(`Cannot clean up ${key} as it is mounted`);\n return;\n }\n\n const byteSize = getByteSize(key) + getByteSize(newState[key]);\n log(\n `Cleaning up unmounted${expired ? \", expired\" : \"\"} ${key} (byteSize=${byteSize.toLocaleString()})`,\n );\n\n numToCut--;\n bytesToCut -= byteSize;\n\n delete newState[key];\n needUpdate = true;\n };\n\n let entriesByTimeAscending = Object.entries(newState).sort(\n (entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs,\n );\n\n // Clean up all expired entries. Note that the auto-expiration in the\n // storage adapter only applies to the entire state, which contains all\n // the different query keys and their values. Each query key has its own\n // timestamp and we need to apply our own expiration here.\n for (const [key] of entriesByTimeAscending) {\n // Anything after this is not expired yet.\n if (!isExpired(newState[key].lastUpdatedMs)) {\n break;\n }\n\n deleteKeyIfNotMounted(key, true);\n }\n\n if (numToCut > 0 || bytesToCut > 0) {\n if (needUpdate) {\n entriesByTimeAscending = Object.entries(newState).sort(\n (entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs,\n );\n }\n\n // Clean up starting from oldest entry first.\n for (const [key] of entriesByTimeAscending) {\n deleteKeyIfNotMounted(key, false);\n\n if (numToCut <= 0 && bytesToCut <= 0) {\n break;\n }\n }\n }\n\n // Log to indicate why trimming was not needed.\n if (!needUpdate) {\n log(\n `No trimmable entries found for ${queryName}: size=${oldSize} (limit=${maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${maxBytes.toLocaleString()})`,\n );\n return;\n }\n\n // Update the state.\n queryState.setValue(newState);\n\n // Log to inform user that the data was trimmed.\n log(\n `Trimmed data for ${queryName}: size=[${oldSize} -> ${Object.keys(newState).length}] (limit=${maxSize}), byteSize=[${oldByteSize.toLocaleString()} -> ${getByteSize(newState).toLocaleString()}] (limit=${maxBytes.toLocaleString()})`,\n );\n }\n\n function setData(\n queryKey: string,\n data: TData,\n dataUpdatedMs = Date.now(),\n ): void {\n const entry: QueryStateValue<TData> = {\n lastUpdatedMs: Date.now(),\n loading: false,\n data,\n dataUpdatedMs,\n };\n\n queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));\n\n enforceSizeLimit();\n }\n\n async function startFetching(\n queryKey: string,\n queryType: QueryType,\n args: TArgs,\n ) {\n log(`Start fetching ${queryKey}, queryType=${queryType}`);\n\n try {\n const data = await (queryType === \"refetch\" && refetchFn\n ? refetchFn(...args)\n : queryFn(...args));\n\n log(`Successfully fetched ${queryKey}`);\n setData(queryKey, data, Date.now());\n return data;\n } catch (e) {\n log(`Failed to fetch ${queryKey} for queryType=${queryType}: ${e}`);\n throw e;\n } finally {\n inflightQueries.delete(queryKey);\n }\n }\n\n function dedupedFetch(\n queryKey: string,\n queryType: QueryType,\n ...args: TArgs\n ): Promise<TData> {\n const inflightQuery = inflightQueries.get(queryKey);\n\n // De-duplicate in-flight requests. Note that queries and refetches are\n // also deduped because they should technically be trying to fetch the same\n // data.\n if (inflightQuery) {\n log(\n `Deduplicating inflight fetch for ${queryKey}, queryType=${queryType}`,\n );\n return inflightQuery.promise;\n }\n\n // Fetch new data\n const promise = startFetching(queryKey, queryType, args);\n const abortController = new AbortController();\n\n inflightQueries.set(queryKey, { promise, abortController });\n\n return promise;\n }\n\n const obj = {\n queryName,\n queryFn,\n refetchFn,\n expiryMs,\n staleMs,\n refetchOnStale,\n maxSize,\n maxBytes,\n keepLastNonEmptyData,\n log,\n queryState,\n\n /** Returns a key to identify requests based on the given args. */\n getQueryKey(args: TArgs): string {\n let s = stringifyDeterministicForKeys(args);\n\n // Ensure the query key is not too long.\n if (s.length > 30) {\n s = XXH.h32(s, 0x99ee).toString(16).slice(2); // remove \"0x\"\n }\n\n return queryName + \":\" + s;\n },\n\n /**\n * Get the AbortController to abort the inflight query (if any).\n *\n * Note that once you get the controller, you can also get the signal easily\n * using `controller.signal`. So if you need both the controller and the\n * signal, just use this function.\n */\n getAbortController(args: TArgs): AbortController | null {\n const queryKey = obj.getQueryKey(args);\n return obj.getAbortControllerByKey(queryKey);\n },\n\n getAbortControllerByKey(queryKey: string): AbortController | null {\n return inflightQueries.get(queryKey)?.abortController ?? null;\n },\n\n /**\n * Get the AbortSignal to check for query abortions.\n *\n * Example using fetch:\n * ```\n * const getUserQuery = sharedQuery({\n * queryName: \"getUser\",\n * queryFn: (userId: string) => {\n * // Get the signal for this current execution.\n * const signal = getUserQuery.getAbortSignal([userId]);\n *\n * // Pass the signal to fetch so that it can be aborted.\n * const response = await fetch(`/users/${userId}`, { signal });\n *\n * // Check for errors.\n * if (!response.ok) {\n * throw new Error(response.statusText);\n * }\n *\n * // Return the data.\n * return await response.json();\n * },\n * });\n * ```\n *\n * Example using `fetcher.ts`:\n * ```\n * const getUserQuery = sharedQuery({\n * queryName: \"getUser\",\n * queryFn: (userId: string) => {\n * // Get the signal for this current execution.\n * const signal = getUserQuery.getAbortSignal([userId]);\n *\n * // Pass the signal to fetch so that it can be aborted.\n * return fetcher.url(`/users/${userId}`).options({ signal }).get().json();\n * },\n * });\n * ```\n */\n getAbortSignal(args: TArgs): AbortSignal | null {\n const queryKey = obj.getQueryKey(args);\n return obj.getAbortSignalByKey(queryKey);\n },\n\n getAbortSignalByKey(queryKey: string): AbortSignal | null {\n return obj.getAbortControllerByKey(queryKey)?.signal ?? null;\n },\n\n /** For cached data, we need to return the correct timestamps as well. */\n async getCachedOrFetch(...args: TArgs): Promise<QueryStateValue<TData>> {\n // Important: wait for data to be loaded from cache first before trying to\n // fetch from server. This fixes a bad bug where the data is big, and the\n // code here starts a server fetch before the cache data loaded.\n const startMs = Date.now();\n await queryState.readyPromise;\n const waitedMs = Date.now() - startMs;\n\n const queryKey = obj.getQueryKey(args);\n const cached = queryState.getSnapshot()?.[queryKey];\n\n if (waitedMs) {\n log(`${queryKey}: Waited for ${waitedMs}ms before data is ready`);\n }\n\n if (cached?.data && !isExpired(cached.dataUpdatedMs ?? 0)) {\n // Return cached value if not stale.\n if (!isStale(cached.dataUpdatedMs ?? 0)) {\n log(`Return fresh data ${queryKey} from cache without fetching`);\n return cached;\n }\n\n // If stale, optionally update the data in the background.\n if (refetchOnStale) {\n log(`refetchOnStale for ${queryKey}`);\n void dedupedFetch(queryKey, \"query\", ...args);\n }\n\n // Still return stale data immediately.\n log(`Returning stale data for ${queryKey}`);\n return cached;\n }\n\n const data = await dedupedFetch(queryKey, \"query\", ...args);\n const now = Date.now();\n\n return {\n // Don't keep old error.\n lastUpdatedMs: now,\n loading: false,\n data,\n dataUpdatedMs: now,\n };\n },\n\n /** Get the entire stored entry for a query key. */\n getQueryValue(queryKey: string): QueryStateValue<TData> | undefined {\n return queryState.getSnapshot()?.[queryKey];\n },\n\n /** Get the current stored data for a query key. */\n getData(queryKey: string): TData | undefined {\n const stateValue: QueryStateValue<TData> | undefined =\n queryState.getSnapshot()?.[queryKey];\n\n // Check for expiration.\n if (stateValue?.lastUpdatedMs) {\n if (isExpired(stateValue.lastUpdatedMs)) {\n obj.deleteData(queryKey);\n return undefined;\n }\n }\n\n return stateValue?.data;\n },\n\n /** Set the data directly if the user obtained it from somewhere else. */\n setData,\n\n /**\n * Do a new fetch even when the data is already cached, but don't fetch if\n * another fetch is already inflight.\n */\n async updateFromSource(...args: TArgs): Promise<TData> {\n const key = obj.getQueryKey(args);\n return await dedupedFetch(key, \"query\", ...args);\n },\n\n /**\n * Perform an explicit refetch. If `options.refetchFn` was not specified,\n * then this would be identical to calling `obj.updateFromSource()`.\n */\n async refetch(...args: TArgs): Promise<TData> {\n const key = obj.getQueryKey(args);\n return await dedupedFetch(key, \"refetch\", ...args);\n },\n\n /** Delete the cached data for one set of arguments. */\n deleteData(queryKey: string): void {\n const record = queryState.getSnapshot();\n const clone = { ...record };\n delete clone[queryKey];\n queryState.setValue(clone);\n\n const inflight = inflightQueries.get(queryKey);\n if (inflight) {\n inflightQueries.delete(queryKey);\n inflight.abortController.abort(\"Deleted\");\n }\n },\n\n /** Delete all currently cached data & all inflight promises. */\n clear(): void {\n queryState.delete();\n\n for (const { abortController } of Array.from(inflightQueries.values())) {\n abortController.abort(\"Cleared\");\n }\n\n inflightQueries.clear();\n },\n\n /** Keep track of mounted keys. */\n mount(queryKey: string): void {\n mountedKeys.set(queryKey, (mountedKeys.get(queryKey) ?? 0) + 1);\n },\n\n /** Clean up mounted keys. */\n unmount(queryKey: string): void {\n const count = mountedKeys.get(queryKey);\n if (count === 1) {\n mountedKeys.delete(queryKey);\n } else {\n mountedKeys.set(queryKey, (count ?? 0) - 1);\n }\n },\n\n /**\n * Discard the oldest entries if the size exceeds the limit. The last\n * remaining record cannot be deleted no matter what the limit is.\n */\n enforceSizeLimit,\n } as const;\n\n return obj;\n}\n\nexport type SharedQuery<TArgs extends unknown[], TData> = ReturnType<\n typeof sharedQuery<TArgs, TData>\n>;\n\n// Using any so that we don't need to typecast later.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst DEFAULT_QUERY_STATE_ENTRY: Readonly<QueryStateValue<any>> = {\n lastUpdatedMs: 0,\n loading: true,\n};\n\n/**\n * React hook to make use of a shared query inside any React component. The\n * args do not need to be specified if no args are needed, i.e. this would work\n * ```\n * const allUsers = useSharedQuery(listUsersQuery);\n * ```\n *\n * Example:\n * ```\n * const users = useSharedQuery(usersQuery, \"userid123\");\n * ```\n */\nexport function useSharedQuery<TArgs extends unknown[], TData>(\n query: SharedQuery<TArgs, TData>,\n // Optional: Default to use no arguments.\n ...args: TArgs\n): UseQueryResult<TData> {\n // We don't expect users to provide a stable `args`, so stabilize it here.\n const stableArgs = useDeepMemo(args);\n\n const [queryState, setQueryState] = useSharedState(query.queryState);\n\n const isMounted = useRef(true);\n\n const queryKey = useMemo(\n () => query.getQueryKey(stableArgs),\n [query, stableArgs],\n );\n\n // Init with empty object.\n const queryStateValue: QueryStateValue<TData> =\n queryState[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY;\n\n const setQueryStateValue = useCallback(\n (next: SetStateAction<QueryStateValue<TData>>) => {\n setQueryState((prev) => {\n const clone = { ...prev };\n clone[queryKey] =\n typeof next === \"function\"\n ? next(clone[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY)\n : next;\n return clone;\n });\n },\n [queryKey, setQueryState],\n );\n\n // Keep the last non-empty data loaded in case you want to show a placeholder\n // during new loads.\n const [lastNonEmptyData, setLastNonEmptyData] = useState<TData | undefined>();\n\n useEffect(() => {\n if (query.keepLastNonEmptyData && queryStateValue.data !== undefined) {\n setLastNonEmptyData(queryStateValue.data);\n }\n }, [query.keepLastNonEmptyData, queryStateValue.data]);\n\n // The fetch logic wrapped in useCallback to be stable for useEffect\n const execute = useCallback(\n async (isRefetch: boolean) => {\n query.log(\n `Begin executing shared query ${queryKey}, isRefetch=${isRefetch}`,\n );\n\n if (!isMounted.current) return;\n\n setQueryStateValue((prev) => ({\n // Keep old data and error.\n ...prev,\n lastUpdatedMs: Date.now(),\n loading: true,\n }));\n\n try {\n let state: QueryStateValue<TData>;\n\n if (isRefetch) {\n const data = await query.refetch(...stableArgs);\n const now = Date.now();\n\n state = {\n // Don't keep old error.\n lastUpdatedMs: now,\n loading: false,\n data,\n dataUpdatedMs: now,\n };\n } else {\n state = await query.getCachedOrFetch(...stableArgs);\n }\n\n if (!isMounted.current) return;\n\n // Make sure to reset loading to false.\n setQueryStateValue({ ...state, loading: false });\n } catch (e) {\n if (!isMounted.current) return;\n\n const now = Date.now();\n\n setQueryStateValue((prev) => ({\n // Keep old data.\n ...prev,\n lastUpdatedMs: now,\n loading: false,\n error: e,\n errorUpdatedMs: now,\n }));\n }\n },\n [query, queryKey, setQueryStateValue, stableArgs],\n );\n\n useEffect(() => {\n isMounted.current = true;\n query.mount(queryKey);\n\n void execute(false);\n\n return () => {\n isMounted.current = false;\n query.unmount(queryKey);\n };\n }, [execute, query, queryKey]);\n\n return useMemo(() => {\n return {\n ...queryStateValue,\n\n // Fill with lastNonEmptyData when data is undefined.\n data:\n queryStateValue.data !== undefined\n ? queryStateValue.data\n : lastNonEmptyData,\n latestData: queryStateValue.data,\n\n // This works only if the user-given queryFn supports abort. If not,\n // this function doesn't do anything, since we don't have any means to\n // abort the running queryFn.\n abortCurrentQuery: (reason?: unknown): boolean => {\n const controller = query.getAbortControllerByKey(queryKey);\n controller?.abort(reason);\n return Boolean(controller);\n },\n refetch: (): Promise<void> => {\n return execute(true);\n },\n setData: (data: TData, dataUpdatedMs?: number): void => {\n query.setData(queryKey, data, dataUpdatedMs);\n },\n deleteData: (): void => {\n query.deleteData(queryKey);\n },\n } satisfies UseQueryResult<TData>;\n }, [execute, lastNonEmptyData, query, queryKey, queryStateValue]);\n}\n"],"mappings":"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","names":["getByteSize","useCallback","useEffect","useMemo","useRef","useState","XXH","init_stringify","SharedQueryConfig","args","configureSharedQuery","config","seenQueryNames","sharedQuery","options","url","queryName","inflightQueries","mountedKeys","queryFn","params","firstArg","entries","k","v","item","fetcher","refetchFn","staleMs","expiryMs","refetchOnStale","store","isValid","u","persistTo","maxSize","maxBytes","keepLastNonEmptyData","log","queryState","sharedState","isStale","dataUpdatedMs","isExpired","enforceSizeLimit","oldState","oldSize","oldByteSize","getByteSize","numToCut","bytesToCut","newState","needUpdate","deleteKeyIfNotMounted","key","expired","byteSize","entriesByTimeAscending","entry1","entry2","setData","queryKey","data","entry","prev","startFetching","queryType","e","dedupedFetch","inflightQuery","promise","abortController","obj","s","stringifyDeterministicForKeys","XXH","startMs","waitedMs","cached","now","stateValue","clone","inflight","count","DEFAULT_QUERY_STATE_ENTRY","useSharedQuery","query","stableArgs","useDeepMemo","setQueryState","useSharedState","isMounted","useRef","useMemo","queryStateValue","setQueryStateValue","useCallback","next","lastNonEmptyData","setLastNonEmptyData","useState","useEffect","execute","isRefetch","state","reason","controller"]}
1
+ {"version":3,"sources":["../src/sharedQuery.ts"],"sourcesContent":["/**\n * @packageDocumentation\n *\n * The \"shared query\" is a data fetching convenience library that helps users\n * to load data, check for staleness and expiration, cache in memory, local\n * storage or indexed db, and be able to manipulate the data anytime.\n *\n * Each shared query creates a new private shared state that handles the data\n * storage. This shared state contains the records for all queries, where each\n * query is a unique set of arguments used to make the query. The number of\n * records and the total byte size of data can be limited so as to ensure the\n * query does not take up too much memory.\n *\n * Think of shared queries as like the open source npm package \"react-query\",\n * but with strong type-checking and built-in support for persistence.\n *\n * Example:\n * ```\n * import { sharedQuery, useSharedQuery } from \"@choksheak/react-utils/sharedQuery\";\n * import { MS_PER_DAY } from \"@choksheak/ts-utils/timeConstants\";\n *\n * // Create a new shared query in the top level scope.\n * // The query function `queryFn` here does not take any arguments.\n * //\n * // Only the queryName and queryFn are required and everything else is\n * // optional.\n * //\n * // The queryName must be unique, otherwise we will throw an\n * // error. This is because defining the same queryName for two different\n * // queries implies that the queries are the same, thus violating the idea\n * // of a \"shared\" query, and almost certainly points to a bug in the code.\n * export const usersQuery = sharedQuery({\n * queryName: \"users\",\n * queryFn: listUsers,\n * persistTo: \"indexedDb\",\n * staleMs: MS_PER_DAY,\n * });\n *\n * // Example of using a query function with arguments.\n * export const getUserQuery = sharedQuery({\n * queryName: \"getUser\",\n * queryFn: getUser, // getUser(userId: string) => User\n * persistTo: \"indexedDb\",\n * staleMs: MS_PER_DAY,\n * });\n *\n * export const UsersComponent: React.FC = () => {\n * // Second argument is optional, and is an array of arguments to be\n * // passed to the `queryFn`.\n * const users = useSharedQuery(usersQuery);\n *\n * return (\n * <>\n * <h1>List of Users</h1>\n *\n * <button onclick={users.refetch}>Refresh</button>\n *\n * {users.loading && <Loader />}\n *\n * {users.error && !users.data && <p>Error: {String(users.error)}</p>}\n *\n * <p>Data: {users.data ? JSON.stringify(users.data) : \"-\"}</p>\n * </>\n * );\n * };\n *\n * export const AnotherComponent: React.FC = () => {\n * // This query will be deduped with the one above, sharing the same\n * // data and the same fetches.\n * const users = useSharedQuery(usersQuery);\n * ...\n * };\n *\n * export const DisplayOneUser: React.FC<{userId: string}> = ({userId}) => {\n * // Example of using the query with an argument. Whenever the argument\n * // changes, the data will be fetched and updated automatically.\n * const user = useSharedQuery(getUserQuery, userId);\n *\n * // You can check for user.loading and user.error if needed.\n *\n * return <>User: {user.data ? JSON.stringify(user.data) : \"-\"}</>;\n * };\n * ```\n */\n\nimport { http, retries } from \"@choksheak/ts-utils/http\";\nimport getByteSize from \"object-sizeof\";\nimport {\n SetStateAction,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport XXH from \"xxhashjs\";\n\nimport { sharedState, SharedStateOptions, useSharedState } from \"./sharedState\";\nimport { PersistTo } from \"./storage\";\nimport { stringifyDeterministicForKeys } from \"./stringify\";\nimport { useDeepMemo } from \"./useDeepMemo\";\n\n/** Type of the queryFn used to fetch the data from source. */\nexport type QueryFn<TArgs extends unknown[], TData> = (\n ...args: TArgs\n) => Promise<TData> | TData;\n\n/** An entry for a persisted state corresponding to one query key. */\nexport type QueryStateValue<TData> = Readonly<{\n /**\n * When this record was last updated. Used for LRU determination. If you want\n * the data age, please use `dataUpdatedMs` instead.\n */\n lastUpdatedMs: number;\n\n loading: boolean;\n data?: TData;\n dataUpdatedMs?: number;\n\n /**\n * Use unknown type for error because we can't assume what type of variable\n * will be thrown from the query function. We also don't want to add this\n * type to QueryStateValue<TError> as it adds complexity to the type.\n */\n error?: unknown;\n\n errorUpdatedMs?: number;\n}>;\n\n/** Return type for useSharedQuery(). */\nexport type UseQueryResult<TData> = QueryStateValue<TData> &\n Readonly<{\n /**\n * Usually same as `data`, but if you specify `keepLastNonEmptyData: true`,\n * then this will contain the latest data (undefined when args just changed)\n * and data will contain the last non empty data.\n */\n latestData: TData | undefined;\n\n /**\n * Cancels the inflight query (if any). Returns true if canceled.\n */\n abortCurrentQuery: (reason?: unknown) => boolean;\n\n /**\n * Manually refresh the data by doing a new query.\n */\n refetch: () => Promise<void>;\n\n /**\n * If you got the data from somewhere else (e.g. an in-memory data update),\n * just set it directly.\n */\n setData: (data: TData, dataUpdatedMs?: number) => void;\n\n deleteData: () => void;\n }>;\n\n/**\n * The full query state is a record of all the fetch states mapped by\n * query key to the state entry.\n */\nexport type SharedQueryState<TData> = Record<string, QueryStateValue<TData>>;\n\nexport type QueryTargetWithFn<TArgs extends unknown[], TData> = {\n /**\n * The name could have been auto-generated, but we let the user give us a\n * human-readable name that can be identified quickly in the logs.\n * Note that the queryKey is a string formed by the queryName plus the query\n * arguments.\n */\n queryName: string;\n\n /** Function to fetch data. */\n queryFn: QueryFn<TArgs, TData>;\n};\n\nexport type QueryTargetWithUrl = {\n /**\n * The queryName can optionally be specified, else we will fallback to use\n * the url as the queryName.\n */\n queryName?: string;\n\n /**\n * URL to fetch data using HTTP GET. Note that using this field will cause\n * both TArgs and TData to be undefined (unknown). Therefore you can make up\n * for this by specifying types with useSharedQuery, like this:\n * ```\n * const usersQuery = useSharedQuery<[{ userId?: string[] }], User[]>({\n * url: \"/api/list-users\",\n * });\n * ```\n *\n * If `userId` were specified as [\"u1\", \"u2\"], the query url would be\n * \"/api/list-users?userId=u1&userId=u2\".\n *\n * You can also pass the params as a string like \"userId=u1&userId=u2\".\n * ```\n * const usersQuery = useSharedQuery<[string], User[]>({\n * url: \"/api/list-users\",\n * });\n * ```\n */\n url: string;\n};\n\n/** Specifies how to fetch the data. */\nexport type QueryTarget<TArgs extends unknown[], TData> =\n | QueryTargetWithFn<TArgs, TData>\n | QueryTargetWithUrl;\n\n/** Options to configure a shared query. */\nexport type SharedQueryOptions<TArgs extends unknown[], TData> = QueryTarget<\n TArgs,\n TData\n> & {\n /**\n * Function to re-fetch data. Usually this would just be the `queryFn`, but\n * if you need a different behavior for refetching, then specify this.\n */\n refetchFn?: QueryFn<TArgs, TData>;\n\n /**\n * ms before data considered stale; 0 means always stale.\n */\n staleMs?: number;\n\n /**\n * ms before data is removed from cache; 0 means never expire.\n */\n expiryMs?: number;\n\n /**\n * Trigger background re-fetch if stale.\n */\n refetchOnStale?: boolean;\n\n /**\n * Shortcut to set the localStorageKey or indexedDbKey automatically from\n * queryName. This is important because it avoids requiring the user to\n * specify the same queryName twice everytime.\n */\n persistTo?: PersistTo;\n\n /**\n * Max number of entries to keep. The oldest entries will be discarded.\n * The last record cannot be discarded. 0 means no limit.\n */\n maxSize?: number;\n\n /**\n * Max number of bytes to keep. The oldest entries will be discarded.\n * The last record cannot be discarded. 0 means no limit.\n */\n maxBytes?: number;\n\n /**\n * Keep a copy of the last non-empty data so that when you change the\n * query arguments, you can still display the old data while waiting for the\n * new data to load.\n */\n keepLastNonEmptyData?: boolean;\n\n /**\n * Customize the logger.\n */\n log?: (...args: unknown[]) => void;\n} & SharedStateOptions<SharedQueryState<TData>>;\n\n/** Users can override these values globally. */\nexport const SharedQueryConfig = {\n /** Refetch on every page load. */\n staleMs: 0,\n\n /** Keep in cache for a long time. */\n expiryMs: 30 * 86_400_000 /* MS_PER_DAY */,\n\n /** Always refetch automatically if the data is stale. */\n refetchOnStale: true,\n\n /** No default persistence configured. */\n persistTo: undefined as PersistTo | undefined,\n\n /** 100 is not too large, but please tweak accordingly. */\n maxSize: 100,\n\n /** 100kb limit before discarding old records. */\n maxBytes: 100_000,\n\n /** Default to log to console. */\n log: (...args: unknown[]) => console.log(\"[sharedQuery]\", ...args),\n};\n\nexport type SharedQueryConfig = typeof SharedQueryConfig;\n\n/** Convenience function to update global defaults. */\nexport function configureSharedQuery(config: Partial<SharedQueryConfig>) {\n Object.assign(SharedQueryConfig, config);\n}\n\n/** Whether this is a normal query or an explicit refetch. */\ntype QueryType = \"query\" | \"refetch\";\n\n// Disallow duplicate query names.\nconst seenQueryNames = new Set<string>();\n\n/**\n * Create a reusable shared query object that can be used to auto de-duplicate\n * and cache all data fetches, with auto-expiration and staleness checks.\n *\n * Each `queryName` must be unique, and can only be declared once, most often\n * in the top level scope. The queryName is mainly used for logging and also\n * used as the storage key when you use the top-level `persistTo`.\n *\n * Example:\n * ```\n * export const usersQuery = sharedQuery({\n * queryName: \"users\",\n * queryFn: listUsers,\n * persistTo: \"indexedDb\",\n * staleMs: MS_PER_DAY,\n * });\n * ```\n */\nexport function sharedQuery<TArgs extends unknown[], TData>(\n // The name could have been auto-generated, but we let the user give us a\n // human-readable name that can be identified quickly in the logs.\n options: SharedQueryOptions<TArgs, TData>,\n) {\n // If queryName is not specified, fallback to use url as the queryName.\n const url = (options as QueryTargetWithUrl).url || \"\";\n const queryName = options.queryName || url;\n\n if (!queryName) {\n throw new Error(`Either queryName or url must be specified.`);\n }\n\n if (seenQueryNames.has(queryName)) {\n throw new Error(`Duplicate shared query name \"${queryName}\"`);\n }\n seenQueryNames.add(queryName);\n\n const inflightQueries = new Map<\n string,\n { promise: Promise<TData>; abortController: AbortController }\n >();\n\n /**\n * Keep track of the mounted query keys. This handles the case where an\n * earlier-started query returns later, and when it returns, it was already\n * unmounted, thus making it eligible for clean up right after fetch.\n */\n const mountedKeys = new Map<string, number>();\n\n let queryFn: QueryFn<TArgs, TData>;\n\n if ((options as QueryTargetWithFn<TArgs, TData>).queryFn) {\n queryFn = (options as QueryTargetWithFn<TArgs, TData>).queryFn;\n } else if (url) {\n const fetcher = http.use(retries());\n\n // Auto-generate the queryFn as a HTTP GET request using the url.\n queryFn = async (...args: TArgs): Promise<TData> => {\n // If args are given, treat the first argument as a search param.\n let params = \"\";\n const firstArg = args?.[0];\n\n if (firstArg) {\n if (typeof firstArg === \"string\") {\n params = firstArg;\n } else if (typeof firstArg === \"object\") {\n const entries: [string, string][] = [];\n\n for (const [k, v] of Object.entries(firstArg)) {\n if (v === undefined) continue;\n\n // Allow the same param to be specified multiple times.\n if (Array.isArray(v)) {\n for (const item of v) {\n if (item === undefined) continue;\n\n // The field values can only be primitives, not objects.\n entries.push([k, String(item)]);\n }\n } else {\n // The field values can only be primitives, not objects.\n entries.push([k, String(v)]);\n }\n }\n\n params = new URLSearchParams(entries).toString();\n }\n\n if (params) {\n if (url.includes(\"?\")) {\n // Append more search params to existing ones.\n params = \"&\" + params;\n } else {\n // Set the full search params string.\n params = \"?\" + params;\n }\n }\n }\n\n const response = await fetcher.get(url + params);\n\n return await response.json();\n };\n } else {\n throw new Error(`Either queryFn or url must be specified`);\n }\n\n const refetchFn = options.refetchFn;\n\n const staleMs = options?.staleMs ?? SharedQueryConfig.staleMs;\n const expiryMs = options?.expiryMs ?? SharedQueryConfig.expiryMs;\n\n const refetchOnStale =\n options?.refetchOnStale ?? SharedQueryConfig.refetchOnStale;\n\n // Apply shortcut when using `persistTo`.\n // Specifying the `store` will take precedence over `persistTo`.\n const store = options.store;\n\n if (store) {\n // Wrap the isValid to account for the query state wrapper.\n if (\"persistTo\" in store && store.isValid) {\n const isValid = store.isValid;\n\n options = {\n ...options,\n store: {\n ...store,\n isValid: (u) => {\n // Supposed type of u: SharedQueryState<TData>\n if (!u || typeof u !== \"object\") {\n return false;\n }\n\n // Since each stored key could contain multiple values, and isValid\n // checks for each value, we need to check every value to make sure\n // they are valid.\n // Supposed type of v: QueryStateValue<TData>\n for (const v of Object.values(u)) {\n if (\n !v ||\n typeof v !== \"object\" ||\n !(\"data\" in v) ||\n !(\"dataUpdatedMs\" in v) ||\n !(\"lastUpdatedMs\" in v) ||\n typeof v.dataUpdatedMs !== \"number\" ||\n typeof v.lastUpdatedMs !== \"number\" ||\n !isValid(v.data)\n ) {\n return false;\n }\n }\n\n return true;\n },\n },\n };\n }\n } else {\n const persistTo = options.persistTo ?? SharedQueryConfig.persistTo;\n\n if (persistTo) {\n options = {\n ...options,\n store: {\n persistTo,\n key: queryName,\n expiryMs,\n },\n };\n }\n }\n\n const maxSize = options?.maxSize ?? SharedQueryConfig.maxSize;\n const maxBytes = options?.maxBytes ?? SharedQueryConfig.maxBytes;\n\n const keepLastNonEmptyData = Boolean(options?.keepLastNonEmptyData);\n\n const log = options?.log ?? SharedQueryConfig.log;\n\n const queryState = sharedState<SharedQueryState<TData>>(\n {}, // defaultValue\n options,\n );\n\n function isStale(dataUpdatedMs: number): boolean {\n if (staleMs === 0) return true; // always stale\n\n const ageMs = Date.now() - dataUpdatedMs;\n return ageMs > staleMs;\n }\n\n function isExpired(dataUpdatedMs: number): boolean {\n if (expiryMs === 0) return false; // never expire\n\n const ageMs = Date.now() - dataUpdatedMs;\n return ageMs > expiryMs;\n }\n\n function enforceSizeLimit(): void {\n // Skip if there are no limits.\n if (!maxSize && !maxBytes) {\n return;\n }\n\n const oldState = queryState.getSnapshot();\n const oldSize = Object.keys(oldState).length;\n\n // Cannot do GC if size is 1 or less.\n if (oldSize <= 1) {\n return;\n }\n\n const oldByteSize = getByteSize(oldState);\n\n let numToCut = maxSize ? oldSize - maxSize : 0;\n let bytesToCut = maxBytes ? oldByteSize - maxBytes : 0;\n\n // Log to indicate why trimming was not needed.\n if (numToCut <= 0 && bytesToCut <= 0) {\n log(\n `No need to trim data for ${queryName}: size=${oldSize} (limit=${maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${maxBytes.toLocaleString()})`,\n );\n return;\n }\n\n // Log to inform user that trimming is needed.\n log(\n `Need to trim ${queryName}: numToCut=${numToCut}, bytesToCut=${bytesToCut}`,\n );\n\n const newState = { ...oldState }; // shallow clone\n let needUpdate = false;\n\n const deleteKeyIfNotMounted = (key: string, expired: boolean) => {\n // Mounted keys cannot be cleaned up as they are visible in the UI.\n if (mountedKeys.has(key)) {\n log(`Cannot clean up ${key} as it is mounted`);\n return;\n }\n\n const byteSize = getByteSize(key) + getByteSize(newState[key]);\n log(\n `Cleaning up unmounted${expired ? \", expired\" : \"\"} ${key} (byteSize=${byteSize.toLocaleString()})`,\n );\n\n numToCut--;\n bytesToCut -= byteSize;\n\n delete newState[key];\n needUpdate = true;\n };\n\n let entriesByTimeAscending = Object.entries(newState).sort(\n (entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs,\n );\n\n // Clean up all expired entries. Note that the auto-expiration in the\n // storage adapter only applies to the entire state, which contains all\n // the different query keys and their values. Each query key has its own\n // timestamp and we need to apply our own expiration here.\n for (const [key] of entriesByTimeAscending) {\n // Anything after this is not expired yet.\n if (!isExpired(newState[key].lastUpdatedMs)) {\n break;\n }\n\n deleteKeyIfNotMounted(key, true);\n }\n\n if (numToCut > 0 || bytesToCut > 0) {\n if (needUpdate) {\n entriesByTimeAscending = Object.entries(newState).sort(\n (entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs,\n );\n }\n\n // Clean up starting from oldest entry first.\n for (const [key] of entriesByTimeAscending) {\n deleteKeyIfNotMounted(key, false);\n\n if (numToCut <= 0 && bytesToCut <= 0) {\n break;\n }\n }\n }\n\n // Log to indicate why trimming was not needed.\n if (!needUpdate) {\n log(\n `No trimmable entries found for ${queryName}: size=${oldSize} (limit=${maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${maxBytes.toLocaleString()})`,\n );\n return;\n }\n\n // Update the state.\n queryState.setValue(newState);\n\n // Log to inform user that the data was trimmed.\n log(\n `Trimmed data for ${queryName}: size=[${oldSize} -> ${Object.keys(newState).length}] (limit=${maxSize}), byteSize=[${oldByteSize.toLocaleString()} -> ${getByteSize(newState).toLocaleString()}] (limit=${maxBytes.toLocaleString()})`,\n );\n }\n\n function setData(\n queryKey: string,\n data: TData,\n dataUpdatedMs = Date.now(),\n ): void {\n const entry: QueryStateValue<TData> = {\n lastUpdatedMs: Date.now(),\n loading: false,\n data,\n dataUpdatedMs,\n };\n\n queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));\n\n enforceSizeLimit();\n }\n\n async function startFetching(\n queryKey: string,\n queryType: QueryType,\n args: TArgs,\n ) {\n log(`Start fetching ${queryKey}, queryType=${queryType}`);\n\n try {\n const data = await (queryType === \"refetch\" && refetchFn\n ? refetchFn(...args)\n : queryFn(...args));\n\n log(`Successfully fetched ${queryKey}`);\n setData(queryKey, data, Date.now());\n return data;\n } catch (e) {\n log(`Failed to fetch ${queryKey} for queryType=${queryType}: ${e}`);\n throw e;\n } finally {\n inflightQueries.delete(queryKey);\n }\n }\n\n function dedupedFetch(\n queryKey: string,\n queryType: QueryType,\n ...args: TArgs\n ): Promise<TData> {\n const inflightQuery = inflightQueries.get(queryKey);\n\n // De-duplicate in-flight requests. Note that queries and refetches are\n // also deduped because they should technically be trying to fetch the same\n // data.\n if (inflightQuery) {\n log(\n `Deduplicating inflight fetch for ${queryKey}, queryType=${queryType}`,\n );\n return inflightQuery.promise;\n }\n\n // Fetch new data\n const promise = startFetching(queryKey, queryType, args);\n const abortController = new AbortController();\n\n inflightQueries.set(queryKey, { promise, abortController });\n\n return promise;\n }\n\n const obj = {\n queryName,\n queryFn,\n refetchFn,\n expiryMs,\n staleMs,\n refetchOnStale,\n maxSize,\n maxBytes,\n keepLastNonEmptyData,\n log,\n queryState,\n\n /** Returns a key to identify requests based on the given args. */\n getQueryKey(args: TArgs): string {\n let s = stringifyDeterministicForKeys(args);\n\n // Ensure the query key is not too long.\n if (s.length > 30) {\n s = XXH.h32(s, 0x99ee).toString(16).slice(2); // remove \"0x\"\n }\n\n return queryName + \":\" + s;\n },\n\n /**\n * Get the AbortController to abort the inflight query (if any).\n *\n * Note that once you get the controller, you can also get the signal easily\n * using `controller.signal`. So if you need both the controller and the\n * signal, just use this function.\n */\n getAbortController(args: TArgs): AbortController | null {\n const queryKey = obj.getQueryKey(args);\n return obj.getAbortControllerByKey(queryKey);\n },\n\n getAbortControllerByKey(queryKey: string): AbortController | null {\n return inflightQueries.get(queryKey)?.abortController ?? null;\n },\n\n /**\n * Get the AbortSignal to check for query abortions.\n *\n * Example using fetch:\n * ```\n * const getUserQuery = sharedQuery({\n * queryName: \"getUser\",\n * queryFn: (userId: string) => {\n * // Get the signal for this current execution.\n * const signal = getUserQuery.getAbortSignal([userId]);\n *\n * // Pass the signal to fetch so that it can be aborted.\n * const response = await fetch(`/users/${userId}`, { signal });\n *\n * // Check for errors.\n * if (!response.ok) {\n * throw new Error(response.statusText);\n * }\n *\n * // Return the data.\n * return await response.json();\n * },\n * });\n * ```\n *\n * Example using `http.ts`:\n * ```\n * const getUserQuery = sharedQuery({\n * queryName: \"getUser\",\n * queryFn: (userId: string) => {\n * // Get the signal for this current execution.\n * const signal = getUserQuery.getAbortSignal([userId]);\n *\n * // Pass the signal to fetch so that it can be aborted.\n * const response = await http.get(`/users/${userId}`, { signal });\n * return await response.json();\n * },\n * });\n * ```\n */\n getAbortSignal(args: TArgs): AbortSignal | null {\n const queryKey = obj.getQueryKey(args);\n return obj.getAbortSignalByKey(queryKey);\n },\n\n getAbortSignalByKey(queryKey: string): AbortSignal | null {\n return obj.getAbortControllerByKey(queryKey)?.signal ?? null;\n },\n\n /** For cached data, we need to return the correct timestamps as well. */\n async getCachedOrFetch(...args: TArgs): Promise<QueryStateValue<TData>> {\n // Important: wait for data to be loaded from cache first before trying to\n // fetch from server. This fixes a bad bug where the data is big, and the\n // code here starts a server fetch before the cache data loaded.\n const startMs = Date.now();\n await queryState.readyPromise;\n const waitedMs = Date.now() - startMs;\n\n const queryKey = obj.getQueryKey(args);\n const cached = queryState.getSnapshot()?.[queryKey];\n\n if (waitedMs) {\n log(`${queryKey}: Waited for ${waitedMs}ms before data is ready`);\n }\n\n if (cached?.data && !isExpired(cached.dataUpdatedMs ?? 0)) {\n // Return cached value if not stale.\n if (!isStale(cached.dataUpdatedMs ?? 0)) {\n log(`Return fresh data ${queryKey} from cache without fetching`);\n return cached;\n }\n\n // If stale, optionally update the data in the background.\n if (refetchOnStale) {\n log(`refetchOnStale for ${queryKey}`);\n void dedupedFetch(queryKey, \"query\", ...args);\n }\n\n // Still return stale data immediately.\n log(`Returning stale data for ${queryKey}`);\n return cached;\n }\n\n const data = await dedupedFetch(queryKey, \"query\", ...args);\n const now = Date.now();\n\n return {\n // Don't keep old error.\n lastUpdatedMs: now,\n loading: false,\n data,\n dataUpdatedMs: now,\n };\n },\n\n /** Get the entire stored entry for a query key. */\n getQueryValue(queryKey: string): QueryStateValue<TData> | undefined {\n return queryState.getSnapshot()?.[queryKey];\n },\n\n /** Get the current stored data for a query key. */\n getData(queryKey: string): TData | undefined {\n const stateValue: QueryStateValue<TData> | undefined =\n queryState.getSnapshot()?.[queryKey];\n\n // Check for expiration.\n if (stateValue?.lastUpdatedMs) {\n if (isExpired(stateValue.lastUpdatedMs)) {\n obj.deleteData(queryKey);\n return undefined;\n }\n }\n\n return stateValue?.data;\n },\n\n /** Set the data directly if the user obtained it from somewhere else. */\n setData,\n\n /**\n * Do a new fetch even when the data is already cached, but don't fetch if\n * another fetch is already inflight.\n */\n async updateFromSource(...args: TArgs): Promise<TData> {\n const key = obj.getQueryKey(args);\n return await dedupedFetch(key, \"query\", ...args);\n },\n\n /**\n * Perform an explicit refetch. If `options.refetchFn` was not specified,\n * then this would be identical to calling `obj.updateFromSource()`.\n */\n async refetch(...args: TArgs): Promise<TData> {\n const key = obj.getQueryKey(args);\n return await dedupedFetch(key, \"refetch\", ...args);\n },\n\n /** Delete the cached data for one set of arguments. */\n deleteData(queryKey: string): void {\n const record = queryState.getSnapshot();\n const clone = { ...record };\n delete clone[queryKey];\n queryState.setValue(clone);\n\n const inflight = inflightQueries.get(queryKey);\n if (inflight) {\n inflightQueries.delete(queryKey);\n inflight.abortController.abort(\"Deleted\");\n }\n },\n\n /** Delete all currently cached data & all inflight promises. */\n clear(): void {\n queryState.delete();\n\n for (const { abortController } of Array.from(inflightQueries.values())) {\n abortController.abort(\"Cleared\");\n }\n\n inflightQueries.clear();\n },\n\n /** Keep track of mounted keys. */\n mount(queryKey: string): void {\n mountedKeys.set(queryKey, (mountedKeys.get(queryKey) ?? 0) + 1);\n },\n\n /** Clean up mounted keys. */\n unmount(queryKey: string): void {\n const count = mountedKeys.get(queryKey);\n if (count === 1) {\n mountedKeys.delete(queryKey);\n } else {\n mountedKeys.set(queryKey, (count ?? 0) - 1);\n }\n },\n\n /**\n * Discard the oldest entries if the size exceeds the limit. The last\n * remaining record cannot be deleted no matter what the limit is.\n */\n enforceSizeLimit,\n } as const;\n\n return obj;\n}\n\nexport type SharedQuery<TArgs extends unknown[], TData> = ReturnType<\n typeof sharedQuery<TArgs, TData>\n>;\n\n// Using any so that we don't need to typecast later.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst DEFAULT_QUERY_STATE_ENTRY: Readonly<QueryStateValue<any>> = {\n lastUpdatedMs: 0,\n loading: true,\n};\n\n/**\n * React hook to make use of a shared query inside any React component. The\n * args do not need to be specified if no args are needed, i.e. this would work\n * ```\n * const allUsers = useSharedQuery(listUsersQuery);\n * ```\n *\n * Example:\n * ```\n * const users = useSharedQuery(usersQuery, \"userid123\");\n * ```\n */\nexport function useSharedQuery<TArgs extends unknown[], TData>(\n query: SharedQuery<TArgs, TData>,\n // Optional: Default to use no arguments.\n ...args: TArgs\n): UseQueryResult<TData> {\n // We don't expect users to provide a stable `args`, so stabilize it here.\n const stableArgs = useDeepMemo(args);\n\n const [queryState, setQueryState] = useSharedState(query.queryState);\n\n const isMounted = useRef(true);\n\n const queryKey = useMemo(\n () => query.getQueryKey(stableArgs),\n [query, stableArgs],\n );\n\n // Init with empty object.\n const queryStateValue: QueryStateValue<TData> =\n queryState[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY;\n\n const setQueryStateValue = useCallback(\n (next: SetStateAction<QueryStateValue<TData>>) => {\n setQueryState((prev) => {\n const clone = { ...prev };\n clone[queryKey] =\n typeof next === \"function\"\n ? next(clone[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY)\n : next;\n return clone;\n });\n },\n [queryKey, setQueryState],\n );\n\n // Keep the last non-empty data loaded in case you want to show a placeholder\n // during new loads.\n const [lastNonEmptyData, setLastNonEmptyData] = useState<TData | undefined>();\n\n useEffect(() => {\n if (query.keepLastNonEmptyData && queryStateValue.data !== undefined) {\n setLastNonEmptyData(queryStateValue.data);\n }\n }, [query.keepLastNonEmptyData, queryStateValue.data]);\n\n // The fetch logic wrapped in useCallback to be stable for useEffect\n const execute = useCallback(\n async (isRefetch: boolean) => {\n query.log(\n `Begin executing shared query ${queryKey}, isRefetch=${isRefetch}`,\n );\n\n if (!isMounted.current) return;\n\n setQueryStateValue((prev) => ({\n // Keep old data and error.\n ...prev,\n lastUpdatedMs: Date.now(),\n loading: true,\n }));\n\n try {\n let state: QueryStateValue<TData>;\n\n if (isRefetch) {\n const data = await query.refetch(...stableArgs);\n const now = Date.now();\n\n state = {\n // Don't keep old error.\n lastUpdatedMs: now,\n loading: false,\n data,\n dataUpdatedMs: now,\n };\n } else {\n state = await query.getCachedOrFetch(...stableArgs);\n }\n\n if (!isMounted.current) return;\n\n // Make sure to reset loading to false.\n setQueryStateValue({ ...state, loading: false });\n } catch (e) {\n if (!isMounted.current) return;\n\n const now = Date.now();\n\n setQueryStateValue((prev) => ({\n // Keep old data.\n ...prev,\n lastUpdatedMs: now,\n loading: false,\n error: e,\n errorUpdatedMs: now,\n }));\n }\n },\n [query, queryKey, setQueryStateValue, stableArgs],\n );\n\n useEffect(() => {\n isMounted.current = true;\n query.mount(queryKey);\n\n void execute(false);\n\n return () => {\n isMounted.current = false;\n query.unmount(queryKey);\n };\n }, [execute, query, queryKey]);\n\n return useMemo(() => {\n return {\n ...queryStateValue,\n\n // Fill with lastNonEmptyData when data is undefined.\n data:\n queryStateValue.data !== undefined\n ? queryStateValue.data\n : lastNonEmptyData,\n latestData: queryStateValue.data,\n\n // This works only if the user-given queryFn supports abort. If not,\n // this function doesn't do anything, since we don't have any means to\n // abort the running queryFn.\n abortCurrentQuery: (reason?: unknown): boolean => {\n const controller = query.getAbortControllerByKey(queryKey);\n controller?.abort(reason);\n return Boolean(controller);\n },\n refetch: (): Promise<void> => {\n return execute(true);\n },\n setData: (data: TData, dataUpdatedMs?: number): void => {\n query.setData(queryKey, data, dataUpdatedMs);\n },\n deleteData: (): void => {\n query.deleteData(queryKey);\n },\n } satisfies UseQueryResult<TData>;\n }, [execute, lastNonEmptyData, query, queryKey, queryStateValue]);\n}\n"],"mappings":"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","names":["http","retries","getByteSize","useCallback","useEffect","useMemo","useRef","useState","XXH","init_stringify","SharedQueryConfig","args","configureSharedQuery","config","seenQueryNames","sharedQuery","options","url","queryName","inflightQueries","mountedKeys","queryFn","fetcher","http","retries","params","firstArg","entries","k","v","item","refetchFn","staleMs","expiryMs","refetchOnStale","store","isValid","u","persistTo","maxSize","maxBytes","keepLastNonEmptyData","log","queryState","sharedState","isStale","dataUpdatedMs","isExpired","enforceSizeLimit","oldState","oldSize","oldByteSize","getByteSize","numToCut","bytesToCut","newState","needUpdate","deleteKeyIfNotMounted","key","expired","byteSize","entriesByTimeAscending","entry1","entry2","setData","queryKey","data","entry","prev","startFetching","queryType","e","dedupedFetch","inflightQuery","promise","abortController","obj","s","stringifyDeterministicForKeys","XXH","startMs","waitedMs","cached","now","stateValue","clone","inflight","count","DEFAULT_QUERY_STATE_ENTRY","useSharedQuery","query","stableArgs","useDeepMemo","setQueryState","useSharedState","isMounted","useRef","useMemo","queryStateValue","setQueryStateValue","useCallback","next","lastNonEmptyData","setLastNonEmptyData","useState","useEffect","execute","isRefetch","state","reason","controller"]}
package/sharedQuery.mjs CHANGED
@@ -1,6 +1,3 @@
1
- import {
2
- fetcher
3
- } from "./chunk-3V7WBGWQ.mjs";
4
1
  import {
5
2
  sharedState,
6
3
  useSharedState
@@ -16,6 +13,7 @@ import {
16
13
  import "./chunk-X6BYQHVC.mjs";
17
14
 
18
15
  // src/sharedQuery.ts
16
+ import { http, retries } from "@choksheak/ts-utils/http";
19
17
  import getByteSize from "object-sizeof";
20
18
  import {
21
19
  useCallback,
@@ -62,7 +60,8 @@ function sharedQuery(options) {
62
60
  if (options.queryFn) {
63
61
  queryFn = options.queryFn;
64
62
  } else if (url) {
65
- queryFn = (...args) => {
63
+ const fetcher = http.use(retries());
64
+ queryFn = async (...args) => {
66
65
  let params = "";
67
66
  const firstArg = args?.[0];
68
67
  if (firstArg) {
@@ -91,7 +90,8 @@ function sharedQuery(options) {
91
90
  }
92
91
  }
93
92
  }
94
- return fetcher.url(url + params).get().json();
93
+ const response = await fetcher.get(url + params);
94
+ return await response.json();
95
95
  };
96
96
  } else {
97
97
  throw new Error(`Either queryFn or url must be specified`);
@@ -320,7 +320,7 @@ function sharedQuery(options) {
320
320
  * });
321
321
  * ```
322
322
  *
323
- * Example using `fetcher.ts`:
323
+ * Example using `http.ts`:
324
324
  * ```
325
325
  * const getUserQuery = sharedQuery({
326
326
  * queryName: "getUser",
@@ -329,7 +329,8 @@ function sharedQuery(options) {
329
329
  * const signal = getUserQuery.getAbortSignal([userId]);
330
330
  *
331
331
  * // Pass the signal to fetch so that it can be aborted.
332
- * return fetcher.url(`/users/${userId}`).options({ signal }).get().json();
332
+ * const response = await http.get(`/users/${userId}`, { signal });
333
+ * return await response.json();
333
334
  * },
334
335
  * });
335
336
  * ```
@@ -1,16 +0,0 @@
1
- // src/fetcher.ts
2
- import wretch from "wretch";
3
- import { retry } from "wretch/middlewares";
4
- var fetcher = wretch().middlewares([
5
- retry({
6
- maxAttempts: 3,
7
- delayTimer: 1e3,
8
- retryOnNetworkError: true
9
- })
10
- ]);
11
- var fetcherNoRetries = wretch();
12
-
13
- export {
14
- fetcher,
15
- fetcherNoRetries
16
- };
@@ -1,2 +0,0 @@
1
- import r from"wretch";import{retry as e}from"wretch/middlewares";var m=r().middlewares([e({maxAttempts:3,delayTimer:1e3,retryOnNetworkError:!0})]),c=r();export{m as a,c as b};
2
- //# sourceMappingURL=chunk-6ZZ54XM5.min.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/fetcher.ts"],"sourcesContent":["/**\n * @packageDocumentation\n *\n * An alternate syntax to fetch() that allows function chaining to compose the\n * entire fetch request. One can also compose partial fetcher objects to be\n * reused in future requests.\n *\n * Examples:\n * ```\n * const userFetcher = fetcher\n * .auth(\"Bearer TOPSECRET\")\n * .content(\"application/json\")\n * .url(\"/api/user\");\n *\n * const user = await userFetcher.get();\n *\n * const updateResult = await userFetcher\n * .body({ id: user.id, name: \"New name\"})\n * .post();\n * ```\n */\n\nimport wretch from \"wretch\";\nimport { retry } from \"wretch/middlewares\";\n\n/**\n * Fetcher with default short retries. Use this to make your queries more\n * robust without any additional work.\n */\nexport const fetcher = wretch().middlewares([\n retry({\n maxAttempts: 3,\n delayTimer: 1000,\n retryOnNetworkError: true,\n }),\n]);\n\n/**\n * Name this as `fetcherNoRetries` to make it easier to understand than\n * `wretch`. Use this if you don't want retries. Use `fetcher` if you want\n * retries.\n */\nexport const fetcherNoRetries = wretch();\n"],"mappings":"AAsBA,OAAOA,MAAY,SACnB,OAAS,SAAAC,MAAa,qBAMf,IAAMC,EAAUF,EAAO,EAAE,YAAY,CAC1CC,EAAM,CACJ,YAAa,EACb,WAAY,IACZ,oBAAqB,EACvB,CAAC,CACH,CAAC,EAOYE,EAAmBH,EAAO","names":["wretch","retry","fetcher","fetcherNoRetries"]}
@@ -1,2 +0,0 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }var _wretch = require('wretch'); var _wretch2 = _interopRequireDefault(_wretch);var _middlewares = require('wretch/middlewares');var m=_wretch2.default.call(void 0, ).middlewares([_middlewares.retry.call(void 0, {maxAttempts:3,delayTimer:1e3,retryOnNetworkError:!0})]),c= exports.b =_wretch2.default.call(void 0, );exports.a = m; exports.b = c;
2
- //# sourceMappingURL=chunk-O3RBYG3W.min.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/fetcher.ts"],"names":["fetcher","wretch","retry","fetcherNoRetries"],"mappings":"AAsBA,yPAAmB,iDACG,IAMTA,CAAAA,CAAUC,8BAAAA,CAAO,CAAE,WAAA,CAAY,CAC1CC,gCAAAA,CACE,WAAA,CAAa,CAAA,CACb,UAAA,CAAY,GAAA,CACZ,mBAAA,CAAqB,CAAA,CACvB,CAAC,CACH,CAAC,CAAA,CAOYC,CAAAA,aAAmBF,8BAAAA,CAAO,CAAA,6BAAA","file":"/Users/choksheaklau/src/react-utils/dist/chunk-O3RBYG3W.min.cjs","sourcesContent":["/**\n * @packageDocumentation\n *\n * An alternate syntax to fetch() that allows function chaining to compose the\n * entire fetch request. One can also compose partial fetcher objects to be\n * reused in future requests.\n *\n * Examples:\n * ```\n * const userFetcher = fetcher\n * .auth(\"Bearer TOPSECRET\")\n * .content(\"application/json\")\n * .url(\"/api/user\");\n *\n * const user = await userFetcher.get();\n *\n * const updateResult = await userFetcher\n * .body({ id: user.id, name: \"New name\"})\n * .post();\n * ```\n */\n\nimport wretch from \"wretch\";\nimport { retry } from \"wretch/middlewares\";\n\n/**\n * Fetcher with default short retries. Use this to make your queries more\n * robust without any additional work.\n */\nexport const fetcher = wretch().middlewares([\n retry({\n maxAttempts: 3,\n delayTimer: 1000,\n retryOnNetworkError: true,\n }),\n]);\n\n/**\n * Name this as `fetcherNoRetries` to make it easier to understand than\n * `wretch`. Use this if you don't want retries. Use `fetcher` if you want\n * retries.\n */\nexport const fetcherNoRetries = wretch();\n"]}
@@ -1,16 +0,0 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }// src/fetcher.ts
2
- var _wretch = require('wretch'); var _wretch2 = _interopRequireDefault(_wretch);
3
- var _middlewares = require('wretch/middlewares');
4
- var fetcher = _wretch2.default.call(void 0, ).middlewares([
5
- _middlewares.retry.call(void 0, {
6
- maxAttempts: 3,
7
- delayTimer: 1e3,
8
- retryOnNetworkError: true
9
- })
10
- ]);
11
- var fetcherNoRetries = _wretch2.default.call(void 0, );
12
-
13
-
14
-
15
-
16
- exports.fetcher = fetcher; exports.fetcherNoRetries = fetcherNoRetries;
package/fetcher.cjs DELETED
@@ -1,9 +0,0 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
-
3
-
4
- var _chunkUFJNJ3WVcjs = require('./chunk-UFJNJ3WV.cjs');
5
- require('./chunk-74BS2FAO.cjs');
6
-
7
-
8
-
9
- exports.fetcher = _chunkUFJNJ3WVcjs.fetcher; exports.fetcherNoRetries = _chunkUFJNJ3WVcjs.fetcherNoRetries;
package/fetcher.d.mts DELETED
@@ -1,36 +0,0 @@
1
- import * as wretch_types from 'wretch/types';
2
-
3
- /**
4
- * @packageDocumentation
5
- *
6
- * An alternate syntax to fetch() that allows function chaining to compose the
7
- * entire fetch request. One can also compose partial fetcher objects to be
8
- * reused in future requests.
9
- *
10
- * Examples:
11
- * ```
12
- * const userFetcher = fetcher
13
- * .auth("Bearer TOPSECRET")
14
- * .content("application/json")
15
- * .url("/api/user");
16
- *
17
- * const user = await userFetcher.get();
18
- *
19
- * const updateResult = await userFetcher
20
- * .body({ id: user.id, name: "New name"})
21
- * .post();
22
- * ```
23
- */
24
- /**
25
- * Fetcher with default short retries. Use this to make your queries more
26
- * robust without any additional work.
27
- */
28
- declare const fetcher: wretch_types.Wretch<unknown, unknown, undefined, undefined>;
29
- /**
30
- * Name this as `fetcherNoRetries` to make it easier to understand than
31
- * `wretch`. Use this if you don't want retries. Use `fetcher` if you want
32
- * retries.
33
- */
34
- declare const fetcherNoRetries: wretch_types.Wretch<unknown, unknown, undefined, undefined>;
35
-
36
- export { fetcher, fetcherNoRetries };
package/fetcher.d.ts DELETED
@@ -1,32 +0,0 @@
1
- /**
2
- * @packageDocumentation
3
- *
4
- * An alternate syntax to fetch() that allows function chaining to compose the
5
- * entire fetch request. One can also compose partial fetcher objects to be
6
- * reused in future requests.
7
- *
8
- * Examples:
9
- * ```
10
- * const userFetcher = fetcher
11
- * .auth("Bearer TOPSECRET")
12
- * .content("application/json")
13
- * .url("/api/user");
14
- *
15
- * const user = await userFetcher.get();
16
- *
17
- * const updateResult = await userFetcher
18
- * .body({ id: user.id, name: "New name"})
19
- * .post();
20
- * ```
21
- */
22
- /**
23
- * Fetcher with default short retries. Use this to make your queries more
24
- * robust without any additional work.
25
- */
26
- export declare const fetcher: import("wretch/types").Wretch<unknown, unknown, undefined, undefined>;
27
- /**
28
- * Name this as `fetcherNoRetries` to make it easier to understand than
29
- * `wretch`. Use this if you don't want retries. Use `fetcher` if you want
30
- * retries.
31
- */
32
- export declare const fetcherNoRetries: import("wretch/types").Wretch<unknown, unknown, undefined, undefined>;
package/fetcher.min.cjs DELETED
@@ -1,2 +0,0 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true});var _chunkO3RBYG3Wmincjs = require('./chunk-O3RBYG3W.min.cjs');require('./chunk-KE3RXVMS.min.cjs');exports.fetcher = _chunkO3RBYG3Wmincjs.a; exports.fetcherNoRetries = _chunkO3RBYG3Wmincjs.b;
2
- //# sourceMappingURL=fetcher.min.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/Users/choksheaklau/src/react-utils/dist/fetcher.min.cjs"],"names":[],"mappings":"AAAA,wIAAyC,oCAAiC,4FAA2C","file":"/Users/choksheaklau/src/react-utils/dist/fetcher.min.cjs"}
package/fetcher.min.mjs DELETED
@@ -1,2 +0,0 @@
1
- import{a,b}from"./chunk-6ZZ54XM5.min.mjs";import"./chunk-MWDH7YON.min.mjs";export{a as fetcher,b as fetcherNoRetries};
2
- //# sourceMappingURL=fetcher.min.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/fetcher.mjs DELETED
@@ -1,9 +0,0 @@
1
- import {
2
- fetcher,
3
- fetcherNoRetries
4
- } from "./chunk-3V7WBGWQ.mjs";
5
- import "./chunk-X6BYQHVC.mjs";
6
- export {
7
- fetcher,
8
- fetcherNoRetries
9
- };