@onekeyfe/hd-transport-web-device 1.2.3-alpha.3 → 1.2.3-alpha.5

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/README.md CHANGED
@@ -26,4 +26,4 @@ yar update:protobuf to generate new ./messages.json and ./src/types/messages.ts
26
26
 
27
27
  ## Docs
28
28
 
29
- Documentation is available [hardware-js-sdk](https://developer.onekey.so/connect-to-hardware/hardware-sdk/start)
29
+ Documentation is available [Hardware SDK Getting Started](https://developer.onekey.so/en/hardware-sdk/getting-started)
@@ -205,12 +205,52 @@ describe('ElectronBleTransport protocol detection', () => {
205
205
  expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
206
206
  });
207
207
 
208
- test('sends a native disconnect even before acquire marks the device connected', async () => {
208
+ test('cancels a pending native connect acquire has not registered yet', async () => {
209
209
  const device = { id: 'pending-connect-id', name: 'OneKey Pro 2' };
210
210
  const nobleBle = createNobleBle(device);
211
+ // Never calls back: the field case the Core acquire deadline has to break.
212
+ nobleBle.connect.mockImplementation(
213
+ () =>
214
+ new Promise<void>(() => {
215
+ // intentionally never settles
216
+ })
217
+ );
218
+ const bleTransport = configureTransport(nobleBle) as any;
219
+
220
+ const pendingAcquire = bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
221
+ pendingAcquire.catch(() => undefined);
222
+ await new Promise(resolve => {
223
+ setTimeout(resolve, 10);
224
+ });
225
+
226
+ // What the deadline / user abort calls. connectedDevices is still empty
227
+ // here, so this only works because the acquire is tracked as in flight.
228
+ await bleTransport.disconnect(device.id);
229
+
230
+ expect(nobleBle.unsubscribe).not.toHaveBeenCalled();
231
+ expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
232
+ });
233
+
234
+ test('leaves a kept-alive link up when a released device is disconnected', async () => {
235
+ const device = { id: 'kept-alive-id', name: 'OneKey Pro 2' };
236
+ const nobleBle = createNobleBle(device);
237
+ const bleTransport = configureTransport(nobleBle) as any;
238
+
239
+ // No acquire in flight and nothing in connectedDevices: the link was
240
+ // released logically and belongs to the main-process keep-alive timer.
241
+ // The routine post-call cancel must not cost the next call a cold connect.
242
+ await bleTransport.disconnect(device.id);
243
+
244
+ expect(nobleBle.disconnect).not.toHaveBeenCalled();
245
+ expect(nobleBle.unsubscribe).not.toHaveBeenCalled();
246
+ });
247
+
248
+ test('forceNative disconnects a device the renderer no longer tracks', async () => {
249
+ const device = { id: 'presumed-dead-id', name: 'OneKey Pro 2' };
250
+ const nobleBle = createNobleBle(device);
211
251
  const bleTransport = configureTransport(nobleBle) as any;
212
252
 
213
- await bleTransport.releaseNative(device.id);
253
+ await bleTransport.releaseNative(device.id, { forceNative: true });
214
254
 
215
255
  expect(nobleBle.unsubscribe).not.toHaveBeenCalled();
216
256
  expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
@@ -498,7 +538,7 @@ describe('ElectronBleTransport protocol detection', () => {
498
538
  await expect(result).resolves.toBe('rejected');
499
539
  });
500
540
 
501
- test('throws when both protocol probes fail', async () => {
541
+ test('disconnects when both protocol probes fail and reconnects for the next acquire', async () => {
502
542
  const device = { id: 'dead-device-id', name: 'Unknown Device' };
503
543
  const nobleBle = createNobleBle(device);
504
544
 
@@ -510,7 +550,25 @@ describe('ElectronBleTransport protocol detection', () => {
510
550
  await expect(transport.acquire({ uuid: device.id })).rejects.toThrow(
511
551
  /Unable to detect BLE protocol/
512
552
  );
553
+ expect(nobleBle.unsubscribe).toHaveBeenCalledWith(device.id);
554
+ expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
513
555
  expect(transport.getProtocolType(device.id)).toBeUndefined();
556
+
557
+ echoProtocolV2(nobleBle, device.id);
558
+ await expect(
559
+ transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
560
+ ).resolves.toMatchObject({ uuid: device.id });
561
+ expect(nobleBle.connect).toHaveBeenCalledTimes(2);
562
+ expect(nobleBle.disconnect.mock.invocationCallOrder.at(-1)).toBeLessThan(
563
+ nobleBle.connect.mock.invocationCallOrder[1]
564
+ );
565
+ await expect(
566
+ transport.call(device.id, 'Ping', { message: 'after-reconnect' })
567
+ ).resolves.toMatchObject({
568
+ type: 'Success',
569
+ message: { message: 'ok' },
570
+ });
571
+ await transport.release(device.id);
514
572
  });
515
573
 
516
574
  test('surfaces Protocol V2 link disabled while the initial V1 probe is active', async () => {
@@ -673,7 +731,7 @@ describe('ElectronBleTransport protocol detection', () => {
673
731
  }
674
732
  });
675
733
 
676
- test('reports a stale bond when a previously confirmed Protocol V2 device stops responding', async () => {
734
+ test('keeps a probe miss retryable after a previously confirmed Protocol V2 connection', async () => {
677
735
  const device = { id: 'reset-pro2-id', name: 'OneKey Pro 2' };
678
736
  const nobleBle = createNobleBle(device);
679
737
  const transport = configureTransport(nobleBle);
@@ -686,7 +744,7 @@ describe('ElectronBleTransport protocol detection', () => {
686
744
  await expect(
687
745
  transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
688
746
  ).rejects.toMatchObject({
689
- errorCode: HardwareErrorCode.BleDeviceBondError,
747
+ errorCode: HardwareErrorCode.RuntimeError,
690
748
  });
691
749
 
692
750
  expect(nobleBle.unsubscribe).toHaveBeenCalledWith(device.id);
@@ -694,6 +752,52 @@ describe('ElectronBleTransport protocol detection', () => {
694
752
  expect(transport.getProtocolType(device.id)).toBeUndefined();
695
753
  });
696
754
 
755
+ test.each([false, true])(
756
+ 'preserves an expected Protocol V2 Ping timeout with a prior successful connection: %s',
757
+ async previouslyConnected => {
758
+ const device = { id: 'timeout-pro2-id', name: 'OneKey Pro 2' };
759
+ const nobleBle = createNobleBle(device);
760
+ const transport = configureTransport(nobleBle);
761
+
762
+ if (previouslyConnected) {
763
+ echoProtocolV2(nobleBle, device.id);
764
+ await transport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
765
+ await transport.release(device.id);
766
+ }
767
+ // Keep the real probe and response timer, but drop its notification.
768
+ nobleBle.write.mockImplementation(() => Promise.resolve());
769
+
770
+ await expect(
771
+ transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
772
+ ).rejects.toMatchObject({
773
+ errorCode: HardwareErrorCode.BleTimeoutError,
774
+ message: 'BLE response timeout after 5000ms for Ping',
775
+ });
776
+
777
+ expect(nobleBle.unsubscribe).toHaveBeenCalledWith(device.id);
778
+ expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
779
+ expect(transport.getProtocolType(device.id)).toBeUndefined();
780
+ }
781
+ );
782
+
783
+ test('preserves a native stale-bond error during an expected Protocol V2 probe', async () => {
784
+ const device = { id: 'stale-bond-probe-id', name: 'OneKey Pro 2' };
785
+ const nobleBle = createNobleBle(device);
786
+ nobleBle.write.mockRejectedValue({
787
+ name: 'HardwareError',
788
+ message: 'Bluetooth pairing information is no longer valid',
789
+ errorCode: HardwareErrorCode.BleBondInvalid,
790
+ });
791
+ const transport = configureTransport(nobleBle);
792
+
793
+ await expect(
794
+ transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
795
+ ).rejects.toMatchObject({ errorCode: HardwareErrorCode.BleBondInvalid });
796
+
797
+ expect(nobleBle.unsubscribe).toHaveBeenCalledWith(device.id);
798
+ expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
799
+ });
800
+
697
801
  test('does not take a Protocol V2 hint from the BLE name', async () => {
698
802
  const device = { id: 'named-pro2-id', name: 'OneKey Pro 2' };
699
803
  const nobleBle = createNobleBle(device);
@@ -31,6 +31,87 @@ function buildAcquirableTransport(path = 'pro-webusb') {
31
31
  }
32
32
 
33
33
  describe('WebUsbTransport protocol probe cache', () => {
34
+ test.each([
35
+ ['V1', false, 0],
36
+ ['V1', true, 1],
37
+ ['V2', false, 1],
38
+ [undefined, false, 1],
39
+ ] as const)(
40
+ 'reconnect protocol=%s first=%s resets USB %s times',
41
+ async (protocol, first, resets) => {
42
+ const webusb = new WebUsbTransport();
43
+ const path = 'connected-usb-device';
44
+ const device = {
45
+ opened: true,
46
+ configuration: { configurationValue: 1 },
47
+ configurations: [],
48
+ reset: jest.fn().mockResolvedValue(undefined),
49
+ selectConfiguration: jest.fn().mockResolvedValue(undefined),
50
+ claimInterface: jest.fn().mockResolvedValue(undefined),
51
+ clearHalt: jest.fn().mockResolvedValue(undefined),
52
+ };
53
+ jest.spyOn(webusb, 'findDevice').mockResolvedValue(device as unknown as USBDevice);
54
+ jest.spyOn(webusb, 'getConnectedDevices').mockResolvedValue([]);
55
+ if (protocol) {
56
+ const state = webusb as unknown as { deviceProtocol: Map<string, 'V1' | 'V2'> };
57
+ state.deviceProtocol.set(path, protocol);
58
+ }
59
+
60
+ await webusb.connectToDevice(path, first);
61
+
62
+ expect(device.reset).toHaveBeenCalledTimes(resets);
63
+ expect(device.claimInterface).toHaveBeenCalledWith(0);
64
+ }
65
+ );
66
+
67
+ test('reports claimInterface failures as WebUSB device access errors', async () => {
68
+ const webusb = new WebUsbTransport();
69
+ const path = 'connected-usb-device';
70
+ const claimError = Object.assign(new Error('Unable to claim interface'), {
71
+ name: 'NetworkError',
72
+ });
73
+ const device = {
74
+ opened: true,
75
+ configuration: { configurationValue: 1 },
76
+ configurations: [],
77
+ claimInterface: jest.fn().mockRejectedValue(claimError),
78
+ };
79
+ jest.spyOn(webusb, 'findDevice').mockResolvedValue(device as unknown as USBDevice);
80
+ jest.spyOn(webusb, 'getConnectedDevices').mockResolvedValue([]);
81
+ const state = webusb as unknown as { deviceProtocol: Map<string, 'V1' | 'V2'> };
82
+ state.deviceProtocol.set(path, 'V1');
83
+
84
+ await expect(webusb.connectToDevice(path, false)).rejects.toMatchObject({
85
+ errorCode: HardwareErrorCode.WebUsbDeviceAccessError,
86
+ params: {
87
+ operation: 'claimInterface',
88
+ nativeErrorName: 'NetworkError',
89
+ nativeErrorMessage: 'Unable to claim interface',
90
+ },
91
+ });
92
+ });
93
+
94
+ test('reports transferIn failures as WebUSB device access errors', async () => {
95
+ const webusb = new WebUsbTransport() as any;
96
+ const path = 'connected-usb-device';
97
+ const transferError = Object.assign(new Error('Transfer endpoint is unavailable'), {
98
+ name: 'NetworkError',
99
+ });
100
+ webusb.findDevice = jest.fn().mockResolvedValue({
101
+ opened: true,
102
+ transferIn: jest.fn().mockRejectedValue(transferError),
103
+ });
104
+
105
+ await expect(webusb.transferInWithRetry(path, 64)).rejects.toMatchObject({
106
+ errorCode: HardwareErrorCode.WebUsbDeviceAccessError,
107
+ params: {
108
+ operation: 'transferIn',
109
+ nativeErrorName: 'NetworkError',
110
+ nativeErrorMessage: 'Transfer endpoint is unavailable',
111
+ },
112
+ });
113
+ });
114
+
34
115
  test('acquire skips the wire probe when the protocol is already cached', async () => {
35
116
  const webusb = buildAcquirableTransport();
36
117
  const path = 'pro-webusb';
@@ -238,9 +238,15 @@ describe('WebUsbTransport Protocol V2 timeout recovery', () => {
238
238
  webusb.resetConnectionAfterProbe = jest.fn();
239
239
  await webusb.rotateProtocolV2UsbGeneration(path, 'test connection');
240
240
 
241
- await expect(webusb.callProtocolV2(path, 'Ping', { message: 'read-error' })).rejects.toThrow(
242
- 'NetworkError'
243
- );
241
+ await expect(
242
+ webusb.callProtocolV2(path, 'Ping', { message: 'read-error' })
243
+ ).rejects.toMatchObject({
244
+ errorCode: HardwareErrorCode.BridgeDeviceDisconnected,
245
+ params: {
246
+ operation: 'transferIn',
247
+ nativeErrorMessage: 'NetworkError: transferIn device disconnected',
248
+ },
249
+ });
244
250
 
245
251
  expect(webusb.readProtocolV2UsbPacket).toHaveBeenCalledTimes(1);
246
252
  expect(webusb.resetProtocolV2UsbNativeLink).toHaveBeenCalledWith(
@@ -25,9 +25,9 @@ export default class ElectronBleTransport {
25
25
  Log?: any;
26
26
  emitter?: EventEmitter;
27
27
  private connectedDevices;
28
+ private acquiringDevices;
28
29
  private deviceProtocol;
29
30
  private deviceProtocolHints;
30
- private confirmedProtocolV2;
31
31
  private deviceMtus;
32
32
  private devicePacketCapacities;
33
33
  private v1Buffers;
@@ -1 +1 @@
1
- {"version":3,"file":"electron-ble-transport.d.ts","sourceRoot":"","sources":["../src/electron-ble-transport.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"electron-ble-transport.d.ts","sourceRoot":"","sources":["../src/electron-ble-transport.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -118,7 +118,7 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
118
118
  /**
119
119
  * Connect to device with retry mechanism
120
120
  */
121
- connect(path: string, first: boolean): Promise<void>;
121
+ connect(path: string, first: boolean): Promise<undefined>;
122
122
  /**
123
123
  * Discover vendor-class (0xFF) interface and its IN/OUT endpoint numbers from USB descriptors.
124
124
  * Falls back to legacy hardcoded values if no vendor interface is found.
@@ -128,7 +128,7 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
128
128
  * Connect to specific device.
129
129
  * Discovers interface/endpoint numbers from USB descriptors on first connection.
130
130
  */
131
- connectToDevice(path: string, first: boolean): Promise<void>;
131
+ connectToDevice(path: string, first: boolean): Promise<undefined>;
132
132
  private closeOpenDevice;
133
133
  private clearEndpointHalt;
134
134
  /**
@@ -141,6 +141,10 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
141
141
  private assertAcquired;
142
142
  post(session: string, name: string, data: Record<string, unknown>): Promise<void>;
143
143
  private getErrorMessage;
144
+ private getErrorName;
145
+ private isWebUsbIoHardwareError;
146
+ private isDeviceStillEnumerated;
147
+ private throwWebUsbIoError;
144
148
  private isRetryablePacketIoError;
145
149
  private reconnectForPacketIoRetry;
146
150
  private getTransferInData;
@@ -216,10 +220,16 @@ declare class ElectronBleTransport {
216
220
  Log?: any;
217
221
  emitter?: EventEmitter;
218
222
  private connectedDevices;
223
+ /**
224
+ * Devices whose acquire() is still in flight.
225
+ *
226
+ * A native connect that never calls back has not reached `connectedDevices`
227
+ * yet, so this is the only record that the renderer is still trying to own
228
+ * the link; `releaseNative()` needs it to cancel that pending connect.
229
+ */
230
+ private acquiringDevices;
219
231
  private deviceProtocol;
220
232
  private deviceProtocolHints;
221
- /** Endpoints that answered a V2 probe in this transport lifetime. Survives disconnect. */
222
- private confirmedProtocolV2;
223
233
  private deviceMtus;
224
234
  private devicePacketCapacities;
225
235
  private v1Buffers;
package/dist/index.js CHANGED
@@ -375,11 +375,13 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
375
375
  if (!device.opened) {
376
376
  yield device.open();
377
377
  }
378
- try {
379
- yield device.reset();
380
- }
381
- catch (error) {
382
- (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
378
+ if (first || this.deviceProtocol.get(path) !== 'V1') {
379
+ try {
380
+ yield device.reset();
381
+ }
382
+ catch (error) {
383
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
384
+ }
383
385
  }
384
386
  yield this.getConnectedDevices();
385
387
  device = yield this.findDevice(path);
@@ -393,7 +395,12 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
393
395
  }
394
396
  const endpoints = this.discoverEndpoints(device);
395
397
  this.deviceEndpoints.set(path, endpoints);
396
- yield device.claimInterface(endpoints.interfaceNumber);
398
+ try {
399
+ yield device.claimInterface(endpoints.interfaceNumber);
400
+ }
401
+ catch (error) {
402
+ return this.throwWebUsbIoError(path, 'claimInterface', error);
403
+ }
397
404
  yield this.clearEndpointHalt(device, 'in', endpoints.endpointIn);
398
405
  yield this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
399
406
  });
@@ -457,6 +464,59 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
457
464
  }
458
465
  return String(error);
459
466
  }
467
+ getErrorName(error) {
468
+ if (typeof error === 'object' && error && 'name' in error) {
469
+ const { name } = error;
470
+ return typeof name === 'string' ? name : String(name !== null && name !== void 0 ? name : '');
471
+ }
472
+ return '';
473
+ }
474
+ isWebUsbIoHardwareError(error) {
475
+ return (error instanceof hdShared.HardwareError &&
476
+ (error.errorCode === hdShared.HardwareErrorCode.BridgeDeviceDisconnected ||
477
+ error.errorCode === hdShared.HardwareErrorCode.WebUsbDeviceAccessError));
478
+ }
479
+ isDeviceStillEnumerated(path) {
480
+ var _a;
481
+ return __awaiter(this, void 0, void 0, function* () {
482
+ if (!this.usb)
483
+ return undefined;
484
+ try {
485
+ const devices = yield this.usb.getDevices();
486
+ return devices.some(device => hdShared.resolveOneKeyUsbDevicePath(device) === path);
487
+ }
488
+ catch (error) {
489
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[WebUsbTransport] failed to verify WebUSB device presence:', error);
490
+ return undefined;
491
+ }
492
+ });
493
+ }
494
+ throwWebUsbIoError(path, operation, error) {
495
+ var _a;
496
+ return __awaiter(this, void 0, void 0, function* () {
497
+ if (this.isWebUsbIoHardwareError(error))
498
+ throw error;
499
+ if (error instanceof transport.ProtocolV2LinkError && error.code !== 'io')
500
+ throw error;
501
+ const nativeError = error instanceof transport.ProtocolV2LinkError ? (_a = error.cause) !== null && _a !== void 0 ? _a : error : error;
502
+ if (this.isWebUsbIoHardwareError(nativeError))
503
+ throw nativeError;
504
+ const nativeErrorName = this.getErrorName(nativeError);
505
+ const nativeErrorMessage = this.getErrorMessage(nativeError);
506
+ const disconnectedByMessage = /(?:device )?disconnected|device not found|action was interrupted/i.test(`${nativeErrorName}: ${nativeErrorMessage}`);
507
+ const stillEnumerated = disconnectedByMessage
508
+ ? false
509
+ : yield this.isDeviceStillEnumerated(path);
510
+ const errorCode = disconnectedByMessage || stillEnumerated === false
511
+ ? hdShared.HardwareErrorCode.BridgeDeviceDisconnected
512
+ : hdShared.HardwareErrorCode.WebUsbDeviceAccessError;
513
+ throw hdShared.ERRORS.TypedError(errorCode, nativeErrorMessage || undefined, {
514
+ operation,
515
+ nativeErrorName: nativeErrorName || undefined,
516
+ nativeErrorMessage: nativeErrorMessage || undefined,
517
+ });
518
+ });
519
+ }
460
520
  isRetryablePacketIoError(error) {
461
521
  const message = this.getErrorMessage(error).toLowerCase();
462
522
  return (message.includes('transferout') ||
@@ -523,7 +583,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
523
583
  lastError = error;
524
584
  const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryablePacketIoError(error);
525
585
  if (!shouldRetry) {
526
- throw error;
586
+ return this.throwWebUsbIoError(path, 'transferOut', error);
527
587
  }
528
588
  try {
529
589
  yield this.reconnectForPacketIoRetry(path, 'out', attempt, error);
@@ -534,7 +594,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
534
594
  }
535
595
  }
536
596
  }
537
- throw lastError;
597
+ return this.throwWebUsbIoError(path, 'transferOut', lastError);
538
598
  });
539
599
  }
540
600
  transferOutOnce(path, packet) {
@@ -575,7 +635,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
575
635
  }
576
636
  const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryablePacketIoError(error);
577
637
  if (!shouldRetry) {
578
- throw error;
638
+ return this.throwWebUsbIoError(path, 'transferIn', error);
579
639
  }
580
640
  try {
581
641
  yield this.reconnectForPacketIoRetry(path, 'in', attempt, error);
@@ -586,7 +646,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
586
646
  }
587
647
  }
588
648
  }
589
- throw lastError;
649
+ return this.throwWebUsbIoError(path, 'transferIn', lastError);
590
650
  });
591
651
  }
592
652
  transferInOnce(path, length) {
@@ -663,7 +723,9 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
663
723
  });
664
724
  return true;
665
725
  }
666
- catch (_error) {
726
+ catch (error) {
727
+ if (this.isWebUsbIoHardwareError(error))
728
+ throw error;
667
729
  return false;
668
730
  }
669
731
  });
@@ -678,6 +740,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
678
740
  timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT,
679
741
  logger: this.Log,
680
742
  logPrefix: 'ProtocolV2 WebUSB',
743
+ shouldRethrow: error => this.isWebUsbIoHardwareError(error),
681
744
  });
682
745
  });
683
746
  }
@@ -724,7 +787,15 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
724
787
  }
725
788
  callProtocolV2(path, name, data, options) {
726
789
  return __awaiter(this, void 0, void 0, function* () {
727
- return this.callProtocolV2Usb(path, name, data, options);
790
+ try {
791
+ return yield this.callProtocolV2Usb(path, name, data, options);
792
+ }
793
+ catch (error) {
794
+ if (!(error instanceof transport.ProtocolV2LinkError) || error.code !== 'io')
795
+ throw error;
796
+ const operation = error.message.includes(' USB write failed:') ? 'transferOut' : 'transferIn';
797
+ return this.throwWebUsbIoError(path, operation, error);
798
+ }
728
799
  });
729
800
  }
730
801
  receiveData(path, timeoutMs) {
@@ -848,9 +919,9 @@ class ElectronBleTransport {
848
919
  this.runPromise = null;
849
920
  this.runPromiseDeviceId = null;
850
921
  this.connectedDevices = new Set();
922
+ this.acquiringDevices = new Set();
851
923
  this.deviceProtocol = new Map();
852
924
  this.deviceProtocolHints = new Map();
853
- this.confirmedProtocolV2 = new Set();
854
925
  this.deviceMtus = new Map();
855
926
  this.devicePacketCapacities = new Map();
856
927
  this.v1Buffers = new Map();
@@ -874,7 +945,7 @@ class ElectronBleTransport {
874
945
  this.rejectProtocolV2Frames(uuid, new Error(reason));
875
946
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('[Electron BLE] Protocol V2 link invalidated:', uuid, reason);
876
947
  if (reason.startsWith('Protocol V2 link-fatal error:')) {
877
- yield this.releaseNative(uuid);
948
+ yield this.releaseNative(uuid, { forceNative: true });
878
949
  }
879
950
  }),
880
951
  });
@@ -1033,6 +1104,7 @@ class ElectronBleTransport {
1033
1104
  this.runPromise = null;
1034
1105
  this.runPromiseDeviceId = null;
1035
1106
  }
1107
+ this.acquiringDevices.add(uuid);
1036
1108
  try {
1037
1109
  if (!((_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle)) {
1038
1110
  throw new Error('Noble BLE API not available');
@@ -1099,6 +1171,9 @@ class ElectronBleTransport {
1099
1171
  this.cleanupDeviceState(uuid);
1100
1172
  this.handleBluetoothError(error);
1101
1173
  }
1174
+ finally {
1175
+ this.acquiringDevices.delete(uuid);
1176
+ }
1102
1177
  });
1103
1178
  }
1104
1179
  release(id, _onclose, keepSession) {
@@ -1119,12 +1194,14 @@ class ElectronBleTransport {
1119
1194
  return this.releaseNative(id);
1120
1195
  });
1121
1196
  }
1122
- releaseNative(id) {
1197
+ releaseNative(id, options) {
1123
1198
  var _a, _b;
1124
1199
  return __awaiter(this, void 0, void 0, function* () {
1200
+ const rendererOwnsLink = this.connectedDevices.has(id) || this.acquiringDevices.has(id);
1201
+ const shouldDisconnect = (options === null || options === void 0 ? void 0 : options.forceNative) === true || rendererOwnsLink;
1125
1202
  try {
1126
1203
  const nobleBle = (_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle;
1127
- if (nobleBle) {
1204
+ if (nobleBle && shouldDisconnect) {
1128
1205
  if (this.connectedDevices.has(id)) {
1129
1206
  yield invokeNobleBle(nobleBle.unsubscribe(id)).catch(error => {
1130
1207
  var _a;
@@ -1165,9 +1242,8 @@ class ElectronBleTransport {
1165
1242
  }
1166
1243
  });
1167
1244
  }
1168
- createProtocolMismatchError(expected, uuid) {
1169
- const isStaleV2Bond = expected === 'V2' && this.confirmedProtocolV2.has(uuid);
1170
- return hdShared.ERRORS.TypedError(isStaleV2Bond ? hdShared.HardwareErrorCode.BleDeviceBondError : hdShared.HardwareErrorCode.RuntimeError, `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`);
1245
+ createProtocolMismatchError(expected) {
1246
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`);
1171
1247
  }
1172
1248
  createProtocolDetectionError() {
1173
1249
  return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleTimeoutError, 'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping');
@@ -1186,13 +1262,12 @@ class ElectronBleTransport {
1186
1262
  return 'V1';
1187
1263
  }
1188
1264
  if (expectedProtocol === 'V2') {
1189
- if (yield this.probeProtocolV2(uuid)) {
1265
+ if (yield this.probeProtocolV2(uuid, expectedProtocol)) {
1190
1266
  this.deviceProtocol.set(uuid, 'V2');
1191
- this.confirmedProtocolV2.add(uuid);
1192
1267
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
1193
1268
  return 'V2';
1194
1269
  }
1195
- throw this.createProtocolMismatchError(expectedProtocol, uuid);
1270
+ throw this.createProtocolMismatchError(expectedProtocol);
1196
1271
  }
1197
1272
  const probeOrder = protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
1198
1273
  for (let i = 0; i < probeOrder.length; i += 1) {
@@ -1203,9 +1278,6 @@ class ElectronBleTransport {
1203
1278
  const detected = protocol === 'V1' ? yield this.probeProtocolV1(uuid) : yield this.probeProtocolV2(uuid);
1204
1279
  if (detected) {
1205
1280
  this.deviceProtocol.set(uuid, protocol);
1206
- if (protocol === 'V2') {
1207
- this.confirmedProtocolV2.add(uuid);
1208
- }
1209
1281
  (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> ${protocol}`);
1210
1282
  return protocol;
1211
1283
  }
@@ -1268,7 +1340,7 @@ class ElectronBleTransport {
1268
1340
  }
1269
1341
  });
1270
1342
  }
1271
- probeProtocolV2(uuid) {
1343
+ probeProtocolV2(uuid, expectedProtocol) {
1272
1344
  var _a;
1273
1345
  return __awaiter(this, void 0, void 0, function* () {
1274
1346
  if (!this._messages || !this._messagesV2) {
@@ -1286,7 +1358,9 @@ class ElectronBleTransport {
1286
1358
  (_a = this.v2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
1287
1359
  this.resetProtocolV2Frames(uuid);
1288
1360
  },
1289
- shouldRethrow: error => hdShared.isBleStaleBondHardwareError(error) || transport.isProtocolV2LinkDisabledError(error),
1361
+ shouldRethrow: error => expectedProtocol === 'V2' ||
1362
+ hdShared.isBleStaleBondHardwareError(error) ||
1363
+ transport.isProtocolV2LinkDisabledError(error),
1290
1364
  });
1291
1365
  if (!detected) {
1292
1366
  this.clearProbeProtocol(uuid, 'V2');
@@ -1566,7 +1640,7 @@ class ElectronBleTransport {
1566
1640
  this.notificationCleanups.delete(uuid);
1567
1641
  this.notificationTokens.delete(uuid);
1568
1642
  if (!isProbeTimeout) {
1569
- yield this.releaseNative(uuid);
1643
+ yield this.releaseNative(uuid, { forceNative: true });
1570
1644
  }
1571
1645
  }
1572
1646
  throw this.normalizeBluetoothError(e);
package/dist/webusb.d.ts CHANGED
@@ -44,14 +44,18 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
44
44
  private createProtocolDetectionError;
45
45
  private detectProtocol;
46
46
  findDevice(path: string): Promise<USBDevice>;
47
- connect(path: string, first: boolean): Promise<void>;
47
+ connect(path: string, first: boolean): Promise<undefined>;
48
48
  private discoverEndpoints;
49
- connectToDevice(path: string, first: boolean): Promise<void>;
49
+ connectToDevice(path: string, first: boolean): Promise<undefined>;
50
50
  private closeOpenDevice;
51
51
  private clearEndpointHalt;
52
52
  private assertAcquired;
53
53
  post(session: string, name: string, data: Record<string, unknown>): Promise<void>;
54
54
  private getErrorMessage;
55
+ private getErrorName;
56
+ private isWebUsbIoHardwareError;
57
+ private isDeviceStillEnumerated;
58
+ private throwWebUsbIoError;
55
59
  private isRetryablePacketIoError;
56
60
  private reconnectForPacketIoRetry;
57
61
  private getTransferInData;
@@ -1 +1 @@
1
- {"version":3,"file":"webusb.d.ts","sourceRoot":"","sources":["../src/webusb.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"webusb.d.ts","sourceRoot":"","sources":["../src/webusb.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-web-device",
3
- "version": "1.2.3-alpha.3",
3
+ "version": "1.2.3-alpha.5",
4
4
  "author": "OneKey",
5
5
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
6
6
  "license": "MIT",
@@ -21,13 +21,13 @@
21
21
  "lint:fix": "eslint . --fix"
22
22
  },
23
23
  "dependencies": {
24
- "@onekeyfe/hd-shared": "1.2.3-alpha.3",
25
- "@onekeyfe/hd-transport": "1.2.3-alpha.3"
24
+ "@onekeyfe/hd-shared": "1.2.3-alpha.5",
25
+ "@onekeyfe/hd-transport": "1.2.3-alpha.5"
26
26
  },
27
27
  "devDependencies": {
28
- "@onekeyfe/hd-transport-electron": "1.2.3-alpha.3",
28
+ "@onekeyfe/hd-transport-electron": "1.2.3-alpha.5",
29
29
  "@types/w3c-web-usb": "^1.0.6",
30
30
  "@types/web-bluetooth": "^0.0.17"
31
31
  },
32
- "gitHead": "8b116adb144b967f042f189cd27384a75ca1f7ae"
32
+ "gitHead": "ee493c3ad41a6bc7fb249fba71d4b9bf8efcced8"
33
33
  }
@@ -117,13 +117,19 @@ export default class ElectronBleTransport {
117
117
 
118
118
  private connectedDevices: Set<string> = new Set();
119
119
 
120
+ /**
121
+ * Devices whose acquire() is still in flight.
122
+ *
123
+ * A native connect that never calls back has not reached `connectedDevices`
124
+ * yet, so this is the only record that the renderer is still trying to own
125
+ * the link; `releaseNative()` needs it to cancel that pending connect.
126
+ */
127
+ private acquiringDevices: Set<string> = new Set();
128
+
120
129
  private deviceProtocol: Map<string, ProtocolType> = new Map();
121
130
 
122
131
  private deviceProtocolHints: Map<string, ProtocolType> = new Map();
123
132
 
124
- /** Endpoints that answered a V2 probe in this transport lifetime. Survives disconnect. */
125
- private confirmedProtocolV2 = new Set<string>();
126
-
127
133
  private deviceMtus: Map<string, number> = new Map();
128
134
 
129
135
  private devicePacketCapacities: Map<string, number> = new Map();
@@ -152,7 +158,7 @@ export default class ElectronBleTransport {
152
158
  this.rejectProtocolV2Frames(uuid, new Error(reason));
153
159
  this.Log?.debug('[Electron BLE] Protocol V2 link invalidated:', uuid, reason);
154
160
  if (reason.startsWith('Protocol V2 link-fatal error:')) {
155
- await this.releaseNative(uuid);
161
+ await this.releaseNative(uuid, { forceNative: true });
156
162
  }
157
163
  },
158
164
  });
@@ -366,6 +372,7 @@ export default class ElectronBleTransport {
366
372
  this.runPromiseDeviceId = null;
367
373
  }
368
374
 
375
+ this.acquiringDevices.add(uuid);
369
376
  try {
370
377
  if (!window.desktopApi?.nobleBle) {
371
378
  throw new Error('Noble BLE API not available');
@@ -437,6 +444,8 @@ export default class ElectronBleTransport {
437
444
  }
438
445
  this.cleanupDeviceState(uuid);
439
446
  this.handleBluetoothError(error);
447
+ } finally {
448
+ this.acquiringDevices.delete(uuid);
440
449
  }
441
450
  }
442
451
 
@@ -457,10 +466,23 @@ export default class ElectronBleTransport {
457
466
  }
458
467
 
459
468
  // Hard teardown, error paths only: a link presumed dead must not be reused.
460
- private async releaseNative(id: string) {
469
+ //
470
+ // The native disconnect is sent while the renderer still owns the link, or is
471
+ // still trying to get it: a stuck acquire has not reached `connectedDevices`
472
+ // yet, and its pending native connect can only be cancelled from here, so the
473
+ // Core acquire deadline would otherwise be unable to terminate it.
474
+ //
475
+ // It is NOT sent for a device the renderer has already released logically.
476
+ // That link belongs to the main-process keep-alive timer, and dropping it on
477
+ // the routine post-call `cancel()` (Device.interruptionFromUser, no acquire
478
+ // held, empty request queue) costs a full cold reconnect on the very next
479
+ // operation — measured 2.93s on Pro and 17-26s on Pro 2.
480
+ private async releaseNative(id: string, options?: { forceNative?: boolean }) {
481
+ const rendererOwnsLink = this.connectedDevices.has(id) || this.acquiringDevices.has(id);
482
+ const shouldDisconnect = options?.forceNative === true || rendererOwnsLink;
461
483
  try {
462
484
  const nobleBle = window.desktopApi?.nobleBle;
463
- if (nobleBle) {
485
+ if (nobleBle && shouldDisconnect) {
464
486
  if (this.connectedDevices.has(id)) {
465
487
  await invokeNobleBle(nobleBle.unsubscribe(id)).catch(error => {
466
488
  this.Log?.debug('[Electron BLE] release unsubscribe failed:', error);
@@ -501,12 +523,9 @@ export default class ElectronBleTransport {
501
523
  }
502
524
  }
503
525
 
504
- private createProtocolMismatchError(expected: ProtocolType, uuid: string) {
505
- // A generic Ping miss is not a bond failure. Only a later miss after this
506
- // endpoint already answered V2, or a native encryption/pairing error, is.
507
- const isStaleV2Bond = expected === 'V2' && this.confirmedProtocolV2.has(uuid);
526
+ private createProtocolMismatchError(expected: ProtocolType) {
508
527
  return ERRORS.TypedError(
509
- isStaleV2Bond ? HardwareErrorCode.BleDeviceBondError : HardwareErrorCode.RuntimeError,
528
+ HardwareErrorCode.RuntimeError,
510
529
  `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
511
530
  );
512
531
  }
@@ -546,13 +565,12 @@ export default class ElectronBleTransport {
546
565
  }
547
566
 
548
567
  if (expectedProtocol === 'V2') {
549
- if (await this.probeProtocolV2(uuid)) {
568
+ if (await this.probeProtocolV2(uuid, expectedProtocol)) {
550
569
  this.deviceProtocol.set(uuid, 'V2');
551
- this.confirmedProtocolV2.add(uuid);
552
570
  this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
553
571
  return 'V2';
554
572
  }
555
- throw this.createProtocolMismatchError(expectedProtocol, uuid);
573
+ throw this.createProtocolMismatchError(expectedProtocol);
556
574
  }
557
575
 
558
576
  // Protocol must be actively probed after connection. Name, PID, and descriptors only
@@ -571,9 +589,6 @@ export default class ElectronBleTransport {
571
589
  protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
572
590
  if (detected) {
573
591
  this.deviceProtocol.set(uuid, protocol);
574
- if (protocol === 'V2') {
575
- this.confirmedProtocolV2.add(uuid);
576
- }
577
592
  this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> ${protocol}`);
578
593
  return protocol;
579
594
  }
@@ -644,7 +659,7 @@ export default class ElectronBleTransport {
644
659
  }
645
660
  }
646
661
 
647
- private async probeProtocolV2(uuid: string) {
662
+ private async probeProtocolV2(uuid: string, expectedProtocol?: ProtocolType) {
648
663
  if (!this._messages || !this._messagesV2) {
649
664
  return false;
650
665
  }
@@ -660,8 +675,11 @@ export default class ElectronBleTransport {
660
675
  this.v2Assemblers.get(uuid)?.reset();
661
676
  this.resetProtocolV2Frames(uuid);
662
677
  },
678
+ // A declared V2 protocol needs no fallback; preserve the actual link failure.
663
679
  shouldRethrow: error =>
664
- isBleStaleBondHardwareError(error) || isProtocolV2LinkDisabledError(error),
680
+ expectedProtocol === 'V2' ||
681
+ isBleStaleBondHardwareError(error) ||
682
+ isProtocolV2LinkDisabledError(error),
665
683
  });
666
684
  if (!detected) {
667
685
  this.clearProbeProtocol(uuid, 'V2');
@@ -977,7 +995,7 @@ export default class ElectronBleTransport {
977
995
  this.notificationCleanups.delete(uuid);
978
996
  this.notificationTokens.delete(uuid);
979
997
  if (!isProbeTimeout) {
980
- await this.releaseNative(uuid);
998
+ await this.releaseNative(uuid, { forceNative: true });
981
999
  }
982
1000
  }
983
1001
  throw this.normalizeBluetoothError(e);
package/src/webusb.ts CHANGED
@@ -13,6 +13,7 @@ import transport, {
13
13
  } from '@onekeyfe/hd-transport';
14
14
  import {
15
15
  ERRORS,
16
+ HardwareError,
16
17
  HardwareErrorCode,
17
18
  ONEKEY_WEBUSB_FILTER,
18
19
  inferProtocolHintFromUsbId,
@@ -70,6 +71,8 @@ interface TransferCancelToken {
70
71
  cancelled: boolean;
71
72
  }
72
73
 
74
+ type WebUsbOperation = 'claimInterface' | 'transferIn' | 'transferOut';
75
+
73
76
  /**
74
77
  * The navigator.usb disconnect listener is module-scoped and attached at most
75
78
  * once: listeners on navigator.usb are global and never garbage collected, so a
@@ -566,10 +569,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
566
569
  if (!device.opened) {
567
570
  await device.open();
568
571
  }
569
- try {
570
- await device.reset();
571
- } catch (error) {
572
- this.Log?.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
572
+ // A V1 packet retry continues an in-flight exchange. Preserve its endpoint
573
+ // state as in the legacy transport; probing and V2 recovery still reset.
574
+ if (first || this.deviceProtocol.get(path) !== 'V1') {
575
+ try {
576
+ await device.reset();
577
+ } catch (error) {
578
+ this.Log?.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
579
+ }
573
580
  }
574
581
  await this.getConnectedDevices();
575
582
  device = await this.findDevice(path);
@@ -588,7 +595,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
588
595
  // Discover endpoints from USB descriptors; descriptors are not used for protocol selection.
589
596
  const endpoints = this.discoverEndpoints(device);
590
597
  this.deviceEndpoints.set(path, endpoints);
591
- await device.claimInterface(endpoints.interfaceNumber);
598
+ try {
599
+ await device.claimInterface(endpoints.interfaceNumber);
600
+ } catch (error) {
601
+ return this.throwWebUsbIoError(path, 'claimInterface', error);
602
+ }
592
603
  await this.clearEndpointHalt(device, 'in', endpoints.endpointIn);
593
604
  await this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
594
605
  }
@@ -658,6 +669,65 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
658
669
  return String(error);
659
670
  }
660
671
 
672
+ private getErrorName(error: unknown) {
673
+ if (typeof error === 'object' && error && 'name' in error) {
674
+ const { name } = error as { name?: unknown };
675
+ return typeof name === 'string' ? name : String(name ?? '');
676
+ }
677
+ return '';
678
+ }
679
+
680
+ private isWebUsbIoHardwareError(error: unknown): error is HardwareError {
681
+ return (
682
+ error instanceof HardwareError &&
683
+ (error.errorCode === HardwareErrorCode.BridgeDeviceDisconnected ||
684
+ error.errorCode === HardwareErrorCode.WebUsbDeviceAccessError)
685
+ );
686
+ }
687
+
688
+ private async isDeviceStillEnumerated(path: string): Promise<boolean | undefined> {
689
+ if (!this.usb) return undefined;
690
+ try {
691
+ const devices = await this.usb.getDevices();
692
+ return devices.some(device => resolveOneKeyUsbDevicePath(device) === path);
693
+ } catch (error) {
694
+ this.Log?.debug('[WebUsbTransport] failed to verify WebUSB device presence:', error);
695
+ return undefined;
696
+ }
697
+ }
698
+
699
+ private async throwWebUsbIoError(
700
+ path: string,
701
+ operation: WebUsbOperation,
702
+ error: unknown
703
+ ): Promise<never> {
704
+ if (this.isWebUsbIoHardwareError(error)) throw error;
705
+ if (error instanceof ProtocolV2LinkError && error.code !== 'io') throw error;
706
+
707
+ const nativeError = error instanceof ProtocolV2LinkError ? error.cause ?? error : error;
708
+ if (this.isWebUsbIoHardwareError(nativeError)) throw nativeError;
709
+
710
+ const nativeErrorName = this.getErrorName(nativeError);
711
+ const nativeErrorMessage = this.getErrorMessage(nativeError);
712
+ const disconnectedByMessage =
713
+ /(?:device )?disconnected|device not found|action was interrupted/i.test(
714
+ `${nativeErrorName}: ${nativeErrorMessage}`
715
+ );
716
+ const stillEnumerated = disconnectedByMessage
717
+ ? false
718
+ : await this.isDeviceStillEnumerated(path);
719
+ const errorCode =
720
+ disconnectedByMessage || stillEnumerated === false
721
+ ? HardwareErrorCode.BridgeDeviceDisconnected
722
+ : HardwareErrorCode.WebUsbDeviceAccessError;
723
+
724
+ throw ERRORS.TypedError(errorCode, nativeErrorMessage || undefined, {
725
+ operation,
726
+ nativeErrorName: nativeErrorName || undefined,
727
+ nativeErrorMessage: nativeErrorMessage || undefined,
728
+ });
729
+ }
730
+
661
731
  private isRetryablePacketIoError(error: unknown) {
662
732
  const message = this.getErrorMessage(error).toLowerCase();
663
733
  return (
@@ -738,7 +808,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
738
808
  lastError = error;
739
809
  const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryablePacketIoError(error);
740
810
  if (!shouldRetry) {
741
- throw error;
811
+ return this.throwWebUsbIoError(path, 'transferOut', error);
742
812
  }
743
813
  try {
744
814
  await this.reconnectForPacketIoRetry(path, 'out', attempt, error);
@@ -752,7 +822,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
752
822
  }
753
823
  }
754
824
  }
755
- throw lastError;
825
+ return this.throwWebUsbIoError(path, 'transferOut', lastError);
756
826
  }
757
827
 
758
828
  private async transferOutOnce(path: string, packet: Uint8Array) {
@@ -794,7 +864,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
794
864
  }
795
865
  const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryablePacketIoError(error);
796
866
  if (!shouldRetry) {
797
- throw error;
867
+ return this.throwWebUsbIoError(path, 'transferIn', error);
798
868
  }
799
869
  try {
800
870
  await this.reconnectForPacketIoRetry(path, 'in', attempt, error);
@@ -808,7 +878,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
808
878
  }
809
879
  }
810
880
  }
811
- throw lastError;
881
+ return this.throwWebUsbIoError(path, 'transferIn', lastError);
812
882
  }
813
883
 
814
884
  private async transferInOnce(path: string, length: number): Promise<DataView> {
@@ -890,7 +960,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
890
960
  }
891
961
  );
892
962
  return true;
893
- } catch (_error) {
963
+ } catch (error) {
964
+ if (this.isWebUsbIoHardwareError(error)) throw error;
894
965
  return false;
895
966
  }
896
967
  }
@@ -905,6 +976,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
905
976
  timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT,
906
977
  logger: this.Log,
907
978
  logPrefix: 'ProtocolV2 WebUSB',
979
+ shouldRethrow: error => this.isWebUsbIoHardwareError(error),
908
980
  });
909
981
  }
910
982
 
@@ -982,7 +1054,13 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
982
1054
  data: Record<string, unknown>,
983
1055
  options?: TransportCallOptions
984
1056
  ) {
985
- return this.callProtocolV2Usb(path, name, data, options);
1057
+ try {
1058
+ return await this.callProtocolV2Usb(path, name, data, options);
1059
+ } catch (error) {
1060
+ if (!(error instanceof ProtocolV2LinkError) || error.code !== 'io') throw error;
1061
+ const operation = error.message.includes(' USB write failed:') ? 'transferOut' : 'transferIn';
1062
+ return this.throwWebUsbIoError(path, operation, error);
1063
+ }
986
1064
  }
987
1065
 
988
1066
  /**