UNPKG

@tanstack/query-core

Version:

The framework agnostic core that powers TanStack Query

339 lines (338 loc) 13.7 kB
import { timeoutManager } from "./timeoutManager.js"; import { isValidTimeout, noop, replaceData, resolveQueryValue, shallowEqualObjects, timeUntilStale } from "./utils.js"; import { isServer } from "./environmentManager.js"; import { Subscribable } from "./subscribable.js"; import { focusManager } from "./focusManager.js"; import { notifyManager } from "./notifyManager.js"; import { fetchState } from "./query.js"; //#region src/queryObserver.ts var QueryObserver = class extends Subscribable { #client; #currentQuery = void 0; #currentQueryInitialState = void 0; #currentResult = void 0; #currentResultState; #currentResultOptions; #selectError; #selectFn; #selectResult; #lastQueryWithDefinedData; #staleTimeoutId; #refetchIntervalId; #currentRefetchInterval; #trackedProps = /* @__PURE__ */ new Set(); constructor(client, options) { super(); this.options = options; this.#client = client; this.#selectError = null; this.bindMethods(); this.setOptions(options); } 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 resolveQueryValue(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 || resolveQueryValue(this.options.enabled, this.#currentQuery) !== resolveQueryValue(prevOptions.enabled, this.#currentQuery) || resolveQueryValue(this.options.staleTime, this.#currentQuery) !== resolveQueryValue(prevOptions.staleTime, this.#currentQuery))) this.#updateStaleTimeout(); const nextRefetchInterval = this.#computeRefetchInterval(); if (mounted && (this.#currentQuery !== prevQuery || resolveQueryValue(this.options.enabled, this.#currentQuery) !== resolveQueryValue(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 (!shallowEqualObjects(this.getCurrentResult(), 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); let unsubscribe = () => {}; let resolveEarly; const cachePromise = new Promise((resolve) => { resolveEarly = resolve; unsubscribe = this.#client.getQueryCache().subscribe((event) => { if (event.type === "updated" && event.query.queryHash === query.queryHash && query.state.data !== void 0) { unsubscribe(); resolve(this.createResult(query, defaultedOptions)); } }); }); return Promise.race([query.fetch().then(() => { const result = this.createResult(query, defaultedOptions); resolveEarly?.(result); return result; }).finally(() => { unsubscribe(); }), cachePromise]); } 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; } #shouldScheduleTimer(timeout) { return !isServer() && resolveQueryValue(this.options.enabled, this.#currentQuery) !== false && isValidTimeout(timeout); } #updateStaleTimeout() { this.#clearStaleTimeout(); const staleTime = resolveQueryValue(this.options.staleTime, this.#currentQuery); if (this.#currentResult.isStale || !this.#shouldScheduleTimer(staleTime)) return; const timeout = timeUntilStale(this.#currentResult.dataUpdatedAt, staleTime) + 1; this.#staleTimeoutId = timeoutManager.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 (this.#currentRefetchInterval === 0 || !this.#shouldScheduleTimer(this.#currentRefetchInterval)) return; this.#refetchIntervalId = timeoutManager.setInterval(() => { if (this.options.refetchIntervalInBackground || focusManager.isFocused()) this.#executeFetch(); }, this.#currentRefetchInterval); } #updateTimers() { this.#updateStaleTimeout(); this.#updateRefetchInterval(this.#computeRefetchInterval()); } #clearStaleTimeout() { if (this.#staleTimeoutId !== void 0) { timeoutManager.clearTimeout(this.#staleTimeoutId); this.#staleTimeoutId = void 0; } } #clearRefetchInterval() { if (this.#refetchIntervalId !== void 0) { timeoutManager.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 queryInitialState = query !== prevQuery ? 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; } } else if (data === void 0) this.#selectError = null; if (this.#selectError) { error = this.#selectError; data = this.#selectResult; errorUpdatedAt = Date.now(); status = "error"; isPlaceholderData = false; } const isFetching = newState.fetchStatus === "fetching"; const isPending = status === "pending"; const isError = status === "error"; const isLoading = isPending && isFetching; const hasData = data !== void 0; return { 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: query.isFetched(), 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, isEnabled: resolveQueryValue(options.enabled, query) !== false }; } 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; return this.#currentResult[typedKey] !== prevResult[typedKey] && includedProps.has(typedKey); }); }; const notifyListeners = shouldNotifyListeners(); notifyManager.batch(() => { if (notifyListeners) this.listeners.forEach((listener) => { listener(this.#currentResult); }); this.#client.getQueryCache().notify({ query: this.#currentQuery, type: "observerResultsUpdated" }); }); } #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(); } }; function shouldLoadOnMount(query, options) { return resolveQueryValue(options.enabled, query) !== false && query.state.data === void 0 && !(query.state.status === "error" && resolveQueryValue(options.retryOnMount, query) === 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 (resolveQueryValue(options.enabled, query) !== false && resolveQueryValue(options.staleTime, query) !== "static") { 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 || resolveQueryValue(prevOptions.enabled, query) === false) && (!options.suspense || query.state.status !== "error") && isStale(query, options); } function isStale(query, options) { return resolveQueryValue(options.enabled, query) !== false && query.isStaleByTime(resolveQueryValue(options.staleTime, query)); } //#endregion export { QueryObserver }; //# sourceMappingURL=queryObserver.js.map