@tanstack/query-core
Version:
The framework agnostic core that powers TanStack Query
443 lines • 13 kB
JavaScript
// src/query.ts
import {
ensureQueryFn,
noop,
replaceData,
resolveEnabled,
resolveStaleTime,
skipToken,
timeUntilStale
} from "./utils.js";
import { notifyManager } from "./notifyManager.js";
import { CancelledError, canFetch, createRetryer } from "./retryer.js";
import { Removable } from "./removable.js";
var Query = class extends Removable {
#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();
}
get meta() {
return this.options.meta;
}
get promise() {
return this.#retryer?.promise;
}
setOptions(options) {
this.options = { ...this.#defaultOptions, ...options };
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);
}
}
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.#retryer?.promise;
this.#retryer?.cancel(options);
return promise ? promise.then(noop).catch(noop) : Promise.resolve();
}
destroy() {
super.destroy();
this.cancel({ silent: true });
}
get resetState() {
return this.#initialState;
}
reset() {
this.destroy();
this.setState(this.resetState);
}
isActive() {
return this.observers.some(
(observer) => resolveEnabled(observer.options.enabled, this) !== false
);
}
isDisabled() {
if (this.getObserversCount() > 0) {
return !this.isActive();
}
return this.options.queryFn === skipToken || !this.isFetched();
}
isFetched() {
return this.state.dataUpdateCount + this.state.errorUpdateCount > 0;
}
isStatic() {
if (this.getObserversCount() > 0) {
return this.observers.some(
(observer) => resolveStaleTime(observer.options.staleTime, this) === "static"
);
}
return false;
}
isStale() {
if (this.getObserversCount() > 0) {
return this.observers.some(
(observer) => observer.getCurrentResult().isStale
);
}
return this.state.data === void 0 || this.state.isInvalidated;
}
isStaleByTime(staleTime = 0) {
if (this.state.data === void 0) {
return true;
}
if (staleTime === "static") {
return false;
}
if (this.state.isInvalidated) {
return true;
}
return !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.#isInitialPausedFetch()) {
this.#retryer.cancel({ revert: true });
} else {
this.#retryer.cancelRetry();
}
}
this.scheduleGc();
}
this.#cache.notify({ type: "observerRemoved", query: this, observer });
}
}
getObserversCount() {
return this.observers.length;
}
#isInitialPausedFetch() {
return this.state.fetchStatus === "paused" && this.state.status === "pending";
}
invalidate() {
if (!this.state.isInvalidated) {
this.#dispatch({ type: "invalidate" });
}
}
async fetch(options, fetchOptions) {
if (this.state.fetchStatus !== "idle" && // If the promise in the retryer is already rejected, we have to definitely
// re-start the fetch; there is a chance that the query is still in a
// pending state when that happens
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 = ensureQueryFn(this.options, fetchOptions);
const createQueryFnContext = () => {
const queryFnContext2 = {
client: this.#client,
queryKey: this.queryKey,
meta: this.meta
};
addSignalProperty(queryFnContext2);
return queryFnContext2;
};
const queryFnContext = createQueryFnContext();
this.#abortSignalConsumed = false;
if (this.options.persister) {
return this.options.persister(
queryFn,
queryFnContext,
this
);
}
return queryFn(queryFnContext);
};
const createFetchContext = () => {
const context2 = {
fetchOptions,
options: this.options,
queryKey: this.queryKey,
client: this.#client,
state: this.state,
fetchFn
};
addSignalProperty(context2);
return context2;
};
const context = createFetchContext();
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 });
}
this.#retryer = createRetryer({
initialPromise: fetchOptions?.initialPromise,
fn: context.fetchFn,
onCancel: (error) => {
if (error instanceof 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 this.#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 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 {
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",
// flag existing data as invalidated if we get a background error
// note that "no data" always means stale so we can set unconditionally here
isInvalidated: true
};
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 fetchState(data, options) {
return {
fetchFailureCount: 0,
fetchFailureReason: null,
fetchStatus: canFetch(options.networkMode) ? "fetching" : "paused",
...data === void 0 && {
error: null,
status: "pending"
}
};
}
function successState(data, dataUpdatedAt) {
return {
data,
dataUpdatedAt: dataUpdatedAt ?? Date.now(),
error: null,
isInvalidated: false,
status: "success"
};
}
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"
};
}
export {
Query,
fetchState
};
//# sourceMappingURL=query.js.map