@choksheak/react-utils 0.3.7 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/sharedQuery.cjs +33 -7
- package/sharedQuery.min.cjs +1 -1
- package/sharedQuery.min.cjs.map +1 -1
- package/sharedQuery.min.mjs +1 -1
- package/sharedQuery.min.mjs.map +1 -1
- package/sharedQuery.mjs +33 -7
- package/sharedState.cjs +3 -2
- package/sharedState.min.cjs +1 -1
- package/sharedState.min.cjs.map +1 -1
- package/sharedState.min.mjs +1 -1
- package/sharedState.min.mjs.map +1 -1
- package/sharedState.mjs +3 -2
- package/storage.cjs +3 -2
- package/storage.min.cjs +1 -1
- package/storage.min.cjs.map +1 -1
- package/storage.min.mjs +1 -1
- package/storage.min.mjs.map +1 -1
- package/storage.mjs +3 -2
package/package.json
CHANGED
package/sharedQuery.cjs
CHANGED
|
@@ -116,9 +116,10 @@ function kvStoreAdapter({
|
|
|
116
116
|
if (IS_NOT_BROWSER) return void 0;
|
|
117
117
|
const fullKey = keyPrefix + key2;
|
|
118
118
|
const value = await import_kvStore.kvStore.get(fullKey);
|
|
119
|
-
if (isValid && !isValid(value)) {
|
|
119
|
+
if (value !== void 0 && isValid && !isValid(value)) {
|
|
120
120
|
console.warn(
|
|
121
|
-
`indexedDb adapter: Auto-discard invalid value for ${key2}
|
|
121
|
+
`indexedDb adapter: Auto-discard invalid value for ${key2}:`,
|
|
122
|
+
value
|
|
122
123
|
);
|
|
123
124
|
await import_kvStore.kvStore.delete(fullKey);
|
|
124
125
|
return void 0;
|
|
@@ -378,13 +379,38 @@ function sharedQuery(options) {
|
|
|
378
379
|
const staleMs = options?.staleMs ?? SharedQueryConfig.staleMs;
|
|
379
380
|
const expiryMs = options?.expiryMs ?? SharedQueryConfig.expiryMs;
|
|
380
381
|
const refetchOnStale = options?.refetchOnStale ?? SharedQueryConfig.refetchOnStale;
|
|
381
|
-
|
|
382
|
+
const store = options.store;
|
|
383
|
+
if (store) {
|
|
384
|
+
if ("persistTo" in store && store.isValid) {
|
|
385
|
+
const isValid = store.isValid;
|
|
386
|
+
options = {
|
|
387
|
+
...options,
|
|
388
|
+
store: {
|
|
389
|
+
...store,
|
|
390
|
+
isValid: (u) => {
|
|
391
|
+
if (!u || typeof u !== "object") {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
for (const v of Object.values(u)) {
|
|
395
|
+
if (!v || typeof v !== "object" || !("data" in v) || !("dataUpdatedMs" in v) || !("lastUpdatedMs" in v) || typeof v.dataUpdatedMs !== "number" || typeof v.lastUpdatedMs !== "number" || !isValid(v.data)) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
382
405
|
const persistTo = options.persistTo ?? SharedQueryConfig.persistTo;
|
|
383
406
|
if (persistTo) {
|
|
384
|
-
options
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
407
|
+
options = {
|
|
408
|
+
...options,
|
|
409
|
+
store: {
|
|
410
|
+
persistTo,
|
|
411
|
+
key: queryName,
|
|
412
|
+
expiryMs
|
|
413
|
+
}
|
|
388
414
|
};
|
|
389
415
|
}
|
|
390
416
|
}
|
package/sharedQuery.min.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var ae=Object.create;var P=Object.defineProperty;var oe=Object.getOwnPropertyDescriptor;var se=Object.getOwnPropertyNames;var ie=Object.getPrototypeOf,ue=Object.prototype.hasOwnProperty;var de=(e,o)=>{for(var n in o)P(e,n,{get:o[n],enumerable:!0})},K=(e,o,n,r)=>{if(o&&typeof o=="object"||typeof o=="function")for(let a of se(o))!ue.call(e,a)&&a!==n&&P(e,a,{get:()=>o[a],enumerable:!(r=oe(o,a))||r.enumerable});return e};var j=(e,o,n)=>(n=e!=null?ae(ie(e)):{},K(o||!e||!e.__esModule?P(n,"default",{value:e,enumerable:!0}):n,e)),ce=e=>K(P({},"__esModule",{value:!0}),e);var me={};de(me,{SharedQueryConfig:()=>M,configureSharedQuery:()=>pe,sharedQuery:()=>ye,useSharedQuery:()=>Te});module.exports=ce(me);var re=require("@choksheak/ts-utils/timeConstants"),U=j(require("object-sizeof")),b=require("react");var J=require("@choksheak/ts-utils/timeConstants"),W=require("react");var O=require("@choksheak/ts-utils/kvStore"),_=require("@choksheak/ts-utils/localStore"),x=typeof window>"u";function Y(e,o){let{store:n}=e??{};if(!n)return null;if("persistTo"in n&&typeof n.persistTo=="string"&&"key"in n&&typeof n.key=="string"){let r=n.expiryMs||o,a={...n,expiryMs:r};switch(n.persistTo){case"localStorage":return le(a);case"indexedDb":return fe(a);default:throw n.persistTo,new Error(`Unknown store.adapter ${n.persistTo}`)}}return n}function le({key:e,expiryMs:o,isValid:n}){let r=(0,_.createLocalStore)(e,{defaultExpiryMs:o});return{set:(a,s)=>{x||r.set(a,s)},get:a=>{if(x)return;let s=r.get(a);if(n&&!n(s)){console.warn(`localStorage adapter: Auto-discard invalid value for ${a}: ${JSON.stringify(s)}`),r.delete(a);return}return s},delete:a=>{x||r.delete(a)},clear:()=>{x||r.clear()}}}function fe({key:e,expiryMs:o,isValid:n}){let r=e+":";return{set:async(a,s)=>{x||await O.kvStore.set(r+a,s,o)},get:async a=>{if(x)return;let s=r+a,u=await O.kvStore.get(s);if(u!==void 0&&n&&!n(u)){console.warn(`indexedDb adapter: Auto-discard invalid value for ${a}:`,u),await O.kvStore.delete(s);return}return u},delete:async a=>{x||await O.kvStore.delete(r+a)},clear:async()=>{x||await O.kvStore.clear()}}}var I={storeExpiryMs:30*J.MS_PER_DAY,lazyLoad:!1};function ge(){let e=new Map,o=new Map;function n(r,a,s){let u=o.get(r);if(!u)return;let T=Array.from(u.values());for(let h of T)try{h(a,s)}catch(S){console.error("Error invoking subscriber:",S)}}return{get(r){return e.get(r)},set(r,a){let s=e.get(r);s!==a&&(e.set(r,a),n(r,a,s))},delete(r){e.delete(r)},setNoNotify(r,a){e.set(r,a)},subscribe(r,a){let s=o.get(r);return s||(s=new Set,o.set(r,s)),s.add(a),()=>{s.delete(a),s.size===0&&o.delete(r)}}}}var Q=ge(),z="state",Se=0;function G(e,o){let n=()=>{},r=new Promise(f=>{n=f}),a=n,s=String(Se++),u=Y(o,I.storeExpiryMs);Q.setNoNotify(s,e);let T=o?.lazyLoad,h=T!==void 0?T:I.lazyLoad,S={readyPromise:r,lazy:h,initStarted:!1,initDone:!1,subscribe(f){return Q.subscribe(s,()=>f())},hasStore(){return!!u},async setInStore(f){return await u?.set(z,f)},async getFromStore(){return await u?.get(z)},getSnapshot(){return Q.get(s)},setValue(f){if(typeof f=="function"){let p=Q.get(s);f=f(p)}Q.set(s,f),S.setInStore(f)},delete(){Q.set(s,e),u?.set(z,e)},async initDefaultValueOnce(){if(S.initDone||S.initStarted)return;S.initStarted=!0;let f=await S.getFromStore(),p=f!==void 0?f:e;Q.set(s,p),S.initDone=!0,a()}};return h||S.initDefaultValueOnce(),S}function H(e){return e.initDefaultValueOnce(),[(0,W.useSyncExternalStore)(n=>e.subscribe(n),()=>e.getSnapshot(),()=>e.getSnapshot()),e.setValue]}function X(e){let o=new Set;return JSON.stringify(e,(n,r)=>{if(typeof r=="function")throw new Error("Functions should not be passed into query parameters");if(!r||typeof r!="object"||Array.isArray(r))return r;if(o.has(r))return"[Circular]";if(o.add(r),r!==e)try{JSON.stringify(r)}catch{return"[Unserializable]"}return Object.keys(r).sort().reduce((a,s)=>(a[s]=r[s],a),{})})}var Z=j(require("lodash/isEqual")),E=require("react");function q(e){let[o,n]=(0,E.useState)(e);return(0,E.useMemo)(()=>(0,Z.default)(o,e)?o:(n(e),e),[o,e])}var M={staleMs:0,expiryMs:30*re.MS_PER_DAY,refetchOnStale:!0,persistTo:void 0,maxSize:100,maxBytes:1e5,log:(...e)=>console.log("[sharedQuery]",...e)};function pe(e){Object.assign(M,e)}var ee=new Set;function ye(e){if(ee.has(e.queryName))throw new Error(`Duplicate shared query name "${e.queryName}"`);ee.add(e.queryName);let o=new Map,n=new Map,r=e.queryName,a=e.queryFn,s=e.refetchFn,u=e?.staleMs??M.staleMs,T=e?.expiryMs??M.expiryMs,h=e?.refetchOnStale??M.refetchOnStale,S=e.store;if(S){if("persistTo"in S&&S.isValid){let t=S.isValid;e={...e,store:{...S,isValid:i=>{if(!i||typeof i!="object")return!1;for(let l of Object.values(i))if(!l||typeof l!="object"||!("data"in l)||!("dataUpdatedMs"in l)||!("lastUpdatedMs"in l)||typeof l.dataUpdatedMs!="number"||typeof l.lastUpdatedMs!="number"||!t(l.data))return!1;return!0}}}}}else{let t=e.persistTo??M.persistTo;t&&(e={...e,store:{persistTo:t,key:r,expiryMs:T}})}let f=e?.maxSize??M.maxSize,p=e?.maxBytes??M.maxBytes,D=!!e?.keepLastNonEmptyData,d=e?.log??M.log,g=G({},e);function v(t){return u===0?!0:Date.now()-t>u}function N(t){return T===0?!1:Date.now()-t>T}function R(){if(!f&&!p)return;let t=g.getSnapshot(),i=Object.keys(t).length;if(i<=1)return;let l=(0,U.default)(t),c=f?i-f:0,y=p?l-p:0;if(c<=0&&y<=0){d(`No need to trim data for ${r}: size=${i} (limit=${f}), byteSize=${l.toLocaleString()} (limit=${p.toLocaleString()})`);return}d(`Need to trim ${r}: numToCut=${c}, bytesToCut=${y}`);let w={...t},$=!1,B=(m,C)=>{if(n.has(m)){d(`Cannot clean up ${m} as it is mounted`);return}let F=(0,U.default)(m)+(0,U.default)(w[m]);d(`Cleaning up unmounted${C?", expired":""} ${m} (byteSize=${F.toLocaleString()})`),c--,y-=F,delete w[m],$=!0},V=Object.entries(w).sort((m,C)=>m[1].lastUpdatedMs-C[1].lastUpdatedMs);for(let[m]of V){if(!N(w[m].lastUpdatedMs))break;B(m,!0)}if(c>0||y>0){$&&(V=Object.entries(w).sort((m,C)=>m[1].lastUpdatedMs-C[1].lastUpdatedMs));for(let[m]of V)if(B(m,!1),c<=0&&y<=0)break}if(!$){d(`No trimmable entries found for ${r}: size=${i} (limit=${f}), byteSize=${l.toLocaleString()} (limit=${p.toLocaleString()})`);return}g.setValue(w),d(`Trimmed data for ${r}: size=[${i} -> ${Object.keys(w).length}] (limit=${f}), byteSize=[${l.toLocaleString()} -> ${(0,U.default)(w).toLocaleString()}] (limit=${p.toLocaleString()})`)}function L(t,i,l=Date.now()){let c={lastUpdatedMs:Date.now(),loading:!1,data:i,dataUpdatedMs:l};g.setValue(y=>({...y,[t]:c})),R()}async function ne(t,i,l){d(`Start fetching ${t}, queryType=${i}`);try{let c=await(i==="refetch"&&s?s(...l):a(...l));return d(`Successfully fetched ${t}`),L(t,c,Date.now()),c}catch(c){throw d(`Failed to fetch ${t} for queryType=${i}: ${c}`),c}finally{o.delete(t)}}function k(t,i,...l){let c=o.get(t);if(c)return d(`Deduplicating inflight fetch for ${t}, queryType=${i}`),c.promise;let y=ne(t,i,l),w=new AbortController;return o.set(t,{promise:y,abortController:w}),y}let A={queryName:r,queryFn:a,refetchFn:s,expiryMs:T,staleMs:u,refetchOnStale:h,maxSize:f,maxBytes:p,keepLastNonEmptyData:D,log:d,queryState:g,getQueryKey(t){return r+":"+X(t)},getAbortController(t){let i=A.getQueryKey(t);return A.getAbortControllerByKey(i)},getAbortControllerByKey(t){return o.get(t)?.abortController??null},getAbortSignal(t){let i=A.getQueryKey(t);return A.getAbortSignalByKey(i)},getAbortSignalByKey(t){return A.getAbortControllerByKey(t)?.signal??null},async getCachedOrFetch(...t){let i=Date.now();await g.readyPromise;let l=Date.now()-i,c=A.getQueryKey(t),y=g.getSnapshot()?.[c];if(l&&d(`${c}: Waited for ${l}ms before data is ready`),y?.data&&!N(y.dataUpdatedMs??0))return v(y.dataUpdatedMs??0)?(h&&(d(`refetchOnStale for ${c}`),k(c,"query",...t)),d(`Returning stale data for ${c}`),y):(d(`Return fresh data ${c} from cache without fetching`),y);let w=await k(c,"query",...t),$=Date.now();return{lastUpdatedMs:$,loading:!1,data:w,dataUpdatedMs:$}},getQueryValue(t){return g.getSnapshot()?.[t]},getData(t){let i=g.getSnapshot()?.[t];if(i?.lastUpdatedMs&&N(i.lastUpdatedMs)){A.deleteData(t);return}return i?.data},setData:L,async updateFromSource(...t){let i=A.getQueryKey(t);return await k(i,"query",...t)},async refetch(...t){let i=A.getQueryKey(t);return await k(i,"refetch",...t)},deleteData(t){let l={...g.getSnapshot()};delete l[t],g.setValue(l);let c=o.get(t);c&&(o.delete(t),c.abortController.abort("Deleted"))},clear(){g.delete();for(let{abortController:t}of Array.from(o.values()))t.abort("Cleared");o.clear()},mount(t){n.set(t,(n.get(t)??0)+1)},unmount(t){let i=n.get(t);i===1?n.delete(t):n.set(t,(i??0)-1)},enforceSizeLimit:R};return A}var te={lastUpdatedMs:0,loading:!0};function Te(e,...o){let n=q(o),[r,a]=H(e.queryState),s=(0,b.useRef)(!0),u=(0,b.useMemo)(()=>e.getQueryKey(n),[e,n]),T=r[u]??te,h=(0,b.useCallback)(D=>{a(d=>{let g={...d};return g[u]=typeof D=="function"?D(g[u]??te):D,g})},[u,a]),[S,f]=(0,b.useState)();(0,b.useEffect)(()=>{e.keepLastNonEmptyData&&T.data!==void 0&&f(T.data)},[e.keepLastNonEmptyData,T.data]);let p=(0,b.useCallback)(async D=>{if(e.log(`Begin executing shared query ${u}, isRefetch=${D}`),!!s.current){h(d=>({...d,lastUpdatedMs:Date.now(),loading:!0}));try{let d;if(D){let g=await e.refetch(...n),v=Date.now();d={lastUpdatedMs:v,loading:!1,data:g,dataUpdatedMs:v}}else d=await e.getCachedOrFetch(...n);if(!s.current)return;h({...d,loading:!1})}catch(d){if(!s.current)return;let g=Date.now();h(v=>({...v,lastUpdatedMs:g,loading:!1,error:d,errorUpdatedMs:g}))}}},[e,u,h,n]);return(0,b.useEffect)(()=>(s.current=!0,e.mount(u),p(!1),()=>{s.current=!1,e.unmount(u)}),[p,e,u]),(0,b.useMemo)(()=>({...T,lastNonEmptyData:S,abortCurrentQuery:D=>{let d=e.getAbortControllerByKey(u);return d?.abort(D),!!d},refetch:()=>p(!0),setData:(D,d)=>{e.setData(u,D,d)},deleteData:()=>{e.deleteData(u)}}),[p,S,e,u,T])}0&&(module.exports={SharedQueryConfig,configureSharedQuery,sharedQuery,useSharedQuery});
|
|
2
2
|
//# sourceMappingURL=sharedQuery.min.cjs.map
|
package/sharedQuery.min.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/sharedQuery.ts","../src/sharedState.ts","../src/storage.ts","../src/stringify.ts","../src/useDeepMemo.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 { MS_PER_DAY } from \"@choksheak/ts-utils/timeConstants\";\nimport getByteSize from \"object-sizeof\";\nimport {\n SetStateAction,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\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 * Return the last non-empty data if you want to display the last loaded data\n * while waiting for the new data to load.\n */\n lastNonEmptyData: 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\n/** Options to configure a shared query. */\nexport type SharedQueryOptions<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 /**\n * Function to fetch data.\n */\n queryFn: QueryFn<TArgs, 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 * 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 (seenQueryNames.has(options.queryName)) {\n throw new Error(`Duplicate shared query name \"${options.queryName}\"`);\n }\n seenQueryNames.add(options.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 const queryName = options.queryName;\n const queryFn = options.queryFn;\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 if (!options.store) {\n const persistTo = options.persistTo ?? SharedQueryConfig.persistTo;\n\n if (persistTo) {\n options.store = {\n persistTo,\n key: queryName,\n expiryMs,\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 return queryName + \":\" + stringifyDeterministicForKeys(args);\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:\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 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 lastNonEmptyData,\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","/**\n * @packageDocumentation\n *\n * The \"shared state\" is a React state that shares the same value across the\n * entire app. Users declare a shared state constant that can be reused across\n * multiple components with the same underlying value.\n *\n * Shared states provide persistence to local storage and/or indexed DB out of\n * the box, by using \"@choksheak/ts-utils\". Note that no matter if the shared\n * state uses persistence or not, it will always maintain a copy of the state\n * in memory. Persisted values can be configured with an automatic expiration\n * duration.\n *\n * This module is largely similar to the open source \"jotai\" npm library. You\n * can use shared states to replace \"jotai\" in your code, and optionally make\n * use of the persistence mechanism. A quick migration guide from jotai is:\n *\n * ```\n * atom() --> sharedState()\n * useAtom() --> useSharedState()\n * useAtomValue() --> useSharedStateValue()\n * useSetAtom() --> useSharedStateSetter()\n * ```\n *\n * Example:\n * ```\n * import { sharedState, useSharedState } from \"@choksheak/react-utils/sharedState\";\n *\n * // Create a new shared state in the top level scope.\n * // The first argument is the default value, which is required.\n * // The second argument is an options object, and is optional (defaults\n * // to storing in memory only without any persistence).\n * const usersState = sharedState(\n * [], { store: { persistTo: \"indexedDb\", key: \"users\" }}\n * );\n *\n * export const UsersComponent: React.FC = () => {\n * // Get both the value and the setter.\n * const [users, setUsers] = useSharedState(usersState);\n * ...\n * };\n *\n * export const ReadOnlyComponent: React.FC = () => {\n * // Get the value only.\n * const users = useSharedStateValue(usersState);\n * ...\n * };\n *\n * export const WriteOnlyComponent: React.FC = () => {\n * // Get the setter only.\n * const setUsers = useSharedStateSetter(usersState);\n * ...\n * };\n * ```\n */\n\nimport { MS_PER_DAY } from \"@choksheak/ts-utils/timeConstants\";\nimport { Dispatch, SetStateAction, useSyncExternalStore } from \"react\";\n\nimport { getStorageAdapter, StorageOptions } from \"./storage\";\n\n/************************************************************************/\n/* Global config */\n/************************************************************************/\n\n/**\n * These values are configurable by the user at any time.\n */\nexport const SharedStateConfig = {\n /**\n * Default duration for storage expiration in milliseconds. Initial value\n * is 30 days, but can be changed to anything.\n */\n storeExpiryMs: 30 * MS_PER_DAY,\n\n /**\n * True to load from storage only on mount, false (default) to load in\n * the top level scope when the shared state is defined.\n */\n lazyLoad: false,\n};\n\nexport type SharedStateConfig = typeof SharedStateConfig;\n\n/** Convenience function to update global defaults. */\nexport function configureSharedState(config: Partial<SharedStateConfig>) {\n Object.assign(SharedStateConfig, config);\n}\n\n/************************************************************************/\n/* Pub-sub store\n/************************************************************************/\n\ntype Subscriber<T> = (next: T, prev: T) => void;\n\nfunction createPubSubStore() {\n const dataByKey = new Map<string, unknown>();\n const subscribersByKey = new Map<string, Set<Subscriber<unknown>>>();\n\n function notify<T>(key: string, next: T, prev: T): void {\n const subscribers = subscribersByKey.get(key);\n if (!subscribers) return;\n\n // Make copy to protect against mutation during iteration.\n const copy = Array.from(subscribers.values());\n for (const subscriber of copy) {\n try {\n subscriber(next, prev);\n } catch (e) {\n console.error(`Error invoking subscriber:`, e);\n }\n }\n }\n\n return {\n // This could be undefined if T includes undefined, else it should never\n // return undefined because the user has to always specify the default value.\n get<T>(key: string): T {\n return dataByKey.get(key) as T;\n },\n\n set<T>(key: string, value: T): void {\n const prev = dataByKey.get(key) as T;\n\n // Do nothing if no change.\n if (prev === value) return;\n\n dataByKey.set(key, value);\n notify(key, value, prev);\n },\n\n delete(key: string): void {\n dataByKey.delete(key);\n },\n\n setNoNotify<T>(key: string, value: T): void {\n dataByKey.set(key, value);\n },\n\n // Subscribe to changes of a specific key\n subscribe<T>(key: string, subscriber: Subscriber<T>): () => void {\n let subscribers = subscribersByKey.get(key);\n if (!subscribers) {\n subscribers = new Set();\n subscribersByKey.set(key, subscribers);\n }\n\n // Type coercion: we store Subscriber<unknown>\n subscribers.add(subscriber as Subscriber<unknown>);\n\n // Return unsubscribe function\n return () => {\n subscribers.delete(subscriber as Subscriber<unknown>);\n if (subscribers.size === 0) subscribersByKey.delete(key);\n };\n },\n } as const;\n}\n\nconst pubSubStore = createPubSubStore();\n\n/************************************************************************/\n/* Shared state implementation */\n/************************************************************************/\n\nconst STORAGE_KEY = \"state\";\n\n/** Options to configure a shared state. */\nexport type SharedStateOptions<T> = {\n lazyLoad?: boolean;\n} & StorageOptions<T>;\n\nlet stateKey = 0;\n\n/**\n * Create a new shared state object. Put this code at the top level scope:\n *\n * Example:\n * ```\n * const counterState = sharedState<number>(0);\n * ```\n */\nexport function sharedState<T>(\n defaultValue: T,\n options?: SharedStateOptions<T>,\n) {\n let readyResolve: () => void = () => {};\n\n const readyPromise = new Promise<void>((resolve) => {\n readyResolve = resolve;\n });\n\n const resolveReadyPromise = readyResolve;\n\n const pubSubKey = String(stateKey++);\n\n // Use max of 1 storage adapter per shared state.\n const storageAdapter = getStorageAdapter(\n options,\n SharedStateConfig.storeExpiryMs,\n );\n\n // Always set the default value first to avoid returning undefineds if that\n // is not part of T.\n pubSubStore.setNoNotify(pubSubKey, defaultValue);\n\n const lazyLoad = options?.lazyLoad;\n const lazy = lazyLoad !== undefined ? lazyLoad : SharedStateConfig.lazyLoad;\n\n const obj = {\n /** Allow users to await for the completion of data init. */\n readyPromise,\n\n lazy,\n\n initStarted: false,\n initDone: false,\n\n /** `subscribe` function required by useSyncExternalStore. */\n subscribe(subscriber: () => void): () => void {\n return pubSubStore.subscribe(pubSubKey, () => subscriber());\n },\n\n /** Returns true if there is a backing store. */\n hasStore(): boolean {\n return Boolean(storageAdapter);\n },\n\n /**\n * Set a value directly into the store. Does nothing if there is no\n * backing store.\n *\n * Warning: If you want to set the value properly, you should use\n * `setValue()` instead which also updates the in-memory copy.\n */\n async setInStore(value: T): Promise<void> {\n return await storageAdapter?.set(STORAGE_KEY, value);\n },\n\n /**\n * Get a value directly from the store. Returns undefined if there is no\n * backing store.\n */\n async getFromStore(): Promise<T | undefined> {\n return await storageAdapter?.get(STORAGE_KEY);\n },\n\n /**\n * Get the value of the shared state.\n * `getSnapshot` function — must return same ref if value unchanged.\n * Note that this would return the default value if data init from store is\n * not completed yet.\n */\n getSnapshot(): T {\n return pubSubStore.get(pubSubKey);\n },\n\n /** Set the value of the shared state. */\n setValue(next: SetStateAction<T>): void {\n if (typeof next === \"function\") {\n const prev = pubSubStore.get<T>(pubSubKey);\n next = (next as (p: T) => T)(prev);\n }\n\n pubSubStore.set(pubSubKey, next);\n void obj.setInStore(next);\n },\n\n /**\n * Remove the value of the shared state. Actually this just sets the value\n * back to the given default value.\n */\n delete() {\n pubSubStore.set(pubSubKey, defaultValue);\n void storageAdapter?.set(STORAGE_KEY, defaultValue);\n },\n\n /** Initial default handling. */\n async initDefaultValueOnce() {\n if (obj.initDone || obj.initStarted) return;\n // @ts-expect-error ts(2540) - Assign to readonly property.\n obj.initStarted = true;\n\n const stored = await obj.getFromStore();\n const value = stored !== undefined ? stored : defaultValue;\n\n pubSubStore.set(pubSubKey, value);\n\n // @ts-expect-error ts(2540) - Assign to readonly property.\n obj.initDone = true;\n resolveReadyPromise();\n },\n } as const;\n\n if (!lazy) {\n void obj.initDefaultValueOnce();\n }\n\n return obj;\n}\n\nexport type SharedState<T> = ReturnType<typeof sharedState<T>>;\n\n/**\n * React hook for subscribing to a key in the global store.\n *\n * Example:\n * ```\n * const [counter, setCounter] = useSharedState(counterState);\n * ```\n */\nexport function useSharedState<T>(\n state: SharedState<T>,\n): [T, Dispatch<SetStateAction<T>>] {\n void state.initDefaultValueOnce();\n\n const value = useSyncExternalStore(\n (onStoreChange) => state.subscribe(onStoreChange),\n () => state.getSnapshot(),\n // getServerSnapshot is needed to prevent SSR dev errors in nextjs.\n () => state.getSnapshot(),\n );\n\n return [value, state.setValue];\n}\n\n/**\n * Same as useSharedState(), but returns the value only.\n *\n * Example:\n * ```\n * const counter = useSharedStateValue(counterState);\n * ```\n */\nexport function useSharedStateValue<T>(state: SharedState<T>) {\n return useSharedState(state)[0];\n}\n\n/**\n * Same as useSharedState(), but returns the setter only.\n *\n * Example:\n * ```\n * const setCounter = useSharedStateSetter(counterState);\n * ```\n */\nexport function useSharedStateSetter<T>(state: SharedState<T>) {\n void state.initDefaultValueOnce();\n\n return state.setValue;\n}\n","/**\n * @packageDocumentation\n *\n * Backing storage implementation for the shared state. Note that this uses the\n * LocalStore and KvStore from package \"@choksheak/ts-utils\", but is customized\n * to be used for shared states.\n *\n * Normally you will not need to use this module directly, but if you have a\n * need for it, feel free to use it.\n */\n\nimport { Duration } from \"@choksheak/ts-utils/duration\";\nimport { kvStore } from \"@choksheak/ts-utils/kvStore\";\nimport { createLocalStore } from \"@choksheak/ts-utils/localStore\";\nimport { StorageAdapter } from \"@choksheak/ts-utils/storageAdapter\";\n\n/** List of all available pre-built persistence options. */\nexport type PersistTo = \"localStorage\" | \"indexedDb\";\n\n/** Options to configure a pre-built store adapter. */\nexport type StoreOptions = {\n /** Choose which pre-built store adapter to use. */\n persistTo: PersistTo;\n\n /** Storage key to act as a namespace to store the data. */\n key: string;\n\n /** How long to wait before the value is considered expired. */\n expiryMs?: number | Duration;\n\n /** Returns true if the given value supposed to be of type T is valid. */\n isValid?: (u: unknown) => boolean;\n};\n\nexport type StoreConstructorArgs = Omit<StoreOptions, \"persistTo\">;\n\n/** Either use a pre-built store, or a custom one you provide. */\nexport type StorageOptions<T> = {\n /**\n * You can either specify the storage adapter directly to use a custom\n * store, or specify the properties to use the pre-built ones.\n */\n store?: StorageAdapter<T> | StoreOptions;\n};\n\n// In SSR (Server-Side Rendering) mode, we never want to use our storage\n// adapters for anything.\nconst IS_NOT_BROWSER = typeof window === \"undefined\";\n\n/**\n * Convert a StorageOptions<T> config object into an actual storage adapter, or\n * returns null if a storage adapter is not configured.\n */\nexport function getStorageAdapter<T>(\n options: StorageOptions<T> | undefined,\n defaultStoreExpiryMs: number,\n): StorageAdapter<T> | null {\n const { store } = options ?? {};\n\n if (!store) {\n return null;\n }\n\n if (\n \"persistTo\" in store &&\n typeof store.persistTo === \"string\" &&\n \"key\" in store &&\n typeof store.key === \"string\"\n ) {\n const expiryMs = store.expiryMs || defaultStoreExpiryMs;\n const config = { ...store, expiryMs };\n\n switch (store.persistTo) {\n case \"localStorage\":\n return localStoreAdapter<T>(config);\n\n case \"indexedDb\":\n return kvStoreAdapter<T>(config);\n\n default:\n store.persistTo satisfies never;\n throw new Error(`Unknown store.adapter ${store.persistTo}`);\n }\n }\n\n return store as StorageAdapter<T>;\n}\n\n/**\n * Create a new SSR-safe local store adapter, which allows users to reuse the\n * same adapter in multiple shared states or shared queries.\n */\nexport function localStoreAdapter<T>({\n key,\n expiryMs,\n isValid,\n}: StoreConstructorArgs) {\n const adapter = createLocalStore(key, { defaultExpiryMs: expiryMs });\n\n return {\n set: (key: string, value: T): void => {\n if (IS_NOT_BROWSER) return; // handle SSR\n\n // Also apply the custom expiryMs on every item in this store.\n adapter.set(key, value);\n },\n\n get: (key: string): T | undefined => {\n if (IS_NOT_BROWSER) return undefined; // handle SSR\n\n const value = adapter.get<T>(key);\n\n if (isValid && !isValid(value)) {\n console.warn(\n `localStorage adapter: Auto-discard invalid value for ${key}: ${JSON.stringify(value)}`,\n );\n adapter.delete(key);\n return undefined;\n }\n\n return value;\n },\n\n delete: (key: string): void => {\n if (IS_NOT_BROWSER) return; // handle SSR\n\n adapter.delete(key);\n },\n\n clear: (): void => {\n if (IS_NOT_BROWSER) return; // handle SSR\n\n adapter.clear();\n },\n };\n}\n\n/**\n * Create a new SSR-safe indexed DB KV store adapter, which allows users to\n * reuse the same adapter in multiple shared states or shared queries.\n */\nexport function kvStoreAdapter<T>({\n key,\n expiryMs,\n isValid,\n}: StoreConstructorArgs) {\n // Use the key prefix as a unique namespace for this store.\n const keyPrefix = key + \":\";\n\n // Prefix all keys using `keyPrefix` so that we can keep all data in\n // the same store in the same DB. If we keep the data in different\n // DB stores, then GC will not be able to clean up abandoned stores.\n return {\n set: async (key: string, value: T): Promise<void> => {\n if (IS_NOT_BROWSER) return; // handle SSR\n\n // Also apply the custom expiryMs on every item in this store.\n await kvStore.set(keyPrefix + key, value, expiryMs);\n },\n get: async (key: string): Promise<T | undefined> => {\n if (IS_NOT_BROWSER) return undefined; // handle SSR\n\n const fullKey = keyPrefix + key;\n const value = await kvStore.get<T>(fullKey);\n\n if (isValid && !isValid(value)) {\n console.warn(\n `indexedDb adapter: Auto-discard invalid value for ${key}: ${JSON.stringify(value)}`,\n );\n await kvStore.delete(fullKey);\n return undefined;\n }\n\n return value;\n },\n delete: async (key: string): Promise<void> => {\n if (IS_NOT_BROWSER) return; // handle SSR\n\n await kvStore.delete(keyPrefix + key);\n },\n clear: async (): Promise<void> => {\n if (IS_NOT_BROWSER) return; // handle SSR\n\n await kvStore.clear();\n },\n };\n}\n","/**\n * @packageDocumentation\n *\n * Implements special stringify for use in the shared queries. This will likely\n * not be reusable for your own use cases.\n *\n * Normally you will not need to use this module directly, but if you have a\n * need for it, feel free to use it.\n */\n\n/**\n * JSON stringify a value of any type, and returns an unsafe string. The string\n * is \"unsafe\" because you might not be able to restore the original object\n * using the serialized JSON. This is specifically used for query key generation\n * and is not suitable for more general uses.\n *\n * Note: The given value should not contain any functions because functions\n * cannot be serialized and deserialized properly.\n */\nexport function stringifyDeterministicForKeys(params: unknown): string {\n const cache = new Set();\n\n return JSON.stringify(params, (_, value) => {\n if (typeof value === \"function\") {\n throw new Error(`Functions should not be passed into query parameters`);\n }\n\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return value;\n }\n\n if (cache.has(value)) {\n // Circular reference detected! Return a placeholder string.\n return \"[Circular]\";\n }\n\n cache.add(value);\n\n // Don't crash on circular references, but don't mark the top level object\n // as circular because we want to return as much string as possible.\n if (value !== params) {\n try {\n JSON.stringify(value);\n } catch {\n return \"[Unserializable]\";\n }\n }\n\n return Object.keys(value)\n .sort()\n .reduce(\n (result: Record<string, unknown>, key: string) => {\n result[key] = value[key];\n return result;\n },\n {} as Record<string, unknown>,\n );\n });\n}\n","/**\n * @packageDocumentation\n *\n * Provides a memoized value in React where the value is checked for deep\n * equality. If the value has changed, but is deeply equal to the previous\n * value, then useDeepMemo will return the same reference to the previous\n * value and ignore the latest value.\n *\n * This is mainly used in shared queries to check for changes in the query\n * arguments, but it might be useful for your own use cases.\n */\n\nimport isEqual from \"lodash/isEqual\";\nimport { useMemo, useState } from \"react\";\n\n/**\n * Provides a stable value for T based on deep equality. The deep equality is\n * determined by the `isEqual()` function in lodash.\n */\nexport function useDeepMemo<T>(value: T): T {\n const [state, setState] = useState<T>(value);\n\n return useMemo(() => {\n if (isEqual(state, value)) {\n return state;\n }\n\n // We check equality, so this should be safe.\n // eslint-disable-next-line react-hooks/set-state-in-render\n setState(value);\n\n return value;\n }, [state, value]);\n}\n"],"mappings":"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","names":["sharedQuery_exports","__export","SharedQueryConfig","configureSharedQuery","sharedQuery","useSharedQuery","__toCommonJS","import_timeConstants","import_object_sizeof","import_react","import_timeConstants","import_react","import_kvStore","import_localStore","IS_NOT_BROWSER","getStorageAdapter","options","defaultStoreExpiryMs","store","expiryMs","config","localStoreAdapter","kvStoreAdapter","key","isValid","adapter","value","keyPrefix","fullKey","SharedStateConfig","createPubSubStore","dataByKey","subscribersByKey","notify","key","next","prev","subscribers","copy","subscriber","e","value","pubSubStore","STORAGE_KEY","stateKey","sharedState","defaultValue","options","readyResolve","readyPromise","resolve","resolveReadyPromise","pubSubKey","storageAdapter","getStorageAdapter","SharedStateConfig","lazyLoad","lazy","obj","stored","useSharedState","state","onStoreChange","stringifyDeterministicForKeys","params","cache","_","value","result","key","import_isEqual","import_react","useDeepMemo","value","state","setState","isEqual","SharedQueryConfig","args","configureSharedQuery","config","seenQueryNames","sharedQuery","options","inflightQueries","mountedKeys","queryName","queryFn","refetchFn","staleMs","expiryMs","refetchOnStale","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","stringifyDeterministicForKeys","startMs","waitedMs","cached","now","stateValue","clone","inflight","count","DEFAULT_QUERY_STATE_ENTRY","useSharedQuery","query","stableArgs","useDeepMemo","setQueryState","useSharedState","isMounted","queryStateValue","setQueryStateValue","next","lastNonEmptyData","setLastNonEmptyData","execute","isRefetch","state","reason","controller"]}
|
|
1
|
+
{"version":3,"sources":["../src/sharedQuery.ts","../src/sharedState.ts","../src/storage.ts","../src/stringify.ts","../src/useDeepMemo.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 { MS_PER_DAY } from \"@choksheak/ts-utils/timeConstants\";\nimport getByteSize from \"object-sizeof\";\nimport {\n SetStateAction,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\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 * Return the last non-empty data if you want to display the last loaded data\n * while waiting for the new data to load.\n */\n lastNonEmptyData: 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\n/** Options to configure a shared query. */\nexport type SharedQueryOptions<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 /**\n * Function to fetch data.\n */\n queryFn: QueryFn<TArgs, 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 * 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 (seenQueryNames.has(options.queryName)) {\n throw new Error(`Duplicate shared query name \"${options.queryName}\"`);\n }\n seenQueryNames.add(options.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 const queryName = options.queryName;\n const queryFn = options.queryFn;\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 return queryName + \":\" + stringifyDeterministicForKeys(args);\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:\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 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 lastNonEmptyData,\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","/**\n * @packageDocumentation\n *\n * The \"shared state\" is a React state that shares the same value across the\n * entire app. Users declare a shared state constant that can be reused across\n * multiple components with the same underlying value.\n *\n * Shared states provide persistence to local storage and/or indexed DB out of\n * the box, by using \"@choksheak/ts-utils\". Note that no matter if the shared\n * state uses persistence or not, it will always maintain a copy of the state\n * in memory. Persisted values can be configured with an automatic expiration\n * duration.\n *\n * This module is largely similar to the open source \"jotai\" npm library. You\n * can use shared states to replace \"jotai\" in your code, and optionally make\n * use of the persistence mechanism. A quick migration guide from jotai is:\n *\n * ```\n * atom() --> sharedState()\n * useAtom() --> useSharedState()\n * useAtomValue() --> useSharedStateValue()\n * useSetAtom() --> useSharedStateSetter()\n * ```\n *\n * Example:\n * ```\n * import { sharedState, useSharedState } from \"@choksheak/react-utils/sharedState\";\n *\n * // Create a new shared state in the top level scope.\n * // The first argument is the default value, which is required.\n * // The second argument is an options object, and is optional (defaults\n * // to storing in memory only without any persistence).\n * const usersState = sharedState(\n * [], { store: { persistTo: \"indexedDb\", key: \"users\" }}\n * );\n *\n * export const UsersComponent: React.FC = () => {\n * // Get both the value and the setter.\n * const [users, setUsers] = useSharedState(usersState);\n * ...\n * };\n *\n * export const ReadOnlyComponent: React.FC = () => {\n * // Get the value only.\n * const users = useSharedStateValue(usersState);\n * ...\n * };\n *\n * export const WriteOnlyComponent: React.FC = () => {\n * // Get the setter only.\n * const setUsers = useSharedStateSetter(usersState);\n * ...\n * };\n * ```\n */\n\nimport { MS_PER_DAY } from \"@choksheak/ts-utils/timeConstants\";\nimport { Dispatch, SetStateAction, useSyncExternalStore } from \"react\";\n\nimport { getStorageAdapter, StorageOptions } from \"./storage\";\n\n/************************************************************************/\n/* Global config */\n/************************************************************************/\n\n/**\n * These values are configurable by the user at any time.\n */\nexport const SharedStateConfig = {\n /**\n * Default duration for storage expiration in milliseconds. Initial value\n * is 30 days, but can be changed to anything.\n */\n storeExpiryMs: 30 * MS_PER_DAY,\n\n /**\n * True to load from storage only on mount, false (default) to load in\n * the top level scope when the shared state is defined.\n */\n lazyLoad: false,\n};\n\nexport type SharedStateConfig = typeof SharedStateConfig;\n\n/** Convenience function to update global defaults. */\nexport function configureSharedState(config: Partial<SharedStateConfig>) {\n Object.assign(SharedStateConfig, config);\n}\n\n/************************************************************************/\n/* Pub-sub store\n/************************************************************************/\n\ntype Subscriber<T> = (next: T, prev: T) => void;\n\nfunction createPubSubStore() {\n const dataByKey = new Map<string, unknown>();\n const subscribersByKey = new Map<string, Set<Subscriber<unknown>>>();\n\n function notify<T>(key: string, next: T, prev: T): void {\n const subscribers = subscribersByKey.get(key);\n if (!subscribers) return;\n\n // Make copy to protect against mutation during iteration.\n const copy = Array.from(subscribers.values());\n for (const subscriber of copy) {\n try {\n subscriber(next, prev);\n } catch (e) {\n console.error(`Error invoking subscriber:`, e);\n }\n }\n }\n\n return {\n // This could be undefined if T includes undefined, else it should never\n // return undefined because the user has to always specify the default value.\n get<T>(key: string): T {\n return dataByKey.get(key) as T;\n },\n\n set<T>(key: string, value: T): void {\n const prev = dataByKey.get(key) as T;\n\n // Do nothing if no change.\n if (prev === value) return;\n\n dataByKey.set(key, value);\n notify(key, value, prev);\n },\n\n delete(key: string): void {\n dataByKey.delete(key);\n },\n\n setNoNotify<T>(key: string, value: T): void {\n dataByKey.set(key, value);\n },\n\n // Subscribe to changes of a specific key\n subscribe<T>(key: string, subscriber: Subscriber<T>): () => void {\n let subscribers = subscribersByKey.get(key);\n if (!subscribers) {\n subscribers = new Set();\n subscribersByKey.set(key, subscribers);\n }\n\n // Type coercion: we store Subscriber<unknown>\n subscribers.add(subscriber as Subscriber<unknown>);\n\n // Return unsubscribe function\n return () => {\n subscribers.delete(subscriber as Subscriber<unknown>);\n if (subscribers.size === 0) subscribersByKey.delete(key);\n };\n },\n } as const;\n}\n\nconst pubSubStore = createPubSubStore();\n\n/************************************************************************/\n/* Shared state implementation */\n/************************************************************************/\n\nconst STORAGE_KEY = \"state\";\n\n/** Options to configure a shared state. */\nexport type SharedStateOptions<T> = {\n lazyLoad?: boolean;\n} & StorageOptions<T>;\n\nlet stateKey = 0;\n\n/**\n * Create a new shared state object. Put this code at the top level scope:\n *\n * Example:\n * ```\n * const counterState = sharedState<number>(0);\n * ```\n */\nexport function sharedState<T>(\n defaultValue: T,\n options?: SharedStateOptions<T>,\n) {\n let readyResolve: () => void = () => {};\n\n const readyPromise = new Promise<void>((resolve) => {\n readyResolve = resolve;\n });\n\n const resolveReadyPromise = readyResolve;\n\n const pubSubKey = String(stateKey++);\n\n // Use max of 1 storage adapter per shared state.\n const storageAdapter = getStorageAdapter(\n options,\n SharedStateConfig.storeExpiryMs,\n );\n\n // Always set the default value first to avoid returning undefineds if that\n // is not part of T.\n pubSubStore.setNoNotify(pubSubKey, defaultValue);\n\n const lazyLoad = options?.lazyLoad;\n const lazy = lazyLoad !== undefined ? lazyLoad : SharedStateConfig.lazyLoad;\n\n const obj = {\n /** Allow users to await for the completion of data init. */\n readyPromise,\n\n lazy,\n\n initStarted: false,\n initDone: false,\n\n /** `subscribe` function required by useSyncExternalStore. */\n subscribe(subscriber: () => void): () => void {\n return pubSubStore.subscribe(pubSubKey, () => subscriber());\n },\n\n /** Returns true if there is a backing store. */\n hasStore(): boolean {\n return Boolean(storageAdapter);\n },\n\n /**\n * Set a value directly into the store. Does nothing if there is no\n * backing store.\n *\n * Warning: If you want to set the value properly, you should use\n * `setValue()` instead which also updates the in-memory copy.\n */\n async setInStore(value: T): Promise<void> {\n return await storageAdapter?.set(STORAGE_KEY, value);\n },\n\n /**\n * Get a value directly from the store. Returns undefined if there is no\n * backing store.\n */\n async getFromStore(): Promise<T | undefined> {\n return await storageAdapter?.get(STORAGE_KEY);\n },\n\n /**\n * Get the value of the shared state.\n * `getSnapshot` function — must return same ref if value unchanged.\n * Note that this would return the default value if data init from store is\n * not completed yet.\n */\n getSnapshot(): T {\n return pubSubStore.get(pubSubKey);\n },\n\n /** Set the value of the shared state. */\n setValue(next: SetStateAction<T>): void {\n if (typeof next === \"function\") {\n const prev = pubSubStore.get<T>(pubSubKey);\n next = (next as (p: T) => T)(prev);\n }\n\n pubSubStore.set(pubSubKey, next);\n void obj.setInStore(next);\n },\n\n /**\n * Remove the value of the shared state. Actually this just sets the value\n * back to the given default value.\n */\n delete() {\n pubSubStore.set(pubSubKey, defaultValue);\n void storageAdapter?.set(STORAGE_KEY, defaultValue);\n },\n\n /** Initial default handling. */\n async initDefaultValueOnce() {\n if (obj.initDone || obj.initStarted) return;\n // @ts-expect-error ts(2540) - Assign to readonly property.\n obj.initStarted = true;\n\n const stored = await obj.getFromStore();\n const value = stored !== undefined ? stored : defaultValue;\n\n pubSubStore.set(pubSubKey, value);\n\n // @ts-expect-error ts(2540) - Assign to readonly property.\n obj.initDone = true;\n resolveReadyPromise();\n },\n } as const;\n\n if (!lazy) {\n void obj.initDefaultValueOnce();\n }\n\n return obj;\n}\n\nexport type SharedState<T> = ReturnType<typeof sharedState<T>>;\n\n/**\n * React hook for subscribing to a key in the global store.\n *\n * Example:\n * ```\n * const [counter, setCounter] = useSharedState(counterState);\n * ```\n */\nexport function useSharedState<T>(\n state: SharedState<T>,\n): [T, Dispatch<SetStateAction<T>>] {\n void state.initDefaultValueOnce();\n\n const value = useSyncExternalStore(\n (onStoreChange) => state.subscribe(onStoreChange),\n () => state.getSnapshot(),\n // getServerSnapshot is needed to prevent SSR dev errors in nextjs.\n () => state.getSnapshot(),\n );\n\n return [value, state.setValue];\n}\n\n/**\n * Same as useSharedState(), but returns the value only.\n *\n * Example:\n * ```\n * const counter = useSharedStateValue(counterState);\n * ```\n */\nexport function useSharedStateValue<T>(state: SharedState<T>) {\n return useSharedState(state)[0];\n}\n\n/**\n * Same as useSharedState(), but returns the setter only.\n *\n * Example:\n * ```\n * const setCounter = useSharedStateSetter(counterState);\n * ```\n */\nexport function useSharedStateSetter<T>(state: SharedState<T>) {\n void state.initDefaultValueOnce();\n\n return state.setValue;\n}\n","/**\n * @packageDocumentation\n *\n * Backing storage implementation for the shared state. Note that this uses the\n * LocalStore and KvStore from package \"@choksheak/ts-utils\", but is customized\n * to be used for shared states.\n *\n * Normally you will not need to use this module directly, but if you have a\n * need for it, feel free to use it.\n */\n\nimport { Duration } from \"@choksheak/ts-utils/duration\";\nimport { kvStore } from \"@choksheak/ts-utils/kvStore\";\nimport { createLocalStore } from \"@choksheak/ts-utils/localStore\";\nimport { StorageAdapter } from \"@choksheak/ts-utils/storageAdapter\";\n\n/** List of all available pre-built persistence options. */\nexport type PersistTo = \"localStorage\" | \"indexedDb\";\n\n/** Options to configure a pre-built store adapter. */\nexport type StoreOptions = {\n /** Choose which pre-built store adapter to use. */\n persistTo: PersistTo;\n\n /** Storage key to act as a namespace to store the data. */\n key: string;\n\n /** How long to wait before the value is considered expired. */\n expiryMs?: number | Duration;\n\n /** Returns true if the given value supposed to be of type T is valid. */\n isValid?: (u: unknown) => boolean;\n};\n\nexport type StoreConstructorArgs = Omit<StoreOptions, \"persistTo\">;\n\n/** Either use a pre-built store, or a custom one you provide. */\nexport type StorageOptions<T> = {\n /**\n * You can either specify the storage adapter directly to use a custom\n * store, or specify the properties to use the pre-built ones.\n */\n store?: StorageAdapter<T> | StoreOptions;\n};\n\n// In SSR (Server-Side Rendering) mode, we never want to use our storage\n// adapters for anything.\nconst IS_NOT_BROWSER = typeof window === \"undefined\";\n\n/**\n * Convert a StorageOptions<T> config object into an actual storage adapter, or\n * returns null if a storage adapter is not configured.\n */\nexport function getStorageAdapter<T>(\n options: StorageOptions<T> | undefined,\n defaultStoreExpiryMs: number,\n): StorageAdapter<T> | null {\n const { store } = options ?? {};\n\n if (!store) {\n return null;\n }\n\n if (\n \"persistTo\" in store &&\n typeof store.persistTo === \"string\" &&\n \"key\" in store &&\n typeof store.key === \"string\"\n ) {\n const expiryMs = store.expiryMs || defaultStoreExpiryMs;\n const config = { ...store, expiryMs };\n\n switch (store.persistTo) {\n case \"localStorage\":\n return localStoreAdapter<T>(config);\n\n case \"indexedDb\":\n return kvStoreAdapter<T>(config);\n\n default:\n store.persistTo satisfies never;\n throw new Error(`Unknown store.adapter ${store.persistTo}`);\n }\n }\n\n return store as StorageAdapter<T>;\n}\n\n/**\n * Create a new SSR-safe local store adapter, which allows users to reuse the\n * same adapter in multiple shared states or shared queries.\n */\nexport function localStoreAdapter<T>({\n key,\n expiryMs,\n isValid,\n}: StoreConstructorArgs) {\n const adapter = createLocalStore(key, { defaultExpiryMs: expiryMs });\n\n return {\n set: (key: string, value: T): void => {\n if (IS_NOT_BROWSER) return; // handle SSR\n\n // Also apply the custom expiryMs on every item in this store.\n adapter.set(key, value);\n },\n\n get: (key: string): T | undefined => {\n if (IS_NOT_BROWSER) return undefined; // handle SSR\n\n const value = adapter.get<T>(key);\n\n if (isValid && !isValid(value)) {\n console.warn(\n `localStorage adapter: Auto-discard invalid value for ${key}: ${JSON.stringify(value)}`,\n );\n adapter.delete(key);\n return undefined;\n }\n\n return value;\n },\n\n delete: (key: string): void => {\n if (IS_NOT_BROWSER) return; // handle SSR\n\n adapter.delete(key);\n },\n\n clear: (): void => {\n if (IS_NOT_BROWSER) return; // handle SSR\n\n adapter.clear();\n },\n };\n}\n\n/**\n * Create a new SSR-safe indexed DB KV store adapter, which allows users to\n * reuse the same adapter in multiple shared states or shared queries.\n */\nexport function kvStoreAdapter<T>({\n key,\n expiryMs,\n isValid,\n}: StoreConstructorArgs) {\n // Use the key prefix as a unique namespace for this store.\n const keyPrefix = key + \":\";\n\n // Prefix all keys using `keyPrefix` so that we can keep all data in\n // the same store in the same DB. If we keep the data in different\n // DB stores, then GC will not be able to clean up abandoned stores.\n return {\n set: async (key: string, value: T): Promise<void> => {\n if (IS_NOT_BROWSER) return; // handle SSR\n\n // Also apply the custom expiryMs on every item in this store.\n await kvStore.set(keyPrefix + key, value, expiryMs);\n },\n get: async (key: string): Promise<T | undefined> => {\n if (IS_NOT_BROWSER) return undefined; // handle SSR\n\n const fullKey = keyPrefix + key;\n const value = await kvStore.get<T>(fullKey);\n\n // If there is no value, don't try to check for validatity.\n if (value !== undefined && isValid && !isValid(value)) {\n console.warn(\n `indexedDb adapter: Auto-discard invalid value for ${key}:`,\n value,\n );\n await kvStore.delete(fullKey);\n return undefined;\n }\n\n return value;\n },\n delete: async (key: string): Promise<void> => {\n if (IS_NOT_BROWSER) return; // handle SSR\n\n await kvStore.delete(keyPrefix + key);\n },\n clear: async (): Promise<void> => {\n if (IS_NOT_BROWSER) return; // handle SSR\n\n await kvStore.clear();\n },\n };\n}\n","/**\n * @packageDocumentation\n *\n * Implements special stringify for use in the shared queries. This will likely\n * not be reusable for your own use cases.\n *\n * Normally you will not need to use this module directly, but if you have a\n * need for it, feel free to use it.\n */\n\n/**\n * JSON stringify a value of any type, and returns an unsafe string. The string\n * is \"unsafe\" because you might not be able to restore the original object\n * using the serialized JSON. This is specifically used for query key generation\n * and is not suitable for more general uses.\n *\n * Note: The given value should not contain any functions because functions\n * cannot be serialized and deserialized properly.\n */\nexport function stringifyDeterministicForKeys(params: unknown): string {\n const cache = new Set();\n\n return JSON.stringify(params, (_, value) => {\n if (typeof value === \"function\") {\n throw new Error(`Functions should not be passed into query parameters`);\n }\n\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return value;\n }\n\n if (cache.has(value)) {\n // Circular reference detected! Return a placeholder string.\n return \"[Circular]\";\n }\n\n cache.add(value);\n\n // Don't crash on circular references, but don't mark the top level object\n // as circular because we want to return as much string as possible.\n if (value !== params) {\n try {\n JSON.stringify(value);\n } catch {\n return \"[Unserializable]\";\n }\n }\n\n return Object.keys(value)\n .sort()\n .reduce(\n (result: Record<string, unknown>, key: string) => {\n result[key] = value[key];\n return result;\n },\n {} as Record<string, unknown>,\n );\n });\n}\n","/**\n * @packageDocumentation\n *\n * Provides a memoized value in React where the value is checked for deep\n * equality. If the value has changed, but is deeply equal to the previous\n * value, then useDeepMemo will return the same reference to the previous\n * value and ignore the latest value.\n *\n * This is mainly used in shared queries to check for changes in the query\n * arguments, but it might be useful for your own use cases.\n */\n\nimport isEqual from \"lodash/isEqual\";\nimport { useMemo, useState } from \"react\";\n\n/**\n * Provides a stable value for T based on deep equality. The deep equality is\n * determined by the `isEqual()` function in lodash.\n */\nexport function useDeepMemo<T>(value: T): T {\n const [state, setState] = useState<T>(value);\n\n return useMemo(() => {\n if (isEqual(state, value)) {\n return state;\n }\n\n // We check equality, so this should be safe.\n // eslint-disable-next-line react-hooks/set-state-in-render\n setState(value);\n\n return value;\n }, [state, value]);\n}\n"],"mappings":"skBAAA,IAAAA,GAAA,GAAAC,GAAAD,GAAA,uBAAAE,EAAA,yBAAAC,GAAA,gBAAAC,GAAA,mBAAAC,KAAA,eAAAC,GAAAN,IAqFA,IAAAO,GAA2B,6CAC3BC,EAAwB,4BACxBC,EAOO,iBCtCP,IAAAC,EAA2B,6CAC3BC,EAA+D,iBC7C/D,IAAAC,EAAwB,uCACxBC,EAAiC,0CAkC3BC,EAAiB,OAAO,OAAW,IAMlC,SAASC,EACdC,EACAC,EAC0B,CAC1B,GAAM,CAAE,MAAAC,CAAM,EAAIF,GAAW,CAAC,EAE9B,GAAI,CAACE,EACH,OAAO,KAGT,GACE,cAAeA,GACf,OAAOA,EAAM,WAAc,UAC3B,QAASA,GACT,OAAOA,EAAM,KAAQ,SACrB,CACA,IAAMC,EAAWD,EAAM,UAAYD,EAC7BG,EAAS,CAAE,GAAGF,EAAO,SAAAC,CAAS,EAEpC,OAAQD,EAAM,UAAW,CACvB,IAAK,eACH,OAAOG,GAAqBD,CAAM,EAEpC,IAAK,YACH,OAAOE,GAAkBF,CAAM,EAEjC,QACE,MAAAF,EAAM,UACA,IAAI,MAAM,yBAAyBA,EAAM,SAAS,EAAE,CAC9D,CACF,CAEA,OAAOA,CACT,CAMO,SAASG,GAAqB,CACnC,IAAAE,EACA,SAAAJ,EACA,QAAAK,CACF,EAAyB,CACvB,IAAMC,KAAU,oBAAiBF,EAAK,CAAE,gBAAiBJ,CAAS,CAAC,EAEnE,MAAO,CACL,IAAK,CAACI,EAAaG,IAAmB,CAChCZ,GAGJW,EAAQ,IAAIF,EAAKG,CAAK,CACxB,EAEA,IAAMH,GAA+B,CACnC,GAAIT,EAAgB,OAEpB,IAAMY,EAAQD,EAAQ,IAAOF,CAAG,EAEhC,GAAIC,GAAW,CAACA,EAAQE,CAAK,EAAG,CAC9B,QAAQ,KACN,wDAAwDH,CAAG,KAAK,KAAK,UAAUG,CAAK,CAAC,EACvF,EACAD,EAAQ,OAAOF,CAAG,EAClB,MACF,CAEA,OAAOG,CACT,EAEA,OAASH,GAAsB,CACzBT,GAEJW,EAAQ,OAAOF,CAAG,CACpB,EAEA,MAAO,IAAY,CACbT,GAEJW,EAAQ,MAAM,CAChB,CACF,CACF,CAMO,SAASH,GAAkB,CAChC,IAAAC,EACA,SAAAJ,EACA,QAAAK,CACF,EAAyB,CAEvB,IAAMG,EAAYJ,EAAM,IAKxB,MAAO,CACL,IAAK,MAAOA,EAAaG,IAA4B,CAC/CZ,GAGJ,MAAM,UAAQ,IAAIa,EAAYJ,EAAKG,EAAOP,CAAQ,CACpD,EACA,IAAK,MAAOI,GAAwC,CAClD,GAAIT,EAAgB,OAEpB,IAAMc,EAAUD,EAAYJ,EACtBG,EAAQ,MAAM,UAAQ,IAAOE,CAAO,EAG1C,GAAIF,IAAU,QAAaF,GAAW,CAACA,EAAQE,CAAK,EAAG,CACrD,QAAQ,KACN,qDAAqDH,CAAG,IACxDG,CACF,EACA,MAAM,UAAQ,OAAOE,CAAO,EAC5B,MACF,CAEA,OAAOF,CACT,EACA,OAAQ,MAAOH,GAA+B,CACxCT,GAEJ,MAAM,UAAQ,OAAOa,EAAYJ,CAAG,CACtC,EACA,MAAO,SAA2B,CAC5BT,GAEJ,MAAM,UAAQ,MAAM,CACtB,CACF,CACF,CDxHO,IAAMe,EAAoB,CAK/B,cAAe,GAAK,aAMpB,SAAU,EACZ,EAeA,SAASC,IAAoB,CAC3B,IAAMC,EAAY,IAAI,IAChBC,EAAmB,IAAI,IAE7B,SAASC,EAAUC,EAAaC,EAASC,EAAe,CACtD,IAAMC,EAAcL,EAAiB,IAAIE,CAAG,EAC5C,GAAI,CAACG,EAAa,OAGlB,IAAMC,EAAO,MAAM,KAAKD,EAAY,OAAO,CAAC,EAC5C,QAAWE,KAAcD,EACvB,GAAI,CACFC,EAAWJ,EAAMC,CAAI,CACvB,OAASI,EAAG,CACV,QAAQ,MAAM,6BAA8BA,CAAC,CAC/C,CAEJ,CAEA,MAAO,CAGL,IAAON,EAAgB,CACrB,OAAOH,EAAU,IAAIG,CAAG,CAC1B,EAEA,IAAOA,EAAaO,EAAgB,CAClC,IAAML,EAAOL,EAAU,IAAIG,CAAG,EAG1BE,IAASK,IAEbV,EAAU,IAAIG,EAAKO,CAAK,EACxBR,EAAOC,EAAKO,EAAOL,CAAI,EACzB,EAEA,OAAOF,EAAmB,CACxBH,EAAU,OAAOG,CAAG,CACtB,EAEA,YAAeA,EAAaO,EAAgB,CAC1CV,EAAU,IAAIG,EAAKO,CAAK,CAC1B,EAGA,UAAaP,EAAaK,EAAuC,CAC/D,IAAIF,EAAcL,EAAiB,IAAIE,CAAG,EAC1C,OAAKG,IACHA,EAAc,IAAI,IAClBL,EAAiB,IAAIE,EAAKG,CAAW,GAIvCA,EAAY,IAAIE,CAAiC,EAG1C,IAAM,CACXF,EAAY,OAAOE,CAAiC,EAChDF,EAAY,OAAS,GAAGL,EAAiB,OAAOE,CAAG,CACzD,CACF,CACF,CACF,CAEA,IAAMQ,EAAcZ,GAAkB,EAMhCa,EAAc,QAOhBC,GAAW,EAUR,SAASC,EACdC,EACAC,EACA,CACA,IAAIC,EAA2B,IAAM,CAAC,EAEhCC,EAAe,IAAI,QAAeC,GAAY,CAClDF,EAAeE,CACjB,CAAC,EAEKC,EAAsBH,EAEtBI,EAAY,OAAOR,IAAU,EAG7BS,EAAiBC,EACrBP,EACAQ,EAAkB,aACpB,EAIAb,EAAY,YAAYU,EAAWN,CAAY,EAE/C,IAAMU,EAAWT,GAAS,SACpBU,EAAOD,IAAa,OAAYA,EAAWD,EAAkB,SAE7DG,EAAM,CAEV,aAAAT,EAEA,KAAAQ,EAEA,YAAa,GACb,SAAU,GAGV,UAAUlB,EAAoC,CAC5C,OAAOG,EAAY,UAAUU,EAAW,IAAMb,EAAW,CAAC,CAC5D,EAGA,UAAoB,CAClB,MAAO,EAAQc,CACjB,EASA,MAAM,WAAWZ,EAAyB,CACxC,OAAO,MAAMY,GAAgB,IAAIV,EAAaF,CAAK,CACrD,EAMA,MAAM,cAAuC,CAC3C,OAAO,MAAMY,GAAgB,IAAIV,CAAW,CAC9C,EAQA,aAAiB,CACf,OAAOD,EAAY,IAAIU,CAAS,CAClC,EAGA,SAASjB,EAA+B,CACtC,GAAI,OAAOA,GAAS,WAAY,CAC9B,IAAMC,EAAOM,EAAY,IAAOU,CAAS,EACzCjB,EAAQA,EAAqBC,CAAI,CACnC,CAEAM,EAAY,IAAIU,EAAWjB,CAAI,EAC1BuB,EAAI,WAAWvB,CAAI,CAC1B,EAMA,QAAS,CACPO,EAAY,IAAIU,EAAWN,CAAY,EAClCO,GAAgB,IAAIV,EAAaG,CAAY,CACpD,EAGA,MAAM,sBAAuB,CAC3B,GAAIY,EAAI,UAAYA,EAAI,YAAa,OAErCA,EAAI,YAAc,GAElB,IAAMC,EAAS,MAAMD,EAAI,aAAa,EAChCjB,EAAQkB,IAAW,OAAYA,EAASb,EAE9CJ,EAAY,IAAIU,EAAWX,CAAK,EAGhCiB,EAAI,SAAW,GACfP,EAAoB,CACtB,CACF,EAEA,OAAKM,GACEC,EAAI,qBAAqB,EAGzBA,CACT,CAYO,SAASE,EACdC,EACkC,CAClC,OAAKA,EAAM,qBAAqB,EASzB,IAPO,wBACXC,GAAkBD,EAAM,UAAUC,CAAa,EAChD,IAAMD,EAAM,YAAY,EAExB,IAAMA,EAAM,YAAY,CAC1B,EAEeA,EAAM,QAAQ,CAC/B,CEjTO,SAASE,EAA8BC,EAAyB,CACrE,IAAMC,EAAQ,IAAI,IAElB,OAAO,KAAK,UAAUD,EAAQ,CAACE,EAAGC,IAAU,CAC1C,GAAI,OAAOA,GAAU,WACnB,MAAM,IAAI,MAAM,sDAAsD,EAGxE,GAAI,CAACA,GAAS,OAAOA,GAAU,UAAY,MAAM,QAAQA,CAAK,EAC5D,OAAOA,EAGT,GAAIF,EAAM,IAAIE,CAAK,EAEjB,MAAO,aAOT,GAJAF,EAAM,IAAIE,CAAK,EAIXA,IAAUH,EACZ,GAAI,CACF,KAAK,UAAUG,CAAK,CACtB,MAAQ,CACN,MAAO,kBACT,CAGF,OAAO,OAAO,KAAKA,CAAK,EACrB,KAAK,EACL,OACC,CAACC,EAAiCC,KAChCD,EAAOC,CAAG,EAAIF,EAAME,CAAG,EAChBD,GAET,CAAC,CACH,CACJ,CAAC,CACH,CC9CA,IAAAE,EAAoB,6BACpBC,EAAkC,iBAM3B,SAASC,EAAeC,EAAa,CAC1C,GAAM,CAACC,EAAOC,CAAQ,KAAI,YAAYF,CAAK,EAE3C,SAAO,WAAQ,OACT,EAAAG,SAAQF,EAAOD,CAAK,EACfC,GAKTC,EAASF,CAAK,EAEPA,GACN,CAACC,EAAOD,CAAK,CAAC,CACnB,CJsMO,IAAMI,EAAoB,CAE/B,QAAS,EAGT,SAAU,GAAK,cAGf,eAAgB,GAGhB,UAAW,OAGX,QAAS,IAGT,SAAU,IAGV,IAAK,IAAIC,IAAoB,QAAQ,IAAI,gBAAiB,GAAGA,CAAI,CACnE,EAKO,SAASC,GAAqBC,EAAoC,CACvE,OAAO,OAAOH,EAAmBG,CAAM,CACzC,CAMA,IAAMC,GAAiB,IAAI,IAoBpB,SAASC,GAGdC,EACA,CACA,GAAIF,GAAe,IAAIE,EAAQ,SAAS,EACtC,MAAM,IAAI,MAAM,gCAAgCA,EAAQ,SAAS,GAAG,EAEtEF,GAAe,IAAIE,EAAQ,SAAS,EAEpC,IAAMC,EAAkB,IAAI,IAUtBC,EAAc,IAAI,IAElBC,EAAYH,EAAQ,UACpBI,EAAUJ,EAAQ,QAClBK,EAAYL,EAAQ,UAEpBM,EAAUN,GAAS,SAAWN,EAAkB,QAChDa,EAAWP,GAAS,UAAYN,EAAkB,SAElDc,EACJR,GAAS,gBAAkBN,EAAkB,eAIzCe,EAAQT,EAAQ,MAEtB,GAAIS,GAEF,GAAI,cAAeA,GAASA,EAAM,QAAS,CACzC,IAAMC,EAAUD,EAAM,QAEtBT,EAAU,CACR,GAAGA,EACH,MAAO,CACL,GAAGS,EACH,QAAUE,GAAM,CAEd,GAAI,CAACA,GAAK,OAAOA,GAAM,SACrB,MAAO,GAOT,QAAWC,KAAK,OAAO,OAAOD,CAAC,EAC7B,GACE,CAACC,GACD,OAAOA,GAAM,UACb,EAAE,SAAUA,IACZ,EAAE,kBAAmBA,IACrB,EAAE,kBAAmBA,IACrB,OAAOA,EAAE,eAAkB,UAC3B,OAAOA,EAAE,eAAkB,UAC3B,CAACF,EAAQE,EAAE,IAAI,EAEf,MAAO,GAIX,MAAO,EACT,CACF,CACF,CACF,MACK,CACL,IAAMC,EAAYb,EAAQ,WAAaN,EAAkB,UAErDmB,IACFb,EAAU,CACR,GAAGA,EACH,MAAO,CACL,UAAAa,EACA,IAAKV,EACL,SAAAI,CACF,CACF,EAEJ,CAEA,IAAMO,EAAUd,GAAS,SAAWN,EAAkB,QAChDqB,EAAWf,GAAS,UAAYN,EAAkB,SAElDsB,EAAuB,EAAQhB,GAAS,qBAExCiB,EAAMjB,GAAS,KAAON,EAAkB,IAExCwB,EAAaC,EACjB,CAAC,EACDnB,CACF,EAEA,SAASoB,EAAQC,EAAgC,CAC/C,OAAIf,IAAY,EAAU,GAEZ,KAAK,IAAI,EAAIe,EACZf,CACjB,CAEA,SAASgB,EAAUD,EAAgC,CACjD,OAAId,IAAa,EAAU,GAEb,KAAK,IAAI,EAAIc,EACZd,CACjB,CAEA,SAASgB,GAAyB,CAEhC,GAAI,CAACT,GAAW,CAACC,EACf,OAGF,IAAMS,EAAWN,EAAW,YAAY,EAClCO,EAAU,OAAO,KAAKD,CAAQ,EAAE,OAGtC,GAAIC,GAAW,EACb,OAGF,IAAMC,KAAc,EAAAC,SAAYH,CAAQ,EAEpCI,EAAWd,EAAUW,EAAUX,EAAU,EACzCe,EAAad,EAAWW,EAAcX,EAAW,EAGrD,GAAIa,GAAY,GAAKC,GAAc,EAAG,CACpCZ,EACE,4BAA4Bd,CAAS,UAAUsB,CAAO,WAAWX,CAAO,eAAeY,EAAY,eAAe,CAAC,WAAWX,EAAS,eAAe,CAAC,GACzJ,EACA,MACF,CAGAE,EACE,gBAAgBd,CAAS,cAAcyB,CAAQ,gBAAgBC,CAAU,EAC3E,EAEA,IAAMC,EAAW,CAAE,GAAGN,CAAS,EAC3BO,EAAa,GAEXC,EAAwB,CAACC,EAAaC,IAAqB,CAE/D,GAAIhC,EAAY,IAAI+B,CAAG,EAAG,CACxBhB,EAAI,mBAAmBgB,CAAG,mBAAmB,EAC7C,MACF,CAEA,IAAME,KAAW,EAAAR,SAAYM,CAAG,KAAI,EAAAN,SAAYG,EAASG,CAAG,CAAC,EAC7DhB,EACE,wBAAwBiB,EAAU,YAAc,EAAE,IAAID,CAAG,cAAcE,EAAS,eAAe,CAAC,GAClG,EAEAP,IACAC,GAAcM,EAEd,OAAOL,EAASG,CAAG,EACnBF,EAAa,EACf,EAEIK,EAAyB,OAAO,QAAQN,CAAQ,EAAE,KACpD,CAACO,EAAQC,IAAWD,EAAO,CAAC,EAAE,cAAgBC,EAAO,CAAC,EAAE,aAC1D,EAMA,OAAW,CAACL,CAAG,IAAKG,EAAwB,CAE1C,GAAI,CAACd,EAAUQ,EAASG,CAAG,EAAE,aAAa,EACxC,MAGFD,EAAsBC,EAAK,EAAI,CACjC,CAEA,GAAIL,EAAW,GAAKC,EAAa,EAAG,CAC9BE,IACFK,EAAyB,OAAO,QAAQN,CAAQ,EAAE,KAChD,CAACO,EAAQC,IAAWD,EAAO,CAAC,EAAE,cAAgBC,EAAO,CAAC,EAAE,aAC1D,GAIF,OAAW,CAACL,CAAG,IAAKG,EAGlB,GAFAJ,EAAsBC,EAAK,EAAK,EAE5BL,GAAY,GAAKC,GAAc,EACjC,KAGN,CAGA,GAAI,CAACE,EAAY,CACfd,EACE,kCAAkCd,CAAS,UAAUsB,CAAO,WAAWX,CAAO,eAAeY,EAAY,eAAe,CAAC,WAAWX,EAAS,eAAe,CAAC,GAC/J,EACA,MACF,CAGAG,EAAW,SAASY,CAAQ,EAG5Bb,EACE,oBAAoBd,CAAS,WAAWsB,CAAO,OAAO,OAAO,KAAKK,CAAQ,EAAE,MAAM,YAAYhB,CAAO,gBAAgBY,EAAY,eAAe,CAAC,UAAO,EAAAC,SAAYG,CAAQ,EAAE,eAAe,CAAC,YAAYf,EAAS,eAAe,CAAC,GACrO,CACF,CAEA,SAASwB,EACPC,EACAC,EACApB,EAAgB,KAAK,IAAI,EACnB,CACN,IAAMqB,EAAgC,CACpC,cAAe,KAAK,IAAI,EACxB,QAAS,GACT,KAAAD,EACA,cAAApB,CACF,EAEAH,EAAW,SAAUyB,IAAU,CAAE,GAAGA,EAAM,CAACH,CAAQ,EAAGE,CAAM,EAAE,EAE9DnB,EAAiB,CACnB,CAEA,eAAeqB,GACbJ,EACAK,EACAlD,EACA,CACAsB,EAAI,kBAAkBuB,CAAQ,eAAeK,CAAS,EAAE,EAExD,GAAI,CACF,IAAMJ,EAAO,MAAOI,IAAc,WAAaxC,EAC3CA,EAAU,GAAGV,CAAI,EACjBS,EAAQ,GAAGT,CAAI,GAEnB,OAAAsB,EAAI,wBAAwBuB,CAAQ,EAAE,EACtCD,EAAQC,EAAUC,EAAM,KAAK,IAAI,CAAC,EAC3BA,CACT,OAASK,EAAG,CACV,MAAA7B,EAAI,mBAAmBuB,CAAQ,kBAAkBK,CAAS,KAAKC,CAAC,EAAE,EAC5DA,CACR,QAAE,CACA7C,EAAgB,OAAOuC,CAAQ,CACjC,CACF,CAEA,SAASO,EACPP,EACAK,KACGlD,EACa,CAChB,IAAMqD,EAAgB/C,EAAgB,IAAIuC,CAAQ,EAKlD,GAAIQ,EACF,OAAA/B,EACE,oCAAoCuB,CAAQ,eAAeK,CAAS,EACtE,EACOG,EAAc,QAIvB,IAAMC,EAAUL,GAAcJ,EAAUK,EAAWlD,CAAI,EACjDuD,EAAkB,IAAI,gBAE5B,OAAAjD,EAAgB,IAAIuC,EAAU,CAAE,QAAAS,EAAS,gBAAAC,CAAgB,CAAC,EAEnDD,CACT,CAEA,IAAME,EAAM,CACV,UAAAhD,EACA,QAAAC,EACA,UAAAC,EACA,SAAAE,EACA,QAAAD,EACA,eAAAE,EACA,QAAAM,EACA,SAAAC,EACA,qBAAAC,EACA,IAAAC,EACA,WAAAC,EAGA,YAAYvB,EAAqB,CAC/B,OAAOQ,EAAY,IAAMiD,EAA8BzD,CAAI,CAC7D,EASA,mBAAmBA,EAAqC,CACtD,IAAM6C,EAAWW,EAAI,YAAYxD,CAAI,EACrC,OAAOwD,EAAI,wBAAwBX,CAAQ,CAC7C,EAEA,wBAAwBA,EAA0C,CAChE,OAAOvC,EAAgB,IAAIuC,CAAQ,GAAG,iBAAmB,IAC3D,EA2BA,eAAe7C,EAAiC,CAC9C,IAAM6C,EAAWW,EAAI,YAAYxD,CAAI,EACrC,OAAOwD,EAAI,oBAAoBX,CAAQ,CACzC,EAEA,oBAAoBA,EAAsC,CACxD,OAAOW,EAAI,wBAAwBX,CAAQ,GAAG,QAAU,IAC1D,EAGA,MAAM,oBAAoB7C,EAA8C,CAItE,IAAM0D,EAAU,KAAK,IAAI,EACzB,MAAMnC,EAAW,aACjB,IAAMoC,EAAW,KAAK,IAAI,EAAID,EAExBb,EAAWW,EAAI,YAAYxD,CAAI,EAC/B4D,EAASrC,EAAW,YAAY,IAAIsB,CAAQ,EAMlD,GAJIc,GACFrC,EAAI,GAAGuB,CAAQ,gBAAgBc,CAAQ,yBAAyB,EAG9DC,GAAQ,MAAQ,CAACjC,EAAUiC,EAAO,eAAiB,CAAC,EAEtD,OAAKnC,EAAQmC,EAAO,eAAiB,CAAC,GAMlC/C,IACFS,EAAI,sBAAsBuB,CAAQ,EAAE,EAC/BO,EAAaP,EAAU,QAAS,GAAG7C,CAAI,GAI9CsB,EAAI,4BAA4BuB,CAAQ,EAAE,EACnCe,IAZLtC,EAAI,qBAAqBuB,CAAQ,8BAA8B,EACxDe,GAcX,IAAMd,EAAO,MAAMM,EAAaP,EAAU,QAAS,GAAG7C,CAAI,EACpD6D,EAAM,KAAK,IAAI,EAErB,MAAO,CAEL,cAAeA,EACf,QAAS,GACT,KAAAf,EACA,cAAee,CACjB,CACF,EAGA,cAAchB,EAAsD,CAClE,OAAOtB,EAAW,YAAY,IAAIsB,CAAQ,CAC5C,EAGA,QAAQA,EAAqC,CAC3C,IAAMiB,EACJvC,EAAW,YAAY,IAAIsB,CAAQ,EAGrC,GAAIiB,GAAY,eACVnC,EAAUmC,EAAW,aAAa,EAAG,CACvCN,EAAI,WAAWX,CAAQ,EACvB,MACF,CAGF,OAAOiB,GAAY,IACrB,EAGA,QAAAlB,EAMA,MAAM,oBAAoB5C,EAA6B,CACrD,IAAMsC,EAAMkB,EAAI,YAAYxD,CAAI,EAChC,OAAO,MAAMoD,EAAad,EAAK,QAAS,GAAGtC,CAAI,CACjD,EAMA,MAAM,WAAWA,EAA6B,CAC5C,IAAMsC,EAAMkB,EAAI,YAAYxD,CAAI,EAChC,OAAO,MAAMoD,EAAad,EAAK,UAAW,GAAGtC,CAAI,CACnD,EAGA,WAAW6C,EAAwB,CAEjC,IAAMkB,EAAQ,CAAE,GADDxC,EAAW,YAAY,CACZ,EAC1B,OAAOwC,EAAMlB,CAAQ,EACrBtB,EAAW,SAASwC,CAAK,EAEzB,IAAMC,EAAW1D,EAAgB,IAAIuC,CAAQ,EACzCmB,IACF1D,EAAgB,OAAOuC,CAAQ,EAC/BmB,EAAS,gBAAgB,MAAM,SAAS,EAE5C,EAGA,OAAc,CACZzC,EAAW,OAAO,EAElB,OAAW,CAAE,gBAAAgC,CAAgB,IAAK,MAAM,KAAKjD,EAAgB,OAAO,CAAC,EACnEiD,EAAgB,MAAM,SAAS,EAGjCjD,EAAgB,MAAM,CACxB,EAGA,MAAMuC,EAAwB,CAC5BtC,EAAY,IAAIsC,GAAWtC,EAAY,IAAIsC,CAAQ,GAAK,GAAK,CAAC,CAChE,EAGA,QAAQA,EAAwB,CAC9B,IAAMoB,EAAQ1D,EAAY,IAAIsC,CAAQ,EAClCoB,IAAU,EACZ1D,EAAY,OAAOsC,CAAQ,EAE3BtC,EAAY,IAAIsC,GAAWoB,GAAS,GAAK,CAAC,CAE9C,EAMA,iBAAArC,CACF,EAEA,OAAO4B,CACT,CAQA,IAAMU,GAA4D,CAChE,cAAe,EACf,QAAS,EACX,EAcO,SAASC,GACdC,KAEGpE,EACoB,CAEvB,IAAMqE,EAAaC,EAAYtE,CAAI,EAE7B,CAACuB,EAAYgD,CAAa,EAAIC,EAAeJ,EAAM,UAAU,EAE7DK,KAAY,UAAO,EAAI,EAEvB5B,KAAW,WACf,IAAMuB,EAAM,YAAYC,CAAU,EAClC,CAACD,EAAOC,CAAU,CACpB,EAGMK,EACJnD,EAAWsB,CAAQ,GAAKqB,GAEpBS,KAAqB,eACxBC,GAAiD,CAChDL,EAAevB,GAAS,CACtB,IAAMe,EAAQ,CAAE,GAAGf,CAAK,EACxB,OAAAe,EAAMlB,CAAQ,EACZ,OAAO+B,GAAS,WACZA,EAAKb,EAAMlB,CAAQ,GAAKqB,EAAyB,EACjDU,EACCb,CACT,CAAC,CACH,EACA,CAAClB,EAAU0B,CAAa,CAC1B,EAIM,CAACM,EAAkBC,CAAmB,KAAI,YAA4B,KAE5E,aAAU,IAAM,CACVV,EAAM,sBAAwBM,EAAgB,OAAS,QACzDI,EAAoBJ,EAAgB,IAAI,CAE5C,EAAG,CAACN,EAAM,qBAAsBM,EAAgB,IAAI,CAAC,EAGrD,IAAMK,KAAU,eACd,MAAOC,GAAuB,CAK5B,GAJAZ,EAAM,IACJ,gCAAgCvB,CAAQ,eAAemC,CAAS,EAClE,EAEI,EAACP,EAAU,QAEf,CAAAE,EAAoB3B,IAAU,CAE5B,GAAGA,EACH,cAAe,KAAK,IAAI,EACxB,QAAS,EACX,EAAE,EAEF,GAAI,CACF,IAAIiC,EAEJ,GAAID,EAAW,CACb,IAAMlC,EAAO,MAAMsB,EAAM,QAAQ,GAAGC,CAAU,EACxCR,EAAM,KAAK,IAAI,EAErBoB,EAAQ,CAEN,cAAepB,EACf,QAAS,GACT,KAAAf,EACA,cAAee,CACjB,CACF,MACEoB,EAAQ,MAAMb,EAAM,iBAAiB,GAAGC,CAAU,EAGpD,GAAI,CAACI,EAAU,QAAS,OAGxBE,EAAmB,CAAE,GAAGM,EAAO,QAAS,EAAM,CAAC,CACjD,OAAS9B,EAAG,CACV,GAAI,CAACsB,EAAU,QAAS,OAExB,IAAMZ,EAAM,KAAK,IAAI,EAErBc,EAAoB3B,IAAU,CAE5B,GAAGA,EACH,cAAea,EACf,QAAS,GACT,MAAOV,EACP,eAAgBU,CAClB,EAAE,CACJ,EACF,EACA,CAACO,EAAOvB,EAAU8B,EAAoBN,CAAU,CAClD,EAEA,sBAAU,KACRI,EAAU,QAAU,GACpBL,EAAM,MAAMvB,CAAQ,EAEfkC,EAAQ,EAAK,EAEX,IAAM,CACXN,EAAU,QAAU,GACpBL,EAAM,QAAQvB,CAAQ,CACxB,GACC,CAACkC,EAASX,EAAOvB,CAAQ,CAAC,KAEtB,WAAQ,KACN,CACL,GAAG6B,EACH,iBAAAG,EAIA,kBAAoBK,GAA8B,CAChD,IAAMC,EAAaf,EAAM,wBAAwBvB,CAAQ,EACzD,OAAAsC,GAAY,MAAMD,CAAM,EACjB,EAAQC,CACjB,EACA,QAAS,IACAJ,EAAQ,EAAI,EAErB,QAAS,CAACjC,EAAapB,IAAiC,CACtD0C,EAAM,QAAQvB,EAAUC,EAAMpB,CAAa,CAC7C,EACA,WAAY,IAAY,CACtB0C,EAAM,WAAWvB,CAAQ,CAC3B,CACF,GACC,CAACkC,EAASF,EAAkBT,EAAOvB,EAAU6B,CAAe,CAAC,CAClE","names":["sharedQuery_exports","__export","SharedQueryConfig","configureSharedQuery","sharedQuery","useSharedQuery","__toCommonJS","import_timeConstants","import_object_sizeof","import_react","import_timeConstants","import_react","import_kvStore","import_localStore","IS_NOT_BROWSER","getStorageAdapter","options","defaultStoreExpiryMs","store","expiryMs","config","localStoreAdapter","kvStoreAdapter","key","isValid","adapter","value","keyPrefix","fullKey","SharedStateConfig","createPubSubStore","dataByKey","subscribersByKey","notify","key","next","prev","subscribers","copy","subscriber","e","value","pubSubStore","STORAGE_KEY","stateKey","sharedState","defaultValue","options","readyResolve","readyPromise","resolve","resolveReadyPromise","pubSubKey","storageAdapter","getStorageAdapter","SharedStateConfig","lazyLoad","lazy","obj","stored","useSharedState","state","onStoreChange","stringifyDeterministicForKeys","params","cache","_","value","result","key","import_isEqual","import_react","useDeepMemo","value","state","setState","isEqual","SharedQueryConfig","args","configureSharedQuery","config","seenQueryNames","sharedQuery","options","inflightQueries","mountedKeys","queryName","queryFn","refetchFn","staleMs","expiryMs","refetchOnStale","store","isValid","u","v","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","stringifyDeterministicForKeys","startMs","waitedMs","cached","now","stateValue","clone","inflight","count","DEFAULT_QUERY_STATE_ENTRY","useSharedQuery","query","stableArgs","useDeepMemo","setQueryState","useSharedState","isMounted","queryStateValue","setQueryStateValue","next","lastNonEmptyData","setLastNonEmptyData","execute","isRefetch","state","reason","controller"]}
|
package/sharedQuery.min.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{MS_PER_DAY as
|
|
1
|
+
import{MS_PER_DAY as ie}from"@choksheak/ts-utils/timeConstants";import U from"object-sizeof";import{useCallback as Y,useEffect as I,useMemo as J,useRef as ue,useState as de}from"react";import{MS_PER_DAY as ee}from"@choksheak/ts-utils/timeConstants";import{useSyncExternalStore as te}from"react";import{kvStore as O}from"@choksheak/ts-utils/kvStore";import{createLocalStore as X}from"@choksheak/ts-utils/localStore";var A=typeof window>"u";function L(e,i){let{store:a}=e??{};if(!a)return null;if("persistTo"in a&&typeof a.persistTo=="string"&&"key"in a&&typeof a.key=="string"){let r=a.expiryMs||i,o={...a,expiryMs:r};switch(a.persistTo){case"localStorage":return Z(o);case"indexedDb":return q(o);default:throw a.persistTo,new Error(`Unknown store.adapter ${a.persistTo}`)}}return a}function Z({key:e,expiryMs:i,isValid:a}){let r=X(e,{defaultExpiryMs:i});return{set:(o,n)=>{A||r.set(o,n)},get:o=>{if(A)return;let n=r.get(o);if(a&&!a(n)){console.warn(`localStorage adapter: Auto-discard invalid value for ${o}: ${JSON.stringify(n)}`),r.delete(o);return}return n},delete:o=>{A||r.delete(o)},clear:()=>{A||r.clear()}}}function q({key:e,expiryMs:i,isValid:a}){let r=e+":";return{set:async(o,n)=>{A||await O.set(r+o,n,i)},get:async o=>{if(A)return;let n=r+o,u=await O.get(n);if(u!==void 0&&a&&!a(u)){console.warn(`indexedDb adapter: Auto-discard invalid value for ${o}:`,u),await O.delete(n);return}return u},delete:async o=>{A||await O.delete(r+o)},clear:async()=>{A||await O.clear()}}}var B={storeExpiryMs:30*ee,lazyLoad:!1};function re(){let e=new Map,i=new Map;function a(r,o,n){let u=i.get(r);if(!u)return;let T=Array.from(u.values());for(let b of T)try{b(o,n)}catch(S){console.error("Error invoking subscriber:",S)}}return{get(r){return e.get(r)},set(r,o){let n=e.get(r);n!==o&&(e.set(r,o),a(r,o,n))},delete(r){e.delete(r)},setNoNotify(r,o){e.set(r,o)},subscribe(r,o){let n=i.get(r);return n||(n=new Set,i.set(r,n)),n.add(o),()=>{n.delete(o),n.size===0&&i.delete(r)}}}}var x=re(),E="state",ne=0;function F(e,i){let a=()=>{},r=new Promise(f=>{a=f}),o=a,n=String(ne++),u=L(i,B.storeExpiryMs);x.setNoNotify(n,e);let T=i?.lazyLoad,b=T!==void 0?T:B.lazyLoad,S={readyPromise:r,lazy:b,initStarted:!1,initDone:!1,subscribe(f){return x.subscribe(n,()=>f())},hasStore(){return!!u},async setInStore(f){return await u?.set(E,f)},async getFromStore(){return await u?.get(E)},getSnapshot(){return x.get(n)},setValue(f){if(typeof f=="function"){let p=x.get(n);f=f(p)}x.set(n,f),S.setInStore(f)},delete(){x.set(n,e),u?.set(E,e)},async initDefaultValueOnce(){if(S.initDone||S.initStarted)return;S.initStarted=!0;let f=await S.getFromStore(),p=f!==void 0?f:e;x.set(n,p),S.initDone=!0,o()}};return b||S.initDefaultValueOnce(),S}function K(e){return e.initDefaultValueOnce(),[te(a=>e.subscribe(a),()=>e.getSnapshot(),()=>e.getSnapshot()),e.setValue]}function j(e){let i=new Set;return JSON.stringify(e,(a,r)=>{if(typeof r=="function")throw new Error("Functions should not be passed into query parameters");if(!r||typeof r!="object"||Array.isArray(r))return r;if(i.has(r))return"[Circular]";if(i.add(r),r!==e)try{JSON.stringify(r)}catch{return"[Unserializable]"}return Object.keys(r).sort().reduce((o,n)=>(o[n]=r[n],o),{})})}import ae from"lodash/isEqual";import{useMemo as oe,useState as se}from"react";function _(e){let[i,a]=se(e);return oe(()=>ae(i,e)?i:(a(e),e),[i,e])}var M={staleMs:0,expiryMs:30*ie,refetchOnStale:!0,persistTo:void 0,maxSize:100,maxBytes:1e5,log:(...e)=>console.log("[sharedQuery]",...e)};function ke(e){Object.assign(M,e)}var W=new Set;function Pe(e){if(W.has(e.queryName))throw new Error(`Duplicate shared query name "${e.queryName}"`);W.add(e.queryName);let i=new Map,a=new Map,r=e.queryName,o=e.queryFn,n=e.refetchFn,u=e?.staleMs??M.staleMs,T=e?.expiryMs??M.expiryMs,b=e?.refetchOnStale??M.refetchOnStale,S=e.store;if(S){if("persistTo"in S&&S.isValid){let t=S.isValid;e={...e,store:{...S,isValid:s=>{if(!s||typeof s!="object")return!1;for(let l of Object.values(s))if(!l||typeof l!="object"||!("data"in l)||!("dataUpdatedMs"in l)||!("lastUpdatedMs"in l)||typeof l.dataUpdatedMs!="number"||typeof l.lastUpdatedMs!="number"||!t(l.data))return!1;return!0}}}}}else{let t=e.persistTo??M.persistTo;t&&(e={...e,store:{persistTo:t,key:r,expiryMs:T}})}let f=e?.maxSize??M.maxSize,p=e?.maxBytes??M.maxBytes,h=!!e?.keepLastNonEmptyData,d=e?.log??M.log,g=F({},e);function Q(t){return u===0?!0:Date.now()-t>u}function k(t){return T===0?!1:Date.now()-t>T}function N(){if(!f&&!p)return;let t=g.getSnapshot(),s=Object.keys(t).length;if(s<=1)return;let l=U(t),c=f?s-f:0,y=p?l-p:0;if(c<=0&&y<=0){d(`No need to trim data for ${r}: size=${s} (limit=${f}), byteSize=${l.toLocaleString()} (limit=${p.toLocaleString()})`);return}d(`Need to trim ${r}: numToCut=${c}, bytesToCut=${y}`);let D={...t},v=!1,z=(m,$)=>{if(a.has(m)){d(`Cannot clean up ${m} as it is mounted`);return}let R=U(m)+U(D[m]);d(`Cleaning up unmounted${$?", expired":""} ${m} (byteSize=${R.toLocaleString()})`),c--,y-=R,delete D[m],v=!0},P=Object.entries(D).sort((m,$)=>m[1].lastUpdatedMs-$[1].lastUpdatedMs);for(let[m]of P){if(!k(D[m].lastUpdatedMs))break;z(m,!0)}if(c>0||y>0){v&&(P=Object.entries(D).sort((m,$)=>m[1].lastUpdatedMs-$[1].lastUpdatedMs));for(let[m]of P)if(z(m,!1),c<=0&&y<=0)break}if(!v){d(`No trimmable entries found for ${r}: size=${s} (limit=${f}), byteSize=${l.toLocaleString()} (limit=${p.toLocaleString()})`);return}g.setValue(D),d(`Trimmed data for ${r}: size=[${s} -> ${Object.keys(D).length}] (limit=${f}), byteSize=[${l.toLocaleString()} -> ${U(D).toLocaleString()}] (limit=${p.toLocaleString()})`)}function V(t,s,l=Date.now()){let c={lastUpdatedMs:Date.now(),loading:!1,data:s,dataUpdatedMs:l};g.setValue(y=>({...y,[t]:c})),N()}async function H(t,s,l){d(`Start fetching ${t}, queryType=${s}`);try{let c=await(s==="refetch"&&n?n(...l):o(...l));return d(`Successfully fetched ${t}`),V(t,c,Date.now()),c}catch(c){throw d(`Failed to fetch ${t} for queryType=${s}: ${c}`),c}finally{i.delete(t)}}function C(t,s,...l){let c=i.get(t);if(c)return d(`Deduplicating inflight fetch for ${t}, queryType=${s}`),c.promise;let y=H(t,s,l),D=new AbortController;return i.set(t,{promise:y,abortController:D}),y}let w={queryName:r,queryFn:o,refetchFn:n,expiryMs:T,staleMs:u,refetchOnStale:b,maxSize:f,maxBytes:p,keepLastNonEmptyData:h,log:d,queryState:g,getQueryKey(t){return r+":"+j(t)},getAbortController(t){let s=w.getQueryKey(t);return w.getAbortControllerByKey(s)},getAbortControllerByKey(t){return i.get(t)?.abortController??null},getAbortSignal(t){let s=w.getQueryKey(t);return w.getAbortSignalByKey(s)},getAbortSignalByKey(t){return w.getAbortControllerByKey(t)?.signal??null},async getCachedOrFetch(...t){let s=Date.now();await g.readyPromise;let l=Date.now()-s,c=w.getQueryKey(t),y=g.getSnapshot()?.[c];if(l&&d(`${c}: Waited for ${l}ms before data is ready`),y?.data&&!k(y.dataUpdatedMs??0))return Q(y.dataUpdatedMs??0)?(b&&(d(`refetchOnStale for ${c}`),C(c,"query",...t)),d(`Returning stale data for ${c}`),y):(d(`Return fresh data ${c} from cache without fetching`),y);let D=await C(c,"query",...t),v=Date.now();return{lastUpdatedMs:v,loading:!1,data:D,dataUpdatedMs:v}},getQueryValue(t){return g.getSnapshot()?.[t]},getData(t){let s=g.getSnapshot()?.[t];if(s?.lastUpdatedMs&&k(s.lastUpdatedMs)){w.deleteData(t);return}return s?.data},setData:V,async updateFromSource(...t){let s=w.getQueryKey(t);return await C(s,"query",...t)},async refetch(...t){let s=w.getQueryKey(t);return await C(s,"refetch",...t)},deleteData(t){let l={...g.getSnapshot()};delete l[t],g.setValue(l);let c=i.get(t);c&&(i.delete(t),c.abortController.abort("Deleted"))},clear(){g.delete();for(let{abortController:t}of Array.from(i.values()))t.abort("Cleared");i.clear()},mount(t){a.set(t,(a.get(t)??0)+1)},unmount(t){let s=a.get(t);s===1?a.delete(t):a.set(t,(s??0)-1)},enforceSizeLimit:N};return w}var G={lastUpdatedMs:0,loading:!0};function Ee(e,...i){let a=_(i),[r,o]=K(e.queryState),n=ue(!0),u=J(()=>e.getQueryKey(a),[e,a]),T=r[u]??G,b=Y(h=>{o(d=>{let g={...d};return g[u]=typeof h=="function"?h(g[u]??G):h,g})},[u,o]),[S,f]=de();I(()=>{e.keepLastNonEmptyData&&T.data!==void 0&&f(T.data)},[e.keepLastNonEmptyData,T.data]);let p=Y(async h=>{if(e.log(`Begin executing shared query ${u}, isRefetch=${h}`),!!n.current){b(d=>({...d,lastUpdatedMs:Date.now(),loading:!0}));try{let d;if(h){let g=await e.refetch(...a),Q=Date.now();d={lastUpdatedMs:Q,loading:!1,data:g,dataUpdatedMs:Q}}else d=await e.getCachedOrFetch(...a);if(!n.current)return;b({...d,loading:!1})}catch(d){if(!n.current)return;let g=Date.now();b(Q=>({...Q,lastUpdatedMs:g,loading:!1,error:d,errorUpdatedMs:g}))}}},[e,u,b,a]);return I(()=>(n.current=!0,e.mount(u),p(!1),()=>{n.current=!1,e.unmount(u)}),[p,e,u]),J(()=>({...T,lastNonEmptyData:S,abortCurrentQuery:h=>{let d=e.getAbortControllerByKey(u);return d?.abort(h),!!d},refetch:()=>p(!0),setData:(h,d)=>{e.setData(u,h,d)},deleteData:()=>{e.deleteData(u)}}),[p,S,e,u,T])}export{M as SharedQueryConfig,ke as configureSharedQuery,Pe as sharedQuery,Ee as useSharedQuery};
|
|
2
2
|
//# sourceMappingURL=sharedQuery.min.mjs.map
|