UNPKG

@tanstack/query-core

Version:

The framework agnostic core that powers TanStack Query

364 lines 10.9 kB
// src/query.ts import { noop, replaceData, timeUntilStale } from "./utils.js"; import { notifyManager } from "./notifyManager.js"; import { canFetch, createRetryer, isCancelledError } from "./retryer.js"; import { Removable } from "./removable.js"; var Query = class extends Removable { constructor(config) { super(); this.#abortSignalConsumed = false; this.#defaultOptions = config.defaultOptions; this.#setOptions(config.options); this.#observers = []; this.#cache = config.cache; this.queryKey = config.queryKey; this.queryHash = config.queryHash; this.#initialState = config.state || getDefaultState(this.options); this.state = this.#initialState; this.scheduleGc(); } #initialState; #revertState; #cache; #promise; #retryer; #observers; #defaultOptions; #abortSignalConsumed; get meta() { return this.options.meta; } #setOptions(options) { this.options = { ...this.#defaultOptions, ...options }; this.updateGcTime(this.options.gcTime); } optionalRemove() { if (!this.#observers.length && this.state.fetchStatus === "idle") { this.#cache.remove(this); } } setData(newData, options) { const data = replaceData(this.state.data, newData, this.options); this.#dispatch({ data, type: "success", dataUpdatedAt: options?.updatedAt, manual: options?.manual }); return data; } setState(state, setStateOptions) { this.#dispatch({ type: "setState", state, setStateOptions }); } cancel(options) { const promise = this.#promise; this.#retryer?.cancel(options); return promise ? promise.then(noop).catch(noop) : Promise.resolve(); } destroy() { super.destroy(); this.cancel({ silent: true }); } reset() { this.destroy(); this.setState(this.#initialState); } isActive() { return this.#observers.some( (observer) => observer.options.enabled !== false ); } isDisabled() { return this.getObserversCount() > 0 && !this.isActive(); } isStale() { return this.state.isInvalidated || !this.state.dataUpdatedAt || this.#observers.some((observer) => observer.getCurrentResult().isStale); } isStaleByTime(staleTime = 0) { return this.state.isInvalidated || !this.state.dataUpdatedAt || !timeUntilStale(this.state.dataUpdatedAt, staleTime); } onFocus() { const observer = this.#observers.find((x) => x.shouldFetchOnWindowFocus()); observer?.refetch({ cancelRefetch: false }); this.#retryer?.continue(); } onOnline() { const observer = this.#observers.find((x) => x.shouldFetchOnReconnect()); observer?.refetch({ cancelRefetch: false }); this.#retryer?.continue(); } addObserver(observer) { if (!this.#observers.includes(observer)) { this.#observers.push(observer); this.clearGcTimeout(); this.#cache.notify({ type: "observerAdded", query: this, observer }); } } removeObserver(observer) { if (this.#observers.includes(observer)) { this.#observers = this.#observers.filter((x) => x !== observer); if (!this.#observers.length) { if (this.#retryer) { if (this.#abortSignalConsumed) { this.#retryer.cancel({ revert: true }); } else { this.#retryer.cancelRetry(); } } this.scheduleGc(); } this.#cache.notify({ type: "observerRemoved", query: this, observer }); } } getObserversCount() { return this.#observers.length; } invalidate() { if (!this.state.isInvalidated) { this.#dispatch({ type: "invalidate" }); } } fetch(options, fetchOptions) { if (this.state.fetchStatus !== "idle") { if (this.state.dataUpdatedAt && fetchOptions?.cancelRefetch) { this.cancel({ silent: true }); } else if (this.#promise) { this.#retryer?.continueRetry(); return this.#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 queryFnContext = { queryKey: this.queryKey, meta: this.meta }; const addSignalProperty = (object) => { Object.defineProperty(object, "signal", { enumerable: true, get: () => { this.#abortSignalConsumed = true; return abortController.signal; } }); }; addSignalProperty(queryFnContext); const fetchFn = () => { if (!this.options.queryFn) { return Promise.reject( new Error(`Missing queryFn: '${this.options.queryHash}'`) ); } this.#abortSignalConsumed = false; if (this.options.persister) { return this.options.persister( this.options.queryFn, queryFnContext, this ); } return this.options.queryFn( queryFnContext ); }; const context = { fetchOptions, options: this.options, queryKey: this.queryKey, state: this.state, fetchFn }; addSignalProperty(context); 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 onError = (error) => { if (!(isCancelledError(error) && error.silent)) { this.#dispatch({ type: "error", error }); } if (!isCancelledError(error)) { this.#cache.config.onError?.( error, this ); this.#cache.config.onSettled?.( this.state.data, error, this ); } if (!this.isFetchingOptimistic) { this.scheduleGc(); } this.isFetchingOptimistic = false; }; this.#retryer = createRetryer({ fn: context.fetchFn, abort: abortController.abort.bind(abortController), onSuccess: (data) => { if (typeof data === "undefined") { 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}` ); } onError(new Error(`${this.queryHash} data is undefined`)); return; } this.setData(data); this.#cache.config.onSuccess?.(data, this); this.#cache.config.onSettled?.( data, this.state.error, this ); if (!this.isFetchingOptimistic) { this.scheduleGc(); } this.isFetchingOptimistic = false; }, onError, 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 }); this.#promise = this.#retryer.promise; return this.#promise; } #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, fetchFailureCount: 0, fetchFailureReason: null, fetchMeta: action.meta ?? null, fetchStatus: canFetch(this.options.networkMode) ? "fetching" : "paused", ...!state.dataUpdatedAt && { error: null, status: "pending" } }; case "success": return { ...state, data: action.data, dataUpdateCount: state.dataUpdateCount + 1, dataUpdatedAt: action.dataUpdatedAt ?? Date.now(), error: null, isInvalidated: false, status: "success", ...!action.manual && { fetchStatus: "idle", fetchFailureCount: 0, fetchFailureReason: null } }; case "error": const error = action.error; if (isCancelledError(error) && error.revert && this.#revertState) { return { ...this.#revertState, fetchStatus: "idle" }; } return { ...state, error, errorUpdateCount: state.errorUpdateCount + 1, errorUpdatedAt: Date.now(), fetchFailureCount: state.fetchFailureCount + 1, fetchFailureReason: error, fetchStatus: "idle", status: "error" }; case "invalidate": return { ...state, isInvalidated: true }; case "setState": return { ...state, ...action.state }; } }; this.state = reducer(this.state); notifyManager.batch(() => { this.#observers.forEach((observer) => { observer.onQueryUpdate(); }); this.#cache.notify({ query: this, type: "updated", action }); }); } }; function getDefaultState(options) { const data = typeof options.initialData === "function" ? options.initialData() : options.initialData; const hasData = typeof data !== "undefined"; 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" }; } export { Query }; //# sourceMappingURL=query.js.map