@onekeyfe/hd-transport-web-device 1.2.0-alpha.154 → 1.2.0-alpha.155

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.
@@ -1,5 +1,5 @@
1
1
  import transport, { PROTOCOL_V2_CHANNEL_BLE_UART, bytesToHex } from '@onekeyfe/hd-transport';
2
- import { HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
2
+ import { EBleDisconnectReason, HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
3
3
  import EventEmitter from 'events';
4
4
 
5
5
  import ElectronBleTransport from '../src/electron-ble-transport';
@@ -134,6 +134,30 @@ const configureTransport = (
134
134
  return transport;
135
135
  };
136
136
 
137
+ /**
138
+ * Wire the mock so acquire()'s Protocol V2 probe gets an answer. Without it
139
+ * detectProtocol throws and the test never reaches the disconnect behaviour.
140
+ */
141
+ const echoProtocolV2 = (nobleBle: ReturnType<typeof createNobleBle>, deviceId: string) => {
142
+ let notificationHandler: ((id: string, data: string) => void) | undefined;
143
+ nobleBle.onNotification.mockImplementation(handler => {
144
+ notificationHandler = handler;
145
+ return jest.fn();
146
+ });
147
+ let responseSeq = 0;
148
+ nobleBle.write.mockImplementation(() => {
149
+ responseSeq += 1;
150
+ const response = ProtocolV2.encodeFrame(
151
+ schemas,
152
+ 'Success',
153
+ { message: 'ok' },
154
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
155
+ );
156
+ setTimeout(() => notificationHandler?.(deviceId, bytesToHex(response)), 0);
157
+ return Promise.resolve();
158
+ });
159
+ };
160
+
137
161
  describe('ElectronBleTransport protocol detection', () => {
138
162
  afterEach(() => {
139
163
  delete (global as any).window;
@@ -624,21 +648,19 @@ describe('ElectronBleTransport protocol detection', () => {
624
648
  }
625
649
  });
626
650
 
627
- test('ignores a delayed disconnect event from the previous BLE connection', async () => {
628
- const device = { id: 'delayed-disconnect-pro2-id', name: 'OneKey Pro 2' };
651
+ test('subscribes to host disconnects once for the transport lifetime', async () => {
652
+ // The previous design registered a listener inside every acquire() and
653
+ // overwrote the cleanup entry without disposing the old one, so each
654
+ // acquire leaked a live handler and a delayed event from a superseded
655
+ // connection could tear down the current one. One transport-lifetime
656
+ // subscription removes that whole class of bug.
657
+ const device = { id: 'single-subscription-pro2-id', name: 'OneKey Pro 2' };
629
658
  const nobleBle = createNobleBle(device);
630
659
  let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
631
- const disconnectHandlers: Array<
632
- (disconnectedDevice: { id: string; name: string | null }) => void
633
- > = [];
634
660
  nobleBle.onNotification.mockImplementation(handler => {
635
661
  notificationHandler = handler;
636
662
  return jest.fn();
637
663
  });
638
- nobleBle.onDeviceDisconnected.mockImplementation(handler => {
639
- disconnectHandlers.push(handler);
640
- return jest.fn();
641
- });
642
664
  let responseSeq = 0;
643
665
  nobleBle.write.mockImplementation(() => {
644
666
  responseSeq += 1;
@@ -654,14 +676,15 @@ describe('ElectronBleTransport protocol detection', () => {
654
676
  const bleTransport = configureTransport(nobleBle);
655
677
 
656
678
  try {
679
+ expect(nobleBle.onDeviceDisconnected).toHaveBeenCalledTimes(1);
680
+
657
681
  await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
658
682
  await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
659
683
 
660
- disconnectHandlers[0]?.(device);
661
-
684
+ expect(nobleBle.onDeviceDisconnected).toHaveBeenCalledTimes(1);
662
685
  expect(bleTransport.getProtocolType(device.id)).toBe('V2');
663
686
  await expect(
664
- bleTransport.call(device.id, 'Ping', { message: 'after-stale-disconnect' })
687
+ bleTransport.call(device.id, 'Ping', { message: 'after-reacquire' })
665
688
  ).resolves.toEqual({
666
689
  type: 'Success',
667
690
  message: { message: 'ok' },
@@ -671,6 +694,95 @@ describe('ElectronBleTransport protocol detection', () => {
671
694
  }
672
695
  });
673
696
 
697
+ test('reports a device that drops after a logical release (OK-60486)', async () => {
698
+ // A logical release keeps the native link alive for the keep-alive window.
699
+ // A drop during that window must still reach consumers, or the UI keeps
700
+ // showing the device as connected forever.
701
+ const device = { id: 'idle-drop-pro2-id', name: 'OneKey Pro 2' };
702
+ const nobleBle = createNobleBle(device);
703
+ const emitter = new EventEmitter();
704
+ let disconnectHandler:
705
+ | ((d: { id: string; name: string; reason?: EBleDisconnectReason }) => void)
706
+ | undefined;
707
+ nobleBle.onDeviceDisconnected.mockImplementation(handler => {
708
+ disconnectHandler = handler;
709
+ return jest.fn();
710
+ });
711
+ echoProtocolV2(nobleBle, device.id);
712
+ const transportDisconnect = jest.fn();
713
+ emitter.on('transport-device-disconnect', transportDisconnect);
714
+ const bleTransport = configureTransport(nobleBle, emitter);
715
+
716
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
717
+ await bleTransport.release(device.id);
718
+
719
+ disconnectHandler?.({ ...device, reason: EBleDisconnectReason.DeviceDisconnected });
720
+
721
+ expect(transportDisconnect).toHaveBeenCalledWith({
722
+ id: device.id,
723
+ connectId: device.id,
724
+ name: device.name,
725
+ });
726
+ });
727
+
728
+ test('reports an idle keep-alive release as a device disconnect', async () => {
729
+ // The main process reclaims idle links on its own timer. That is still a
730
+ // closed link, and consumers track link liveness, so it is reported like
731
+ // any other drop. Nothing reconnects on its own, so the state settles once
732
+ // until the user acts.
733
+ const device = { id: 'keep-alive-pro2-id', name: 'OneKey Pro 2' };
734
+ const nobleBle = createNobleBle(device);
735
+ const emitter = new EventEmitter();
736
+ let disconnectHandler:
737
+ | ((d: { id: string; name: string; reason?: EBleDisconnectReason }) => void)
738
+ | undefined;
739
+ nobleBle.onDeviceDisconnected.mockImplementation(handler => {
740
+ disconnectHandler = handler;
741
+ return jest.fn();
742
+ });
743
+ echoProtocolV2(nobleBle, device.id);
744
+ const transportDisconnect = jest.fn();
745
+ emitter.on('transport-device-disconnect', transportDisconnect);
746
+ const bleTransport = configureTransport(nobleBle, emitter);
747
+
748
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
749
+ disconnectHandler?.({ ...device, reason: EBleDisconnectReason.IdleKeepAlive });
750
+
751
+ expect(transportDisconnect).toHaveBeenCalledWith({
752
+ id: device.id,
753
+ connectId: device.id,
754
+ name: device.name,
755
+ });
756
+ // The link really is gone, so cached link state must be dropped too.
757
+ expect(bleTransport.getProtocolType(device.id)).toBeUndefined();
758
+ });
759
+
760
+ test('treats a disconnect with no reason as a real device drop', async () => {
761
+ // An older host bridge does not send `reason`; defaulting to "device left"
762
+ // preserves the pre-existing behaviour rather than silently ignoring it.
763
+ const device = { id: 'legacy-host-pro2-id', name: 'OneKey Pro 2' };
764
+ const nobleBle = createNobleBle(device);
765
+ const emitter = new EventEmitter();
766
+ let disconnectHandler: ((d: { id: string; name: string }) => void) | undefined;
767
+ nobleBle.onDeviceDisconnected.mockImplementation(handler => {
768
+ disconnectHandler = handler;
769
+ return jest.fn();
770
+ });
771
+ echoProtocolV2(nobleBle, device.id);
772
+ const transportDisconnect = jest.fn();
773
+ emitter.on('transport-device-disconnect', transportDisconnect);
774
+ const bleTransport = configureTransport(nobleBle, emitter);
775
+
776
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
777
+ disconnectHandler?.(device);
778
+
779
+ expect(transportDisconnect).toHaveBeenCalledWith({
780
+ id: device.id,
781
+ connectId: device.id,
782
+ name: device.name,
783
+ });
784
+ });
785
+
674
786
  test('preserves the active Protocol V2 link when the same schema is configured again', async () => {
675
787
  const device = { id: 'stable-schema-pro2-id', name: 'OneKey Pro 2' };
676
788
  const nobleBle = createNobleBle(device);
@@ -38,12 +38,13 @@ export default class ElectronBleTransport {
38
38
  private warnedMissingRelease;
39
39
  private notificationCleanups;
40
40
  private mtuCleanups;
41
- private disconnectCleanups;
41
+ private hostDisconnectCleanup?;
42
42
  private notificationTokens;
43
43
  private nextNotificationToken;
44
44
  private handleBluetoothError;
45
45
  private cleanupDeviceState;
46
46
  init(logger: any, emitter?: EventEmitter): void;
47
+ private subscribeHostDisconnects;
47
48
  configure(signedData: any): void;
48
49
  configureProtocolV2(signedData: any): void;
49
50
  listen(): Promise<OneKeyDeviceInfo[]>;
@@ -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":";AAuBA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,KAAK,EACV,gBAAgB,EAChB,YAAY,EAEZ,oBAAoB,EACrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AAIvC,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,UAAU,CAAC,EAAE,UAAU,CAAC;KACzB;CACF;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,gBAAgB,CAAC,EAAE,YAAY,CAAC;IAChC,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B,CAAC;AAiCF,MAAM,CAAC,OAAO,OAAO,oBAAoB;IACvC,OAAO,CAAC,SAAS,CAA0D;IAE3E,OAAO,CAAC,WAAW,CAA0D;IAE7E,OAAO,CAAC,6BAA6B,CAAqB;IAE1D,IAAI,SAA0B;IAE9B,UAAU,UAAS;IAEnB,UAAU,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,GAAG,IAAI,CAAQ;IAExD,OAAO,CAAC,kBAAkB,CAAuB;IAEjD,GAAG,CAAC,EAAE,GAAG,CAAC;IAEV,OAAO,CAAC,EAAE,YAAY,CAAC;IAEvB,OAAO,CAAC,gBAAgB,CAA0B;IAElD,OAAO,CAAC,cAAc,CAAwC;IAE9D,OAAO,CAAC,mBAAmB,CAAwC;IAGnE,OAAO,CAAC,mBAAmB,CAAqB;IAEhD,OAAO,CAAC,UAAU,CAAkC;IAEpD,OAAO,CAAC,sBAAsB,CAAkC;IAEhE,OAAO,CAAC,SAAS,CAAsE;IAEvF,OAAO,CAAC,YAAY,CAAoD;IAExE,OAAO,CAAC,aAAa,CAAwC;IAE7D,OAAO,CAAC,eAAe,CAAgD;IAEvE,OAAO,CAAC,eAAe,CAmBpB;IAGH,OAAO,CAAC,oBAAoB,CAAS;IAErC,OAAO,CAAC,oBAAoB,CAAsC;IAElE,OAAO,CAAC,WAAW,CAAsC;IAUzD,OAAO,CAAC,qBAAqB,CAAC,CAAa;IAE3C,OAAO,CAAC,kBAAkB,CAAkC;IAE5D,OAAO,CAAC,qBAAqB,CAAK;IAElC,OAAO,CAAC,oBAAoB;IA+B5B,OAAO,CAAC,kBAAkB;IAiC1B,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,YAAY;IAqBxC,OAAO,CAAC,wBAAwB;IAgChC,SAAS,CAAC,UAAU,EAAE,GAAG;IAKzB,mBAAmB,CAAC,UAAU,EAAE,GAAG;IAgB7B,MAAM;IAIN,SAAS,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAiBxC,OAAO,CAAC,KAAK,EAAE,eAAe;;;;;;;;;;;IAmF9B,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,OAAO;IAY7D,UAAU,CAAC,EAAE,EAAE,MAAM;YAKb,aAAa;YAiBb,cAAc;IAwB5B,OAAO,CAAC,2BAA2B;IAUnC,OAAO,CAAC,4BAA4B;IAOpC,OAAO,CAAC,kBAAkB;YAMZ,cAAc;IAoD5B,OAAO,CAAC,8BAA8B;YAgBxB,iCAAiC;YAwBjC,eAAe;YAkBf,eAAe;YAuBf,SAAS;YAST,wBAAwB;IAKtC,OAAO,CAAC,uBAAuB;IAc/B,OAAO,CAAC,qBAAqB;IAU7B,OAAO,CAAC,oBAAoB;IAqB5B,OAAO,CAAC,kBAAkB;IAwB1B,OAAO,CAAC,4BAA4B;IAkBpC,OAAO,CAAC,uBAAuB;IAS/B,OAAO,CAAC,sBAAsB;IAU9B,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,sBAAsB;YAShB,mBAAmB;IAiBjC,OAAO,CAAC,4BAA4B;IAqB9B,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,oBAAoB;IAuB1B,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;YAatD,cAAc;YAqFd,cAAc;IA0B5B,OAAO,CAAC,uBAAuB;IAyC/B,OAAO,CAAC,6BAA6B;IA4CrC,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;CAGxD"}
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as _onekeyfe_hd_transport from '@onekeyfe/hd-transport';
2
2
  import _onekeyfe_hd_transport__default, { ProtocolV2UsbTransportBase, AcquireInput, TransportCallOptions, ProtocolV2Schemas, ProtocolV2CallContext, ProtocolType, OneKeyDeviceInfoBase, OneKeyDeviceInfo } from '@onekeyfe/hd-transport';
3
+ import EventEmitter from 'events';
3
4
  import { Deferred } from '@onekeyfe/hd-shared';
4
5
  import { DesktopAPI } from '@onekeyfe/hd-transport-electron';
5
- import EventEmitter from 'events';
6
6
 
7
7
  /**
8
8
  * Device information with path and WebUSB device instance
@@ -42,6 +42,7 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
42
42
  configured: boolean;
43
43
  Log?: any;
44
44
  usb?: USB;
45
+ emitter?: EventEmitter;
45
46
  /**
46
47
  * Cached list of connected devices
47
48
  * This is essential for maintaining device references between operations
@@ -54,7 +55,12 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
54
55
  /**
55
56
  * Initialize WebUSB transport
56
57
  */
57
- init(logger: any): void;
58
+ init(logger: any, emitter?: EventEmitter): void;
59
+ /**
60
+ * Announce that a USB device left. Called from the module-scoped disconnect
61
+ * listener, which is why it is public rather than inlined.
62
+ */
63
+ emitDeviceDisconnect(path: string, device?: USBDevice): void;
58
64
  /**
59
65
  * Protocol type is a property of the physical device keyed by USB serial number.
60
66
  * It can only change across a device reboot (e.g. normal ↔ bootloader mode), and
@@ -223,12 +229,26 @@ declare class ElectronBleTransport {
223
229
  private warnedMissingRelease;
224
230
  private notificationCleanups;
225
231
  private mtuCleanups;
226
- private disconnectCleanups;
232
+ /**
233
+ * Transport-lifetime subscription to host BLE disconnects.
234
+ *
235
+ * This must NOT be scoped to acquire()/release(): a logical release keeps the
236
+ * native link alive for the keep-alive window, so a device that drops while
237
+ * idle would otherwise go unobserved and consumers would never learn it left
238
+ * (OK-60486).
239
+ */
240
+ private hostDisconnectCleanup?;
227
241
  private notificationTokens;
228
242
  private nextNotificationToken;
229
243
  private handleBluetoothError;
230
244
  private cleanupDeviceState;
231
245
  init(logger: any, emitter?: EventEmitter): void;
246
+ /**
247
+ * One host subscription for the whole transport lifetime. init() can run
248
+ * again after an SDK reset, so drop the previous listener first rather than
249
+ * stacking duplicates.
250
+ */
251
+ private subscribeHostDisconnects;
232
252
  configure(signedData: any): void;
233
253
  configureProtocolV2(signedData: any): void;
234
254
  listen(): Promise<OneKeyDeviceInfo[]>;
package/dist/index.js CHANGED
@@ -68,6 +68,7 @@ function registerActiveWebUsbTransport(instance, usb) {
68
68
  if (!path)
69
69
  return;
70
70
  activeWebUsbTransport === null || activeWebUsbTransport === void 0 ? void 0 : activeWebUsbTransport.markProtocolStale(path);
71
+ activeWebUsbTransport === null || activeWebUsbTransport === void 0 ? void 0 : activeWebUsbTransport.emitDeviceDisconnect(path, event.device);
71
72
  });
72
73
  }
73
74
  class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
@@ -91,8 +92,9 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
91
92
  this.endpointId = ENDPOINT_ID;
92
93
  this.interfaceId = INTERFACE_ID;
93
94
  }
94
- init(logger) {
95
+ init(logger, emitter) {
95
96
  this.Log = logger;
97
+ this.emitter = emitter;
96
98
  const { usb } = navigator;
97
99
  if (!usb) {
98
100
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'WebUSB is not supported by current browsers');
@@ -100,6 +102,14 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
100
102
  this.usb = usb;
101
103
  registerActiveWebUsbTransport(this, usb);
102
104
  }
105
+ emitDeviceDisconnect(path, device) {
106
+ var _a, _b;
107
+ (_a = this.emitter) === null || _a === void 0 ? void 0 : _a.emit(transport.TRANSPORT_EVENT.DEVICE_DISCONNECT, {
108
+ name: (_b = device === null || device === void 0 ? void 0 : device.productName) !== null && _b !== void 0 ? _b : '',
109
+ id: path,
110
+ connectId: path,
111
+ });
112
+ }
103
113
  markProtocolStale(path) {
104
114
  this.staleProtocolPaths.add(path);
105
115
  this.probedDeviceObjects.delete(path);
@@ -839,7 +849,6 @@ class ElectronBleTransport {
839
849
  this.warnedMissingRelease = false;
840
850
  this.notificationCleanups = new Map();
841
851
  this.mtuCleanups = new Map();
842
- this.disconnectCleanups = new Map();
843
852
  this.notificationTokens = new Map();
844
853
  this.nextNotificationToken = 1;
845
854
  }
@@ -899,11 +908,6 @@ class ElectronBleTransport {
899
908
  mtuCleanup();
900
909
  this.mtuCleanups.delete(deviceId);
901
910
  }
902
- const disconnectCleanup = this.disconnectCleanups.get(deviceId);
903
- if (disconnectCleanup) {
904
- disconnectCleanup();
905
- this.disconnectCleanups.delete(deviceId);
906
- }
907
911
  }
908
912
  init(logger, emitter) {
909
913
  var _a, _b;
@@ -912,8 +916,26 @@ class ElectronBleTransport {
912
916
  if (!((_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle)) {
913
917
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble BLE API is not available. Please ensure you are running in Electron with Noble support.');
914
918
  }
919
+ this.subscribeHostDisconnects();
915
920
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('[Electron BLE] Transport initialized');
916
921
  }
922
+ subscribeHostDisconnects() {
923
+ var _a, _b, _c;
924
+ (_a = this.hostDisconnectCleanup) === null || _a === void 0 ? void 0 : _a.call(this);
925
+ this.hostDisconnectCleanup = (_c = (_b = window.desktopApi) === null || _b === void 0 ? void 0 : _b.nobleBle) === null || _c === void 0 ? void 0 : _c.onDeviceDisconnected((disconnectedDevice) => {
926
+ var _a, _b, _c;
927
+ const uuid = disconnectedDevice === null || disconnectedDevice === void 0 ? void 0 : disconnectedDevice.id;
928
+ if (!uuid)
929
+ return;
930
+ this.cleanupDeviceState(uuid);
931
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] Device link dropped:', uuid, (_b = disconnectedDevice.reason) !== null && _b !== void 0 ? _b : hdShared.EBleDisconnectReason.DeviceDisconnected);
932
+ (_c = this.emitter) === null || _c === void 0 ? void 0 : _c.emit(transport.TRANSPORT_EVENT.DEVICE_DISCONNECT, {
933
+ name: disconnectedDevice.name,
934
+ id: uuid,
935
+ connectId: uuid,
936
+ });
937
+ });
938
+ }
917
939
  configure(signedData) {
918
940
  this._messages = parseConfigure(signedData);
919
941
  this.configured = true;
@@ -1004,7 +1026,6 @@ class ElectronBleTransport {
1004
1026
  yield this.refreshBlePacketCapacity(uuid);
1005
1027
  const cleanup = this.createNotificationSubscription(uuid);
1006
1028
  this.notificationCleanups.set(uuid, cleanup);
1007
- const connectionToken = this.notificationTokens.get(uuid);
1008
1029
  const protocolType = yield this.detectProtocol(uuid, expectedProtocol, protocolHint);
1009
1030
  if (protocolType === 'V2') {
1010
1031
  (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug('[Electron BLE] Protocol V2 write configured', {
@@ -1013,19 +1034,6 @@ class ElectronBleTransport {
1013
1034
  packetCapacity: (_d = this.devicePacketCapacities.get(uuid)) !== null && _d !== void 0 ? _d : BLE_PACKET_SIZE_FALLBACK,
1014
1035
  });
1015
1036
  }
1016
- const disconnectCleanup = window.desktopApi.nobleBle.onDeviceDisconnected((disconnectedDevice) => {
1017
- var _a;
1018
- if (disconnectedDevice.id === uuid &&
1019
- this.notificationTokens.get(uuid) === connectionToken) {
1020
- this.cleanupDeviceState(uuid);
1021
- (_a = this.emitter) === null || _a === void 0 ? void 0 : _a.emit(transport.TRANSPORT_EVENT.DEVICE_DISCONNECT, {
1022
- name: disconnectedDevice.name,
1023
- id: disconnectedDevice.id,
1024
- connectId: disconnectedDevice.id,
1025
- });
1026
- }
1027
- });
1028
- this.disconnectCleanups.set(uuid, disconnectCleanup);
1029
1037
  return Object.assign(Object.assign({}, toBleDescriptor({ id: device.id, name: device.name }, protocolType)), { uuid });
1030
1038
  }
1031
1039
  catch (error) {
package/dist/webusb.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  /// <reference types="w3c-web-usb" />
2
+ /// <reference types="node" />
2
3
  import transport, { ProtocolV2UsbTransportBase } from '@onekeyfe/hd-transport';
4
+ import type EventEmitter from 'events';
3
5
  import type { AcquireInput, OneKeyDeviceInfoBase, ProtocolType, ProtocolV2CallContext, ProtocolV2Schemas, TransportCallOptions } from '@onekeyfe/hd-transport';
4
6
  export interface DeviceInfo extends OneKeyDeviceInfoBase {
5
7
  path: string;
@@ -21,12 +23,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
21
23
  configured: boolean;
22
24
  Log?: any;
23
25
  usb?: USB;
26
+ emitter?: EventEmitter;
24
27
  deviceList: Array<DeviceInfo>;
25
28
  configurationId: number;
26
29
  endpointId: number;
27
30
  interfaceId: number;
28
31
  constructor();
29
- init(logger: any): void;
32
+ init(logger: any, emitter?: EventEmitter): void;
33
+ emitDeviceDisconnect(path: string, device?: USBDevice): void;
30
34
  markProtocolStale(path: string): void;
31
35
  configure(signedData: any): void;
32
36
  configureProtocolV2(signedData: any): void;
@@ -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.0-alpha.154",
3
+ "version": "1.2.0-alpha.155",
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.0-alpha.154",
25
- "@onekeyfe/hd-transport": "1.2.0-alpha.154"
24
+ "@onekeyfe/hd-shared": "1.2.0-alpha.155",
25
+ "@onekeyfe/hd-transport": "1.2.0-alpha.155"
26
26
  },
27
27
  "devDependencies": {
28
- "@onekeyfe/hd-transport-electron": "1.2.0-alpha.154",
28
+ "@onekeyfe/hd-transport-electron": "1.2.0-alpha.155",
29
29
  "@types/w3c-web-usb": "^1.0.6",
30
30
  "@types/web-bluetooth": "^0.0.17"
31
31
  },
32
- "gitHead": "0b81bf264b2b33e00550b1008264c02e0a049b68"
32
+ "gitHead": "922cb4e29b2d89511c9b67c25840294335cb8005"
33
33
  }
@@ -11,6 +11,7 @@ import transport, {
11
11
  writeProtocolV2BleFrame,
12
12
  } from '@onekeyfe/hd-transport';
13
13
  import {
14
+ EBleDisconnectReason,
14
15
  ERRORS,
15
16
  HardwareErrorCode,
16
17
  HardwareErrorCodeMessage,
@@ -144,7 +145,15 @@ export default class ElectronBleTransport {
144
145
 
145
146
  private mtuCleanups: Map<string, () => void> = new Map();
146
147
 
147
- private disconnectCleanups: Map<string, () => void> = new Map();
148
+ /**
149
+ * Transport-lifetime subscription to host BLE disconnects.
150
+ *
151
+ * This must NOT be scoped to acquire()/release(): a logical release keeps the
152
+ * native link alive for the keep-alive window, so a device that drops while
153
+ * idle would otherwise go unobserved and consumers would never learn it left
154
+ * (OK-60486).
155
+ */
156
+ private hostDisconnectCleanup?: () => void;
148
157
 
149
158
  private notificationTokens: Map<string, number> = new Map();
150
159
 
@@ -212,12 +221,6 @@ export default class ElectronBleTransport {
212
221
  mtuCleanup();
213
222
  this.mtuCleanups.delete(deviceId);
214
223
  }
215
-
216
- const disconnectCleanup = this.disconnectCleanups.get(deviceId);
217
- if (disconnectCleanup) {
218
- disconnectCleanup();
219
- this.disconnectCleanups.delete(deviceId);
220
- }
221
224
  }
222
225
 
223
226
  init(logger: any, emitter?: EventEmitter) {
@@ -231,9 +234,48 @@ export default class ElectronBleTransport {
231
234
  );
232
235
  }
233
236
 
237
+ this.subscribeHostDisconnects();
238
+
234
239
  this.Log?.debug('[Electron BLE] Transport initialized');
235
240
  }
236
241
 
242
+ /**
243
+ * One host subscription for the whole transport lifetime. init() can run
244
+ * again after an SDK reset, so drop the previous listener first rather than
245
+ * stacking duplicates.
246
+ */
247
+ private subscribeHostDisconnects() {
248
+ this.hostDisconnectCleanup?.();
249
+ this.hostDisconnectCleanup = window.desktopApi?.nobleBle?.onDeviceDisconnected(
250
+ (disconnectedDevice: { id: string; name: string; reason?: EBleDisconnectReason }) => {
251
+ const uuid = disconnectedDevice?.id;
252
+ if (!uuid) return;
253
+
254
+ // The link is gone, so renderer-side state must go with it; the next
255
+ // acquire reconnects from scratch.
256
+ this.cleanupDeviceState(uuid);
257
+
258
+ // Every link drop is reported, including the main process reclaiming
259
+ // an idle link on its keep-alive timer: consumers track whether a BLE
260
+ // link is live, not whether the peripheral is theoretically in range,
261
+ // and a link we closed ourselves is still a closed link. Nothing
262
+ // reconnects on its own, so this settles once until the user acts.
263
+ // `reason` is carried for diagnostics only — behaviour is uniform.
264
+ this.Log?.debug(
265
+ '[Electron BLE] Device link dropped:',
266
+ uuid,
267
+ disconnectedDevice.reason ?? EBleDisconnectReason.DeviceDisconnected
268
+ );
269
+
270
+ this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
271
+ name: disconnectedDevice.name,
272
+ id: uuid,
273
+ connectId: uuid,
274
+ });
275
+ }
276
+ );
277
+ }
278
+
237
279
  configure(signedData: any) {
238
280
  this._messages = parseConfigure(signedData);
239
281
  this.configured = true;
@@ -330,7 +372,6 @@ export default class ElectronBleTransport {
330
372
 
331
373
  const cleanup = this.createNotificationSubscription(uuid);
332
374
  this.notificationCleanups.set(uuid, cleanup);
333
- const connectionToken = this.notificationTokens.get(uuid);
334
375
 
335
376
  const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
336
377
  if (protocolType === 'V2') {
@@ -341,23 +382,6 @@ export default class ElectronBleTransport {
341
382
  });
342
383
  }
343
384
 
344
- const disconnectCleanup = window.desktopApi.nobleBle.onDeviceDisconnected(
345
- (disconnectedDevice: any) => {
346
- if (
347
- disconnectedDevice.id === uuid &&
348
- this.notificationTokens.get(uuid) === connectionToken
349
- ) {
350
- this.cleanupDeviceState(uuid);
351
- this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
352
- name: disconnectedDevice.name,
353
- id: disconnectedDevice.id,
354
- connectId: disconnectedDevice.id,
355
- });
356
- }
357
- }
358
- );
359
- this.disconnectCleanups.set(uuid, disconnectCleanup);
360
-
361
385
  return {
362
386
  ...toBleDescriptor({ id: device.id, name: device.name }, protocolType),
363
387
  uuid,
package/src/webusb.ts CHANGED
@@ -8,6 +8,7 @@ import transport, {
8
8
  PROTOCOL_V2_FRAME_MAX_BYTES,
9
9
  ProtocolV2LinkError,
10
10
  ProtocolV2UsbTransportBase,
11
+ TRANSPORT_EVENT,
11
12
  probeProtocolV2 as probeProtocolV2Helper,
12
13
  } from '@onekeyfe/hd-transport';
13
14
  import {
@@ -21,6 +22,7 @@ import {
21
22
  } from '@onekeyfe/hd-shared';
22
23
  import ByteBuffer from 'bytebuffer';
23
24
 
25
+ import type EventEmitter from 'events';
24
26
  import type {
25
27
  AcquireInput,
26
28
  OneKeyDeviceInfoBase,
@@ -87,6 +89,10 @@ function registerActiveWebUsbTransport(instance: WebUsbTransport, usb: USB) {
87
89
  const path = resolveOneKeyUsbDevicePath(event.device);
88
90
  if (!path) return;
89
91
  activeWebUsbTransport?.markProtocolStale(path);
92
+ // WebUSB has no device-list poller behind it, so this event is the only
93
+ // signal that the device is gone. Without it consumers never see a
94
+ // DEVICE.DISCONNECT for USB (OK-60486).
95
+ activeWebUsbTransport?.emitDeviceDisconnect(path, event.device);
90
96
  });
91
97
  }
92
98
 
@@ -134,6 +140,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
134
140
 
135
141
  usb?: USB;
136
142
 
143
+ emitter?: EventEmitter;
144
+
137
145
  /**
138
146
  * Cached list of connected devices
139
147
  * This is essential for maintaining device references between operations
@@ -157,8 +165,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
157
165
  /**
158
166
  * Initialize WebUSB transport
159
167
  */
160
- init(logger: any) {
168
+ init(logger: any, emitter?: EventEmitter) {
161
169
  this.Log = logger;
170
+ this.emitter = emitter;
162
171
 
163
172
  const { usb } = navigator;
164
173
  if (!usb) {
@@ -171,6 +180,18 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
171
180
  registerActiveWebUsbTransport(this, usb);
172
181
  }
173
182
 
183
+ /**
184
+ * Announce that a USB device left. Called from the module-scoped disconnect
185
+ * listener, which is why it is public rather than inlined.
186
+ */
187
+ emitDeviceDisconnect(path: string, device?: USBDevice) {
188
+ this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
189
+ name: device?.productName ?? '',
190
+ id: path,
191
+ connectId: path,
192
+ });
193
+ }
194
+
174
195
  /**
175
196
  * Protocol type is a property of the physical device keyed by USB serial number.
176
197
  * It can only change across a device reboot (e.g. normal ↔ bootloader mode), and