From a6107861a64eaa7abd5eb2abfc87d81e404b1a0f Mon Sep 17 00:00:00 2001 From: Melwyn Date: Fri, 24 Jul 2020 12:30:21 +0530 Subject: [PATCH 1/8] Add support for request cancellation using AbortController --- package-lock.json | 2 +- src/index.js | 4 +++- test/index.test.js | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1cba8c6..8743b3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "redaxios", - "version": "0.2.0", + "version": "0.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/index.js b/src/index.js index 39f523b..f74793b 100644 --- a/src/index.js +++ b/src/index.js @@ -29,6 +29,7 @@ * @property {Array<(body: any, headers: Headers) => any?>} [transformRequest] An array of transformations to apply to the outgoing request * @property {string} [baseURL] a base URL from which to resolve all URLs * @property {typeof window.fetch} [fetch] Custom window.fetch implementation + * @property {AbortSignal} [signal] Signal returned by AbortController * @property {any} [data] */ @@ -195,7 +196,8 @@ export default (function create(/** @type {Options} */ defaults) { method: _method || options.method, body: data, headers: deepMerge(options.headers, customHeaders, true), - credentials: options.withCredentials ? 'include' : undefined + credentials: options.withCredentials ? 'include' : undefined, + signal: options.signal }).then((res) => { for (const i in res) { if (typeof res[i] != 'function') response[i] = res[i]; diff --git a/test/index.test.js b/test/index.test.js index 2682d3d..7bd4651 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -273,4 +273,23 @@ describe('redaxios', () => { expect(fetchMock).toHaveBeenCalledWith('/foo?e=iamthelaw', jasmine.any(Object)); }); }); + + describe('options.signal', () => { + it('should cancel a request when signal is passed', async () => { + const cancelToken = new axios.CancelToken(); + + const axiosGet = axios.get(jsonExample, { + signal: cancelToken.signal + }); + cancelToken.abort(); + + const spy = jasmine.createSpy(); + await axiosGet.catch(spy); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith( + jasmine.objectContaining({ code: 20, message: 'The user aborted a request.', name: 'AbortError' }) + ); + }); + }); }); From 7bb95a6e5b5a32c188f1caa4a162a972472f796e Mon Sep 17 00:00:00 2001 From: Melwyn Date: Sat, 25 Jul 2020 06:28:14 +0530 Subject: [PATCH 2/8] Implement CancelToken similar to axios CancelToken API --- src/index.js | 44 ++++++++++++++++++++++++++++++++++++++++---- test/index.test.js | 36 ++++++++++++++++++++++++++++++++---- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/src/index.js b/src/index.js index f74793b..2c2de4f 100644 --- a/src/index.js +++ b/src/index.js @@ -29,7 +29,7 @@ * @property {Array<(body: any, headers: Headers) => any?>} [transformRequest] An array of transformations to apply to the outgoing request * @property {string} [baseURL] a base URL from which to resolve all URLs * @property {typeof window.fetch} [fetch] Custom window.fetch implementation - * @property {AbortSignal} [signal] Signal returned by AbortController + * @property {AbortSignal} [cancelToken] signal returned by AbortController * @property {any} [data] */ @@ -65,6 +65,11 @@ * @type {(url: string, body?: any, config?: Options) => Promise>} */ +/** + * @typedef CancelTokenSourceMethod + * @type {() => { token: AbortSignal, cancel: () => void }} + */ + /** */ export default (function create(/** @type {Options} */ defaults) { defaults = defaults || {}; @@ -136,6 +141,37 @@ export default (function create(/** @type {Options} */ defaults) { return out; } + /** + * CancelToken + * @private + * @param {Function} executor + * @returns {AbortSignal} + */ + function CancelToken(executor) { + if (typeof executor !== 'function') { + throw new TypeError('executor must be a function.'); + } + + const ac = new AbortController(); + executor(() => ac.abort()); + + return ac.signal; + } + + /** + * @public + * @type {CancelTokenSourceMethod} + * @returns + */ + CancelToken.source = () => { + const ac = new AbortController(); + + return { + token: ac.signal, + cancel: () => ac.abort() + }; + }; + /** * Issues a request. * @public @@ -197,7 +233,7 @@ export default (function create(/** @type {Options} */ defaults) { body: data, headers: deepMerge(options.headers, customHeaders, true), credentials: options.withCredentials ? 'include' : undefined, - signal: options.signal + signal: options.cancelToken }).then((res) => { for (const i in res) { if (typeof res[i] != 'function') response[i] = res[i]; @@ -224,9 +260,9 @@ export default (function create(/** @type {Options} */ defaults) { /** * @public - * @type {AbortController} + * @type {Function} */ - redaxios.CancelToken = /** @type {any} */ (typeof AbortController == 'function' ? AbortController : Object); + redaxios.CancelToken = CancelToken; /** * @public diff --git a/test/index.test.js b/test/index.test.js index 7bd4651..b59f091 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -275,13 +275,14 @@ describe('redaxios', () => { }); describe('options.signal', () => { - it('should cancel a request when signal is passed', async () => { - const cancelToken = new axios.CancelToken(); + it('should cancel a request when cancelToken is passed as source.token', async () => { + const CancelToken = axios.CancelToken; + const source = CancelToken.source(); const axiosGet = axios.get(jsonExample, { - signal: cancelToken.signal + cancelToken: source.token }); - cancelToken.abort(); + source.cancel(); const spy = jasmine.createSpy(); await axiosGet.catch(spy); @@ -291,5 +292,32 @@ describe('redaxios', () => { jasmine.objectContaining({ code: 20, message: 'The user aborted a request.', name: 'AbortError' }) ); }); + + it('should cancel a request when cancelToken is passed as instance CreateToken', async () => { + const CancelToken = axios.CancelToken; + let cancel; + + const axiosGet = axios.get(jsonExample, { + cancelToken: new CancelToken(function executor(c) { + cancel = c; + }) + }); + + cancel(); + + const spy = jasmine.createSpy(); + await axiosGet.catch(spy); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith( + jasmine.objectContaining({ code: 20, message: 'The user aborted a request.', name: 'AbortError' }) + ); + }); + + it('should throw TypeError if no executor function is passed to CancelToken constructor', () => { + const CancelToken = axios.CancelToken; + + expect(() => new CancelToken()).toThrowError('executor must be a function.'); + }); }); }); From 8b7faf00b0996b1f3d07377ff3451d80a087c92b Mon Sep 17 00:00:00 2001 From: Melwyn Date: Sat, 25 Jul 2020 10:51:58 +0530 Subject: [PATCH 3/8] Rename test suit for request cancellation (options.cancelToken) --- test/index.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/index.test.js b/test/index.test.js index b59f091..dca6316 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -274,7 +274,7 @@ describe('redaxios', () => { }); }); - describe('options.signal', () => { + describe('Request cancellation using options.cancelToken', () => { it('should cancel a request when cancelToken is passed as source.token', async () => { const CancelToken = axios.CancelToken; const source = CancelToken.source(); From 5ff06871bf774b39f6676cef8f710651432c90e4 Mon Sep 17 00:00:00 2001 From: Melwyn Date: Sat, 25 Jul 2020 17:00:03 +0530 Subject: [PATCH 4/8] Refactor CancelToken to slightly reduce size --- src/index.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index 2c2de4f..b4639f7 100644 --- a/src/index.js +++ b/src/index.js @@ -153,9 +153,11 @@ export default (function create(/** @type {Options} */ defaults) { } const ac = new AbortController(); - executor(() => ac.abort()); + const { signal, abort } = ac; - return ac.signal; + executor(abort.bind(ac)); + + return signal; } /** @@ -165,10 +167,11 @@ export default (function create(/** @type {Options} */ defaults) { */ CancelToken.source = () => { const ac = new AbortController(); + const { signal: token, abort } = ac; return { - token: ac.signal, - cancel: () => ac.abort() + token, + cancel: abort.bind(ac) }; }; From 1b775a34bf6eb1781958b5479283f67efb8411e0 Mon Sep 17 00:00:00 2001 From: Melwyn Date: Sat, 25 Jul 2020 21:38:36 +0530 Subject: [PATCH 5/8] Refactor by removing destructuring to reduce size --- src/index.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/index.js b/src/index.js index b4639f7..6889358 100644 --- a/src/index.js +++ b/src/index.js @@ -153,11 +153,10 @@ export default (function create(/** @type {Options} */ defaults) { } const ac = new AbortController(); - const { signal, abort } = ac; - executor(abort.bind(ac)); + executor(ac.abort.bind(ac)); - return signal; + return ac.signal; } /** @@ -167,11 +166,10 @@ export default (function create(/** @type {Options} */ defaults) { */ CancelToken.source = () => { const ac = new AbortController(); - const { signal: token, abort } = ac; return { - token, - cancel: abort.bind(ac) + token: ac.signal, + cancel: ac.abort.bind(ac) }; }; From a1f829d4d4142310086a6292af41653c053b10e9 Mon Sep 17 00:00:00 2001 From: Melwyn Date: Sat, 25 Jul 2020 22:24:54 +0530 Subject: [PATCH 6/8] Fix type annotations for CancelToken --- src/index.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 6889358..06efadc 100644 --- a/src/index.js +++ b/src/index.js @@ -65,6 +65,11 @@ * @type {(url: string, body?: any, config?: Options) => Promise>} */ +/** + * @typedef CancelToken + * @type {{ (executor: Function): AbortSignal; source(): { token: AbortSignal; cancel: () => void; }; }} + */ + /** * @typedef CancelTokenSourceMethod * @type {() => { token: AbortSignal, cancel: () => void }} @@ -160,7 +165,7 @@ export default (function create(/** @type {Options} */ defaults) { } /** - * @public + * @private * @type {CancelTokenSourceMethod} * @returns */ @@ -261,7 +266,7 @@ export default (function create(/** @type {Options} */ defaults) { /** * @public - * @type {Function} + * @type {CancelToken} */ redaxios.CancelToken = CancelToken; From 817e89ee595ac0dd753f8ce8d8019ff7e51de9fe Mon Sep 17 00:00:00 2001 From: Melwyn Date: Sat, 25 Jul 2020 23:39:31 +0530 Subject: [PATCH 7/8] Implment isCancel function to check if error is due to request cancellation --- src/index.js | 7 +++++++ test/index.test.js | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 06efadc..2168a0e 100644 --- a/src/index.js +++ b/src/index.js @@ -270,6 +270,13 @@ export default (function create(/** @type {Options} */ defaults) { */ redaxios.CancelToken = CancelToken; + /** + * @public + * @param {DOMError} e + * @returns {boolean} + */ + redaxios.isCancel = (e) => e.name === 'AbortError'; + /** * @public * @type {Options} diff --git a/test/index.test.js b/test/index.test.js index dca6316..59048aa 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -306,8 +306,10 @@ describe('redaxios', () => { cancel(); const spy = jasmine.createSpy(); - await axiosGet.catch(spy); + let error; + await axiosGet.catch((e) => ((error = e), spy(e))); + expect(axios.isCancel(error)).toBeTruthy(true); expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledWith( jasmine.objectContaining({ code: 20, message: 'The user aborted a request.', name: 'AbortError' }) From 97b1d8688c393c61294c43aad0a55a745f59c4ec Mon Sep 17 00:00:00 2001 From: Melwyn Saldanha Date: Sat, 10 Oct 2020 05:31:27 +0530 Subject: [PATCH 8/8] fix: order of tests --- test/index.test.js | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/test/index.test.js b/test/index.test.js index a720bc2..2803fc3 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -301,6 +301,24 @@ describe('redaxios', () => { }); }); + describe('static helpers', () => { + it(`#all should work`, async () => { + const result = await axios.all([Promise.resolve('hello'), Promise.resolve('world')]); + + expect(result).toEqual(['hello', 'world']); + }); + + it(`#spread should work`, async () => { + const result = await axios.all([Promise.resolve('hello'), Promise.resolve('world')]).then( + axios.spread((item1, item2) => { + return `${item1} ${item2}`; + }) + ); + + expect(result).toEqual('hello world'); + }); + }); + describe('Request cancellation using options.cancelToken', () => { it('should cancel a request when cancelToken is passed as source.token', async () => { const CancelToken = axios.CancelToken; @@ -348,22 +366,5 @@ describe('redaxios', () => { expect(() => new CancelToken()).toThrowError('executor must be a function.'); }); - describe('static helpers', () => { - it(`#all should work`, async () => { - const result = await axios.all([Promise.resolve('hello'), Promise.resolve('world')]); - - expect(result).toEqual(['hello', 'world']); - }); - - it(`#spread should work`, async () => { - const result = await axios.all([Promise.resolve('hello'), Promise.resolve('world')]).then( - axios.spread((item1, item2) => { - return `${item1} ${item2}`; - }) - ); - - expect(result).toEqual('hello world'); - }); - }); }); });