UNPKG

@tanstack/query-core

Version:

The framework agnostic core that powers TanStack Query

378 lines (377 loc) • 15.9 kB
import { timeoutManager } from "./timeoutManager.js"; //#region src/utils.ts /** * @deprecated use `environmentManager.isServer()` instead. */ const isServer = typeof window === "undefined" || "Deno" in globalThis; function noop() {} /** * Resolves an {@link Updater}: calls it with `input` if it is a function, otherwise returns it as * is. * @param updater - The new value, or a function that receives `input` and returns the new value. * @param input - The current value, passed to `updater` if it is a function. * @returns The new value. */ function functionalUpdate(updater, input) { return typeof updater === "function" ? updater(input) : updater; } /** * Checks whether a value can be used as a timeout: a non-negative, finite number. * @param value - The value to check. * @returns `true` if `value` is a number that is `>= 0` and not `Infinity`. */ function isValidTimeout(value) { return typeof value === "number" && value >= 0 && value !== Infinity; } /** * Calculates how long until data updated at `updatedAt` becomes stale. * @param updatedAt - The timestamp, in milliseconds, at which the data was last updated. * @param staleTime - The time, in milliseconds, after which data is considered stale. Defaults to * `0`. * @returns The remaining time in milliseconds, or `0` if the data is already stale. */ function timeUntilStale(updatedAt, staleTime) { return Math.max(updatedAt + (staleTime || 0) - Date.now(), 0); } /** * Resolves an option that can be given either as a value or as a function of the query, such as * `staleTime`. * @param value - The option value, or a function that receives `query` and returns the value. * @param query - The query passed to `value` if it is a function. * @returns The resolved value, or `undefined` if `value` is `undefined`. */ function resolveQueryValue(value, query) { return typeof value === "function" ? value(query) : value; } /** * Checks whether a query matches the given {@link QueryFilters}. * Every filter that is specified must match; filters that are left unspecified are ignored. * @param filters - The filters to check the query against. * @param query - The query to check. * @returns `true` if the query matches every specified filter. * @example * ```ts * const queryCache = queryClient.getQueryCache() * * const matchingQueries = queryCache * .getAll() * .filter((query) => matchQuery({ queryKey: ['posts'] }, query)) * ``` */ function matchQuery(filters, query) { const { type = "all", exact, fetchStatus, predicate, queryKey, stale } = filters; if (queryKey) { if (exact) { if (query.queryHash !== hashQueryKeyByOptions(queryKey, query.options)) return false; } else if (!partialMatchKey(query.queryKey, queryKey)) return false; } if (type !== "all") { const isActive = query.isActive(); if (type === "active" && !isActive) return false; if (type === "inactive" && isActive) return false; } if (typeof stale === "boolean" && query.isStale() !== stale) return false; if (fetchStatus && fetchStatus !== query.state.fetchStatus) return false; if (predicate && !predicate(query)) return false; return true; } /** * Checks whether a mutation matches the given {@link MutationFilters}. * Every filter that is specified must match; filters that are left unspecified are ignored. * If a `mutationKey` filter is provided but the mutation has no `mutationKey` of its own, it does not match. * @param filters - The filters to check the mutation against. * @param mutation - The mutation to check. * @returns `true` if the mutation matches every specified filter. * @example * ```ts * const mutationCache = queryClient.getMutationCache() * * const matchingMutations = mutationCache * .getAll() * .filter((mutation) => matchMutation({ mutationKey: ['addPost'] }, mutation)) * ``` */ function matchMutation(filters, mutation) { const { exact, status, predicate, mutationKey } = filters; if (mutationKey) { if (!mutation.options.mutationKey) return false; if (exact) { if (hashKey(mutation.options.mutationKey) !== hashKey(mutationKey)) return false; } else if (!partialMatchKey(mutation.options.mutationKey, mutationKey)) return false; } if (status && mutation.state.status !== status) return false; if (predicate && !predicate(mutation)) return false; return true; } /** * Hashes a query key with the query's `queryKeyHashFn` option, falling back to {@link hashKey}. * @param queryKey - The query key to hash. * @param options - The query options that may provide a custom `queryKeyHashFn`. * @returns The query hash, a string that identifies the query in the cache. */ function hashQueryKeyByOptions(queryKey, options) { return ((options === null || options === void 0 ? void 0 : options.queryKeyHashFn) || hashKey)(queryKey); } /** * Default query & mutation keys hash function. * Hashes the value into a stable hash. * @param queryKey - The query or mutation key to hash. * @returns The stable hash of the key, as a JSON string. * @example * ```ts * // Object keys are sorted, so key order doesn't affect the hash: * hashKey(['todos', { page: 1, filter: 'done' }]) // === '["todos",{"filter":"done","page":1}]' * ``` */ function hashKey(queryKey) { return JSON.stringify(queryKey, (_, val) => isPlainObject(val) ? Object.keys(val).sort().reduce((result, key) => { result[key] = val[key]; return result; }, {}) : val); } function partialMatchKey(a, b) { if (a === b) return true; if (typeof a !== typeof b) return false; if (a && b && typeof a === "object" && typeof b === "object") { if (Array.isArray(a) && Array.isArray(b)) { if (b.length > a.length) return false; for (let i = 0; i < b.length; i++) if (!partialMatchKey(a[i], b[i])) return false; return true; } const bKeys = Object.keys(b); for (const key of bKeys) if (!partialMatchKey(a[key], b[key])) return false; return true; } return false; } const hasOwn = Object.prototype.hasOwnProperty; function replaceEqualDeep(a, b, depth = 0) { if (a === b) return a; if (depth > 500) return b; const array = isPlainArray(a) && isPlainArray(b); if (!array && !(isPlainObject(a) && isPlainObject(b))) return b; const aSize = (array ? a : Object.keys(a)).length; const bItems = array ? b : Object.keys(b); const bSize = bItems.length; const copy = array ? new Array(bSize) : {}; let equalItems = 0; for (let i = 0; i < bSize; i++) { const key = array ? i : bItems[i]; const aItem = a[key]; const bItem = b[key]; if (aItem === bItem) { copy[key] = aItem; if (array ? i < aSize : hasOwn.call(a, key)) equalItems++; continue; } if (aItem === null || bItem === null || typeof aItem !== "object" || typeof bItem !== "object") { copy[key] = bItem; continue; } const v = replaceEqualDeep(aItem, bItem, depth + 1); copy[key] = v; if (v === aItem) equalItems++; } return aSize === bSize && equalItems === aSize ? a : copy; } /** * Shallow compare objects. * @param a - The first object. * @param b - The second object, or `undefined`. * @returns `true` if both objects have the same keys, with strictly equal values for each key. */ function shallowEqualObjects(a, b) { if (!b || Object.keys(a).length !== Object.keys(b).length) return false; for (const key in a) if (a[key] !== b[key]) return false; return true; } /** * Checks whether a value is an array with no holes and no extra properties. * @param value - The value to check. * @returns `true` if `value` is an array whose number of own keys equals its `length`. */ function isPlainArray(value) { return Array.isArray(value) && value.length === Object.keys(value).length; } /** * Checks whether a value is a plain object, e.g. created with an object literal or * `Object.create(null)`. * @param o - The value to check. * @returns `true` if `o` is a plain object. */ function isPlainObject(o) { if (!hasObjectPrototype(o)) return false; const objectPrototype = Object.getPrototypeOf(o); const ctor = objectPrototype === null || objectPrototype === void 0 ? void 0 : objectPrototype.constructor; if (ctor === void 0) return true; if (typeof ctor !== "function") return false; const prot = ctor.prototype; if (!hasObjectPrototype(prot)) return false; if (!prot.hasOwnProperty("isPrototypeOf")) return false; if (objectPrototype !== Object.prototype) return false; return true; } /** * Checks whether a value's `Object.prototype.toString` tag is `[object Object]`. * @param o - The value to check. * @returns `true` if the tag is `[object Object]`. */ function hasObjectPrototype(o) { return Object.prototype.toString.call(o) === "[object Object]"; } /** * Waits for the given time, using the configured {@link timeoutManager}. * @param timeout - The time to wait, in milliseconds. * @returns A promise that resolves after `timeout` milliseconds. */ function sleep(timeout) { return new Promise((resolve) => { timeoutManager.setTimeout(resolve, timeout); }); } /** * Applies a query's `structuralSharing` option when writing new data. * @param prevData - The previously cached data. * @param data - The new data. * @param options - The query options, whose `structuralSharing` decides how the data is merged. * @returns The result of a custom `structuralSharing` function, `data` with its unchanged parts * replaced by those of `prevData` by default, or `data` as is if `structuralSharing` is `false`. * @throws {Error} The error from the default structural sharing, e.g. when the data is not JSON serializable. * Outside production, it is also logged to the console. */ function replaceData(prevData, data, options) { if (typeof options.structuralSharing === "function") return options.structuralSharing(prevData, data); else if (options.structuralSharing !== false) { if (process.env.NODE_ENV !== "production") try { return replaceEqualDeep(prevData, data); } catch (error) { console.error(`Structural sharing requires data to be JSON serializable. To fix this, turn off structuralSharing or return JSON-serializable data from your queryFn. [${options.queryHash}]: ${error}`); throw error; } return replaceEqualDeep(prevData, data); } return data; } /** * Intended to be passed as a query's `placeholderData` option, for example * `placeholderData: keepPreviousData`. Instead of resetting the query's data to `undefined` while a new * query key is fetching, it keeps displaying the previously fetched data until the new data arrives. * @param previousData - The data of the previous query key, passed by the observer. * @returns The previous data, unchanged. * @example * ```ts * new QueryObserver(queryClient, { * queryKey: ['posts', page], * queryFn: () => fetchPosts(page), * placeholderData: keepPreviousData, * }) * ``` */ function keepPreviousData(previousData) { return previousData; } /** * Returns a new array with `item` added at the end. If that makes the array longer than `max`, * the first item is dropped. Only one item is dropped, so the result can still exceed `max` if * `items` already did. * @param items - The current items. * @param item - The value to append. * @param max - The length above which an item is dropped. `0` (the default) means no limit. * @returns A new array with `item` added. */ function addToEnd(items, item, max = 0) { const newItems = [...items, item]; return max && newItems.length > max ? newItems.slice(1) : newItems; } /** * Returns a new array with `item` added at the start. If that makes the array longer than `max`, * the last item is dropped. Only one item is dropped, so the result can still exceed `max` if * `items` already did. * @param items - The current items. * @param item - The value to prepend. * @param max - The length above which an item is dropped. `0` (the default) means no limit. * @returns A new array with `item` added. */ function addToStart(items, item, max = 0) { const newItems = [item, ...items]; return max && newItems.length > max ? newItems.slice(0, -1) : newItems; } /** * Sentinel value that can be passed as a query's `queryFn` to conditionally disable the query (equivalent * to `enabled: false`) while preserving full type inference for the query's data. Unlike `enabled: false`, * a query disabled via `skipToken` cannot be triggered with `refetch`. * @example * ```ts * new QueryObserver(queryClient, { * queryKey: ['post', postId], * queryFn: postId != null ? () => fetchPost(postId) : skipToken, * }) * ``` */ const skipToken = Symbol(); /** * Returns the function to call for a fetch. If there is no `queryFn` but the fetch was started from * an `initialPromise`, that promise is returned instead. If there is no usable `queryFn` (missing or * {@link skipToken}), the returned function rejects with a `Missing queryFn` error. * @param options - The query options with the `queryFn`, and the `queryHash` used in error messages. * @param fetchOptions - The fetch options, which may carry an `initialPromise`. * @returns The function to call for the fetch. */ function ensureQueryFn(options, fetchOptions) { if (process.env.NODE_ENV !== "production") { if (options.queryFn === skipToken) console.error(`Attempted to invoke queryFn when set to skipToken. This is likely a configuration error. Query hash: '${options.queryHash}'`); } if (!options.queryFn && (fetchOptions === null || fetchOptions === void 0 ? void 0 : fetchOptions.initialPromise)) return () => fetchOptions.initialPromise; if (!options.queryFn || options.queryFn === skipToken) return () => Promise.reject(/* @__PURE__ */ new Error(`Missing queryFn: '${options.queryHash}'`)); return options.queryFn; } /** * Resolves a `throwOnError` option to a boolean. * If `throwOnError` is a function, it is called with `params` (e.g. the error and, depending on the caller, * additional context such as the query or mutation) and its result is returned, allowing the throwing * behavior to be decided per error. Otherwise, `throwOnError` itself is coerced to a boolean (`undefined` * resolves to `false`). * @param throwOnError - The `throwOnError` option: a boolean, a function that decides per error, or * `undefined`. * @param params - The arguments passed to `throwOnError` if it is a function. * @returns Whether the error should be thrown. * @example * ```ts * const throwOnError = * query.state.error && typeof options.throwOnError === 'function' * ? shouldThrowError(options.throwOnError, [query.state.error, query]) * : options.throwOnError * ``` */ function shouldThrowError(throwOnError, params) { if (typeof throwOnError === "function") return throwOnError(...params); return !!throwOnError; } /** * Adds a lazy `signal` getter to an object, such as a query function context. The first time * `signal` is read, `onCancelled` is registered for the signal's `abort` event (or called right * away if it is already aborted), so cancellation is only tracked when the signal is actually used. * @param object - The object to add the `signal` property to. * @param getSignal - Returns the `AbortSignal`. Called once, when `signal` is first read. * @param onCancelled - Called when the signal is aborted, if `signal` was read. * @returns The same object, with the `signal` property added. */ function addConsumeAwareSignal(object, getSignal, onCancelled) { let consumed = false; let signal; Object.defineProperty(object, "signal", { enumerable: true, get: () => { signal ?? (signal = getSignal()); if (consumed) return signal; consumed = true; if (signal.aborted) onCancelled(); else signal.addEventListener("abort", onCancelled, { once: true }); return signal; } }); return object; } //#endregion export { addConsumeAwareSignal, addToEnd, addToStart, ensureQueryFn, functionalUpdate, hashKey, hashQueryKeyByOptions, isPlainArray, isPlainObject, isServer, isValidTimeout, keepPreviousData, matchMutation, matchQuery, noop, partialMatchKey, replaceData, replaceEqualDeep, resolveQueryValue, shallowEqualObjects, shouldThrowError, skipToken, sleep, timeUntilStale }; //# sourceMappingURL=utils.js.map