@onekeyfe/hd-transport-electron 1.2.0-alpha.13 → 1.2.0-alpha.130

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,3 +1,5 @@
1
1
  import type { WebContents } from 'electron';
2
+ import type { NobleBleWriteOptions } from './types/desktop-api';
3
+ export declare function resolveNobleBleWritePacingDelay(options?: NobleBleWriteOptions): number;
2
4
  export declare function setupNobleBleHandlers(webContents: WebContents): void;
3
5
  //# sourceMappingURL=noble-ble-handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAsB,WAAW,EAAE,MAAM,UAAU,CAAC;AAw8ChE,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAiKpE"}
1
+ {"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAA+B,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AA0EhE,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE,oBAAoB,UAI7E;AA8lDD,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAgMpE"}
@@ -0,0 +1,3 @@
1
+ export declare const NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS = 5000;
2
+ export declare const NOBLE_BLE_CONNECTION_TIMEOUT_MS = 10000;
3
+ //# sourceMappingURL=noble-ble-timeouts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"noble-ble-timeouts.d.ts","sourceRoot":"","sources":["../src/noble-ble-timeouts.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kCAAkC,OAAQ,CAAC;AACxD,eAAO,MAAM,+BAA+B,QAAS,CAAC"}
@@ -1,3 +1,6 @@
1
+ export interface NobleBleWriteOptions {
2
+ pacingDelayMs?: number;
3
+ }
1
4
  export interface NobleBleAPI {
2
5
  enumerate: () => Promise<{
3
6
  id: string;
@@ -6,13 +9,19 @@ export interface NobleBleAPI {
6
9
  getDevice: (uuid: string) => Promise<{
7
10
  id: string;
8
11
  name: string;
12
+ mtu?: number;
9
13
  } | null>;
10
14
  connect: (uuid: string) => Promise<void>;
15
+ release?: (uuid: string, keepSession?: boolean) => Promise<void>;
11
16
  disconnect: (uuid: string) => Promise<void>;
12
17
  subscribe: (uuid: string) => Promise<void>;
13
18
  unsubscribe: (uuid: string) => Promise<void>;
14
- write: (uuid: string, data: string) => Promise<void>;
19
+ write: (uuid: string, data: string, options?: NobleBleWriteOptions) => Promise<void>;
15
20
  onNotification: (callback: (deviceId: string, data: string) => void) => () => void;
21
+ onMtuChanged?: (callback: (device: {
22
+ id: string;
23
+ mtu: number;
24
+ }) => void) => () => void;
16
25
  onDeviceDisconnected: (callback: (device: {
17
26
  id: string;
18
27
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"desktop-api.d.ts","sourceRoot":"","sources":["../../src/types/desktop-api.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAAC;IACzD,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IAC1E,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,cAAc,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IACnF,oBAAoB,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IAC/F,iBAAiB,EAAE,MAAM,OAAO,CAAC;QAC/B,SAAS,EAAE,OAAO,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,EAAE,OAAO,CAAC;QACrB,WAAW,EAAE,OAAO,CAAC;KACtB,CAAC,CAAC;CACJ;AAGD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,WAAW,CAAC;CACxB"}
1
+ {"version":3,"file":"desktop-api.d.ts","sourceRoot":"","sources":["../../src/types/desktop-api.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,oBAAoB;IACnC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAAC;IACzD,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACxF,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAGzC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrF,cAAc,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IACnF,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IACvF,oBAAoB,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IAC/F,iBAAiB,EAAE,MAAM,OAAO,CAAC;QAC/B,SAAS,EAAE,OAAO,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,EAAE,OAAO,CAAC;QACrB,WAAW,EAAE,OAAO,CAAC;KACtB,CAAC,CAAC;CACJ;AAGD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,WAAW,CAAC;CACxB"}
@@ -4,6 +4,7 @@ export interface DeviceInfo extends OneKeyDeviceInfoBase {
4
4
  id: string;
5
5
  name: string;
6
6
  state: string;
7
+ mtu?: number;
7
8
  }
8
9
  export interface CharacteristicPair {
9
10
  write: Characteristic;
@@ -1 +1 @@
1
- {"version":3,"file":"noble-extended.d.ts","sourceRoot":"","sources":["../../src/types/noble-extended.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAGnE,MAAM,WAAW,UAAW,SAAQ,oBAAoB;IACtD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAGD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,cAAc,CAAC;CACxB;AAGD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CACX,YAAY,EAAE,MAAM,EAAE,EACtB,eAAe,EAAE,OAAO,EACxB,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,GACjC,IAAI,CAAC;IACR,YAAY,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC1C,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAClE,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;IACxE,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAC9E,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;CACrF;AAGD,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC7C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;CAC9C;AAGD,wBAAgB,OAAO,CACrB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,EACjC,OAAO,EAAE,MAAM,EACf,GAAG,IAAI,EAAE,GAAG,EAAE,GACb,IAAI,CAMN"}
1
+ {"version":3,"file":"noble-extended.d.ts","sourceRoot":"","sources":["../../src/types/noble-extended.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAGnE,MAAM,WAAW,UAAW,SAAQ,oBAAoB;IACtD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAGD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,cAAc,CAAC;CACxB;AAGD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CACX,YAAY,EAAE,MAAM,EAAE,EACtB,eAAe,EAAE,OAAO,EACxB,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,GACjC,IAAI,CAAC;IACR,YAAY,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC1C,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAClE,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;IACxE,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAC9E,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;CACrF;AAGD,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC7C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;CAC9C;AAGD,wBAAgB,OAAO,CACrB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,EACjC,OAAO,EAAE,MAAM,EACf,GAAG,IAAI,EAAE,GAAG,EAAE,GACb,IAAI,CAMN"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-electron",
3
- "version": "1.2.0-alpha.13",
3
+ "version": "1.2.0-alpha.130",
4
4
  "author": "OneKey",
5
5
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
6
6
  "license": "MIT",
@@ -25,9 +25,9 @@
25
25
  "electron-log": ">=4.0.0"
26
26
  },
27
27
  "dependencies": {
28
- "@onekeyfe/hd-core": "1.2.0-alpha.13",
29
- "@onekeyfe/hd-shared": "1.2.0-alpha.13",
30
- "@onekeyfe/hd-transport": "1.2.0-alpha.13",
28
+ "@onekeyfe/hd-core": "1.2.0-alpha.130",
29
+ "@onekeyfe/hd-shared": "1.2.0-alpha.130",
30
+ "@onekeyfe/hd-transport": "1.2.0-alpha.130",
31
31
  "@stoprocent/noble": "2.3.16",
32
32
  "p-retry": "^4.6.2"
33
33
  },
@@ -36,5 +36,5 @@
36
36
  "electron": "^25.0.0",
37
37
  "typescript": "^5.3.3"
38
38
  },
39
- "gitHead": "a907a7ec140123107f6188b37550a7143f1e0d91"
39
+ "gitHead": "287003096bc4df3a5824f2bea77e50f7fdc20565"
40
40
  }
@@ -0,0 +1,21 @@
1
+ import { runBleCallbackOperation } from '../ble-ops';
2
+
3
+ describe('Electron Noble callback operations', () => {
4
+ test('settles when Noble never invokes the callback', async () => {
5
+ const result = await runBleCallbackOperation(() => undefined, {
6
+ timeoutMs: 5,
7
+ timeoutBehavior: 'resolve',
8
+ });
9
+
10
+ expect(result).toBeUndefined();
11
+ });
12
+
13
+ test('rejects callback errors before the timeout', async () => {
14
+ await expect(
15
+ runBleCallbackOperation(callback => callback(new Error('operation failed')), {
16
+ timeoutMs: 50,
17
+ timeoutBehavior: 'reject',
18
+ })
19
+ ).rejects.toThrow('operation failed');
20
+ });
21
+ });
@@ -0,0 +1,13 @@
1
+ import { resolveBlePacketCapacity } from '../ble-packet-capacity';
2
+
3
+ describe('resolveBlePacketCapacity', () => {
4
+ test('uses ATT MTU payload capacity with an upper bound', () => {
5
+ expect(resolveBlePacketCapacity(247, 244, 192)).toBe(244);
6
+ expect(resolveBlePacketCapacity(185, 244, 192)).toBe(182);
7
+ });
8
+
9
+ test('preserves the compatibility fallback when MTU is unavailable', () => {
10
+ expect(resolveBlePacketCapacity(null, 244, 192)).toBe(192);
11
+ expect(resolveBlePacketCapacity(undefined, 244, 192)).toBe(192);
12
+ });
13
+ });
@@ -0,0 +1,383 @@
1
+ import { EventEmitter } from 'events';
2
+ import { EOneKeyBleMessageKeys } from '@onekeyfe/hd-shared';
3
+
4
+ import {
5
+ NOBLE_BLE_CONNECTION_TIMEOUT_MS,
6
+ NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS,
7
+ } from '../noble-ble-timeouts';
8
+
9
+ import type { WebContents } from 'electron';
10
+
11
+ type IpcHandler = (...args: unknown[]) => Promise<unknown> | unknown;
12
+
13
+ const createPeripheral = (id: string, localName?: string, serviceUuids = ['0001', 'fffd']) => ({
14
+ id,
15
+ state: 'disconnected',
16
+ advertisement: {
17
+ localName,
18
+ serviceUuids,
19
+ },
20
+ });
21
+
22
+ describe('Electron Noble BLE device discovery', () => {
23
+ afterEach(() => {
24
+ jest.useRealTimers();
25
+ jest.resetModules();
26
+ jest.clearAllMocks();
27
+ });
28
+
29
+ test('allows enough time for a slow targeted scan and connection', () => {
30
+ expect(NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS).toBe(5_000);
31
+ expect(NOBLE_BLE_CONNECTION_TIMEOUT_MS).toBe(10_000);
32
+ });
33
+
34
+ test('keeps safe pacing by default and allows an explicit high-throughput bypass', async () => {
35
+ const { resolveNobleBleWritePacingDelay } = await import('../noble-ble-handler');
36
+
37
+ expect(resolveNobleBleWritePacingDelay()).toBe(5);
38
+ expect(resolveNobleBleWritePacingDelay({ pacingDelayMs: 0 })).toBe(0);
39
+ expect(resolveNobleBleWritePacingDelay({ pacingDelayMs: 3.8 })).toBe(3);
40
+ });
41
+
42
+ test('waits for Noble to stop scanning before enumeration resolves', async () => {
43
+ jest.useFakeTimers({ doNotFake: ['performance'] });
44
+
45
+ const handlers = new Map<string, IpcHandler>();
46
+ const ipcMain = {
47
+ handle: jest.fn((channel: string, handler: IpcHandler) => {
48
+ handlers.set(channel, handler);
49
+ }),
50
+ removeHandler: jest.fn((channel: string) => {
51
+ handlers.delete(channel);
52
+ }),
53
+ };
54
+ const noble = new EventEmitter() as EventEmitter & {
55
+ state: string;
56
+ startScanning: jest.Mock;
57
+ stopScanning: jest.Mock;
58
+ };
59
+ let resolveScanStarted = () => undefined;
60
+ const scanStarted = new Promise<void>(resolve => {
61
+ resolveScanStarted = resolve;
62
+ });
63
+ let stopScanningCallback: (() => void) | undefined;
64
+ noble.state = 'poweredOn';
65
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
66
+ callback?.();
67
+ noble.emit('discover', createPeripheral('onekey-device', 'Pro2 A1B2'));
68
+ resolveScanStarted();
69
+ });
70
+ noble.stopScanning = jest.fn(callback => {
71
+ stopScanningCallback = callback;
72
+ });
73
+
74
+ jest.doMock('@stoprocent/noble', () => noble);
75
+ jest.doMock('electron', () => ({ ipcMain }));
76
+ jest.doMock('electron-log', () => ({
77
+ info: jest.fn(),
78
+ debug: jest.fn(),
79
+ error: jest.fn(),
80
+ }));
81
+
82
+ const { setupNobleBleHandlers } = await import('../noble-ble-handler');
83
+ setupNobleBleHandlers({
84
+ on: jest.fn(),
85
+ send: jest.fn(),
86
+ } as unknown as WebContents);
87
+
88
+ const enumerate = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE);
89
+ if (!enumerate) {
90
+ throw new Error('Electron Noble BLE enumerate handler was not registered');
91
+ }
92
+
93
+ let enumerationResolved = false;
94
+ const devicesPromise = Promise.resolve(enumerate()).then(devices => {
95
+ enumerationResolved = true;
96
+ return devices;
97
+ });
98
+ await scanStarted;
99
+ jest.advanceTimersByTime(5_000);
100
+ await Promise.resolve();
101
+
102
+ expect(noble.stopScanning).toHaveBeenCalledTimes(1);
103
+ expect(enumerationResolved).toBe(false);
104
+
105
+ stopScanningCallback?.();
106
+ await expect(devicesPromise).resolves.toEqual([
107
+ expect.objectContaining({ id: 'onekey-device' }),
108
+ ]);
109
+ });
110
+
111
+ test('waits for a targeted scan to stop before connecting', async () => {
112
+ jest.useFakeTimers({ doNotFake: ['performance'] });
113
+
114
+ const handlers = new Map<string, IpcHandler>();
115
+ const ipcMain = {
116
+ handle: jest.fn((channel: string, handler: IpcHandler) => {
117
+ handlers.set(channel, handler);
118
+ }),
119
+ removeHandler: jest.fn((channel: string) => {
120
+ handlers.delete(channel);
121
+ }),
122
+ };
123
+ const noble = new EventEmitter() as EventEmitter & {
124
+ state: string;
125
+ startScanning: jest.Mock;
126
+ stopScanning: jest.Mock;
127
+ };
128
+ let resolveScanStarted = () => undefined;
129
+ const scanStarted = new Promise<void>(resolve => {
130
+ resolveScanStarted = resolve;
131
+ });
132
+ let stopScanningCallback: (() => void) | undefined;
133
+ const peripheral = Object.assign(
134
+ new EventEmitter(),
135
+ createPeripheral('target-device', 'Pro2 A1B2'),
136
+ {
137
+ connect: jest.fn((callback: (error?: Error) => void) => {
138
+ callback(new Error('expected test connection failure'));
139
+ }),
140
+ }
141
+ );
142
+ noble.state = 'poweredOn';
143
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
144
+ callback?.();
145
+ noble.emit('discover', peripheral);
146
+ resolveScanStarted();
147
+ });
148
+ noble.stopScanning = jest.fn(callback => {
149
+ stopScanningCallback = callback;
150
+ });
151
+
152
+ jest.doMock('@stoprocent/noble', () => noble);
153
+ jest.doMock('electron', () => ({ ipcMain }));
154
+ jest.doMock('electron-log', () => ({
155
+ info: jest.fn(),
156
+ debug: jest.fn(),
157
+ error: jest.fn(),
158
+ }));
159
+
160
+ const { setupNobleBleHandlers } = await import('../noble-ble-handler');
161
+ setupNobleBleHandlers({
162
+ on: jest.fn(),
163
+ send: jest.fn(),
164
+ } as unknown as WebContents);
165
+
166
+ const connect = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT);
167
+ if (!connect) {
168
+ throw new Error('Electron Noble BLE connect handler was not registered');
169
+ }
170
+
171
+ const connectPromise = Promise.resolve(connect(undefined, 'target-device'));
172
+ await scanStarted;
173
+
174
+ expect(noble.startScanning).toHaveBeenCalledWith([], true, expect.any(Function));
175
+ expect(noble.stopScanning).toHaveBeenCalledTimes(1);
176
+ expect(peripheral.connect).not.toHaveBeenCalled();
177
+
178
+ stopScanningCallback?.();
179
+ await expect(connectPromise).rejects.toThrow('expected test connection failure');
180
+ expect(peripheral.connect).toHaveBeenCalledTimes(1);
181
+ });
182
+
183
+ test('disconnects a connection callback that arrives after timeout', async () => {
184
+ jest.useFakeTimers({ doNotFake: ['performance'] });
185
+
186
+ const handlers = new Map<string, IpcHandler>();
187
+ const ipcMain = {
188
+ handle: jest.fn((channel: string, handler: IpcHandler) => {
189
+ handlers.set(channel, handler);
190
+ }),
191
+ removeHandler: jest.fn((channel: string) => {
192
+ handlers.delete(channel);
193
+ }),
194
+ };
195
+ const noble = new EventEmitter() as EventEmitter & {
196
+ state: string;
197
+ startScanning: jest.Mock;
198
+ stopScanning: jest.Mock;
199
+ };
200
+ let connectCallback: ((error?: Error) => void) | undefined;
201
+ let resolveConnectStarted = () => undefined;
202
+ const connectStarted = new Promise<void>(resolve => {
203
+ resolveConnectStarted = resolve;
204
+ });
205
+ const peripheral = Object.assign(
206
+ new EventEmitter(),
207
+ createPeripheral('slow-device', 'Pro2 A1B2'),
208
+ {
209
+ connect: jest.fn((callback: (error?: Error) => void) => {
210
+ connectCallback = callback;
211
+ resolveConnectStarted();
212
+ }),
213
+ disconnect: jest.fn((callback: () => void) => callback()),
214
+ }
215
+ );
216
+ noble.state = 'poweredOn';
217
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
218
+ callback?.();
219
+ noble.emit('discover', peripheral);
220
+ });
221
+ noble.stopScanning = jest.fn(callback => callback?.());
222
+
223
+ jest.doMock('@stoprocent/noble', () => noble);
224
+ jest.doMock('electron', () => ({ ipcMain }));
225
+ jest.doMock('electron-log', () => ({
226
+ info: jest.fn(),
227
+ debug: jest.fn(),
228
+ error: jest.fn(),
229
+ }));
230
+
231
+ const { setupNobleBleHandlers } = await import('../noble-ble-handler');
232
+ setupNobleBleHandlers({
233
+ on: jest.fn(),
234
+ send: jest.fn(),
235
+ } as unknown as WebContents);
236
+
237
+ const connect = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT);
238
+ if (!connect) {
239
+ throw new Error('Electron Noble BLE connect handler was not registered');
240
+ }
241
+
242
+ const connectPromise = Promise.resolve(connect(undefined, 'slow-device'));
243
+ await connectStarted;
244
+ expect(peripheral.connect).toHaveBeenCalledTimes(1);
245
+
246
+ jest.advanceTimersByTime(NOBLE_BLE_CONNECTION_TIMEOUT_MS);
247
+ await expect(connectPromise).rejects.toThrow('Connection timeout');
248
+
249
+ connectCallback?.();
250
+ await Promise.resolve();
251
+ expect(peripheral.disconnect).toHaveBeenCalledTimes(1);
252
+ });
253
+
254
+ test('enumerates a Pro2 communication advertisement after Find My changes its name', async () => {
255
+ jest.useFakeTimers({ doNotFake: ['performance'] });
256
+
257
+ const handlers = new Map<string, IpcHandler>();
258
+ const ipcMain = {
259
+ handle: jest.fn((channel: string, handler: IpcHandler) => {
260
+ handlers.set(channel, handler);
261
+ }),
262
+ removeHandler: jest.fn((channel: string) => {
263
+ handlers.delete(channel);
264
+ }),
265
+ };
266
+ const noble = new EventEmitter() as EventEmitter & {
267
+ state: string;
268
+ startScanning: jest.Mock;
269
+ stopScanning: jest.Mock;
270
+ };
271
+ let resolveScanStarted = () => undefined;
272
+ const scanStarted = new Promise<void>(resolve => {
273
+ resolveScanStarted = resolve;
274
+ });
275
+ noble.state = 'poweredOn';
276
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
277
+ callback?.();
278
+ noble.emit('discover', createPeripheral('find-my-device', 'Pro2 A1B2 - Find My', ['fffd']));
279
+ noble.emit('discover', createPeripheral('onekey-device', 'Pro2 A1B2 - Find My'));
280
+ resolveScanStarted();
281
+ });
282
+ noble.stopScanning = jest.fn(callback => callback?.());
283
+
284
+ jest.doMock('@stoprocent/noble', () => noble);
285
+ jest.doMock('electron', () => ({ ipcMain }));
286
+ jest.doMock('electron-log', () => ({
287
+ info: jest.fn(),
288
+ debug: jest.fn(),
289
+ error: jest.fn(),
290
+ }));
291
+
292
+ const { setupNobleBleHandlers } = await import('../noble-ble-handler');
293
+ setupNobleBleHandlers({
294
+ on: jest.fn(),
295
+ send: jest.fn(),
296
+ } as unknown as WebContents);
297
+
298
+ const enumerate = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE);
299
+ if (!enumerate) {
300
+ throw new Error('Electron Noble BLE enumerate handler was not registered');
301
+ }
302
+
303
+ const devicesPromise = Promise.resolve(enumerate());
304
+ await scanStarted;
305
+ expect(noble.startScanning).toHaveBeenCalledWith([], true, expect.any(Function));
306
+ jest.advanceTimersByTime(5000);
307
+
308
+ await expect(devicesPromise).resolves.toEqual([
309
+ expect.objectContaining({
310
+ id: 'onekey-device',
311
+ name: 'Pro2 A1B2 - Find My',
312
+ }),
313
+ ]);
314
+ });
315
+
316
+ test('accepts a communication peripheral after its scan response gains a name', async () => {
317
+ jest.useFakeTimers({ doNotFake: ['performance'] });
318
+
319
+ const handlers = new Map<string, IpcHandler>();
320
+ const ipcMain = {
321
+ handle: jest.fn((channel: string, handler: IpcHandler) => {
322
+ handlers.set(channel, handler);
323
+ }),
324
+ removeHandler: jest.fn((channel: string) => {
325
+ handlers.delete(channel);
326
+ }),
327
+ };
328
+ const noble = new EventEmitter() as EventEmitter & {
329
+ state: string;
330
+ startScanning: jest.Mock;
331
+ stopScanning: jest.Mock;
332
+ };
333
+ let resolveScanStarted = () => undefined;
334
+ const scanStarted = new Promise<void>(resolve => {
335
+ resolveScanStarted = resolve;
336
+ });
337
+ const findMyPeripheral = createPeripheral('find-my-device');
338
+ noble.state = 'poweredOn';
339
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
340
+ callback?.();
341
+ noble.emit('discover', findMyPeripheral);
342
+ findMyPeripheral.advertisement.localName = 'Pro2 A1B2 - Find My';
343
+ noble.emit('discover', findMyPeripheral);
344
+ noble.emit('discover', createPeripheral('onekey-device', 'Pro2 A1B2'));
345
+ resolveScanStarted();
346
+ });
347
+ noble.stopScanning = jest.fn(callback => callback?.());
348
+
349
+ jest.doMock('@stoprocent/noble', () => noble);
350
+ jest.doMock('electron', () => ({ ipcMain }));
351
+ jest.doMock('electron-log', () => ({
352
+ info: jest.fn(),
353
+ debug: jest.fn(),
354
+ error: jest.fn(),
355
+ }));
356
+
357
+ const { setupNobleBleHandlers } = await import('../noble-ble-handler');
358
+ setupNobleBleHandlers({
359
+ on: jest.fn(),
360
+ send: jest.fn(),
361
+ } as unknown as WebContents);
362
+
363
+ const enumerate = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE);
364
+ if (!enumerate) {
365
+ throw new Error('Electron Noble BLE enumerate handler was not registered');
366
+ }
367
+
368
+ const devicesPromise = Promise.resolve(enumerate());
369
+ await scanStarted;
370
+ jest.advanceTimersByTime(5000);
371
+
372
+ await expect(devicesPromise).resolves.toEqual([
373
+ expect.objectContaining({
374
+ id: 'find-my-device',
375
+ name: 'Pro2 A1B2 - Find My',
376
+ }),
377
+ expect.objectContaining({
378
+ id: 'onekey-device',
379
+ name: 'Pro2 A1B2',
380
+ }),
381
+ ]);
382
+ });
383
+ });
package/src/ble-ops.ts CHANGED
@@ -1,6 +1,40 @@
1
1
  import type { Characteristic } from '@stoprocent/noble';
2
2
  import type { Logger } from './types/noble-extended';
3
3
 
4
+ type BleCallback = (error?: Error) => void;
5
+
6
+ export function runBleCallbackOperation(
7
+ operation: (callback: BleCallback) => void,
8
+ options: { timeoutMs: number; timeoutBehavior: 'resolve' | 'reject' }
9
+ ): Promise<void> {
10
+ return new Promise<void>((resolve, reject) => {
11
+ let settled = false;
12
+ const settle = (error?: Error) => {
13
+ if (settled) return;
14
+ settled = true;
15
+ clearTimeout(timeout);
16
+ if (error) {
17
+ reject(error);
18
+ } else {
19
+ resolve();
20
+ }
21
+ };
22
+ const timeout = setTimeout(() => {
23
+ if (options.timeoutBehavior === 'reject') {
24
+ settle(new Error(`BLE operation timed out after ${options.timeoutMs}ms`));
25
+ } else {
26
+ settle();
27
+ }
28
+ }, options.timeoutMs);
29
+
30
+ try {
31
+ operation(settle);
32
+ } catch (error) {
33
+ settle(error instanceof Error ? error : new Error(String(error)));
34
+ }
35
+ });
36
+ }
37
+
4
38
  export interface SoftRefreshParams {
5
39
  deviceId: string;
6
40
  notifyCharacteristic: Characteristic | null | undefined;
@@ -30,18 +64,14 @@ export async function softRefreshSubscription(params: SoftRefreshParams): Promis
30
64
 
31
65
  subscriptionOperations.set(deviceId, 'subscribing');
32
66
 
33
- await new Promise<void>(resolve => {
34
- notifyCharacteristic.unsubscribe(() => resolve());
67
+ await runBleCallbackOperation(callback => notifyCharacteristic.unsubscribe(callback), {
68
+ timeoutMs: 250,
69
+ timeoutBehavior: 'resolve',
35
70
  });
36
71
 
37
- await new Promise<void>((resolve, reject) => {
38
- notifyCharacteristic.subscribe((error?: Error) => {
39
- if (error) {
40
- reject(error);
41
- return;
42
- }
43
- resolve();
44
- });
72
+ await runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
73
+ timeoutMs: 8_000,
74
+ timeoutBehavior: 'reject',
45
75
  });
46
76
 
47
77
  notifyCharacteristic.removeAllListeners('data');
@@ -0,0 +1,13 @@
1
+ const BLE_ATT_HEADER_BYTES = 3;
2
+
3
+ export function resolveBlePacketCapacity(
4
+ mtu: number | null | undefined,
5
+ maximumPacketCapacity: number,
6
+ fallbackPacketCapacity: number
7
+ ): number {
8
+ if (typeof mtu !== 'number' || !Number.isFinite(mtu) || mtu <= BLE_ATT_HEADER_BYTES) {
9
+ return fallbackPacketCapacity;
10
+ }
11
+
12
+ return Math.min(maximumPacketCapacity, Math.floor(mtu) - BLE_ATT_HEADER_BYTES);
13
+ }
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@ import type { WebContents } from 'electron';
3
3
  export * from './types';
4
4
 
5
5
  // Export Desktop API types for other packages to reuse
6
- export type { DesktopAPI, NobleBleAPI } from './types/desktop-api';
6
+ export type { DesktopAPI, NobleBleAPI, NobleBleWriteOptions } from './types/desktop-api';
7
7
 
8
8
  export async function initNobleBleSupport(webContents: WebContents) {
9
9
  const { setupNobleBleHandlers } = await import('./noble-ble-handler');