@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.
- package/dist/{index-8e6acfc1.js → index-052b4b5b.js} +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/{noble-ble-handler-3a1d131d.js → noble-ble-handler-eff26dd3.js} +97 -18
- package/dist/noble-ble-handler.d.ts.map +1 -1
- package/dist/types/noble-extended.d.ts +1 -0
- package/dist/types/noble-extended.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/noble-ble-handler.test.ts +85 -9
- package/src/noble-ble-handler.ts +97 -20
- package/src/types/noble-extended.ts +1 -0
|
@@ -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-
|
|
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-
|
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var index = require('./index-
|
|
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
|
-
|
|
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(() =>
|
|
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 = () =>
|
|
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
|
-
|
|
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;
|
|
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.
|
|
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.
|
|
29
|
-
"@onekeyfe/hd-shared": "1.2.2-alpha.
|
|
30
|
-
"@onekeyfe/hd-transport": "1.2.2-alpha.
|
|
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": "
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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);
|
package/src/noble-ble-handler.ts
CHANGED
|
@@ -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
|
-
|
|
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(() =>
|
|
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 = () =>
|
|
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
|
-
|
|
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;
|