diff --git a/src/index.js b/src/index.js index c266c73..2f11913 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} [cancelToken] signal returned by AbortController * @property {any} [data] */ @@ -64,6 +65,16 @@ * @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 }} + */ + /** */ export default (function create(/** @type {Options} */ defaults) { defaults = defaults || {}; @@ -138,6 +149,38 @@ 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.bind(ac)); + + return ac.signal; + } + + /** + * @private + * @type {CancelTokenSourceMethod} + * @returns + */ + CancelToken.source = () => { + const ac = new AbortController(); + + return { + token: ac.signal, + cancel: ac.abort.bind(ac) + }; + }; + /** * Issues a request. * @public @@ -199,7 +242,8 @@ export default (function create(/** @type {Options} */ defaults) { method: _method || options.method, body: data, headers: deepMerge(options.headers, customHeaders, true), - credentials: options.withCredentials ? 'include' : 'same-origin' + credentials: options.withCredentials ? 'include' : 'same-origin', + signal: options.cancelToken }).then((res) => { for (const i in res) { if (typeof res[i] != 'function') response[i] = res[i]; @@ -225,9 +269,16 @@ export default (function create(/** @type {Options} */ defaults) { /** * @public - * @type {AbortController} + * @type {CancelToken} + */ + redaxios.CancelToken = CancelToken; + + /** + * @public + * @param {DOMError} e + * @returns {boolean} */ - redaxios.CancelToken = /** @type {any} */ (typeof AbortController == 'function' ? AbortController : Object); + redaxios.isCancel = (e) => e.name === 'AbortError'; /** * @public diff --git a/test/index.test.js b/test/index.test.js index cab5374..2803fc3 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -318,4 +318,53 @@ describe('redaxios', () => { 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; + const source = CancelToken.source(); + + const axiosGet = axios.get(jsonExample, { + cancelToken: source.token + }); + source.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 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(); + 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' }) + ); + }); + + 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.'); + }); + }); });