123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580 |
- import { shallowEqualObjects, noop, isServer, isValidTimeout, timeUntilStale, replaceData } from './utils.mjs';
- import { notifyManager } from './notifyManager.mjs';
- import { focusManager } from './focusManager.mjs';
- import { Subscribable } from './subscribable.mjs';
- import { canFetch, isCancelledError } from './retryer.mjs';
- 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.mjs.map
|