@onekeyfe/hd-transport-electron 1.2.2-alpha.102 → 1.2.2-alpha.103

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.
@@ -55,13 +55,13 @@ function invokeNobleBleIpc(request) {
55
55
 
56
56
  function initNobleBleSupport(webContents) {
57
57
  return __awaiter(this, void 0, void 0, function* () {
58
- const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-3a1d131d.js'); });
58
+ const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-eff26dd3.js'); });
59
59
  setupNobleBleHandlers(webContents);
60
60
  });
61
61
  }
62
62
  function disposeNobleBleSupport(releaseNoble) {
63
63
  return __awaiter(this, void 0, void 0, function* () {
64
- const handler = yield Promise.resolve().then(function () { return require('./noble-ble-handler-3a1d131d.js'); });
64
+ const handler = yield Promise.resolve().then(function () { return require('./noble-ble-handler-eff26dd3.js'); });
65
65
  yield handler.disposeNobleBleSupport(releaseNoble);
66
66
  });
67
67
  }
package/dist/index.d.ts CHANGED
@@ -18,6 +18,7 @@ interface NobleModule {
18
18
  startScanning(serviceUUIDs: string[], allowDuplicates: boolean, callback?: (error?: Error) => void): void;
19
19
  stopScanning(callback?: () => void): void;
20
20
  stop(): void;
21
+ cancelConnect?(id: string): void;
21
22
  on(event: 'stateChange', listener: (state: string) => void): void;
22
23
  on(event: 'discover', listener: (peripheral: Peripheral) => void): void;
23
24
  removeListener(event: 'stateChange', listener: (state: string) => void): void;
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var index = require('./index-8e6acfc1.js');
5
+ var index = require('./index-052b4b5b.js');
6
6
 
7
7
 
8
8
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-8e6acfc1.js');
3
+ var index = require('./index-052b4b5b.js');
4
4
  var hdShared = require('@onekeyfe/hd-shared');
5
5
  var pRetry = require('p-retry');
6
6
 
@@ -101,9 +101,20 @@ const NOBLE_BLE_SUBSCRIBE_TIMEOUT_MS = 10000;
101
101
  let noble = null;
102
102
  let logger = null;
103
103
  let disposing = false;
104
+ let nativeReleased = false;
104
105
  let disposePromise;
106
+ let windowCleanup;
105
107
  let removeIpcHandlers;
106
108
  const pendingCancellations = new Set();
109
+ const nativeConnections = new Set();
110
+ function trackNativeConnection(operation, cancel) {
111
+ const connection = {
112
+ cancel,
113
+ settled: operation.then(() => undefined, () => undefined),
114
+ };
115
+ nativeConnections.add(connection);
116
+ connection.settled = connection.settled.finally(() => nativeConnections.delete(connection));
117
+ }
107
118
  function assertBleActive() {
108
119
  if (disposing) {
109
120
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, 'Noble BLE is shutting down');
@@ -182,9 +193,24 @@ function connectPeripheralWithCancellation(peripheral, deviceId, messagePrefix =
182
193
  (_a = connectingDevices
183
194
  .get(deviceId)) === null || _a === void 0 ? void 0 : _a.cancel(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, `Device ${deviceId} connect attempt was superseded`));
184
195
  connectingDevices.set(deviceId, pendingConnection);
185
- peripheral.connect(error => {
196
+ let completeNative;
197
+ trackNativeConnection(new Promise(resolve => {
198
+ completeNative = resolve;
199
+ }), () => {
200
+ var _a;
201
+ if (typeof peripheral.cancelConnect === 'function')
202
+ peripheral.cancelConnect();
203
+ else
204
+ (_a = noble === null || noble === void 0 ? void 0 : noble.cancelConnect) === null || _a === void 0 ? void 0 : _a.call(noble, deviceId);
205
+ });
206
+ const onConnect = (error) => {
207
+ if (nativeReleased) {
208
+ completeNative();
209
+ return;
210
+ }
186
211
  if (error) {
187
212
  settle(() => reject(createNobleBleConnectionError(error, messagePrefix)));
213
+ completeNative();
188
214
  return;
189
215
  }
190
216
  if (!settle(() => {
@@ -195,13 +221,27 @@ function connectPeripheralWithCancellation(peripheral, deviceId, messagePrefix =
195
221
  if (!activeConnection || activeConnection.peripheral !== peripheral) {
196
222
  try {
197
223
  peripheral.removeAllListeners('disconnect');
198
- peripheral.disconnect(() => undefined);
224
+ runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
225
+ timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
226
+ timeoutBehavior: 'resolve',
227
+ })
228
+ .catch(() => undefined)
229
+ .finally(completeNative);
230
+ return;
199
231
  }
200
232
  catch (_a) {
201
233
  }
202
234
  }
203
235
  }
204
- });
236
+ completeNative();
237
+ };
238
+ try {
239
+ peripheral.connect(onConnect);
240
+ }
241
+ catch (error) {
242
+ completeNative();
243
+ settle(() => reject(error));
244
+ }
205
245
  });
206
246
  }
207
247
  const pairedDevices = new Set();
@@ -903,7 +943,7 @@ function enumerateDevices() {
903
943
  }
904
944
  function stopScanning() {
905
945
  return index.__awaiter(this, void 0, void 0, function* () {
906
- if (!noble)
946
+ if (!noble || nativeReleased)
907
947
  return;
908
948
  const nobleInstance = noble;
909
949
  yield waitForNobleScanStop(nobleInstance);
@@ -1168,12 +1208,28 @@ function tryDirectConnectById(deviceId) {
1168
1208
  return undefined;
1169
1209
  let timer;
1170
1210
  let cancel;
1211
+ let abandoned = false;
1171
1212
  try {
1172
1213
  const directPromise = nobleInstance.connectAsync(deviceId);
1214
+ trackNativeConnection(directPromise.then((late) => index.__awaiter(this, void 0, void 0, function* () {
1215
+ if ((!abandoned && !disposing) || nativeReleased)
1216
+ return;
1217
+ const peripheral = late !== null && late !== void 0 ? late : discoveredDevices.get(deviceId);
1218
+ if ((peripheral === null || peripheral === void 0 ? void 0 : peripheral.state) === 'connected' && !connectedDevices.has(deviceId)) {
1219
+ peripheral.removeAllListeners('disconnect');
1220
+ yield runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
1221
+ timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
1222
+ timeoutBehavior: 'resolve',
1223
+ });
1224
+ }
1225
+ })), () => { var _a; return (_a = nobleInstance.cancelConnect) === null || _a === void 0 ? void 0 : _a.call(nobleInstance, deviceId); });
1173
1226
  const raced = yield Promise.race([
1174
1227
  directPromise,
1175
1228
  new Promise(resolve => {
1176
- cancel = () => resolve('timeout');
1229
+ cancel = () => {
1230
+ abandoned = true;
1231
+ resolve('timeout');
1232
+ };
1177
1233
  pendingCancellations.add(cancel);
1178
1234
  timer = setTimeout(cancel, DIRECT_CONNECT_TIMEOUT_MS);
1179
1235
  }),
@@ -1183,17 +1239,6 @@ function tryDirectConnectById(deviceId) {
1183
1239
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct connect-by-id timed out, falling back to scan', {
1184
1240
  deviceId,
1185
1241
  });
1186
- directPromise
1187
- .then(late => {
1188
- const latePeripheral = late !== null && late !== void 0 ? late : discoveredDevices.get(deviceId);
1189
- if (latePeripheral &&
1190
- latePeripheral.state === 'connected' &&
1191
- !connectedDevices.has(deviceId)) {
1192
- latePeripheral.removeAllListeners('disconnect');
1193
- latePeripheral.disconnect(() => undefined);
1194
- }
1195
- })
1196
- .catch(() => undefined);
1197
1242
  return undefined;
1198
1243
  }
1199
1244
  const peripheral = raced !== null && raced !== void 0 ? raced : discoveredDevices.get(deviceId);
@@ -1333,6 +1378,8 @@ function connectDevice(deviceId, webContents) {
1333
1378
  function disconnectDevice(deviceId) {
1334
1379
  var _a;
1335
1380
  return index.__awaiter(this, void 0, void 0, function* () {
1381
+ if (nativeReleased)
1382
+ return;
1336
1383
  const pendingConnection = connectingDevices.get(deviceId);
1337
1384
  const peripheral = (_a = connectedDevices.get(deviceId)) !== null && _a !== void 0 ? _a : pendingConnection === null || pendingConnection === void 0 ? void 0 : pendingConnection.peripheral;
1338
1385
  if (!peripheral) {
@@ -1362,6 +1409,8 @@ function disconnectDevice(deviceId) {
1362
1409
  }
1363
1410
  function unsubscribeNotifications(deviceId) {
1364
1411
  return index.__awaiter(this, void 0, void 0, function* () {
1412
+ if (nativeReleased)
1413
+ return;
1365
1414
  const peripheral = connectedDevices.get(deviceId);
1366
1415
  const characteristics = deviceCharacteristics.get(deviceId);
1367
1416
  if (!peripheral || !characteristics) {
@@ -1469,6 +1518,7 @@ function subscribeNotifications(deviceId, callback) {
1469
1518
  function setupNobleBleHandlers(webContents) {
1470
1519
  if (disposing)
1471
1520
  return;
1521
+ let windowDestroyed = false;
1472
1522
  try {
1473
1523
  logger = require('electron-log');
1474
1524
  const { ipcMain } = require('electron');
@@ -1479,7 +1529,12 @@ function setupNobleBleHandlers(webContents) {
1479
1529
  ipcMain.removeHandler(channel);
1480
1530
  ipcMain.handle(channel, (...args) => index.__awaiter(this, void 0, void 0, function* () {
1481
1531
  try {
1532
+ if (windowCleanup)
1533
+ yield windowCleanup;
1482
1534
  assertBleActive();
1535
+ if (windowDestroyed) {
1536
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, 'BLE window destroyed');
1537
+ }
1483
1538
  return yield Promise.resolve(listener(...args));
1484
1539
  }
1485
1540
  catch (error) {
@@ -1582,15 +1637,27 @@ function setupNobleBleHandlers(webContents) {
1582
1637
  }
1583
1638
  }));
1584
1639
  webContents.on('destroyed', () => {
1640
+ windowDestroyed = true;
1585
1641
  if (disposing)
1586
1642
  return;
1587
1643
  safeLog(logger, 'info', 'Cleaning up Noble BLE handlers');
1588
- (() => index.__awaiter(this, void 0, void 0, function* () {
1644
+ const previousCleanup = windowCleanup;
1645
+ windowCleanup = (() => index.__awaiter(this, void 0, void 0, function* () {
1646
+ if (previousCleanup)
1647
+ yield previousCleanup;
1648
+ if (disposing)
1649
+ return;
1589
1650
  const deviceIds = Array.from(connectedDevices.keys());
1590
1651
  for (const deviceId of deviceIds) {
1652
+ if (disposing)
1653
+ return;
1591
1654
  yield unsubscribeNotifications(deviceId).catch(() => undefined);
1655
+ if (disposing)
1656
+ return;
1592
1657
  yield disconnectDevice(deviceId).catch(() => undefined);
1593
1658
  }
1659
+ if (disposing)
1660
+ return;
1594
1661
  yield stopScanning().catch(() => undefined);
1595
1662
  cleanupNobleListeners();
1596
1663
  discoveredDevices.clear();
@@ -1621,11 +1688,22 @@ function disposeNobleBleSupport(releaseNoble = instance => instance.stop()) {
1621
1688
  for (const pending of connectingDevices.values()) {
1622
1689
  pending.cancel(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, 'Noble BLE is shutting down'));
1623
1690
  }
1691
+ const connections = Array.from(nativeConnections);
1692
+ for (const connection of connections) {
1693
+ try {
1694
+ connection.cancel();
1695
+ }
1696
+ catch (error) {
1697
+ logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Native connect cancellation failed', error);
1698
+ }
1699
+ }
1624
1700
  let timeout;
1625
1701
  disposePromise = (() => index.__awaiter(this, void 0, void 0, function* () {
1626
1702
  try {
1627
1703
  yield Promise.race([
1628
1704
  Promise.allSettled([
1705
+ windowCleanup,
1706
+ ...connections.map(connection => connection.settled),
1629
1707
  stopScanning(),
1630
1708
  ...pendingPeripherals.map(peripheral => runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
1631
1709
  timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
@@ -1646,6 +1724,7 @@ function disposeNobleBleSupport(releaseNoble = instance => instance.stop()) {
1646
1724
  }
1647
1725
  finally {
1648
1726
  clearTimeout(timeout);
1727
+ nativeReleased = true;
1649
1728
  cleanupNobleListeners();
1650
1729
  if (instance && persistentStateListener) {
1651
1730
  instance.removeListener('stateChange', persistentStateListener);
@@ -1 +1 @@
1
- {"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAA+B,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,KAAK,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAiB1F,KAAK,mBAAmB,GAAG,KAAK,GAAG;IACjC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,mBAAmB,EAAE,aAAa,SAAK,+CAiB3F;AAED,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,OAAO,GAAG,wBAAwB,CAgCvF;AAmJD,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE,oBAAoB,UAI7E;AAsxDD,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CA+MpE;AAOD,wBAAgB,sBAAsB,CACpC,YAAY,GAAE,CAAC,QAAQ,EAAE;IAAE,IAAI,IAAI,IAAI,CAAA;CAAE,KAAK,IAAkC,GAC/E,OAAO,CAAC,IAAI,CAAC,CAqDf"}
1
+ {"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAA+B,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,KAAK,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAgC1F,KAAK,mBAAmB,GAAG,KAAK,GAAG;IACjC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,mBAAmB,EAAE,aAAa,SAAK,+CAiB3F;AAED,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,OAAO,GAAG,wBAAwB,CAgCvF;AA+KD,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE,oBAAoB,UAI7E;AA4xDD,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CA+NpE;AAOD,wBAAgB,sBAAsB,CACpC,YAAY,GAAE,CAAC,QAAQ,EAAE;IAAE,IAAI,IAAI,IAAI,CAAA;CAAE,KAAK,IAAkC,GAC/E,OAAO,CAAC,IAAI,CAAC,CAiEf"}
@@ -15,6 +15,7 @@ export interface NobleModule {
15
15
  startScanning(serviceUUIDs: string[], allowDuplicates: boolean, callback?: (error?: Error) => void): void;
16
16
  stopScanning(callback?: () => void): void;
17
17
  stop(): void;
18
+ cancelConnect?(id: string): void;
18
19
  on(event: 'stateChange', listener: (state: string) => void): void;
19
20
  on(event: 'discover', listener: (peripheral: Peripheral) => void): void;
20
21
  removeListener(event: 'stateChange', listener: (state: string) => void): void;
@@ -1 +1 @@
1
- {"version":3,"file":"noble-extended.d.ts","sourceRoot":"","sources":["../../src/types/noble-extended.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAGnE,MAAM,WAAW,UAAW,SAAQ,oBAAoB;IACtD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAGD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,cAAc,CAAC;CACxB;AAGD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CACX,YAAY,EAAE,MAAM,EAAE,EACtB,eAAe,EAAE,OAAO,EACxB,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,GACjC,IAAI,CAAC;IACR,YAAY,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC1C,IAAI,IAAI,IAAI,CAAC;IACb,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAClE,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;IACxE,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAC9E,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;CACrF;AAGD,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC7C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;CAC9C;AAGD,wBAAgB,OAAO,CACrB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,EACjC,OAAO,EAAE,MAAM,EACf,GAAG,IAAI,EAAE,GAAG,EAAE,GACb,IAAI,CAMN"}
1
+ {"version":3,"file":"noble-extended.d.ts","sourceRoot":"","sources":["../../src/types/noble-extended.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAGnE,MAAM,WAAW,UAAW,SAAQ,oBAAoB;IACtD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAGD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,cAAc,CAAC;CACxB;AAGD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CACX,YAAY,EAAE,MAAM,EAAE,EACtB,eAAe,EAAE,OAAO,EACxB,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,GACjC,IAAI,CAAC;IACR,YAAY,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC1C,IAAI,IAAI,IAAI,CAAC;IACb,aAAa,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAClE,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;IACxE,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAC9E,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;CACrF;AAGD,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC7C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;CAC9C;AAGD,wBAAgB,OAAO,CACrB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,EACjC,OAAO,EAAE,MAAM,EACf,GAAG,IAAI,EAAE,GAAG,EAAE,GACb,IAAI,CAMN"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-electron",
3
- "version": "1.2.2-alpha.102",
3
+ "version": "1.2.2-alpha.103",
4
4
  "author": "OneKey",
5
5
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
6
6
  "license": "MIT",
@@ -25,9 +25,9 @@
25
25
  "electron-log": ">=4.0.0"
26
26
  },
27
27
  "dependencies": {
28
- "@onekeyfe/hd-core": "1.2.2-alpha.102",
29
- "@onekeyfe/hd-shared": "1.2.2-alpha.102",
30
- "@onekeyfe/hd-transport": "1.2.2-alpha.102",
28
+ "@onekeyfe/hd-core": "1.2.2-alpha.103",
29
+ "@onekeyfe/hd-shared": "1.2.2-alpha.103",
30
+ "@onekeyfe/hd-transport": "1.2.2-alpha.103",
31
31
  "@stoprocent/noble": "2.3.16",
32
32
  "p-retry": "^4.6.2"
33
33
  },
@@ -36,5 +36,5 @@
36
36
  "electron": "^25.0.0",
37
37
  "typescript": "^5.3.3"
38
38
  },
39
- "gitHead": "abddc95e03d794d9c6fce7765d59de1f48a684d0"
39
+ "gitHead": "cd5aaa8a42a61c93f28b7d4931081fb7cd652e26"
40
40
  }
@@ -750,6 +750,10 @@ describe('Electron Noble BLE device discovery', () => {
750
750
  });
751
751
 
752
752
  describe('Noble BLE process shutdown', () => {
753
+ const flushCallbacks = () =>
754
+ new Promise<void>(resolve => {
755
+ setImmediate(resolve);
756
+ });
753
757
  afterEach(() => {
754
758
  jest.useRealTimers();
755
759
  jest.resetModules();
@@ -825,16 +829,20 @@ describe('Noble BLE process shutdown', () => {
825
829
  expect(jest.getTimerCount()).toBe(0);
826
830
  });
827
831
 
828
- test('disconnects an in-flight connection and rejects its late success', async () => {
832
+ test('cancels native connects and waits for disconnect before release', async () => {
829
833
  const { sdk, native, handlers } = await setup();
830
834
  await handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK)?.({});
831
- let connectCallback: (() => void) | undefined;
835
+ let connectCallback: ((error?: Error) => void) | undefined;
836
+ let disconnectCallback: (() => void) | undefined;
832
837
  const peripheral = Object.assign(new EventEmitter(), {
833
838
  ...createPeripheral('pending-device', 'OneKey Pro'),
834
839
  connect: jest.fn(callback => {
835
840
  connectCallback = callback;
836
841
  }),
837
- disconnect: jest.fn(callback => callback?.()),
842
+ cancelConnect: jest.fn(() => connectCallback?.(new Error('connection canceled'))),
843
+ disconnect: jest.fn(callback => {
844
+ disconnectCallback = callback;
845
+ }),
838
846
  discoverServices: jest.fn(),
839
847
  });
840
848
  native.emit('discover', peripheral);
@@ -842,14 +850,70 @@ describe('Noble BLE process shutdown', () => {
842
850
  await Promise.resolve();
843
851
  await Promise.resolve();
844
852
  expect(peripheral.connect).toHaveBeenCalledTimes(1);
845
- await sdk.disposeNobleBleSupport();
853
+ const disposing = sdk.disposeNobleBleSupport();
854
+ await flushCallbacks();
855
+ expect(peripheral.cancelConnect).toHaveBeenCalledTimes(1);
846
856
  expect(peripheral.disconnect).toHaveBeenCalled();
857
+ expect(native.stop).not.toHaveBeenCalled();
858
+ disconnectCallback?.();
859
+ await disposing;
847
860
  expect(native.stop).toHaveBeenCalledTimes(1);
848
- connectCallback?.();
849
861
  expect(await connecting).toMatchObject({ success: false });
850
862
  expect(peripheral.discoverServices).not.toHaveBeenCalled();
851
863
  });
852
864
 
865
+ test.each(['peripheral', 'direct'])(
866
+ 'keeps late %s connects inert after the disposal timeout',
867
+ async route => {
868
+ jest.useFakeTimers({ doNotFake: ['performance', 'setImmediate'] });
869
+ const { sdk, native, handlers } = await setup();
870
+ await handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK)?.({});
871
+ let finishConnect: () => void = () => undefined;
872
+ const peripheral = Object.assign(new EventEmitter(), {
873
+ ...createPeripheral('late-device', 'OneKey Pro'),
874
+ connect: jest.fn(callback => {
875
+ finishConnect = () => callback();
876
+ }),
877
+ cancelConnect: jest.fn(),
878
+ disconnect: jest.fn(callback => callback?.()),
879
+ discoverServices: jest.fn(),
880
+ });
881
+ const connectAsync = jest.fn(
882
+ () =>
883
+ new Promise(resolve => {
884
+ finishConnect = () => resolve(peripheral);
885
+ })
886
+ );
887
+ const cancelConnect = jest.fn();
888
+ if (route === 'direct') Object.assign(native, { connectAsync, cancelConnect });
889
+ else native.emit('discover', peripheral);
890
+ const connecting = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT)?.({}, peripheral.id);
891
+ await flushCallbacks();
892
+ if (route === 'direct') {
893
+ jest.advanceTimersByTime(1500);
894
+ await flushCallbacks();
895
+ }
896
+ expect(route === 'direct' ? connectAsync : peripheral.connect).toHaveBeenCalledTimes(1);
897
+ const disposing = sdk.disposeNobleBleSupport();
898
+ await flushCallbacks();
899
+ expect(route === 'direct' ? cancelConnect : peripheral.cancelConnect).toHaveBeenCalledTimes(
900
+ 1
901
+ );
902
+ expect(native.stop).not.toHaveBeenCalled();
903
+ jest.advanceTimersByTime(3500);
904
+ await disposing;
905
+ const disconnects = peripheral.disconnect.mock.calls.length;
906
+ peripheral.state = 'connected';
907
+ finishConnect();
908
+ await flushCallbacks();
909
+ expect(peripheral.disconnect).toHaveBeenCalledTimes(disconnects);
910
+ expect(peripheral.discoverServices).not.toHaveBeenCalled();
911
+ expect(native.stop).toHaveBeenCalledTimes(1);
912
+ expect(await connecting).toMatchObject({ success: false });
913
+ expect(jest.getTimerCount()).toBe(0);
914
+ }
915
+ );
916
+
853
917
  test('allows a host to defer shared native release until both transports are idle', async () => {
854
918
  const { sdk, native, handlers } = await setup();
855
919
  await handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK)?.({});
@@ -859,14 +923,26 @@ describe('Noble BLE process shutdown', () => {
859
923
  expect(native.stop).not.toHaveBeenCalled();
860
924
  });
861
925
 
862
- test('window destruction preserves the native manager for a soft restart', async () => {
926
+ test('replacement window scanning waits for old cleanup and preserves native', async () => {
927
+ jest.useFakeTimers({ doNotFake: ['performance', 'setImmediate'] });
863
928
  const { sdk, native, handlers, window } = await setup();
864
929
  await handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK)?.({});
930
+ let finishOldCleanup: (() => void) | undefined;
931
+ native.stopScanning.mockImplementationOnce(callback => {
932
+ finishOldCleanup = callback;
933
+ });
865
934
  window.emit('destroyed');
866
- await Promise.resolve();
867
- await Promise.resolve();
868
935
  sdk.setupNobleBleHandlers(new EventEmitter() as unknown as WebContents);
869
- await handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK)?.({});
936
+ const scan = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE)?.({});
937
+ await flushCallbacks();
938
+ expect(native.startScanning).not.toHaveBeenCalled();
939
+ finishOldCleanup?.();
940
+ await flushCallbacks();
941
+ expect(native.startScanning).toHaveBeenCalledTimes(1);
942
+ expect(native.listenerCount('discover')).toBe(1);
943
+ native.emit('discover', createPeripheral('replacement-device', 'OneKey Pro'));
944
+ jest.advanceTimersByTime(5000);
945
+ expect(await scan).toEqual([expect.objectContaining({ id: 'replacement-device' })]);
870
946
  expect(native.stop).not.toHaveBeenCalled();
871
947
  await sdk.disposeNobleBleSupport();
872
948
  expect(native.stop).toHaveBeenCalledTimes(1);
@@ -41,9 +41,24 @@ import type { CharacteristicPair, DeviceInfo, Logger, NobleModule } from './type
41
41
  let noble: NobleModule | null = null;
42
42
  let logger: Logger | null = null;
43
43
  let disposing = false;
44
+ let nativeReleased = false;
44
45
  let disposePromise: Promise<void> | undefined;
46
+ let windowCleanup: Promise<void> | undefined;
45
47
  let removeIpcHandlers: (() => void) | undefined;
46
48
  const pendingCancellations = new Set<() => void>();
49
+ const nativeConnections = new Set<{ cancel: () => void; settled: Promise<void> }>();
50
+
51
+ function trackNativeConnection(operation: Promise<unknown>, cancel: () => void): void {
52
+ const connection = {
53
+ cancel,
54
+ settled: operation.then(
55
+ () => undefined,
56
+ () => undefined
57
+ ),
58
+ };
59
+ nativeConnections.add(connection);
60
+ connection.settled = connection.settled.finally(() => nativeConnections.delete(connection));
61
+ }
47
62
 
48
63
  function assertBleActive(): void {
49
64
  if (disposing) {
@@ -170,9 +185,24 @@ function connectPeripheralWithCancellation(
170
185
  );
171
186
  connectingDevices.set(deviceId, pendingConnection);
172
187
 
173
- peripheral.connect(error => {
188
+ let completeNative!: () => void;
189
+ trackNativeConnection(
190
+ new Promise<void>(resolve => {
191
+ completeNative = resolve;
192
+ }),
193
+ () => {
194
+ if (typeof peripheral.cancelConnect === 'function') peripheral.cancelConnect();
195
+ else noble?.cancelConnect?.(deviceId);
196
+ }
197
+ );
198
+ const onConnect = (error?: Error | null) => {
199
+ if (nativeReleased) {
200
+ completeNative();
201
+ return;
202
+ }
174
203
  if (error) {
175
204
  settle(() => reject(createNobleBleConnectionError(error, messagePrefix)));
205
+ completeNative();
176
206
  return;
177
207
  }
178
208
 
@@ -186,13 +216,26 @@ function connectPeripheralWithCancellation(
186
216
  if (!activeConnection || activeConnection.peripheral !== peripheral) {
187
217
  try {
188
218
  peripheral.removeAllListeners('disconnect');
189
- peripheral.disconnect(() => undefined);
219
+ runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
220
+ timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
221
+ timeoutBehavior: 'resolve',
222
+ })
223
+ .catch(() => undefined)
224
+ .finally(completeNative);
225
+ return;
190
226
  } catch {
191
227
  // Best-effort cleanup for a stale native callback.
192
228
  }
193
229
  }
194
230
  }
195
- });
231
+ completeNative();
232
+ };
233
+ try {
234
+ peripheral.connect(onConnect);
235
+ } catch (error) {
236
+ completeNative();
237
+ settle(() => reject(error));
238
+ }
196
239
  });
197
240
  }
198
241
  const pairedDevices = new Set<string>(); // Windows BLE device pairing status tracking
@@ -1199,7 +1242,7 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
1199
1242
 
1200
1243
  // Stop scanning
1201
1244
  async function stopScanning(): Promise<void> {
1202
- if (!noble) return;
1245
+ if (!noble || nativeReleased) return;
1203
1246
  const nobleInstance = noble;
1204
1247
  await waitForNobleScanStop(nobleInstance);
1205
1248
  logger?.info('[NobleBLE] Scanning stopped');
@@ -1651,13 +1694,31 @@ async function tryDirectConnectById(deviceId: string): Promise<Peripheral | unde
1651
1694
 
1652
1695
  let timer: ReturnType<typeof setTimeout> | undefined;
1653
1696
  let cancel: (() => void) | undefined;
1697
+ let abandoned = false;
1654
1698
  try {
1655
1699
  // The late-orphan guard must attach to THIS pending connect.
1656
1700
  const directPromise = nobleInstance.connectAsync(deviceId);
1701
+ trackNativeConnection(
1702
+ directPromise.then(async late => {
1703
+ if ((!abandoned && !disposing) || nativeReleased) return;
1704
+ const peripheral = late ?? discoveredDevices.get(deviceId);
1705
+ if (peripheral?.state === 'connected' && !connectedDevices.has(deviceId)) {
1706
+ peripheral.removeAllListeners('disconnect');
1707
+ await runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
1708
+ timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
1709
+ timeoutBehavior: 'resolve',
1710
+ });
1711
+ }
1712
+ }),
1713
+ () => nobleInstance.cancelConnect?.(deviceId)
1714
+ );
1657
1715
  const raced = await Promise.race([
1658
1716
  directPromise,
1659
1717
  new Promise<'timeout'>(resolve => {
1660
- cancel = () => resolve('timeout');
1718
+ cancel = () => {
1719
+ abandoned = true;
1720
+ resolve('timeout');
1721
+ };
1661
1722
  pendingCancellations.add(cancel);
1662
1723
  timer = setTimeout(cancel, DIRECT_CONNECT_TIMEOUT_MS);
1663
1724
  }),
@@ -1667,20 +1728,6 @@ async function tryDirectConnectById(deviceId: string): Promise<Peripheral | unde
1667
1728
  logger?.info('[NobleBLE] Direct connect-by-id timed out, falling back to scan', {
1668
1729
  deviceId,
1669
1730
  });
1670
- // Promise.race times out the caller only; a late success orphans the link.
1671
- directPromise
1672
- .then(late => {
1673
- const latePeripheral = late ?? discoveredDevices.get(deviceId);
1674
- if (
1675
- latePeripheral &&
1676
- latePeripheral.state === 'connected' &&
1677
- !connectedDevices.has(deviceId)
1678
- ) {
1679
- latePeripheral.removeAllListeners('disconnect');
1680
- latePeripheral.disconnect(() => undefined);
1681
- }
1682
- })
1683
- .catch(() => undefined);
1684
1731
  return undefined;
1685
1732
  }
1686
1733
  // Backends emit `discover` as a side effect, so the cache may hold it.
@@ -1877,6 +1924,7 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
1877
1924
 
1878
1925
  // Disconnect device
1879
1926
  async function disconnectDevice(deviceId: string): Promise<void> {
1927
+ if (nativeReleased) return;
1880
1928
  const pendingConnection = connectingDevices.get(deviceId);
1881
1929
  const peripheral = connectedDevices.get(deviceId) ?? pendingConnection?.peripheral;
1882
1930
  if (!peripheral) {
@@ -1917,6 +1965,7 @@ async function disconnectDevice(deviceId: string): Promise<void> {
1917
1965
 
1918
1966
  // Unsubscribe from notifications
1919
1967
  async function unsubscribeNotifications(deviceId: string): Promise<void> {
1968
+ if (nativeReleased) return;
1920
1969
  const peripheral = connectedDevices.get(deviceId);
1921
1970
  const characteristics = deviceCharacteristics.get(deviceId);
1922
1971
 
@@ -2074,6 +2123,7 @@ async function subscribeNotifications(
2074
2123
  // Setup IPC handlers
2075
2124
  export function setupNobleBleHandlers(webContents: WebContents): void {
2076
2125
  if (disposing) return;
2126
+ let windowDestroyed = false;
2077
2127
  try {
2078
2128
  // @ts-ignore – electron-log is only available at runtime
2079
2129
  // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
@@ -2092,7 +2142,15 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
2092
2142
  ipcMain.removeHandler(channel);
2093
2143
  ipcMain.handle(channel, async (...args) => {
2094
2144
  try {
2145
+ // A replacement renderer must not race the old renderer's shared-state cleanup.
2146
+ if (windowCleanup) await windowCleanup;
2095
2147
  assertBleActive();
2148
+ if (windowDestroyed) {
2149
+ throw ERRORS.TypedError(
2150
+ HardwareErrorCode.BleDeviceDisconnected,
2151
+ 'BLE window destroyed'
2152
+ );
2153
+ }
2096
2154
  return await Promise.resolve(listener(...args));
2097
2155
  } catch (error) {
2098
2156
  return createNobleBleIpcErrorResponse(error);
@@ -2250,15 +2308,22 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
2250
2308
 
2251
2309
  // Window cleanup also runs during renderer soft restart; native release belongs to app quit.
2252
2310
  webContents.on('destroyed', () => {
2311
+ windowDestroyed = true;
2253
2312
  if (disposing) return;
2254
2313
  safeLog(logger, 'info', 'Cleaning up Noble BLE handlers');
2255
- (async () => {
2314
+ const previousCleanup = windowCleanup;
2315
+ windowCleanup = (async () => {
2316
+ if (previousCleanup) await previousCleanup;
2317
+ if (disposing) return;
2256
2318
  const deviceIds = Array.from(connectedDevices.keys());
2257
2319
  for (const deviceId of deviceIds) {
2320
+ if (disposing) return;
2258
2321
  await unsubscribeNotifications(deviceId).catch(() => undefined);
2322
+ if (disposing) return;
2259
2323
  await disconnectDevice(deviceId).catch(() => undefined);
2260
2324
  }
2261
2325
 
2326
+ if (disposing) return;
2262
2327
  await stopScanning().catch(() => undefined);
2263
2328
  // persistentStateListener is process-lifetime, NOT per-window: this
2264
2329
  // destroy handler also fires on a renderer soft restart, and removing
@@ -2302,11 +2367,21 @@ export function disposeNobleBleSupport(
2302
2367
  ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected, 'Noble BLE is shutting down')
2303
2368
  );
2304
2369
  }
2370
+ const connections = Array.from(nativeConnections);
2371
+ for (const connection of connections) {
2372
+ try {
2373
+ connection.cancel();
2374
+ } catch (error) {
2375
+ logger?.warn('[NobleBLE] Native connect cancellation failed', error);
2376
+ }
2377
+ }
2305
2378
  let timeout: ReturnType<typeof setTimeout> | undefined;
2306
2379
  disposePromise = (async () => {
2307
2380
  try {
2308
2381
  await Promise.race([
2309
2382
  Promise.allSettled([
2383
+ windowCleanup,
2384
+ ...connections.map(connection => connection.settled),
2310
2385
  stopScanning(),
2311
2386
  ...pendingPeripherals.map(peripheral =>
2312
2387
  runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
@@ -2328,6 +2403,8 @@ export function disposeNobleBleSupport(
2328
2403
  ]);
2329
2404
  } finally {
2330
2405
  clearTimeout(timeout);
2406
+ // Bounded cleanup may finish before a broken native callback; it must stay inert afterwards.
2407
+ nativeReleased = true;
2331
2408
  cleanupNobleListeners();
2332
2409
  if (instance && persistentStateListener) {
2333
2410
  instance.removeListener('stateChange', persistentStateListener);
@@ -30,6 +30,7 @@ export interface NobleModule {
30
30
  ): void;
31
31
  stopScanning(callback?: () => void): void;
32
32
  stop(): void;
33
+ cancelConnect?(id: string): void;
33
34
  on(event: 'stateChange', listener: (state: string) => void): void;
34
35
  on(event: 'discover', listener: (peripheral: Peripheral) => void): void;
35
36
  removeListener(event: 'stateChange', listener: (state: string) => void): void;