queryObserver.mjs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. import { shallowEqualObjects, noop, isServer, isValidTimeout, timeUntilStale, replaceData } from './utils.mjs';
  2. import { notifyManager } from './notifyManager.mjs';
  3. import { focusManager } from './focusManager.mjs';
  4. import { Subscribable } from './subscribable.mjs';
  5. import { canFetch, isCancelledError } from './retryer.mjs';
  6. class QueryObserver extends Subscribable {
  7. constructor(client, options) {
  8. super();
  9. this.client = client;
  10. this.options = options;
  11. this.trackedProps = new Set();
  12. this.selectError = null;
  13. this.bindMethods();
  14. this.setOptions(options);
  15. }
  16. bindMethods() {
  17. this.remove = this.remove.bind(this);
  18. this.refetch = this.refetch.bind(this);
  19. }
  20. onSubscribe() {
  21. if (this.listeners.size === 1) {
  22. this.currentQuery.addObserver(this);
  23. if (shouldFetchOnMount(this.currentQuery, this.options)) {
  24. this.executeFetch();
  25. }
  26. this.updateTimers();
  27. }
  28. }
  29. onUnsubscribe() {
  30. if (!this.hasListeners()) {
  31. this.destroy();
  32. }
  33. }
  34. shouldFetchOnReconnect() {
  35. return shouldFetchOn(this.currentQuery, this.options, this.options.refetchOnReconnect);
  36. }
  37. shouldFetchOnWindowFocus() {
  38. return shouldFetchOn(this.currentQuery, this.options, this.options.refetchOnWindowFocus);
  39. }
  40. destroy() {
  41. this.listeners = new Set();
  42. this.clearStaleTimeout();
  43. this.clearRefetchInterval();
  44. this.currentQuery.removeObserver(this);
  45. }
  46. setOptions(options, notifyOptions) {
  47. const prevOptions = this.options;
  48. const prevQuery = this.currentQuery;
  49. this.options = this.client.defaultQueryOptions(options);
  50. if (process.env.NODE_ENV !== 'production' && typeof (options == null ? void 0 : options.isDataEqual) !== 'undefined') {
  51. 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");
  52. }
  53. if (!shallowEqualObjects(prevOptions, this.options)) {
  54. this.client.getQueryCache().notify({
  55. type: 'observerOptionsUpdated',
  56. query: this.currentQuery,
  57. observer: this
  58. });
  59. }
  60. if (typeof this.options.enabled !== 'undefined' && typeof this.options.enabled !== 'boolean') {
  61. throw new Error('Expected enabled to be a boolean');
  62. } // Keep previous query key if the user does not supply one
  63. if (!this.options.queryKey) {
  64. this.options.queryKey = prevOptions.queryKey;
  65. }
  66. this.updateQuery();
  67. const mounted = this.hasListeners(); // Fetch if there are subscribers
  68. if (mounted && shouldFetchOptionally(this.currentQuery, prevQuery, this.options, prevOptions)) {
  69. this.executeFetch();
  70. } // Update result
  71. this.updateResult(notifyOptions); // Update stale interval if needed
  72. if (mounted && (this.currentQuery !== prevQuery || this.options.enabled !== prevOptions.enabled || this.options.staleTime !== prevOptions.staleTime)) {
  73. this.updateStaleTimeout();
  74. }
  75. const nextRefetchInterval = this.computeRefetchInterval(); // Update refetch interval if needed
  76. if (mounted && (this.currentQuery !== prevQuery || this.options.enabled !== prevOptions.enabled || nextRefetchInterval !== this.currentRefetchInterval)) {
  77. this.updateRefetchInterval(nextRefetchInterval);
  78. }
  79. }
  80. getOptimisticResult(options) {
  81. const query = this.client.getQueryCache().build(this.client, options);
  82. const result = this.createResult(query, options);
  83. if (shouldAssignObserverCurrentProperties(this, result, options)) {
  84. // this assigns the optimistic result to the current Observer
  85. // because if the query function changes, useQuery will be performing
  86. // an effect where it would fetch again.
  87. // When the fetch finishes, we perform a deep data cloning in order
  88. // to reuse objects references. This deep data clone is performed against
  89. // the `observer.currentResult.data` property
  90. // When QueryKey changes, we refresh the query and get new `optimistic`
  91. // result, while we leave the `observer.currentResult`, so when new data
  92. // arrives, it finds the old `observer.currentResult` which is related
  93. // to the old QueryKey. Which means that currentResult and selectData are
  94. // out of sync already.
  95. // To solve this, we move the cursor of the currentResult everytime
  96. // an observer reads an optimistic value.
  97. // When keeping the previous data, the result doesn't change until new
  98. // data arrives.
  99. this.currentResult = result;
  100. this.currentResultOptions = this.options;
  101. this.currentResultState = this.currentQuery.state;
  102. }
  103. return result;
  104. }
  105. getCurrentResult() {
  106. return this.currentResult;
  107. }
  108. trackResult(result) {
  109. const trackedResult = {};
  110. Object.keys(result).forEach(key => {
  111. Object.defineProperty(trackedResult, key, {
  112. configurable: false,
  113. enumerable: true,
  114. get: () => {
  115. this.trackedProps.add(key);
  116. return result[key];
  117. }
  118. });
  119. });
  120. return trackedResult;
  121. }
  122. getCurrentQuery() {
  123. return this.currentQuery;
  124. }
  125. remove() {
  126. this.client.getQueryCache().remove(this.currentQuery);
  127. }
  128. refetch({
  129. refetchPage,
  130. ...options
  131. } = {}) {
  132. return this.fetch({ ...options,
  133. meta: {
  134. refetchPage
  135. }
  136. });
  137. }
  138. fetchOptimistic(options) {
  139. const defaultedOptions = this.client.defaultQueryOptions(options);
  140. const query = this.client.getQueryCache().build(this.client, defaultedOptions);
  141. query.isFetchingOptimistic = true;
  142. return query.fetch().then(() => this.createResult(query, defaultedOptions));
  143. }
  144. fetch(fetchOptions) {
  145. var _fetchOptions$cancelR;
  146. return this.executeFetch({ ...fetchOptions,
  147. cancelRefetch: (_fetchOptions$cancelR = fetchOptions.cancelRefetch) != null ? _fetchOptions$cancelR : true
  148. }).then(() => {
  149. this.updateResult();
  150. return this.currentResult;
  151. });
  152. }
  153. executeFetch(fetchOptions) {
  154. // Make sure we reference the latest query as the current one might have been removed
  155. this.updateQuery(); // Fetch
  156. let promise = this.currentQuery.fetch(this.options, fetchOptions);
  157. if (!(fetchOptions != null && fetchOptions.throwOnError)) {
  158. promise = promise.catch(noop);
  159. }
  160. return promise;
  161. }
  162. updateStaleTimeout() {
  163. this.clearStaleTimeout();
  164. if (isServer || this.currentResult.isStale || !isValidTimeout(this.options.staleTime)) {
  165. return;
  166. }
  167. const time = timeUntilStale(this.currentResult.dataUpdatedAt, this.options.staleTime); // The timeout is sometimes triggered 1 ms before the stale time expiration.
  168. // To mitigate this issue we always add 1 ms to the timeout.
  169. const timeout = time + 1;
  170. this.staleTimeoutId = setTimeout(() => {
  171. if (!this.currentResult.isStale) {
  172. this.updateResult();
  173. }
  174. }, timeout);
  175. }
  176. computeRefetchInterval() {
  177. var _this$options$refetch;
  178. 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;
  179. }
  180. updateRefetchInterval(nextInterval) {
  181. this.clearRefetchInterval();
  182. this.currentRefetchInterval = nextInterval;
  183. if (isServer || this.options.enabled === false || !isValidTimeout(this.currentRefetchInterval) || this.currentRefetchInterval === 0) {
  184. return;
  185. }
  186. this.refetchIntervalId = setInterval(() => {
  187. if (this.options.refetchIntervalInBackground || focusManager.isFocused()) {
  188. this.executeFetch();
  189. }
  190. }, this.currentRefetchInterval);
  191. }
  192. updateTimers() {
  193. this.updateStaleTimeout();
  194. this.updateRefetchInterval(this.computeRefetchInterval());
  195. }
  196. clearStaleTimeout() {
  197. if (this.staleTimeoutId) {
  198. clearTimeout(this.staleTimeoutId);
  199. this.staleTimeoutId = undefined;
  200. }
  201. }
  202. clearRefetchInterval() {
  203. if (this.refetchIntervalId) {
  204. clearInterval(this.refetchIntervalId);
  205. this.refetchIntervalId = undefined;
  206. }
  207. }
  208. createResult(query, options) {
  209. const prevQuery = this.currentQuery;
  210. const prevOptions = this.options;
  211. const prevResult = this.currentResult;
  212. const prevResultState = this.currentResultState;
  213. const prevResultOptions = this.currentResultOptions;
  214. const queryChange = query !== prevQuery;
  215. const queryInitialState = queryChange ? query.state : this.currentQueryInitialState;
  216. const prevQueryResult = queryChange ? this.currentResult : this.previousQueryResult;
  217. const {
  218. state
  219. } = query;
  220. let {
  221. dataUpdatedAt,
  222. error,
  223. errorUpdatedAt,
  224. fetchStatus,
  225. status
  226. } = state;
  227. let isPreviousData = false;
  228. let isPlaceholderData = false;
  229. let data; // Optimistically set result in fetching state if needed
  230. if (options._optimisticResults) {
  231. const mounted = this.hasListeners();
  232. const fetchOnMount = !mounted && shouldFetchOnMount(query, options);
  233. const fetchOptionally = mounted && shouldFetchOptionally(query, prevQuery, options, prevOptions);
  234. if (fetchOnMount || fetchOptionally) {
  235. fetchStatus = canFetch(query.options.networkMode) ? 'fetching' : 'paused';
  236. if (!dataUpdatedAt) {
  237. status = 'loading';
  238. }
  239. }
  240. if (options._optimisticResults === 'isRestoring') {
  241. fetchStatus = 'idle';
  242. }
  243. } // Keep previous data if needed
  244. if (options.keepPreviousData && !state.dataUpdatedAt && prevQueryResult != null && prevQueryResult.isSuccess && status !== 'error') {
  245. data = prevQueryResult.data;
  246. dataUpdatedAt = prevQueryResult.dataUpdatedAt;
  247. status = prevQueryResult.status;
  248. isPreviousData = true;
  249. } // Select data if needed
  250. else if (options.select && typeof state.data !== 'undefined') {
  251. // Memoize select result
  252. if (prevResult && state.data === (prevResultState == null ? void 0 : prevResultState.data) && options.select === this.selectFn) {
  253. data = this.selectResult;
  254. } else {
  255. try {
  256. this.selectFn = options.select;
  257. data = options.select(state.data);
  258. data = replaceData(prevResult == null ? void 0 : prevResult.data, data, options);
  259. this.selectResult = data;
  260. this.selectError = null;
  261. } catch (selectError) {
  262. if (process.env.NODE_ENV !== 'production') {
  263. this.client.getLogger().error(selectError);
  264. }
  265. this.selectError = selectError;
  266. }
  267. }
  268. } // Use query data
  269. else {
  270. data = state.data;
  271. } // Show placeholder data if needed
  272. if (typeof options.placeholderData !== 'undefined' && typeof data === 'undefined' && status === 'loading') {
  273. let placeholderData; // Memoize placeholder data
  274. if (prevResult != null && prevResult.isPlaceholderData && options.placeholderData === (prevResultOptions == null ? void 0 : prevResultOptions.placeholderData)) {
  275. placeholderData = prevResult.data;
  276. } else {
  277. placeholderData = typeof options.placeholderData === 'function' ? options.placeholderData() : options.placeholderData;
  278. if (options.select && typeof placeholderData !== 'undefined') {
  279. try {
  280. placeholderData = options.select(placeholderData);
  281. this.selectError = null;
  282. } catch (selectError) {
  283. if (process.env.NODE_ENV !== 'production') {
  284. this.client.getLogger().error(selectError);
  285. }
  286. this.selectError = selectError;
  287. }
  288. }
  289. }
  290. if (typeof placeholderData !== 'undefined') {
  291. status = 'success';
  292. data = replaceData(prevResult == null ? void 0 : prevResult.data, placeholderData, options);
  293. isPlaceholderData = true;
  294. }
  295. }
  296. if (this.selectError) {
  297. error = this.selectError;
  298. data = this.selectResult;
  299. errorUpdatedAt = Date.now();
  300. status = 'error';
  301. }
  302. const isFetching = fetchStatus === 'fetching';
  303. const isLoading = status === 'loading';
  304. const isError = status === 'error';
  305. const result = {
  306. status,
  307. fetchStatus,
  308. isLoading,
  309. isSuccess: status === 'success',
  310. isError,
  311. isInitialLoading: isLoading && isFetching,
  312. data,
  313. dataUpdatedAt,
  314. error,
  315. errorUpdatedAt,
  316. failureCount: state.fetchFailureCount,
  317. failureReason: state.fetchFailureReason,
  318. errorUpdateCount: state.errorUpdateCount,
  319. isFetched: state.dataUpdateCount > 0 || state.errorUpdateCount > 0,
  320. isFetchedAfterMount: state.dataUpdateCount > queryInitialState.dataUpdateCount || state.errorUpdateCount > queryInitialState.errorUpdateCount,
  321. isFetching,
  322. isRefetching: isFetching && !isLoading,
  323. isLoadingError: isError && state.dataUpdatedAt === 0,
  324. isPaused: fetchStatus === 'paused',
  325. isPlaceholderData,
  326. isPreviousData,
  327. isRefetchError: isError && state.dataUpdatedAt !== 0,
  328. isStale: isStale(query, options),
  329. refetch: this.refetch,
  330. remove: this.remove
  331. };
  332. return result;
  333. }
  334. updateResult(notifyOptions) {
  335. const prevResult = this.currentResult;
  336. const nextResult = this.createResult(this.currentQuery, this.options);
  337. this.currentResultState = this.currentQuery.state;
  338. this.currentResultOptions = this.options; // Only notify and update result if something has changed
  339. if (shallowEqualObjects(nextResult, prevResult)) {
  340. return;
  341. }
  342. this.currentResult = nextResult; // Determine which callbacks to trigger
  343. const defaultNotifyOptions = {
  344. cache: true
  345. };
  346. const shouldNotifyListeners = () => {
  347. if (!prevResult) {
  348. return true;
  349. }
  350. const {
  351. notifyOnChangeProps
  352. } = this.options;
  353. const notifyOnChangePropsValue = typeof notifyOnChangeProps === 'function' ? notifyOnChangeProps() : notifyOnChangeProps;
  354. if (notifyOnChangePropsValue === 'all' || !notifyOnChangePropsValue && !this.trackedProps.size) {
  355. return true;
  356. }
  357. const includedProps = new Set(notifyOnChangePropsValue != null ? notifyOnChangePropsValue : this.trackedProps);
  358. if (this.options.useErrorBoundary) {
  359. includedProps.add('error');
  360. }
  361. return Object.keys(this.currentResult).some(key => {
  362. const typedKey = key;
  363. const changed = this.currentResult[typedKey] !== prevResult[typedKey];
  364. return changed && includedProps.has(typedKey);
  365. });
  366. };
  367. if ((notifyOptions == null ? void 0 : notifyOptions.listeners) !== false && shouldNotifyListeners()) {
  368. defaultNotifyOptions.listeners = true;
  369. }
  370. this.notify({ ...defaultNotifyOptions,
  371. ...notifyOptions
  372. });
  373. }
  374. updateQuery() {
  375. const query = this.client.getQueryCache().build(this.client, this.options);
  376. if (query === this.currentQuery) {
  377. return;
  378. }
  379. const prevQuery = this.currentQuery;
  380. this.currentQuery = query;
  381. this.currentQueryInitialState = query.state;
  382. this.previousQueryResult = this.currentResult;
  383. if (this.hasListeners()) {
  384. prevQuery == null ? void 0 : prevQuery.removeObserver(this);
  385. query.addObserver(this);
  386. }
  387. }
  388. onQueryUpdate(action) {
  389. const notifyOptions = {};
  390. if (action.type === 'success') {
  391. notifyOptions.onSuccess = !action.manual;
  392. } else if (action.type === 'error' && !isCancelledError(action.error)) {
  393. notifyOptions.onError = true;
  394. }
  395. this.updateResult(notifyOptions);
  396. if (this.hasListeners()) {
  397. this.updateTimers();
  398. }
  399. }
  400. notify(notifyOptions) {
  401. notifyManager.batch(() => {
  402. // First trigger the configuration callbacks
  403. if (notifyOptions.onSuccess) {
  404. var _this$options$onSucce, _this$options, _this$options$onSettl, _this$options2;
  405. (_this$options$onSucce = (_this$options = this.options).onSuccess) == null ? void 0 : _this$options$onSucce.call(_this$options, this.currentResult.data);
  406. (_this$options$onSettl = (_this$options2 = this.options).onSettled) == null ? void 0 : _this$options$onSettl.call(_this$options2, this.currentResult.data, null);
  407. } else if (notifyOptions.onError) {
  408. var _this$options$onError, _this$options3, _this$options$onSettl2, _this$options4;
  409. (_this$options$onError = (_this$options3 = this.options).onError) == null ? void 0 : _this$options$onError.call(_this$options3, this.currentResult.error);
  410. (_this$options$onSettl2 = (_this$options4 = this.options).onSettled) == null ? void 0 : _this$options$onSettl2.call(_this$options4, undefined, this.currentResult.error);
  411. } // Then trigger the listeners
  412. if (notifyOptions.listeners) {
  413. this.listeners.forEach(({
  414. listener
  415. }) => {
  416. listener(this.currentResult);
  417. });
  418. } // Then the cache listeners
  419. if (notifyOptions.cache) {
  420. this.client.getQueryCache().notify({
  421. query: this.currentQuery,
  422. type: 'observerResultsUpdated'
  423. });
  424. }
  425. });
  426. }
  427. }
  428. function shouldLoadOnMount(query, options) {
  429. return options.enabled !== false && !query.state.dataUpdatedAt && !(query.state.status === 'error' && options.retryOnMount === false);
  430. }
  431. function shouldFetchOnMount(query, options) {
  432. return shouldLoadOnMount(query, options) || query.state.dataUpdatedAt > 0 && shouldFetchOn(query, options, options.refetchOnMount);
  433. }
  434. function shouldFetchOn(query, options, field) {
  435. if (options.enabled !== false) {
  436. const value = typeof field === 'function' ? field(query) : field;
  437. return value === 'always' || value !== false && isStale(query, options);
  438. }
  439. return false;
  440. }
  441. function shouldFetchOptionally(query, prevQuery, options, prevOptions) {
  442. return options.enabled !== false && (query !== prevQuery || prevOptions.enabled === false) && (!options.suspense || query.state.status !== 'error') && isStale(query, options);
  443. }
  444. function isStale(query, options) {
  445. return query.isStaleByTime(options.staleTime);
  446. } // this function would decide if we will update the observer's 'current'
  447. // properties after an optimistic reading via getOptimisticResult
  448. function shouldAssignObserverCurrentProperties(observer, optimisticResult, options) {
  449. // it is important to keep this condition like this for three reasons:
  450. // 1. It will get removed in the v5
  451. // 2. it reads: don't update the properties if we want to keep the previous
  452. // data.
  453. // 3. The opposite condition (!options.keepPreviousData) would fallthrough
  454. // and will result in a bad decision
  455. if (options.keepPreviousData) {
  456. return false;
  457. } // this means we want to put some placeholder data when pending and queryKey
  458. // changed.
  459. if (options.placeholderData !== undefined) {
  460. // re-assign properties only if current data is placeholder data
  461. // which means that data did not arrive yet, so, if there is some cached data
  462. // we need to "prepare" to receive it
  463. return optimisticResult.isPlaceholderData;
  464. } // if the newly created result isn't what the observer is holding as current,
  465. // then we'll need to update the properties as well
  466. if (!shallowEqualObjects(observer.getCurrentResult(), optimisticResult)) {
  467. return true;
  468. } // basically, just keep previous properties if nothing changed
  469. return false;
  470. }
  471. export { QueryObserver };
  472. //# sourceMappingURL=queryObserver.mjs.map