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

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.
@@ -30,12 +30,44 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
30
30
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
31
31
  };
32
32
 
33
+ function isNobleBleIpcErrorResponse(response) {
34
+ var _a;
35
+ if (!response || typeof response !== 'object') {
36
+ return false;
37
+ }
38
+ const candidate = response;
39
+ return (candidate.type === 'NobleBleIpcError' &&
40
+ candidate.success === false &&
41
+ Boolean(candidate.error) &&
42
+ typeof ((_a = candidate.error) === null || _a === void 0 ? void 0 : _a.name) === 'string' &&
43
+ typeof candidate.error.message === 'string' &&
44
+ typeof candidate.error.errorCode === 'number');
45
+ }
46
+ function invokeNobleBleIpc(request) {
47
+ return __awaiter(this, void 0, void 0, function* () {
48
+ const response = yield request;
49
+ if (isNobleBleIpcErrorResponse(response)) {
50
+ return Promise.reject(response.error);
51
+ }
52
+ return response;
53
+ });
54
+ }
55
+
33
56
  function initNobleBleSupport(webContents) {
34
57
  return __awaiter(this, void 0, void 0, function* () {
35
- const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-0b455c4f.js'); });
58
+ const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-3a1d131d.js'); });
36
59
  setupNobleBleHandlers(webContents);
37
60
  });
38
61
  }
62
+ function disposeNobleBleSupport(releaseNoble) {
63
+ return __awaiter(this, void 0, void 0, function* () {
64
+ const handler = yield Promise.resolve().then(function () { return require('./noble-ble-handler-3a1d131d.js'); });
65
+ yield handler.disposeNobleBleSupport(releaseNoble);
66
+ });
67
+ }
39
68
 
40
69
  exports.__awaiter = __awaiter;
70
+ exports.disposeNobleBleSupport = disposeNobleBleSupport;
41
71
  exports.initNobleBleSupport = initNobleBleSupport;
72
+ exports.invokeNobleBleIpc = invokeNobleBleIpc;
73
+ exports.isNobleBleIpcErrorResponse = isNobleBleIpcErrorResponse;
package/dist/index.d.ts CHANGED
@@ -17,6 +17,7 @@ interface NobleModule {
17
17
  state: string;
18
18
  startScanning(serviceUUIDs: string[], allowDuplicates: boolean, callback?: (error?: Error) => void): void;
19
19
  stopScanning(callback?: () => void): void;
20
+ stop(): void;
20
21
  on(event: 'stateChange', listener: (state: string) => void): void;
21
22
  on(event: 'discover', listener: (peripheral: Peripheral) => void): void;
22
23
  removeListener(event: 'stateChange', listener: (state: string) => void): void;
@@ -48,6 +49,12 @@ type NobleBleIpcErrorResponse = {
48
49
  success: false;
49
50
  error: NobleBleIpcErrorPayload;
50
51
  };
52
+ declare function isNobleBleIpcErrorResponse(response: unknown): response is NobleBleIpcErrorResponse;
53
+ /**
54
+ * Keeps structured Noble errors intact across Electron's IPC and contextBridge
55
+ * boundaries. Every preload implementation must use this for Noble invokes.
56
+ */
57
+ declare function invokeNobleBleIpc<T>(request: Promise<T | NobleBleIpcErrorResponse>): Promise<T>;
51
58
  interface NobleBleAPI {
52
59
  enumerate: () => Promise<{
53
60
  id: string;
@@ -92,5 +99,9 @@ interface DesktopAPI {
92
99
  }
93
100
 
94
101
  declare function initNobleBleSupport(webContents: WebContents): Promise<void>;
102
+ /** Terminal process cleanup. A host sharing Noble can defer native stop until all users are idle. */
103
+ declare function disposeNobleBleSupport(releaseNoble?: (noble: {
104
+ stop(): void;
105
+ }) => void): Promise<void>;
95
106
 
96
- export { CharacteristicPair, DesktopAPI, DeviceInfo, Logger, NobleBleAPI, NobleBleIpcErrorPayload, NobleBleIpcErrorResponse, NobleBleWriteOptions, NobleModule, initNobleBleSupport };
107
+ export { CharacteristicPair, DesktopAPI, DeviceInfo, Logger, NobleBleAPI, NobleBleIpcErrorPayload, NobleBleIpcErrorResponse, NobleBleWriteOptions, NobleModule, disposeNobleBleSupport, initNobleBleSupport, invokeNobleBleIpc, isNobleBleIpcErrorResponse };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,cAAc,SAAS,CAAC;AAGxB,YAAY,EACV,UAAU,EACV,WAAW,EACX,uBAAuB,EACvB,wBAAwB,EACxB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAE7B,wBAAsB,mBAAmB,CAAC,WAAW,EAAE,WAAW,iBAGjE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,cAAc,SAAS,CAAC;AAGxB,YAAY,EACV,UAAU,EACV,WAAW,EACX,uBAAuB,EACvB,wBAAwB,EACxB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,iBAAiB,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEpF,wBAAsB,mBAAmB,CAAC,WAAW,EAAE,WAAW,iBAGjE;AAGD,wBAAsB,sBAAsB,CAAC,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE;IAAE,IAAI,IAAI,IAAI,CAAA;CAAE,KAAK,IAAI,iBAG5F"}
package/dist/index.js CHANGED
@@ -2,8 +2,11 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var index = require('./index-d51c7da6.js');
5
+ var index = require('./index-8e6acfc1.js');
6
6
 
7
7
 
8
8
 
9
+ exports.disposeNobleBleSupport = index.disposeNobleBleSupport;
9
10
  exports.initNobleBleSupport = index.initNobleBleSupport;
11
+ exports.invokeNobleBleIpc = index.invokeNobleBleIpc;
12
+ exports.isNobleBleIpcErrorResponse = index.isNobleBleIpcErrorResponse;
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-d51c7da6.js');
3
+ var index = require('./index-8e6acfc1.js');
4
4
  var hdShared = require('@onekeyfe/hd-shared');
5
5
  var pRetry = require('p-retry');
6
6
 
@@ -100,9 +100,19 @@ 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 disposePromise;
105
+ let removeIpcHandlers;
106
+ const pendingCancellations = new Set();
107
+ function assertBleActive() {
108
+ if (disposing) {
109
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, 'Noble BLE is shutting down');
110
+ }
111
+ }
103
112
  function createNobleBleConnectionError(error, messagePrefix = '') {
104
113
  const errorMessage = error.message;
105
- const isInvalidMacOsBond = error.nativeErrorCode === 14 && error.nativeErrorDomain === 'CBErrorDomain';
114
+ const isInvalidMacOsBond = (error.nativeErrorCode === 14 && error.nativeErrorDomain === 'CBErrorDomain') ||
115
+ (error.nativeErrorCode === 15 && error.nativeErrorDomain === 'CBATTErrorDomain');
106
116
  if (isInvalidMacOsBond) {
107
117
  const nativeErrorMessage = `${messagePrefix}${errorMessage}`;
108
118
  return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleBondInvalid, `${hdShared.HardwareErrorCodeMessage[hdShared.HardwareErrorCode.BleBondInvalid]} (${nativeErrorMessage})`, {
@@ -143,6 +153,57 @@ let persistentStateListener = null;
143
153
  let persistentDiscoverListener = null;
144
154
  const discoveredDevices = new Map();
145
155
  const connectedDevices = new Map();
156
+ const connectingDevices = new Map();
157
+ let nextConnectionGeneration = 0;
158
+ function connectPeripheralWithCancellation(peripheral, deviceId, messagePrefix = '') {
159
+ assertBleActive();
160
+ return new Promise((resolve, reject) => {
161
+ var _a;
162
+ const generation = ++nextConnectionGeneration;
163
+ let settled = false;
164
+ const settle = (settler) => {
165
+ var _a;
166
+ if (settled)
167
+ return false;
168
+ settled = true;
169
+ if (((_a = connectingDevices.get(deviceId)) === null || _a === void 0 ? void 0 : _a.generation) === generation) {
170
+ connectingDevices.delete(deviceId);
171
+ }
172
+ settler();
173
+ return true;
174
+ };
175
+ const pendingConnection = {
176
+ peripheral,
177
+ generation,
178
+ cancel: error => {
179
+ settle(() => reject(error));
180
+ },
181
+ };
182
+ (_a = connectingDevices
183
+ .get(deviceId)) === null || _a === void 0 ? void 0 : _a.cancel(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, `Device ${deviceId} connect attempt was superseded`));
184
+ connectingDevices.set(deviceId, pendingConnection);
185
+ peripheral.connect(error => {
186
+ if (error) {
187
+ settle(() => reject(createNobleBleConnectionError(error, messagePrefix)));
188
+ return;
189
+ }
190
+ if (!settle(() => {
191
+ connectedDevices.set(deviceId, peripheral);
192
+ resolve();
193
+ })) {
194
+ const activeConnection = connectingDevices.get(deviceId);
195
+ if (!activeConnection || activeConnection.peripheral !== peripheral) {
196
+ try {
197
+ peripheral.removeAllListeners('disconnect');
198
+ peripheral.disconnect(() => undefined);
199
+ }
200
+ catch (_a) {
201
+ }
202
+ }
203
+ }
204
+ });
205
+ });
206
+ }
146
207
  const pairedDevices = new Set();
147
208
  const deviceCharacteristics = new Map();
148
209
  const notificationCallbacks = new Map();
@@ -281,6 +342,7 @@ function updateBluetoothState(state) {
281
342
  }
282
343
  function initializeNoble() {
283
344
  return index.__awaiter(this, void 0, void 0, function* () {
345
+ assertBleActive();
284
346
  if (noble)
285
347
  return;
286
348
  try {
@@ -297,9 +359,11 @@ function initializeNoble() {
297
359
  return;
298
360
  }
299
361
  const timeout = setTimeout(() => {
362
+ cleanup();
300
363
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout'));
301
364
  }, BLUETOOTH_INIT_TIMEOUT);
302
365
  const cleanup = () => {
366
+ pendingCancellations.delete(cancel);
303
367
  clearTimeout(timeout);
304
368
  if (noble) {
305
369
  noble.removeListener('stateChange', onStateChange);
@@ -324,8 +388,14 @@ function initializeNoble() {
324
388
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError));
325
389
  }
326
390
  };
391
+ const cancel = () => {
392
+ cleanup();
393
+ reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, 'Noble BLE is shutting down'));
394
+ };
395
+ pendingCancellations.add(cancel);
327
396
  noble.on('stateChange', onStateChange);
328
397
  });
398
+ assertBleActive();
329
399
  if (!persistentDiscoverListener) {
330
400
  persistentDiscoverListener = (peripheral) => {
331
401
  handleDeviceDiscovered(peripheral);
@@ -372,6 +442,8 @@ function broadcastToAllWebContents(channel, payload) {
372
442
  }
373
443
  function armIdleDisconnect(deviceId, ms = BLE_IDLE_DISCONNECT_MS, reason = 'idle') {
374
444
  clearIdleDisconnect(deviceId);
445
+ if (disposing)
446
+ return;
375
447
  idleDisconnectTimers.set(deviceId, setTimeout(() => {
376
448
  var _a;
377
449
  idleDisconnectTimers.delete(deviceId);
@@ -407,7 +479,7 @@ function armIdleDisconnect(deviceId, ms = BLE_IDLE_DISCONNECT_MS, reason = 'idle
407
479
  }, ms));
408
480
  }
409
481
  function cleanupDevice(deviceId, webContents, options = {}) {
410
- var _a;
482
+ var _a, _b;
411
483
  const { cleanupConnection = true, cleanupDiscoveredCache = false, sendDisconnectEvent = false, cancelOperations = true, reason = 'unknown', disconnectReason = hdShared.EBleDisconnectReason.DeviceDisconnected, } = options;
412
484
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Starting device cleanup', {
413
485
  deviceId,
@@ -421,6 +493,8 @@ function cleanupDevice(deviceId, webContents, options = {}) {
421
493
  const peripheral = connectedDevices.get(deviceId);
422
494
  const deviceName = ((_a = peripheral === null || peripheral === void 0 ? void 0 : peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device';
423
495
  if (cleanupConnection) {
496
+ (_b = connectingDevices
497
+ .get(deviceId)) === null || _b === void 0 ? void 0 : _b.cancel(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, `Device ${deviceId} disconnected while connecting`));
424
498
  const disconnectEntry = deviceDisconnectListeners.get(deviceId);
425
499
  if (disconnectEntry) {
426
500
  disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
@@ -499,6 +573,7 @@ function setupMtuListener(peripheral, deviceId, webContents) {
499
573
  }
500
574
  function writeCharacteristicWithoutResponse(deviceId, writeCharacteristic, buffer) {
501
575
  return index.__awaiter(this, void 0, void 0, function* () {
576
+ assertBleActive();
502
577
  return new Promise((resolve, reject) => {
503
578
  writeCharacteristic.write(buffer, true, (error) => {
504
579
  if (error) {
@@ -701,6 +776,7 @@ function waitForNobleScanStop(nobleInstance) {
701
776
  }
702
777
  function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS) {
703
778
  return index.__awaiter(this, void 0, void 0, function* () {
779
+ assertBleActive();
704
780
  if (!noble) {
705
781
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
706
782
  }
@@ -712,6 +788,7 @@ function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN
712
788
  if (settled)
713
789
  return;
714
790
  settled = true;
791
+ pendingCancellations.delete(cancel);
715
792
  if (timeoutId)
716
793
  clearTimeout(timeoutId);
717
794
  nobleInstance.removeListener('discover', onDiscover);
@@ -721,6 +798,7 @@ function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN
721
798
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, error.message));
722
799
  return;
723
800
  }
801
+ assertBleActive();
724
802
  if (peripheral) {
725
803
  discoveredDevices.set(peripheral.id, peripheral);
726
804
  }
@@ -740,6 +818,10 @@ function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN
740
818
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
741
819
  finish(null).catch(reject);
742
820
  }, timeoutMs);
821
+ const cancel = () => {
822
+ finish(null, new Error('Noble BLE is shutting down')).catch(reject);
823
+ };
824
+ pendingCancellations.add(cancel);
743
825
  nobleInstance.on('discover', onDiscover);
744
826
  nobleInstance.startScanning([], true, (error) => {
745
827
  if (error) {
@@ -759,14 +841,18 @@ function enumerateDevices() {
759
841
  if (!noble) {
760
842
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
761
843
  }
844
+ assertBleActive();
762
845
  const nobleInstance = noble;
763
846
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Starting device enumeration');
764
847
  discoveredDevices.clear();
765
848
  ensureDiscoverListener();
766
849
  return new Promise((resolve, reject) => {
767
850
  const devices = [];
851
+ let settled = false;
768
852
  let intervalId;
769
853
  const cleanup = () => index.__awaiter(this, void 0, void 0, function* () {
854
+ settled = true;
855
+ pendingCancellations.delete(cancel);
770
856
  clearTimeout(timeoutId);
771
857
  if (intervalId)
772
858
  clearInterval(intervalId);
@@ -788,13 +874,21 @@ function enumerateDevices() {
788
874
  connectedDevices.forEach(pushDevice);
789
875
  };
790
876
  const timeoutId = setTimeout(() => index.__awaiter(this, void 0, void 0, function* () {
877
+ if (settled)
878
+ return;
791
879
  checkDevices();
792
880
  yield cleanup();
793
881
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scan completed, found devices:', devices.length);
794
882
  resolve(devices);
795
883
  }), DEVICE_SCAN_TIMEOUT);
884
+ const cancel = () => {
885
+ cleanup().then(() => resolve([]), reject);
886
+ };
887
+ pendingCancellations.add(cancel);
796
888
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning for OneKey BLE devices');
797
889
  nobleInstance.startScanning([], true, (error) => index.__awaiter(this, void 0, void 0, function* () {
890
+ if (settled)
891
+ return;
798
892
  if (error) {
799
893
  yield cleanup();
800
894
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to start scanning:', error);
@@ -853,13 +947,16 @@ function getDevice(deviceId) {
853
947
  }
854
948
  function discoverServicesAndCharacteristics(peripheral) {
855
949
  return index.__awaiter(this, void 0, void 0, function* () {
950
+ assertBleActive();
856
951
  let timeoutId;
857
952
  let onDisconnect;
858
953
  const cleanup = () => {
859
954
  if (timeoutId)
860
955
  clearTimeout(timeoutId);
861
- if (onDisconnect)
956
+ if (onDisconnect) {
862
957
  peripheral.removeListener('disconnect', onDisconnect);
958
+ pendingCancellations.delete(onDisconnect);
959
+ }
863
960
  };
864
961
  const timeoutPromise = new Promise((_, reject) => {
865
962
  timeoutId = setTimeout(() => {
@@ -872,6 +969,7 @@ function discoverServicesAndCharacteristics(peripheral) {
872
969
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Device disconnected during service discovery');
873
970
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'Device disconnected during service discovery'));
874
971
  };
972
+ pendingCancellations.add(onDisconnect);
875
973
  peripheral.once('disconnect', onDisconnect);
876
974
  });
877
975
  const discoveryPromise = (() => index.__awaiter(this, void 0, void 0, function* () {
@@ -886,6 +984,7 @@ function discoverServicesAndCharacteristics(peripheral) {
886
984
  }
887
985
  });
888
986
  });
987
+ assertBleActive();
889
988
  if (!services || services.length === 0) {
890
989
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'No OneKey services found');
891
990
  }
@@ -958,17 +1057,8 @@ function forceReconnectPeripheral(peripheral, deviceId) {
958
1057
  callback();
959
1058
  }), { timeoutMs: BLE_CLEANUP_TIMEOUT, timeoutBehavior: 'resolve' });
960
1059
  }
961
- yield new Promise((resolve, reject) => {
962
- peripheral.connect(error => {
963
- if (error) {
964
- reject(createNobleBleConnectionError(error));
965
- return;
966
- }
967
- resolve();
968
- });
969
- });
1060
+ yield connectPeripheralWithCancellation(peripheral, deviceId);
970
1061
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Force reconnect successful');
971
- connectedDevices.set(deviceId, peripheral);
972
1062
  yield hdShared.wait(500);
973
1063
  });
974
1064
  }
@@ -985,17 +1075,7 @@ function freshScanAndDiscover(deviceId, webContents) {
985
1075
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found in fresh scan`);
986
1076
  }
987
1077
  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
- });
1078
+ yield connectPeripheralWithCancellation(freshPeripheral, deviceId, 'Fresh peripheral connection failed: ');
999
1079
  setupDisconnectListener(freshPeripheral, deviceId, webContents);
1000
1080
  yield hdShared.wait(500);
1001
1081
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Attempting service discovery with fresh peripheral');
@@ -1034,6 +1114,7 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
1034
1114
  minTimeout: 500,
1035
1115
  maxTimeout: 3000,
1036
1116
  onFailedAttempt: error => {
1117
+ assertBleActive();
1037
1118
  logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] Service discovery attempt ${error.attemptNumber} failed:`, {
1038
1119
  message: error.message,
1039
1120
  retriesLeft: error.retriesLeft,
@@ -1049,11 +1130,13 @@ function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
1049
1130
  yield forceReconnectPeripheral(peripheral, deviceId);
1050
1131
  }
1051
1132
  catch (resetError) {
1052
- if (hdShared.isBleStaleBondHardwareError(resetError)) {
1133
+ if (hdShared.isBleStaleBondHardwareError(resetError) ||
1134
+ (resetError === null || resetError === void 0 ? void 0 : resetError.errorCode) === hdShared.HardwareErrorCode.BleDeviceDisconnected) {
1053
1135
  throw resetError;
1054
1136
  }
1055
1137
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError);
1056
1138
  }
1139
+ assertBleActive();
1057
1140
  setupDisconnectListener(peripheral, deviceId, webContents);
1058
1141
  try {
1059
1142
  return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
@@ -1076,21 +1159,26 @@ const directConnectCooldownUntil = new Map();
1076
1159
  function tryDirectConnectById(deviceId) {
1077
1160
  var _a;
1078
1161
  return index.__awaiter(this, void 0, void 0, function* () {
1162
+ assertBleActive();
1079
1163
  const nobleInstance = noble;
1080
1164
  if (!(nobleInstance === null || nobleInstance === void 0 ? void 0 : nobleInstance.connectAsync))
1081
1165
  return undefined;
1082
1166
  const cooldownUntil = (_a = directConnectCooldownUntil.get(deviceId)) !== null && _a !== void 0 ? _a : 0;
1083
1167
  if (Date.now() < cooldownUntil)
1084
1168
  return undefined;
1169
+ let timer;
1170
+ let cancel;
1085
1171
  try {
1086
1172
  const directPromise = nobleInstance.connectAsync(deviceId);
1087
1173
  const raced = yield Promise.race([
1088
1174
  directPromise,
1089
1175
  new Promise(resolve => {
1090
- setTimeout(() => resolve('timeout'), DIRECT_CONNECT_TIMEOUT_MS);
1176
+ cancel = () => resolve('timeout');
1177
+ pendingCancellations.add(cancel);
1178
+ timer = setTimeout(cancel, DIRECT_CONNECT_TIMEOUT_MS);
1091
1179
  }),
1092
1180
  ]);
1093
- if (raced === 'timeout') {
1181
+ if (raced === 'timeout' || disposing) {
1094
1182
  directConnectCooldownUntil.set(deviceId, Date.now() + DIRECT_CONNECT_COOLDOWN_MS);
1095
1183
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct connect-by-id timed out, falling back to scan', {
1096
1184
  deviceId,
@@ -1122,6 +1210,11 @@ function tryDirectConnectById(deviceId) {
1122
1210
  });
1123
1211
  return undefined;
1124
1212
  }
1213
+ finally {
1214
+ clearTimeout(timer);
1215
+ if (cancel)
1216
+ pendingCancellations.delete(cancel);
1217
+ }
1125
1218
  });
1126
1219
  }
1127
1220
  function connectDevice(deviceId, webContents) {
@@ -1174,6 +1267,7 @@ function connectDevice(deviceId, webContents) {
1174
1267
  peripheral = byIdFirst ? yield scanForPeripheral() : yield connectById();
1175
1268
  }
1176
1269
  }
1270
+ assertBleActive();
1177
1271
  if (!peripheral) {
1178
1272
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found`);
1179
1273
  }
@@ -1184,6 +1278,7 @@ function connectDevice(deviceId, webContents) {
1184
1278
  if (!connectedDevices.has(deviceId)) {
1185
1279
  connectedDevices.set(deviceId, peripheral);
1186
1280
  }
1281
+ assertBleActive();
1187
1282
  setupDisconnectListener(peripheral, deviceId, webContents);
1188
1283
  if (deviceCharacteristics.has(deviceId)) {
1189
1284
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device characteristics already available');
@@ -1208,6 +1303,7 @@ function connectDevice(deviceId, webContents) {
1208
1303
  }
1209
1304
  try {
1210
1305
  const characteristics = yield setupConnectionAndDiscoverServices(peripheral, deviceId, webContents);
1306
+ assertBleActive();
1211
1307
  deviceCharacteristics.set(deviceId, characteristics);
1212
1308
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device ready for communication:', deviceId);
1213
1309
  }
@@ -1218,38 +1314,31 @@ function connectDevice(deviceId, webContents) {
1218
1314
  }
1219
1315
  return;
1220
1316
  }
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
- });
1317
+ const connectedPeripheral = peripheral;
1318
+ yield connectPeripheralWithCancellation(connectedPeripheral, deviceId);
1319
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connected to device:', deviceId);
1320
+ try {
1321
+ const characteristics = yield setupConnectionAndDiscoverServices(connectedPeripheral, deviceId, webContents);
1322
+ assertBleActive();
1323
+ deviceCharacteristics.set(deviceId, characteristics);
1324
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device ready for communication:', deviceId);
1325
+ }
1326
+ catch (setupError) {
1327
+ logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection setup failed:', setupError);
1328
+ yield disconnectDevice(deviceId).catch(() => undefined);
1329
+ throw setupError;
1330
+ }
1245
1331
  });
1246
1332
  }
1247
1333
  function disconnectDevice(deviceId) {
1334
+ var _a;
1248
1335
  return index.__awaiter(this, void 0, void 0, function* () {
1249
- const peripheral = connectedDevices.get(deviceId);
1336
+ const pendingConnection = connectingDevices.get(deviceId);
1337
+ const peripheral = (_a = connectedDevices.get(deviceId)) !== null && _a !== void 0 ? _a : pendingConnection === null || pendingConnection === void 0 ? void 0 : pendingConnection.peripheral;
1250
1338
  if (!peripheral) {
1251
1339
  return;
1252
1340
  }
1341
+ pendingConnection === null || pendingConnection === void 0 ? void 0 : pendingConnection.cancel(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, `Device ${deviceId} connection cancelled`));
1253
1342
  const disconnectEntry = deviceDisconnectListeners.get(deviceId);
1254
1343
  if (disconnectEntry) {
1255
1344
  disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
@@ -1292,7 +1381,8 @@ function unsubscribeNotifications(deviceId) {
1292
1381
  subscribedDevices.delete(deviceId);
1293
1382
  }
1294
1383
  finally {
1295
- subscriptionOperations.set(deviceId, 'idle');
1384
+ if (!disposing)
1385
+ subscriptionOperations.set(deviceId, 'idle');
1296
1386
  }
1297
1387
  });
1298
1388
  }
@@ -1342,10 +1432,12 @@ function subscribeNotifications(deviceId, callback) {
1342
1432
  timeoutMs: BLE_CLEANUP_TIMEOUT,
1343
1433
  timeoutBehavior: 'resolve',
1344
1434
  });
1435
+ assertBleActive();
1345
1436
  yield runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
1346
1437
  timeoutMs: NOBLE_BLE_SUBSCRIBE_TIMEOUT_MS,
1347
1438
  timeoutBehavior: 'reject',
1348
1439
  });
1440
+ assertBleActive();
1349
1441
  notifyCharacteristic.on('data', (data) => {
1350
1442
  if (!pairedDevices.has(deviceId)) {
1351
1443
  pairedDevices.add(deviceId);
@@ -1358,25 +1450,36 @@ function subscribeNotifications(deviceId, callback) {
1358
1450
  const subscribeStartedAt = Date.now();
1359
1451
  try {
1360
1452
  yield rebuildAppSubscription(deviceId, notifyCharacteristic);
1453
+ assertBleActive();
1361
1454
  subscribedDevices.set(deviceId, true);
1362
1455
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Notification subscription active', {
1363
1456
  deviceId,
1364
1457
  ms: Date.now() - subscribeStartedAt,
1365
1458
  });
1366
1459
  }
1460
+ catch (error) {
1461
+ throw createNobleBleConnectionError(error, 'Notification subscription failed: ');
1462
+ }
1367
1463
  finally {
1368
- subscriptionOperations.set(deviceId, 'idle');
1464
+ if (!disposing)
1465
+ subscriptionOperations.set(deviceId, 'idle');
1369
1466
  }
1370
1467
  });
1371
1468
  }
1372
1469
  function setupNobleBleHandlers(webContents) {
1470
+ if (disposing)
1471
+ return;
1373
1472
  try {
1374
1473
  logger = require('electron-log');
1375
1474
  const { ipcMain } = require('electron');
1475
+ const channels = new Set();
1476
+ removeIpcHandlers = () => channels.forEach(channel => ipcMain.removeHandler(channel));
1376
1477
  const handle = (channel, listener) => {
1478
+ channels.add(channel);
1377
1479
  ipcMain.removeHandler(channel);
1378
1480
  ipcMain.handle(channel, (...args) => index.__awaiter(this, void 0, void 0, function* () {
1379
1481
  try {
1482
+ assertBleActive();
1380
1483
  return yield Promise.resolve(listener(...args));
1381
1484
  }
1382
1485
  catch (error) {
@@ -1479,6 +1582,8 @@ function setupNobleBleHandlers(webContents) {
1479
1582
  }
1480
1583
  }));
1481
1584
  webContents.on('destroyed', () => {
1585
+ if (disposing)
1586
+ return;
1482
1587
  safeLog(logger, 'info', 'Cleaning up Noble BLE handlers');
1483
1588
  (() => index.__awaiter(this, void 0, void 0, function* () {
1484
1589
  const deviceIds = Array.from(connectedDevices.keys());
@@ -1501,8 +1606,65 @@ function setupNobleBleHandlers(webContents) {
1501
1606
  throw error;
1502
1607
  }
1503
1608
  }
1609
+ function disposeNobleBleSupport(releaseNoble = instance => instance.stop()) {
1610
+ if (disposePromise)
1611
+ return disposePromise;
1612
+ disposing = true;
1613
+ removeIpcHandlers === null || removeIpcHandlers === void 0 ? void 0 : removeIpcHandlers();
1614
+ const instance = noble;
1615
+ const deviceIds = new Set([...connectedDevices.keys(), ...connectingDevices.keys()]);
1616
+ const pendingPeripherals = Array.from(connectingDevices.values(), pending => pending.peripheral);
1617
+ for (const cancel of pendingCancellations)
1618
+ cancel();
1619
+ for (const id of idleDisconnectTimers.keys())
1620
+ clearIdleDisconnect(id);
1621
+ for (const pending of connectingDevices.values()) {
1622
+ pending.cancel(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, 'Noble BLE is shutting down'));
1623
+ }
1624
+ let timeout;
1625
+ disposePromise = (() => index.__awaiter(this, void 0, void 0, function* () {
1626
+ try {
1627
+ yield Promise.race([
1628
+ Promise.allSettled([
1629
+ stopScanning(),
1630
+ ...pendingPeripherals.map(peripheral => runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
1631
+ timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
1632
+ timeoutBehavior: 'resolve',
1633
+ })),
1634
+ ...Array.from(deviceIds, (id) => index.__awaiter(this, void 0, void 0, function* () {
1635
+ yield unsubscribeNotifications(id).catch(() => undefined);
1636
+ yield disconnectDevice(id).catch(() => undefined);
1637
+ })),
1638
+ ]),
1639
+ new Promise(resolve => {
1640
+ timeout = setTimeout(() => {
1641
+ logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Process dispose timed out; releasing native manager');
1642
+ resolve();
1643
+ }, 3500);
1644
+ }),
1645
+ ]);
1646
+ }
1647
+ finally {
1648
+ clearTimeout(timeout);
1649
+ cleanupNobleListeners();
1650
+ if (instance && persistentStateListener) {
1651
+ instance.removeListener('stateChange', persistentStateListener);
1652
+ persistentStateListener = null;
1653
+ }
1654
+ for (const id of deviceIds)
1655
+ cleanupDevice(id);
1656
+ discoveredDevices.clear();
1657
+ directConnectCooldownUntil.clear();
1658
+ if (instance)
1659
+ releaseNoble(instance);
1660
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Process dispose completed');
1661
+ }
1662
+ }))();
1663
+ return disposePromise;
1664
+ }
1504
1665
 
1505
1666
  exports.createNobleBleConnectionError = createNobleBleConnectionError;
1506
1667
  exports.createNobleBleIpcErrorResponse = createNobleBleIpcErrorResponse;
1668
+ exports.disposeNobleBleSupport = disposeNobleBleSupport;
1507
1669
  exports.resolveNobleBleWritePacingDelay = resolveNobleBleWritePacingDelay;
1508
1670
  exports.setupNobleBleHandlers = setupNobleBleHandlers;