@onekeyfe/hd-core 1.1.34-alpha.0 → 1.1.34-alpha.2

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.
@@ -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 {
package/src/core/index.ts CHANGED
@@ -2,6 +2,7 @@ import semver from 'semver';
2
2
  import EventEmitter from 'events';
3
3
  import {
4
4
  ERRORS,
5
+ ERROR_CODES_REQUIRE_DISCONNECT,
5
6
  ERROR_CODES_REQUIRE_RELEASE,
6
7
  HardwareError,
7
8
  HardwareErrorCode,
@@ -832,6 +833,16 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
832
833
  });
833
834
  }
834
835
  } catch (err) {
836
+ // Device.run()'s REQUIRE_DISCONNECT handling never sees acquire/initialize
837
+ // failures — drop the wedged link here so retries cold-connect instead of
838
+ // reusing it forever.
839
+ if (
840
+ ERROR_CODES_REQUIRE_DISCONNECT.includes(err.errorCode) &&
841
+ device.mainId &&
842
+ device.deviceConnector
843
+ ) {
844
+ await device.deviceConnector.disconnect(device.mainId).catch(() => undefined);
845
+ }
835
846
  if (err.errorCode === HardwareErrorCode.BleTimeoutError && retryCount < 6) {
836
847
  const nextRetry = retryCount + 1;
837
848
  Log.debug(`Bluetooth connect timeout and will retry, retry count: ${nextRetry}`);
@@ -1,5 +1,8 @@
1
1
  import { httpRequest as browserHttpRequest } from './networkUtils';
2
2
 
3
- export const httpRequest = (url: string, type: string): any => browserHttpRequest(url, type);
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 const httpRequest = async (url: string, type = 'text') => {
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
- if (+response.status === 200) {
16
- if (type === 'json') {
17
- return response.data;
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
- if (type === 'binary') {
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
- return response.data;
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
  };