From 0499b556bf1fa295107c7e2f2b1776564fd8675d Mon Sep 17 00:00:00 2001 From: Prateek Surana Date: Sat, 20 Nov 2021 12:54:17 +0530 Subject: [PATCH 01/10] :fire: Remove the cancel method on promise for cancelling promise --- src/core/infiniteQueryBehavior.ts | 10 +--------- src/core/retryer.ts | 7 ------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/core/infiniteQueryBehavior.ts b/src/core/infiniteQueryBehavior.ts index 0bd46be8e0..f588e7a4e1 100644 --- a/src/core/infiniteQueryBehavior.ts +++ b/src/core/infiniteQueryBehavior.ts @@ -1,5 +1,5 @@ import type { QueryBehavior } from './query' -import { isCancelable } from './retryer' + import type { InfiniteData, QueryFunctionContext, @@ -73,11 +73,6 @@ export function infiniteQueryBehavior< buildNewPages(pages, param, page, previous) ) - if (isCancelable(queryFnResult)) { - const promiseAsAny = promise as any - promiseAsAny.cancel = queryFnResult.cancel - } - return promise } @@ -153,9 +148,6 @@ export function infiniteQueryBehavior< finalPromiseAsAny.cancel = () => { cancelled = true abortController?.abort() - if (isCancelable(promise)) { - promise.cancel() - } } return finalPromise diff --git a/src/core/retryer.ts b/src/core/retryer.ts index d1c6e162d0..4babaf7e6d 100644 --- a/src/core/retryer.ts +++ b/src/core/retryer.ts @@ -144,13 +144,6 @@ export class Retryer { reject(new CancelledError(cancelOptions)) this.abort?.() - - // Cancel transport if supported - if (isCancelable(promiseOrValue)) { - try { - promiseOrValue.cancel() - } catch {} - } } } From 8c78e702a4f2586037bc97a3c851b28e97f86f27 Mon Sep 17 00:00:00 2001 From: Prateek Surana Date: Sat, 20 Nov 2021 15:32:25 +0530 Subject: [PATCH 02/10] =?UTF-8?q?=E2=9C=85=20=20Fix=20query=20client=20tes?= =?UTF-8?q?ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/tests/queryClient.test.tsx | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/core/tests/queryClient.test.tsx b/src/core/tests/queryClient.test.tsx index 16fe2936f7..71b3de40bf 100644 --- a/src/core/tests/queryClient.test.tsx +++ b/src/core/tests/queryClient.test.tsx @@ -927,7 +927,7 @@ describe('queryClient', () => { test('should cancel ongoing fetches if cancelRefetch option is set (default value)', async () => { const key = queryKey() - const cancelFn = jest.fn() + const abortFn = jest.fn() let fetchCount = 0 const observer = new QueryObserver(queryClient, { queryKey: key, @@ -936,25 +936,29 @@ describe('queryClient', () => { }) observer.subscribe() - queryClient.fetchQuery(key, () => { + queryClient.fetchQuery(key, ({ signal }) => { const promise = new Promise(resolve => { fetchCount++ setTimeout(() => resolve(5), 10) + if (signal) { + signal.addEventListener('abort', abortFn) + } }) - // @ts-expect-error - promise.cancel = cancelFn + return promise }) await queryClient.refetchQueries() observer.destroy() - expect(cancelFn).toHaveBeenCalledTimes(1) + if (typeof AbortSignal === 'function') { + expect(abortFn).toHaveBeenCalledTimes(1) + } expect(fetchCount).toBe(2) }) test('should not cancel ongoing fetches if cancelRefetch option is set to false', async () => { const key = queryKey() - const cancelFn = jest.fn() + const abortFn = jest.fn() let fetchCount = 0 const observer = new QueryObserver(queryClient, { queryKey: key, @@ -963,19 +967,23 @@ describe('queryClient', () => { }) observer.subscribe() - queryClient.fetchQuery(key, () => { + queryClient.fetchQuery(key, ({ signal }) => { const promise = new Promise(resolve => { fetchCount++ setTimeout(() => resolve(5), 10) + if (signal) { + signal.addEventListener('abort', abortFn) + } }) - // @ts-expect-error - promise.cancel = cancelFn + return promise }) await queryClient.refetchQueries(undefined, { cancelRefetch: false }) observer.destroy() - expect(cancelFn).toHaveBeenCalledTimes(0) + if (typeof AbortSignal === 'function') { + expect(abortFn).toHaveBeenCalledTimes(0) + } expect(fetchCount).toBe(1) }) }) From 42cc36566349ae4733f3e682009ad7ef6d9f2790 Mon Sep 17 00:00:00 2001 From: Prateek Surana Date: Sat, 20 Nov 2021 16:17:39 +0530 Subject: [PATCH 03/10] =?UTF-8?q?=E2=9C=85=20=20=20Update=20query=20and=20?= =?UTF-8?q?useQuery=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/tests/query.test.tsx | 37 ----------------------------- src/reactjs/tests/useQuery.test.tsx | 9 +++---- 2 files changed, 5 insertions(+), 41 deletions(-) diff --git a/src/core/tests/query.test.tsx b/src/core/tests/query.test.tsx index 5bd06adf2d..ab4c7175a9 100644 --- a/src/core/tests/query.test.tsx +++ b/src/core/tests/query.test.tsx @@ -329,43 +329,6 @@ describe('query', () => { expect(isCancelledError(error)).toBe(true) }) - test('should call cancel() fn if it was provided and not continue when last observer unsubscribed', async () => { - const key = queryKey() - - const cancel = jest.fn() - - queryClient.prefetchQuery(key, async () => { - const promise = new Promise((resolve, reject) => { - sleep(100).then(() => resolve('data')) - cancel.mockImplementation(() => { - reject(new Error('Cancelled')) - }) - }) as any - promise.cancel = cancel - return promise - }) - - await sleep(10) - - // Subscribe and unsubscribe to simulate cancellation because the last observer unsubscribed - const observer = new QueryObserver(queryClient, { - queryKey: key, - enabled: false, - }) - const unsubscribe = observer.subscribe() - unsubscribe() - - await sleep(100) - - const query = queryCache.find(key)! - - expect(cancel).toHaveBeenCalled() - expect(query.state).toMatchObject({ - data: undefined, - status: 'idle', - }) - }) - test('should not continue if explicitly cancelled', async () => { const key = queryKey() diff --git a/src/reactjs/tests/useQuery.test.tsx b/src/reactjs/tests/useQuery.test.tsx index c0a9a77584..403685afc7 100644 --- a/src/reactjs/tests/useQuery.test.tsx +++ b/src/reactjs/tests/useQuery.test.tsx @@ -3952,14 +3952,13 @@ describe('useQuery', () => { const key = queryKey() let cancelFn: jest.Mock = jest.fn() - const queryFn = () => { + const queryFn = ({ signal }: { signal?: AbortSignal }) => { const promise = new Promise((resolve, reject) => { cancelFn = jest.fn(() => reject('Cancelled')) + signal?.addEventListener('abort', cancelFn) sleep(10).then(() => resolve('OK')) }) - ;(promise as any).cancel = cancelFn - return promise } @@ -3981,7 +3980,9 @@ describe('useQuery', () => { await waitFor(() => rendered.getByText('off')) - expect(cancelFn).toHaveBeenCalled() + if (typeof AbortSignal === 'function') { + expect(cancelFn).toHaveBeenCalled() + } }) it('should cancel the query if the signal was consumed and there are no more subscriptions', async () => { From 27d29302e88ab5dcf4c0741d3fe2faa11cbab470 Mon Sep 17 00:00:00 2001 From: Prateek Surana Date: Sat, 20 Nov 2021 21:29:20 +0530 Subject: [PATCH 04/10] =?UTF-8?q?=E2=9C=85=20=20=20Update=20use=20infinite?= =?UTF-8?q?=20query=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/retryer.ts | 7 +++++++ src/reactjs/tests/useInfiniteQuery.test.tsx | 9 +++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/core/retryer.ts b/src/core/retryer.ts index 4babaf7e6d..d1c6e162d0 100644 --- a/src/core/retryer.ts +++ b/src/core/retryer.ts @@ -144,6 +144,13 @@ export class Retryer { reject(new CancelledError(cancelOptions)) this.abort?.() + + // Cancel transport if supported + if (isCancelable(promiseOrValue)) { + try { + promiseOrValue.cancel() + } catch {} + } } } diff --git a/src/reactjs/tests/useInfiniteQuery.test.tsx b/src/reactjs/tests/useInfiniteQuery.test.tsx index 540b44956d..3d4f581396 100644 --- a/src/reactjs/tests/useInfiniteQuery.test.tsx +++ b/src/reactjs/tests/useInfiniteQuery.test.tsx @@ -1782,14 +1782,13 @@ describe('useInfiniteQuery', () => { const key = queryKey() let cancelFn: jest.Mock = jest.fn() - const queryFn = () => { + const queryFn = ({ signal }: { signal?: AbortSignal }) => { const promise = new Promise((resolve, reject) => { cancelFn = jest.fn(() => reject('Cancelled')) + signal?.addEventListener('abort', cancelFn) sleep(10).then(() => resolve('OK')) }) - ;(promise as any).cancel = cancelFn - return promise } @@ -1811,6 +1810,8 @@ describe('useInfiniteQuery', () => { await waitFor(() => rendered.getByText('off')) - expect(cancelFn).toHaveBeenCalled() + if (typeof AbortSignal === 'function') { + expect(cancelFn).toHaveBeenCalled() + } }) }) From 1445e8a8909615220b96632fe58dd38e34f50c6e Mon Sep 17 00:00:00 2001 From: Prateek Surana Date: Sat, 20 Nov 2021 22:33:27 +0530 Subject: [PATCH 05/10] =?UTF-8?q?=F0=9F=93=9D=20=20=20Update=20migartion?= =?UTF-8?q?=20guide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/src/pages/guides/migrating-to-react-query-4.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/src/pages/guides/migrating-to-react-query-4.md b/docs/src/pages/guides/migrating-to-react-query-4.md index 01a7d79c84..29f94490c2 100644 --- a/docs/src/pages/guides/migrating-to-react-query-4.md +++ b/docs/src/pages/guides/migrating-to-react-query-4.md @@ -148,6 +148,10 @@ Since these plugins are no longer experimental, their import paths have also bee + import { createAsyncStoragePersister } from 'react-query/createAsyncStoragePersister' ``` +### The `cancel` method on promises is no longer supported + +The [old `cancel` method](/query-cancellation#old-cancel-function) that allowed you to define a `cancel` function on promises which were then used by the library to support query cancellation has been removed. We recommend you to use the [newer API](/query-cancellation) (introduced with v3.30.0) for query cancellation that uses the [`AbortController` API](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) internally and provides you with an [`AbortSignal` instance](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) for your query function to support query cancellation. + ## New Features 🚀 ### Mutation Cache Garbage Collection From 767780cb18e2d7bb8114648a413a648b18713097 Mon Sep 17 00:00:00 2001 From: Prateek Surana Date: Sat, 20 Nov 2021 22:43:48 +0530 Subject: [PATCH 06/10] :bug: Fix linking in documentation --- docs/src/pages/guides/migrating-to-react-query-4.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/pages/guides/migrating-to-react-query-4.md b/docs/src/pages/guides/migrating-to-react-query-4.md index 29f94490c2..2f7224b092 100644 --- a/docs/src/pages/guides/migrating-to-react-query-4.md +++ b/docs/src/pages/guides/migrating-to-react-query-4.md @@ -150,7 +150,7 @@ Since these plugins are no longer experimental, their import paths have also bee ### The `cancel` method on promises is no longer supported -The [old `cancel` method](/query-cancellation#old-cancel-function) that allowed you to define a `cancel` function on promises which were then used by the library to support query cancellation has been removed. We recommend you to use the [newer API](/query-cancellation) (introduced with v3.30.0) for query cancellation that uses the [`AbortController` API](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) internally and provides you with an [`AbortSignal` instance](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) for your query function to support query cancellation. +The [old `cancel` method](../guides/query-cancellation#old-cancel-function) that allowed you to define a `cancel` function on promises which were then used by the library to support query cancellation has been removed. We recommend you to use the [newer API](../guides/query-cancellation) (introduced with v3.30.0) for query cancellation that uses the [`AbortController` API](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) internally and provides you with an [`AbortSignal` instance](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) for your query function to support query cancellation. ## New Features 🚀 From ca56c55699e79192499d405c842c23723b8638fc Mon Sep 17 00:00:00 2001 From: Prateek Surana Date: Sat, 20 Nov 2021 23:58:25 +0530 Subject: [PATCH 07/10] :pencil: Fix grammatical errors in docs Co-authored-by: Dominik Dorfmeister --- docs/src/pages/guides/migrating-to-react-query-4.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/pages/guides/migrating-to-react-query-4.md b/docs/src/pages/guides/migrating-to-react-query-4.md index 2f7224b092..da8fc13b92 100644 --- a/docs/src/pages/guides/migrating-to-react-query-4.md +++ b/docs/src/pages/guides/migrating-to-react-query-4.md @@ -150,7 +150,7 @@ Since these plugins are no longer experimental, their import paths have also bee ### The `cancel` method on promises is no longer supported -The [old `cancel` method](../guides/query-cancellation#old-cancel-function) that allowed you to define a `cancel` function on promises which were then used by the library to support query cancellation has been removed. We recommend you to use the [newer API](../guides/query-cancellation) (introduced with v3.30.0) for query cancellation that uses the [`AbortController` API](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) internally and provides you with an [`AbortSignal` instance](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) for your query function to support query cancellation. +The [old `cancel` method](../guides/query-cancellation#old-cancel-function) that allowed you to define a `cancel` function on promises, which was then used by the library to support query cancellation, has been removed. We recommend to use the [newer API](../guides/query-cancellation) (introduced with v3.30.0) for query cancellation that uses the [`AbortController` API](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) internally and provides you with an [`AbortSignal` instance](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) for your query function to support query cancellation. ## New Features 🚀 From b56a1187deb60387ebbcdb3197618aea7f7711b0 Mon Sep 17 00:00:00 2001 From: Prateek Surana Date: Sun, 21 Nov 2021 23:09:40 +0530 Subject: [PATCH 08/10] :refactor: Use abortSignal for query cancellation in InfiniteQueryBehavior --- src/core/infiniteQueryBehavior.ts | 9 ++++----- src/core/query.ts | 14 +++++++++++++- src/core/retryer.ts | 20 -------------------- 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/core/infiniteQueryBehavior.ts b/src/core/infiniteQueryBehavior.ts index f588e7a4e1..13848b37ce 100644 --- a/src/core/infiniteQueryBehavior.ts +++ b/src/core/infiniteQueryBehavior.ts @@ -143,12 +143,11 @@ export function infiniteQueryBehavior< pageParams: newPageParams, })) - const finalPromiseAsAny = finalPromise as any - - finalPromiseAsAny.cancel = () => { - cancelled = true + context.signal?.addEventListener('abort', () => { + cancelled = true; abortController?.abort() - } + }) + return finalPromise } diff --git a/src/core/query.ts b/src/core/query.ts index 643825e381..95934f3a48 100644 --- a/src/core/query.ts +++ b/src/core/query.ts @@ -65,6 +65,7 @@ export interface FetchContext< > { fetchFn: () => unknown | Promise fetchOptions?: FetchOptions + signal?: AbortSignal options: QueryOptions queryKey: EnsuredQueryKey state: QueryState @@ -316,7 +317,7 @@ export class Query< // If the transport layer does not support cancellation // we'll let the query continue so the result can be cached if (this.retryer) { - if (this.retryer.isTransportCancelable || this.abortSignalConsumed) { + if (this.abortSignalConsumed) { this.retryer.cancel({ revert: true }) } else { this.retryer.cancelRetry() @@ -412,6 +413,17 @@ export class Query< meta: this.meta, } + Object.defineProperty(context, 'signal', { + enumerable: true, + get: () => { + if (abortController) { + this.abortSignalConsumed = true + return abortController.signal + } + return undefined + }, + }) + if (this.options.behavior?.onFetch) { this.options.behavior?.onFetch(context) } diff --git a/src/core/retryer.ts b/src/core/retryer.ts index d1c6e162d0..81c5408794 100644 --- a/src/core/retryer.ts +++ b/src/core/retryer.ts @@ -34,15 +34,6 @@ type RetryDelayFunction = ( function defaultRetryDelay(failureCount: number) { return Math.min(1000 * 2 ** failureCount, 30000) } - -interface Cancelable { - cancel(): void -} - -export function isCancelable(value: any): value is Cancelable { - return typeof value?.cancel === 'function' -} - export class CancelledError { revert?: boolean silent?: boolean @@ -65,7 +56,6 @@ export class Retryer { failureCount: number isPaused: boolean isResolved: boolean - isTransportCancelable: boolean promise: Promise private abort?: () => void @@ -86,7 +76,6 @@ export class Retryer { this.failureCount = 0 this.isPaused = false this.isResolved = false - this.isTransportCancelable = false this.promise = new Promise((outerResolve, outerReject) => { promiseResolve = outerResolve promiseReject = outerReject @@ -144,18 +133,9 @@ export class Retryer { reject(new CancelledError(cancelOptions)) this.abort?.() - - // Cancel transport if supported - if (isCancelable(promiseOrValue)) { - try { - promiseOrValue.cancel() - } catch {} - } } } - // Check if the transport layer support cancellation - this.isTransportCancelable = isCancelable(promiseOrValue) Promise.resolve(promiseOrValue) .then(resolve) From d202427135ac95004f4800ac07d1b83f16b74d3b Mon Sep 17 00:00:00 2001 From: Prateek Surana Date: Sun, 21 Nov 2021 23:11:25 +0530 Subject: [PATCH 09/10] =?UTF-8?q?=F0=9F=9A=A8=20=20Fix=20lint=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/infiniteQueryBehavior.ts | 3 +-- src/core/retryer.ts | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/core/infiniteQueryBehavior.ts b/src/core/infiniteQueryBehavior.ts index 13848b37ce..3fe93a0377 100644 --- a/src/core/infiniteQueryBehavior.ts +++ b/src/core/infiniteQueryBehavior.ts @@ -144,10 +144,9 @@ export function infiniteQueryBehavior< })) context.signal?.addEventListener('abort', () => { - cancelled = true; + cancelled = true abortController?.abort() }) - return finalPromise } diff --git a/src/core/retryer.ts b/src/core/retryer.ts index 81c5408794..82e4d53792 100644 --- a/src/core/retryer.ts +++ b/src/core/retryer.ts @@ -136,7 +136,6 @@ export class Retryer { } } - Promise.resolve(promiseOrValue) .then(resolve) .catch(error => { From 2fd241ef6f783f9654339a8358cc8fcfd620ac4a Mon Sep 17 00:00:00 2001 From: Prateek Surana Date: Mon, 22 Nov 2021 13:47:20 +0530 Subject: [PATCH 10/10] :recycle: Move define signal property to a separate function --- src/core/query.ts | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/src/core/query.ts b/src/core/query.ts index 95934f3a48..d939387490 100644 --- a/src/core/query.ts +++ b/src/core/query.ts @@ -383,16 +383,23 @@ export class Query< meta: this.meta, } - Object.defineProperty(queryFnContext, 'signal', { - enumerable: true, - get: () => { - if (abortController) { - this.abortSignalConsumed = true - return abortController.signal - } - return undefined - }, - }) + // Adds an enumerable signal property to the object that + // which sets abortSignalConsumed to true when the signal + // is read. + const addSignalProperty = (object: unknown) => { + Object.defineProperty(object, 'signal', { + enumerable: true, + get: () => { + if (abortController) { + this.abortSignalConsumed = true + return abortController.signal + } + return undefined + }, + }) + } + + addSignalProperty(queryFnContext) // Create fetch function const fetchFn = () => { @@ -413,16 +420,7 @@ export class Query< meta: this.meta, } - Object.defineProperty(context, 'signal', { - enumerable: true, - get: () => { - if (abortController) { - this.abortSignalConsumed = true - return abortController.signal - } - return undefined - }, - }) + addSignalProperty(context) if (this.options.behavior?.onFetch) { this.options.behavior?.onFetch(context)