Skip to content

Commit 100863f

Browse files
melwyn95osbre
authored andcommitted
Add request cancellation support
developit#47
1 parent d008a9d commit 100863f

2 files changed

Lines changed: 102 additions & 3 deletions

File tree

src/index.js

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
* @property {Array<(body: any, headers?: RequestHeaders) => any?>} [transformRequest] An array of transformations to apply to the outgoing request
3030
* @property {string} [baseURL] a base URL from which to resolve all URLs
3131
* @property {typeof window.fetch} [fetch] Custom window.fetch implementation
32+
* @property {AbortSignal} [cancelToken] signal returned by AbortController
3233
* @property {any} [data]
3334
*/
3435

@@ -64,6 +65,16 @@
6465
* @type {<T=any>(url: string, body?: any, config?: Options) => Promise<Response<T>>}
6566
*/
6667

68+
/**
69+
* @typedef CancelToken
70+
* @type {{ (executor: Function): AbortSignal; source(): { token: AbortSignal; cancel: () => void; }; }}
71+
*/
72+
73+
/**
74+
* @typedef CancelTokenSourceMethod
75+
* @type {() => { token: AbortSignal, cancel: () => void }}
76+
*/
77+
6778
/**
6879
* @public
6980
* @param {Options} [defaults = {}]
@@ -138,6 +149,37 @@ function create(defaults) {
138149
return out;
139150
}
140151

152+
/**
153+
* CancelToken
154+
* @private
155+
* @param {Function} executor
156+
* @returns {AbortSignal}
157+
*/
158+
function CancelToken(executor) {
159+
if (typeof executor !== 'function') {
160+
throw new TypeError('executor must be a function.');
161+
}
162+
163+
const ac = new AbortController();
164+
executor(ac.abort.bind(ac));
165+
166+
return ac.signal;
167+
}
168+
169+
/**
170+
* @private
171+
* @type {CancelTokenSourceMethod}
172+
* @returns
173+
*/
174+
CancelToken.source = () => {
175+
const ac = new AbortController();
176+
177+
return {
178+
token: ac.signal,
179+
cancel: ac.abort.bind(ac)
180+
};
181+
};
182+
141183
/**
142184
* Issues a request.
143185
* @public
@@ -199,7 +241,8 @@ function create(defaults) {
199241
method: (_method || options.method || 'get').toUpperCase(),
200242
body: data,
201243
headers: deepMerge(options.headers, customHeaders, true),
202-
credentials: options.withCredentials ? 'include' : _undefined
244+
credentials: options.withCredentials ? 'include' : _undefined,
245+
signal: options.cancelToken
203246
}).then((res) => {
204247
for (const i in res) {
205248
if (typeof res[i] != 'function') response[i] = res[i];
@@ -226,9 +269,16 @@ function create(defaults) {
226269

227270
/**
228271
* @public
229-
* @type {AbortController}
272+
* @type {CancelToken}
273+
*/
274+
redaxios.CancelToken = CancelToken;
275+
276+
/**
277+
* @public
278+
* @param {DOMError} e
279+
* @returns {boolean}
230280
*/
231-
redaxios.CancelToken = /** @type {any} */ (typeof AbortController == 'function' ? AbortController : Object);
281+
redaxios.isCancel = (e) => e.name === 'AbortError';
232282

233283
/**
234284
* @public

test/index.test.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,4 +336,53 @@ describe('redaxios', () => {
336336
expect(result).toEqual('hello world');
337337
});
338338
});
339+
340+
describe('Request cancellation using options.cancelToken', () => {
341+
it('should cancel a request when cancelToken is passed as source.token', async () => {
342+
const CancelToken = axios.CancelToken;
343+
const source = CancelToken.source();
344+
345+
const axiosGet = axios.get(jsonExample, {
346+
cancelToken: source.token
347+
});
348+
source.cancel();
349+
350+
const spy = jasmine.createSpy();
351+
await axiosGet.catch(spy);
352+
353+
expect(spy).toHaveBeenCalledTimes(1);
354+
expect(spy).toHaveBeenCalledWith(
355+
jasmine.objectContaining({ code: 20, message: 'The user aborted a request.', name: 'AbortError' })
356+
);
357+
});
358+
359+
it('should cancel a request when cancelToken is passed as instance CreateToken', async () => {
360+
const CancelToken = axios.CancelToken;
361+
let cancel;
362+
363+
const axiosGet = axios.get(jsonExample, {
364+
cancelToken: new CancelToken(function executor(c) {
365+
cancel = c;
366+
})
367+
});
368+
369+
cancel();
370+
371+
const spy = jasmine.createSpy();
372+
let error;
373+
await axiosGet.catch((e) => ((error = e), spy(e)));
374+
375+
expect(axios.isCancel(error)).toBeTruthy(true);
376+
expect(spy).toHaveBeenCalledTimes(1);
377+
expect(spy).toHaveBeenCalledWith(
378+
jasmine.objectContaining({ code: 20, message: 'The user aborted a request.', name: 'AbortError' })
379+
);
380+
});
381+
382+
it('should throw TypeError if no executor function is passed to CancelToken constructor', () => {
383+
const CancelToken = axios.CancelToken;
384+
385+
expect(() => new CancelToken()).toThrowError('executor must be a function.');
386+
});
387+
});
339388
});

0 commit comments

Comments
 (0)