@choksheak/react-utils 0.4.9 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/choksheaklau/src/react-utils/dist/chunk-AX6NADU2.min.cjs","../src/sharedQuery.ts"],"names":["hasWindow","SharedQueryConfig","args","configureSharedQuery","config","moduleQueryRegistry","getOrCreateQueryRegistry","g","sharedQuery1","options","registry","key","existing","obj","sharedQuery2","url","queryName","inflightQueries","mountedKeys","queryFn","fetcher","http","retries","params","firstArg","entries","k","v","item","refetchFn","staleMs","expiryMs","store","isValid","u","persistTo","maxSize","maxBytes","keepLastNonEmptyData","log","queryState","sharedState","getQueryKey","s","stringifyDeterministicForKeys","XXH","getAbortControllerByKey","queryKey","getAbortController","getAbortSignal","getAbortSignalByKey","isStale","dataUpdatedMs","isExpired","enforceSizeLimit","oldState","oldSize","oldByteSize","getByteSize","numToCut","bytesToCut"],"mappings":"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","file":"/Users/choksheaklau/src/react-utils/dist/chunk-AX6NADU2.min.cjs","sourcesContent":[null,"/**\n * @packageDocumentation\n *\n * The \"shared query\" is a data fetching convenience library that helps users\n * to load data, check for staleness and expiration, cache in memory, local\n * storage or indexed db, and be able to manipulate the data anytime.\n *\n * Each shared query creates a new private shared state that handles the data\n * storage. This shared state contains the records for all queries, where each\n * query is a unique set of arguments used to make the query. The number of\n * records and the total byte size of data can be limited so as to ensure the\n * query does not take up too much memory.\n *\n * Think of shared queries as like the open source npm package \"react-query\",\n * but with strong type-checking and built-in support for persistence.\n *\n * Example:\n * ```\n * import { sharedQuery, useSharedQuery } from \"@choksheak/react-utils/sharedQuery\";\n * import { MS_PER_DAY } from \"@choksheak/ts-utils/timeConstants\";\n *\n * // Create a new shared query in the top level scope.\n * // The query function `queryFn` here does not take any arguments.\n * //\n * // Only the queryName and queryFn are required and everything else is\n * // optional.\n * //\n * // The queryName must be unique, otherwise we will throw an\n * // error. This is because defining the same queryName for two different\n * // queries implies that the queries are the same, thus violating the idea\n * // of a \"shared\" query, and almost certainly points to a bug in the code.\n * export const usersQuery = sharedQuery({\n * queryName: \"users\",\n * queryFn: listUsers,\n * persistTo: \"indexedDb\",\n * staleMs: MS_PER_DAY,\n * });\n *\n * // Example of using a query function with arguments.\n * export const getUserQuery = sharedQuery({\n * queryName: \"getUser\",\n * queryFn: getUser, // getUser(userId: string) => User\n * persistTo: \"indexedDb\",\n * staleMs: MS_PER_DAY,\n * });\n *\n * export const UsersComponent: React.FC = () => {\n * // Second argument is optional, and is an array of arguments to be\n * // passed to the `queryFn`.\n * const users = useSharedQuery(usersQuery);\n *\n * return (\n * <>\n * <h1>List of Users</h1>\n *\n * <button onclick={users.refetch}>Refresh</button>\n *\n * {users.loading && <Loader />}\n *\n * {users.error && !users.data && <p>Error: {String(users.error)}</p>}\n *\n * <p>Data: {users.data ? JSON.stringify(users.data) : \"-\"}</p>\n * </>\n * );\n * };\n *\n * export const AnotherComponent: React.FC = () => {\n * // This query will be deduped with the one above, sharing the same\n * // data and the same fetches.\n * const users = useSharedQuery(usersQuery);\n * ...\n * };\n *\n * export const DisplayOneUser: React.FC<{userId: string}> = ({userId}) => {\n * // Example of using the query with an argument. Whenever the argument\n * // changes, the data will be fetched and updated automatically.\n * const user = useSharedQuery(getUserQuery, userId);\n *\n * // You can check for user.loading and user.error if needed.\n *\n * return <>User: {user.data ? JSON.stringify(user.data) : \"-\"}</>;\n * };\n * ```\n */\n\nimport { http, retries } from \"@choksheak/ts-utils/http\";\nimport getByteSize from \"object-sizeof\";\nimport { SetStateAction, useEffect, useMemo, useRef, useState } from \"react\";\nimport XXH from \"xxhashjs\";\n\nimport { sharedState, SharedStateOptions, useSharedState } from \"./sharedState\";\nimport { PersistTo } from \"./storage\";\nimport { stringifyDeterministicForKeys } from \"./stringify\";\nimport { useDeepMemo } from \"./useDeepMemo\";\n\nconst hasWindow = typeof window !== \"undefined\";\n\n/** Type of the queryFn used to fetch the data from source. */\nexport type QueryFn<TArgs extends unknown[], TData> = (\n ...args: TArgs\n) => Promise<TData> | TData;\n\n/** An entry for a persisted state corresponding to one query key. */\nexport type QueryStateValue<TData> = Readonly<{\n /**\n * When this record was last updated. Used for LRU determination. If you want\n * the data age, please use `dataUpdatedMs` instead.\n */\n lastUpdatedMs: number;\n\n loading: boolean;\n data?: TData;\n dataUpdatedMs?: number;\n\n /**\n * Use unknown type for error because we can't assume what type of variable\n * will be thrown from the query function. We also don't want to add this\n * type to QueryStateValue<TError> as it adds complexity to the type.\n */\n error?: unknown;\n\n errorUpdatedMs?: number;\n}>;\n\n/** Return type for useSharedQuery(). */\nexport type UseQueryResult<TData> = QueryStateValue<TData> &\n Readonly<{\n /**\n * Usually same as `data`, but if you specify `keepLastNonEmptyData: true`,\n * then this will contain the latest data (undefined when args just changed)\n * and data will contain the last non empty data.\n */\n latestData: TData | undefined;\n\n /**\n * Cancels the inflight query (if any). Returns true if canceled.\n */\n abortCurrentQuery: (reason?: unknown) => boolean;\n\n /**\n * Let the caller get the data immediately no matter what. Throws an error\n * if the fetch fails.\n */\n getCachedOrFetch: () => Promise<TData>;\n\n /**\n * Get the cached state or do a new fetch, including errors. Never throws\n * an error.\n */\n getCachedStateOrFetch: () => Promise<QueryStateValue<TData>>;\n\n /**\n * Manually refresh the data by doing a new query.\n */\n refetch: () => Promise<TData | undefined>;\n\n /**\n * If you got the data from somewhere else (e.g. an in-memory data update),\n * just set it directly.\n */\n setData: (data: TData, dataUpdatedMs?: number) => void;\n\n deleteData: () => void;\n }>;\n\n/**\n * The full query state is a record of all the fetch states mapped by\n * query key to the state entry.\n */\nexport type SharedQueryState<TData> = Record<string, QueryStateValue<TData>>;\n\nexport type QueryTargetWithFn<TArgs extends unknown[], TData> = {\n /**\n * The name could have been auto-generated, but we let the user give us a\n * human-readable name that can be identified quickly in the logs.\n * Note that the queryKey is a string formed by the queryName plus the query\n * arguments.\n */\n queryName: string;\n\n /** Function to fetch data. */\n queryFn: QueryFn<TArgs, TData>;\n};\n\nexport type QueryTargetWithUrl = {\n /**\n * The queryName can optionally be specified, else we will fallback to use\n * the url as the queryName.\n */\n queryName?: string;\n\n /**\n * URL to fetch data using HTTP GET. Note that using this field will cause\n * both TArgs and TData to be undefined (unknown). Therefore you can make up\n * for this by specifying types with useSharedQuery, like this:\n * ```\n * const usersQuery = useSharedQuery<[{ userId?: string[] }], User[]>({\n * url: \"/api/list-users\",\n * });\n * ```\n *\n * If `userId` were specified as [\"u1\", \"u2\"], the query url would be\n * \"/api/list-users?userId=u1&userId=u2\".\n *\n * You can also pass the params as a string like \"userId=u1&userId=u2\".\n * ```\n * const usersQuery = useSharedQuery<[string], User[]>({\n * url: \"/api/list-users\",\n * });\n * ```\n */\n url: string;\n};\n\n/** Specifies how to fetch the data. */\nexport type QueryTarget<TArgs extends unknown[], TData> =\n | QueryTargetWithFn<TArgs, TData>\n | QueryTargetWithUrl;\n\n/** Options to configure a shared query. */\nexport type SharedQueryOptions<TArgs extends unknown[], TData> = QueryTarget<\n TArgs,\n TData\n> & {\n /**\n * Function to re-fetch data. Usually this would just be the `queryFn`, but\n * if you need a different behavior for refetching, then specify this.\n */\n refetchFn?: QueryFn<TArgs, TData>;\n\n /**\n * ms before data considered stale; 0 means always stale.\n */\n staleMs?: number;\n\n /**\n * ms before data is removed from cache; 0 means never expire.\n */\n expiryMs?: number;\n\n /**\n * 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 /** Customize the logger. */\n log?: (...args: unknown[]) => void;\n} & SharedStateOptions<SharedQueryState<TData>>;\n\n/** Users can override these values globally. */\nexport const SharedQueryConfig = {\n /** Refetch on every page load. */\n staleMs: 0,\n\n /** Keep in cache for a long time. */\n expiryMs: 30 * 86_400_000 /* MS_PER_DAY */,\n\n /** 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[]) => {\n if (!hasWindow) return; // don't log in the SSR server side\n console.log(\"[sharedQuery]\", ...args);\n },\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\nconst moduleQueryRegistry = new Map<string, unknown>();\n\n/**\n * When bundlers duplicate this module across chunks, each copy would otherwise\n * create a separate queryState (different pubSub keys in the same store).\n * Return the same SharedQuery for a given queryName app-wide.\n */\nfunction getOrCreateQueryRegistry(): Map<string, unknown> {\n if (hasWindow) {\n const g = window.globalThis as {\n reactUtilsQueryRegistry?: Map<string, unknown>;\n };\n return (g.reactUtilsQueryRegistry ??= new Map());\n }\n return moduleQueryRegistry;\n}\n\n// Split out function to auto-derive the return type.\nfunction sharedQuery1<TArgs extends unknown[], TData>(\n options: SharedQueryOptions<TArgs, TData>,\n) {\n const registry = getOrCreateQueryRegistry();\n const key = options.queryName || (options as QueryTargetWithUrl).url;\n\n const existing = registry.get(key);\n if (existing) {\n return existing;\n }\n\n const obj = sharedQuery2(options);\n registry.set(key, obj);\n return obj;\n}\n\n// Split out function to auto-derive the return type.\nfunction sharedQuery2<TArgs extends unknown[], TData>(\n // The name could have been auto-generated, but we let the user give us a\n // human-readable name that can be identified quickly in the logs.\n options: SharedQueryOptions<TArgs, TData>,\n) {\n // If queryName is not specified, fallback to use url as the queryName.\n const url = (options as QueryTargetWithUrl).url || \"\";\n const queryName = options.queryName || url;\n\n if (!queryName) {\n throw new Error(`Either queryName or url must be specified.`);\n }\n\n const inflightQueries = new Map<\n string,\n { promise: Promise<TData>; abortController: AbortController }\n >();\n\n /**\n * Keep track of the mounted query keys. This handles the case where an\n * earlier-started query returns later, and when it returns, it was already\n * unmounted, thus making it eligible for clean up right after fetch.\n */\n const mountedKeys = new Map<string, number>();\n\n let queryFn: QueryFn<TArgs, TData>;\n\n if ((options as QueryTargetWithFn<TArgs, TData>).queryFn) {\n queryFn = (options as QueryTargetWithFn<TArgs, TData>).queryFn;\n } else if (url) {\n const fetcher = http.use(retries());\n\n // Auto-generate the queryFn as a HTTP GET request using the url.\n queryFn = async (...args: TArgs): Promise<TData> => {\n // If args are given, treat the first argument as a search param.\n let params = \"\";\n const firstArg = args?.[0];\n\n if (firstArg) {\n if (typeof firstArg === \"string\") {\n params = firstArg;\n } else if (typeof firstArg === \"object\") {\n const entries: [string, string][] = [];\n\n for (const [k, v] of Object.entries(firstArg)) {\n if (v === undefined) continue;\n\n // Allow the same param to be specified multiple times.\n if (Array.isArray(v)) {\n for (const item of v) {\n if (item === undefined) continue;\n\n // The field values can only be primitives, not objects.\n entries.push([k, String(item)]);\n }\n } else {\n // The field values can only be primitives, not objects.\n entries.push([k, String(v)]);\n }\n }\n\n params = new URLSearchParams(entries).toString();\n }\n\n if (params) {\n if (url.includes(\"?\")) {\n // Append more search params to existing ones.\n params = \"&\" + params;\n } else {\n // Set the full search params string.\n params = \"?\" + params;\n }\n }\n }\n\n const response = await fetcher.get(url + params);\n\n return await response.json();\n };\n } else {\n throw new Error(`Either queryFn or url must be specified`);\n }\n\n const refetchFn = options.refetchFn;\n\n const staleMs = options?.staleMs ?? SharedQueryConfig.staleMs;\n const expiryMs = options?.expiryMs ?? SharedQueryConfig.expiryMs;\n\n // 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. Treats cached errors as invalid.\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 // Debug logging.\n options.name = queryName;\n\n const queryState = sharedState<SharedQueryState<TData>>(\n {}, // defaultValue\n options,\n );\n\n /** Returns a key to identify requests based on the given args. */\n function getQueryKey(args: TArgs): string {\n let s = stringifyDeterministicForKeys(args);\n\n // Ensure the query key is not too long.\n if (s.length > 30) {\n s = XXH.h32(s, 0x99ee).toString(16).slice(2); // remove \"0x\"\n }\n\n return queryName + \":\" + s;\n }\n\n /**\n * Get the AbortController to abort the inflight query (if any).\n *\n * Note that once you get the controller, you can also get the signal easily\n * using `controller.signal`. So if you need both the controller and the\n * signal, just use this function.\n */\n function getAbortControllerByKey(queryKey: string): AbortController | null {\n return inflightQueries.get(queryKey)?.abortController ?? null;\n }\n\n function getAbortController(args: TArgs): AbortController | null {\n const queryKey = getQueryKey(args);\n return getAbortControllerByKey(queryKey);\n }\n\n /**\n * Get the AbortSignal to check for query abortions.\n *\n * Example using fetch:\n * ```\n * const getUserQuery = sharedQuery({\n * queryName: \"getUser\",\n * queryFn: (userId: string) => {\n * // Get the signal for this current execution.\n * const signal = getUserQuery.getAbortSignal([userId]);\n *\n * // Pass the signal to fetch so that it can be aborted.\n * const response = await fetch(`/users/${userId}`, { signal });\n *\n * // Check for errors.\n * if (!response.ok) {\n * throw new Error(response.statusText);\n * }\n *\n * // Return the data.\n * return await response.json();\n * },\n * });\n * ```\n *\n * Example using `http.ts`:\n * ```\n * const getUserQuery = sharedQuery({\n * queryName: \"getUser\",\n * queryFn: (userId: string) => {\n * // Get the signal for this current execution.\n * const signal = getUserQuery.getAbortSignal([userId]);\n *\n * // Pass the signal to fetch so that it can be aborted.\n * const response = await http.get(`/users/${userId}`, { signal });\n * return await response.json();\n * },\n * });\n * ```\n */\n function getAbortSignal(args: TArgs): AbortSignal | null {\n const queryKey = getQueryKey(args);\n return getAbortSignalByKey(queryKey);\n }\n\n function getAbortSignalByKey(queryKey: string): AbortSignal | null {\n return getAbortControllerByKey(queryKey)?.signal ?? null;\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 /**\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 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): 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 false;\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 return true;\n };\n\n // lastUpdatedMs refers to either the dataUpdatedMs or the errorUpdatedMs.\n const entriesByLastUpdatedMsAsc = Object.fromEntries(\n Object.entries(newState).sort(\n (entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs,\n ),\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 Object.keys(entriesByLastUpdatedMsAsc)) {\n const state = newState[key];\n\n // Skip current fetches.\n if (!state?.lastUpdatedMs || state?.loading) {\n continue;\n }\n\n // Anything after this is not expired yet.\n if (state?.lastUpdatedMs && !isExpired(state.lastUpdatedMs)) {\n break;\n }\n\n if (deleteKeyIfNotMounted(key, true)) {\n delete entriesByLastUpdatedMsAsc[key];\n }\n }\n\n if (numToCut > 0 || bytesToCut > 0) {\n // Clean up starting from oldest entry first.\n for (const key of Object.keys(entriesByLastUpdatedMsAsc)) {\n const state = newState[key];\n\n // Skip current fetches.\n if (!state?.lastUpdatedMs || state?.loading) {\n continue;\n }\n\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 getQueryValue(queryKey: string): QueryStateValue<TData> | undefined {\n return queryState.getSnapshot()?.[queryKey];\n }\n\n /** Set the value of one entry inside the query state map. */\n function setQueryValue(\n queryKey: string,\n next: SetStateAction<QueryStateValue<TData>>,\n ) {\n queryState.setValue((prev) => {\n const value =\n typeof next === \"function\"\n ? next(prev[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY)\n : next;\n return { ...prev, [queryKey]: value };\n });\n\n enforceSizeLimit();\n }\n\n /** Get the current stored data for a query key. */\n function getData(queryKey: string): TData | undefined {\n const stateValue = getQueryValue(queryKey);\n\n // Check for expiration.\n if (stateValue?.dataUpdatedMs) {\n if (isExpired(stateValue.dataUpdatedMs)) {\n deleteData(queryKey);\n return undefined;\n }\n }\n\n return stateValue?.data;\n }\n\n /** Set the data of one entry inside the query state map. */\n function setData(\n queryKey: string,\n data: TData,\n dataUpdatedMs = Date.now(),\n ): void {\n setQueryValue(queryKey, {\n lastUpdatedMs: Date.now(),\n loading: false,\n data,\n dataUpdatedMs,\n });\n }\n\n /** Delete the cached data for one set of arguments. */\n function 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 async function wrapLoading(\n queryKey: string,\n fetchData: () => Promise<TData>,\n ): Promise<TData> {\n // Mark loading started and delete the old data + error.\n setQueryValue(queryKey, DEFAULT_QUERY_STATE_ENTRY);\n\n try {\n const data = await fetchData();\n\n // Mark loading succeeded.\n if (getQueryValue(queryKey)?.loading) {\n const now = Date.now();\n\n setQueryValue(queryKey, {\n data,\n loading: false,\n lastUpdatedMs: now,\n dataUpdatedMs: now,\n });\n }\n\n return data;\n } catch (e) {\n // Mark loading errored.\n if (getQueryValue(queryKey)?.loading) {\n const now = Date.now();\n\n setQueryValue(queryKey, {\n loading: false,\n lastUpdatedMs: now,\n error: e,\n errorUpdatedMs: now,\n });\n }\n\n throw e;\n }\n }\n\n /**\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 */\n async function waitForStateInit(queryKey: string) {\n const startMs = Date.now();\n await queryState.readyPromise;\n const waitedMs = Date.now() - startMs;\n\n if (waitedMs) {\n log(`${queryKey}: Waited for ${waitedMs}ms before data is ready`);\n }\n }\n\n /**\n * If the cached data is updated, don't fetch it. Ignore cached errors.\n * Throw an error if fetch fails.\n */\n async function getCachedOrFetch(\n queryKey: string,\n ...args: TArgs\n ): Promise<TData> {\n await waitForStateInit(queryKey);\n const cached = getQueryValue(queryKey);\n\n if (\n cached?.data !== undefined &&\n cached.dataUpdatedMs &&\n !isExpired(cached.dataUpdatedMs)\n ) {\n // Return cached value if not stale.\n if (!isStale(cached.dataUpdatedMs)) {\n log(`Return fresh data ${queryKey} from cache without fetching`);\n return cached.data;\n }\n\n // If stale, update the data in the background.\n void dedupedFetch(queryKey, \"query\", ...args);\n\n // Still return stale data immediately.\n log(`Returning stale data for ${queryKey}`);\n return cached.data;\n }\n\n return await dedupedFetch(queryKey, \"query\", ...args);\n }\n\n /**\n * If the cached data or error is updated, then don't attempt a new fetch.\n * Never throws an error.\n */\n async function getCachedStateOrFetch(\n queryKey: string,\n ...args: TArgs\n ): Promise<QueryStateValue<TData>> {\n await waitForStateInit(queryKey);\n const cached = getQueryValue(queryKey);\n\n if (\n cached?.data !== undefined &&\n cached.dataUpdatedMs &&\n !isExpired(cached.dataUpdatedMs)\n ) {\n // Return cached value if not stale.\n if (!isStale(cached.dataUpdatedMs)) {\n log(`Return fresh data ${queryKey} from cache without fetching`);\n return cached;\n }\n\n // If stale, update the data in the background.\n void dedupedFetch(queryKey, \"query\", ...args);\n\n // Still return stale data immediately.\n log(`Returning stale data for ${queryKey}`);\n return cached;\n }\n\n if (\n cached?.error !== undefined &&\n cached.errorUpdatedMs &&\n !isExpired(cached.errorUpdatedMs)\n ) {\n // Return cached value if not stale.\n if (!isStale(cached.errorUpdatedMs)) {\n log(`Return fresh error ${queryKey} from cache without fetching`);\n return cached;\n }\n\n // If stale, update the data in the background.\n void dedupedFetch(queryKey, \"query\", ...args);\n\n // Still return stale data immediately.\n log(`Returning stale error for ${queryKey}`);\n return cached;\n }\n\n try {\n await dedupedFetch(queryKey, \"query\", ...args);\n } catch {\n // ignore errors which will be saved in `state.error`.\n }\n\n return getQueryValue(queryKey) ?? DEFAULT_QUERY_STATE_ENTRY;\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 return wrapLoading(queryKey, () => {\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\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 function fetch(queryKey: string, ...args: TArgs): Promise<TData> {\n return await dedupedFetch(queryKey, \"query\", ...args);\n }\n\n /**\n * Perform an explicit refetch. If `options.refetchFn` was not specified,\n * then this would be identical to calling `query.fetch()`.\n */\n async function refetch(queryKey: string, ...args: TArgs): Promise<TData> {\n return await dedupedFetch(queryKey, \"refetch\", ...args);\n }\n\n /** Delete all currently cached data & all inflight promises. */\n function 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 function mount(queryKey: string): void {\n mountedKeys.set(queryKey, (mountedKeys.get(queryKey) ?? 0) + 1);\n }\n\n /** Clean up mounted keys. */\n function 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 return {\n queryName,\n queryFn,\n refetchFn,\n expiryMs,\n staleMs,\n maxSize,\n maxBytes,\n keepLastNonEmptyData,\n log,\n queryState,\n getQueryKey,\n getAbortController,\n getAbortControllerByKey,\n getAbortSignal,\n getAbortSignalByKey,\n isStale,\n isExpired,\n enforceSizeLimit,\n getQueryValue,\n setQueryValue,\n getData,\n setData,\n deleteData,\n getCachedOrFetch,\n getCachedStateOrFetch,\n fetch,\n refetch,\n clear,\n mount,\n unmount,\n } as const;\n}\n\nexport type SharedQuery<TArgs extends unknown[], TData> = ReturnType<\n typeof sharedQuery2<TArgs, TData>\n>;\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 return sharedQuery1(options) as 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] = useSharedState(query.queryState);\n\n const isMounted = useRef(true);\n const initQueryKeyDone = useRef(\"\");\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 // 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 // eslint-disable-next-line react-hooks/set-state-in-effect\n setLastNonEmptyData(queryStateValue.data);\n }\n }, [query.keepLastNonEmptyData, queryStateValue.data]);\n\n // Track mounted state.\n useEffect(() => {\n isMounted.current = true;\n query.mount(queryKey);\n\n return () => {\n isMounted.current = false;\n query.unmount(queryKey);\n };\n }, [query, queryKey]);\n\n // Initial fetch.\n useEffect(() => {\n // No need to init the same queryKey more than once.\n if (initQueryKeyDone.current === queryKey) return;\n initQueryKeyDone.current = queryKey;\n\n void query.getCachedStateOrFetch(queryKey, ...stableArgs);\n }, [query, queryKey, stableArgs]);\n\n return useMemo(() => {\n return {\n ...queryStateValue,\n\n // Fill with lastNonEmptyData when data is undefined.\n data:\n queryStateValue.data !== undefined\n ? queryStateValue.data\n : lastNonEmptyData,\n\n latestData: queryStateValue.data,\n\n // This works only if the user-given queryFn supports abort. If not,\n // this function doesn't do anything, since we don't have any means to\n // abort the running queryFn.\n abortCurrentQuery: (reason?: unknown): boolean => {\n const controller = query.getAbortControllerByKey(queryKey);\n controller?.abort(reason);\n return Boolean(controller);\n },\n getCachedOrFetch: async (): Promise<TData> => {\n return await query.getCachedOrFetch(queryKey, ...stableArgs);\n },\n getCachedStateOrFetch: async (): Promise<QueryStateValue<TData>> => {\n return await query.getCachedStateOrFetch(queryKey, ...stableArgs);\n },\n refetch: (): Promise<TData | undefined> => {\n return query.refetch(queryKey, ...stableArgs);\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 }, [lastNonEmptyData, query, queryKey, queryStateValue, stableArgs]);\n}\n"]}
@@ -0,0 +1,2 @@
1
+ import{c as B,d as W}from"./chunk-25NYDKKO.min.js";import{a as _}from"./chunk-TDP547RX.min.js";import{a as I}from"./chunk-MBPSSROD.min.js";import{http as ut,retries as dt}from"@choksheak/ts-utils/http";import k from"object-sizeof";import{useEffect as V,useMemo as X,useRef as Y,useState as ct}from"react";import lt from"xxhashjs";var H=typeof window<"u",A={staleMs:0,expiryMs:30*864e5,persistTo:void 0,maxSize:100,maxBytes:1e5,log:(...r)=>{H&&console.log("[sharedQuery]",...r)}};function At(r){Object.assign(A,r)}var ft=new Map;function gt(){if(H){let r=window.globalThis;return r.reactUtilsQueryRegistry??=new Map}return ft}function Tt(r){let m=gt(),s=r.queryName||r.url,c=m.get(s);if(c)return c;let l=pt(r);return m.set(s,l),l}function pt(r){let m=r.url||"",s=r.queryName||m;if(!s)throw new Error("Either queryName or url must be specified.");let c=new Map,l=new Map,D;if(r.queryFn)D=r.queryFn;else if(m){let t=ut.use(dt());D=async(...a)=>{let e="",n=a?.[0];if(n){if(typeof n=="string")e=n;else if(typeof n=="object"){let d=[];for(let[C,M]of Object.entries(n))if(M!==void 0)if(Array.isArray(M))for(let x of M)x!==void 0&&d.push([C,String(x)]);else d.push([C,String(M)]);e=new URLSearchParams(d).toString()}e&&(m.includes("?")?e="&"+e:e="?"+e)}return await(await t.get(m+e)).json()}}else throw new Error("Either queryFn or url must be specified");let o=r.refetchFn,f=r?.staleMs??A.staleMs,S=r?.expiryMs??A.expiryMs,b=r.store;if(b){if("persistTo"in b&&b.isValid){let t=b.isValid;r={...r,store:{...b,isValid:a=>{if(!a||typeof a!="object")return!1;for(let e of Object.values(a))if(!e||typeof e!="object"||!("data"in e)||!("dataUpdatedMs"in e)||!("lastUpdatedMs"in e)||typeof e.dataUpdatedMs!="number"||typeof e.lastUpdatedMs!="number"||!t(e.data))return!1;return!0}}}}}else{let t=r.persistTo??A.persistTo;t&&(r={...r,store:{persistTo:t,key:s,expiryMs:S}})}let T=r?.maxSize??A.maxSize,g=r?.maxBytes??A.maxBytes,G=!!r?.keepLastNonEmptyData,i=r?.log??A.log;r.name=s;let h=B({},r);function P(t){let a=_(t);return a.length>30&&(a=lt.h32(a,39406).toString(16).slice(2)),s+":"+a}function R(t){return c.get(t)?.abortController??null}function J(t){let a=P(t);return R(a)}function Z(t){let a=P(t);return v(a)}function v(t){return R(t)?.signal??null}function F(t){return f===0?!0:Date.now()-t>f}function U(t){return S===0?!1:Date.now()-t>S}function L(){if(!T&&!g)return;let t=h.getSnapshot(),a=Object.keys(t).length;if(a<=1)return;let e=k(t),n=T?a-T:0,y=g?e-g:0;if(n<=0&&y<=0){i(`No need to trim data for ${s}: size=${a} (limit=${T}), byteSize=${e.toLocaleString()} (limit=${g.toLocaleString()})`);return}i(`Need to trim ${s}: numToCut=${n}, bytesToCut=${y}`);let d={...t},C=!1,M=(u,p)=>{if(l.has(u))return i(`Cannot clean up ${u} as it is mounted`),!1;let j=k(u)+k(d[u]);return i(`Cleaning up unmounted${p?", expired":""} ${u} (byteSize=${j.toLocaleString()})`),n--,y-=j,delete d[u],C=!0,!0},x=Object.fromEntries(Object.entries(d).sort((u,p)=>u[1].lastUpdatedMs-p[1].lastUpdatedMs));for(let u of Object.keys(x)){let p=d[u];if(!(!p?.lastUpdatedMs||p?.loading)){if(p?.lastUpdatedMs&&!U(p.lastUpdatedMs))break;M(u,!0)&&delete x[u]}}if(n>0||y>0)for(let u of Object.keys(x)){let p=d[u];if(!(!p?.lastUpdatedMs||p?.loading)&&(M(u,!1),n<=0&&y<=0))break}if(!C){i(`No trimmable entries found for ${s}: size=${a} (limit=${T}), byteSize=${e.toLocaleString()} (limit=${g.toLocaleString()})`);return}h.setValue(d),i(`Trimmed data for ${s}: size=[${a} -> ${Object.keys(d).length}] (limit=${T}), byteSize=[${e.toLocaleString()} -> ${k(d).toLocaleString()}] (limit=${g.toLocaleString()})`)}function w(t){return h.getSnapshot()?.[t]}function $(t,a){h.setValue(e=>{let n=typeof a=="function"?a(e[t]??O):a;return{...e,[t]:n}}),L()}function K(t){let a=w(t);if(a?.dataUpdatedMs&&U(a.dataUpdatedMs)){N(t);return}return a?.data}function E(t,a,e=Date.now()){$(t,{lastUpdatedMs:Date.now(),loading:!1,data:a,dataUpdatedMs:e})}function N(t){let e={...h.getSnapshot()};delete e[t],h.setValue(e);let n=c.get(t);n&&(c.delete(t),n.abortController.abort("Deleted"))}async function q(t,a){$(t,O);try{let e=await a();if(w(t)?.loading){let n=Date.now();$(t,{data:e,loading:!1,lastUpdatedMs:n,dataUpdatedMs:n})}return e}catch(e){if(w(t)?.loading){let n=Date.now();$(t,{loading:!1,lastUpdatedMs:n,error:e,errorUpdatedMs:n})}throw e}}async function z(t){let a=Date.now();await h.readyPromise;let e=Date.now()-a;e&&i(`${t}: Waited for ${e}ms before data is ready`)}async function tt(t,...a){await z(t);let e=w(t);return e?.data!==void 0&&e.dataUpdatedMs&&!U(e.dataUpdatedMs)?F(e.dataUpdatedMs)?(Q(t,"query",...a),i(`Returning stale data for ${t}`),e.data):(i(`Return fresh data ${t} from cache without fetching`),e.data):await Q(t,"query",...a)}async function et(t,...a){await z(t);let e=w(t);if(e?.data!==void 0&&e.dataUpdatedMs&&!U(e.dataUpdatedMs))return F(e.dataUpdatedMs)?(Q(t,"query",...a),i(`Returning stale data for ${t}`),e):(i(`Return fresh data ${t} from cache without fetching`),e);if(e?.error!==void 0&&e.errorUpdatedMs&&!U(e.errorUpdatedMs))return F(e.errorUpdatedMs)?(Q(t,"query",...a),i(`Returning stale error for ${t}`),e):(i(`Return fresh error ${t} from cache without fetching`),e);try{await Q(t,"query",...a)}catch{}return w(t)??O}async function at(t,a,e){i(`Start fetching ${t}, queryType=${a}`);try{let n=await(a==="refetch"&&o?o(...e):D(...e));return i(`Successfully fetched ${t}`),E(t,n,Date.now()),n}catch(n){throw i(`Failed to fetch ${t} for queryType=${a}: ${n}`),n}finally{c.delete(t)}}function Q(t,a,...e){let n=c.get(t);return n?(i(`Deduplicating inflight fetch for ${t}, queryType=${a}`),n.promise):q(t,()=>{let y=at(t,a,e),d=new AbortController;return c.set(t,{promise:y,abortController:d}),y})}async function rt(t,...a){return await Q(t,"query",...a)}async function nt(t,...a){return await Q(t,"refetch",...a)}function st(){h.delete();for(let{abortController:t}of Array.from(c.values()))t.abort("Cleared");c.clear()}function ot(t){l.set(t,(l.get(t)??0)+1)}function it(t){let a=l.get(t);a===1?l.delete(t):l.set(t,(a??0)-1)}return{queryName:s,queryFn:D,refetchFn:o,expiryMs:S,staleMs:f,maxSize:T,maxBytes:g,keepLastNonEmptyData:G,log:i,queryState:h,getQueryKey:P,getAbortController:J,getAbortControllerByKey:R,getAbortSignal:Z,getAbortSignalByKey:v,isStale:F,isExpired:U,enforceSizeLimit:L,getQueryValue:w,setQueryValue:$,getData:K,setData:E,deleteData:N,getCachedOrFetch:tt,getCachedStateOrFetch:et,fetch:rt,refetch:nt,clear:st,mount:ot,unmount:it}}function Ut(r){return Tt(r)}var O={lastUpdatedMs:0,loading:!0};function xt(r,...m){let s=I(m),[c]=W(r.queryState),l=Y(!0),D=Y(""),o=X(()=>r.getQueryKey(s),[r,s]),f=c[o]??O,[S,b]=ct();return V(()=>{r.keepLastNonEmptyData&&f.data!==void 0&&b(f.data)},[r.keepLastNonEmptyData,f.data]),V(()=>(l.current=!0,r.mount(o),()=>{l.current=!1,r.unmount(o)}),[r,o]),V(()=>{D.current!==o&&(D.current=o,r.getCachedStateOrFetch(o,...s))},[r,o,s]),X(()=>({...f,data:f.data!==void 0?f.data:S,latestData:f.data,abortCurrentQuery:T=>{let g=r.getAbortControllerByKey(o);return g?.abort(T),!!g},getCachedOrFetch:async()=>await r.getCachedOrFetch(o,...s),getCachedStateOrFetch:async()=>await r.getCachedStateOrFetch(o,...s),refetch:()=>r.refetch(o,...s),setData:(T,g)=>{r.setData(o,T,g)},deleteData:()=>{r.deleteData(o)}}),[S,r,o,f,s])}export{A as a,At as b,Ut as c,xt as d};
2
+ //# sourceMappingURL=chunk-JHM37XTC.min.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sharedQuery.ts"],"sourcesContent":["/**\n * @packageDocumentation\n *\n * The \"shared query\" is a data fetching convenience library that helps users\n * to load data, check for staleness and expiration, cache in memory, local\n * storage or indexed db, and be able to manipulate the data anytime.\n *\n * Each shared query creates a new private shared state that handles the data\n * storage. This shared state contains the records for all queries, where each\n * query is a unique set of arguments used to make the query. The number of\n * records and the total byte size of data can be limited so as to ensure the\n * query does not take up too much memory.\n *\n * Think of shared queries as like the open source npm package \"react-query\",\n * but with strong type-checking and built-in support for persistence.\n *\n * Example:\n * ```\n * import { sharedQuery, useSharedQuery } from \"@choksheak/react-utils/sharedQuery\";\n * import { MS_PER_DAY } from \"@choksheak/ts-utils/timeConstants\";\n *\n * // Create a new shared query in the top level scope.\n * // The query function `queryFn` here does not take any arguments.\n * //\n * // Only the queryName and queryFn are required and everything else is\n * // optional.\n * //\n * // The queryName must be unique, otherwise we will throw an\n * // error. This is because defining the same queryName for two different\n * // queries implies that the queries are the same, thus violating the idea\n * // of a \"shared\" query, and almost certainly points to a bug in the code.\n * export const usersQuery = sharedQuery({\n * queryName: \"users\",\n * queryFn: listUsers,\n * persistTo: \"indexedDb\",\n * staleMs: MS_PER_DAY,\n * });\n *\n * // Example of using a query function with arguments.\n * export const getUserQuery = sharedQuery({\n * queryName: \"getUser\",\n * queryFn: getUser, // getUser(userId: string) => User\n * persistTo: \"indexedDb\",\n * staleMs: MS_PER_DAY,\n * });\n *\n * export const UsersComponent: React.FC = () => {\n * // Second argument is optional, and is an array of arguments to be\n * // passed to the `queryFn`.\n * const users = useSharedQuery(usersQuery);\n *\n * return (\n * <>\n * <h1>List of Users</h1>\n *\n * <button onclick={users.refetch}>Refresh</button>\n *\n * {users.loading && <Loader />}\n *\n * {users.error && !users.data && <p>Error: {String(users.error)}</p>}\n *\n * <p>Data: {users.data ? JSON.stringify(users.data) : \"-\"}</p>\n * </>\n * );\n * };\n *\n * export const AnotherComponent: React.FC = () => {\n * // This query will be deduped with the one above, sharing the same\n * // data and the same fetches.\n * const users = useSharedQuery(usersQuery);\n * ...\n * };\n *\n * export const DisplayOneUser: React.FC<{userId: string}> = ({userId}) => {\n * // Example of using the query with an argument. Whenever the argument\n * // changes, the data will be fetched and updated automatically.\n * const user = useSharedQuery(getUserQuery, userId);\n *\n * // You can check for user.loading and user.error if needed.\n *\n * return <>User: {user.data ? JSON.stringify(user.data) : \"-\"}</>;\n * };\n * ```\n */\n\nimport { http, retries } from \"@choksheak/ts-utils/http\";\nimport getByteSize from \"object-sizeof\";\nimport { SetStateAction, useEffect, useMemo, useRef, useState } from \"react\";\nimport XXH from \"xxhashjs\";\n\nimport { sharedState, SharedStateOptions, useSharedState } from \"./sharedState\";\nimport { PersistTo } from \"./storage\";\nimport { stringifyDeterministicForKeys } from \"./stringify\";\nimport { useDeepMemo } from \"./useDeepMemo\";\n\nconst hasWindow = typeof window !== \"undefined\";\n\n/** Type of the queryFn used to fetch the data from source. */\nexport type QueryFn<TArgs extends unknown[], TData> = (\n ...args: TArgs\n) => Promise<TData> | TData;\n\n/** An entry for a persisted state corresponding to one query key. */\nexport type QueryStateValue<TData> = Readonly<{\n /**\n * When this record was last updated. Used for LRU determination. If you want\n * the data age, please use `dataUpdatedMs` instead.\n */\n lastUpdatedMs: number;\n\n loading: boolean;\n data?: TData;\n dataUpdatedMs?: number;\n\n /**\n * Use unknown type for error because we can't assume what type of variable\n * will be thrown from the query function. We also don't want to add this\n * type to QueryStateValue<TError> as it adds complexity to the type.\n */\n error?: unknown;\n\n errorUpdatedMs?: number;\n}>;\n\n/** Return type for useSharedQuery(). */\nexport type UseQueryResult<TData> = QueryStateValue<TData> &\n Readonly<{\n /**\n * Usually same as `data`, but if you specify `keepLastNonEmptyData: true`,\n * then this will contain the latest data (undefined when args just changed)\n * and data will contain the last non empty data.\n */\n latestData: TData | undefined;\n\n /**\n * Cancels the inflight query (if any). Returns true if canceled.\n */\n abortCurrentQuery: (reason?: unknown) => boolean;\n\n /**\n * Let the caller get the data immediately no matter what. Throws an error\n * if the fetch fails.\n */\n getCachedOrFetch: () => Promise<TData>;\n\n /**\n * Get the cached state or do a new fetch, including errors. Never throws\n * an error.\n */\n getCachedStateOrFetch: () => Promise<QueryStateValue<TData>>;\n\n /**\n * Manually refresh the data by doing a new query.\n */\n refetch: () => Promise<TData | undefined>;\n\n /**\n * If you got the data from somewhere else (e.g. an in-memory data update),\n * just set it directly.\n */\n setData: (data: TData, dataUpdatedMs?: number) => void;\n\n deleteData: () => void;\n }>;\n\n/**\n * The full query state is a record of all the fetch states mapped by\n * query key to the state entry.\n */\nexport type SharedQueryState<TData> = Record<string, QueryStateValue<TData>>;\n\nexport type QueryTargetWithFn<TArgs extends unknown[], TData> = {\n /**\n * The name could have been auto-generated, but we let the user give us a\n * human-readable name that can be identified quickly in the logs.\n * Note that the queryKey is a string formed by the queryName plus the query\n * arguments.\n */\n queryName: string;\n\n /** Function to fetch data. */\n queryFn: QueryFn<TArgs, TData>;\n};\n\nexport type QueryTargetWithUrl = {\n /**\n * The queryName can optionally be specified, else we will fallback to use\n * the url as the queryName.\n */\n queryName?: string;\n\n /**\n * URL to fetch data using HTTP GET. Note that using this field will cause\n * both TArgs and TData to be undefined (unknown). Therefore you can make up\n * for this by specifying types with useSharedQuery, like this:\n * ```\n * const usersQuery = useSharedQuery<[{ userId?: string[] }], User[]>({\n * url: \"/api/list-users\",\n * });\n * ```\n *\n * If `userId` were specified as [\"u1\", \"u2\"], the query url would be\n * \"/api/list-users?userId=u1&userId=u2\".\n *\n * You can also pass the params as a string like \"userId=u1&userId=u2\".\n * ```\n * const usersQuery = useSharedQuery<[string], User[]>({\n * url: \"/api/list-users\",\n * });\n * ```\n */\n url: string;\n};\n\n/** Specifies how to fetch the data. */\nexport type QueryTarget<TArgs extends unknown[], TData> =\n | QueryTargetWithFn<TArgs, TData>\n | QueryTargetWithUrl;\n\n/** Options to configure a shared query. */\nexport type SharedQueryOptions<TArgs extends unknown[], TData> = QueryTarget<\n TArgs,\n TData\n> & {\n /**\n * Function to re-fetch data. Usually this would just be the `queryFn`, but\n * if you need a different behavior for refetching, then specify this.\n */\n refetchFn?: QueryFn<TArgs, TData>;\n\n /**\n * ms before data considered stale; 0 means always stale.\n */\n staleMs?: number;\n\n /**\n * ms before data is removed from cache; 0 means never expire.\n */\n expiryMs?: number;\n\n /**\n * 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 /** Customize the logger. */\n log?: (...args: unknown[]) => void;\n} & SharedStateOptions<SharedQueryState<TData>>;\n\n/** Users can override these values globally. */\nexport const SharedQueryConfig = {\n /** Refetch on every page load. */\n staleMs: 0,\n\n /** Keep in cache for a long time. */\n expiryMs: 30 * 86_400_000 /* MS_PER_DAY */,\n\n /** 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[]) => {\n if (!hasWindow) return; // don't log in the SSR server side\n console.log(\"[sharedQuery]\", ...args);\n },\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\nconst moduleQueryRegistry = new Map<string, unknown>();\n\n/**\n * When bundlers duplicate this module across chunks, each copy would otherwise\n * create a separate queryState (different pubSub keys in the same store).\n * Return the same SharedQuery for a given queryName app-wide.\n */\nfunction getOrCreateQueryRegistry(): Map<string, unknown> {\n if (hasWindow) {\n const g = window.globalThis as {\n reactUtilsQueryRegistry?: Map<string, unknown>;\n };\n return (g.reactUtilsQueryRegistry ??= new Map());\n }\n return moduleQueryRegistry;\n}\n\n// Split out function to auto-derive the return type.\nfunction sharedQuery1<TArgs extends unknown[], TData>(\n options: SharedQueryOptions<TArgs, TData>,\n) {\n const registry = getOrCreateQueryRegistry();\n const key = options.queryName || (options as QueryTargetWithUrl).url;\n\n const existing = registry.get(key);\n if (existing) {\n return existing;\n }\n\n const obj = sharedQuery2(options);\n registry.set(key, obj);\n return obj;\n}\n\n// Split out function to auto-derive the return type.\nfunction sharedQuery2<TArgs extends unknown[], TData>(\n // The name could have been auto-generated, but we let the user give us a\n // human-readable name that can be identified quickly in the logs.\n options: SharedQueryOptions<TArgs, TData>,\n) {\n // If queryName is not specified, fallback to use url as the queryName.\n const url = (options as QueryTargetWithUrl).url || \"\";\n const queryName = options.queryName || url;\n\n if (!queryName) {\n throw new Error(`Either queryName or url must be specified.`);\n }\n\n const inflightQueries = new Map<\n string,\n { promise: Promise<TData>; abortController: AbortController }\n >();\n\n /**\n * Keep track of the mounted query keys. This handles the case where an\n * earlier-started query returns later, and when it returns, it was already\n * unmounted, thus making it eligible for clean up right after fetch.\n */\n const mountedKeys = new Map<string, number>();\n\n let queryFn: QueryFn<TArgs, TData>;\n\n if ((options as QueryTargetWithFn<TArgs, TData>).queryFn) {\n queryFn = (options as QueryTargetWithFn<TArgs, TData>).queryFn;\n } else if (url) {\n const fetcher = http.use(retries());\n\n // Auto-generate the queryFn as a HTTP GET request using the url.\n queryFn = async (...args: TArgs): Promise<TData> => {\n // If args are given, treat the first argument as a search param.\n let params = \"\";\n const firstArg = args?.[0];\n\n if (firstArg) {\n if (typeof firstArg === \"string\") {\n params = firstArg;\n } else if (typeof firstArg === \"object\") {\n const entries: [string, string][] = [];\n\n for (const [k, v] of Object.entries(firstArg)) {\n if (v === undefined) continue;\n\n // Allow the same param to be specified multiple times.\n if (Array.isArray(v)) {\n for (const item of v) {\n if (item === undefined) continue;\n\n // The field values can only be primitives, not objects.\n entries.push([k, String(item)]);\n }\n } else {\n // The field values can only be primitives, not objects.\n entries.push([k, String(v)]);\n }\n }\n\n params = new URLSearchParams(entries).toString();\n }\n\n if (params) {\n if (url.includes(\"?\")) {\n // Append more search params to existing ones.\n params = \"&\" + params;\n } else {\n // Set the full search params string.\n params = \"?\" + params;\n }\n }\n }\n\n const response = await fetcher.get(url + params);\n\n return await response.json();\n };\n } else {\n throw new Error(`Either queryFn or url must be specified`);\n }\n\n const refetchFn = options.refetchFn;\n\n const staleMs = options?.staleMs ?? SharedQueryConfig.staleMs;\n const expiryMs = options?.expiryMs ?? SharedQueryConfig.expiryMs;\n\n // 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. Treats cached errors as invalid.\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 // Debug logging.\n options.name = queryName;\n\n const queryState = sharedState<SharedQueryState<TData>>(\n {}, // defaultValue\n options,\n );\n\n /** Returns a key to identify requests based on the given args. */\n function getQueryKey(args: TArgs): string {\n let s = stringifyDeterministicForKeys(args);\n\n // Ensure the query key is not too long.\n if (s.length > 30) {\n s = XXH.h32(s, 0x99ee).toString(16).slice(2); // remove \"0x\"\n }\n\n return queryName + \":\" + s;\n }\n\n /**\n * Get the AbortController to abort the inflight query (if any).\n *\n * Note that once you get the controller, you can also get the signal easily\n * using `controller.signal`. So if you need both the controller and the\n * signal, just use this function.\n */\n function getAbortControllerByKey(queryKey: string): AbortController | null {\n return inflightQueries.get(queryKey)?.abortController ?? null;\n }\n\n function getAbortController(args: TArgs): AbortController | null {\n const queryKey = getQueryKey(args);\n return getAbortControllerByKey(queryKey);\n }\n\n /**\n * Get the AbortSignal to check for query abortions.\n *\n * Example using fetch:\n * ```\n * const getUserQuery = sharedQuery({\n * queryName: \"getUser\",\n * queryFn: (userId: string) => {\n * // Get the signal for this current execution.\n * const signal = getUserQuery.getAbortSignal([userId]);\n *\n * // Pass the signal to fetch so that it can be aborted.\n * const response = await fetch(`/users/${userId}`, { signal });\n *\n * // Check for errors.\n * if (!response.ok) {\n * throw new Error(response.statusText);\n * }\n *\n * // Return the data.\n * return await response.json();\n * },\n * });\n * ```\n *\n * Example using `http.ts`:\n * ```\n * const getUserQuery = sharedQuery({\n * queryName: \"getUser\",\n * queryFn: (userId: string) => {\n * // Get the signal for this current execution.\n * const signal = getUserQuery.getAbortSignal([userId]);\n *\n * // Pass the signal to fetch so that it can be aborted.\n * const response = await http.get(`/users/${userId}`, { signal });\n * return await response.json();\n * },\n * });\n * ```\n */\n function getAbortSignal(args: TArgs): AbortSignal | null {\n const queryKey = getQueryKey(args);\n return getAbortSignalByKey(queryKey);\n }\n\n function getAbortSignalByKey(queryKey: string): AbortSignal | null {\n return getAbortControllerByKey(queryKey)?.signal ?? null;\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 /**\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 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): 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 false;\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 return true;\n };\n\n // lastUpdatedMs refers to either the dataUpdatedMs or the errorUpdatedMs.\n const entriesByLastUpdatedMsAsc = Object.fromEntries(\n Object.entries(newState).sort(\n (entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs,\n ),\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 Object.keys(entriesByLastUpdatedMsAsc)) {\n const state = newState[key];\n\n // Skip current fetches.\n if (!state?.lastUpdatedMs || state?.loading) {\n continue;\n }\n\n // Anything after this is not expired yet.\n if (state?.lastUpdatedMs && !isExpired(state.lastUpdatedMs)) {\n break;\n }\n\n if (deleteKeyIfNotMounted(key, true)) {\n delete entriesByLastUpdatedMsAsc[key];\n }\n }\n\n if (numToCut > 0 || bytesToCut > 0) {\n // Clean up starting from oldest entry first.\n for (const key of Object.keys(entriesByLastUpdatedMsAsc)) {\n const state = newState[key];\n\n // Skip current fetches.\n if (!state?.lastUpdatedMs || state?.loading) {\n continue;\n }\n\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 getQueryValue(queryKey: string): QueryStateValue<TData> | undefined {\n return queryState.getSnapshot()?.[queryKey];\n }\n\n /** Set the value of one entry inside the query state map. */\n function setQueryValue(\n queryKey: string,\n next: SetStateAction<QueryStateValue<TData>>,\n ) {\n queryState.setValue((prev) => {\n const value =\n typeof next === \"function\"\n ? next(prev[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY)\n : next;\n return { ...prev, [queryKey]: value };\n });\n\n enforceSizeLimit();\n }\n\n /** Get the current stored data for a query key. */\n function getData(queryKey: string): TData | undefined {\n const stateValue = getQueryValue(queryKey);\n\n // Check for expiration.\n if (stateValue?.dataUpdatedMs) {\n if (isExpired(stateValue.dataUpdatedMs)) {\n deleteData(queryKey);\n return undefined;\n }\n }\n\n return stateValue?.data;\n }\n\n /** Set the data of one entry inside the query state map. */\n function setData(\n queryKey: string,\n data: TData,\n dataUpdatedMs = Date.now(),\n ): void {\n setQueryValue(queryKey, {\n lastUpdatedMs: Date.now(),\n loading: false,\n data,\n dataUpdatedMs,\n });\n }\n\n /** Delete the cached data for one set of arguments. */\n function 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 async function wrapLoading(\n queryKey: string,\n fetchData: () => Promise<TData>,\n ): Promise<TData> {\n // Mark loading started and delete the old data + error.\n setQueryValue(queryKey, DEFAULT_QUERY_STATE_ENTRY);\n\n try {\n const data = await fetchData();\n\n // Mark loading succeeded.\n if (getQueryValue(queryKey)?.loading) {\n const now = Date.now();\n\n setQueryValue(queryKey, {\n data,\n loading: false,\n lastUpdatedMs: now,\n dataUpdatedMs: now,\n });\n }\n\n return data;\n } catch (e) {\n // Mark loading errored.\n if (getQueryValue(queryKey)?.loading) {\n const now = Date.now();\n\n setQueryValue(queryKey, {\n loading: false,\n lastUpdatedMs: now,\n error: e,\n errorUpdatedMs: now,\n });\n }\n\n throw e;\n }\n }\n\n /**\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 */\n async function waitForStateInit(queryKey: string) {\n const startMs = Date.now();\n await queryState.readyPromise;\n const waitedMs = Date.now() - startMs;\n\n if (waitedMs) {\n log(`${queryKey}: Waited for ${waitedMs}ms before data is ready`);\n }\n }\n\n /**\n * If the cached data is updated, don't fetch it. Ignore cached errors.\n * Throw an error if fetch fails.\n */\n async function getCachedOrFetch(\n queryKey: string,\n ...args: TArgs\n ): Promise<TData> {\n await waitForStateInit(queryKey);\n const cached = getQueryValue(queryKey);\n\n if (\n cached?.data !== undefined &&\n cached.dataUpdatedMs &&\n !isExpired(cached.dataUpdatedMs)\n ) {\n // Return cached value if not stale.\n if (!isStale(cached.dataUpdatedMs)) {\n log(`Return fresh data ${queryKey} from cache without fetching`);\n return cached.data;\n }\n\n // If stale, update the data in the background.\n void dedupedFetch(queryKey, \"query\", ...args);\n\n // Still return stale data immediately.\n log(`Returning stale data for ${queryKey}`);\n return cached.data;\n }\n\n return await dedupedFetch(queryKey, \"query\", ...args);\n }\n\n /**\n * If the cached data or error is updated, then don't attempt a new fetch.\n * Never throws an error.\n */\n async function getCachedStateOrFetch(\n queryKey: string,\n ...args: TArgs\n ): Promise<QueryStateValue<TData>> {\n await waitForStateInit(queryKey);\n const cached = getQueryValue(queryKey);\n\n if (\n cached?.data !== undefined &&\n cached.dataUpdatedMs &&\n !isExpired(cached.dataUpdatedMs)\n ) {\n // Return cached value if not stale.\n if (!isStale(cached.dataUpdatedMs)) {\n log(`Return fresh data ${queryKey} from cache without fetching`);\n return cached;\n }\n\n // If stale, update the data in the background.\n void dedupedFetch(queryKey, \"query\", ...args);\n\n // Still return stale data immediately.\n log(`Returning stale data for ${queryKey}`);\n return cached;\n }\n\n if (\n cached?.error !== undefined &&\n cached.errorUpdatedMs &&\n !isExpired(cached.errorUpdatedMs)\n ) {\n // Return cached value if not stale.\n if (!isStale(cached.errorUpdatedMs)) {\n log(`Return fresh error ${queryKey} from cache without fetching`);\n return cached;\n }\n\n // If stale, update the data in the background.\n void dedupedFetch(queryKey, \"query\", ...args);\n\n // Still return stale data immediately.\n log(`Returning stale error for ${queryKey}`);\n return cached;\n }\n\n try {\n await dedupedFetch(queryKey, \"query\", ...args);\n } catch {\n // ignore errors which will be saved in `state.error`.\n }\n\n return getQueryValue(queryKey) ?? DEFAULT_QUERY_STATE_ENTRY;\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 return wrapLoading(queryKey, () => {\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\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 function fetch(queryKey: string, ...args: TArgs): Promise<TData> {\n return await dedupedFetch(queryKey, \"query\", ...args);\n }\n\n /**\n * Perform an explicit refetch. If `options.refetchFn` was not specified,\n * then this would be identical to calling `query.fetch()`.\n */\n async function refetch(queryKey: string, ...args: TArgs): Promise<TData> {\n return await dedupedFetch(queryKey, \"refetch\", ...args);\n }\n\n /** Delete all currently cached data & all inflight promises. */\n function 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 function mount(queryKey: string): void {\n mountedKeys.set(queryKey, (mountedKeys.get(queryKey) ?? 0) + 1);\n }\n\n /** Clean up mounted keys. */\n function 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 return {\n queryName,\n queryFn,\n refetchFn,\n expiryMs,\n staleMs,\n maxSize,\n maxBytes,\n keepLastNonEmptyData,\n log,\n queryState,\n getQueryKey,\n getAbortController,\n getAbortControllerByKey,\n getAbortSignal,\n getAbortSignalByKey,\n isStale,\n isExpired,\n enforceSizeLimit,\n getQueryValue,\n setQueryValue,\n getData,\n setData,\n deleteData,\n getCachedOrFetch,\n getCachedStateOrFetch,\n fetch,\n refetch,\n clear,\n mount,\n unmount,\n } as const;\n}\n\nexport type SharedQuery<TArgs extends unknown[], TData> = ReturnType<\n typeof sharedQuery2<TArgs, TData>\n>;\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 return sharedQuery1(options) as 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] = useSharedState(query.queryState);\n\n const isMounted = useRef(true);\n const initQueryKeyDone = useRef(\"\");\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 // 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 // eslint-disable-next-line react-hooks/set-state-in-effect\n setLastNonEmptyData(queryStateValue.data);\n }\n }, [query.keepLastNonEmptyData, queryStateValue.data]);\n\n // Track mounted state.\n useEffect(() => {\n isMounted.current = true;\n query.mount(queryKey);\n\n return () => {\n isMounted.current = false;\n query.unmount(queryKey);\n };\n }, [query, queryKey]);\n\n // Initial fetch.\n useEffect(() => {\n // No need to init the same queryKey more than once.\n if (initQueryKeyDone.current === queryKey) return;\n initQueryKeyDone.current = queryKey;\n\n void query.getCachedStateOrFetch(queryKey, ...stableArgs);\n }, [query, queryKey, stableArgs]);\n\n return useMemo(() => {\n return {\n ...queryStateValue,\n\n // Fill with lastNonEmptyData when data is undefined.\n data:\n queryStateValue.data !== undefined\n ? queryStateValue.data\n : lastNonEmptyData,\n\n latestData: queryStateValue.data,\n\n // This works only if the user-given queryFn supports abort. If not,\n // this function doesn't do anything, since we don't have any means to\n // abort the running queryFn.\n abortCurrentQuery: (reason?: unknown): boolean => {\n const controller = query.getAbortControllerByKey(queryKey);\n controller?.abort(reason);\n return Boolean(controller);\n },\n getCachedOrFetch: async (): Promise<TData> => {\n return await query.getCachedOrFetch(queryKey, ...stableArgs);\n },\n getCachedStateOrFetch: async (): Promise<QueryStateValue<TData>> => {\n return await query.getCachedStateOrFetch(queryKey, ...stableArgs);\n },\n refetch: (): Promise<TData | undefined> => {\n return query.refetch(queryKey, ...stableArgs);\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 }, [lastNonEmptyData, query, queryKey, queryStateValue, stableArgs]);\n}\n"],"mappings":"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","names":["http","retries","getByteSize","useEffect","useMemo","useRef","useState","XXH","hasWindow","SharedQueryConfig","args","configureSharedQuery","config","moduleQueryRegistry","getOrCreateQueryRegistry","g","sharedQuery1","options","registry","key","existing","obj","sharedQuery2","url","queryName","inflightQueries","mountedKeys","queryFn","fetcher","http","retries","params","firstArg","entries","k","v","item","refetchFn","staleMs","expiryMs","store","isValid","u","persistTo","maxSize","maxBytes","keepLastNonEmptyData","log","queryState","sharedState","getQueryKey","s","stringifyDeterministicForKeys","XXH","getAbortControllerByKey","queryKey","getAbortController","getAbortSignal","getAbortSignalByKey","isStale","dataUpdatedMs","isExpired","enforceSizeLimit","oldState","oldSize","oldByteSize","getByteSize","numToCut","bytesToCut","newState","needUpdate","deleteKeyIfNotMounted","expired","byteSize","entriesByLastUpdatedMsAsc","entry1","entry2","state","getQueryValue","setQueryValue","next","prev","value","DEFAULT_QUERY_STATE_ENTRY","getData","stateValue","deleteData","setData","data","clone","inflight","wrapLoading","fetchData","now","waitForStateInit","startMs","waitedMs","getCachedOrFetch","cached","dedupedFetch","getCachedStateOrFetch","startFetching","queryType","e","inflightQuery","promise","abortController","fetch","refetch","clear","mount","unmount","count","sharedQuery","useSharedQuery","query","stableArgs","useDeepMemo","useSharedState","isMounted","useRef","initQueryKeyDone","useMemo","queryStateValue","lastNonEmptyData","setLastNonEmptyData","useState","useEffect","reason","controller"]}
@@ -112,10 +112,15 @@ function sharedState(defaultValue, options) {
112
112
  const lazyLoad = _optionalChain([options, 'optionalAccess', _3 => _3.lazyLoad]);
113
113
  const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateConfig.lazyLoad;
114
114
  function completeInit(stored) {
115
+ if (obj.initDone) {
116
+ return false;
117
+ }
115
118
  const value = stored !== void 0 ? stored : defaultValue;
116
119
  pubSubStore.set(pubSubKey, value);
120
+ obj.initStarted = true;
117
121
  obj.initDone = true;
118
122
  resolveReadyPromise();
123
+ return true;
119
124
  }
120
125
  const obj = {
121
126
  name: _optionalChain([options, 'optionalAccess', _4 => _4.name]),
@@ -168,27 +173,42 @@ function sharedState(defaultValue, options) {
168
173
  const prev = pubSubStore.get(pubSubKey);
169
174
  next = next(prev);
170
175
  }
171
- pubSubStore.set(pubSubKey, next);
172
- void obj.setInStore(next);
176
+ const updated = completeInit(next);
177
+ if (!updated) {
178
+ pubSubStore.set(pubSubKey, next);
179
+ }
180
+ return obj.setInStore(next);
173
181
  },
174
182
  /**
175
183
  * Remove the value of the shared state. Actually this just sets the value
176
184
  * back to the given default value.
177
185
  */
178
186
  delete() {
179
- pubSubStore.set(pubSubKey, defaultValue);
180
- return _optionalChain([storageAdapter, 'optionalAccess', _9 => _9.set, 'call', _10 => _10(STORAGE_KEY, defaultValue)]);
187
+ return obj.setValue(defaultValue);
181
188
  },
182
189
  /** Initial default handling. */
183
190
  initDefaultValueOnce() {
184
191
  if (obj.initDone || obj.initStarted) return;
185
192
  obj.initStarted = true;
186
- const maybePromise = obj.getFromStore();
187
- if (maybePromise instanceof Promise) {
188
- return maybePromise.then(completeInit);
189
- } else {
190
- const stored = maybePromise;
191
- completeInit(stored);
193
+ try {
194
+ const storedOrPromise = obj.getFromStore();
195
+ if (storedOrPromise instanceof Promise) {
196
+ storedOrPromise.then(completeInit).catch((e) => {
197
+ log(
198
+ `[ERROR] Failed to load from store${_optionalChain([options, 'optionalAccess', _9 => _9.name]) ? ` (${options.name})` : ""}:`,
199
+ e
200
+ );
201
+ completeInit(void 0);
202
+ });
203
+ } else {
204
+ completeInit(storedOrPromise);
205
+ }
206
+ } catch (e) {
207
+ log(
208
+ `[ERROR] Error while loading from store${_optionalChain([options, 'optionalAccess', _10 => _10.name]) ? ` (${options.name})` : ""}:`,
209
+ e
210
+ );
211
+ completeInit(void 0);
192
212
  }
193
213
  }
194
214
  };
@@ -112,10 +112,15 @@ function sharedState(defaultValue, options) {
112
112
  const lazyLoad = options?.lazyLoad;
113
113
  const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateConfig.lazyLoad;
114
114
  function completeInit(stored) {
115
+ if (obj.initDone) {
116
+ return false;
117
+ }
115
118
  const value = stored !== void 0 ? stored : defaultValue;
116
119
  pubSubStore.set(pubSubKey, value);
120
+ obj.initStarted = true;
117
121
  obj.initDone = true;
118
122
  resolveReadyPromise();
123
+ return true;
119
124
  }
120
125
  const obj = {
121
126
  name: options?.name,
@@ -168,27 +173,42 @@ function sharedState(defaultValue, options) {
168
173
  const prev = pubSubStore.get(pubSubKey);
169
174
  next = next(prev);
170
175
  }
171
- pubSubStore.set(pubSubKey, next);
172
- void obj.setInStore(next);
176
+ const updated = completeInit(next);
177
+ if (!updated) {
178
+ pubSubStore.set(pubSubKey, next);
179
+ }
180
+ return obj.setInStore(next);
173
181
  },
174
182
  /**
175
183
  * Remove the value of the shared state. Actually this just sets the value
176
184
  * back to the given default value.
177
185
  */
178
186
  delete() {
179
- pubSubStore.set(pubSubKey, defaultValue);
180
- return storageAdapter?.set(STORAGE_KEY, defaultValue);
187
+ return obj.setValue(defaultValue);
181
188
  },
182
189
  /** Initial default handling. */
183
190
  initDefaultValueOnce() {
184
191
  if (obj.initDone || obj.initStarted) return;
185
192
  obj.initStarted = true;
186
- const maybePromise = obj.getFromStore();
187
- if (maybePromise instanceof Promise) {
188
- return maybePromise.then(completeInit);
189
- } else {
190
- const stored = maybePromise;
191
- completeInit(stored);
193
+ try {
194
+ const storedOrPromise = obj.getFromStore();
195
+ if (storedOrPromise instanceof Promise) {
196
+ storedOrPromise.then(completeInit).catch((e) => {
197
+ log(
198
+ `[ERROR] Failed to load from store${options?.name ? ` (${options.name})` : ""}:`,
199
+ e
200
+ );
201
+ completeInit(void 0);
202
+ });
203
+ } else {
204
+ completeInit(storedOrPromise);
205
+ }
206
+ } catch (e) {
207
+ log(
208
+ `[ERROR] Error while loading from store${options?.name ? ` (${options.name})` : ""}:`,
209
+ e
210
+ );
211
+ completeInit(void 0);
192
212
  }
193
213
  }
194
214
  };
package/index.cjs CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
 
5
5
 
6
- var _chunk2VVAT5AAcjs = require('./chunk-2VVAT5AA.cjs');
6
+ var _chunk5WECMKSVcjs = require('./chunk-5WECMKSV.cjs');
7
7
 
8
8
 
9
9
 
@@ -11,7 +11,7 @@ var _chunk2VVAT5AAcjs = require('./chunk-2VVAT5AA.cjs');
11
11
 
12
12
 
13
13
 
14
- var _chunkKXFV42DPcjs = require('./chunk-KXFV42DP.cjs');
14
+ var _chunkYMQDZV47cjs = require('./chunk-YMQDZV47.cjs');
15
15
 
16
16
 
17
17
 
@@ -40,4 +40,4 @@ require('./chunk-IS3BZTWE.cjs');
40
40
 
41
41
 
42
42
 
43
- exports.SharedQueryConfig = _chunk2VVAT5AAcjs.SharedQueryConfig; exports.SharedStateConfig = _chunkKXFV42DPcjs.SharedStateConfig; exports.configureSharedQuery = _chunk2VVAT5AAcjs.configureSharedQuery; exports.configureSharedState = _chunkKXFV42DPcjs.configureSharedState; exports.getStorageAdapter = _chunk5PDGU37Ecjs.getStorageAdapter; exports.kvStoreAdapter = _chunk5PDGU37Ecjs.kvStoreAdapter; exports.localStoreAdapter = _chunk5PDGU37Ecjs.localStoreAdapter; exports.sharedQuery = _chunk2VVAT5AAcjs.sharedQuery; exports.sharedState = _chunkKXFV42DPcjs.sharedState; exports.stringifyDeterministicForKeys = _chunkCYTO5EZEcjs.stringifyDeterministicForKeys; exports.useDeepMemo = _chunkWXLUEDBOcjs.useDeepMemo; exports.useSharedQuery = _chunk2VVAT5AAcjs.useSharedQuery; exports.useSharedState = _chunkKXFV42DPcjs.useSharedState; exports.useSharedStateSetter = _chunkKXFV42DPcjs.useSharedStateSetter; exports.useSharedStateValue = _chunkKXFV42DPcjs.useSharedStateValue;
43
+ exports.SharedQueryConfig = _chunk5WECMKSVcjs.SharedQueryConfig; exports.SharedStateConfig = _chunkYMQDZV47cjs.SharedStateConfig; exports.configureSharedQuery = _chunk5WECMKSVcjs.configureSharedQuery; exports.configureSharedState = _chunkYMQDZV47cjs.configureSharedState; exports.getStorageAdapter = _chunk5PDGU37Ecjs.getStorageAdapter; exports.kvStoreAdapter = _chunk5PDGU37Ecjs.kvStoreAdapter; exports.localStoreAdapter = _chunk5PDGU37Ecjs.localStoreAdapter; exports.sharedQuery = _chunk5WECMKSVcjs.sharedQuery; exports.sharedState = _chunkYMQDZV47cjs.sharedState; exports.stringifyDeterministicForKeys = _chunkCYTO5EZEcjs.stringifyDeterministicForKeys; exports.useDeepMemo = _chunkWXLUEDBOcjs.useDeepMemo; exports.useSharedQuery = _chunk5WECMKSVcjs.useSharedQuery; exports.useSharedState = _chunkYMQDZV47cjs.useSharedState; exports.useSharedStateSetter = _chunkYMQDZV47cjs.useSharedStateSetter; exports.useSharedStateValue = _chunkYMQDZV47cjs.useSharedStateValue;
package/index.d.cts CHANGED
@@ -1,8 +1,8 @@
1
1
  export { QueryFn, QueryStateValue, QueryTarget, QueryTargetWithFn, QueryTargetWithUrl, SharedQuery, SharedQueryConfig, SharedQueryOptions, SharedQueryState, UseQueryResult, configureSharedQuery, sharedQuery, useSharedQuery } from './sharedQuery.cjs';
2
2
  export { useDeepMemo } from './useDeepMemo.cjs';
3
- export { stringifyDeterministicForKeys } from './stringify.cjs';
4
- export { PersistTo, StorageOptions, StoreConstructorArgs, StoreOptions, getStorageAdapter, kvStoreAdapter, localStoreAdapter } from './storage.cjs';
5
3
  export { SharedState, SharedStateConfig, SharedStateOptions, configureSharedState, sharedState, useSharedState, useSharedStateSetter, useSharedStateValue } from './sharedState.cjs';
4
+ export { PersistTo, StorageOptions, StoreConstructorArgs, StoreOptions, getStorageAdapter, kvStoreAdapter, localStoreAdapter } from './storage.cjs';
5
+ export { stringifyDeterministicForKeys } from './stringify.cjs';
6
6
  import '@choksheak/ts-utils/storageAdapter';
7
7
  import 'react';
8
8
  import '@choksheak/ts-utils/duration';
package/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export { QueryFn, QueryStateValue, QueryTarget, QueryTargetWithFn, QueryTargetWithUrl, SharedQuery, SharedQueryConfig, SharedQueryOptions, SharedQueryState, UseQueryResult, configureSharedQuery, sharedQuery, useSharedQuery } from './sharedQuery.js';
2
2
  export { useDeepMemo } from './useDeepMemo.js';
3
- export { stringifyDeterministicForKeys } from './stringify.js';
4
- export { PersistTo, StorageOptions, StoreConstructorArgs, StoreOptions, getStorageAdapter, kvStoreAdapter, localStoreAdapter } from './storage.js';
5
3
  export { SharedState, SharedStateConfig, SharedStateOptions, configureSharedState, sharedState, useSharedState, useSharedStateSetter, useSharedStateValue } from './sharedState.js';
4
+ export { PersistTo, StorageOptions, StoreConstructorArgs, StoreOptions, getStorageAdapter, kvStoreAdapter, localStoreAdapter } from './storage.js';
5
+ export { stringifyDeterministicForKeys } from './stringify.js';
6
6
  import '@choksheak/ts-utils/storageAdapter';
7
7
  import 'react';
8
8
  import '@choksheak/ts-utils/duration';
package/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  configureSharedQuery,
4
4
  sharedQuery,
5
5
  useSharedQuery
6
- } from "./chunk-TSVUVVWZ.js";
6
+ } from "./chunk-5O4HBYCM.js";
7
7
  import {
8
8
  SharedStateConfig,
9
9
  configureSharedState,
@@ -11,7 +11,7 @@ import {
11
11
  useSharedState,
12
12
  useSharedStateSetter,
13
13
  useSharedStateValue
14
- } from "./chunk-H4OYYPJV.js";
14
+ } from "./chunk-ZEVXDXJD.js";
15
15
  import {
16
16
  getStorageAdapter,
17
17
  kvStoreAdapter,
package/index.min.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true});var _chunkCW3XVEI4mincjs = require('./chunk-CW3XVEI4.min.cjs');var _chunkSQX7XDQJmincjs = require('./chunk-SQX7XDQJ.min.cjs');var _chunkUFJQLQFImincjs = require('./chunk-UFJQLQFI.min.cjs');var _chunkKGN4HQPPmincjs = require('./chunk-KGN4HQPP.min.cjs');var _chunkJOHZV3FDmincjs = require('./chunk-JOHZV3FD.min.cjs');require('./chunk-ZCDXPV3A.min.cjs');exports.SharedQueryConfig = _chunkCW3XVEI4mincjs.a; exports.SharedStateConfig = _chunkSQX7XDQJmincjs.a; exports.configureSharedQuery = _chunkCW3XVEI4mincjs.b; exports.configureSharedState = _chunkSQX7XDQJmincjs.b; exports.getStorageAdapter = _chunkUFJQLQFImincjs.a; exports.kvStoreAdapter = _chunkUFJQLQFImincjs.c; exports.localStoreAdapter = _chunkUFJQLQFImincjs.b; exports.sharedQuery = _chunkCW3XVEI4mincjs.c; exports.sharedState = _chunkSQX7XDQJmincjs.c; exports.stringifyDeterministicForKeys = _chunkKGN4HQPPmincjs.a; exports.useDeepMemo = _chunkJOHZV3FDmincjs.a; exports.useSharedQuery = _chunkCW3XVEI4mincjs.d; exports.useSharedState = _chunkSQX7XDQJmincjs.d; exports.useSharedStateSetter = _chunkSQX7XDQJmincjs.f; exports.useSharedStateValue = _chunkSQX7XDQJmincjs.e;
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});var _chunkAX6NADU2mincjs = require('./chunk-AX6NADU2.min.cjs');var _chunk2WE3FDDWmincjs = require('./chunk-2WE3FDDW.min.cjs');var _chunkUFJQLQFImincjs = require('./chunk-UFJQLQFI.min.cjs');var _chunkKGN4HQPPmincjs = require('./chunk-KGN4HQPP.min.cjs');var _chunkJOHZV3FDmincjs = require('./chunk-JOHZV3FD.min.cjs');require('./chunk-ZCDXPV3A.min.cjs');exports.SharedQueryConfig = _chunkAX6NADU2mincjs.a; exports.SharedStateConfig = _chunk2WE3FDDWmincjs.a; exports.configureSharedQuery = _chunkAX6NADU2mincjs.b; exports.configureSharedState = _chunk2WE3FDDWmincjs.b; exports.getStorageAdapter = _chunkUFJQLQFImincjs.a; exports.kvStoreAdapter = _chunkUFJQLQFImincjs.c; exports.localStoreAdapter = _chunkUFJQLQFImincjs.b; exports.sharedQuery = _chunkAX6NADU2mincjs.c; exports.sharedState = _chunk2WE3FDDWmincjs.c; exports.stringifyDeterministicForKeys = _chunkKGN4HQPPmincjs.a; exports.useDeepMemo = _chunkJOHZV3FDmincjs.a; exports.useSharedQuery = _chunkAX6NADU2mincjs.d; exports.useSharedState = _chunk2WE3FDDWmincjs.d; exports.useSharedStateSetter = _chunk2WE3FDDWmincjs.f; exports.useSharedStateValue = _chunk2WE3FDDWmincjs.e;
2
2
  //# sourceMappingURL=index.min.cjs.map
package/index.min.js CHANGED
@@ -1,2 +1,2 @@
1
- import{a as d,b as g,c as h,d as i}from"./chunk-ZCVFQ4FC.min.js";import{a as f,b as m,c as p,d as t,e as x,f as a}from"./chunk-GV7KUTU2.min.js";import{a as o,b as r,c as e}from"./chunk-QASRLGQX.min.js";import{a as b}from"./chunk-TDP547RX.min.js";import{a as c}from"./chunk-MBPSSROD.min.js";import"./chunk-UNSZMVZB.min.js";export{d as SharedQueryConfig,f as SharedStateConfig,g as configureSharedQuery,m as configureSharedState,o as getStorageAdapter,e as kvStoreAdapter,r as localStoreAdapter,h as sharedQuery,p as sharedState,b as stringifyDeterministicForKeys,c as useDeepMemo,i as useSharedQuery,t as useSharedState,a as useSharedStateSetter,x as useSharedStateValue};
1
+ import{a as d,b as g,c as h,d as i}from"./chunk-JHM37XTC.min.js";import{a as f,b as m,c as p,d as t,e as x,f as a}from"./chunk-25NYDKKO.min.js";import{a as o,b as r,c as e}from"./chunk-QASRLGQX.min.js";import{a as b}from"./chunk-TDP547RX.min.js";import{a as c}from"./chunk-MBPSSROD.min.js";import"./chunk-UNSZMVZB.min.js";export{d as SharedQueryConfig,f as SharedStateConfig,g as configureSharedQuery,m as configureSharedState,o as getStorageAdapter,e as kvStoreAdapter,r as localStoreAdapter,h as sharedQuery,p as sharedState,b as stringifyDeterministicForKeys,c as useDeepMemo,i as useSharedQuery,t as useSharedState,a as useSharedStateSetter,x as useSharedStateValue};
2
2
  //# sourceMappingURL=index.min.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@choksheak/react-utils",
3
3
  "license": "The Unlicense",
4
- "version": "0.4.9",
4
+ "version": "0.5.1",
5
5
  "description": "Miscellaneous React+TypeScript utilities with support for full tree-shaking.",
6
6
  "private": false,
7
7
  "sideEffects": false,
@@ -30,7 +30,7 @@
30
30
  "homepage": "https://github.com/choksheak/react-utils#readme",
31
31
  "devDependencies": {
32
32
  "@choksheak/bump-version": "^0.1.2",
33
- "@choksheak/ts-utils": "^0.3.8",
33
+ "@choksheak/ts-utils": "^0.4.6",
34
34
  "@eslint/compat": "^1.4.0",
35
35
  "@eslint/js": "^9.38.0",
36
36
  "@swc/core": "^1.14.0",
@@ -67,7 +67,7 @@
67
67
  "vitest": "^4.0.3"
68
68
  },
69
69
  "peerDependencies": {
70
- "@choksheak/ts-utils": ">=0.3.8",
70
+ "@choksheak/ts-utils": ">=0.4.6",
71
71
  "react": ">=16"
72
72
  },
73
73
  "dependencies": {
@@ -88,16 +88,16 @@
88
88
  "import": "./NoSSR.js",
89
89
  "require": "./NoSSR.cjs"
90
90
  },
91
- "./chunk-2VVAT5AA": {
92
- "types": "./chunk-2VVAT5AA.d.ts",
93
- "import": "./chunk-2VVAT5AA.js",
94
- "require": "./chunk-2VVAT5AA.cjs"
95
- },
96
91
  "./chunk-5PDGU37E": {
97
92
  "types": "./chunk-5PDGU37E.d.ts",
98
93
  "import": "./chunk-5PDGU37E.js",
99
94
  "require": "./chunk-5PDGU37E.cjs"
100
95
  },
96
+ "./chunk-5WECMKSV": {
97
+ "types": "./chunk-5WECMKSV.d.ts",
98
+ "import": "./chunk-5WECMKSV.js",
99
+ "require": "./chunk-5WECMKSV.cjs"
100
+ },
101
101
  "./chunk-CYTO5EZE": {
102
102
  "types": "./chunk-CYTO5EZE.d.ts",
103
103
  "import": "./chunk-CYTO5EZE.js",
@@ -108,16 +108,16 @@
108
108
  "import": "./chunk-IS3BZTWE.js",
109
109
  "require": "./chunk-IS3BZTWE.cjs"
110
110
  },
111
- "./chunk-KXFV42DP": {
112
- "types": "./chunk-KXFV42DP.d.ts",
113
- "import": "./chunk-KXFV42DP.js",
114
- "require": "./chunk-KXFV42DP.cjs"
115
- },
116
111
  "./chunk-WXLUEDBO": {
117
112
  "types": "./chunk-WXLUEDBO.d.ts",
118
113
  "import": "./chunk-WXLUEDBO.js",
119
114
  "require": "./chunk-WXLUEDBO.cjs"
120
115
  },
116
+ "./chunk-YMQDZV47": {
117
+ "types": "./chunk-YMQDZV47.d.ts",
118
+ "import": "./chunk-YMQDZV47.js",
119
+ "require": "./chunk-YMQDZV47.cjs"
120
+ },
121
121
  "./dist-UKP7SPWZ": {
122
122
  "types": "./dist-UKP7SPWZ.d.ts",
123
123
  "import": "./dist-UKP7SPWZ.js",
package/sharedQuery.cjs CHANGED
@@ -3,8 +3,8 @@
3
3
 
4
4
 
5
5
 
6
- var _chunk2VVAT5AAcjs = require('./chunk-2VVAT5AA.cjs');
7
- require('./chunk-KXFV42DP.cjs');
6
+ var _chunk5WECMKSVcjs = require('./chunk-5WECMKSV.cjs');
7
+ require('./chunk-YMQDZV47.cjs');
8
8
  require('./chunk-5PDGU37E.cjs');
9
9
  require('./chunk-CYTO5EZE.cjs');
10
10
  require('./chunk-WXLUEDBO.cjs');
@@ -14,4 +14,4 @@ require('./chunk-IS3BZTWE.cjs');
14
14
 
15
15
 
16
16
 
17
- exports.SharedQueryConfig = _chunk2VVAT5AAcjs.SharedQueryConfig; exports.configureSharedQuery = _chunk2VVAT5AAcjs.configureSharedQuery; exports.sharedQuery = _chunk2VVAT5AAcjs.sharedQuery; exports.useSharedQuery = _chunk2VVAT5AAcjs.useSharedQuery;
17
+ exports.SharedQueryConfig = _chunk5WECMKSVcjs.SharedQueryConfig; exports.configureSharedQuery = _chunk5WECMKSVcjs.configureSharedQuery; exports.sharedQuery = _chunk5WECMKSVcjs.sharedQuery; exports.useSharedQuery = _chunk5WECMKSVcjs.useSharedQuery;