@onekeyfe/hd-transport-electron 1.2.2-alpha.9 → 1.2.3-alpha.2

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-00297050.js');
3
+ var index = require('./index-985e2bf3.js');
4
4
  var hdShared = require('@onekeyfe/hd-shared');
5
5
  var pRetry = require('p-retry');
6
6
 
@@ -96,10 +96,66 @@ 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
+ }
124
+ function createNobleBleConnectionError(error, messagePrefix = '') {
125
+ const errorMessage = error.message;
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
+ }
150
+ }
151
+ return {
152
+ type: 'NobleBleIpcError',
153
+ success: false,
154
+ error: Object.assign({ name,
155
+ message,
156
+ errorCode }, (params !== undefined ? { params } : {})),
157
+ };
158
+ }
103
159
  const bluetoothState = {
104
160
  available: false,
105
161
  unsupported: false,
@@ -109,6 +165,86 @@ let persistentStateListener = null;
109
165
  let persistentDiscoverListener = null;
110
166
  const discoveredDevices = new Map();
111
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
+ }
112
248
  const pairedDevices = new Set();
113
249
  const deviceCharacteristics = new Map();
114
250
  const notificationCallbacks = new Map();
@@ -247,6 +383,7 @@ function updateBluetoothState(state) {
247
383
  }
248
384
  function initializeNoble() {
249
385
  return index.__awaiter(this, void 0, void 0, function* () {
386
+ assertBleActive();
250
387
  if (noble)
251
388
  return;
252
389
  try {
@@ -263,9 +400,11 @@ function initializeNoble() {
263
400
  return;
264
401
  }
265
402
  const timeout = setTimeout(() => {
403
+ cleanup();
266
404
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout'));
267
405
  }, BLUETOOTH_INIT_TIMEOUT);
268
406
  const cleanup = () => {
407
+ pendingCancellations.delete(cancel);
269
408
  clearTimeout(timeout);
270
409
  if (noble) {
271
410
  noble.removeListener('stateChange', onStateChange);
@@ -290,8 +429,14 @@ function initializeNoble() {
290
429
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError));
291
430
  }
292
431
  };
432
+ const cancel = () => {
433
+ cleanup();
434
+ reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, 'Noble BLE is shutting down'));
435
+ };
436
+ pendingCancellations.add(cancel);
293
437
  noble.on('stateChange', onStateChange);
294
438
  });
439
+ assertBleActive();
295
440
  if (!persistentDiscoverListener) {
296
441
  persistentDiscoverListener = (peripheral) => {
297
442
  handleDeviceDiscovered(peripheral);
@@ -338,6 +483,8 @@ function broadcastToAllWebContents(channel, payload) {
338
483
  }
339
484
  function armIdleDisconnect(deviceId, ms = BLE_IDLE_DISCONNECT_MS, reason = 'idle') {
340
485
  clearIdleDisconnect(deviceId);
486
+ if (disposing)
487
+ return;
341
488
  idleDisconnectTimers.set(deviceId, setTimeout(() => {
342
489
  var _a;
343
490
  idleDisconnectTimers.delete(deviceId);
@@ -373,7 +520,7 @@ function armIdleDisconnect(deviceId, ms = BLE_IDLE_DISCONNECT_MS, reason = 'idle
373
520
  }, ms));
374
521
  }
375
522
  function cleanupDevice(deviceId, webContents, options = {}) {
376
- var _a;
523
+ var _a, _b;
377
524
  const { cleanupConnection = true, cleanupDiscoveredCache = false, sendDisconnectEvent = false, cancelOperations = true, reason = 'unknown', disconnectReason = hdShared.EBleDisconnectReason.DeviceDisconnected, } = options;
378
525
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Starting device cleanup', {
379
526
  deviceId,
@@ -387,6 +534,8 @@ function cleanupDevice(deviceId, webContents, options = {}) {
387
534
  const peripheral = connectedDevices.get(deviceId);
388
535
  const deviceName = ((_a = peripheral === null || peripheral === void 0 ? void 0 : peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device';
389
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`));
390
539
  const disconnectEntry = deviceDisconnectListeners.get(deviceId);
391
540
  if (disconnectEntry) {
392
541
  disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
@@ -465,6 +614,7 @@ function setupMtuListener(peripheral, deviceId, webContents) {
465
614
  }
466
615
  function writeCharacteristicWithoutResponse(deviceId, writeCharacteristic, buffer) {
467
616
  return index.__awaiter(this, void 0, void 0, function* () {
617
+ assertBleActive();
468
618
  return new Promise((resolve, reject) => {
469
619
  writeCharacteristic.write(buffer, true, (error) => {
470
620
  if (error) {
@@ -667,6 +817,7 @@ function waitForNobleScanStop(nobleInstance) {
667
817
  }
668
818
  function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS) {
669
819
  return index.__awaiter(this, void 0, void 0, function* () {
820
+ assertBleActive();
670
821
  if (!noble) {
671
822
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
672
823
  }
@@ -678,6 +829,7 @@ function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN
678
829
  if (settled)
679
830
  return;
680
831
  settled = true;
832
+ pendingCancellations.delete(cancel);
681
833
  if (timeoutId)
682
834
  clearTimeout(timeoutId);
683
835
  nobleInstance.removeListener('discover', onDiscover);
@@ -687,6 +839,7 @@ function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN
687
839
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, error.message));
688
840
  return;
689
841
  }
842
+ assertBleActive();
690
843
  if (peripheral) {
691
844
  discoveredDevices.set(peripheral.id, peripheral);
692
845
  }
@@ -706,6 +859,10 @@ function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN
706
859
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
707
860
  finish(null).catch(reject);
708
861
  }, timeoutMs);
862
+ const cancel = () => {
863
+ finish(null, new Error('Noble BLE is shutting down')).catch(reject);
864
+ };
865
+ pendingCancellations.add(cancel);
709
866
  nobleInstance.on('discover', onDiscover);
710
867
  nobleInstance.startScanning([], true, (error) => {
711
868
  if (error) {
@@ -717,7 +874,7 @@ function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN
717
874
  });
718
875
  });
719
876
  }
720
- function enumerateDevices() {
877
+ function enumerateDevices(isWindowDestroyed) {
721
878
  return index.__awaiter(this, void 0, void 0, function* () {
722
879
  if (!noble) {
723
880
  yield initializeNoble();
@@ -725,19 +882,31 @@ function enumerateDevices() {
725
882
  if (!noble) {
726
883
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
727
884
  }
885
+ assertBleActive();
886
+ if (isWindowDestroyed())
887
+ return [];
728
888
  const nobleInstance = noble;
729
889
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Starting device enumeration');
730
890
  discoveredDevices.clear();
731
891
  ensureDiscoverListener();
732
892
  return new Promise((resolve, reject) => {
733
893
  const devices = [];
894
+ let settled = false;
734
895
  let intervalId;
735
- const cleanup = () => index.__awaiter(this, void 0, void 0, function* () {
736
- clearTimeout(timeoutId);
737
- if (intervalId)
738
- clearInterval(intervalId);
739
- yield waitForNobleScanStop(nobleInstance);
740
- });
896
+ let cleanupPromise;
897
+ const cleanup = () => {
898
+ if (!cleanupPromise) {
899
+ settled = true;
900
+ pendingCancellations.delete(cancel);
901
+ clearTimeout(timeoutId);
902
+ if (intervalId)
903
+ clearInterval(intervalId);
904
+ cleanupPromise = waitForNobleScanStop(nobleInstance).finally(() => {
905
+ pendingEnumerations.delete(cancel);
906
+ });
907
+ }
908
+ return cleanupPromise;
909
+ };
741
910
  const pushDevice = (peripheral, id) => {
742
911
  var _a;
743
912
  if (devices.some(d => d.id === id))
@@ -754,13 +923,20 @@ function enumerateDevices() {
754
923
  connectedDevices.forEach(pushDevice);
755
924
  };
756
925
  const timeoutId = setTimeout(() => index.__awaiter(this, void 0, void 0, function* () {
926
+ if (settled)
927
+ return;
757
928
  checkDevices();
758
929
  yield cleanup();
759
930
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scan completed, found devices:', devices.length);
760
931
  resolve(devices);
761
932
  }), DEVICE_SCAN_TIMEOUT);
933
+ const cancel = () => cleanup().then(() => resolve([]), reject);
934
+ pendingCancellations.add(cancel);
935
+ pendingEnumerations.add(cancel);
762
936
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning for OneKey BLE devices');
763
937
  nobleInstance.startScanning([], true, (error) => index.__awaiter(this, void 0, void 0, function* () {
938
+ if (settled)
939
+ return;
764
940
  if (error) {
765
941
  yield cleanup();
766
942
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to start scanning:', error);
@@ -775,7 +951,7 @@ function enumerateDevices() {
775
951
  }
776
952
  function stopScanning() {
777
953
  return index.__awaiter(this, void 0, void 0, function* () {
778
- if (!noble)
954
+ if (!noble || nativeReleased)
779
955
  return;
780
956
  const nobleInstance = noble;
781
957
  yield waitForNobleScanStop(nobleInstance);
@@ -819,13 +995,16 @@ function getDevice(deviceId) {
819
995
  }
820
996
  function discoverServicesAndCharacteristics(peripheral) {
821
997
  return index.__awaiter(this, void 0, void 0, function* () {
998
+ assertBleActive();
822
999
  let timeoutId;
823
1000
  let onDisconnect;
824
1001
  const cleanup = () => {
825
1002
  if (timeoutId)
826
1003
  clearTimeout(timeoutId);
827
- if (onDisconnect)
1004
+ if (onDisconnect) {
828
1005
  peripheral.removeListener('disconnect', onDisconnect);
1006
+ pendingCancellations.delete(onDisconnect);
1007
+ }
829
1008
  };
830
1009
  const timeoutPromise = new Promise((_, reject) => {
831
1010
  timeoutId = setTimeout(() => {
@@ -838,6 +1017,7 @@ function discoverServicesAndCharacteristics(peripheral) {
838
1017
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Device disconnected during service discovery');
839
1018
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'Device disconnected during service discovery'));
840
1019
  };
1020
+ pendingCancellations.add(onDisconnect);
841
1021
  peripheral.once('disconnect', onDisconnect);
842
1022
  });
843
1023
  const discoveryPromise = (() => index.__awaiter(this, void 0, void 0, function* () {
@@ -852,6 +1032,7 @@ function discoverServicesAndCharacteristics(peripheral) {
852
1032
  }
853
1033
  });
854
1034
  });
1035
+ assertBleActive();
855
1036
  if (!services || services.length === 0) {
856
1037
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'No OneKey services found');
857
1038
  }
@@ -924,12 +1105,8 @@ function forceReconnectPeripheral(peripheral, deviceId) {
924
1105
  callback();
925
1106
  }), { timeoutMs: BLE_CLEANUP_TIMEOUT, timeoutBehavior: 'resolve' });
926
1107
  }
927
- yield runBleCallbackOperation(callback => peripheral.connect(callback), {
928
- timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
929
- timeoutBehavior: 'reject',
930
- });
1108
+ yield connectPeripheralWithCancellation(peripheral, deviceId);
931
1109
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Force reconnect successful');
932
- connectedDevices.set(deviceId, peripheral);
933
1110
  yield hdShared.wait(500);
934
1111
  });
935
1112
  }
@@ -946,17 +1123,7 @@ function freshScanAndDiscover(deviceId, webContents) {
946
1123
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found in fresh scan`);
947
1124
  }
948
1125
  discoveredDevices.set(deviceId, freshPeripheral);
949
- yield new Promise((resolve, reject) => {
950
- freshPeripheral.connect((error) => {
951
- if (error) {
952
- reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, `Fresh peripheral connection failed: ${error.message}`));
953
- }
954
- else {
955
- connectedDevices.set(deviceId, freshPeripheral);
956
- resolve();
957
- }
958
- });
959
- });
1126
+ yield connectPeripheralWithCancellation(freshPeripheral, deviceId, 'Fresh peripheral connection failed: ');
960
1127
  setupDisconnectListener(freshPeripheral, deviceId, webContents);
961
1128
  yield hdShared.wait(500);
962
1129
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Attempting service discovery with fresh peripheral');
@@ -995,6 +1162,7 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
995
1162
  minTimeout: 500,
996
1163
  maxTimeout: 3000,
997
1164
  onFailedAttempt: error => {
1165
+ assertBleActive();
998
1166
  logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] Service discovery attempt ${error.attemptNumber} failed:`, {
999
1167
  message: error.message,
1000
1168
  retriesLeft: error.retriesLeft,
@@ -1010,8 +1178,13 @@ function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
1010
1178
  yield forceReconnectPeripheral(peripheral, deviceId);
1011
1179
  }
1012
1180
  catch (resetError) {
1181
+ if (hdShared.isBleStaleBondHardwareError(resetError) ||
1182
+ (resetError === null || resetError === void 0 ? void 0 : resetError.errorCode) === hdShared.HardwareErrorCode.BleDeviceDisconnected) {
1183
+ throw resetError;
1184
+ }
1013
1185
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError);
1014
1186
  }
1187
+ assertBleActive();
1015
1188
  setupDisconnectListener(peripheral, deviceId, webContents);
1016
1189
  try {
1017
1190
  return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
@@ -1034,36 +1207,46 @@ const directConnectCooldownUntil = new Map();
1034
1207
  function tryDirectConnectById(deviceId) {
1035
1208
  var _a;
1036
1209
  return index.__awaiter(this, void 0, void 0, function* () {
1210
+ assertBleActive();
1037
1211
  const nobleInstance = noble;
1038
1212
  if (!(nobleInstance === null || nobleInstance === void 0 ? void 0 : nobleInstance.connectAsync))
1039
1213
  return undefined;
1040
1214
  const cooldownUntil = (_a = directConnectCooldownUntil.get(deviceId)) !== null && _a !== void 0 ? _a : 0;
1041
1215
  if (Date.now() < cooldownUntil)
1042
1216
  return undefined;
1217
+ let timer;
1218
+ let cancel;
1219
+ let abandoned = false;
1043
1220
  try {
1044
1221
  const directPromise = nobleInstance.connectAsync(deviceId);
1222
+ trackNativeConnection(directPromise.then((late) => index.__awaiter(this, void 0, void 0, function* () {
1223
+ if ((!abandoned && !disposing) || nativeReleased)
1224
+ return;
1225
+ const peripheral = late !== null && late !== void 0 ? late : discoveredDevices.get(deviceId);
1226
+ if ((peripheral === null || peripheral === void 0 ? void 0 : peripheral.state) === 'connected' && !connectedDevices.has(deviceId)) {
1227
+ peripheral.removeAllListeners('disconnect');
1228
+ yield runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
1229
+ timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
1230
+ timeoutBehavior: 'resolve',
1231
+ });
1232
+ }
1233
+ })), () => { var _a; return (_a = nobleInstance.cancelConnect) === null || _a === void 0 ? void 0 : _a.call(nobleInstance, deviceId); });
1045
1234
  const raced = yield Promise.race([
1046
1235
  directPromise,
1047
1236
  new Promise(resolve => {
1048
- setTimeout(() => resolve('timeout'), DIRECT_CONNECT_TIMEOUT_MS);
1237
+ cancel = () => {
1238
+ abandoned = true;
1239
+ resolve('timeout');
1240
+ };
1241
+ pendingCancellations.add(cancel);
1242
+ timer = setTimeout(cancel, DIRECT_CONNECT_TIMEOUT_MS);
1049
1243
  }),
1050
1244
  ]);
1051
- if (raced === 'timeout') {
1245
+ if (raced === 'timeout' || disposing) {
1052
1246
  directConnectCooldownUntil.set(deviceId, Date.now() + DIRECT_CONNECT_COOLDOWN_MS);
1053
1247
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct connect-by-id timed out, falling back to scan', {
1054
1248
  deviceId,
1055
1249
  });
1056
- directPromise
1057
- .then(late => {
1058
- const latePeripheral = late !== null && late !== void 0 ? late : discoveredDevices.get(deviceId);
1059
- if (latePeripheral &&
1060
- latePeripheral.state === 'connected' &&
1061
- !connectedDevices.has(deviceId)) {
1062
- latePeripheral.removeAllListeners('disconnect');
1063
- latePeripheral.disconnect(() => undefined);
1064
- }
1065
- })
1066
- .catch(() => undefined);
1067
1250
  return undefined;
1068
1251
  }
1069
1252
  const peripheral = raced !== null && raced !== void 0 ? raced : discoveredDevices.get(deviceId);
@@ -1080,6 +1263,11 @@ function tryDirectConnectById(deviceId) {
1080
1263
  });
1081
1264
  return undefined;
1082
1265
  }
1266
+ finally {
1267
+ clearTimeout(timer);
1268
+ if (cancel)
1269
+ pendingCancellations.delete(cancel);
1270
+ }
1083
1271
  });
1084
1272
  }
1085
1273
  function connectDevice(deviceId, webContents) {
@@ -1132,6 +1320,7 @@ function connectDevice(deviceId, webContents) {
1132
1320
  peripheral = byIdFirst ? yield scanForPeripheral() : yield connectById();
1133
1321
  }
1134
1322
  }
1323
+ assertBleActive();
1135
1324
  if (!peripheral) {
1136
1325
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found`);
1137
1326
  }
@@ -1142,6 +1331,7 @@ function connectDevice(deviceId, webContents) {
1142
1331
  if (!connectedDevices.has(deviceId)) {
1143
1332
  connectedDevices.set(deviceId, peripheral);
1144
1333
  }
1334
+ assertBleActive();
1145
1335
  setupDisconnectListener(peripheral, deviceId, webContents);
1146
1336
  if (deviceCharacteristics.has(deviceId)) {
1147
1337
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device characteristics already available');
@@ -1166,6 +1356,7 @@ function connectDevice(deviceId, webContents) {
1166
1356
  }
1167
1357
  try {
1168
1358
  const characteristics = yield setupConnectionAndDiscoverServices(peripheral, deviceId, webContents);
1359
+ assertBleActive();
1169
1360
  deviceCharacteristics.set(deviceId, characteristics);
1170
1361
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device ready for communication:', deviceId);
1171
1362
  }
@@ -1176,54 +1367,33 @@ function connectDevice(deviceId, webContents) {
1176
1367
  }
1177
1368
  return;
1178
1369
  }
1179
- return new Promise((resolve, reject) => {
1180
- let connectionTimedOut = false;
1181
- const timeout = setTimeout(() => {
1182
- connectionTimedOut = true;
1183
- reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, 'Connection timeout'));
1184
- }, NOBLE_BLE_CONNECTION_TIMEOUT_MS);
1185
- const connectedPeripheral = peripheral;
1186
- connectedPeripheral.connect((error) => index.__awaiter(this, void 0, void 0, function* () {
1187
- clearTimeout(timeout);
1188
- if (connectionTimedOut) {
1189
- if (!error) {
1190
- try {
1191
- connectedPeripheral.disconnect(() => undefined);
1192
- }
1193
- catch (_a) {
1194
- }
1195
- }
1196
- return;
1197
- }
1198
- if (error) {
1199
- logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection failed:', error);
1200
- reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, error.message));
1201
- return;
1202
- }
1203
- logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connected to device:', deviceId);
1204
- connectedDevices.set(deviceId, connectedPeripheral);
1205
- try {
1206
- const characteristics = yield setupConnectionAndDiscoverServices(connectedPeripheral, deviceId, webContents);
1207
- deviceCharacteristics.set(deviceId, characteristics);
1208
- logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device ready for communication:', deviceId);
1209
- resolve();
1210
- }
1211
- catch (setupError) {
1212
- logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection setup failed:', setupError);
1213
- disconnectDevice(deviceId)
1214
- .catch(() => undefined)
1215
- .then(() => reject(setupError));
1216
- }
1217
- }));
1218
- });
1370
+ const connectedPeripheral = peripheral;
1371
+ yield connectPeripheralWithCancellation(connectedPeripheral, deviceId);
1372
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connected to device:', deviceId);
1373
+ try {
1374
+ const characteristics = yield setupConnectionAndDiscoverServices(connectedPeripheral, deviceId, webContents);
1375
+ assertBleActive();
1376
+ deviceCharacteristics.set(deviceId, characteristics);
1377
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device ready for communication:', deviceId);
1378
+ }
1379
+ catch (setupError) {
1380
+ logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection setup failed:', setupError);
1381
+ yield disconnectDevice(deviceId).catch(() => undefined);
1382
+ throw setupError;
1383
+ }
1219
1384
  });
1220
1385
  }
1221
1386
  function disconnectDevice(deviceId) {
1387
+ var _a;
1222
1388
  return index.__awaiter(this, void 0, void 0, function* () {
1223
- const peripheral = connectedDevices.get(deviceId);
1389
+ if (nativeReleased)
1390
+ return;
1391
+ const pendingConnection = connectingDevices.get(deviceId);
1392
+ const peripheral = (_a = connectedDevices.get(deviceId)) !== null && _a !== void 0 ? _a : pendingConnection === null || pendingConnection === void 0 ? void 0 : pendingConnection.peripheral;
1224
1393
  if (!peripheral) {
1225
1394
  return;
1226
1395
  }
1396
+ pendingConnection === null || pendingConnection === void 0 ? void 0 : pendingConnection.cancel(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, `Device ${deviceId} connection cancelled`));
1227
1397
  const disconnectEntry = deviceDisconnectListeners.get(deviceId);
1228
1398
  if (disconnectEntry) {
1229
1399
  disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
@@ -1247,6 +1417,8 @@ function disconnectDevice(deviceId) {
1247
1417
  }
1248
1418
  function unsubscribeNotifications(deviceId) {
1249
1419
  return index.__awaiter(this, void 0, void 0, function* () {
1420
+ if (nativeReleased)
1421
+ return;
1250
1422
  const peripheral = connectedDevices.get(deviceId);
1251
1423
  const characteristics = deviceCharacteristics.get(deviceId);
1252
1424
  if (!peripheral || !characteristics) {
@@ -1266,7 +1438,8 @@ function unsubscribeNotifications(deviceId) {
1266
1438
  subscribedDevices.delete(deviceId);
1267
1439
  }
1268
1440
  finally {
1269
- subscriptionOperations.set(deviceId, 'idle');
1441
+ if (!disposing)
1442
+ subscriptionOperations.set(deviceId, 'idle');
1270
1443
  }
1271
1444
  });
1272
1445
  }
@@ -1316,10 +1489,12 @@ function subscribeNotifications(deviceId, callback) {
1316
1489
  timeoutMs: BLE_CLEANUP_TIMEOUT,
1317
1490
  timeoutBehavior: 'resolve',
1318
1491
  });
1492
+ assertBleActive();
1319
1493
  yield runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
1320
- timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
1494
+ timeoutMs: NOBLE_BLE_SUBSCRIBE_TIMEOUT_MS,
1321
1495
  timeoutBehavior: 'reject',
1322
1496
  });
1497
+ assertBleActive();
1323
1498
  notifyCharacteristic.on('data', (data) => {
1324
1499
  if (!pairedDevices.has(deviceId)) {
1325
1500
  pairedDevices.add(deviceId);
@@ -1332,29 +1507,53 @@ function subscribeNotifications(deviceId, callback) {
1332
1507
  const subscribeStartedAt = Date.now();
1333
1508
  try {
1334
1509
  yield rebuildAppSubscription(deviceId, notifyCharacteristic);
1510
+ assertBleActive();
1335
1511
  subscribedDevices.set(deviceId, true);
1336
1512
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Notification subscription active', {
1337
1513
  deviceId,
1338
1514
  ms: Date.now() - subscribeStartedAt,
1339
1515
  });
1340
1516
  }
1517
+ catch (error) {
1518
+ throw createNobleBleConnectionError(error, 'Notification subscription failed: ');
1519
+ }
1341
1520
  finally {
1342
- subscriptionOperations.set(deviceId, 'idle');
1521
+ if (!disposing)
1522
+ subscriptionOperations.set(deviceId, 'idle');
1343
1523
  }
1344
1524
  });
1345
1525
  }
1346
1526
  function setupNobleBleHandlers(webContents) {
1527
+ if (disposing)
1528
+ return;
1529
+ let windowDestroyed = false;
1347
1530
  try {
1348
1531
  logger = require('electron-log');
1349
1532
  const { ipcMain } = require('electron');
1533
+ const channels = new Set();
1534
+ removeIpcHandlers = () => channels.forEach(channel => ipcMain.removeHandler(channel));
1350
1535
  const handle = (channel, listener) => {
1536
+ channels.add(channel);
1351
1537
  ipcMain.removeHandler(channel);
1352
- ipcMain.handle(channel, listener);
1538
+ ipcMain.handle(channel, (...args) => index.__awaiter(this, void 0, void 0, function* () {
1539
+ try {
1540
+ if (windowCleanup)
1541
+ yield windowCleanup;
1542
+ assertBleActive();
1543
+ if (windowDestroyed) {
1544
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, 'BLE window destroyed');
1545
+ }
1546
+ return yield Promise.resolve(listener(...args));
1547
+ }
1548
+ catch (error) {
1549
+ return createNobleBleIpcErrorResponse(error);
1550
+ }
1551
+ }));
1353
1552
  };
1354
1553
  safeLog(logger, 'info', 'Setting up Noble BLE IPC handlers');
1355
1554
  handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE, () => index.__awaiter(this, void 0, void 0, function* () {
1356
1555
  try {
1357
- const devices = yield enumerateDevices();
1556
+ const devices = yield enumerateDevices(() => windowDestroyed);
1358
1557
  safeLog(logger, 'debug', 'Enumeration completed', {
1359
1558
  count: devices.length,
1360
1559
  devices: devices.map(device => ({ id: device.id, name: device.name })),
@@ -1446,13 +1645,29 @@ function setupNobleBleHandlers(webContents) {
1446
1645
  }
1447
1646
  }));
1448
1647
  webContents.on('destroyed', () => {
1648
+ windowDestroyed = true;
1649
+ if (disposing)
1650
+ return;
1449
1651
  safeLog(logger, 'info', 'Cleaning up Noble BLE handlers');
1450
- (() => index.__awaiter(this, void 0, void 0, function* () {
1652
+ const previousCleanup = windowCleanup;
1653
+ const enumerations = Array.from(pendingEnumerations, cancel => cancel());
1654
+ windowCleanup = (() => index.__awaiter(this, void 0, void 0, function* () {
1655
+ if (previousCleanup)
1656
+ yield previousCleanup;
1657
+ yield Promise.allSettled(enumerations);
1658
+ if (disposing)
1659
+ return;
1451
1660
  const deviceIds = Array.from(connectedDevices.keys());
1452
1661
  for (const deviceId of deviceIds) {
1662
+ if (disposing)
1663
+ return;
1453
1664
  yield unsubscribeNotifications(deviceId).catch(() => undefined);
1665
+ if (disposing)
1666
+ return;
1454
1667
  yield disconnectDevice(deviceId).catch(() => undefined);
1455
1668
  }
1669
+ if (disposing)
1670
+ return;
1456
1671
  yield stopScanning().catch(() => undefined);
1457
1672
  cleanupNobleListeners();
1458
1673
  discoveredDevices.clear();
@@ -1468,6 +1683,77 @@ function setupNobleBleHandlers(webContents) {
1468
1683
  throw error;
1469
1684
  }
1470
1685
  }
1686
+ function disposeNobleBleSupport(releaseNoble = instance => instance.stop()) {
1687
+ if (disposePromise)
1688
+ return disposePromise;
1689
+ disposing = true;
1690
+ removeIpcHandlers === null || removeIpcHandlers === void 0 ? void 0 : removeIpcHandlers();
1691
+ const instance = noble;
1692
+ const deviceIds = new Set([...connectedDevices.keys(), ...connectingDevices.keys()]);
1693
+ const pendingPeripherals = Array.from(connectingDevices.values(), pending => pending.peripheral);
1694
+ for (const cancel of pendingCancellations)
1695
+ cancel();
1696
+ for (const id of idleDisconnectTimers.keys())
1697
+ clearIdleDisconnect(id);
1698
+ for (const pending of connectingDevices.values()) {
1699
+ pending.cancel(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, 'Noble BLE is shutting down'));
1700
+ }
1701
+ const connections = Array.from(nativeConnections);
1702
+ for (const connection of connections) {
1703
+ try {
1704
+ connection.cancel();
1705
+ }
1706
+ catch (error) {
1707
+ logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Native connect cancellation failed', error);
1708
+ }
1709
+ }
1710
+ let timeout;
1711
+ disposePromise = (() => index.__awaiter(this, void 0, void 0, function* () {
1712
+ try {
1713
+ yield Promise.race([
1714
+ Promise.allSettled([
1715
+ windowCleanup,
1716
+ ...connections.map(connection => connection.settled),
1717
+ stopScanning(),
1718
+ ...pendingPeripherals.map(peripheral => runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
1719
+ timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
1720
+ timeoutBehavior: 'resolve',
1721
+ })),
1722
+ ...Array.from(deviceIds, (id) => index.__awaiter(this, void 0, void 0, function* () {
1723
+ yield unsubscribeNotifications(id).catch(() => undefined);
1724
+ yield disconnectDevice(id).catch(() => undefined);
1725
+ })),
1726
+ ]),
1727
+ new Promise(resolve => {
1728
+ timeout = setTimeout(() => {
1729
+ logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Process dispose timed out; releasing native manager');
1730
+ resolve();
1731
+ }, 3500);
1732
+ }),
1733
+ ]);
1734
+ }
1735
+ finally {
1736
+ clearTimeout(timeout);
1737
+ nativeReleased = true;
1738
+ cleanupNobleListeners();
1739
+ if (instance && persistentStateListener) {
1740
+ instance.removeListener('stateChange', persistentStateListener);
1741
+ persistentStateListener = null;
1742
+ }
1743
+ for (const id of deviceIds)
1744
+ cleanupDevice(id);
1745
+ discoveredDevices.clear();
1746
+ directConnectCooldownUntil.clear();
1747
+ if (instance)
1748
+ releaseNoble(instance);
1749
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Process dispose completed');
1750
+ }
1751
+ }))();
1752
+ return disposePromise;
1753
+ }
1471
1754
 
1755
+ exports.createNobleBleConnectionError = createNobleBleConnectionError;
1756
+ exports.createNobleBleIpcErrorResponse = createNobleBleIpcErrorResponse;
1757
+ exports.disposeNobleBleSupport = disposeNobleBleSupport;
1472
1758
  exports.resolveNobleBleWritePacingDelay = resolveNobleBleWritePacingDelay;
1473
1759
  exports.setupNobleBleHandlers = setupNobleBleHandlers;