@onekeyfe/hwk-trezor-connector-electron-ble 1.2.2-alpha.8 → 1.2.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.
package/dist/main.d.mts CHANGED
@@ -14,6 +14,7 @@ interface NobleLike {
14
14
  removeListener(event: string, handler: (...args: any[]) => void): NobleLike;
15
15
  startScanningAsync(serviceUuids: string[], allowDuplicates: boolean): Promise<void>;
16
16
  stopScanningAsync(): Promise<void>;
17
+ stop?(): void;
17
18
  /**
18
19
  * Connect by id/address with NO scan. Both native backends support this and
19
20
  * emit a `discover` for the peripheral as a side effect: Windows synthesizes
@@ -22,6 +23,7 @@ interface NobleLike {
22
23
  * (lib/mac/src/ble_manager.mm). Optional so a stub noble can omit it.
23
24
  */
24
25
  connectAsync?(idOrAddress: string): Promise<NoblePeripheralLike | undefined>;
26
+ cancelConnect?(idOrAddress: string): void;
25
27
  reset?(): Promise<void>;
26
28
  }
27
29
  interface NoblePeripheralLike {
@@ -44,6 +46,7 @@ interface NoblePeripheralLike {
44
46
  rssi: number;
45
47
  state: string;
46
48
  connectAsync(): Promise<void>;
49
+ cancelConnect?(): void;
47
50
  disconnectAsync(): Promise<void>;
48
51
  discoverSomeServicesAndCharacteristicsAsync(serviceUuids: string[], characteristicUuids: string[]): Promise<{
49
52
  characteristics: NobleCharacteristicLike[];
@@ -89,13 +92,20 @@ declare class NobleBleHandler {
89
92
  private readonly _discovered;
90
93
  private readonly _lastSeen;
91
94
  private readonly _connected;
92
- private _stateChangeHandler?;
93
95
  private _discoverHandler?;
94
96
  private _scanning;
95
97
  private _idleStopTimer?;
96
98
  private _onNotification?;
97
99
  private _onDeviceDisconnected?;
98
100
  private _initialized;
101
+ private _disposed;
102
+ private _disposePromise?;
103
+ private _releasePromise?;
104
+ private _initPromise?;
105
+ private readonly _nobleInstances;
106
+ private readonly _pendingCancellations;
107
+ private readonly _connectAttempts;
108
+ private _nativeReleased;
99
109
  private _lastNobleRecoverAt?;
100
110
  /** The connect currently in flight, so cancelPairing can abandon it. */
101
111
  private _activeConnect?;
@@ -220,8 +230,17 @@ declare class NobleBleHandler {
220
230
  subscribe(id: string): Promise<void>;
221
231
  unsubscribe(id: string): Promise<void>;
222
232
  write(id: string, hexData: string): Promise<void>;
223
- /** Tear down all active connections — called on app quit. */
233
+ /** Retire a renderer's handler without stopping a process-wide native manager. */
224
234
  dispose(): Promise<void>;
235
+ /**
236
+ * Terminal native release, including instances replaced by adapter recovery.
237
+ * A host sharing Noble must defer stop() until all transports have disposed,
238
+ * and deduplicate instances passed to releaseNoble across those transports.
239
+ */
240
+ disposeForAppQuit(releaseNoble?: (instance: {
241
+ stop?(): void;
242
+ }) => void): Promise<void>;
243
+ private _assertActive;
225
244
  private _cleanupDevice;
226
245
  private _requireEntry;
227
246
  private _requireNoble;
@@ -247,13 +266,16 @@ interface InitTrezorBleSupportOptions extends NobleBleHandlerOptions {
247
266
  interface TrezorBleSupportHandle {
248
267
  handler: NobleBleHandler;
249
268
  dispose(): Promise<void>;
269
+ disposeForAppQuit(releaseNoble?: (instance: {
270
+ stop?(): void;
271
+ }) => void): Promise<void>;
250
272
  }
251
273
  /**
252
274
  * Wire a `NobleBleHandler` to Electron's IPC so the renderer can drive BLE
253
275
  * via `window.desktopApi.trezorBle`. Call once from the main process after
254
276
  * `BrowserWindow` is ready.
255
277
  *
256
- * Returns a disposer the caller should invoke on app quit.
278
+ * Call dispose() when retiring a renderer and disposeForAppQuit() before Node teardown.
257
279
  */
258
280
  declare function initTrezorBleSupport(webContents: WebContentsLike, options?: InitTrezorBleSupportOptions): TrezorBleSupportHandle;
259
281
 
package/dist/main.d.ts CHANGED
@@ -14,6 +14,7 @@ interface NobleLike {
14
14
  removeListener(event: string, handler: (...args: any[]) => void): NobleLike;
15
15
  startScanningAsync(serviceUuids: string[], allowDuplicates: boolean): Promise<void>;
16
16
  stopScanningAsync(): Promise<void>;
17
+ stop?(): void;
17
18
  /**
18
19
  * Connect by id/address with NO scan. Both native backends support this and
19
20
  * emit a `discover` for the peripheral as a side effect: Windows synthesizes
@@ -22,6 +23,7 @@ interface NobleLike {
22
23
  * (lib/mac/src/ble_manager.mm). Optional so a stub noble can omit it.
23
24
  */
24
25
  connectAsync?(idOrAddress: string): Promise<NoblePeripheralLike | undefined>;
26
+ cancelConnect?(idOrAddress: string): void;
25
27
  reset?(): Promise<void>;
26
28
  }
27
29
  interface NoblePeripheralLike {
@@ -44,6 +46,7 @@ interface NoblePeripheralLike {
44
46
  rssi: number;
45
47
  state: string;
46
48
  connectAsync(): Promise<void>;
49
+ cancelConnect?(): void;
47
50
  disconnectAsync(): Promise<void>;
48
51
  discoverSomeServicesAndCharacteristicsAsync(serviceUuids: string[], characteristicUuids: string[]): Promise<{
49
52
  characteristics: NobleCharacteristicLike[];
@@ -89,13 +92,20 @@ declare class NobleBleHandler {
89
92
  private readonly _discovered;
90
93
  private readonly _lastSeen;
91
94
  private readonly _connected;
92
- private _stateChangeHandler?;
93
95
  private _discoverHandler?;
94
96
  private _scanning;
95
97
  private _idleStopTimer?;
96
98
  private _onNotification?;
97
99
  private _onDeviceDisconnected?;
98
100
  private _initialized;
101
+ private _disposed;
102
+ private _disposePromise?;
103
+ private _releasePromise?;
104
+ private _initPromise?;
105
+ private readonly _nobleInstances;
106
+ private readonly _pendingCancellations;
107
+ private readonly _connectAttempts;
108
+ private _nativeReleased;
99
109
  private _lastNobleRecoverAt?;
100
110
  /** The connect currently in flight, so cancelPairing can abandon it. */
101
111
  private _activeConnect?;
@@ -220,8 +230,17 @@ declare class NobleBleHandler {
220
230
  subscribe(id: string): Promise<void>;
221
231
  unsubscribe(id: string): Promise<void>;
222
232
  write(id: string, hexData: string): Promise<void>;
223
- /** Tear down all active connections — called on app quit. */
233
+ /** Retire a renderer's handler without stopping a process-wide native manager. */
224
234
  dispose(): Promise<void>;
235
+ /**
236
+ * Terminal native release, including instances replaced by adapter recovery.
237
+ * A host sharing Noble must defer stop() until all transports have disposed,
238
+ * and deduplicate instances passed to releaseNoble across those transports.
239
+ */
240
+ disposeForAppQuit(releaseNoble?: (instance: {
241
+ stop?(): void;
242
+ }) => void): Promise<void>;
243
+ private _assertActive;
225
244
  private _cleanupDevice;
226
245
  private _requireEntry;
227
246
  private _requireNoble;
@@ -247,13 +266,16 @@ interface InitTrezorBleSupportOptions extends NobleBleHandlerOptions {
247
266
  interface TrezorBleSupportHandle {
248
267
  handler: NobleBleHandler;
249
268
  dispose(): Promise<void>;
269
+ disposeForAppQuit(releaseNoble?: (instance: {
270
+ stop?(): void;
271
+ }) => void): Promise<void>;
250
272
  }
251
273
  /**
252
274
  * Wire a `NobleBleHandler` to Electron's IPC so the renderer can drive BLE
253
275
  * via `window.desktopApi.trezorBle`. Call once from the main process after
254
276
  * `BrowserWindow` is ready.
255
277
  *
256
- * Returns a disposer the caller should invoke on app quit.
278
+ * Call dispose() when retiring a renderer and disposeForAppQuit() before Node teardown.
257
279
  */
258
280
  declare function initTrezorBleSupport(webContents: WebContentsLike, options?: InitTrezorBleSupportOptions): TrezorBleSupportHandle;
259
281
 
package/dist/main.js CHANGED
@@ -97,6 +97,11 @@ var NobleBleHandler = class {
97
97
  this._connected = /* @__PURE__ */ new Map();
98
98
  this._scanning = false;
99
99
  this._initialized = false;
100
+ this._disposed = false;
101
+ this._nobleInstances = /* @__PURE__ */ new Set();
102
+ this._pendingCancellations = /* @__PURE__ */ new Set();
103
+ this._connectAttempts = /* @__PURE__ */ new Set();
104
+ this._nativeReleased = false;
100
105
  this._factory = options.nobleFactory ?? DEFAULT_NOBLE_FACTORY;
101
106
  this._uuids = options.uuids ?? import_hwk_trezor_adapter.TREZOR_BLE_UUIDS;
102
107
  this._chunkSize = options.chunkSize ?? import_hwk_trezor_adapter.TREZOR_BLE_PACKET_SIZE;
@@ -110,20 +115,34 @@ var NobleBleHandler = class {
110
115
  this._onDeviceDisconnected = handler;
111
116
  }
112
117
  async init() {
118
+ this._assertActive();
113
119
  if (this._initialized) return;
114
- this._noble = this._factory();
115
- this._discoverHandler = (peripheral) => {
116
- this._discovered.set(peripheral.id, peripheral);
117
- this._lastSeen.set(peripheral.id, Date.now());
118
- };
119
- this._noble.on("discover", this._discoverHandler);
120
- await this._waitForPoweredOn(TREZOR_BLE_POWER_ON_TIMEOUT_MS);
121
- this._initialized = true;
120
+ if (this._initPromise) return this._initPromise;
121
+ this._initPromise = (async () => {
122
+ this._noble ?? (this._noble = this._factory());
123
+ this._nobleInstances.add(this._noble);
124
+ this._discoverHandler ?? (this._discoverHandler = (peripheral) => {
125
+ this._discovered.set(peripheral.id, peripheral);
126
+ this._lastSeen.set(peripheral.id, Date.now());
127
+ });
128
+ this._noble.removeListener("discover", this._discoverHandler);
129
+ this._noble.on("discover", this._discoverHandler);
130
+ await this._waitForPoweredOn(TREZOR_BLE_POWER_ON_TIMEOUT_MS);
131
+ this._assertActive();
132
+ this._initialized = true;
133
+ })();
134
+ try {
135
+ await this._initPromise;
136
+ } finally {
137
+ this._initPromise = void 0;
138
+ }
122
139
  }
123
140
  async checkAvailability() {
124
- if (!this._initialized) {
141
+ this._assertActive();
142
+ if (!this._noble) {
125
143
  try {
126
144
  this._noble = this._factory();
145
+ this._nobleInstances.add(this._noble);
127
146
  } catch {
128
147
  return { available: false, state: "unsupported", initialized: false };
129
148
  }
@@ -164,6 +183,7 @@ var NobleBleHandler = class {
164
183
  await this._recoverNobleIfStuck(String(error));
165
184
  }
166
185
  }
186
+ this._assertActive();
167
187
  this._armIdleStop();
168
188
  const devices = this._snapshot();
169
189
  if (devices.length === 0) {
@@ -217,6 +237,7 @@ var NobleBleHandler = class {
217
237
  * app restart that is otherwise the only cure.
218
238
  */
219
239
  async _recoverNobleIfStuck(reason) {
240
+ if (this._disposed) return;
220
241
  const state = this._noble?.state;
221
242
  if (state === "poweredOn" || !this._initialized) return;
222
243
  if (this._connected.size > 0) {
@@ -240,6 +261,7 @@ var NobleBleHandler = class {
240
261
  }
241
262
  const fresh = this._createFreshNoble();
242
263
  this._noble = fresh;
264
+ this._nobleInstances.add(fresh);
243
265
  if (this._discoverHandler) {
244
266
  fresh.on("discover", this._discoverHandler);
245
267
  }
@@ -253,6 +275,7 @@ var NobleBleHandler = class {
253
275
  }
254
276
  }
255
277
  _armIdleStop() {
278
+ if (this._disposed) return;
256
279
  this._clearIdleStop();
257
280
  this._idleStopTimer = setTimeout(() => {
258
281
  void this._stopContinuousScan();
@@ -345,6 +368,7 @@ var NobleBleHandler = class {
345
368
  const finish = (p) => {
346
369
  if (done) return;
347
370
  done = true;
371
+ this._pendingCancellations.delete(cancel);
348
372
  clearTimeout(timer);
349
373
  noble.removeListener("discover", onDiscover);
350
374
  void noble.stopScanningAsync().catch(() => void 0);
@@ -355,8 +379,10 @@ var NobleBleHandler = class {
355
379
  if (peripheral.id === id) finish(peripheral);
356
380
  };
357
381
  const timer = setTimeout(() => finish(this._discovered.get(id)), timeoutMs);
382
+ const cancel = () => finish();
383
+ this._pendingCancellations.add(cancel);
358
384
  noble.on("discover", onDiscover);
359
- void noble.startScanningAsync([], false);
385
+ void noble.startScanningAsync([], false).catch(() => finish());
360
386
  });
361
387
  }
362
388
  /**
@@ -416,10 +442,18 @@ var NobleBleHandler = class {
416
442
  // waits for a CoreBluetooth disconnect event that never comes); bound it so a
417
443
  // cleanup disconnect can't hang the connect flow.
418
444
  async _safeDisconnect(peripheral) {
419
- await Promise.race([
420
- peripheral.disconnectAsync().catch(() => void 0),
421
- delay(BLE_DISCONNECT_TIMEOUT_MS)
422
- ]);
445
+ if (this._nativeReleased) return;
446
+ let timeout;
447
+ try {
448
+ await Promise.race([
449
+ peripheral.disconnectAsync().catch(() => void 0),
450
+ new Promise((resolve) => {
451
+ timeout = setTimeout(resolve, BLE_DISCONNECT_TIMEOUT_MS);
452
+ })
453
+ ]);
454
+ } finally {
455
+ clearTimeout(timeout);
456
+ }
423
457
  }
424
458
  // noble has no connect timeout, so a stale bond hangs anywhere — connectAsync
425
459
  // OR the post-connect (encrypted) service discovery. One overall timeout
@@ -427,6 +461,7 @@ var NobleBleHandler = class {
427
461
  // out` reject (device unreachable) vs a connectAsync `connection failed`
428
462
  // reject (link refused / stale bond) — mapped to different error codes there.
429
463
  async connect(id) {
464
+ this._assertActive();
430
465
  const claim = { abandoned: false };
431
466
  let abandon;
432
467
  const abandoned = new Promise((_, reject) => {
@@ -439,23 +474,37 @@ var NobleBleHandler = class {
439
474
  () => abandon(new Error(`connect timed out after ${this._connectTimeoutMs}ms`)),
440
475
  this._connectTimeoutMs
441
476
  );
442
- const attempt = { id, abandon };
477
+ const attempt = {
478
+ id,
479
+ abandon,
480
+ cancelNative: () => claim.cancelNative?.(),
481
+ settled: Promise.resolve(void 0)
482
+ };
443
483
  this._activeConnect = attempt;
444
- try {
445
- return await Promise.race([this._connectInner(id, claim), abandoned]);
446
- } catch (error) {
447
- const peripheral = this._discovered.get(id);
448
- if (peripheral) await this._safeDisconnect(peripheral);
449
- throw error;
450
- } finally {
451
- if (timer) clearTimeout(timer);
452
- if (this._activeConnect === attempt) this._activeConnect = void 0;
453
- }
484
+ this._connectAttempts.add(attempt);
485
+ const nativeOperation = this._connectInner(id, claim);
486
+ const caller = (async () => {
487
+ try {
488
+ return await Promise.race([nativeOperation, abandoned]);
489
+ } catch (error) {
490
+ const peripheral = this._discovered.get(id);
491
+ if (peripheral) await this._safeDisconnect(peripheral);
492
+ throw error;
493
+ } finally {
494
+ clearTimeout(timer);
495
+ if (this._activeConnect === attempt) this._activeConnect = void 0;
496
+ }
497
+ })();
498
+ attempt.settled = Promise.allSettled([nativeOperation, caller]).finally(() => {
499
+ this._connectAttempts.delete(attempt);
500
+ });
501
+ return caller;
454
502
  }
455
503
  async _connectInner(id, claim) {
456
504
  await this.init();
457
505
  await this._pauseScan();
458
506
  await delay(BLE_CONNECT_SETTLE_MS);
507
+ this._assertActive();
459
508
  let route = "cache";
460
509
  let peripheral = this._discovered.get(id);
461
510
  if (!peripheral) {
@@ -466,6 +515,8 @@ var NobleBleHandler = class {
466
515
  route = "direct";
467
516
  }
468
517
  if (!peripheral) {
518
+ const native2 = this._requireNoble();
519
+ claim.cancelNative = () => native2.cancelConnect?.(id);
469
520
  peripheral = await this._directConnect(id);
470
521
  }
471
522
  if (!peripheral) {
@@ -478,7 +529,7 @@ var NobleBleHandler = class {
478
529
  throw new Error(`Trezor BLE device not found: ${id}`);
479
530
  }
480
531
  const abortIfAbandoned = async (stage) => {
481
- if (!claim.abandoned) return;
532
+ if (!claim.abandoned && !this._disposed) return;
482
533
  if (peripheral && peripheral.state === "connected" && !this._connected.has(id)) {
483
534
  await this._safeDisconnect(peripheral);
484
535
  }
@@ -487,6 +538,15 @@ var NobleBleHandler = class {
487
538
  };
488
539
  await abortIfAbandoned("resolve");
489
540
  const wasConnected = peripheral.state === "connected";
541
+ const connectingPeripheral = peripheral;
542
+ const native = this._requireNoble();
543
+ claim.cancelNative = () => {
544
+ if (connectingPeripheral.state === "connecting" && connectingPeripheral.cancelConnect) {
545
+ connectingPeripheral.cancelConnect();
546
+ } else {
547
+ native.cancelConnect?.(id);
548
+ }
549
+ };
490
550
  this._log("warn", "connect.route", {
491
551
  id,
492
552
  route,
@@ -572,6 +632,7 @@ var NobleBleHandler = class {
572
632
  if (!entry.writeChar) throw new Error(`Trezor BLE write char missing for ${id}`);
573
633
  const buffer = Buffer.from(hexData, "hex");
574
634
  for (let offset = 0; offset < buffer.length; offset += this._chunkSize) {
635
+ this._assertActive();
575
636
  const slice = buffer.subarray(offset, offset + this._chunkSize);
576
637
  const chunk = Buffer.alloc(this._chunkSize);
577
638
  slice.copy(chunk);
@@ -581,22 +642,98 @@ var NobleBleHandler = class {
581
642
  }
582
643
  }
583
644
  }
584
- /** Tear down all active connections — called on app quit. */
585
- async dispose() {
645
+ /** Retire a renderer's handler without stopping a process-wide native manager. */
646
+ dispose() {
647
+ if (this._disposePromise) return this._disposePromise;
648
+ this._disposed = true;
586
649
  this._clearIdleStop();
587
650
  this._scanning = false;
588
- for (const id of Array.from(this._connected.keys())) {
589
- await this.disconnect(id);
651
+ this._onNotification = void 0;
652
+ this._onDeviceDisconnected = void 0;
653
+ const connections = Array.from(this._connectAttempts);
654
+ for (const attempt of connections) {
655
+ attempt.abandon(new Error("Trezor BLE is shutting down"));
656
+ try {
657
+ attempt.cancelNative();
658
+ } catch (error) {
659
+ this._log("warn", "dispose.cancelConnect.error", { error: String(error) });
660
+ }
590
661
  }
662
+ for (const cancel of this._pendingCancellations) cancel();
591
663
  if (this._noble && this._discoverHandler) {
592
664
  this._noble.removeListener("discover", this._discoverHandler);
593
665
  }
594
- if (this._noble && this._stateChangeHandler) {
595
- this._noble.removeListener("stateChange", this._stateChangeHandler);
666
+ const entries = Array.from(this._connected.entries());
667
+ for (const [id, entry] of entries) {
668
+ if (entry.disconnectHandler) {
669
+ entry.peripheral.removeListener("disconnect", entry.disconnectHandler);
670
+ }
671
+ this._cleanupDevice(id, false);
596
672
  }
597
- this._discovered.clear();
598
- this._lastSeen.clear();
599
- this._initialized = false;
673
+ let timeout;
674
+ this._disposePromise = (async () => {
675
+ try {
676
+ await Promise.race([
677
+ Promise.allSettled([
678
+ ...connections.map((attempt) => attempt.settled),
679
+ ...Array.from(this._nobleInstances, async (instance) => instance.stopScanningAsync()),
680
+ ...entries.map(async ([, entry]) => {
681
+ let unsubscribeTimeout;
682
+ try {
683
+ await Promise.race([
684
+ entry.notifyChar?.unsubscribeAsync().catch(() => void 0),
685
+ new Promise((resolve) => {
686
+ unsubscribeTimeout = setTimeout(resolve, 250);
687
+ })
688
+ ]);
689
+ } finally {
690
+ clearTimeout(unsubscribeTimeout);
691
+ await this._safeDisconnect(entry.peripheral);
692
+ }
693
+ })
694
+ ]),
695
+ new Promise((resolve) => {
696
+ timeout = setTimeout(() => {
697
+ this._log("warn", "dispose.timeout");
698
+ resolve();
699
+ }, 3500);
700
+ })
701
+ ]);
702
+ } finally {
703
+ clearTimeout(timeout);
704
+ this._discovered.clear();
705
+ this._lastSeen.clear();
706
+ this._initialized = false;
707
+ }
708
+ })();
709
+ return this._disposePromise;
710
+ }
711
+ /**
712
+ * Terminal native release, including instances replaced by adapter recovery.
713
+ * A host sharing Noble must defer stop() until all transports have disposed,
714
+ * and deduplicate instances passed to releaseNoble across those transports.
715
+ */
716
+ disposeForAppQuit(releaseNoble = (instance) => instance.stop?.()) {
717
+ if (!this._releasePromise) {
718
+ this._releasePromise = this.dispose().finally(() => {
719
+ this._nativeReleased = true;
720
+ let releaseError;
721
+ for (const instance of this._nobleInstances) {
722
+ try {
723
+ releaseNoble(instance);
724
+ } catch (error) {
725
+ releaseError = error instanceof Error ? error : new Error(String(error));
726
+ }
727
+ }
728
+ this._nobleInstances.clear();
729
+ if (releaseError) throw releaseError;
730
+ this._log("info", "dispose.native.done");
731
+ });
732
+ }
733
+ return this._releasePromise;
734
+ }
735
+ _assertActive() {
736
+ if (this._disposed) throw new Error("Trezor BLE is shutting down");
600
737
  }
601
738
  _cleanupDevice(id, unexpected) {
602
739
  const entry = this._connected.get(id);
@@ -611,11 +748,13 @@ var NobleBleHandler = class {
611
748
  }
612
749
  }
613
750
  _requireEntry(id) {
751
+ this._assertActive();
614
752
  const entry = this._connected.get(id);
615
753
  if (!entry) throw new Error(`Trezor BLE device is not connected: ${id}`);
616
754
  return entry;
617
755
  }
618
756
  _requireNoble() {
757
+ this._assertActive();
619
758
  if (!this._noble) throw new Error("Trezor BLE: noble was not initialized");
620
759
  return this._noble;
621
760
  }
@@ -623,8 +762,17 @@ var NobleBleHandler = class {
623
762
  const noble = this._requireNoble();
624
763
  if (noble.state === "poweredOn") return;
625
764
  await new Promise((resolve, reject) => {
765
+ const cleanup = () => {
766
+ clearTimeout(timer);
767
+ noble.removeListener("stateChange", handler);
768
+ this._pendingCancellations.delete(cancel);
769
+ };
770
+ const cancel = () => {
771
+ cleanup();
772
+ reject(new Error("Trezor BLE is shutting down"));
773
+ };
626
774
  const timer = setTimeout(() => {
627
- if (this._stateChangeHandler) noble.removeListener("stateChange", this._stateChangeHandler);
775
+ cleanup();
628
776
  reject(
629
777
  new Error(
630
778
  `Trezor BLE: noble did not reach poweredOn within ${timeoutMs}ms (last state: ${noble.state})`
@@ -633,16 +781,14 @@ var NobleBleHandler = class {
633
781
  }, timeoutMs);
634
782
  const handler = (state) => {
635
783
  if (state === "poweredOn") {
636
- clearTimeout(timer);
784
+ cleanup();
637
785
  resolve();
638
786
  } else if (state === "unsupported" || state === "unauthorized") {
639
- clearTimeout(timer);
640
- if (this._stateChangeHandler)
641
- noble.removeListener("stateChange", this._stateChangeHandler);
787
+ cleanup();
642
788
  reject(new Error(`Trezor BLE: noble state ${state}`));
643
789
  }
644
790
  };
645
- this._stateChangeHandler = handler;
791
+ this._pendingCancellations.add(cancel);
646
792
  noble.on("stateChange", handler);
647
793
  });
648
794
  }
@@ -661,6 +807,7 @@ var DEFAULT_IPC_MAIN = () => {
661
807
  function initTrezorBleSupport(webContents, options = {}) {
662
808
  const ipcMain = options.ipcMain ?? DEFAULT_IPC_MAIN();
663
809
  const handler = new NobleBleHandler(options);
810
+ let disposed = false;
664
811
  handler.setNotificationListener((id, hexData) => {
665
812
  webContents.send(TREZOR_BLE_CHANNELS.notification, id, hexData);
666
813
  });
@@ -668,7 +815,10 @@ function initTrezorBleSupport(webContents, options = {}) {
668
815
  webContents.send(TREZOR_BLE_CHANNELS.disconnected, id);
669
816
  });
670
817
  const handle = (channel, fn) => {
671
- ipcMain.handle(channel, async (_event, ...args) => fn(...args));
818
+ ipcMain.handle(channel, async (_event, ...args) => {
819
+ if (disposed) throw new Error("Trezor BLE is shutting down");
820
+ return fn(...args);
821
+ });
672
822
  };
673
823
  handle(
674
824
  TREZOR_BLE_CHANNELS.scan,
@@ -684,13 +834,22 @@ function initTrezorBleSupport(webContents, options = {}) {
684
834
  handle(TREZOR_BLE_CHANNELS.getDevice, (id) => handler.getDevice(id));
685
835
  handle(TREZOR_BLE_CHANNELS.readRssi, (id) => handler.readRssi(id));
686
836
  handle(TREZOR_BLE_CHANNELS.cancelPairing, () => handler.cancelPairing());
837
+ const removeHandlers = () => {
838
+ if (disposed) return;
839
+ disposed = true;
840
+ for (const channel of Object.values(TREZOR_BLE_CHANNELS)) {
841
+ ipcMain.removeHandler(channel);
842
+ }
843
+ };
687
844
  return {
688
845
  handler,
689
- dispose: async () => {
690
- for (const channel of Object.values(TREZOR_BLE_CHANNELS)) {
691
- ipcMain.removeHandler(channel);
692
- }
693
- await handler.dispose();
846
+ dispose: () => {
847
+ removeHandlers();
848
+ return handler.dispose();
849
+ },
850
+ disposeForAppQuit: (releaseNoble) => {
851
+ removeHandlers();
852
+ return handler.disposeForAppQuit(releaseNoble);
694
853
  }
695
854
  };
696
855
  }