2
0
mirror of https://github.com/tenrok/axios.git synced 2026-06-20 20:00:40 +03:00

fix(fetch): optimize signals composing logic; (#6582)

This commit is contained in:
Dmitriy Mozgovoy
2024-08-30 21:26:12 +03:00
committed by GitHub
parent ee208cfcae
commit df9889b83c
4 changed files with 101 additions and 49 deletions
+9 -16
View File
@@ -113,18 +113,13 @@ export default isFetchSupported && (async (config) => {
responseType = responseType ? (responseType + '').toLowerCase() : 'text'; responseType = responseType ? (responseType + '').toLowerCase() : 'text';
let [composedSignal, stopTimeout] = (signal || cancelToken || timeout) ? let composedSignal = composeSignals([signal, cancelToken && cancelToken.toAbortSignal()], timeout);
composeSignals([signal, cancelToken], timeout) : [];
let finished, request; let request;
const onFinish = () => { const unsubscribe = composedSignal && composedSignal.unsubscribe && (() => {
!finished && setTimeout(() => { composedSignal.unsubscribe();
composedSignal && composedSignal.unsubscribe(); });
});
finished = true;
}
let requestContentLength; let requestContentLength;
@@ -176,7 +171,7 @@ export default isFetchSupported && (async (config) => {
const isStreamResponse = supportsResponseStream && (responseType === 'stream' || responseType === 'response'); const isStreamResponse = supportsResponseStream && (responseType === 'stream' || responseType === 'response');
if (supportsResponseStream && (onDownloadProgress || isStreamResponse)) { if (supportsResponseStream && (onDownloadProgress || (isStreamResponse && unsubscribe))) {
const options = {}; const options = {};
['status', 'statusText', 'headers'].forEach(prop => { ['status', 'statusText', 'headers'].forEach(prop => {
@@ -193,7 +188,7 @@ export default isFetchSupported && (async (config) => {
response = new Response( response = new Response(
trackStream(response.body, DEFAULT_CHUNK_SIZE, onProgress, () => { trackStream(response.body, DEFAULT_CHUNK_SIZE, onProgress, () => {
flush && flush(); flush && flush();
isStreamResponse && onFinish(); unsubscribe && unsubscribe();
}, encodeText), }, encodeText),
options options
); );
@@ -203,9 +198,7 @@ export default isFetchSupported && (async (config) => {
let responseData = await resolvers[utils.findKey(resolvers, responseType) || 'text'](response, config); let responseData = await resolvers[utils.findKey(resolvers, responseType) || 'text'](response, config);
!isStreamResponse && onFinish(); !isStreamResponse && unsubscribe && unsubscribe();
stopTimeout && stopTimeout();
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
settle(resolve, reject, { settle(resolve, reject, {
@@ -218,7 +211,7 @@ export default isFetchSupported && (async (config) => {
}) })
}) })
} catch (err) { } catch (err) {
onFinish(); unsubscribe && unsubscribe();
if (err && err.name === 'TypeError' && /fetch/i.test(err.message)) { if (err && err.name === 'TypeError' && /fetch/i.test(err.message)) {
throw Object.assign( throw Object.assign(
+14
View File
@@ -102,6 +102,20 @@ class CancelToken {
} }
} }
toAbortSignal() {
const controller = new AbortController();
const abort = (err) => {
controller.abort(err);
};
this.subscribe(abort);
controller.signal.unsubscribe = () => this.unsubscribe(abort);
return controller.signal;
}
/** /**
* Returns an object that contains a new `CancelToken` and a function that, when called, * Returns an object that contains a new `CancelToken` and a function that, when called,
* cancels the `CancelToken`. * cancels the `CancelToken`.
+34 -32
View File
@@ -1,46 +1,48 @@
import CanceledError from "../cancel/CanceledError.js"; import CanceledError from "../cancel/CanceledError.js";
import AxiosError from "../core/AxiosError.js"; import AxiosError from "../core/AxiosError.js";
import utils from '../utils.js';
const composeSignals = (signals, timeout) => { const composeSignals = (signals, timeout) => {
let controller = new AbortController(); const {length} = (signals = signals ? signals.filter(Boolean) : []);
let aborted; if (timeout || length) {
let controller = new AbortController();
const onabort = function (cancel) { let aborted;
if (!aborted) {
aborted = true; const onabort = function (reason) {
unsubscribe(); if (!aborted) {
const err = cancel instanceof Error ? cancel : this.reason; aborted = true;
controller.abort(err instanceof AxiosError ? err : new CanceledError(err instanceof Error ? err.message : err)); unsubscribe();
const err = reason instanceof Error ? reason : this.reason;
controller.abort(err instanceof AxiosError ? err : new CanceledError(err instanceof Error ? err.message : err));
}
} }
}
let timer = timeout && setTimeout(() => { let timer = timeout && setTimeout(() => {
onabort(new AxiosError(`timeout ${timeout} of ms exceeded`, AxiosError.ETIMEDOUT))
}, timeout)
const unsubscribe = () => {
if (signals) {
timer && clearTimeout(timer);
timer = null; timer = null;
signals.forEach(signal => { onabort(new AxiosError(`timeout ${timeout} of ms exceeded`, AxiosError.ETIMEDOUT))
signal && }, timeout)
(signal.removeEventListener ? signal.removeEventListener('abort', onabort) : signal.unsubscribe(onabort));
}); const unsubscribe = () => {
signals = null; if (signals) {
timer && clearTimeout(timer);
timer = null;
signals.forEach(signal => {
signal.unsubscribe ? signal.unsubscribe(onabort) : signal.removeEventListener('abort', onabort);
});
signals = null;
}
} }
signals.forEach((signal) => signal.addEventListener('abort', onabort));
const {signal} = controller;
signal.unsubscribe = () => utils.asap(unsubscribe);
return signal;
} }
signals.forEach((signal) => signal && signal.addEventListener && signal.addEventListener('abort', onabort));
const {signal} = controller;
signal.unsubscribe = unsubscribe;
return [signal, () => {
timer && clearTimeout(timer);
timer = null;
}];
} }
export default composeSignals; export default composeSignals;
+43
View File
@@ -0,0 +1,43 @@
import assert from 'assert';
import composeSignals from '../../../lib/helpers/composeSignals.js';
describe('helpers::composeSignals', () => {
before(function () {
if (typeof AbortController !== 'function') {
this.skip();
}
});
it('should abort when any of the signals abort', () => {
let called;
const controllerA = new AbortController();
const controllerB = new AbortController();
const signal = composeSignals([controllerA.signal, controllerB.signal]);
signal.addEventListener('abort', () => {
called = true;
});
controllerA.abort(new Error('test'));
assert.ok(called);
});
it('should abort on timeout', async () => {
const signal = composeSignals([], 100);
await new Promise(resolve => {
signal.addEventListener('abort', resolve);
});
assert.match(String(signal.reason), /timeout 100 of ms exceeded/);
});
it('should return undefined if signals and timeout are not provided', async () => {
const signal = composeSignals([]);
assert.strictEqual(signal, undefined);
});
});