@tanstack/query-core
Version:
The framework agnostic core that powers TanStack Query
463 lines • 16 kB
JavaScript
// src/queryObserver.ts
import { focusManager } from "./focusManager.js";
import { notifyManager } from "./notifyManager.js";
import { fetchState } from "./query.js";
import { Subscribable } from "./subscribable.js";
import { pendingThenable } from "./thenable.js";
import {
isServer,
isValidTimeout,
noop,
replaceData,
resolveEnabled,
resolveStaleTime,
shallowEqualObjects,
timeUntilStale
} from "./utils.js";
var QueryObserver = class extends Subscribable {
constructor(client, options) {
super();
this.options = options;
this.#client = client;
this.#selectError = null;
this.#currentThenable = pendingThenable();
if (!this.options.experimental_prefetchInRender) {
this.#currentThenable.reject(
new Error("experimental_prefetchInRender feature flag is not enabled")
);
}
this.bindMethods();
this.setOptions(options);
}
#client;
#currentQuery = void 0;
#currentQueryInitialState = void 0;
#currentResult = void 0;
#currentResultState;
#currentResultOptions;
#currentThenable;
#selectError;
#selectFn;
#selectResult;
// This property keeps track of the last query with defined data.
// It will be used to pass the previous data and query to the placeholder function between renders.
#lastQueryWithDefinedData;
#staleTimeoutId;
#refetchIntervalId;
#currentRefetchInterval;
#trackedProps = /* @__PURE__ */ new Set();
bindMethods() {
this.refetch = this.refetch.bind(this);
}
onSubscribe() {
if (this.listeners.size === 1) {
this.#currentQuery.addObserver(this);
if (shouldFetchOnMount(this.#currentQuery, this.options)) {
this.#executeFetch();
} else {
this.updateResult();
}
this.#updateTimers();
}
}
onUnsubscribe() {
if (!this.hasListeners()) {
this.destroy();
}
}
shouldFetchOnReconnect() {
return shouldFetchOn(
this.#currentQuery,
this.options,
this.options.refetchOnReconnect
);
}
shouldFetchOnWindowFocus() {
return shouldFetchOn(
this.#currentQuery,
this.options,
this.options.refetchOnWindowFocus
);
}
destroy() {
this.listeners = /* @__PURE__ */ new Set();
this.#clearStaleTimeout();
this.#clearRefetchInterval();
this.#currentQuery.removeObserver(this);
}
setOptions(options) {
const prevOptions = this.options;
const prevQuery = this.#currentQuery;
this.options = this.#client.defaultQueryOptions(options);
if (this.options.enabled !== void 0 && typeof this.options.enabled !== "boolean" && typeof this.options.enabled !== "function" && typeof resolveEnabled(this.options.enabled, this.#currentQuery) !== "boolean") {
throw new Error(
"Expected enabled to be a boolean or a callback that returns a boolean"
);
}
this.#updateQuery();
this.#currentQuery.setOptions(this.options);
if (prevOptions._defaulted && !shallowEqualObjects(this.options, prevOptions)) {
this.#client.getQueryCache().notify({
type: "observerOptionsUpdated",
query: this.#currentQuery,
observer: this
});
}
const mounted = this.hasListeners();
if (mounted && shouldFetchOptionally(
this.#currentQuery,
prevQuery,
this.options,
prevOptions
)) {
this.#executeFetch();
}
this.updateResult();
if (mounted && (this.#currentQuery !== prevQuery || resolveEnabled(this.options.enabled, this.#currentQuery) !== resolveEnabled(prevOptions.enabled, this.#currentQuery) || resolveStaleTime(this.options.staleTime, this.#currentQuery) !== resolveStaleTime(prevOptions.staleTime, this.#currentQuery))) {
this.#updateStaleTimeout();
}
const nextRefetchInterval = this.#computeRefetchInterval();
if (mounted && (this.#currentQuery !== prevQuery || resolveEnabled(this.options.enabled, this.#currentQuery) !== resolveEnabled(prevOptions.enabled, this.#currentQuery) || nextRefetchInterval !== this.#currentRefetchInterval)) {
this.#updateRefetchInterval(nextRefetchInterval);
}
}
getOptimisticResult(options) {
const query = this.#client.getQueryCache().build(this.#client, options);
const result = this.createResult(query, options);
if (shouldAssignObserverCurrentProperties(this, result)) {
this.#currentResult = result;
this.#currentResultOptions = this.options;
this.#currentResultState = this.#currentQuery.state;
}
return result;
}
getCurrentResult() {
return this.#currentResult;
}
trackResult(result, onPropTracked) {
return new Proxy(result, {
get: (target, key) => {
this.trackProp(key);
onPropTracked?.(key);
return Reflect.get(target, key);
}
});
}
trackProp(key) {
this.#trackedProps.add(key);
}
getCurrentQuery() {
return this.#currentQuery;
}
refetch({ ...options } = {}) {
return this.fetch({
...options
});
}
fetchOptimistic(options) {
const defaultedOptions = this.#client.defaultQueryOptions(options);
const query = this.#client.getQueryCache().build(this.#client, defaultedOptions);
return query.fetch().then(() => this.createResult(query, defaultedOptions));
}
fetch(fetchOptions) {
return this.#executeFetch({
...fetchOptions,
cancelRefetch: fetchOptions.cancelRefetch ?? true
}).then(() => {
this.updateResult();
return this.#currentResult;
});
}
#executeFetch(fetchOptions) {
this.#updateQuery();
let promise = this.#currentQuery.fetch(
this.options,
fetchOptions
);
if (!fetchOptions?.throwOnError) {
promise = promise.catch(noop);
}
return promise;
}
#updateStaleTimeout() {
this.#clearStaleTimeout();
const staleTime = resolveStaleTime(
this.options.staleTime,
this.#currentQuery
);
if (isServer || this.#currentResult.isStale || !isValidTimeout(staleTime)) {
return;
}
const time = timeUntilStale(this.#currentResult.dataUpdatedAt, staleTime);
const timeout = time + 1;
this.#staleTimeoutId = setTimeout(() => {
if (!this.#currentResult.isStale) {
this.updateResult();
}
}, timeout);
}
#computeRefetchInterval() {
return (typeof this.options.refetchInterval === "function" ? this.options.refetchInterval(this.#currentQuery) : this.options.refetchInterval) ?? false;
}
#updateRefetchInterval(nextInterval) {
this.#clearRefetchInterval();
this.#currentRefetchInterval = nextInterval;
if (isServer || resolveEnabled(this.options.enabled, this.#currentQuery) === false || !isValidTimeout(this.#currentRefetchInterval) || this.#currentRefetchInterval === 0) {
return;
}
this.#refetchIntervalId = setInterval(() => {
if (this.options.refetchIntervalInBackground || focusManager.isFocused()) {
this.#executeFetch();
}
}, this.#currentRefetchInterval);
}
#updateTimers() {
this.#updateStaleTimeout();
this.#updateRefetchInterval(this.#computeRefetchInterval());
}
#clearStaleTimeout() {
if (this.#staleTimeoutId) {
clearTimeout(this.#staleTimeoutId);
this.#staleTimeoutId = void 0;
}
}
#clearRefetchInterval() {
if (this.#refetchIntervalId) {
clearInterval(this.#refetchIntervalId);
this.#refetchIntervalId = void 0;
}
}
createResult(query, options) {
const prevQuery = this.#currentQuery;
const prevOptions = this.options;
const prevResult = this.#currentResult;
const prevResultState = this.#currentResultState;
const prevResultOptions = this.#currentResultOptions;
const queryChange = query !== prevQuery;
const queryInitialState = queryChange ? query.state : this.#currentQueryInitialState;
const { state } = query;
let newState = { ...state };
let isPlaceholderData = false;
let data;
if (options._optimisticResults) {
const mounted = this.hasListeners();
const fetchOnMount = !mounted && shouldFetchOnMount(query, options);
const fetchOptionally = mounted && shouldFetchOptionally(query, prevQuery, options, prevOptions);
if (fetchOnMount || fetchOptionally) {
newState = {
...newState,
...fetchState(state.data, query.options)
};
}
if (options._optimisticResults === "isRestoring") {
newState.fetchStatus = "idle";
}
}
let { error, errorUpdatedAt, status } = newState;
data = newState.data;
let skipSelect = false;
if (options.placeholderData !== void 0 && data === void 0 && status === "pending") {
let placeholderData;
if (prevResult?.isPlaceholderData && options.placeholderData === prevResultOptions?.placeholderData) {
placeholderData = prevResult.data;
skipSelect = true;
} else {
placeholderData = typeof options.placeholderData === "function" ? options.placeholderData(
this.#lastQueryWithDefinedData?.state.data,
this.#lastQueryWithDefinedData
) : options.placeholderData;
}
if (placeholderData !== void 0) {
status = "success";
data = replaceData(
prevResult?.data,
placeholderData,
options
);
isPlaceholderData = true;
}
}
if (options.select && data !== void 0 && !skipSelect) {
if (prevResult && data === prevResultState?.data && options.select === this.#selectFn) {
data = this.#selectResult;
} else {
try {
this.#selectFn = options.select;
data = options.select(data);
data = replaceData(prevResult?.data, data, options);
this.#selectResult = data;
this.#selectError = null;
} catch (selectError) {
this.#selectError = selectError;
}
}
}
if (this.#selectError) {
error = this.#selectError;
data = this.#selectResult;
errorUpdatedAt = Date.now();
status = "error";
}
const isFetching = newState.fetchStatus === "fetching";
const isPending = status === "pending";
const isError = status === "error";
const isLoading = isPending && isFetching;
const hasData = data !== void 0;
const result = {
status,
fetchStatus: newState.fetchStatus,
isPending,
isSuccess: status === "success",
isError,
isInitialLoading: isLoading,
isLoading,
data,
dataUpdatedAt: newState.dataUpdatedAt,
error,
errorUpdatedAt,
failureCount: newState.fetchFailureCount,
failureReason: newState.fetchFailureReason,
errorUpdateCount: newState.errorUpdateCount,
isFetched: newState.dataUpdateCount > 0 || newState.errorUpdateCount > 0,
isFetchedAfterMount: newState.dataUpdateCount > queryInitialState.dataUpdateCount || newState.errorUpdateCount > queryInitialState.errorUpdateCount,
isFetching,
isRefetching: isFetching && !isPending,
isLoadingError: isError && !hasData,
isPaused: newState.fetchStatus === "paused",
isPlaceholderData,
isRefetchError: isError && hasData,
isStale: isStale(query, options),
refetch: this.refetch,
promise: this.#currentThenable
};
const nextResult = result;
if (this.options.experimental_prefetchInRender) {
const finalizeThenableIfPossible = (thenable) => {
if (nextResult.status === "error") {
thenable.reject(nextResult.error);
} else if (nextResult.data !== void 0) {
thenable.resolve(nextResult.data);
}
};
const recreateThenable = () => {
const pending = this.#currentThenable = nextResult.promise = pendingThenable();
finalizeThenableIfPossible(pending);
};
const prevThenable = this.#currentThenable;
switch (prevThenable.status) {
case "pending":
if (query.queryHash === prevQuery.queryHash) {
finalizeThenableIfPossible(prevThenable);
}
break;
case "fulfilled":
if (nextResult.status === "error" || nextResult.data !== prevThenable.value) {
recreateThenable();
}
break;
case "rejected":
if (nextResult.status !== "error" || nextResult.error !== prevThenable.reason) {
recreateThenable();
}
break;
}
}
return nextResult;
}
updateResult() {
const prevResult = this.#currentResult;
const nextResult = this.createResult(this.#currentQuery, this.options);
this.#currentResultState = this.#currentQuery.state;
this.#currentResultOptions = this.options;
if (this.#currentResultState.data !== void 0) {
this.#lastQueryWithDefinedData = this.#currentQuery;
}
if (shallowEqualObjects(nextResult, prevResult)) {
return;
}
this.#currentResult = nextResult;
const shouldNotifyListeners = () => {
if (!prevResult) {
return true;
}
const { notifyOnChangeProps } = this.options;
const notifyOnChangePropsValue = typeof notifyOnChangeProps === "function" ? notifyOnChangeProps() : notifyOnChangeProps;
if (notifyOnChangePropsValue === "all" || !notifyOnChangePropsValue && !this.#trackedProps.size) {
return true;
}
const includedProps = new Set(
notifyOnChangePropsValue ?? this.#trackedProps
);
if (this.options.throwOnError) {
includedProps.add("error");
}
return Object.keys(this.#currentResult).some((key) => {
const typedKey = key;
const changed = this.#currentResult[typedKey] !== prevResult[typedKey];
return changed && includedProps.has(typedKey);
});
};
this.#notify({ listeners: shouldNotifyListeners() });
}
#updateQuery() {
const query = this.#client.getQueryCache().build(this.#client, this.options);
if (query === this.#currentQuery) {
return;
}
const prevQuery = this.#currentQuery;
this.#currentQuery = query;
this.#currentQueryInitialState = query.state;
if (this.hasListeners()) {
prevQuery?.removeObserver(this);
query.addObserver(this);
}
}
onQueryUpdate() {
this.updateResult();
if (this.hasListeners()) {
this.#updateTimers();
}
}
#notify(notifyOptions) {
notifyManager.batch(() => {
if (notifyOptions.listeners) {
this.listeners.forEach((listener) => {
listener(this.#currentResult);
});
}
this.#client.getQueryCache().notify({
query: this.#currentQuery,
type: "observerResultsUpdated"
});
});
}
};
function shouldLoadOnMount(query, options) {
return resolveEnabled(options.enabled, query) !== false && query.state.data === void 0 && !(query.state.status === "error" && options.retryOnMount === false);
}
function shouldFetchOnMount(query, options) {
return shouldLoadOnMount(query, options) || query.state.data !== void 0 && shouldFetchOn(query, options, options.refetchOnMount);
}
function shouldFetchOn(query, options, field) {
if (resolveEnabled(options.enabled, query) !== false) {
const value = typeof field === "function" ? field(query) : field;
return value === "always" || value !== false && isStale(query, options);
}
return false;
}
function shouldFetchOptionally(query, prevQuery, options, prevOptions) {
return (query !== prevQuery || resolveEnabled(prevOptions.enabled, query) === false) && (!options.suspense || query.state.status !== "error") && isStale(query, options);
}
function isStale(query, options) {
return resolveEnabled(options.enabled, query) !== false && query.isStaleByTime(resolveStaleTime(options.staleTime, query));
}
function shouldAssignObserverCurrentProperties(observer, optimisticResult) {
if (!shallowEqualObjects(observer.getCurrentResult(), optimisticResult)) {
return true;
}
return false;
}
export {
QueryObserver
};
//# sourceMappingURL=queryObserver.js.map