UNPKG

@tanstack/query-core

Version:

The framework agnostic core that powers TanStack Query

604 lines (603 loc) • 19.9 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const require_utils = require("./utils.cjs"); const require_notifyManager = require("./notifyManager.cjs"); const require_retryer = require("./retryer.cjs"); const require_removable = require("./removable.cjs"); const require_infiniteQueryBehavior = require("./infiniteQueryBehavior.cjs"); //#region src/query.ts /** * Represents a single cached query. A `Query` holds the query's key, options, * state (data/error/status), and the observers currently subscribed to it. * * Instances are created and managed internally by `QueryCache`; application * code typically interacts with queries indirectly through `QueryClient` or * a framework hook like `useQuery`. Direct access to a `Query` instance is * possible via `queryCache.find()`/`findAll()` for inspecting cache state. * @example * ```ts * const queryCache = queryClient.getQueryCache() * const query = queryCache.find({ queryKey: ['posts'] }) * * if (query) { * console.log(query.state.dataUpdatedAt) * } * ``` */ var Query = class extends require_removable.Removable { #queryType; #initialState; #revertState; #cache; #client; #retryer; #defaultOptions; #abortSignalConsumed; constructor(config) { super(); this.#abortSignalConsumed = false; this.#defaultOptions = config.defaultOptions; this.setOptions(config.options); this.observers = []; this.#client = config.client; this.#cache = this.#client.getQueryCache(); this.queryKey = config.queryKey; this.queryHash = config.queryHash; this.#initialState = getDefaultState(this.options); this.state = config.state ?? this.#initialState; this.scheduleGc(); } /** * The `meta` object passed in the query's options, if any. * @returns The query's `meta`, or `undefined` if none was set. */ get meta() { return this.options.meta; } /** * `'infinite'` for an infinite query, otherwise `undefined`. * @internal * @returns The type of the query, set from the `_type` option (e.g. `'infinite'`). */ get queryType() { return this.#queryType; } /** * The promise for the currently in-flight fetch, if the query is fetching. * `undefined` when the query is not fetching. * @returns The promise of the in-flight fetch, or `undefined`. */ get promise() { return this.#retryer?.promise; } /** * Replaces the query's options and applies their `gcTime`. If the query has no data yet, it gets * the `initialData` from the new options, if any. * @internal * @param options - The new query options, merged on top of the query's default options. */ setOptions(options) { this.options = { ...this.#defaultOptions, ...options }; if (options?._type) this.#queryType = options._type; this.updateGcTime(this.options.gcTime); if (this.state && this.state.data === void 0) { const defaultState = getDefaultState(this.options); if (defaultState.data !== void 0) { this.setState(successState(defaultState.data, defaultState.dataUpdatedAt)); this.#initialState = defaultState; } } } optionalRemove() { if (!this.observers.length && this.state.fetchStatus === "idle") this.#cache.remove(this); } /** * Writes new data to the query and marks it as successfully fetched. * @internal * @param newData - The data to write. Structural sharing with the current data is applied. * @param options - Set `updatedAt` to override the timestamp, and `manual` to mark the write as * manual (e.g. from `queryClient.setQueryData`). * @returns The data that was written. */ setData(newData, options) { const data = require_utils.replaceData(this.state.data, newData, this.options); this.#dispatch({ data, type: "success", dataUpdatedAt: options?.updatedAt, manual: options?.manual }); return data; } /** * Merges the given partial state directly into this query's state, notifying observers. Used * by persistence and broadcast plugins to restore a state snapshot, and by devtools to let a * user manually trigger a loading/error state or edit the cached data. * @param state - The partial state to merge into the query's state. */ setState(state) { this.#dispatch({ type: "setState", state }); } /** * Cancels the query's currently in-flight fetch, if any. * - Returns a promise that resolves once the cancellation has settled. * - If no fetch is in progress, resolves immediately. * @param options - Set `revert` to restore the state from before the fetch started, and `silent` * to suppress the cancellation error when a new fetch replaces the cancelled one. * @returns A promise that resolves once the cancellation has settled. * @example * ```ts * await query.cancel() * ``` */ cancel(options) { const promise = this.#retryer?.promise; this.#retryer?.cancel(options); return promise ? promise.then(require_utils.noop).catch(require_utils.noop) : Promise.resolve(); } /** * Clears the query's garbage collection timeout and silently cancels any * in-flight fetch. Called by `QueryCache` when the query is removed from * the cache. * @see {@link Query#cancel} */ destroy() { super.destroy(); this.cancel({ silent: true }); } /** * The state the query was created with. * @internal * @returns The state the query had when it was first created, used by {@link Query#reset}. */ get resetState() { return this.#initialState; } /** * Resets the query back to its initial state (the state it had when it was * first created, e.g. any `initialData`), destroying it first to cancel any * in-flight fetch. */ reset() { this.destroy(); this.setState(this.resetState); } /** * Returns `true` if the query has at least one observer for which `enabled` * does not resolve to `false`. * @returns `true` if the query has an enabled observer. */ isActive() { return this.observers.some((observer) => require_utils.resolveQueryValue(observer.options.enabled, this) !== false); } /** * Returns `true` if the query is disabled, meaning it will not fetch * automatically. * - If the query has observers, it is disabled when none of them are active * (see `isActive`). * - If the query has no observers, it is disabled when its `queryFn` is * `skipToken` or it has never been fetched. * @returns `true` if the query is disabled. */ isDisabled() { if (this.getObserversCount() > 0) return !this.isActive(); return this.options.queryFn === require_utils.skipToken || !this.isFetched(); } /** * Returns `true` if the query has been fetched, i.e. it has resolved with * either data or an error at least once. * @returns `true` if the query has been fetched. */ isFetched() { return this.state.dataUpdateCount + this.state.errorUpdateCount > 0; } /** * Returns `true` if the query has at least one observer configured with * `staleTime: 'static'`, meaning it is treated as never stale. * @returns `true` if the query is static. */ isStatic() { if (this.getObserversCount() > 0) return this.observers.some((observer) => require_utils.resolveQueryValue(observer.options.staleTime, this) === "static"); return false; } /** * Returns `true` if the query is stale. * - If the query has observers, defers to whether any observer's current * result reports `isStale` (which accounts for each observer's own * `staleTime` and `enabled` state). * - If the query has no observers, it is considered stale when it has no * data or has been invalidated. * @returns `true` if the query is stale. * @see {@link Query#isStaleByTime} * @example * ```ts * if (query.isStale()) { * // refetch or otherwise treat the cached data as outdated * } * ``` */ isStale() { if (this.getObserversCount() > 0) return this.observers.some((observer) => observer.getCurrentResult().isStale); return this.state.data === void 0 || this.state.isInvalidated; } /** * Returns `true` if the query's data is stale relative to the given * `staleTime` (defaults to `0`). * - A query with no data is always stale. * - `staleTime: 'static'` is never stale. * - An invalidated query is always stale. * - Otherwise, staleness is based on elapsed time since `dataUpdatedAt`. * @param staleTime - The time, in milliseconds, after which data is considered stale, or * `'static'` to never treat existing data as stale. A query without data is stale either way. * @returns `true` if the query's data is stale. * @see {@link Query#isStale} * @example * ```ts * const isStale = query.isStaleByTime(1000 * 60) * ``` */ isStaleByTime(staleTime = 0) { if (this.state.data === void 0) return true; if (staleTime === "static") return false; if (this.state.isInvalidated) return true; return !require_utils.timeUntilStale(this.state.dataUpdatedAt, staleTime); } /** * Refetches the query when the window regains focus, if an observer wants that, and continues a * paused fetch. * @internal */ onFocus() { this.observers.find((x) => x.shouldFetchOnWindowFocus())?.refetch({ cancelRefetch: false }); this.#retryer?.continue(); } /** * Refetches the query when the app reconnects, if an observer wants that, and continues a paused * fetch. * @internal */ onOnline() { this.observers.find((x) => x.shouldFetchOnReconnect())?.refetch({ cancelRefetch: false }); this.#retryer?.continue(); } /** * Subscribes an observer to the query and stops its garbage collection. * @internal * @param observer - The observer to add. Adding an observer that is already subscribed does * nothing. */ addObserver(observer) { if (!this.observers.includes(observer)) { this.observers.push(observer); this.clearGcTimeout(); this.#cache.notify({ type: "observerAdded", query: this, observer }); } } /** * Unsubscribes an observer from the query. * @internal * @param observer - The observer to remove. When the last observer is removed, the in-flight * fetch is cancelled if its abort signal was consumed (otherwise only its retries are stopped), * and garbage collection is scheduled. */ removeObserver(observer) { const index = this.observers.indexOf(observer); if (index !== -1) { this.observers.splice(index, 1); if (!this.observers.length) { if (this.#retryer) { if (this.#abortSignalConsumed || this.state.fetchStatus === "paused" && this.state.status === "pending") this.#retryer.cancel({ revert: true }); else this.#retryer.cancelRetry(); } this.scheduleGc(); } this.#cache.notify({ type: "observerRemoved", query: this, observer }); } } /** * Returns the number of observers currently subscribed to this query. * @returns The number of observers. * @example * ```ts * if (query.getObserversCount() === 0) { * // no component is currently watching this query * } * ``` */ getObserversCount() { return this.observers.length; } /** * Marks the query as invalidated, unless it is already invalidated. This * updates `state.isInvalidated` and notifies observers, but does not by * itself trigger a refetch. * @example * ```ts * query.invalidate() * ``` */ invalidate() { if (!this.state.isInvalidated) this.#dispatch({ type: "invalidate" }); } /** * Fetches the query, i.e. runs its `queryFn` (through any configured * retryer/behavior) and updates the query's state with the result. * - If a fetch is already in flight, returns its promise instead of * starting a new one, unless `fetchOptions.cancelRefetch` is set and the * query already has data, in which case the current fetch is silently * cancelled first. * - If `options` is passed, it replaces the query's current options * before fetching. * @param options - Query options that replace the query's current options before fetching. They * are not applied when an in-flight fetch is reused. * @param fetchOptions - Set `cancelRefetch` to cancel an in-flight fetch first (only if the query * already has data), and `meta` to pass extra information to the query's behavior. * @returns A promise that resolves with the fetched data, or rejects with the fetch error. If the * fetch is cancelled with `revert` while the query has data, it resolves with the restored data * instead. */ async fetch(options, fetchOptions) { if (this.state.fetchStatus !== "idle" && this.#retryer?.status() !== "rejected") { if (this.state.data !== void 0 && fetchOptions?.cancelRefetch) this.cancel({ silent: true }); else if (this.#retryer) { this.#retryer.continueRetry(); return this.#retryer.promise; } } if (options) this.setOptions(options); if (!this.options.queryFn) { const observer = this.observers.find((x) => x.options.queryFn); if (observer) this.setOptions(observer.options); } if (process.env.NODE_ENV !== "production") { if (!Array.isArray(this.options.queryKey)) console.error(`As of v4, queryKey needs to be an Array. If you are using a string like 'repoData', please change it to an Array, e.g. ['repoData']`); } const abortController = new AbortController(); const addSignalProperty = (object) => { Object.defineProperty(object, "signal", { enumerable: true, get: () => { this.#abortSignalConsumed = true; return abortController.signal; } }); }; const fetchFn = () => { const queryFn = require_utils.ensureQueryFn(this.options, fetchOptions); const createQueryFnContext = () => { const queryFnContext = { client: this.#client, queryKey: this.queryKey, meta: this.meta }; addSignalProperty(queryFnContext); return queryFnContext; }; const queryFnContext = createQueryFnContext(); this.#abortSignalConsumed = false; if (this.options.persister) return this.options.persister(queryFn, queryFnContext, this); return queryFn(queryFnContext); }; const createFetchContext = () => { const context = { fetchOptions, options: this.options, queryKey: this.queryKey, client: this.#client, state: this.state, fetchFn }; addSignalProperty(context); return context; }; const context = createFetchContext(); (this.#queryType === "infinite" ? require_infiniteQueryBehavior.infiniteQueryBehavior(this.options.pages) : this.options.behavior)?.onFetch(context, this); this.#revertState = this.state; if (this.state.fetchStatus === "idle" || this.state.fetchMeta !== context.fetchOptions?.meta) this.#dispatch({ type: "fetch", meta: context.fetchOptions?.meta }); const retryer = this.#retryer = require_retryer.createRetryer({ initialPromise: fetchOptions?.initialPromise, fn: context.fetchFn, onCancel: (error) => { if (error instanceof require_retryer.CancelledError && error.revert) this.setState({ ...this.#revertState, fetchStatus: "idle" }); abortController.abort(); }, onFail: (failureCount, error) => { this.#dispatch({ type: "failed", failureCount, error }); }, onPause: () => { this.#dispatch({ type: "pause" }); }, onContinue: () => { this.#dispatch({ type: "continue" }); }, retry: context.options.retry, retryDelay: context.options.retryDelay, networkMode: context.options.networkMode, canRun: () => true }); try { const data = await retryer.start(); if (data === void 0) { if (process.env.NODE_ENV !== "production") console.error(`Query data cannot be undefined. Please make sure to return a value other than undefined from your query function. Affected query key: ${this.queryHash}`); throw new Error(`${this.queryHash} data is undefined`); } this.setData(data); this.#cache.config.onSuccess?.(data, this); this.#cache.config.onSettled?.(data, this.state.error, this); return data; } catch (error) { if (error instanceof require_retryer.CancelledError) { if (error.silent) return this.#retryer.promise; else if (error.revert) { if (this.state.data === void 0) throw error; return this.state.data; } } this.#dispatch({ type: "error", error }); this.#cache.config.onError?.(error, this); this.#cache.config.onSettled?.(this.state.data, error, this); throw error; } finally { if (this.#retryer === retryer) this.#retryer = void 0; this.scheduleGc(); } } #dispatch(action) { const reducer = (state) => { switch (action.type) { case "failed": return { ...state, fetchFailureCount: action.failureCount, fetchFailureReason: action.error }; case "pause": return { ...state, fetchStatus: "paused" }; case "continue": return { ...state, fetchStatus: "fetching" }; case "fetch": return { ...state, ...fetchState(state.data, this.options), fetchMeta: action.meta ?? null }; case "success": const newState = { ...state, ...successState(action.data, action.dataUpdatedAt), dataUpdateCount: state.dataUpdateCount + 1, ...!action.manual && { fetchStatus: "idle", fetchFailureCount: 0, fetchFailureReason: null } }; this.#revertState = action.manual ? newState : void 0; return newState; case "error": const error = action.error; return { ...state, error, errorUpdateCount: state.errorUpdateCount + 1, errorUpdatedAt: Date.now(), fetchFailureCount: state.fetchFailureCount + 1, fetchFailureReason: error, fetchStatus: "idle", status: "error", isInvalidated: true }; case "invalidate": return { ...state, isInvalidated: true }; case "setState": return { ...state, ...action.state }; } }; this.state = reducer(this.state); require_notifyManager.notifyManager.batch(() => { this.observers.slice().forEach((observer) => { observer.onQueryUpdate(); }); this.#cache.notify({ query: this, type: "updated", action }); }); } }; /** * Returns the state changes applied when a query starts fetching. * @param data - The query's current data. If it is `undefined`, the query is also reset to * `'pending'` with no error. * @param options - The query options, whose `networkMode` decides whether the query is `'fetching'` * or `'paused'`. * @returns The partial query state for a starting fetch. */ function fetchState(data, options) { return { fetchFailureCount: 0, fetchFailureReason: null, fetchStatus: require_retryer.canFetch(options.networkMode) ? "fetching" : "paused", ...data === void 0 && { error: null, status: "pending" } }; } /** * Returns the state changes applied when a query receives data. * @param data - The new data. * @param dataUpdatedAt - The timestamp the data is recorded with. Defaults to the current time. * @returns The partial query state for a successful update. */ function successState(data, dataUpdatedAt) { return { data, dataUpdatedAt: dataUpdatedAt ?? Date.now(), error: null, isInvalidated: false, status: "success" }; } /** * Returns the initial state of a query, based on its `initialData` and `initialDataUpdatedAt` * options. * @param options - The query options. * @returns The initial query state: `'success'` if `initialData` is set, otherwise `'pending'`. */ function getDefaultState(options) { const data = typeof options.initialData === "function" ? options.initialData() : options.initialData; const hasData = data !== void 0; const initialDataUpdatedAt = hasData ? typeof options.initialDataUpdatedAt === "function" ? options.initialDataUpdatedAt() : options.initialDataUpdatedAt : 0; return { data, dataUpdateCount: 0, dataUpdatedAt: hasData ? initialDataUpdatedAt ?? Date.now() : 0, error: null, errorUpdateCount: 0, errorUpdatedAt: 0, fetchFailureCount: 0, fetchFailureReason: null, fetchMeta: null, isInvalidated: false, status: hasData ? "success" : "pending", fetchStatus: "idle" }; } //#endregion exports.Query = Query; exports.fetchState = fetchState; //# sourceMappingURL=query.cjs.map