@onekeyfe/hd-transport-electron 1.2.2-alpha.12 → 1.2.2-alpha.122

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-eea100c1.js');
3
+ var index = require('./index-474dbbae.js');
4
4
  var hdShared = require('@onekeyfe/hd-shared');
5
5
  var pRetry = require('p-retry');
6
6
 
@@ -96,22 +96,65 @@ function softRefreshSubscription(params) {
96
96
  }
97
97
 
98
98
  const NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS = 5000;
99
- const NOBLE_BLE_CONNECTION_TIMEOUT_MS = 10000;
99
+ 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 pendingEnumerations = new Set();
110
+ const nativeConnections = new Set();
111
+ function trackNativeConnection(operation, cancel) {
112
+ const connection = {
113
+ cancel,
114
+ settled: operation.then(() => undefined, () => undefined),
115
+ };
116
+ nativeConnections.add(connection);
117
+ connection.settled = connection.settled.finally(() => nativeConnections.delete(connection));
118
+ }
119
+ function assertBleActive() {
120
+ if (disposing) {
121
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, 'Noble BLE is shutting down');
122
+ }
123
+ }
103
124
  function createNobleBleConnectionError(error, messagePrefix = '') {
104
125
  const errorMessage = error.message;
105
- const normalizedErrorMessage = errorMessage.toLowerCase();
106
- let errorCode = hdShared.HardwareErrorCode.BleConnectedError;
107
- if (hdShared.isBleStaleBondErrorText(errorMessage)) {
108
- errorCode =
109
- normalizedErrorMessage.includes('peer removed pairing information') ||
110
- normalizedErrorMessage.includes('cberrordomain:14')
111
- ? hdShared.HardwareErrorCode.BlePeerRemovedPairingInformation
112
- : hdShared.HardwareErrorCode.BleDeviceBondError;
126
+ const isInvalidMacOsBond = (error.nativeErrorCode === 14 && error.nativeErrorDomain === 'CBErrorDomain') ||
127
+ (error.nativeErrorCode === 15 && error.nativeErrorDomain === 'CBATTErrorDomain');
128
+ if (isInvalidMacOsBond) {
129
+ const nativeErrorMessage = `${messagePrefix}${errorMessage}`;
130
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleBondInvalid, `${hdShared.HardwareErrorCodeMessage[hdShared.HardwareErrorCode.BleBondInvalid]} (${nativeErrorMessage})`, {
131
+ nativeErrorMessage,
132
+ });
133
+ }
134
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, `${messagePrefix}${errorMessage}`);
135
+ }
136
+ function createNobleBleIpcErrorResponse(error) {
137
+ const candidate = error;
138
+ const errorCode = typeof (candidate === null || candidate === void 0 ? void 0 : candidate.errorCode) === 'number' ? candidate.errorCode : hdShared.HardwareErrorCode.UnknownError;
139
+ const message = typeof (candidate === null || candidate === void 0 ? void 0 : candidate.message) === 'string' ? candidate.message : String(error !== null && error !== void 0 ? error : 'Unknown error');
140
+ const name = typeof (candidate === null || candidate === void 0 ? void 0 : candidate.name) === 'string' ? candidate.name : 'Error';
141
+ let params;
142
+ if ((candidate === null || candidate === void 0 ? void 0 : candidate.params) !== undefined) {
143
+ try {
144
+ const serializedParams = JSON.stringify(candidate.params);
145
+ params = serializedParams === undefined ? undefined : JSON.parse(serializedParams);
146
+ }
147
+ catch (_a) {
148
+ params = undefined;
149
+ }
113
150
  }
114
- return hdShared.ERRORS.TypedError(errorCode, `${messagePrefix}${errorMessage}`);
151
+ return {
152
+ type: 'NobleBleIpcError',
153
+ success: false,
154
+ error: Object.assign({ name,
155
+ message,
156
+ errorCode }, (params !== undefined ? { params } : {})),
157
+ };
115
158
  }
116
159
  const bluetoothState = {
117
160
  available: false,
@@ -122,6 +165,86 @@ let persistentStateListener = null;
122
165
  let persistentDiscoverListener = null;
123
166
  const discoveredDevices = new Map();
124
167
  const connectedDevices = new Map();
168
+ const connectingDevices = new Map();
169
+ let nextConnectionGeneration = 0;
170
+ function connectPeripheralWithCancellation(peripheral, deviceId, messagePrefix = '') {
171
+ assertBleActive();
172
+ return new Promise((resolve, reject) => {
173
+ var _a;
174
+ const generation = ++nextConnectionGeneration;
175
+ let settled = false;
176
+ const settle = (settler) => {
177
+ var _a;
178
+ if (settled)
179
+ return false;
180
+ settled = true;
181
+ if (((_a = connectingDevices.get(deviceId)) === null || _a === void 0 ? void 0 : _a.generation) === generation) {
182
+ connectingDevices.delete(deviceId);
183
+ }
184
+ settler();
185
+ return true;
186
+ };
187
+ const pendingConnection = {
188
+ peripheral,
189
+ generation,
190
+ cancel: error => {
191
+ settle(() => reject(error));
192
+ },
193
+ };
194
+ (_a = connectingDevices
195
+ .get(deviceId)) === null || _a === void 0 ? void 0 : _a.cancel(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, `Device ${deviceId} connect attempt was superseded`));
196
+ connectingDevices.set(deviceId, pendingConnection);
197
+ let completeNative;
198
+ trackNativeConnection(new Promise(resolve => {
199
+ completeNative = resolve;
200
+ }), () => {
201
+ var _a;
202
+ if (typeof peripheral.cancelConnect === 'function')
203
+ peripheral.cancelConnect();
204
+ else
205
+ (_a = noble === null || noble === void 0 ? void 0 : noble.cancelConnect) === null || _a === void 0 ? void 0 : _a.call(noble, deviceId);
206
+ });
207
+ const onConnect = (error) => {
208
+ if (nativeReleased) {
209
+ completeNative();
210
+ return;
211
+ }
212
+ if (error) {
213
+ settle(() => reject(createNobleBleConnectionError(error, messagePrefix)));
214
+ completeNative();
215
+ return;
216
+ }
217
+ if (!settle(() => {
218
+ connectedDevices.set(deviceId, peripheral);
219
+ resolve();
220
+ })) {
221
+ const activeConnection = connectingDevices.get(deviceId);
222
+ if (!activeConnection || activeConnection.peripheral !== peripheral) {
223
+ try {
224
+ peripheral.removeAllListeners('disconnect');
225
+ runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
226
+ timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
227
+ timeoutBehavior: 'resolve',
228
+ })
229
+ .catch(() => undefined)
230
+ .finally(completeNative);
231
+ return;
232
+ }
233
+ catch (_a) {
234
+ }
235
+ }
236
+ }
237
+ completeNative();
238
+ };
239
+ try {
240
+ peripheral.connect(onConnect);
241
+ }
242
+ catch (error) {
243
+ completeNative();
244
+ settle(() => reject(error));
245
+ }
246
+ });
247
+ }
125
248
  const pairedDevices = new Set();
126
249
  const deviceCharacteristics = new Map();
127
250
  const notificationCallbacks = new Map();
@@ -260,6 +383,7 @@ function updateBluetoothState(state) {
260
383
  }
261
384
  function initializeNoble() {
262
385
  return index.__awaiter(this, void 0, void 0, function* () {
386
+ assertBleActive();
263
387
  if (noble)
264
388
  return;
265
389
  try {
@@ -276,9 +400,11 @@ function initializeNoble() {
276
400
  return;
277
401
  }
278
402
  const timeout = setTimeout(() => {
403
+ cleanup();
279
404
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout'));
280
405
  }, BLUETOOTH_INIT_TIMEOUT);
281
406
  const cleanup = () => {
407
+ pendingCancellations.delete(cancel);
282
408
  clearTimeout(timeout);
283
409
  if (noble) {
284
410
  noble.removeListener('stateChange', onStateChange);
@@ -303,8 +429,14 @@ function initializeNoble() {
303
429
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError));
304
430
  }
305
431
  };
432
+ const cancel = () => {
433
+ cleanup();
434
+ reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, 'Noble BLE is shutting down'));
435
+ };
436
+ pendingCancellations.add(cancel);
306
437
  noble.on('stateChange', onStateChange);
307
438
  });
439
+ assertBleActive();
308
440
  if (!persistentDiscoverListener) {
309
441
  persistentDiscoverListener = (peripheral) => {
310
442
  handleDeviceDiscovered(peripheral);
@@ -351,6 +483,8 @@ function broadcastToAllWebContents(channel, payload) {
351
483
  }
352
484
  function armIdleDisconnect(deviceId, ms = BLE_IDLE_DISCONNECT_MS, reason = 'idle') {
353
485
  clearIdleDisconnect(deviceId);
486
+ if (disposing)
487
+ return;
354
488
  idleDisconnectTimers.set(deviceId, setTimeout(() => {
355
489
  var _a;
356
490
  idleDisconnectTimers.delete(deviceId);
@@ -386,7 +520,7 @@ function armIdleDisconnect(deviceId, ms = BLE_IDLE_DISCONNECT_MS, reason = 'idle
386
520
  }, ms));
387
521
  }
388
522
  function cleanupDevice(deviceId, webContents, options = {}) {
389
- var _a;
523
+ var _a, _b;
390
524
  const { cleanupConnection = true, cleanupDiscoveredCache = false, sendDisconnectEvent = false, cancelOperations = true, reason = 'unknown', disconnectReason = hdShared.EBleDisconnectReason.DeviceDisconnected, } = options;
391
525
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Starting device cleanup', {
392
526
  deviceId,
@@ -400,6 +534,8 @@ function cleanupDevice(deviceId, webContents, options = {}) {
400
534
  const peripheral = connectedDevices.get(deviceId);
401
535
  const deviceName = ((_a = peripheral === null || peripheral === void 0 ? void 0 : peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device';
402
536
  if (cleanupConnection) {
537
+ (_b = connectingDevices
538
+ .get(deviceId)) === null || _b === void 0 ? void 0 : _b.cancel(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, `Device ${deviceId} disconnected while connecting`));
403
539
  const disconnectEntry = deviceDisconnectListeners.get(deviceId);
404
540
  if (disconnectEntry) {
405
541
  disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
@@ -478,6 +614,7 @@ function setupMtuListener(peripheral, deviceId, webContents) {
478
614
  }
479
615
  function writeCharacteristicWithoutResponse(deviceId, writeCharacteristic, buffer) {
480
616
  return index.__awaiter(this, void 0, void 0, function* () {
617
+ assertBleActive();
481
618
  return new Promise((resolve, reject) => {
482
619
  writeCharacteristic.write(buffer, true, (error) => {
483
620
  if (error) {
@@ -680,6 +817,7 @@ function waitForNobleScanStop(nobleInstance) {
680
817
  }
681
818
  function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS) {
682
819
  return index.__awaiter(this, void 0, void 0, function* () {
820
+ assertBleActive();
683
821
  if (!noble) {
684
822
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
685
823
  }
@@ -691,6 +829,7 @@ function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN
691
829
  if (settled)
692
830
  return;
693
831
  settled = true;
832
+ pendingCancellations.delete(cancel);
694
833
  if (timeoutId)
695
834
  clearTimeout(timeoutId);
696
835
  nobleInstance.removeListener('discover', onDiscover);
@@ -700,6 +839,7 @@ function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN
700
839
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, error.message));
701
840
  return;
702
841
  }
842
+ assertBleActive();
703
843
  if (peripheral) {
704
844
  discoveredDevices.set(peripheral.id, peripheral);
705
845
  }
@@ -719,6 +859,10 @@ function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN
719
859
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
720
860
  finish(null).catch(reject);
721
861
  }, timeoutMs);
862
+ const cancel = () => {
863
+ finish(null, new Error('Noble BLE is shutting down')).catch(reject);
864
+ };
865
+ pendingCancellations.add(cancel);
722
866
  nobleInstance.on('discover', onDiscover);
723
867
  nobleInstance.startScanning([], true, (error) => {
724
868
  if (error) {
@@ -730,7 +874,7 @@ function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN
730
874
  });
731
875
  });
732
876
  }
733
- function enumerateDevices() {
877
+ function enumerateDevices(isWindowDestroyed) {
734
878
  return index.__awaiter(this, void 0, void 0, function* () {
735
879
  if (!noble) {
736
880
  yield initializeNoble();
@@ -738,19 +882,30 @@ function enumerateDevices() {
738
882
  if (!noble) {
739
883
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
740
884
  }
885
+ assertBleActive();
886
+ if (isWindowDestroyed())
887
+ return [];
741
888
  const nobleInstance = noble;
742
889
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Starting device enumeration');
743
- discoveredDevices.clear();
744
890
  ensureDiscoverListener();
745
891
  return new Promise((resolve, reject) => {
746
892
  const devices = [];
893
+ let settled = false;
747
894
  let intervalId;
748
- const cleanup = () => index.__awaiter(this, void 0, void 0, function* () {
749
- clearTimeout(timeoutId);
750
- if (intervalId)
751
- clearInterval(intervalId);
752
- yield waitForNobleScanStop(nobleInstance);
753
- });
895
+ let cleanupPromise;
896
+ const cleanup = () => {
897
+ if (!cleanupPromise) {
898
+ settled = true;
899
+ pendingCancellations.delete(cancel);
900
+ clearTimeout(timeoutId);
901
+ if (intervalId)
902
+ clearInterval(intervalId);
903
+ cleanupPromise = waitForNobleScanStop(nobleInstance).finally(() => {
904
+ pendingEnumerations.delete(cancel);
905
+ });
906
+ }
907
+ return cleanupPromise;
908
+ };
754
909
  const pushDevice = (peripheral, id) => {
755
910
  var _a;
756
911
  if (devices.some(d => d.id === id))
@@ -767,13 +922,20 @@ function enumerateDevices() {
767
922
  connectedDevices.forEach(pushDevice);
768
923
  };
769
924
  const timeoutId = setTimeout(() => index.__awaiter(this, void 0, void 0, function* () {
925
+ if (settled)
926
+ return;
770
927
  checkDevices();
771
928
  yield cleanup();
772
929
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scan completed, found devices:', devices.length);
773
930
  resolve(devices);
774
931
  }), DEVICE_SCAN_TIMEOUT);
932
+ const cancel = () => cleanup().then(() => resolve([]), reject);
933
+ pendingCancellations.add(cancel);
934
+ pendingEnumerations.add(cancel);
775
935
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning for OneKey BLE devices');
776
936
  nobleInstance.startScanning([], true, (error) => index.__awaiter(this, void 0, void 0, function* () {
937
+ if (settled)
938
+ return;
777
939
  if (error) {
778
940
  yield cleanup();
779
941
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to start scanning:', error);
@@ -788,7 +950,7 @@ function enumerateDevices() {
788
950
  }
789
951
  function stopScanning() {
790
952
  return index.__awaiter(this, void 0, void 0, function* () {
791
- if (!noble)
953
+ if (!noble || nativeReleased)
792
954
  return;
793
955
  const nobleInstance = noble;
794
956
  yield waitForNobleScanStop(nobleInstance);
@@ -832,13 +994,16 @@ function getDevice(deviceId) {
832
994
  }
833
995
  function discoverServicesAndCharacteristics(peripheral) {
834
996
  return index.__awaiter(this, void 0, void 0, function* () {
997
+ assertBleActive();
835
998
  let timeoutId;
836
999
  let onDisconnect;
837
1000
  const cleanup = () => {
838
1001
  if (timeoutId)
839
1002
  clearTimeout(timeoutId);
840
- if (onDisconnect)
1003
+ if (onDisconnect) {
841
1004
  peripheral.removeListener('disconnect', onDisconnect);
1005
+ pendingCancellations.delete(onDisconnect);
1006
+ }
842
1007
  };
843
1008
  const timeoutPromise = new Promise((_, reject) => {
844
1009
  timeoutId = setTimeout(() => {
@@ -851,6 +1016,7 @@ function discoverServicesAndCharacteristics(peripheral) {
851
1016
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Device disconnected during service discovery');
852
1017
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'Device disconnected during service discovery'));
853
1018
  };
1019
+ pendingCancellations.add(onDisconnect);
854
1020
  peripheral.once('disconnect', onDisconnect);
855
1021
  });
856
1022
  const discoveryPromise = (() => index.__awaiter(this, void 0, void 0, function* () {
@@ -865,6 +1031,7 @@ function discoverServicesAndCharacteristics(peripheral) {
865
1031
  }
866
1032
  });
867
1033
  });
1034
+ assertBleActive();
868
1035
  if (!services || services.length === 0) {
869
1036
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'No OneKey services found');
870
1037
  }
@@ -937,12 +1104,8 @@ function forceReconnectPeripheral(peripheral, deviceId) {
937
1104
  callback();
938
1105
  }), { timeoutMs: BLE_CLEANUP_TIMEOUT, timeoutBehavior: 'resolve' });
939
1106
  }
940
- yield runBleCallbackOperation(callback => peripheral.connect(callback), {
941
- timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
942
- timeoutBehavior: 'reject',
943
- });
1107
+ yield connectPeripheralWithCancellation(peripheral, deviceId);
944
1108
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Force reconnect successful');
945
- connectedDevices.set(deviceId, peripheral);
946
1109
  yield hdShared.wait(500);
947
1110
  });
948
1111
  }
@@ -959,17 +1122,7 @@ function freshScanAndDiscover(deviceId, webContents) {
959
1122
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found in fresh scan`);
960
1123
  }
961
1124
  discoveredDevices.set(deviceId, freshPeripheral);
962
- yield new Promise((resolve, reject) => {
963
- freshPeripheral.connect((error) => {
964
- if (error) {
965
- reject(createNobleBleConnectionError(error, 'Fresh peripheral connection failed: '));
966
- }
967
- else {
968
- connectedDevices.set(deviceId, freshPeripheral);
969
- resolve();
970
- }
971
- });
972
- });
1125
+ yield connectPeripheralWithCancellation(freshPeripheral, deviceId, 'Fresh peripheral connection failed: ');
973
1126
  setupDisconnectListener(freshPeripheral, deviceId, webContents);
974
1127
  yield hdShared.wait(500);
975
1128
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Attempting service discovery with fresh peripheral');
@@ -1008,6 +1161,7 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
1008
1161
  minTimeout: 500,
1009
1162
  maxTimeout: 3000,
1010
1163
  onFailedAttempt: error => {
1164
+ assertBleActive();
1011
1165
  logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] Service discovery attempt ${error.attemptNumber} failed:`, {
1012
1166
  message: error.message,
1013
1167
  retriesLeft: error.retriesLeft,
@@ -1023,8 +1177,13 @@ function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
1023
1177
  yield forceReconnectPeripheral(peripheral, deviceId);
1024
1178
  }
1025
1179
  catch (resetError) {
1180
+ if (hdShared.isBleStaleBondHardwareError(resetError) ||
1181
+ (resetError === null || resetError === void 0 ? void 0 : resetError.errorCode) === hdShared.HardwareErrorCode.BleDeviceDisconnected) {
1182
+ throw resetError;
1183
+ }
1026
1184
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError);
1027
1185
  }
1186
+ assertBleActive();
1028
1187
  setupDisconnectListener(peripheral, deviceId, webContents);
1029
1188
  try {
1030
1189
  return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
@@ -1047,36 +1206,46 @@ const directConnectCooldownUntil = new Map();
1047
1206
  function tryDirectConnectById(deviceId) {
1048
1207
  var _a;
1049
1208
  return index.__awaiter(this, void 0, void 0, function* () {
1209
+ assertBleActive();
1050
1210
  const nobleInstance = noble;
1051
1211
  if (!(nobleInstance === null || nobleInstance === void 0 ? void 0 : nobleInstance.connectAsync))
1052
1212
  return undefined;
1053
1213
  const cooldownUntil = (_a = directConnectCooldownUntil.get(deviceId)) !== null && _a !== void 0 ? _a : 0;
1054
1214
  if (Date.now() < cooldownUntil)
1055
1215
  return undefined;
1216
+ let timer;
1217
+ let cancel;
1218
+ let abandoned = false;
1056
1219
  try {
1057
1220
  const directPromise = nobleInstance.connectAsync(deviceId);
1221
+ trackNativeConnection(directPromise.then((late) => index.__awaiter(this, void 0, void 0, function* () {
1222
+ if ((!abandoned && !disposing) || nativeReleased)
1223
+ return;
1224
+ const peripheral = late !== null && late !== void 0 ? late : discoveredDevices.get(deviceId);
1225
+ if ((peripheral === null || peripheral === void 0 ? void 0 : peripheral.state) === 'connected' && !connectedDevices.has(deviceId)) {
1226
+ peripheral.removeAllListeners('disconnect');
1227
+ yield runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
1228
+ timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
1229
+ timeoutBehavior: 'resolve',
1230
+ });
1231
+ }
1232
+ })), () => { var _a; return (_a = nobleInstance.cancelConnect) === null || _a === void 0 ? void 0 : _a.call(nobleInstance, deviceId); });
1058
1233
  const raced = yield Promise.race([
1059
1234
  directPromise,
1060
1235
  new Promise(resolve => {
1061
- setTimeout(() => resolve('timeout'), DIRECT_CONNECT_TIMEOUT_MS);
1236
+ cancel = () => {
1237
+ abandoned = true;
1238
+ resolve('timeout');
1239
+ };
1240
+ pendingCancellations.add(cancel);
1241
+ timer = setTimeout(cancel, DIRECT_CONNECT_TIMEOUT_MS);
1062
1242
  }),
1063
1243
  ]);
1064
- if (raced === 'timeout') {
1244
+ if (raced === 'timeout' || disposing) {
1065
1245
  directConnectCooldownUntil.set(deviceId, Date.now() + DIRECT_CONNECT_COOLDOWN_MS);
1066
1246
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct connect-by-id timed out, falling back to scan', {
1067
1247
  deviceId,
1068
1248
  });
1069
- directPromise
1070
- .then(late => {
1071
- const latePeripheral = late !== null && late !== void 0 ? late : discoveredDevices.get(deviceId);
1072
- if (latePeripheral &&
1073
- latePeripheral.state === 'connected' &&
1074
- !connectedDevices.has(deviceId)) {
1075
- latePeripheral.removeAllListeners('disconnect');
1076
- latePeripheral.disconnect(() => undefined);
1077
- }
1078
- })
1079
- .catch(() => undefined);
1080
1249
  return undefined;
1081
1250
  }
1082
1251
  const peripheral = raced !== null && raced !== void 0 ? raced : discoveredDevices.get(deviceId);
@@ -1093,6 +1262,11 @@ function tryDirectConnectById(deviceId) {
1093
1262
  });
1094
1263
  return undefined;
1095
1264
  }
1265
+ finally {
1266
+ clearTimeout(timer);
1267
+ if (cancel)
1268
+ pendingCancellations.delete(cancel);
1269
+ }
1096
1270
  });
1097
1271
  }
1098
1272
  function connectDevice(deviceId, webContents) {
@@ -1106,7 +1280,10 @@ function connectDevice(deviceId, webContents) {
1106
1280
  totalDiscovered: discoveredDevices.size,
1107
1281
  totalConnected: connectedDevices.size,
1108
1282
  });
1109
- let peripheral = (_a = discoveredDevices.get(deviceId)) !== null && _a !== void 0 ? _a : connectedDevices.get(deviceId);
1283
+ let peripheral = (_a = connectedDevices.get(deviceId)) !== null && _a !== void 0 ? _a : discoveredDevices.get(deviceId);
1284
+ if ((peripheral === null || peripheral === void 0 ? void 0 : peripheral.state) !== 'connected') {
1285
+ peripheral = undefined;
1286
+ }
1110
1287
  if (!peripheral) {
1111
1288
  if (!noble) {
1112
1289
  yield initializeNoble();
@@ -1145,6 +1322,7 @@ function connectDevice(deviceId, webContents) {
1145
1322
  peripheral = byIdFirst ? yield scanForPeripheral() : yield connectById();
1146
1323
  }
1147
1324
  }
1325
+ assertBleActive();
1148
1326
  if (!peripheral) {
1149
1327
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found`);
1150
1328
  }
@@ -1155,6 +1333,7 @@ function connectDevice(deviceId, webContents) {
1155
1333
  if (!connectedDevices.has(deviceId)) {
1156
1334
  connectedDevices.set(deviceId, peripheral);
1157
1335
  }
1336
+ assertBleActive();
1158
1337
  setupDisconnectListener(peripheral, deviceId, webContents);
1159
1338
  if (deviceCharacteristics.has(deviceId)) {
1160
1339
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device characteristics already available');
@@ -1179,6 +1358,7 @@ function connectDevice(deviceId, webContents) {
1179
1358
  }
1180
1359
  try {
1181
1360
  const characteristics = yield setupConnectionAndDiscoverServices(peripheral, deviceId, webContents);
1361
+ assertBleActive();
1182
1362
  deviceCharacteristics.set(deviceId, characteristics);
1183
1363
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device ready for communication:', deviceId);
1184
1364
  }
@@ -1189,54 +1369,33 @@ function connectDevice(deviceId, webContents) {
1189
1369
  }
1190
1370
  return;
1191
1371
  }
1192
- return new Promise((resolve, reject) => {
1193
- let connectionTimedOut = false;
1194
- const timeout = setTimeout(() => {
1195
- connectionTimedOut = true;
1196
- reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, 'Connection timeout'));
1197
- }, NOBLE_BLE_CONNECTION_TIMEOUT_MS);
1198
- const connectedPeripheral = peripheral;
1199
- connectedPeripheral.connect((error) => index.__awaiter(this, void 0, void 0, function* () {
1200
- clearTimeout(timeout);
1201
- if (connectionTimedOut) {
1202
- if (!error) {
1203
- try {
1204
- connectedPeripheral.disconnect(() => undefined);
1205
- }
1206
- catch (_a) {
1207
- }
1208
- }
1209
- return;
1210
- }
1211
- if (error) {
1212
- logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection failed:', error);
1213
- reject(createNobleBleConnectionError(error));
1214
- return;
1215
- }
1216
- logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connected to device:', deviceId);
1217
- connectedDevices.set(deviceId, connectedPeripheral);
1218
- try {
1219
- const characteristics = yield setupConnectionAndDiscoverServices(connectedPeripheral, deviceId, webContents);
1220
- deviceCharacteristics.set(deviceId, characteristics);
1221
- logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device ready for communication:', deviceId);
1222
- resolve();
1223
- }
1224
- catch (setupError) {
1225
- logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection setup failed:', setupError);
1226
- disconnectDevice(deviceId)
1227
- .catch(() => undefined)
1228
- .then(() => reject(setupError));
1229
- }
1230
- }));
1231
- });
1372
+ const connectedPeripheral = peripheral;
1373
+ yield connectPeripheralWithCancellation(connectedPeripheral, deviceId);
1374
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connected to device:', deviceId);
1375
+ try {
1376
+ const characteristics = yield setupConnectionAndDiscoverServices(connectedPeripheral, deviceId, webContents);
1377
+ assertBleActive();
1378
+ deviceCharacteristics.set(deviceId, characteristics);
1379
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device ready for communication:', deviceId);
1380
+ }
1381
+ catch (setupError) {
1382
+ logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection setup failed:', setupError);
1383
+ yield disconnectDevice(deviceId).catch(() => undefined);
1384
+ throw setupError;
1385
+ }
1232
1386
  });
1233
1387
  }
1234
1388
  function disconnectDevice(deviceId) {
1389
+ var _a;
1235
1390
  return index.__awaiter(this, void 0, void 0, function* () {
1236
- const peripheral = connectedDevices.get(deviceId);
1391
+ if (nativeReleased)
1392
+ return;
1393
+ const pendingConnection = connectingDevices.get(deviceId);
1394
+ const peripheral = (_a = connectedDevices.get(deviceId)) !== null && _a !== void 0 ? _a : pendingConnection === null || pendingConnection === void 0 ? void 0 : pendingConnection.peripheral;
1237
1395
  if (!peripheral) {
1238
1396
  return;
1239
1397
  }
1398
+ pendingConnection === null || pendingConnection === void 0 ? void 0 : pendingConnection.cancel(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, `Device ${deviceId} connection cancelled`));
1240
1399
  const disconnectEntry = deviceDisconnectListeners.get(deviceId);
1241
1400
  if (disconnectEntry) {
1242
1401
  disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
@@ -1260,6 +1419,8 @@ function disconnectDevice(deviceId) {
1260
1419
  }
1261
1420
  function unsubscribeNotifications(deviceId) {
1262
1421
  return index.__awaiter(this, void 0, void 0, function* () {
1422
+ if (nativeReleased)
1423
+ return;
1263
1424
  const peripheral = connectedDevices.get(deviceId);
1264
1425
  const characteristics = deviceCharacteristics.get(deviceId);
1265
1426
  if (!peripheral || !characteristics) {
@@ -1279,7 +1440,8 @@ function unsubscribeNotifications(deviceId) {
1279
1440
  subscribedDevices.delete(deviceId);
1280
1441
  }
1281
1442
  finally {
1282
- subscriptionOperations.set(deviceId, 'idle');
1443
+ if (!disposing)
1444
+ subscriptionOperations.set(deviceId, 'idle');
1283
1445
  }
1284
1446
  });
1285
1447
  }
@@ -1329,10 +1491,12 @@ function subscribeNotifications(deviceId, callback) {
1329
1491
  timeoutMs: BLE_CLEANUP_TIMEOUT,
1330
1492
  timeoutBehavior: 'resolve',
1331
1493
  });
1494
+ assertBleActive();
1332
1495
  yield runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
1333
- timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
1496
+ timeoutMs: NOBLE_BLE_SUBSCRIBE_TIMEOUT_MS,
1334
1497
  timeoutBehavior: 'reject',
1335
1498
  });
1499
+ assertBleActive();
1336
1500
  notifyCharacteristic.on('data', (data) => {
1337
1501
  if (!pairedDevices.has(deviceId)) {
1338
1502
  pairedDevices.add(deviceId);
@@ -1345,29 +1509,53 @@ function subscribeNotifications(deviceId, callback) {
1345
1509
  const subscribeStartedAt = Date.now();
1346
1510
  try {
1347
1511
  yield rebuildAppSubscription(deviceId, notifyCharacteristic);
1512
+ assertBleActive();
1348
1513
  subscribedDevices.set(deviceId, true);
1349
1514
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Notification subscription active', {
1350
1515
  deviceId,
1351
1516
  ms: Date.now() - subscribeStartedAt,
1352
1517
  });
1353
1518
  }
1519
+ catch (error) {
1520
+ throw createNobleBleConnectionError(error, 'Notification subscription failed: ');
1521
+ }
1354
1522
  finally {
1355
- subscriptionOperations.set(deviceId, 'idle');
1523
+ if (!disposing)
1524
+ subscriptionOperations.set(deviceId, 'idle');
1356
1525
  }
1357
1526
  });
1358
1527
  }
1359
1528
  function setupNobleBleHandlers(webContents) {
1529
+ if (disposing)
1530
+ return;
1531
+ let windowDestroyed = false;
1360
1532
  try {
1361
1533
  logger = require('electron-log');
1362
1534
  const { ipcMain } = require('electron');
1535
+ const channels = new Set();
1536
+ removeIpcHandlers = () => channels.forEach(channel => ipcMain.removeHandler(channel));
1363
1537
  const handle = (channel, listener) => {
1538
+ channels.add(channel);
1364
1539
  ipcMain.removeHandler(channel);
1365
- ipcMain.handle(channel, listener);
1540
+ ipcMain.handle(channel, (...args) => index.__awaiter(this, void 0, void 0, function* () {
1541
+ try {
1542
+ if (windowCleanup)
1543
+ yield windowCleanup;
1544
+ assertBleActive();
1545
+ if (windowDestroyed) {
1546
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, 'BLE window destroyed');
1547
+ }
1548
+ return yield Promise.resolve(listener(...args));
1549
+ }
1550
+ catch (error) {
1551
+ return createNobleBleIpcErrorResponse(error);
1552
+ }
1553
+ }));
1366
1554
  };
1367
1555
  safeLog(logger, 'info', 'Setting up Noble BLE IPC handlers');
1368
1556
  handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE, () => index.__awaiter(this, void 0, void 0, function* () {
1369
1557
  try {
1370
- const devices = yield enumerateDevices();
1558
+ const devices = yield enumerateDevices(() => windowDestroyed);
1371
1559
  safeLog(logger, 'debug', 'Enumeration completed', {
1372
1560
  count: devices.length,
1373
1561
  devices: devices.map(device => ({ id: device.id, name: device.name })),
@@ -1459,13 +1647,29 @@ function setupNobleBleHandlers(webContents) {
1459
1647
  }
1460
1648
  }));
1461
1649
  webContents.on('destroyed', () => {
1650
+ windowDestroyed = true;
1651
+ if (disposing)
1652
+ return;
1462
1653
  safeLog(logger, 'info', 'Cleaning up Noble BLE handlers');
1463
- (() => index.__awaiter(this, void 0, void 0, function* () {
1654
+ const previousCleanup = windowCleanup;
1655
+ const enumerations = Array.from(pendingEnumerations, cancel => cancel());
1656
+ windowCleanup = (() => index.__awaiter(this, void 0, void 0, function* () {
1657
+ if (previousCleanup)
1658
+ yield previousCleanup;
1659
+ yield Promise.allSettled(enumerations);
1660
+ if (disposing)
1661
+ return;
1464
1662
  const deviceIds = Array.from(connectedDevices.keys());
1465
1663
  for (const deviceId of deviceIds) {
1664
+ if (disposing)
1665
+ return;
1466
1666
  yield unsubscribeNotifications(deviceId).catch(() => undefined);
1667
+ if (disposing)
1668
+ return;
1467
1669
  yield disconnectDevice(deviceId).catch(() => undefined);
1468
1670
  }
1671
+ if (disposing)
1672
+ return;
1469
1673
  yield stopScanning().catch(() => undefined);
1470
1674
  cleanupNobleListeners();
1471
1675
  discoveredDevices.clear();
@@ -1481,7 +1685,77 @@ function setupNobleBleHandlers(webContents) {
1481
1685
  throw error;
1482
1686
  }
1483
1687
  }
1688
+ function disposeNobleBleSupport(releaseNoble = instance => instance.stop()) {
1689
+ if (disposePromise)
1690
+ return disposePromise;
1691
+ disposing = true;
1692
+ removeIpcHandlers === null || removeIpcHandlers === void 0 ? void 0 : removeIpcHandlers();
1693
+ const instance = noble;
1694
+ const deviceIds = new Set([...connectedDevices.keys(), ...connectingDevices.keys()]);
1695
+ const pendingPeripherals = Array.from(connectingDevices.values(), pending => pending.peripheral);
1696
+ for (const cancel of pendingCancellations)
1697
+ cancel();
1698
+ for (const id of idleDisconnectTimers.keys())
1699
+ clearIdleDisconnect(id);
1700
+ for (const pending of connectingDevices.values()) {
1701
+ pending.cancel(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, 'Noble BLE is shutting down'));
1702
+ }
1703
+ const connections = Array.from(nativeConnections);
1704
+ for (const connection of connections) {
1705
+ try {
1706
+ connection.cancel();
1707
+ }
1708
+ catch (error) {
1709
+ logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Native connect cancellation failed', error);
1710
+ }
1711
+ }
1712
+ let timeout;
1713
+ disposePromise = (() => index.__awaiter(this, void 0, void 0, function* () {
1714
+ try {
1715
+ yield Promise.race([
1716
+ Promise.allSettled([
1717
+ windowCleanup,
1718
+ ...connections.map(connection => connection.settled),
1719
+ stopScanning(),
1720
+ ...pendingPeripherals.map(peripheral => runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
1721
+ timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
1722
+ timeoutBehavior: 'resolve',
1723
+ })),
1724
+ ...Array.from(deviceIds, (id) => index.__awaiter(this, void 0, void 0, function* () {
1725
+ yield unsubscribeNotifications(id).catch(() => undefined);
1726
+ yield disconnectDevice(id).catch(() => undefined);
1727
+ })),
1728
+ ]),
1729
+ new Promise(resolve => {
1730
+ timeout = setTimeout(() => {
1731
+ logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Process dispose timed out; releasing native manager');
1732
+ resolve();
1733
+ }, 3500);
1734
+ }),
1735
+ ]);
1736
+ }
1737
+ finally {
1738
+ clearTimeout(timeout);
1739
+ nativeReleased = true;
1740
+ cleanupNobleListeners();
1741
+ if (instance && persistentStateListener) {
1742
+ instance.removeListener('stateChange', persistentStateListener);
1743
+ persistentStateListener = null;
1744
+ }
1745
+ for (const id of deviceIds)
1746
+ cleanupDevice(id);
1747
+ discoveredDevices.clear();
1748
+ directConnectCooldownUntil.clear();
1749
+ if (instance)
1750
+ releaseNoble(instance);
1751
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Process dispose completed');
1752
+ }
1753
+ }))();
1754
+ return disposePromise;
1755
+ }
1484
1756
 
1485
1757
  exports.createNobleBleConnectionError = createNobleBleConnectionError;
1758
+ exports.createNobleBleIpcErrorResponse = createNobleBleIpcErrorResponse;
1759
+ exports.disposeNobleBleSupport = disposeNobleBleSupport;
1486
1760
  exports.resolveNobleBleWritePacingDelay = resolveNobleBleWritePacingDelay;
1487
1761
  exports.setupNobleBleHandlers = setupNobleBleHandlers;