@tanstack/query-core
Version:
The framework agnostic core that powers TanStack Query
402 lines (401 loc) • 16.4 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const require_timeoutManager = require("./timeoutManager.cjs");
//#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?.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?.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) => {
require_timeoutManager.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?.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 ??= getSignal();
if (consumed) return signal;
consumed = true;
if (signal.aborted) onCancelled();
else signal.addEventListener("abort", onCancelled, { once: true });
return signal;
}
});
return object;
}
//#endregion
exports.addConsumeAwareSignal = addConsumeAwareSignal;
exports.addToEnd = addToEnd;
exports.addToStart = addToStart;
exports.ensureQueryFn = ensureQueryFn;
exports.functionalUpdate = functionalUpdate;
exports.hashKey = hashKey;
exports.hashQueryKeyByOptions = hashQueryKeyByOptions;
exports.isPlainArray = isPlainArray;
exports.isPlainObject = isPlainObject;
exports.isServer = isServer;
exports.isValidTimeout = isValidTimeout;
exports.keepPreviousData = keepPreviousData;
exports.matchMutation = matchMutation;
exports.matchQuery = matchQuery;
exports.noop = noop;
exports.partialMatchKey = partialMatchKey;
exports.replaceData = replaceData;
exports.replaceEqualDeep = replaceEqualDeep;
exports.resolveQueryValue = resolveQueryValue;
exports.shallowEqualObjects = shallowEqualObjects;
exports.shouldThrowError = shouldThrowError;
exports.skipToken = skipToken;
exports.sleep = sleep;
exports.timeUntilStale = timeUntilStale;
//# sourceMappingURL=utils.cjs.map