@onekeyfe/hd-core 1.1.32 → 1.1.34-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +394 -0
- package/__tests__/kaspaSignTransaction.test.ts +60 -0
- package/__tests__/networkUtils.test.ts +386 -0
- package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
- package/dist/api/firmware/getBinary.d.ts +7 -3
- package/dist/api/firmware/getBinary.d.ts.map +1 -1
- package/dist/api/kaspa/KaspaSignTransaction.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts +4 -4
- package/dist/index.d.ts +12 -3
- package/dist/index.js +323 -45
- package/dist/utils/assets.d.ts +2 -1
- package/dist/utils/assets.d.ts.map +1 -1
- package/dist/utils/networkUtils.d.ts +9 -1
- package/dist/utils/networkUtils.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV2.ts +122 -27
- package/src/api/firmware/getBinary.ts +8 -3
- package/src/api/kaspa/KaspaSignTransaction.ts +12 -1
- package/src/utils/assets.ts +4 -1
- package/src/utils/networkUtils.ts +270 -14
|
@@ -31,6 +31,7 @@ import { DEVICE } from '../events';
|
|
|
31
31
|
|
|
32
32
|
import type { Features, KnownDevice } from '../types';
|
|
33
33
|
import type { Device } from '../device/Device';
|
|
34
|
+
import type { FirmwareBinary } from './firmware/getBinary';
|
|
34
35
|
|
|
35
36
|
type Params = {
|
|
36
37
|
binary?: ArrayBuffer;
|
|
@@ -43,6 +44,77 @@ type Params = {
|
|
|
43
44
|
|
|
44
45
|
const Log = getLogger(LoggerNames.Method);
|
|
45
46
|
|
|
47
|
+
const FIRMWARE_DOWNLOAD_REQUEST_OPTIONS = {
|
|
48
|
+
connectTimeoutMs: 60_000,
|
|
49
|
+
readTimeoutMs: 60_000,
|
|
50
|
+
overallTimeoutMs: 180_000,
|
|
51
|
+
maxRetries: 2,
|
|
52
|
+
retryDelayMs: 500,
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
const normalizeFirmwareBinary = (binary: unknown): FirmwareBinary | undefined => {
|
|
56
|
+
if (typeof binary !== 'object' || binary === null) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isNodeBuffer =
|
|
61
|
+
typeof Buffer !== 'undefined' &&
|
|
62
|
+
typeof Buffer.isBuffer === 'function' &&
|
|
63
|
+
Buffer.isBuffer(binary);
|
|
64
|
+
if (isNodeBuffer) {
|
|
65
|
+
return binary.byteLength > 0 ? binary : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (typeof ArrayBuffer !== 'undefined' && binary instanceof ArrayBuffer) {
|
|
69
|
+
return binary.byteLength > 0 ? binary : undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (
|
|
73
|
+
typeof ArrayBuffer !== 'undefined' &&
|
|
74
|
+
typeof ArrayBuffer.isView === 'function' &&
|
|
75
|
+
ArrayBuffer.isView(binary)
|
|
76
|
+
) {
|
|
77
|
+
if (binary.byteLength <= 0) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
const source = new Uint8Array(binary.buffer, binary.byteOffset, binary.byteLength);
|
|
81
|
+
const normalized = new Uint8Array(binary.byteLength);
|
|
82
|
+
normalized.set(source);
|
|
83
|
+
return normalized.buffer;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const customBuffer = binary as {
|
|
87
|
+
[index: number]: unknown;
|
|
88
|
+
byteLength?: unknown;
|
|
89
|
+
constructor?: {
|
|
90
|
+
isBuffer?: (value: unknown) => boolean;
|
|
91
|
+
};
|
|
92
|
+
length?: unknown;
|
|
93
|
+
};
|
|
94
|
+
if (
|
|
95
|
+
typeof customBuffer.constructor?.isBuffer !== 'function' ||
|
|
96
|
+
!customBuffer.constructor.isBuffer(binary) ||
|
|
97
|
+
typeof customBuffer.byteLength !== 'number' ||
|
|
98
|
+
!Number.isSafeInteger(customBuffer.byteLength) ||
|
|
99
|
+
customBuffer.byteLength <= 0 ||
|
|
100
|
+
typeof customBuffer.length !== 'number' ||
|
|
101
|
+
customBuffer.length !== customBuffer.byteLength
|
|
102
|
+
) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const { byteLength } = customBuffer;
|
|
107
|
+
const normalized = new Uint8Array(byteLength);
|
|
108
|
+
for (let index = 0; index < byteLength; index += 1) {
|
|
109
|
+
const { [index]: byte } = customBuffer;
|
|
110
|
+
if (typeof byte !== 'number' || !Number.isInteger(byte) || byte < 0 || byte > 255) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
normalized[index] = byte;
|
|
114
|
+
}
|
|
115
|
+
return normalized.buffer;
|
|
116
|
+
};
|
|
117
|
+
|
|
46
118
|
export default class FirmwareUpdateV2 extends BaseMethod<Params> {
|
|
47
119
|
checkPromise: Deferred<any> | null = null;
|
|
48
120
|
|
|
@@ -286,6 +358,48 @@ export default class FirmwareUpdateV2 extends BaseMethod<Params> {
|
|
|
286
358
|
|
|
287
359
|
this.checkVersionForCopyTouchResource(features, firmwareType);
|
|
288
360
|
|
|
361
|
+
let preparedBinary: FirmwareBinary | undefined;
|
|
362
|
+
const acquireFirmwareBinary = async (): Promise<FirmwareBinary> => {
|
|
363
|
+
try {
|
|
364
|
+
if (preparedBinary) {
|
|
365
|
+
return preparedBinary;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (params.binary !== undefined) {
|
|
369
|
+
preparedBinary = normalizeFirmwareBinary(params.binary);
|
|
370
|
+
if (!preparedBinary) {
|
|
371
|
+
throw new Error('firmware binary is empty or invalid');
|
|
372
|
+
}
|
|
373
|
+
return preparedBinary;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (!device.features) {
|
|
377
|
+
throw ERRORS.TypedError(
|
|
378
|
+
HardwareErrorCode.RuntimeError,
|
|
379
|
+
'no features found for this device'
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
this.postTipMessage('DownloadFirmware');
|
|
384
|
+
const firmware = await getBinary({
|
|
385
|
+
features: device.features,
|
|
386
|
+
version: params.version,
|
|
387
|
+
updateType: params.updateType,
|
|
388
|
+
isUpdateBootloader: params.isUpdateBootloader,
|
|
389
|
+
firmwareType,
|
|
390
|
+
requestOptions: FIRMWARE_DOWNLOAD_REQUEST_OPTIONS,
|
|
391
|
+
});
|
|
392
|
+
preparedBinary = normalizeFirmwareBinary(firmware.binary);
|
|
393
|
+
if (!preparedBinary) {
|
|
394
|
+
throw new Error('downloaded firmware binary is empty or invalid');
|
|
395
|
+
}
|
|
396
|
+
this.postTipMessage('DownloadFirmwareSuccess');
|
|
397
|
+
return preparedBinary;
|
|
398
|
+
} catch (err) {
|
|
399
|
+
throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
289
403
|
if (!features?.bootloader_mode && features) {
|
|
290
404
|
const uuid = getDeviceUUID(features);
|
|
291
405
|
// should go to bootloader mode manually
|
|
@@ -319,6 +433,12 @@ export default class FirmwareUpdateV2 extends BaseMethod<Params> {
|
|
|
319
433
|
// check if the device commands has been disposed
|
|
320
434
|
this.device?.commands?.checkDisposed();
|
|
321
435
|
|
|
436
|
+
// A failed firmware download must leave the device in normal mode.
|
|
437
|
+
await acquireFirmwareBinary();
|
|
438
|
+
|
|
439
|
+
// The request may outlive the current transport command instance.
|
|
440
|
+
this.device?.commands?.checkDisposed();
|
|
441
|
+
|
|
322
442
|
// auto go to bootloader mode
|
|
323
443
|
try {
|
|
324
444
|
this.postTipMessage('AutoRebootToBootloader');
|
|
@@ -357,33 +477,8 @@ export default class FirmwareUpdateV2 extends BaseMethod<Params> {
|
|
|
357
477
|
}
|
|
358
478
|
}
|
|
359
479
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
try {
|
|
363
|
-
if (params.binary) {
|
|
364
|
-
binary = this.params.binary;
|
|
365
|
-
} else {
|
|
366
|
-
if (!device.features) {
|
|
367
|
-
throw ERRORS.TypedError(
|
|
368
|
-
HardwareErrorCode.RuntimeError,
|
|
369
|
-
'no features found for this device'
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
this.postTipMessage('DownloadFirmware');
|
|
373
|
-
|
|
374
|
-
const firmware = await getBinary({
|
|
375
|
-
features: device.features,
|
|
376
|
-
version: params.version,
|
|
377
|
-
updateType: params.updateType,
|
|
378
|
-
isUpdateBootloader: params.isUpdateBootloader,
|
|
379
|
-
firmwareType,
|
|
380
|
-
});
|
|
381
|
-
binary = firmware.binary;
|
|
382
|
-
this.postTipMessage('DownloadFirmwareSuccess');
|
|
383
|
-
}
|
|
384
|
-
} catch (err) {
|
|
385
|
-
throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err);
|
|
386
|
-
}
|
|
480
|
+
// Devices already in bootloader mode still acquire through the same helper.
|
|
481
|
+
const binary = await acquireFirmwareBinary();
|
|
387
482
|
|
|
388
483
|
// check if the device commands has been disposed
|
|
389
484
|
this.device?.commands?.checkDisposed();
|
|
@@ -7,9 +7,12 @@ import { findLatestRelease } from '../../utils/release';
|
|
|
7
7
|
import { getFirmwareUpdateField } from '../../utils/deviceFeaturesUtils';
|
|
8
8
|
|
|
9
9
|
import type { Features } from '../../types';
|
|
10
|
+
import type { HttpRequestOptions } from '../../utils/networkUtils';
|
|
10
11
|
import type { EFirmwareType } from '@onekeyfe/hd-shared';
|
|
11
12
|
import type { IFirmwareField } from '../../data-manager/DataManager';
|
|
12
13
|
|
|
14
|
+
export type FirmwareBinary = ArrayBuffer | Buffer;
|
|
15
|
+
|
|
13
16
|
export interface GetInfoProps {
|
|
14
17
|
features: Features;
|
|
15
18
|
updateType: 'firmware' | 'ble';
|
|
@@ -20,6 +23,7 @@ export interface GetInfoProps {
|
|
|
20
23
|
|
|
21
24
|
interface GetBinaryProps extends GetInfoProps {
|
|
22
25
|
version?: number[];
|
|
26
|
+
requestOptions?: HttpRequestOptions;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export const getBinary = async ({
|
|
@@ -28,6 +32,7 @@ export const getBinary = async ({
|
|
|
28
32
|
version,
|
|
29
33
|
isUpdateBootloader,
|
|
30
34
|
firmwareType,
|
|
35
|
+
requestOptions,
|
|
31
36
|
}: GetBinaryProps) => {
|
|
32
37
|
const releaseInfo = getInfo({
|
|
33
38
|
features,
|
|
@@ -55,9 +60,9 @@ export const getBinary = async ({
|
|
|
55
60
|
: isUpdateBootloader
|
|
56
61
|
? releaseInfo.bootloaderResource
|
|
57
62
|
: releaseInfo.url;
|
|
58
|
-
let fw;
|
|
63
|
+
let fw: FirmwareBinary;
|
|
59
64
|
try {
|
|
60
|
-
fw = await httpRequest(url, 'binary');
|
|
65
|
+
fw = await httpRequest(url, 'binary', requestOptions);
|
|
61
66
|
} catch {
|
|
62
67
|
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Method_FirmwareUpdate_DownloadFailed');
|
|
63
68
|
}
|
|
@@ -69,7 +74,7 @@ export const getBinary = async ({
|
|
|
69
74
|
};
|
|
70
75
|
|
|
71
76
|
export const getSysResourceBinary = async (url: string) => {
|
|
72
|
-
let fw;
|
|
77
|
+
let fw: FirmwareBinary;
|
|
73
78
|
try {
|
|
74
79
|
fw = await httpRequest(url, 'binary');
|
|
75
80
|
} catch {
|
|
@@ -476,7 +476,18 @@ export default class KaspaSignTransaction extends BaseMethod<KaspaSignTransactio
|
|
|
476
476
|
'KaspaSignTransaction: device firmware uses the streaming protocol; every output requires address or addressN'
|
|
477
477
|
);
|
|
478
478
|
}
|
|
479
|
-
|
|
479
|
+
try {
|
|
480
|
+
return await this.signTxStream(typedCall, response);
|
|
481
|
+
} catch (error) {
|
|
482
|
+
// Device rejected our refTxs; the caller decides whether to sign without them.
|
|
483
|
+
if (
|
|
484
|
+
error instanceof HardwareError &&
|
|
485
|
+
String(error.message).toLowerCase().includes('previous transaction id mismatch')
|
|
486
|
+
) {
|
|
487
|
+
throw ERRORS.TypedError(HardwareErrorCode.KaspaPrevTxIdMismatch, String(error.message));
|
|
488
|
+
}
|
|
489
|
+
throw error;
|
|
490
|
+
}
|
|
480
491
|
}
|
|
481
492
|
|
|
482
493
|
// Legacy answer to a streaming-only packet: no prehash material exists.
|
package/src/utils/assets.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { httpRequest as browserHttpRequest } from './networkUtils';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import type { HttpRequestOptions } from './networkUtils';
|
|
4
|
+
|
|
5
|
+
export const httpRequest = (url: string, type: string, options?: HttpRequestOptions): any =>
|
|
6
|
+
browserHttpRequest(url, type, options);
|
|
4
7
|
|
|
5
8
|
export const getTimeStamp = () => new Date().getTime();
|
|
@@ -1,25 +1,281 @@
|
|
|
1
|
-
import axios from 'axios';
|
|
1
|
+
import axios, { type AxiosResponse } from 'axios';
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export type HttpRequestOptions = {
|
|
4
|
+
/** Axios adapter-defined request timeout retained for existing opt-in callers. */
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
/** Deadline for DNS, connection, TLS, redirects, and the first response-body progress. */
|
|
7
|
+
connectTimeoutMs?: number;
|
|
8
|
+
/** Maximum idle interval between response-body progress events. */
|
|
9
|
+
readTimeoutMs?: number;
|
|
10
|
+
/** Wall-clock deadline across all attempts and retry backoff. */
|
|
11
|
+
overallTimeoutMs?: number;
|
|
12
|
+
maxRetries?: number;
|
|
13
|
+
retryDelayMs?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const MAX_RETRY_COUNT = 3;
|
|
17
|
+
const REQUEST_PHASE_TIMEOUT_CODES = {
|
|
18
|
+
connect: 'ECONNECTTIMEDOUT',
|
|
19
|
+
read: 'EREADTIMEDOUT',
|
|
20
|
+
} as const;
|
|
21
|
+
const RETRYABLE_HTTP_STATUSES = new Set([500, 502, 503, 504]);
|
|
22
|
+
const RETRYABLE_NETWORK_ERROR_CODES = new Set([
|
|
23
|
+
REQUEST_PHASE_TIMEOUT_CODES.connect,
|
|
24
|
+
REQUEST_PHASE_TIMEOUT_CODES.read,
|
|
25
|
+
'ECONNABORTED',
|
|
26
|
+
'ECONNREFUSED',
|
|
27
|
+
'ECONNRESET',
|
|
28
|
+
'EHOSTUNREACH',
|
|
29
|
+
'ENETDOWN',
|
|
30
|
+
'ENETUNREACH',
|
|
31
|
+
'ENOTFOUND',
|
|
32
|
+
'EAI_AGAIN',
|
|
33
|
+
'ERR_NETWORK',
|
|
34
|
+
'ETIMEDOUT',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const createAbortError = () => {
|
|
38
|
+
const error = new Error('httpRequest aborted');
|
|
39
|
+
error.name = 'AbortError';
|
|
40
|
+
return error;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const createOverallTimeoutError = (url: string, timeoutMs: number) => {
|
|
44
|
+
const error = new Error(`httpRequest overall timeout: ${url} ${timeoutMs}ms`);
|
|
45
|
+
error.name = 'HttpRequestOverallTimeoutError';
|
|
46
|
+
return error;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const createRequestPhaseTimeoutError = (
|
|
50
|
+
url: string,
|
|
51
|
+
phase: keyof typeof REQUEST_PHASE_TIMEOUT_CODES,
|
|
52
|
+
timeoutMs: number
|
|
53
|
+
) => {
|
|
54
|
+
const error = new Error(`httpRequest ${phase} timeout: ${url} ${timeoutMs}ms`);
|
|
55
|
+
error.name = 'HttpRequestPhaseTimeoutError';
|
|
56
|
+
return Object.assign(error, {
|
|
57
|
+
code: REQUEST_PHASE_TIMEOUT_CODES[phase],
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const waitForRetry = async (delayMs: number, attempt: number, signal?: AbortSignal) => {
|
|
62
|
+
const backoffMs = delayMs * 2 ** attempt;
|
|
63
|
+
if (backoffMs <= 0) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
await new Promise<void>((resolve, reject) => {
|
|
67
|
+
const timeoutTimer = setTimeout(() => {
|
|
68
|
+
signal?.removeEventListener('abort', handleAbort);
|
|
69
|
+
resolve();
|
|
70
|
+
}, backoffMs);
|
|
71
|
+
const handleAbort = () => {
|
|
72
|
+
clearTimeout(timeoutTimer);
|
|
73
|
+
reject(createAbortError());
|
|
74
|
+
};
|
|
75
|
+
if (signal?.aborted) {
|
|
76
|
+
handleAbort();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
signal?.addEventListener('abort', handleAbort, { once: true });
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const isRetryableRequestError = (error: unknown) => {
|
|
84
|
+
if (axios.isCancel(error)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (
|
|
88
|
+
typeof error === 'object' &&
|
|
89
|
+
error !== null &&
|
|
90
|
+
'code' in error &&
|
|
91
|
+
typeof error.code === 'string' &&
|
|
92
|
+
RETRYABLE_NETWORK_ERROR_CODES.has(error.code)
|
|
93
|
+
) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if (!axios.isAxiosError(error)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const status = error.response?.status;
|
|
100
|
+
if (status !== undefined) {
|
|
101
|
+
return RETRYABLE_HTTP_STATUSES.has(Number(status));
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const httpRequest = async <T = unknown>(
|
|
107
|
+
url: string,
|
|
108
|
+
type = 'text',
|
|
109
|
+
options: HttpRequestOptions = {}
|
|
110
|
+
): Promise<T> => {
|
|
4
111
|
const headers: any = {};
|
|
5
112
|
if (url.indexOf('ngrok-free.app') > -1) {
|
|
6
113
|
headers['ngrok-skip-browser-warning'] = true;
|
|
7
114
|
}
|
|
8
|
-
const response = await axios.request({
|
|
9
|
-
url,
|
|
10
|
-
withCredentials: false,
|
|
11
|
-
responseType: type === 'binary' ? 'arraybuffer' : 'json',
|
|
12
|
-
headers,
|
|
13
|
-
});
|
|
14
115
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
116
|
+
const timeoutMs =
|
|
117
|
+
Number.isSafeInteger(options.timeoutMs) && Number(options.timeoutMs) > 0
|
|
118
|
+
? Number(options.timeoutMs)
|
|
119
|
+
: undefined;
|
|
120
|
+
const connectTimeoutMs =
|
|
121
|
+
Number.isSafeInteger(options.connectTimeoutMs) && Number(options.connectTimeoutMs) > 0
|
|
122
|
+
? Number(options.connectTimeoutMs)
|
|
123
|
+
: undefined;
|
|
124
|
+
const readTimeoutMs =
|
|
125
|
+
Number.isSafeInteger(options.readTimeoutMs) && Number(options.readTimeoutMs) > 0
|
|
126
|
+
? Number(options.readTimeoutMs)
|
|
127
|
+
: undefined;
|
|
128
|
+
const overallTimeoutMs =
|
|
129
|
+
Number.isSafeInteger(options.overallTimeoutMs) && Number(options.overallTimeoutMs) > 0
|
|
130
|
+
? Number(options.overallTimeoutMs)
|
|
131
|
+
: undefined;
|
|
132
|
+
const maxRetries =
|
|
133
|
+
Number.isSafeInteger(options.maxRetries) && Number(options.maxRetries) > 0
|
|
134
|
+
? Math.min(Number(options.maxRetries), MAX_RETRY_COUNT)
|
|
135
|
+
: 0;
|
|
136
|
+
const retryDelayMs =
|
|
137
|
+
Number.isSafeInteger(options.retryDelayMs) && Number(options.retryDelayMs) > 0
|
|
138
|
+
? Number(options.retryDelayMs)
|
|
139
|
+
: 0;
|
|
140
|
+
const overallTimeoutError = overallTimeoutMs
|
|
141
|
+
? createOverallTimeoutError(url, overallTimeoutMs)
|
|
142
|
+
: undefined;
|
|
143
|
+
const overallDeadlineAt = overallTimeoutMs ? Date.now() + overallTimeoutMs : undefined;
|
|
144
|
+
const assertWithinOverallDeadline = () => {
|
|
145
|
+
if (overallDeadlineAt !== undefined && overallTimeoutError && Date.now() >= overallDeadlineAt) {
|
|
146
|
+
throw overallTimeoutError;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const request = async (attempt: number, signal?: AbortSignal): Promise<T> => {
|
|
151
|
+
assertWithinOverallDeadline();
|
|
152
|
+
if (signal?.aborted) {
|
|
153
|
+
throw createAbortError();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let response: AxiosResponse<T> | undefined;
|
|
157
|
+
let requestError: unknown;
|
|
158
|
+
let requestFailed = false;
|
|
159
|
+
let phaseTimeoutError: ReturnType<typeof createRequestPhaseTimeoutError> | undefined;
|
|
160
|
+
let phaseTimeoutTimer: ReturnType<typeof setTimeout> | undefined;
|
|
161
|
+
const hasPhaseDeadline = Boolean(connectTimeoutMs || readTimeoutMs);
|
|
162
|
+
const attemptController = signal || hasPhaseDeadline ? new AbortController() : undefined;
|
|
163
|
+
let attemptSettled = false;
|
|
164
|
+
let lastProgressLoaded = 0;
|
|
165
|
+
const handleParentAbort = () => {
|
|
166
|
+
attemptController?.abort();
|
|
167
|
+
};
|
|
168
|
+
const clearPhaseTimeout = () => {
|
|
169
|
+
if (phaseTimeoutTimer) {
|
|
170
|
+
clearTimeout(phaseTimeoutTimer);
|
|
171
|
+
phaseTimeoutTimer = undefined;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
const armPhaseTimeout = (
|
|
175
|
+
phase: keyof typeof REQUEST_PHASE_TIMEOUT_CODES,
|
|
176
|
+
phaseTimeoutMs: number | undefined
|
|
177
|
+
) => {
|
|
178
|
+
clearPhaseTimeout();
|
|
179
|
+
if (!phaseTimeoutMs || !attemptController) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
phaseTimeoutTimer = setTimeout(() => {
|
|
183
|
+
phaseTimeoutError = createRequestPhaseTimeoutError(url, phase, phaseTimeoutMs);
|
|
184
|
+
attemptController.abort();
|
|
185
|
+
}, phaseTimeoutMs);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
if (signal) {
|
|
189
|
+
signal.addEventListener('abort', handleParentAbort, { once: true });
|
|
190
|
+
}
|
|
191
|
+
armPhaseTimeout('connect', connectTimeoutMs);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
response = await axios.request<T>({
|
|
195
|
+
url,
|
|
196
|
+
withCredentials: false,
|
|
197
|
+
responseType: type === 'binary' ? 'arraybuffer' : 'json',
|
|
198
|
+
headers,
|
|
199
|
+
...(timeoutMs ? { timeout: timeoutMs } : {}),
|
|
200
|
+
...(attemptController ? { signal: attemptController.signal } : {}),
|
|
201
|
+
...(hasPhaseDeadline
|
|
202
|
+
? {
|
|
203
|
+
adapter: ['xhr', 'http'],
|
|
204
|
+
onDownloadProgress: (progressEvent: { loaded: number }) => {
|
|
205
|
+
if (
|
|
206
|
+
attemptSettled ||
|
|
207
|
+
attemptController?.signal.aborted ||
|
|
208
|
+
!Number.isFinite(progressEvent.loaded) ||
|
|
209
|
+
progressEvent.loaded <= lastProgressLoaded
|
|
210
|
+
) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
lastProgressLoaded = progressEvent.loaded;
|
|
214
|
+
armPhaseTimeout('read', readTimeoutMs);
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
: {}),
|
|
218
|
+
});
|
|
219
|
+
if (phaseTimeoutError) {
|
|
220
|
+
requestFailed = true;
|
|
221
|
+
requestError = phaseTimeoutError;
|
|
222
|
+
response = undefined;
|
|
223
|
+
}
|
|
224
|
+
} catch (error) {
|
|
225
|
+
requestFailed = true;
|
|
226
|
+
requestError = phaseTimeoutError ?? (signal?.aborted ? createAbortError() : error);
|
|
227
|
+
} finally {
|
|
228
|
+
attemptSettled = true;
|
|
229
|
+
clearPhaseTimeout();
|
|
230
|
+
signal?.removeEventListener('abort', handleParentAbort);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
assertWithinOverallDeadline();
|
|
234
|
+
|
|
235
|
+
if (requestFailed) {
|
|
236
|
+
if (attempt >= maxRetries || !isRetryableRequestError(requestError)) {
|
|
237
|
+
throw requestError;
|
|
238
|
+
}
|
|
239
|
+
await waitForRetry(retryDelayMs, attempt, signal);
|
|
240
|
+
assertWithinOverallDeadline();
|
|
241
|
+
return request(attempt + 1, signal);
|
|
18
242
|
}
|
|
19
|
-
|
|
243
|
+
|
|
244
|
+
if (!response) {
|
|
245
|
+
throw new Error(`httpRequest completed without a response: ${url}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (+response.status === 200) {
|
|
20
249
|
return response.data;
|
|
21
250
|
}
|
|
22
|
-
|
|
251
|
+
|
|
252
|
+
if (RETRYABLE_HTTP_STATUSES.has(Number(response.status)) && attempt < maxRetries) {
|
|
253
|
+
await waitForRetry(retryDelayMs, attempt, signal);
|
|
254
|
+
assertWithinOverallDeadline();
|
|
255
|
+
return request(attempt + 1, signal);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
throw new Error(`httpRequest error: ${url} ${response.statusText}`);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
if (!overallTimeoutMs) {
|
|
262
|
+
return request(0);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const controller = new AbortController();
|
|
266
|
+
const requestPromise = request(0, controller.signal);
|
|
267
|
+
let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
|
|
268
|
+
try {
|
|
269
|
+
return await new Promise<T>((resolve, reject) => {
|
|
270
|
+
timeoutTimer = setTimeout(() => {
|
|
271
|
+
reject(overallTimeoutError);
|
|
272
|
+
controller.abort();
|
|
273
|
+
}, overallTimeoutMs);
|
|
274
|
+
requestPromise.then(resolve, reject);
|
|
275
|
+
});
|
|
276
|
+
} finally {
|
|
277
|
+
if (timeoutTimer) {
|
|
278
|
+
clearTimeout(timeoutTimer);
|
|
279
|
+
}
|
|
23
280
|
}
|
|
24
|
-
throw new Error(`httpRequest error: ${url} ${response.statusText}`);
|
|
25
281
|
};
|