query.mjs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. import { replaceData, noop, timeUntilStale, getAbortController } from './utils.mjs';
  2. import { defaultLogger } from './logger.mjs';
  3. import { notifyManager } from './notifyManager.mjs';
  4. import { createRetryer, isCancelledError, canFetch } from './retryer.mjs';
  5. import { Removable } from './removable.mjs';
  6. // CLASS
  7. class Query extends Removable {
  8. constructor(config) {
  9. super();
  10. this.abortSignalConsumed = false;
  11. this.defaultOptions = config.defaultOptions;
  12. this.setOptions(config.options);
  13. this.observers = [];
  14. this.cache = config.cache;
  15. this.logger = config.logger || defaultLogger;
  16. this.queryKey = config.queryKey;
  17. this.queryHash = config.queryHash;
  18. this.initialState = config.state || getDefaultState(this.options);
  19. this.state = this.initialState;
  20. this.scheduleGc();
  21. }
  22. get meta() {
  23. return this.options.meta;
  24. }
  25. setOptions(options) {
  26. this.options = { ...this.defaultOptions,
  27. ...options
  28. };
  29. this.updateCacheTime(this.options.cacheTime);
  30. }
  31. optionalRemove() {
  32. if (!this.observers.length && this.state.fetchStatus === 'idle') {
  33. this.cache.remove(this);
  34. }
  35. }
  36. setData(newData, options) {
  37. const data = replaceData(this.state.data, newData, this.options); // Set data and mark it as cached
  38. this.dispatch({
  39. data,
  40. type: 'success',
  41. dataUpdatedAt: options == null ? void 0 : options.updatedAt,
  42. manual: options == null ? void 0 : options.manual
  43. });
  44. return data;
  45. }
  46. setState(state, setStateOptions) {
  47. this.dispatch({
  48. type: 'setState',
  49. state,
  50. setStateOptions
  51. });
  52. }
  53. cancel(options) {
  54. var _this$retryer;
  55. const promise = this.promise;
  56. (_this$retryer = this.retryer) == null ? void 0 : _this$retryer.cancel(options);
  57. return promise ? promise.then(noop).catch(noop) : Promise.resolve();
  58. }
  59. destroy() {
  60. super.destroy();
  61. this.cancel({
  62. silent: true
  63. });
  64. }
  65. reset() {
  66. this.destroy();
  67. this.setState(this.initialState);
  68. }
  69. isActive() {
  70. return this.observers.some(observer => observer.options.enabled !== false);
  71. }
  72. isDisabled() {
  73. return this.getObserversCount() > 0 && !this.isActive();
  74. }
  75. isStale() {
  76. return this.state.isInvalidated || !this.state.dataUpdatedAt || this.observers.some(observer => observer.getCurrentResult().isStale);
  77. }
  78. isStaleByTime(staleTime = 0) {
  79. return this.state.isInvalidated || !this.state.dataUpdatedAt || !timeUntilStale(this.state.dataUpdatedAt, staleTime);
  80. }
  81. onFocus() {
  82. var _this$retryer2;
  83. const observer = this.observers.find(x => x.shouldFetchOnWindowFocus());
  84. if (observer) {
  85. observer.refetch({
  86. cancelRefetch: false
  87. });
  88. } // Continue fetch if currently paused
  89. (_this$retryer2 = this.retryer) == null ? void 0 : _this$retryer2.continue();
  90. }
  91. onOnline() {
  92. var _this$retryer3;
  93. const observer = this.observers.find(x => x.shouldFetchOnReconnect());
  94. if (observer) {
  95. observer.refetch({
  96. cancelRefetch: false
  97. });
  98. } // Continue fetch if currently paused
  99. (_this$retryer3 = this.retryer) == null ? void 0 : _this$retryer3.continue();
  100. }
  101. addObserver(observer) {
  102. if (!this.observers.includes(observer)) {
  103. this.observers.push(observer); // Stop the query from being garbage collected
  104. this.clearGcTimeout();
  105. this.cache.notify({
  106. type: 'observerAdded',
  107. query: this,
  108. observer
  109. });
  110. }
  111. }
  112. removeObserver(observer) {
  113. if (this.observers.includes(observer)) {
  114. this.observers = this.observers.filter(x => x !== observer);
  115. if (!this.observers.length) {
  116. // If the transport layer does not support cancellation
  117. // we'll let the query continue so the result can be cached
  118. if (this.retryer) {
  119. if (this.abortSignalConsumed) {
  120. this.retryer.cancel({
  121. revert: true
  122. });
  123. } else {
  124. this.retryer.cancelRetry();
  125. }
  126. }
  127. this.scheduleGc();
  128. }
  129. this.cache.notify({
  130. type: 'observerRemoved',
  131. query: this,
  132. observer
  133. });
  134. }
  135. }
  136. getObserversCount() {
  137. return this.observers.length;
  138. }
  139. invalidate() {
  140. if (!this.state.isInvalidated) {
  141. this.dispatch({
  142. type: 'invalidate'
  143. });
  144. }
  145. }
  146. fetch(options, fetchOptions) {
  147. var _this$options$behavio, _context$fetchOptions;
  148. if (this.state.fetchStatus !== 'idle') {
  149. if (this.state.dataUpdatedAt && fetchOptions != null && fetchOptions.cancelRefetch) {
  150. // Silently cancel current fetch if the user wants to cancel refetches
  151. this.cancel({
  152. silent: true
  153. });
  154. } else if (this.promise) {
  155. var _this$retryer4;
  156. // make sure that retries that were potentially cancelled due to unmounts can continue
  157. (_this$retryer4 = this.retryer) == null ? void 0 : _this$retryer4.continueRetry(); // Return current promise if we are already fetching
  158. return this.promise;
  159. }
  160. } // Update config if passed, otherwise the config from the last execution is used
  161. if (options) {
  162. this.setOptions(options);
  163. } // Use the options from the first observer with a query function if no function is found.
  164. // This can happen when the query is hydrated or created with setQueryData.
  165. if (!this.options.queryFn) {
  166. const observer = this.observers.find(x => x.options.queryFn);
  167. if (observer) {
  168. this.setOptions(observer.options);
  169. }
  170. }
  171. if (process.env.NODE_ENV !== 'production') {
  172. if (!Array.isArray(this.options.queryKey)) {
  173. this.logger.error("As of v4, queryKey needs to be an Array. If you are using a string like 'repoData', please change it to an Array, e.g. ['repoData']");
  174. }
  175. }
  176. const abortController = getAbortController(); // Create query function context
  177. const queryFnContext = {
  178. queryKey: this.queryKey,
  179. pageParam: undefined,
  180. meta: this.meta
  181. }; // Adds an enumerable signal property to the object that
  182. // which sets abortSignalConsumed to true when the signal
  183. // is read.
  184. const addSignalProperty = object => {
  185. Object.defineProperty(object, 'signal', {
  186. enumerable: true,
  187. get: () => {
  188. if (abortController) {
  189. this.abortSignalConsumed = true;
  190. return abortController.signal;
  191. }
  192. return undefined;
  193. }
  194. });
  195. };
  196. addSignalProperty(queryFnContext); // Create fetch function
  197. const fetchFn = () => {
  198. if (!this.options.queryFn) {
  199. return Promise.reject("Missing queryFn for queryKey '" + this.options.queryHash + "'");
  200. }
  201. this.abortSignalConsumed = false;
  202. return this.options.queryFn(queryFnContext);
  203. }; // Trigger behavior hook
  204. const context = {
  205. fetchOptions,
  206. options: this.options,
  207. queryKey: this.queryKey,
  208. state: this.state,
  209. fetchFn
  210. };
  211. addSignalProperty(context);
  212. (_this$options$behavio = this.options.behavior) == null ? void 0 : _this$options$behavio.onFetch(context); // Store state in case the current fetch needs to be reverted
  213. this.revertState = this.state; // Set to fetching state if not already in it
  214. if (this.state.fetchStatus === 'idle' || this.state.fetchMeta !== ((_context$fetchOptions = context.fetchOptions) == null ? void 0 : _context$fetchOptions.meta)) {
  215. var _context$fetchOptions2;
  216. this.dispatch({
  217. type: 'fetch',
  218. meta: (_context$fetchOptions2 = context.fetchOptions) == null ? void 0 : _context$fetchOptions2.meta
  219. });
  220. }
  221. const onError = error => {
  222. // Optimistically update state if needed
  223. if (!(isCancelledError(error) && error.silent)) {
  224. this.dispatch({
  225. type: 'error',
  226. error: error
  227. });
  228. }
  229. if (!isCancelledError(error)) {
  230. var _this$cache$config$on, _this$cache$config, _this$cache$config$on2, _this$cache$config2;
  231. // Notify cache callback
  232. (_this$cache$config$on = (_this$cache$config = this.cache.config).onError) == null ? void 0 : _this$cache$config$on.call(_this$cache$config, error, this);
  233. (_this$cache$config$on2 = (_this$cache$config2 = this.cache.config).onSettled) == null ? void 0 : _this$cache$config$on2.call(_this$cache$config2, this.state.data, error, this);
  234. if (process.env.NODE_ENV !== 'production') {
  235. this.logger.error(error);
  236. }
  237. }
  238. if (!this.isFetchingOptimistic) {
  239. // Schedule query gc after fetching
  240. this.scheduleGc();
  241. }
  242. this.isFetchingOptimistic = false;
  243. }; // Try to fetch the data
  244. this.retryer = createRetryer({
  245. fn: context.fetchFn,
  246. abort: abortController == null ? void 0 : abortController.abort.bind(abortController),
  247. onSuccess: data => {
  248. var _this$cache$config$on3, _this$cache$config3, _this$cache$config$on4, _this$cache$config4;
  249. if (typeof data === 'undefined') {
  250. if (process.env.NODE_ENV !== 'production') {
  251. this.logger.error("Query data cannot be undefined. Please make sure to return a value other than undefined from your query function. Affected query key: " + this.queryHash);
  252. }
  253. onError(new Error(this.queryHash + " data is undefined"));
  254. return;
  255. }
  256. this.setData(data); // Notify cache callback
  257. (_this$cache$config$on3 = (_this$cache$config3 = this.cache.config).onSuccess) == null ? void 0 : _this$cache$config$on3.call(_this$cache$config3, data, this);
  258. (_this$cache$config$on4 = (_this$cache$config4 = this.cache.config).onSettled) == null ? void 0 : _this$cache$config$on4.call(_this$cache$config4, data, this.state.error, this);
  259. if (!this.isFetchingOptimistic) {
  260. // Schedule query gc after fetching
  261. this.scheduleGc();
  262. }
  263. this.isFetchingOptimistic = false;
  264. },
  265. onError,
  266. onFail: (failureCount, error) => {
  267. this.dispatch({
  268. type: 'failed',
  269. failureCount,
  270. error
  271. });
  272. },
  273. onPause: () => {
  274. this.dispatch({
  275. type: 'pause'
  276. });
  277. },
  278. onContinue: () => {
  279. this.dispatch({
  280. type: 'continue'
  281. });
  282. },
  283. retry: context.options.retry,
  284. retryDelay: context.options.retryDelay,
  285. networkMode: context.options.networkMode
  286. });
  287. this.promise = this.retryer.promise;
  288. return this.promise;
  289. }
  290. dispatch(action) {
  291. const reducer = state => {
  292. var _action$meta, _action$dataUpdatedAt;
  293. switch (action.type) {
  294. case 'failed':
  295. return { ...state,
  296. fetchFailureCount: action.failureCount,
  297. fetchFailureReason: action.error
  298. };
  299. case 'pause':
  300. return { ...state,
  301. fetchStatus: 'paused'
  302. };
  303. case 'continue':
  304. return { ...state,
  305. fetchStatus: 'fetching'
  306. };
  307. case 'fetch':
  308. return { ...state,
  309. fetchFailureCount: 0,
  310. fetchFailureReason: null,
  311. fetchMeta: (_action$meta = action.meta) != null ? _action$meta : null,
  312. fetchStatus: canFetch(this.options.networkMode) ? 'fetching' : 'paused',
  313. ...(!state.dataUpdatedAt && {
  314. error: null,
  315. status: 'loading'
  316. })
  317. };
  318. case 'success':
  319. return { ...state,
  320. data: action.data,
  321. dataUpdateCount: state.dataUpdateCount + 1,
  322. dataUpdatedAt: (_action$dataUpdatedAt = action.dataUpdatedAt) != null ? _action$dataUpdatedAt : Date.now(),
  323. error: null,
  324. isInvalidated: false,
  325. status: 'success',
  326. ...(!action.manual && {
  327. fetchStatus: 'idle',
  328. fetchFailureCount: 0,
  329. fetchFailureReason: null
  330. })
  331. };
  332. case 'error':
  333. const error = action.error;
  334. if (isCancelledError(error) && error.revert && this.revertState) {
  335. return { ...this.revertState,
  336. fetchStatus: 'idle'
  337. };
  338. }
  339. return { ...state,
  340. error: error,
  341. errorUpdateCount: state.errorUpdateCount + 1,
  342. errorUpdatedAt: Date.now(),
  343. fetchFailureCount: state.fetchFailureCount + 1,
  344. fetchFailureReason: error,
  345. fetchStatus: 'idle',
  346. status: 'error'
  347. };
  348. case 'invalidate':
  349. return { ...state,
  350. isInvalidated: true
  351. };
  352. case 'setState':
  353. return { ...state,
  354. ...action.state
  355. };
  356. }
  357. };
  358. this.state = reducer(this.state);
  359. notifyManager.batch(() => {
  360. this.observers.forEach(observer => {
  361. observer.onQueryUpdate(action);
  362. });
  363. this.cache.notify({
  364. query: this,
  365. type: 'updated',
  366. action
  367. });
  368. });
  369. }
  370. }
  371. function getDefaultState(options) {
  372. const data = typeof options.initialData === 'function' ? options.initialData() : options.initialData;
  373. const hasData = typeof data !== 'undefined';
  374. const initialDataUpdatedAt = hasData ? typeof options.initialDataUpdatedAt === 'function' ? options.initialDataUpdatedAt() : options.initialDataUpdatedAt : 0;
  375. return {
  376. data,
  377. dataUpdateCount: 0,
  378. dataUpdatedAt: hasData ? initialDataUpdatedAt != null ? initialDataUpdatedAt : Date.now() : 0,
  379. error: null,
  380. errorUpdateCount: 0,
  381. errorUpdatedAt: 0,
  382. fetchFailureCount: 0,
  383. fetchFailureReason: null,
  384. fetchMeta: null,
  385. isInvalidated: false,
  386. status: hasData ? 'success' : 'loading',
  387. fetchStatus: 'idle'
  388. };
  389. }
  390. export { Query };
  391. //# sourceMappingURL=query.mjs.map