import { shallowEqualObjects, noop, isServer, isValidTimeout, timeUntilStale, replaceData } from './utils.esm.js'; import { notifyManager } from './notifyManager.esm.js'; import { focusManager } from './focusManager.esm.js'; import { Subscribable } from './subscribable.esm.js'; import { canFetch, isCancelledError } from './retryer.esm.js'; class QueryObserver extends Subscribable { constructor(client, options) { super(); this.client = client; this.options = options; this.trackedProps = new Set(); this.selectError = null; this.bindMethods(); this.setOptions(options); } bindMethods() { this.remove = this.remove.bind(this); this.refetch = this.refetch.bind(this); } onSubscribe() { if (this.listeners.size === 1) { this.currentQuery.addObserver(this); if (shouldFetchOnMount(this.currentQuery, this.options)) { this.executeFetch(); } 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 = new Set(); this.clearStaleTimeout(); this.clearRefetchInterval(); this.currentQuery.removeObserver(this); } setOptions(options, notifyOptions) { const prevOptions = this.options; const prevQuery = this.currentQuery; this.options = this.client.defaultQueryOptions(options); if (process.env.NODE_ENV !== 'production' && typeof (options == null ? void 0 : options.isDataEqual) !== 'undefined') { this.client.getLogger().error("The isDataEqual option has been deprecated and will be removed in the next major version. You can achieve the same functionality by passing a function as the structuralSharing option"); } if (!shallowEqualObjects(prevOptions, this.options)) { this.client.getQueryCache().notify({ type: 'observerOptionsUpdated', query: this.currentQuery, observer: this }); } if (typeof this.options.enabled !== 'undefined' && typeof this.options.enabled !== 'boolean') { throw new Error('Expected enabled to be a boolean'); } // Keep previous query key if the user does not supply one if (!this.options.queryKey) { this.options.queryKey = prevOptions.queryKey; } this.updateQuery(); const mounted = this.hasListeners(); // Fetch if there are subscribers if (mounted && shouldFetchOptionally(this.currentQuery, prevQuery, this.options, prevOptions)) { this.executeFetch(); } // Update result this.updateResult(notifyOptions); // Update stale interval if needed if (mounted && (this.currentQuery !== prevQuery || this.options.enabled !== prevOptions.enabled || this.options.staleTime !== prevOptions.staleTime)) { this.updateStaleTimeout(); } const nextRefetchInterval = this.computeRefetchInterval(); // Update refetch interval if needed if (mounted && (this.currentQuery !== prevQuery || this.options.enabled !== prevOptions.enabled || 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, options)) { // this assigns the optimistic result to the current Observer // because if the query function changes, useQuery will be performing // an effect where it would fetch again. // When the fetch finishes, we perform a deep data cloning in order // to reuse objects references. This deep data clone is performed against // the `observer.currentResult.data` property // When QueryKey changes, we refresh the query and get new `optimistic` // result, while we leave the `observer.currentResult`, so when new data // arrives, it finds the old `observer.currentResult` which is related // to the old QueryKey. Which means that currentResult and selectData are // out of sync already. // To solve this, we move the cursor of the currentResult everytime // an observer reads an optimistic value. // When keeping the previous data, the result doesn't change until new // data arrives. this.currentResult = result; this.currentResultOptions = this.options; this.currentResultState = this.currentQuery.state; } return result; } getCurrentResult() { return this.currentResult; } trackResult(result) { const trackedResult = {}; Object.keys(result).forEach(key => { Object.defineProperty(trackedResult, key, { configurable: false, enumerable: true, get: () => { this.trackedProps.add(key); return result[key]; } }); }); return trackedResult; } getCurrentQuery() { return this.currentQuery; } remove() { this.client.getQueryCache().remove(this.currentQuery); } refetch({ refetchPage, ...options } = {}) { return this.fetch({ ...options, meta: { refetchPage } }); } fetchOptimistic(options) { const defaultedOptions = this.client.defaultQueryOptions(options); const query = this.client.getQueryCache().build(this.client, defaultedOptions); query.isFetchingOptimistic = true; return query.fetch().then(() => this.createResult(query, defaultedOptions)); } fetch(fetchOptions) { var _fetchOptions$cancelR; return this.executeFetch({ ...fetchOptions, cancelRefetch: (_fetchOptions$cancelR = fetchOptions.cancelRefetch) != null ? _fetchOptions$cancelR : true }).then(() => { this.updateResult(); return this.currentResult; }); } executeFetch(fetchOptions) { // Make sure we reference the latest query as the current one might have been removed this.updateQuery(); // Fetch let promise = this.currentQuery.fetch(this.options, fetchOptions); if (!(fetchOptions != null && fetchOptions.throwOnError)) { promise = promise.catch(noop); } return promise; } updateStaleTimeout() { this.clearStaleTimeout(); if (isServer || this.currentResult.isStale || !isValidTimeout(this.options.staleTime)) { return; } const time = timeUntilStale(this.currentResult.dataUpdatedAt, this.options.staleTime); // The timeout is sometimes triggered 1 ms before the stale time expiration. // To mitigate this issue we always add 1 ms to the timeout. const timeout = time + 1; this.staleTimeoutId = setTimeout(() => { if (!this.currentResult.isStale) { this.updateResult(); } }, timeout); } computeRefetchInterval() { var _this$options$refetch; return typeof this.options.refetchInterval === 'function' ? this.options.refetchInterval(this.currentResult.data, this.currentQuery) : (_this$options$refetch = this.options.refetchInterval) != null ? _this$options$refetch : false; } updateRefetchInterval(nextInterval) { this.clearRefetchInterval(); this.currentRefetchInterval = nextInterval; if (isServer || this.options.enabled === 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 = undefined; } } clearRefetchInterval() { if (this.refetchIntervalId) { clearInterval(this.refetchIntervalId); this.refetchIntervalId = undefined; } } 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 prevQueryResult = queryChange ? this.currentResult : this.previousQueryResult; const { state } = query; let { dataUpdatedAt, error, errorUpdatedAt, fetchStatus, status } = state; let isPreviousData = false; let isPlaceholderData = false; let data; // Optimistically set result in fetching state if needed if (options._optimisticResults) { const mounted = this.hasListeners(); const fetchOnMount = !mounted && shouldFetchOnMount(query, options); const fetchOptionally = mounted && shouldFetchOptionally(query, prevQuery, options, prevOptions); if (fetchOnMount || fetchOptionally) { fetchStatus = canFetch(query.options.networkMode) ? 'fetching' : 'paused'; if (!dataUpdatedAt) { status = 'loading'; } } if (options._optimisticResults === 'isRestoring') { fetchStatus = 'idle'; } } // Keep previous data if needed if (options.keepPreviousData && !state.dataUpdatedAt && prevQueryResult != null && prevQueryResult.isSuccess && status !== 'error') { data = prevQueryResult.data; dataUpdatedAt = prevQueryResult.dataUpdatedAt; status = prevQueryResult.status; isPreviousData = true; } // Select data if needed else if (options.select && typeof state.data !== 'undefined') { // Memoize select result if (prevResult && state.data === (prevResultState == null ? void 0 : prevResultState.data) && options.select === this.selectFn) { data = this.selectResult; } else { try { this.selectFn = options.select; data = options.select(state.data); data = replaceData(prevResult == null ? void 0 : prevResult.data, data, options); this.selectResult = data; this.selectError = null; } catch (selectError) { if (process.env.NODE_ENV !== 'production') { this.client.getLogger().error(selectError); } this.selectError = selectError; } } } // Use query data else { data = state.data; } // Show placeholder data if needed if (typeof options.placeholderData !== 'undefined' && typeof data === 'undefined' && status === 'loading') { let placeholderData; // Memoize placeholder data if (prevResult != null && prevResult.isPlaceholderData && options.placeholderData === (prevResultOptions == null ? void 0 : prevResultOptions.placeholderData)) { placeholderData = prevResult.data; } else { placeholderData = typeof options.placeholderData === 'function' ? options.placeholderData() : options.placeholderData; if (options.select && typeof placeholderData !== 'undefined') { try { placeholderData = options.select(placeholderData); this.selectError = null; } catch (selectError) { if (process.env.NODE_ENV !== 'production') { this.client.getLogger().error(selectError); } this.selectError = selectError; } } } if (typeof placeholderData !== 'undefined') { status = 'success'; data = replaceData(prevResult == null ? void 0 : prevResult.data, placeholderData, options); isPlaceholderData = true; } } if (this.selectError) { error = this.selectError; data = this.selectResult; errorUpdatedAt = Date.now(); status = 'error'; } const isFetching = fetchStatus === 'fetching'; const isLoading = status === 'loading'; const isError = status === 'error'; const result = { status, fetchStatus, isLoading, isSuccess: status === 'success', isError, isInitialLoading: isLoading && isFetching, data, dataUpdatedAt, error, errorUpdatedAt, failureCount: state.fetchFailureCount, failureReason: state.fetchFailureReason, errorUpdateCount: state.errorUpdateCount, isFetched: state.dataUpdateCount > 0 || state.errorUpdateCount > 0, isFetchedAfterMount: state.dataUpdateCount > queryInitialState.dataUpdateCount || state.errorUpdateCount > queryInitialState.errorUpdateCount, isFetching, isRefetching: isFetching && !isLoading, isLoadingError: isError && state.dataUpdatedAt === 0, isPaused: fetchStatus === 'paused', isPlaceholderData, isPreviousData, isRefetchError: isError && state.dataUpdatedAt !== 0, isStale: isStale(query, options), refetch: this.refetch, remove: this.remove }; return result; } updateResult(notifyOptions) { const prevResult = this.currentResult; const nextResult = this.createResult(this.currentQuery, this.options); this.currentResultState = this.currentQuery.state; this.currentResultOptions = this.options; // Only notify and update result if something has changed if (shallowEqualObjects(nextResult, prevResult)) { return; } this.currentResult = nextResult; // Determine which callbacks to trigger const defaultNotifyOptions = { cache: true }; 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 != null ? notifyOnChangePropsValue : this.trackedProps); if (this.options.useErrorBoundary) { 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); }); }; if ((notifyOptions == null ? void 0 : notifyOptions.listeners) !== false && shouldNotifyListeners()) { defaultNotifyOptions.listeners = true; } this.notify({ ...defaultNotifyOptions, ...notifyOptions }); } 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; this.previousQueryResult = this.currentResult; if (this.hasListeners()) { prevQuery == null ? void 0 : prevQuery.removeObserver(this); query.addObserver(this); } } onQueryUpdate(action) { const notifyOptions = {}; if (action.type === 'success') { notifyOptions.onSuccess = !action.manual; } else if (action.type === 'error' && !isCancelledError(action.error)) { notifyOptions.onError = true; } this.updateResult(notifyOptions); if (this.hasListeners()) { this.updateTimers(); } } notify(notifyOptions) { notifyManager.batch(() => { // First trigger the configuration callbacks if (notifyOptions.onSuccess) { var _this$options$onSucce, _this$options, _this$options$onSettl, _this$options2; (_this$options$onSucce = (_this$options = this.options).onSuccess) == null ? void 0 : _this$options$onSucce.call(_this$options, this.currentResult.data); (_this$options$onSettl = (_this$options2 = this.options).onSettled) == null ? void 0 : _this$options$onSettl.call(_this$options2, this.currentResult.data, null); } else if (notifyOptions.onError) { var _this$options$onError, _this$options3, _this$options$onSettl2, _this$options4; (_this$options$onError = (_this$options3 = this.options).onError) == null ? void 0 : _this$options$onError.call(_this$options3, this.currentResult.error); (_this$options$onSettl2 = (_this$options4 = this.options).onSettled) == null ? void 0 : _this$options$onSettl2.call(_this$options4, undefined, this.currentResult.error); } // Then trigger the listeners if (notifyOptions.listeners) { this.listeners.forEach(({ listener }) => { listener(this.currentResult); }); } // Then the cache listeners if (notifyOptions.cache) { this.client.getQueryCache().notify({ query: this.currentQuery, type: 'observerResultsUpdated' }); } }); } } function shouldLoadOnMount(query, options) { return options.enabled !== false && !query.state.dataUpdatedAt && !(query.state.status === 'error' && options.retryOnMount === false); } function shouldFetchOnMount(query, options) { return shouldLoadOnMount(query, options) || query.state.dataUpdatedAt > 0 && shouldFetchOn(query, options, options.refetchOnMount); } function shouldFetchOn(query, options, field) { if (options.enabled !== 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 options.enabled !== false && (query !== prevQuery || prevOptions.enabled === false) && (!options.suspense || query.state.status !== 'error') && isStale(query, options); } function isStale(query, options) { return query.isStaleByTime(options.staleTime); } // this function would decide if we will update the observer's 'current' // properties after an optimistic reading via getOptimisticResult function shouldAssignObserverCurrentProperties(observer, optimisticResult, options) { // it is important to keep this condition like this for three reasons: // 1. It will get removed in the v5 // 2. it reads: don't update the properties if we want to keep the previous // data. // 3. The opposite condition (!options.keepPreviousData) would fallthrough // and will result in a bad decision if (options.keepPreviousData) { return false; } // this means we want to put some placeholder data when pending and queryKey // changed. if (options.placeholderData !== undefined) { // re-assign properties only if current data is placeholder data // which means that data did not arrive yet, so, if there is some cached data // we need to "prepare" to receive it return optimisticResult.isPlaceholderData; } // if the newly created result isn't what the observer is holding as current, // then we'll need to update the properties as well if (!shallowEqualObjects(observer.getCurrentResult(), optimisticResult)) { return true; } // basically, just keep previous properties if nothing changed return false; } export { QueryObserver }; //# sourceMappingURL=queryObserver.esm.js.map