Skip to content

Commit e3c76fc

Browse files
fix(adapter): fix progress event emitting; (#6518)
1 parent 85d4d0e commit e3c76fc

File tree

10 files changed

+204
-151
lines changed

10 files changed

+204
-151
lines changed

lib/adapters/fetch.js

+29-27
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,10 @@ import AxiosError from "../core/AxiosError.js";
44
import composeSignals from "../helpers/composeSignals.js";
55
import {trackStream} from "../helpers/trackStream.js";
66
import AxiosHeaders from "../core/AxiosHeaders.js";
7-
import progressEventReducer from "../helpers/progressEventReducer.js";
7+
import {progressEventReducer, progressEventDecorator, asyncDecorator} from "../helpers/progressEventReducer.js";
88
import resolveConfig from "../helpers/resolveConfig.js";
99
import settle from "../core/settle.js";
1010

11-
const fetchProgressDecorator = (total, fn) => {
12-
const lengthComputable = total != null;
13-
return (loaded) => setTimeout(() => fn({
14-
lengthComputable,
15-
total,
16-
loaded
17-
}));
18-
}
19-
2011
const isFetchSupported = typeof fetch === 'function' && typeof Request === 'function' && typeof Response === 'function';
2112
const isReadableStreamSupported = isFetchSupported && typeof ReadableStream === 'function';
2213

@@ -26,7 +17,15 @@ const encodeText = isFetchSupported && (typeof TextEncoder === 'function' ?
2617
async (str) => new Uint8Array(await new Response(str).arrayBuffer())
2718
);
2819

29-
const supportsRequestStream = isReadableStreamSupported && (() => {
20+
const test = (fn, ...args) => {
21+
try {
22+
return !!fn(...args);
23+
} catch (e) {
24+
return false
25+
}
26+
}
27+
28+
const supportsRequestStream = isReadableStreamSupported && test(() => {
3029
let duplexAccessed = false;
3130

3231
const hasContentType = new Request(platform.origin, {
@@ -39,17 +38,13 @@ const supportsRequestStream = isReadableStreamSupported && (() => {
3938
}).headers.has('Content-Type');
4039

4140
return duplexAccessed && !hasContentType;
42-
})();
41+
});
4342

4443
const DEFAULT_CHUNK_SIZE = 64 * 1024;
4544

46-
const supportsResponseStream = isReadableStreamSupported && !!(()=> {
47-
try {
48-
return utils.isReadableStream(new Response('').body);
49-
} catch(err) {
50-
// return undefined
51-
}
52-
})();
45+
const supportsResponseStream = isReadableStreamSupported &&
46+
test(() => utils.isReadableStream(new Response('').body));
47+
5348

5449
const resolvers = {
5550
stream: supportsResponseStream && ((res) => res.body)
@@ -77,7 +72,7 @@ const getBodyLength = async (body) => {
7772
return (await new Request(body).arrayBuffer()).byteLength;
7873
}
7974

80-
if(utils.isArrayBufferView(body)) {
75+
if(utils.isArrayBufferView(body) || utils.isArrayBuffer(body)) {
8176
return body.byteLength;
8277
}
8378

@@ -147,10 +142,12 @@ export default isFetchSupported && (async (config) => {
147142
}
148143

149144
if (_request.body) {
150-
data = trackStream(_request.body, DEFAULT_CHUNK_SIZE, fetchProgressDecorator(
145+
const [onProgress, flush] = progressEventDecorator(
151146
requestContentLength,
152-
progressEventReducer(onUploadProgress)
153-
), null, encodeText);
147+
progressEventReducer(asyncDecorator(onUploadProgress))
148+
);
149+
150+
data = trackStream(_request.body, DEFAULT_CHUNK_SIZE, onProgress, flush, encodeText);
154151
}
155152
}
156153

@@ -181,11 +178,16 @@ export default isFetchSupported && (async (config) => {
181178

182179
const responseContentLength = utils.toFiniteNumber(response.headers.get('content-length'));
183180

181+
const [onProgress, flush] = onDownloadProgress && progressEventDecorator(
182+
responseContentLength,
183+
progressEventReducer(asyncDecorator(onDownloadProgress), true)
184+
) || [];
185+
184186
response = new Response(
185-
trackStream(response.body, DEFAULT_CHUNK_SIZE, onDownloadProgress && fetchProgressDecorator(
186-
responseContentLength,
187-
progressEventReducer(onDownloadProgress, true)
188-
), isStreamResponse && onFinish, encodeText),
187+
trackStream(response.body, DEFAULT_CHUNK_SIZE, onProgress, () => {
188+
flush && flush();
189+
isStreamResponse && onFinish();
190+
}, encodeText),
189191
options
190192
);
191193
}

lib/adapters/http.js

+25-15
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import formDataToStream from "../helpers/formDataToStream.js";
2424
import readBlob from "../helpers/readBlob.js";
2525
import ZlibHeaderTransformStream from '../helpers/ZlibHeaderTransformStream.js';
2626
import callbackify from "../helpers/callbackify.js";
27+
import {progressEventReducer, progressEventDecorator, asyncDecorator} from "../helpers/progressEventReducer.js";
2728

2829
const zlibOptions = {
2930
flush: zlib.constants.Z_SYNC_FLUSH,
@@ -45,6 +46,14 @@ const supportedProtocols = platform.protocols.map(protocol => {
4546
return protocol + ':';
4647
});
4748

49+
const flushOnFinish = (stream, [throttled, flush]) => {
50+
stream
51+
.on('end', flush)
52+
.on('error', flush);
53+
54+
return throttled;
55+
}
56+
4857
/**
4958
* If the proxy or config beforeRedirects functions are defined, call them with the options
5059
* object.
@@ -278,8 +287,7 @@ export default isHttpAdapterSupported && function httpAdapter(config) {
278287
// Only set header if it hasn't been set in config
279288
headers.set('User-Agent', 'axios/' + VERSION, false);
280289

281-
const onDownloadProgress = config.onDownloadProgress;
282-
const onUploadProgress = config.onUploadProgress;
290+
const {onUploadProgress, onDownloadProgress} = config;
283291
const maxRate = config.maxRate;
284292
let maxUploadRate = undefined;
285293
let maxDownloadRate = undefined;
@@ -352,15 +360,16 @@ export default isHttpAdapterSupported && function httpAdapter(config) {
352360
}
353361

354362
data = stream.pipeline([data, new AxiosTransformStream({
355-
length: contentLength,
356363
maxRate: utils.toFiniteNumber(maxUploadRate)
357364
})], utils.noop);
358365

359-
onUploadProgress && data.on('progress', progress => {
360-
onUploadProgress(Object.assign(progress, {
361-
upload: true
362-
}));
363-
});
366+
onUploadProgress && data.on('progress', flushOnFinish(
367+
data,
368+
progressEventDecorator(
369+
contentLength,
370+
progressEventReducer(asyncDecorator(onUploadProgress), false, 3)
371+
)
372+
));
364373
}
365374

366375
// HTTP basic authentication
@@ -459,17 +468,18 @@ export default isHttpAdapterSupported && function httpAdapter(config) {
459468

460469
const responseLength = +res.headers['content-length'];
461470

462-
if (onDownloadProgress) {
471+
if (onDownloadProgress || maxDownloadRate) {
463472
const transformStream = new AxiosTransformStream({
464-
length: utils.toFiniteNumber(responseLength),
465473
maxRate: utils.toFiniteNumber(maxDownloadRate)
466474
});
467475

468-
onDownloadProgress && transformStream.on('progress', progress => {
469-
onDownloadProgress(Object.assign(progress, {
470-
download: true
471-
}));
472-
});
476+
onDownloadProgress && transformStream.on('progress', flushOnFinish(
477+
transformStream,
478+
progressEventDecorator(
479+
responseLength,
480+
progressEventReducer(asyncDecorator(onDownloadProgress), true, 3)
481+
)
482+
));
473483

474484
streams.push(transformStream);
475485
}

lib/adapters/xhr.js

+19-12
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import CanceledError from '../cancel/CanceledError.js';
66
import parseProtocol from '../helpers/parseProtocol.js';
77
import platform from '../platform/index.js';
88
import AxiosHeaders from '../core/AxiosHeaders.js';
9-
import progressEventReducer from '../helpers/progressEventReducer.js';
9+
import {progressEventReducer} from '../helpers/progressEventReducer.js';
1010
import resolveConfig from "../helpers/resolveConfig.js";
1111

1212
const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';
@@ -16,16 +16,18 @@ export default isXHRAdapterSupported && function (config) {
1616
const _config = resolveConfig(config);
1717
let requestData = _config.data;
1818
const requestHeaders = AxiosHeaders.from(_config.headers).normalize();
19-
let {responseType} = _config;
19+
let {responseType, onUploadProgress, onDownloadProgress} = _config;
2020
let onCanceled;
21+
let uploadThrottled, downloadThrottled;
22+
let flushUpload, flushDownload;
23+
2124
function done() {
22-
if (_config.cancelToken) {
23-
_config.cancelToken.unsubscribe(onCanceled);
24-
}
25+
flushUpload && flushUpload(); // flush events
26+
flushDownload && flushDownload(); // flush events
2527

26-
if (_config.signal) {
27-
_config.signal.removeEventListener('abort', onCanceled);
28-
}
28+
_config.cancelToken && _config.cancelToken.unsubscribe(onCanceled);
29+
30+
_config.signal && _config.signal.removeEventListener('abort', onCanceled);
2931
}
3032

3133
let request = new XMLHttpRequest();
@@ -149,13 +151,18 @@ export default isXHRAdapterSupported && function (config) {
149151
}
150152

151153
// Handle progress if needed
152-
if (typeof _config.onDownloadProgress === 'function') {
153-
request.addEventListener('progress', progressEventReducer(_config.onDownloadProgress, true));
154+
if (onDownloadProgress) {
155+
([downloadThrottled, flushDownload] = progressEventReducer(onDownloadProgress, true));
156+
request.addEventListener('progress', downloadThrottled);
154157
}
155158

156159
// Not all browsers support upload events
157-
if (typeof _config.onUploadProgress === 'function' && request.upload) {
158-
request.upload.addEventListener('progress', progressEventReducer(_config.onUploadProgress));
160+
if (onUploadProgress && request.upload) {
161+
([uploadThrottled, flushUpload] = progressEventReducer(onUploadProgress));
162+
163+
request.upload.addEventListener('progress', uploadThrottled);
164+
165+
request.upload.addEventListener('loadend', flushUpload);
159166
}
160167

161168
if (_config.cancelToken || _config.signal) {

lib/helpers/AxiosTransformStream.js

+3-52
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import stream from 'stream';
44
import utils from '../utils.js';
5-
import throttle from './throttle.js';
6-
import speedometer from './speedometer.js';
75

86
const kInternals = Symbol('internals');
97

@@ -24,12 +22,8 @@ class AxiosTransformStream extends stream.Transform{
2422
readableHighWaterMark: options.chunkSize
2523
});
2624

27-
const self = this;
28-
2925
const internals = this[kInternals] = {
30-
length: options.length,
3126
timeWindow: options.timeWindow,
32-
ticksRate: options.ticksRate,
3327
chunkSize: options.chunkSize,
3428
maxRate: options.maxRate,
3529
minChunkSize: options.minChunkSize,
@@ -41,48 +35,13 @@ class AxiosTransformStream extends stream.Transform{
4135
onReadCallback: null
4236
};
4337

44-
const _speedometer = speedometer(internals.ticksRate * options.samplesCount, internals.timeWindow);
45-
4638
this.on('newListener', event => {
4739
if (event === 'progress') {
4840
if (!internals.isCaptured) {
4941
internals.isCaptured = true;
5042
}
5143
}
5244
});
53-
54-
let bytesNotified = 0;
55-
56-
internals.updateProgress = throttle(function throttledHandler() {
57-
const totalBytes = internals.length;
58-
const bytesTransferred = internals.bytesSeen;
59-
const progressBytes = bytesTransferred - bytesNotified;
60-
if (!progressBytes || self.destroyed) return;
61-
62-
const rate = _speedometer(progressBytes);
63-
64-
bytesNotified = bytesTransferred;
65-
66-
process.nextTick(() => {
67-
self.emit('progress', {
68-
loaded: bytesTransferred,
69-
total: totalBytes,
70-
progress: totalBytes ? (bytesTransferred / totalBytes) : undefined,
71-
bytes: progressBytes,
72-
rate: rate ? rate : undefined,
73-
estimated: rate && totalBytes && bytesTransferred <= totalBytes ?
74-
(totalBytes - bytesTransferred) / rate : undefined,
75-
lengthComputable: totalBytes != null
76-
});
77-
});
78-
}, internals.ticksRate);
79-
80-
const onFinish = () => {
81-
internals.updateProgress.call(true);
82-
};
83-
84-
this.once('end', onFinish);
85-
this.once('error', onFinish);
8645
}
8746

8847
_read(size) {
@@ -96,7 +55,6 @@ class AxiosTransformStream extends stream.Transform{
9655
}
9756

9857
_transform(chunk, encoding, callback) {
99-
const self = this;
10058
const internals = this[kInternals];
10159
const maxRate = internals.maxRate;
10260

@@ -108,16 +66,14 @@ class AxiosTransformStream extends stream.Transform{
10866
const bytesThreshold = (maxRate / divider);
10967
const minChunkSize = internals.minChunkSize !== false ? Math.max(internals.minChunkSize, bytesThreshold * 0.01) : 0;
11068

111-
function pushChunk(_chunk, _callback) {
69+
const pushChunk = (_chunk, _callback) => {
11270
const bytes = Buffer.byteLength(_chunk);
11371
internals.bytesSeen += bytes;
11472
internals.bytes += bytes;
11573

116-
if (internals.isCaptured) {
117-
internals.updateProgress();
118-
}
74+
internals.isCaptured && this.emit('progress', internals.bytesSeen);
11975

120-
if (self.push(_chunk)) {
76+
if (this.push(_chunk)) {
12177
process.nextTick(_callback);
12278
} else {
12379
internals.onReadCallback = () => {
@@ -182,11 +138,6 @@ class AxiosTransformStream extends stream.Transform{
182138
}
183139
});
184140
}
185-
186-
setLength(length) {
187-
this[kInternals].length = +length;
188-
return this;
189-
}
190141
}
191142

192143
export default AxiosTransformStream;

lib/helpers/progressEventReducer.js

+16-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import speedometer from "./speedometer.js";
22
import throttle from "./throttle.js";
3+
import utils from "../utils.js";
34

4-
export default (listener, isDownloadStream, freq = 3) => {
5+
export const progressEventReducer = (listener, isDownloadStream, freq = 3) => {
56
let bytesNotified = 0;
67
const _speedometer = speedometer(50, 250);
78

@@ -22,11 +23,22 @@ export default (listener, isDownloadStream, freq = 3) => {
2223
rate: rate ? rate : undefined,
2324
estimated: rate && total && inRange ? (total - loaded) / rate : undefined,
2425
event: e,
25-
lengthComputable: total != null
26+
lengthComputable: total != null,
27+
[isDownloadStream ? 'download' : 'upload']: true
2628
};
2729

28-
data[isDownloadStream ? 'download' : 'upload'] = true;
29-
3030
listener(data);
3131
}, freq);
3232
}
33+
34+
export const progressEventDecorator = (total, throttled) => {
35+
const lengthComputable = total != null;
36+
37+
return [(loaded) => throttled[0]({
38+
lengthComputable,
39+
total,
40+
loaded
41+
}), throttled[1]];
42+
}
43+
44+
export const asyncDecorator = (fn) => (...args) => utils.asap(() => fn(...args));

0 commit comments

Comments
 (0)