@onekeyfe/hd-transport-electron 1.2.2-alpha.101 → 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.
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-d51c7da6.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
 
@@ -100,9 +100,30 @@ const NOBLE_BLE_SUBSCRIBE_TIMEOUT_MS = 10000;
100
100
 
101
101
  let noble = null;
102
102
  let logger = null;
103
+ let disposing = false;
104
+ let nativeReleased = false;
105
+ let disposePromise;
106
+ let windowCleanup;
107
+ let removeIpcHandlers;
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
+ }
118
+ function assertBleActive() {
119
+ if (disposing) {
120
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, 'Noble BLE is shutting down');
121
+ }
122
+ }
103
123
  function createNobleBleConnectionError(error, messagePrefix = '') {
104
124
  const errorMessage = error.message;
105
- const isInvalidMacOsBond = error.nativeErrorCode === 14 && error.nativeErrorDomain === 'CBErrorDomain';
125
+ const isInvalidMacOsBond = (error.nativeErrorCode === 14 && error.nativeErrorDomain === 'CBErrorDomain') ||
126
+ (error.nativeErrorCode === 15 && error.nativeErrorDomain === 'CBATTErrorDomain');
106
127
  if (isInvalidMacOsBond) {
107
128
  const nativeErrorMessage = `${messagePrefix}${errorMessage}`;
108
129
  return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleBondInvalid, `${hdShared.HardwareErrorCodeMessage[hdShared.HardwareErrorCode.BleBondInvalid]} (${nativeErrorMessage})`, {
@@ -143,6 +164,86 @@ let persistentStateListener = null;
143
164
  let persistentDiscoverListener = null;
144
165
  const discoveredDevices = new Map();
145
166
  const connectedDevices = new Map();
167
+ const connectingDevices = new Map();
168
+ let nextConnectionGeneration = 0;
169
+ function connectPeripheralWithCancellation(peripheral, deviceId, messagePrefix = '') {
170
+ assertBleActive();
171
+ return new Promise((resolve, reject) => {
172
+ var _a;
173
+ const generation = ++nextConnectionGeneration;
174
+ let settled = false;
175
+ const settle = (settler) => {
176
+ var _a;
177
+ if (settled)
178
+ return false;
179
+ settled = true;
180
+ if (((_a = connectingDevices.get(deviceId)) === null || _a === void 0 ? void 0 : _a.generation) === generation) {
181
+ connectingDevices.delete(deviceId);
182
+ }
183
+ settler();
184
+ return true;
185
+ };
186
+ const pendingConnection = {
187
+ peripheral,
188
+ generation,
189
+ cancel: error => {
190
+ settle(() => reject(error));
191
+ },
192
+ };
193
+ (_a = connectingDevices
194
+ .get(deviceId)) === null || _a === void 0 ? void 0 : _a.cancel(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, `Device ${deviceId} connect attempt was superseded`));
195
+ connectingDevices.set(deviceId, pendingConnection);
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
+ }
211
+ if (error) {
212
+ settle(() => reject(createNobleBleConnectionError(error, messagePrefix)));
213
+ completeNative();
214
+ return;
215
+ }
216
+ if (!settle(() => {
217
+ connectedDevices.set(deviceId, peripheral);
218
+ resolve();
219
+ })) {
220
+ const activeConnection = connectingDevices.get(deviceId);
221
+ if (!activeConnection || activeConnection.peripheral !== peripheral) {
222
+ try {
223
+ peripheral.removeAllListeners('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;
231
+ }
232
+ catch (_a) {
233
+ }
234
+ }
235
+ }
236
+ completeNative();
237
+ };
238
+ try {
239
+ peripheral.connect(onConnect);
240
+ }
241
+ catch (error) {
242
+ completeNative();
243
+ settle(() => reject(error));
244
+ }
245
+ });
246
+ }
146
247
  const pairedDevices = new Set();
147
248
  const deviceCharacteristics = new Map();
148
249
  const notificationCallbacks = new Map();
@@ -281,6 +382,7 @@ function updateBluetoothState(state) {
281
382
  }
282
383
  function initializeNoble() {
283
384
  return index.__awaiter(this, void 0, void 0, function* () {
385
+ assertBleActive();
284
386
  if (noble)
285
387
  return;
286
388
  try {
@@ -297,9 +399,11 @@ function initializeNoble() {
297
399
  return;
298
400
  }
299
401
  const timeout = setTimeout(() => {
402
+ cleanup();
300
403
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout'));
301
404
  }, BLUETOOTH_INIT_TIMEOUT);
302
405
  const cleanup = () => {
406
+ pendingCancellations.delete(cancel);
303
407
  clearTimeout(timeout);
304
408
  if (noble) {
305
409
  noble.removeListener('stateChange', onStateChange);
@@ -324,8 +428,14 @@ function initializeNoble() {
324
428
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError));
325
429
  }
326
430
  };
431
+ const cancel = () => {
432
+ cleanup();
433
+ reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, 'Noble BLE is shutting down'));
434
+ };
435
+ pendingCancellations.add(cancel);
327
436
  noble.on('stateChange', onStateChange);
328
437
  });
438
+ assertBleActive();
329
439
  if (!persistentDiscoverListener) {
330
440
  persistentDiscoverListener = (peripheral) => {
331
441
  handleDeviceDiscovered(peripheral);
@@ -372,6 +482,8 @@ function broadcastToAllWebContents(channel, payload) {
372
482
  }
373
483
  function armIdleDisconnect(deviceId, ms = BLE_IDLE_DISCONNECT_MS, reason = 'idle') {
374
484
  clearIdleDisconnect(deviceId);
485
+ if (disposing)
486
+ return;
375
487
  idleDisconnectTimers.set(deviceId, setTimeout(() => {
376
488
  var _a;
377
489
  idleDisconnectTimers.delete(deviceId);
@@ -407,7 +519,7 @@ function armIdleDisconnect(deviceId, ms = BLE_IDLE_DISCONNECT_MS, reason = 'idle
407
519
  }, ms));
408
520
  }
409
521
  function cleanupDevice(deviceId, webContents, options = {}) {
410
- var _a;
522
+ var _a, _b;
411
523
  const { cleanupConnection = true, cleanupDiscoveredCache = false, sendDisconnectEvent = false, cancelOperations = true, reason = 'unknown', disconnectReason = hdShared.EBleDisconnectReason.DeviceDisconnected, } = options;
412
524
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Starting device cleanup', {
413
525
  deviceId,
@@ -421,6 +533,8 @@ function cleanupDevice(deviceId, webContents, options = {}) {
421
533
  const peripheral = connectedDevices.get(deviceId);
422
534
  const deviceName = ((_a = peripheral === null || peripheral === void 0 ? void 0 : peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device';
423
535
  if (cleanupConnection) {
536
+ (_b = connectingDevices
537
+ .get(deviceId)) === null || _b === void 0 ? void 0 : _b.cancel(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, `Device ${deviceId} disconnected while connecting`));
424
538
  const disconnectEntry = deviceDisconnectListeners.get(deviceId);
425
539
  if (disconnectEntry) {
426
540
  disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
@@ -499,6 +613,7 @@ function setupMtuListener(peripheral, deviceId, webContents) {
499
613
  }
500
614
  function writeCharacteristicWithoutResponse(deviceId, writeCharacteristic, buffer) {
501
615
  return index.__awaiter(this, void 0, void 0, function* () {
616
+ assertBleActive();
502
617
  return new Promise((resolve, reject) => {
503
618
  writeCharacteristic.write(buffer, true, (error) => {
504
619
  if (error) {
@@ -701,6 +816,7 @@ function waitForNobleScanStop(nobleInstance) {
701
816
  }
702
817
  function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS) {
703
818
  return index.__awaiter(this, void 0, void 0, function* () {
819
+ assertBleActive();
704
820
  if (!noble) {
705
821
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
706
822
  }
@@ -712,6 +828,7 @@ function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN
712
828
  if (settled)
713
829
  return;
714
830
  settled = true;
831
+ pendingCancellations.delete(cancel);
715
832
  if (timeoutId)
716
833
  clearTimeout(timeoutId);
717
834
  nobleInstance.removeListener('discover', onDiscover);
@@ -721,6 +838,7 @@ function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN
721
838
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, error.message));
722
839
  return;
723
840
  }
841
+ assertBleActive();
724
842
  if (peripheral) {
725
843
  discoveredDevices.set(peripheral.id, peripheral);
726
844
  }
@@ -740,6 +858,10 @@ function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN
740
858
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
741
859
  finish(null).catch(reject);
742
860
  }, timeoutMs);
861
+ const cancel = () => {
862
+ finish(null, new Error('Noble BLE is shutting down')).catch(reject);
863
+ };
864
+ pendingCancellations.add(cancel);
743
865
  nobleInstance.on('discover', onDiscover);
744
866
  nobleInstance.startScanning([], true, (error) => {
745
867
  if (error) {
@@ -759,14 +881,18 @@ function enumerateDevices() {
759
881
  if (!noble) {
760
882
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
761
883
  }
884
+ assertBleActive();
762
885
  const nobleInstance = noble;
763
886
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Starting device enumeration');
764
887
  discoveredDevices.clear();
765
888
  ensureDiscoverListener();
766
889
  return new Promise((resolve, reject) => {
767
890
  const devices = [];
891
+ let settled = false;
768
892
  let intervalId;
769
893
  const cleanup = () => index.__awaiter(this, void 0, void 0, function* () {
894
+ settled = true;
895
+ pendingCancellations.delete(cancel);
770
896
  clearTimeout(timeoutId);
771
897
  if (intervalId)
772
898
  clearInterval(intervalId);
@@ -788,13 +914,21 @@ function enumerateDevices() {
788
914
  connectedDevices.forEach(pushDevice);
789
915
  };
790
916
  const timeoutId = setTimeout(() => index.__awaiter(this, void 0, void 0, function* () {
917
+ if (settled)
918
+ return;
791
919
  checkDevices();
792
920
  yield cleanup();
793
921
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scan completed, found devices:', devices.length);
794
922
  resolve(devices);
795
923
  }), DEVICE_SCAN_TIMEOUT);
924
+ const cancel = () => {
925
+ cleanup().then(() => resolve([]), reject);
926
+ };
927
+ pendingCancellations.add(cancel);
796
928
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning for OneKey BLE devices');
797
929
  nobleInstance.startScanning([], true, (error) => index.__awaiter(this, void 0, void 0, function* () {
930
+ if (settled)
931
+ return;
798
932
  if (error) {
799
933
  yield cleanup();
800
934
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to start scanning:', error);
@@ -809,7 +943,7 @@ function enumerateDevices() {
809
943
  }
810
944
  function stopScanning() {
811
945
  return index.__awaiter(this, void 0, void 0, function* () {
812
- if (!noble)
946
+ if (!noble || nativeReleased)
813
947
  return;
814
948
  const nobleInstance = noble;
815
949
  yield waitForNobleScanStop(nobleInstance);
@@ -853,13 +987,16 @@ function getDevice(deviceId) {
853
987
  }
854
988
  function discoverServicesAndCharacteristics(peripheral) {
855
989
  return index.__awaiter(this, void 0, void 0, function* () {
990
+ assertBleActive();
856
991
  let timeoutId;
857
992
  let onDisconnect;
858
993
  const cleanup = () => {
859
994
  if (timeoutId)
860
995
  clearTimeout(timeoutId);
861
- if (onDisconnect)
996
+ if (onDisconnect) {
862
997
  peripheral.removeListener('disconnect', onDisconnect);
998
+ pendingCancellations.delete(onDisconnect);
999
+ }
863
1000
  };
864
1001
  const timeoutPromise = new Promise((_, reject) => {
865
1002
  timeoutId = setTimeout(() => {
@@ -872,6 +1009,7 @@ function discoverServicesAndCharacteristics(peripheral) {
872
1009
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Device disconnected during service discovery');
873
1010
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'Device disconnected during service discovery'));
874
1011
  };
1012
+ pendingCancellations.add(onDisconnect);
875
1013
  peripheral.once('disconnect', onDisconnect);
876
1014
  });
877
1015
  const discoveryPromise = (() => index.__awaiter(this, void 0, void 0, function* () {
@@ -886,6 +1024,7 @@ function discoverServicesAndCharacteristics(peripheral) {
886
1024
  }
887
1025
  });
888
1026
  });
1027
+ assertBleActive();
889
1028
  if (!services || services.length === 0) {
890
1029
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'No OneKey services found');
891
1030
  }
@@ -958,17 +1097,8 @@ function forceReconnectPeripheral(peripheral, deviceId) {
958
1097
  callback();
959
1098
  }), { timeoutMs: BLE_CLEANUP_TIMEOUT, timeoutBehavior: 'resolve' });
960
1099
  }
961
- yield new Promise((resolve, reject) => {
962
- peripheral.connect(error => {
963
- if (error) {
964
- reject(createNobleBleConnectionError(error));
965
- return;
966
- }
967
- resolve();
968
- });
969
- });
1100
+ yield connectPeripheralWithCancellation(peripheral, deviceId);
970
1101
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Force reconnect successful');
971
- connectedDevices.set(deviceId, peripheral);
972
1102
  yield hdShared.wait(500);
973
1103
  });
974
1104
  }
@@ -985,17 +1115,7 @@ function freshScanAndDiscover(deviceId, webContents) {
985
1115
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found in fresh scan`);
986
1116
  }
987
1117
  discoveredDevices.set(deviceId, freshPeripheral);
988
- yield new Promise((resolve, reject) => {
989
- freshPeripheral.connect((error) => {
990
- if (error) {
991
- reject(createNobleBleConnectionError(error, 'Fresh peripheral connection failed: '));
992
- }
993
- else {
994
- connectedDevices.set(deviceId, freshPeripheral);
995
- resolve();
996
- }
997
- });
998
- });
1118
+ yield connectPeripheralWithCancellation(freshPeripheral, deviceId, 'Fresh peripheral connection failed: ');
999
1119
  setupDisconnectListener(freshPeripheral, deviceId, webContents);
1000
1120
  yield hdShared.wait(500);
1001
1121
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Attempting service discovery with fresh peripheral');
@@ -1034,6 +1154,7 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
1034
1154
  minTimeout: 500,
1035
1155
  maxTimeout: 3000,
1036
1156
  onFailedAttempt: error => {
1157
+ assertBleActive();
1037
1158
  logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] Service discovery attempt ${error.attemptNumber} failed:`, {
1038
1159
  message: error.message,
1039
1160
  retriesLeft: error.retriesLeft,
@@ -1049,11 +1170,13 @@ function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
1049
1170
  yield forceReconnectPeripheral(peripheral, deviceId);
1050
1171
  }
1051
1172
  catch (resetError) {
1052
- if (hdShared.isBleStaleBondHardwareError(resetError)) {
1173
+ if (hdShared.isBleStaleBondHardwareError(resetError) ||
1174
+ (resetError === null || resetError === void 0 ? void 0 : resetError.errorCode) === hdShared.HardwareErrorCode.BleDeviceDisconnected) {
1053
1175
  throw resetError;
1054
1176
  }
1055
1177
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError);
1056
1178
  }
1179
+ assertBleActive();
1057
1180
  setupDisconnectListener(peripheral, deviceId, webContents);
1058
1181
  try {
1059
1182
  return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
@@ -1076,36 +1199,46 @@ const directConnectCooldownUntil = new Map();
1076
1199
  function tryDirectConnectById(deviceId) {
1077
1200
  var _a;
1078
1201
  return index.__awaiter(this, void 0, void 0, function* () {
1202
+ assertBleActive();
1079
1203
  const nobleInstance = noble;
1080
1204
  if (!(nobleInstance === null || nobleInstance === void 0 ? void 0 : nobleInstance.connectAsync))
1081
1205
  return undefined;
1082
1206
  const cooldownUntil = (_a = directConnectCooldownUntil.get(deviceId)) !== null && _a !== void 0 ? _a : 0;
1083
1207
  if (Date.now() < cooldownUntil)
1084
1208
  return undefined;
1209
+ let timer;
1210
+ let cancel;
1211
+ let abandoned = false;
1085
1212
  try {
1086
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); });
1087
1226
  const raced = yield Promise.race([
1088
1227
  directPromise,
1089
1228
  new Promise(resolve => {
1090
- setTimeout(() => resolve('timeout'), DIRECT_CONNECT_TIMEOUT_MS);
1229
+ cancel = () => {
1230
+ abandoned = true;
1231
+ resolve('timeout');
1232
+ };
1233
+ pendingCancellations.add(cancel);
1234
+ timer = setTimeout(cancel, DIRECT_CONNECT_TIMEOUT_MS);
1091
1235
  }),
1092
1236
  ]);
1093
- if (raced === 'timeout') {
1237
+ if (raced === 'timeout' || disposing) {
1094
1238
  directConnectCooldownUntil.set(deviceId, Date.now() + DIRECT_CONNECT_COOLDOWN_MS);
1095
1239
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct connect-by-id timed out, falling back to scan', {
1096
1240
  deviceId,
1097
1241
  });
1098
- directPromise
1099
- .then(late => {
1100
- const latePeripheral = late !== null && late !== void 0 ? late : discoveredDevices.get(deviceId);
1101
- if (latePeripheral &&
1102
- latePeripheral.state === 'connected' &&
1103
- !connectedDevices.has(deviceId)) {
1104
- latePeripheral.removeAllListeners('disconnect');
1105
- latePeripheral.disconnect(() => undefined);
1106
- }
1107
- })
1108
- .catch(() => undefined);
1109
1242
  return undefined;
1110
1243
  }
1111
1244
  const peripheral = raced !== null && raced !== void 0 ? raced : discoveredDevices.get(deviceId);
@@ -1122,6 +1255,11 @@ function tryDirectConnectById(deviceId) {
1122
1255
  });
1123
1256
  return undefined;
1124
1257
  }
1258
+ finally {
1259
+ clearTimeout(timer);
1260
+ if (cancel)
1261
+ pendingCancellations.delete(cancel);
1262
+ }
1125
1263
  });
1126
1264
  }
1127
1265
  function connectDevice(deviceId, webContents) {
@@ -1174,6 +1312,7 @@ function connectDevice(deviceId, webContents) {
1174
1312
  peripheral = byIdFirst ? yield scanForPeripheral() : yield connectById();
1175
1313
  }
1176
1314
  }
1315
+ assertBleActive();
1177
1316
  if (!peripheral) {
1178
1317
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found`);
1179
1318
  }
@@ -1184,6 +1323,7 @@ function connectDevice(deviceId, webContents) {
1184
1323
  if (!connectedDevices.has(deviceId)) {
1185
1324
  connectedDevices.set(deviceId, peripheral);
1186
1325
  }
1326
+ assertBleActive();
1187
1327
  setupDisconnectListener(peripheral, deviceId, webContents);
1188
1328
  if (deviceCharacteristics.has(deviceId)) {
1189
1329
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device characteristics already available');
@@ -1208,6 +1348,7 @@ function connectDevice(deviceId, webContents) {
1208
1348
  }
1209
1349
  try {
1210
1350
  const characteristics = yield setupConnectionAndDiscoverServices(peripheral, deviceId, webContents);
1351
+ assertBleActive();
1211
1352
  deviceCharacteristics.set(deviceId, characteristics);
1212
1353
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device ready for communication:', deviceId);
1213
1354
  }
@@ -1218,38 +1359,33 @@ function connectDevice(deviceId, webContents) {
1218
1359
  }
1219
1360
  return;
1220
1361
  }
1221
- return new Promise((resolve, reject) => {
1222
- const connectedPeripheral = peripheral;
1223
- connectedPeripheral.connect((error) => index.__awaiter(this, void 0, void 0, function* () {
1224
- if (error) {
1225
- logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection failed:', error);
1226
- reject(createNobleBleConnectionError(error));
1227
- return;
1228
- }
1229
- logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connected to device:', deviceId);
1230
- connectedDevices.set(deviceId, connectedPeripheral);
1231
- try {
1232
- const characteristics = yield setupConnectionAndDiscoverServices(connectedPeripheral, deviceId, webContents);
1233
- deviceCharacteristics.set(deviceId, characteristics);
1234
- logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device ready for communication:', deviceId);
1235
- resolve();
1236
- }
1237
- catch (setupError) {
1238
- logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection setup failed:', setupError);
1239
- disconnectDevice(deviceId)
1240
- .catch(() => undefined)
1241
- .then(() => reject(setupError));
1242
- }
1243
- }));
1244
- });
1362
+ const connectedPeripheral = peripheral;
1363
+ yield connectPeripheralWithCancellation(connectedPeripheral, deviceId);
1364
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connected to device:', deviceId);
1365
+ try {
1366
+ const characteristics = yield setupConnectionAndDiscoverServices(connectedPeripheral, deviceId, webContents);
1367
+ assertBleActive();
1368
+ deviceCharacteristics.set(deviceId, characteristics);
1369
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device ready for communication:', deviceId);
1370
+ }
1371
+ catch (setupError) {
1372
+ logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection setup failed:', setupError);
1373
+ yield disconnectDevice(deviceId).catch(() => undefined);
1374
+ throw setupError;
1375
+ }
1245
1376
  });
1246
1377
  }
1247
1378
  function disconnectDevice(deviceId) {
1379
+ var _a;
1248
1380
  return index.__awaiter(this, void 0, void 0, function* () {
1249
- const peripheral = connectedDevices.get(deviceId);
1381
+ if (nativeReleased)
1382
+ return;
1383
+ const pendingConnection = connectingDevices.get(deviceId);
1384
+ const peripheral = (_a = connectedDevices.get(deviceId)) !== null && _a !== void 0 ? _a : pendingConnection === null || pendingConnection === void 0 ? void 0 : pendingConnection.peripheral;
1250
1385
  if (!peripheral) {
1251
1386
  return;
1252
1387
  }
1388
+ pendingConnection === null || pendingConnection === void 0 ? void 0 : pendingConnection.cancel(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, `Device ${deviceId} connection cancelled`));
1253
1389
  const disconnectEntry = deviceDisconnectListeners.get(deviceId);
1254
1390
  if (disconnectEntry) {
1255
1391
  disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
@@ -1273,6 +1409,8 @@ function disconnectDevice(deviceId) {
1273
1409
  }
1274
1410
  function unsubscribeNotifications(deviceId) {
1275
1411
  return index.__awaiter(this, void 0, void 0, function* () {
1412
+ if (nativeReleased)
1413
+ return;
1276
1414
  const peripheral = connectedDevices.get(deviceId);
1277
1415
  const characteristics = deviceCharacteristics.get(deviceId);
1278
1416
  if (!peripheral || !characteristics) {
@@ -1292,7 +1430,8 @@ function unsubscribeNotifications(deviceId) {
1292
1430
  subscribedDevices.delete(deviceId);
1293
1431
  }
1294
1432
  finally {
1295
- subscriptionOperations.set(deviceId, 'idle');
1433
+ if (!disposing)
1434
+ subscriptionOperations.set(deviceId, 'idle');
1296
1435
  }
1297
1436
  });
1298
1437
  }
@@ -1342,10 +1481,12 @@ function subscribeNotifications(deviceId, callback) {
1342
1481
  timeoutMs: BLE_CLEANUP_TIMEOUT,
1343
1482
  timeoutBehavior: 'resolve',
1344
1483
  });
1484
+ assertBleActive();
1345
1485
  yield runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
1346
1486
  timeoutMs: NOBLE_BLE_SUBSCRIBE_TIMEOUT_MS,
1347
1487
  timeoutBehavior: 'reject',
1348
1488
  });
1489
+ assertBleActive();
1349
1490
  notifyCharacteristic.on('data', (data) => {
1350
1491
  if (!pairedDevices.has(deviceId)) {
1351
1492
  pairedDevices.add(deviceId);
@@ -1358,25 +1499,42 @@ function subscribeNotifications(deviceId, callback) {
1358
1499
  const subscribeStartedAt = Date.now();
1359
1500
  try {
1360
1501
  yield rebuildAppSubscription(deviceId, notifyCharacteristic);
1502
+ assertBleActive();
1361
1503
  subscribedDevices.set(deviceId, true);
1362
1504
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Notification subscription active', {
1363
1505
  deviceId,
1364
1506
  ms: Date.now() - subscribeStartedAt,
1365
1507
  });
1366
1508
  }
1509
+ catch (error) {
1510
+ throw createNobleBleConnectionError(error, 'Notification subscription failed: ');
1511
+ }
1367
1512
  finally {
1368
- subscriptionOperations.set(deviceId, 'idle');
1513
+ if (!disposing)
1514
+ subscriptionOperations.set(deviceId, 'idle');
1369
1515
  }
1370
1516
  });
1371
1517
  }
1372
1518
  function setupNobleBleHandlers(webContents) {
1519
+ if (disposing)
1520
+ return;
1521
+ let windowDestroyed = false;
1373
1522
  try {
1374
1523
  logger = require('electron-log');
1375
1524
  const { ipcMain } = require('electron');
1525
+ const channels = new Set();
1526
+ removeIpcHandlers = () => channels.forEach(channel => ipcMain.removeHandler(channel));
1376
1527
  const handle = (channel, listener) => {
1528
+ channels.add(channel);
1377
1529
  ipcMain.removeHandler(channel);
1378
1530
  ipcMain.handle(channel, (...args) => index.__awaiter(this, void 0, void 0, function* () {
1379
1531
  try {
1532
+ if (windowCleanup)
1533
+ yield windowCleanup;
1534
+ assertBleActive();
1535
+ if (windowDestroyed) {
1536
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, 'BLE window destroyed');
1537
+ }
1380
1538
  return yield Promise.resolve(listener(...args));
1381
1539
  }
1382
1540
  catch (error) {
@@ -1479,13 +1637,27 @@ function setupNobleBleHandlers(webContents) {
1479
1637
  }
1480
1638
  }));
1481
1639
  webContents.on('destroyed', () => {
1640
+ windowDestroyed = true;
1641
+ if (disposing)
1642
+ return;
1482
1643
  safeLog(logger, 'info', 'Cleaning up Noble BLE handlers');
1483
- (() => 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;
1484
1650
  const deviceIds = Array.from(connectedDevices.keys());
1485
1651
  for (const deviceId of deviceIds) {
1652
+ if (disposing)
1653
+ return;
1486
1654
  yield unsubscribeNotifications(deviceId).catch(() => undefined);
1655
+ if (disposing)
1656
+ return;
1487
1657
  yield disconnectDevice(deviceId).catch(() => undefined);
1488
1658
  }
1659
+ if (disposing)
1660
+ return;
1489
1661
  yield stopScanning().catch(() => undefined);
1490
1662
  cleanupNobleListeners();
1491
1663
  discoveredDevices.clear();
@@ -1501,8 +1673,77 @@ function setupNobleBleHandlers(webContents) {
1501
1673
  throw error;
1502
1674
  }
1503
1675
  }
1676
+ function disposeNobleBleSupport(releaseNoble = instance => instance.stop()) {
1677
+ if (disposePromise)
1678
+ return disposePromise;
1679
+ disposing = true;
1680
+ removeIpcHandlers === null || removeIpcHandlers === void 0 ? void 0 : removeIpcHandlers();
1681
+ const instance = noble;
1682
+ const deviceIds = new Set([...connectedDevices.keys(), ...connectingDevices.keys()]);
1683
+ const pendingPeripherals = Array.from(connectingDevices.values(), pending => pending.peripheral);
1684
+ for (const cancel of pendingCancellations)
1685
+ cancel();
1686
+ for (const id of idleDisconnectTimers.keys())
1687
+ clearIdleDisconnect(id);
1688
+ for (const pending of connectingDevices.values()) {
1689
+ pending.cancel(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, 'Noble BLE is shutting down'));
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
+ }
1700
+ let timeout;
1701
+ disposePromise = (() => index.__awaiter(this, void 0, void 0, function* () {
1702
+ try {
1703
+ yield Promise.race([
1704
+ Promise.allSettled([
1705
+ windowCleanup,
1706
+ ...connections.map(connection => connection.settled),
1707
+ stopScanning(),
1708
+ ...pendingPeripherals.map(peripheral => runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
1709
+ timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
1710
+ timeoutBehavior: 'resolve',
1711
+ })),
1712
+ ...Array.from(deviceIds, (id) => index.__awaiter(this, void 0, void 0, function* () {
1713
+ yield unsubscribeNotifications(id).catch(() => undefined);
1714
+ yield disconnectDevice(id).catch(() => undefined);
1715
+ })),
1716
+ ]),
1717
+ new Promise(resolve => {
1718
+ timeout = setTimeout(() => {
1719
+ logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Process dispose timed out; releasing native manager');
1720
+ resolve();
1721
+ }, 3500);
1722
+ }),
1723
+ ]);
1724
+ }
1725
+ finally {
1726
+ clearTimeout(timeout);
1727
+ nativeReleased = true;
1728
+ cleanupNobleListeners();
1729
+ if (instance && persistentStateListener) {
1730
+ instance.removeListener('stateChange', persistentStateListener);
1731
+ persistentStateListener = null;
1732
+ }
1733
+ for (const id of deviceIds)
1734
+ cleanupDevice(id);
1735
+ discoveredDevices.clear();
1736
+ directConnectCooldownUntil.clear();
1737
+ if (instance)
1738
+ releaseNoble(instance);
1739
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Process dispose completed');
1740
+ }
1741
+ }))();
1742
+ return disposePromise;
1743
+ }
1504
1744
 
1505
1745
  exports.createNobleBleConnectionError = createNobleBleConnectionError;
1506
1746
  exports.createNobleBleIpcErrorResponse = createNobleBleIpcErrorResponse;
1747
+ exports.disposeNobleBleSupport = disposeNobleBleSupport;
1507
1748
  exports.resolveNobleBleWritePacingDelay = resolveNobleBleWritePacingDelay;
1508
1749
  exports.setupNobleBleHandlers = setupNobleBleHandlers;