@onekeyfe/hd-core 1.2.0-alpha.56 → 1.2.0-alpha.58

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.
Files changed (36) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +84 -2
  2. package/__tests__/DeviceCommands.test.ts +17 -6
  3. package/__tests__/core-initialization.test.ts +23 -0
  4. package/__tests__/device-connector-protocol.test.ts +81 -0
  5. package/__tests__/device-settings.test.ts +39 -18
  6. package/__tests__/protocol-v2-resources.test.ts +24 -0
  7. package/__tests__/protocol-v2.test.ts +46 -1
  8. package/__tests__/protocolV2FileWrite.test.ts +113 -0
  9. package/dist/api/FirmwareUpdateV4.d.ts +0 -1
  10. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  11. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  12. package/dist/api/device/DeviceSettings.d.ts.map +1 -1
  13. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +4 -2
  14. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  15. package/dist/api/helpers/protocolV2FileWrite.d.ts +7 -0
  16. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  17. package/dist/core/index.d.ts.map +1 -1
  18. package/dist/data-manager/DataManager.d.ts +1 -0
  19. package/dist/data-manager/DataManager.d.ts.map +1 -1
  20. package/dist/data-manager/TransportManager.d.ts.map +1 -1
  21. package/dist/device/DeviceCommands.d.ts.map +1 -1
  22. package/dist/device/DeviceConnector.d.ts +2 -1
  23. package/dist/device/DeviceConnector.d.ts.map +1 -1
  24. package/dist/index.d.ts +3 -1
  25. package/dist/index.js +448 -336
  26. package/package.json +4 -4
  27. package/src/api/FirmwareUpdateV4.ts +39 -69
  28. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +3 -1
  29. package/src/api/device/DeviceSettings.ts +5 -4
  30. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +12 -1
  31. package/src/api/helpers/protocolV2FileWrite.ts +192 -73
  32. package/src/core/index.ts +9 -6
  33. package/src/data-manager/DataManager.ts +25 -4
  34. package/src/data-manager/TransportManager.ts +6 -0
  35. package/src/device/DeviceCommands.ts +20 -2
  36. package/src/device/DeviceConnector.ts +33 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-core",
3
- "version": "1.2.0-alpha.56",
3
+ "version": "1.2.0-alpha.58",
4
4
  "description": "Core processes and APIs for communicating with OneKey hardware devices.",
5
5
  "author": "OneKey",
6
6
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
@@ -25,8 +25,8 @@
25
25
  "url": "https://github.com/OneKeyHQ/hardware-js-sdk/issues"
26
26
  },
27
27
  "dependencies": {
28
- "@onekeyfe/hd-shared": "1.2.0-alpha.56",
29
- "@onekeyfe/hd-transport": "1.2.0-alpha.56",
28
+ "@onekeyfe/hd-shared": "1.2.0-alpha.58",
29
+ "@onekeyfe/hd-transport": "1.2.0-alpha.58",
30
30
  "axios": "1.15.2",
31
31
  "bignumber.js": "^9.0.2",
32
32
  "bytebuffer": "^5.0.1",
@@ -44,5 +44,5 @@
44
44
  "@types/w3c-web-usb": "^1.0.10",
45
45
  "@types/web-bluetooth": "^0.0.21"
46
46
  },
47
- "gitHead": "2c472748d67072307c21f8d33bc021cf18fe2ad7"
47
+ "gitHead": "43c11efeb7001741d4258c855b287993bc0a798e"
48
48
  }
@@ -8,6 +8,7 @@ import { sha256 } from '@noble/hashes/sha256';
8
8
 
9
9
  import { FirmwareUpdateTipMessage, UI_REQUEST } from '../events/ui-request';
10
10
  import { validateProtocolV2FilesystemPath } from './helpers/filesystemValidation';
11
+ import { writeProtocolV2File } from './helpers/protocolV2FileWrite';
11
12
  import { validateParams } from './helpers/paramsValidator';
12
13
  import {
13
14
  LoggerNames,
@@ -53,7 +54,6 @@ import type {
53
54
 
54
55
  const Log = getLogger(LoggerNames.Method);
55
56
 
56
- const SESSION_ERROR = 'session not found';
57
57
  const PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT = 90 * 1000;
58
58
  const PROTOCOL_V2_FINAL_RECONNECT_TIMEOUT = 3 * 60 * 1000;
59
59
  const PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT = 5 * 1000;
@@ -974,15 +974,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
974
974
  }
975
975
  if (this.isProtocolV2BootloaderMode()) {
976
976
  Log.debug('Protocol V2 device is already in bootloader mode, skip reboot');
977
+ this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
977
978
  return false;
978
979
  }
979
980
 
980
981
  try {
981
982
  this.postTipMessage(FirmwareUpdateTipMessage.AutoRebootToBootloader);
982
983
  await this.protocolV2Reboot(DeviceRebootType.Bootloader);
983
- this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
984
984
  await wait(1000);
985
985
  await this.waitForProtocolV2BootloaderMode();
986
+ this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
986
987
  return true;
987
988
  } catch (error) {
988
989
  if (error instanceof HardwareError) {
@@ -1631,7 +1632,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1631
1632
  onTransferredBytes,
1632
1633
  }: ProtocolV2FileTransferParams) {
1633
1634
  const chunkSize = this.getProtocolV2FirmwareChunkSize();
1634
- let offset = 0;
1635
1635
  const getUploadProgress = (fileOffset: number) => {
1636
1636
  if (totalSize !== undefined && processedSize !== undefined) {
1637
1637
  return Math.min(Math.ceil(((processedSize + fileOffset) / totalSize) * 100), 99);
@@ -1639,37 +1639,43 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1639
1639
  return Math.min(Math.ceil((fileOffset / payload.byteLength) * 100), 99);
1640
1640
  };
1641
1641
 
1642
- while (offset < payload.byteLength) {
1643
- const chunkEnd = Math.min(offset + chunkSize, payload.byteLength);
1644
- const chunkLength = chunkEnd - offset;
1645
- const chunk = payload.slice(offset, chunkEnd);
1646
- const overwrite = offset === 0;
1647
- const progress = getProtocolV2DeviceTransferProgress(
1648
- (processedSize ?? 0) + offset,
1649
- (processedSize ?? 0) + chunkEnd,
1650
- totalSize ?? payload.byteLength
1651
- );
1652
-
1653
- const writeRes = await this.fileWriteChunk(
1654
- filePath,
1655
- payload.byteLength,
1656
- offset,
1657
- chunk,
1658
- overwrite,
1659
- progress
1660
- );
1661
- const rawProcessedByte = writeRes.message.processed_byte;
1662
- const processedByte = Number(rawProcessedByte);
1663
- const nextOffset = rawProcessedByte === undefined ? offset + chunkLength : processedByte;
1664
- if (!Number.isFinite(nextOffset) || nextOffset <= offset || nextOffset > chunkEnd) {
1665
- throw ERRORS.TypedError(
1666
- HardwareErrorCode.EmmcFileWriteFirmwareError,
1667
- `invalid processed_byte ${writeRes.message.processed_byte} for offset ${offset}`
1668
- );
1642
+ try {
1643
+ await writeProtocolV2File({
1644
+ commands: this.device.getCommands(),
1645
+ path: filePath,
1646
+ data: payload,
1647
+ totalSize: payload.byteLength,
1648
+ chunkSize,
1649
+ overwrite: true,
1650
+ append: false,
1651
+ writeWithResponse: true,
1652
+ maxChunkRetries: 0,
1653
+ getUiPercentage: ({ offset, chunkLength }) =>
1654
+ getProtocolV2DeviceTransferProgress(
1655
+ (processedSize ?? 0) + offset,
1656
+ (processedSize ?? 0) + offset + chunkLength,
1657
+ totalSize ?? payload.byteLength
1658
+ ),
1659
+ onProgress: progress => {
1660
+ const transferredBytes = (processedSize ?? 0) + progress.transferredBytes;
1661
+ onTransferredBytes?.(transferredBytes);
1662
+ this.postProgressMessage(getUploadProgress(progress.transferredBytes), 'transferData', {
1663
+ transferredBytes,
1664
+ totalBytes: totalSize ?? payload.byteLength,
1665
+ rateBytesPerSecond: progress.rateBytesPerSecond,
1666
+ elapsedMs: progress.elapsedMs,
1667
+ });
1668
+ },
1669
+ });
1670
+ } catch (error) {
1671
+ if (
1672
+ error instanceof HardwareError &&
1673
+ error.errorCode === HardwareErrorCode.RuntimeError &&
1674
+ error.message.includes('FilesystemFileWrite')
1675
+ ) {
1676
+ throw ERRORS.TypedError(HardwareErrorCode.EmmcFileWriteFirmwareError, error.message);
1669
1677
  }
1670
- offset = nextOffset;
1671
- onTransferredBytes?.((processedSize ?? 0) + offset);
1672
- this.postProgressMessage(getUploadProgress(offset), 'transferData');
1678
+ throw error;
1673
1679
  }
1674
1680
 
1675
1681
  return totalSize !== undefined ? (processedSize ?? 0) + payload.byteLength : 0;
@@ -1689,42 +1695,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1689
1695
  return env ?? 'unknown';
1690
1696
  }
1691
1697
 
1692
- private async fileWriteChunk(
1693
- filePath: string,
1694
- totalFileSize: number,
1695
- offset: number,
1696
- chunk: ArrayBuffer | Buffer,
1697
- overwrite: boolean,
1698
- progress: number | null
1699
- ): Promise<TypedResponseMessage<'FilesystemFile'>> {
1700
- const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
1701
- const writeRes = await typedCall(
1702
- 'FilesystemFileWrite',
1703
- 'FilesystemFile',
1704
- {
1705
- file: {
1706
- path: filePath,
1707
- offset,
1708
- total_size: totalFileSize,
1709
- data: chunk,
1710
- },
1711
- overwrite,
1712
- append: false,
1713
- ui_percentage: progress ?? undefined,
1714
- },
1715
- { writeWithResponse: true }
1716
- );
1717
- if (writeRes.type !== 'FilesystemFile') {
1718
- if ((writeRes as any).type === 'CallMethodError') {
1719
- if (((writeRes as any).message.error ?? '').indexOf(SESSION_ERROR) > -1) {
1720
- throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, SESSION_ERROR);
1721
- }
1722
- }
1723
- throw ERRORS.TypedError(HardwareErrorCode.EmmcFileWriteFirmwareError, 'transfer data error');
1724
- }
1725
- return writeRes;
1726
- }
1727
-
1728
1698
  private async recoverProtocolV2FileTransfer() {
1729
1699
  const env = DataManager.getSettings('env');
1730
1700
  if (DataManager.isBleConnect(env)) {
@@ -393,12 +393,14 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
393
393
  // the root fingerprint, so each nested chain method must resume the
394
394
  // requested standard or hidden wallet before sending its device command.
395
395
  const useEmptyPassphrase = this.payload.useEmptyPassphrase === true;
396
+ const deriveCardano = method.name.startsWith('cardano');
396
397
  const shouldResumeWalletSession = useEmptyPassphrase || !!this.payload.passphraseState;
397
398
  if (this.device.isProtocolV2() && shouldResumeWalletSession) {
398
399
  const passphraseStateSafety = await this.device.checkPassphraseStateSafety(
399
400
  this.payload.passphraseState,
400
401
  useEmptyPassphrase,
401
- this.payload.skipPassphraseCheck
402
+ this.payload.skipPassphraseCheck,
403
+ deriveCardano
402
404
  );
403
405
  if (!passphraseStateSafety) {
404
406
  throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
@@ -7,7 +7,6 @@ import { validateParams } from '../helpers/paramsValidator';
7
7
  import {
8
8
  mapApplySettingsToState,
9
9
  mapCommonSettingsToProtocolV2,
10
- mapDeviceSettingsToState,
11
10
  } from '../../device/DeviceStateMapper';
12
11
  import { getProtocolV2SettingsBehavior } from '../../protocols/protocol-v2/settingsUnlockPolicy';
13
12
  import {
@@ -163,6 +162,8 @@ export default class DeviceSettings extends BaseMethod<ApplySettings> {
163
162
  async run() {
164
163
  try {
165
164
  if (this.device.isProtocolV2()) {
165
+ const refreshStatusAndSettings = () =>
166
+ this.device.getDeviceState({ refreshSections: ['status', 'settings'] });
166
167
  assertSettingsSupported(this.payload, DEVICE_SETTINGS_V1_ONLY_FIELDS, 'Protocol V2');
167
168
  const capabilities = getDeviceSettingsCapabilities(
168
169
  this.device.getCurrentDeviceType(),
@@ -194,7 +195,7 @@ export default class DeviceSettings extends BaseMethod<ApplySettings> {
194
195
  const res = await this.device.commands.typedCall('DeviceSettingsPageShow', 'Success', {
195
196
  page: DeviceSettingsPage.DevicePassphrase,
196
197
  });
197
- const updated = await this.device.getDeviceState({ refreshSections: ['status'] });
198
+ const updated = await refreshStatusAndSettings();
198
199
  const lockedAfterDisabling =
199
200
  requestedPassphrase === false &&
200
201
  current.status.unlocked === true &&
@@ -219,7 +220,7 @@ export default class DeviceSettings extends BaseMethod<ApplySettings> {
219
220
  const res = await this.device.commands.typedCall('DeviceSettingsPageShow', 'Success', {
220
221
  page: DeviceSettingsPage.DeviceAirgap,
221
222
  });
222
- const updated = await this.device.getDeviceState({ refreshSections: ['settings'] });
223
+ const updated = await refreshStatusAndSettings();
223
224
  if (updated.settings.airgapMode !== requestedAirgap) {
224
225
  throw TypedError(
225
226
  HardwareErrorCode.RuntimeError,
@@ -235,7 +236,7 @@ export default class DeviceSettings extends BaseMethod<ApplySettings> {
235
236
  const res = await this.device.commands.typedCall('DeviceSettingsSet', 'Success', {
236
237
  settings,
237
238
  });
238
- this.device.updateState(mapDeviceSettingsToState(settings), 'settings-write');
239
+ await refreshStatusAndSettings();
239
240
  return res.message;
240
241
  }
241
242
 
@@ -15,6 +15,7 @@ import { BaseMethod } from '../BaseMethod';
15
15
  import { DEVICE } from '../../events';
16
16
 
17
17
  import type {
18
+ FirmwareProgress,
18
19
  IFirmwareUpdateProgressType,
19
20
  IFirmwareUpdateTipMessage,
20
21
  } from '../../events/ui-request';
@@ -28,6 +29,11 @@ const Log = getLogger(LoggerNames.Method);
28
29
  const SESSION_ERROR = 'session not found';
29
30
  const FIRMWARE_UPDATE_CONFIRM = 'Firmware install confirmed';
30
31
 
32
+ type FirmwareTransferMetrics = Pick<
33
+ FirmwareProgress['payload'],
34
+ 'transferredBytes' | 'totalBytes' | 'rateBytesPerSecond' | 'elapsedMs'
35
+ >;
36
+
31
37
  const isDeviceDisconnectedError = (error: unknown) => {
32
38
  const message = error instanceof Error ? error.message : String(error ?? '');
33
39
  return (
@@ -82,12 +88,17 @@ export class FirmwareUpdateBaseMethod<Params> extends BaseMethod<Params> {
82
88
  * @description Post the progress message
83
89
  * @param progress Post the percentage of the progress
84
90
  */
85
- postProgressMessage = (progress: number, progressType: IFirmwareUpdateProgressType) => {
91
+ postProgressMessage = (
92
+ progress: number,
93
+ progressType: IFirmwareUpdateProgressType,
94
+ metrics?: FirmwareTransferMetrics
95
+ ) => {
86
96
  this.postMessage(
87
97
  createUiMessage(UI_REQUEST.FIRMWARE_PROGRESS, {
88
98
  device: this.device.toMessageObject() as KnownDevice,
89
99
  progress,
90
100
  progressType,
101
+ ...metrics,
91
102
  })
92
103
  );
93
104
  };
@@ -5,9 +5,12 @@ import {
5
5
  import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
6
6
 
7
7
  import { DataManager } from '../../data-manager';
8
+ import { LoggerNames, getLogger } from '../../utils/logger';
8
9
 
9
10
  import type { DeviceCommands } from '../../device/DeviceCommands';
10
11
 
12
+ const Log = getLogger(LoggerNames.Method);
13
+
11
14
  export type ProtocolV2FileWriteData = ArrayBuffer | Uint8Array | Blob | string;
12
15
 
13
16
  export type ProtocolV2FileWriteProgress = {
@@ -18,6 +21,12 @@ export type ProtocolV2FileWriteProgress = {
18
21
  elapsedMs: number;
19
22
  };
20
23
 
24
+ export type ProtocolV2FileWriteChunkContext = {
25
+ offset: number;
26
+ chunkLength: number;
27
+ totalSize: number;
28
+ };
29
+
21
30
  export type ProtocolV2FileWriteOptions = {
22
31
  commands: Pick<DeviceCommands, 'typedCall'>;
23
32
  path: string;
@@ -30,14 +39,54 @@ export type ProtocolV2FileWriteOptions = {
30
39
  append?: boolean;
31
40
  uiPercentage?: number;
32
41
  timeoutMs?: number;
42
+ writeWithResponse?: boolean;
33
43
  maxChunkRetries?: number;
34
44
  paceMs?: number;
35
45
  throwIfAborted?: () => void;
46
+ getUiPercentage?: (context: ProtocolV2FileWriteChunkContext) => number | undefined;
36
47
  onProgress?: (progress: ProtocolV2FileWriteProgress) => void;
37
48
  };
38
49
 
39
50
  const MIN_FILE_CHUNK_SIZE = 64;
40
51
  const FILE_TRANSFER_RATE_WINDOW_MS = 1000;
52
+ const FILE_TRANSFER_LOG_INTERVAL_MS = 10_000;
53
+ const SESSION_ERROR = 'session not found';
54
+
55
+ function formatFileTransferRate(bytesPerSecond: number) {
56
+ return (Math.max(bytesPerSecond, 0) / 1024).toFixed(2);
57
+ }
58
+
59
+ function getAverageFileTransferRate(transferredBytes: number, elapsedMs: number) {
60
+ if (elapsedMs <= 0) return 0;
61
+ return Math.round((Math.max(transferredBytes, 0) / elapsedMs) * 1000);
62
+ }
63
+
64
+ function getFileTransferTransport() {
65
+ const env = DataManager.getSettings('env');
66
+ return env && DataManager.isBleConnect(env) ? 'BLE' : String(env ?? 'unknown');
67
+ }
68
+
69
+ function logFileTransferMetrics({
70
+ transport,
71
+ status,
72
+ transferredBytes,
73
+ totalBytes,
74
+ elapsedMs,
75
+ rateBytesPerSecond,
76
+ }: {
77
+ transport: string;
78
+ status: 'progress' | 'completed' | 'failed';
79
+ transferredBytes: number;
80
+ totalBytes: number;
81
+ elapsedMs: number;
82
+ rateBytesPerSecond: number;
83
+ }) {
84
+ Log.log(
85
+ `[FileWrite] metrics transport=${transport} status=${status} bytes=${transferredBytes}/${totalBytes} elapsed=${(
86
+ elapsedMs / 1000
87
+ ).toFixed(2)}s speed=${formatFileTransferRate(rateBytesPerSecond)} KiB/s`
88
+ );
89
+ }
41
90
 
42
91
  export function isProtocolV2ResponseTimeout(error: unknown) {
43
92
  if (!error || typeof error !== 'object') return false;
@@ -125,89 +174,159 @@ export async function writeProtocolV2File(options: ProtocolV2FileWriteOptions) {
125
174
  let rateWindowStartedAt = startTime;
126
175
  let rateWindowStartedBytes = 0;
127
176
  let rateBytesPerSecond: number | undefined;
177
+ let lastConfirmedAt = startTime;
178
+ let logWindowStartedAt = startTime;
179
+ let logWindowStartedBytes = 0;
180
+ const transport = getFileTransferTransport();
128
181
 
129
- while (written < dataLength) {
130
- options.throwIfAborted?.();
131
- const chunk = data.slice(written, Math.min(written + chunkSize, dataLength));
132
- const offset = startOffset + written;
133
- const progress =
134
- options.uiPercentage ??
135
- getDeviceTransferProgress(offset, offset + chunk.byteLength, totalSize);
136
- const request = {
137
- file: { path: options.path, offset, total_size: totalSize, data: chunk },
138
- overwrite: chunks === 0 ? options.overwrite ?? false : false,
139
- append: options.append ?? false,
140
- ui_percentage: progress,
141
- };
142
- const maxChunkRetries = Math.max(Math.floor(options.maxChunkRetries ?? 0), 0);
143
- let retryCount = 0;
144
- let response;
145
- let isWritePending = true;
146
- while (isWritePending) {
147
- try {
148
- response = await options.commands.typedCall(
149
- 'FilesystemFileWrite',
150
- 'FilesystemFile',
151
- request,
152
- { timeoutMs: options.timeoutMs }
182
+ try {
183
+ while (written < dataLength) {
184
+ options.throwIfAborted?.();
185
+ const chunk = data.slice(written, Math.min(written + chunkSize, dataLength));
186
+ const offset = startOffset + written;
187
+ const progress =
188
+ options.uiPercentage ??
189
+ options.getUiPercentage?.({
190
+ offset,
191
+ chunkLength: chunk.byteLength,
192
+ totalSize,
193
+ }) ??
194
+ getDeviceTransferProgress(offset, offset + chunk.byteLength, totalSize);
195
+ const request = {
196
+ file: { path: options.path, offset, total_size: totalSize, data: chunk },
197
+ overwrite: chunks === 0 ? options.overwrite ?? false : false,
198
+ append: options.append ?? false,
199
+ ui_percentage: progress,
200
+ };
201
+ const maxChunkRetries = Math.max(Math.floor(options.maxChunkRetries ?? 0), 0);
202
+ let retryCount = 0;
203
+ let response;
204
+ let isWritePending = true;
205
+ while (isWritePending) {
206
+ try {
207
+ const callOptions =
208
+ options.writeWithResponse === undefined
209
+ ? { timeoutMs: options.timeoutMs }
210
+ : {
211
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
212
+ writeWithResponse: options.writeWithResponse,
213
+ };
214
+ response = await options.commands.typedCall(
215
+ 'FilesystemFileWrite',
216
+ 'FilesystemFile',
217
+ request,
218
+ callOptions
219
+ );
220
+ isWritePending = false;
221
+ } catch (error) {
222
+ if (retryCount >= maxChunkRetries || !isProtocolV2ResponseTimeout(error)) throw error;
223
+ retryCount += 1;
224
+ options.throwIfAborted?.();
225
+ }
226
+ }
227
+ if (!response) {
228
+ throw ERRORS.TypedError(
229
+ HardwareErrorCode.RuntimeError,
230
+ 'FilesystemFileWrite completed without a response'
153
231
  );
154
- isWritePending = false;
155
- } catch (error) {
156
- if (retryCount >= maxChunkRetries || !isProtocolV2ResponseTimeout(error)) throw error;
157
- retryCount += 1;
158
- options.throwIfAborted?.();
232
+ }
233
+ const responseType = (response as { type?: string }).type;
234
+ if (responseType && responseType !== 'FilesystemFile') {
235
+ const responseError = (response as { message?: { error?: unknown } }).message?.error;
236
+ if (typeof responseError === 'string' && responseError.includes(SESSION_ERROR)) {
237
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, SESSION_ERROR);
238
+ }
239
+ throw ERRORS.TypedError(
240
+ HardwareErrorCode.RuntimeError,
241
+ `FilesystemFileWrite received unexpected response ${responseType}`
242
+ );
243
+ }
244
+ options.throwIfAborted?.();
245
+ lastMessage = response.message;
246
+ const rawProcessedByte = response.message?.processed_byte;
247
+ const processedByte = Number(rawProcessedByte);
248
+ if (
249
+ rawProcessedByte !== undefined &&
250
+ (!Number.isFinite(processedByte) ||
251
+ processedByte <= offset ||
252
+ processedByte > offset + chunk.byteLength)
253
+ ) {
254
+ throw ERRORS.TypedError(
255
+ HardwareErrorCode.RuntimeError,
256
+ `FilesystemFileWrite invalid processed_byte ${processedByte}`
257
+ );
258
+ }
259
+ written =
260
+ rawProcessedByte === undefined ? written + chunk.byteLength : processedByte - startOffset;
261
+ chunks += 1;
262
+ const now = Date.now();
263
+ lastConfirmedAt = now;
264
+ const elapsedMs = now - startTime;
265
+ const transferredBytes = Math.min(written, dataLength);
266
+ const rateWindowElapsedMs = now - rateWindowStartedAt;
267
+ if (rateWindowElapsedMs >= FILE_TRANSFER_RATE_WINDOW_MS) {
268
+ const rateWindowBytes = Math.max(transferredBytes - rateWindowStartedBytes, 0);
269
+ rateBytesPerSecond = Math.round((rateWindowBytes / rateWindowElapsedMs) * 1000);
270
+ rateWindowStartedAt = now;
271
+ rateWindowStartedBytes = transferredBytes;
272
+ } else if (rateBytesPerSecond === undefined && elapsedMs > 0) {
273
+ rateBytesPerSecond = Math.round((transferredBytes / elapsedMs) * 1000);
274
+ }
275
+ options.onProgress?.({
276
+ progress: getConfirmedProgress(startOffset + written, totalSize, written, dataLength),
277
+ transferredBytes,
278
+ totalBytes: dataLength,
279
+ rateBytesPerSecond,
280
+ elapsedMs,
281
+ });
282
+ const logWindowElapsedMs = now - logWindowStartedAt;
283
+ if (logWindowElapsedMs >= FILE_TRANSFER_LOG_INTERVAL_MS && transferredBytes < dataLength) {
284
+ const logWindowBytes = Math.max(transferredBytes - logWindowStartedBytes, 0);
285
+ logFileTransferMetrics({
286
+ transport,
287
+ status: 'progress',
288
+ transferredBytes,
289
+ totalBytes: dataLength,
290
+ elapsedMs,
291
+ rateBytesPerSecond: getAverageFileTransferRate(logWindowBytes, logWindowElapsedMs),
292
+ });
293
+ logWindowStartedAt = now;
294
+ logWindowStartedBytes = transferredBytes;
295
+ }
296
+ if (options.paceMs && options.paceMs > 0) {
297
+ await new Promise(resolve => {
298
+ setTimeout(resolve, options.paceMs);
299
+ });
159
300
  }
160
301
  }
161
- if (!response) {
162
- throw ERRORS.TypedError(
163
- HardwareErrorCode.RuntimeError,
164
- 'FilesystemFileWrite completed without a response'
165
- );
166
- }
167
- options.throwIfAborted?.();
168
- lastMessage = response.message;
169
- const rawProcessedByte = response.message?.processed_byte;
170
- const processedByte = Number(rawProcessedByte);
171
- if (
172
- rawProcessedByte !== undefined &&
173
- (!Number.isFinite(processedByte) ||
174
- processedByte <= offset ||
175
- processedByte > offset + chunk.byteLength)
176
- ) {
177
- throw ERRORS.TypedError(
178
- HardwareErrorCode.RuntimeError,
179
- `FilesystemFileWrite invalid processed_byte ${processedByte}`
180
- );
181
- }
182
- written =
183
- rawProcessedByte === undefined ? written + chunk.byteLength : processedByte - startOffset;
184
- chunks += 1;
302
+ } catch (error) {
185
303
  const now = Date.now();
186
- const elapsedMs = now - startTime;
187
- const transferredBytes = Math.min(written, dataLength);
188
- const rateWindowElapsedMs = now - rateWindowStartedAt;
189
- if (rateWindowElapsedMs >= FILE_TRANSFER_RATE_WINDOW_MS) {
190
- const rateWindowBytes = Math.max(transferredBytes - rateWindowStartedBytes, 0);
191
- rateBytesPerSecond = Math.round((rateWindowBytes / rateWindowElapsedMs) * 1000);
192
- rateWindowStartedAt = now;
193
- rateWindowStartedBytes = transferredBytes;
194
- } else if (rateBytesPerSecond === undefined && elapsedMs > 0) {
195
- rateBytesPerSecond = Math.round((transferredBytes / elapsedMs) * 1000);
196
- }
197
- options.onProgress?.({
198
- progress: getConfirmedProgress(startOffset + written, totalSize, written, dataLength),
199
- transferredBytes,
304
+ const elapsedMs = Math.max(now - startTime, 0);
305
+ const logWindowElapsedMs = Math.max(now - logWindowStartedAt, 0);
306
+ const logWindowBytes = Math.max(written - logWindowStartedBytes, 0);
307
+ logFileTransferMetrics({
308
+ transport,
309
+ status: 'failed',
310
+ transferredBytes: written,
200
311
  totalBytes: dataLength,
201
- rateBytesPerSecond,
202
312
  elapsedMs,
313
+ rateBytesPerSecond: getAverageFileTransferRate(logWindowBytes, logWindowElapsedMs),
203
314
  });
204
- if (options.paceMs && options.paceMs > 0) {
205
- await new Promise(resolve => {
206
- setTimeout(resolve, options.paceMs);
207
- });
208
- }
315
+ throw error;
209
316
  }
210
317
 
318
+ const elapsedMs = Math.max(lastConfirmedAt - startTime, 0);
319
+ const logWindowElapsedMs = Math.max(lastConfirmedAt - logWindowStartedAt, 0);
320
+ const logWindowBytes = Math.max(written - logWindowStartedBytes, 0);
321
+ logFileTransferMetrics({
322
+ transport,
323
+ status: 'completed',
324
+ transferredBytes: written,
325
+ totalBytes: dataLength,
326
+ elapsedMs,
327
+ rateBytesPerSecond: getAverageFileTransferRate(logWindowBytes, logWindowElapsedMs),
328
+ });
329
+
211
330
  return {
212
331
  ...lastMessage,
213
332
  path: options.path,
package/src/core/index.ts CHANGED
@@ -652,6 +652,12 @@ const onCallDevice = async (
652
652
  requestQueue.resolveRequest(method.responseID, messageResponse);
653
653
  completeMethodRequestContext(method);
654
654
  } catch (error) {
655
+ // Device.run may release the request before transport callbacks finish after a timeout.
656
+ // Ignore the stale callback because the caller already received the connection error.
657
+ if (!requestQueue.getTask(method.responseID)) {
658
+ Log.debug(`Call API - Ignore late inner method result`, error);
659
+ return;
660
+ }
655
661
  Log.debug(`Call API - Inner Method Run Error`, error);
656
662
  messageResponse = createResponseMessage(method.responseID, false, { error });
657
663
  requestQueue.resolveRequest(method.responseID, messageResponse);
@@ -1647,12 +1653,8 @@ export const init = async (
1647
1653
  plugin?: LowlevelTransportSharedPlugin
1648
1654
  ) => {
1649
1655
  try {
1650
- try {
1651
- await DataManager.load(settings);
1652
- initTransport(Transport, plugin);
1653
- } catch {
1654
- Log.error('DataManager.load error');
1655
- }
1656
+ await DataManager.load(settings);
1657
+ initTransport(Transport, plugin);
1656
1658
  enableLog(DataManager.getSettings('debug'));
1657
1659
  if (DataManager.getSettings('env') !== 'react-native') {
1658
1660
  setLoggerPostMessage(postMessage);
@@ -1663,6 +1665,7 @@ export const init = async (
1663
1665
  return _core;
1664
1666
  } catch (error) {
1665
1667
  Log.error('core init', error);
1668
+ throw error;
1666
1669
  }
1667
1670
  };
1668
1671