@onekeyfe/hd-transport-electron 1.2.8-alpha.131 → 1.2.8-alpha.133
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/{index-f41d5fc1.js → index-5b7f7420.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/{noble-ble-handler-180255d0.js → noble-ble-handler-a420607d.js} +20 -3
- package/dist/noble-ble-handler.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/noble-ble-handler.test.ts +308 -0
- package/src/noble-ble-handler.ts +31 -13
|
@@ -55,13 +55,13 @@ function invokeNobleBleIpc(request) {
|
|
|
55
55
|
|
|
56
56
|
function initNobleBleSupport(webContents) {
|
|
57
57
|
return __awaiter(this, void 0, void 0, function* () {
|
|
58
|
-
const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-
|
|
58
|
+
const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-a420607d.js'); });
|
|
59
59
|
setupNobleBleHandlers(webContents);
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
62
|
function disposeNobleBleSupport(releaseNoble) {
|
|
63
63
|
return __awaiter(this, void 0, void 0, function* () {
|
|
64
|
-
const handler = yield Promise.resolve().then(function () { return require('./noble-ble-handler-
|
|
64
|
+
const handler = yield Promise.resolve().then(function () { return require('./noble-ble-handler-a420607d.js'); });
|
|
65
65
|
yield handler.disposeNobleBleSupport(releaseNoble);
|
|
66
66
|
});
|
|
67
67
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var index = require('./index-
|
|
3
|
+
var index = require('./index-5b7f7420.js');
|
|
4
4
|
var hdShared = require('@onekeyfe/hd-shared');
|
|
5
5
|
var pRetry = require('p-retry');
|
|
6
6
|
|
|
@@ -1224,6 +1224,8 @@ function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
|
|
|
1224
1224
|
const DIRECT_CONNECT_TIMEOUT_MS = 2000;
|
|
1225
1225
|
const DIRECT_CONNECT_COOLDOWN_MS = 15000;
|
|
1226
1226
|
const directConnectCooldownUntil = new Map();
|
|
1227
|
+
const pendingDirectNativeConnections = new Set();
|
|
1228
|
+
const deviceConnectionPromises = new Map();
|
|
1227
1229
|
function tryDirectConnectById(deviceId) {
|
|
1228
1230
|
var _a;
|
|
1229
1231
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
@@ -1231,6 +1233,8 @@ function tryDirectConnectById(deviceId) {
|
|
|
1231
1233
|
const nobleInstance = noble;
|
|
1232
1234
|
if (!(nobleInstance === null || nobleInstance === void 0 ? void 0 : nobleInstance.connectAsync))
|
|
1233
1235
|
return undefined;
|
|
1236
|
+
if (pendingDirectNativeConnections.has(deviceId))
|
|
1237
|
+
return undefined;
|
|
1234
1238
|
const cooldownUntil = (_a = directConnectCooldownUntil.get(deviceId)) !== null && _a !== void 0 ? _a : 0;
|
|
1235
1239
|
if (Date.now() < cooldownUntil)
|
|
1236
1240
|
return undefined;
|
|
@@ -1239,7 +1243,9 @@ function tryDirectConnectById(deviceId) {
|
|
|
1239
1243
|
let abandoned = false;
|
|
1240
1244
|
try {
|
|
1241
1245
|
const directPromise = nobleInstance.connectAsync(deviceId);
|
|
1242
|
-
|
|
1246
|
+
pendingDirectNativeConnections.add(deviceId);
|
|
1247
|
+
trackNativeConnection(directPromise
|
|
1248
|
+
.then((late) => index.__awaiter(this, void 0, void 0, function* () {
|
|
1243
1249
|
if ((!abandoned && !disposing) || nativeReleased)
|
|
1244
1250
|
return;
|
|
1245
1251
|
const peripheral = late !== null && late !== void 0 ? late : discoveredDevices.get(deviceId);
|
|
@@ -1250,7 +1256,8 @@ function tryDirectConnectById(deviceId) {
|
|
|
1250
1256
|
timeoutBehavior: 'resolve',
|
|
1251
1257
|
});
|
|
1252
1258
|
}
|
|
1253
|
-
}))
|
|
1259
|
+
}))
|
|
1260
|
+
.finally(() => pendingDirectNativeConnections.delete(deviceId)), () => { var _a; return (_a = nobleInstance.cancelConnect) === null || _a === void 0 ? void 0 : _a.call(nobleInstance, deviceId); });
|
|
1254
1261
|
const raced = yield Promise.race([
|
|
1255
1262
|
directPromise,
|
|
1256
1263
|
new Promise(resolve => {
|
|
@@ -1291,6 +1298,16 @@ function tryDirectConnectById(deviceId) {
|
|
|
1291
1298
|
});
|
|
1292
1299
|
}
|
|
1293
1300
|
function connectDevice(deviceId, webContents) {
|
|
1301
|
+
const pending = deviceConnectionPromises.get(deviceId);
|
|
1302
|
+
if (pending)
|
|
1303
|
+
return pending;
|
|
1304
|
+
const connecting = connectDeviceOnce(deviceId, webContents).finally(() => {
|
|
1305
|
+
deviceConnectionPromises.delete(deviceId);
|
|
1306
|
+
});
|
|
1307
|
+
deviceConnectionPromises.set(deviceId, connecting);
|
|
1308
|
+
return connecting;
|
|
1309
|
+
}
|
|
1310
|
+
function connectDeviceOnce(deviceId, webContents) {
|
|
1294
1311
|
var _a, _b, _c;
|
|
1295
1312
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
1296
1313
|
yield initializeNoble();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAA+B,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,KAAK,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAiC1F,KAAK,mBAAmB,GAAG,KAAK,GAAG;IACjC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,mBAAmB,EAAE,aAAa,SAAK,+CAG3F;AAED,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,OAAO,GAAG,wBAAwB,CAgCvF;AA+KD,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE,oBAAoB,UAI7E;
|
|
1
|
+
{"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAA+B,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,KAAK,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAiC1F,KAAK,mBAAmB,GAAG,KAAK,GAAG;IACjC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,mBAAmB,EAAE,aAAa,SAAK,+CAG3F;AAED,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,OAAO,GAAG,wBAAwB,CAgCvF;AA+KD,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE,oBAAoB,UAI7E;AAm2DD,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAkOpE;AAOD,wBAAgB,sBAAsB,CACpC,YAAY,GAAE,CAAC,QAAQ,EAAE;IAAE,IAAI,IAAI,IAAI,CAAA;CAAE,KAAK,IAAkC,GAC/E,OAAO,CAAC,IAAI,CAAC,CAiEf"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport-electron",
|
|
3
|
-
"version": "1.2.8-alpha.
|
|
3
|
+
"version": "1.2.8-alpha.133",
|
|
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.8-alpha.
|
|
29
|
-
"@onekeyfe/hd-shared": "1.2.8-alpha.
|
|
30
|
-
"@onekeyfe/hd-transport": "1.2.8-alpha.
|
|
28
|
+
"@onekeyfe/hd-core": "1.2.8-alpha.133",
|
|
29
|
+
"@onekeyfe/hd-shared": "1.2.8-alpha.133",
|
|
30
|
+
"@onekeyfe/hd-transport": "1.2.8-alpha.133",
|
|
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": "
|
|
39
|
+
"gitHead": "c859b88e03cf7a80e3168ab048fd1af6cb286af6"
|
|
40
40
|
}
|
|
@@ -1015,6 +1015,241 @@ describe('Noble BLE process shutdown', () => {
|
|
|
1015
1015
|
expect(jest.getTimerCount()).toBe(0);
|
|
1016
1016
|
});
|
|
1017
1017
|
|
|
1018
|
+
test('keeps one native direct connection across repeated probe timeouts for the same device', async () => {
|
|
1019
|
+
jest.useFakeTimers({ doNotFake: ['performance', 'setImmediate'] });
|
|
1020
|
+
const { sdk, native, handlers } = await setup();
|
|
1021
|
+
// Use Noble's real exclusive listeners without loading its native bindings.
|
|
1022
|
+
const Noble = jest.requireActual('@stoprocent/noble/lib/noble');
|
|
1023
|
+
const direct = new Noble({ connect: jest.fn(), cancelConnect: jest.fn() });
|
|
1024
|
+
const connectAsync = jest.fn((id: string) => direct.connectAsync(id));
|
|
1025
|
+
const cancelConnect = jest.fn((id: string) => direct.cancelConnect(id));
|
|
1026
|
+
Object.assign(native, { connectAsync, cancelConnect });
|
|
1027
|
+
const deviceId = 'aabbccddeeff';
|
|
1028
|
+
|
|
1029
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1030
|
+
const connecting = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT)?.({}, deviceId);
|
|
1031
|
+
await flushCallbacks();
|
|
1032
|
+
jest.advanceTimersByTime(1500);
|
|
1033
|
+
await flushCallbacks();
|
|
1034
|
+
expect(connectAsync).toHaveBeenCalledTimes(1);
|
|
1035
|
+
jest.advanceTimersByTime(2000);
|
|
1036
|
+
await flushCallbacks();
|
|
1037
|
+
expect(await connecting).toMatchObject({
|
|
1038
|
+
success: false,
|
|
1039
|
+
error: { errorCode: HardwareErrorCode.DeviceNotFound },
|
|
1040
|
+
});
|
|
1041
|
+
jest.advanceTimersByTime(15_000);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
expect(cancelConnect).not.toHaveBeenCalled();
|
|
1045
|
+
const disposing = sdk.disposeNobleBleSupport();
|
|
1046
|
+
await flushCallbacks();
|
|
1047
|
+
expect(native.stop).toHaveBeenCalledTimes(1);
|
|
1048
|
+
await disposing;
|
|
1049
|
+
expect(cancelConnect).toHaveBeenCalledTimes(1);
|
|
1050
|
+
expect(direct.listenerCount(`connect:${deviceId}`)).toBe(0);
|
|
1051
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
test('cleans up native success arriving after the direct probe timeout', async () => {
|
|
1055
|
+
jest.useFakeTimers({ doNotFake: ['performance', 'setImmediate'] });
|
|
1056
|
+
const { sdk, native, handlers } = await setup();
|
|
1057
|
+
const Noble = jest.requireActual('@stoprocent/noble/lib/noble');
|
|
1058
|
+
const disconnect = jest.fn();
|
|
1059
|
+
const direct = new Noble({ connect: jest.fn(), cancelConnect: jest.fn(), disconnect });
|
|
1060
|
+
disconnect.mockImplementation((id: string) => direct._onDisconnect(id));
|
|
1061
|
+
const peripheral = direct._createPeripheral(
|
|
1062
|
+
'aabbccddeeff',
|
|
1063
|
+
'',
|
|
1064
|
+
'',
|
|
1065
|
+
true,
|
|
1066
|
+
{ localName: 'OneKey Pro' },
|
|
1067
|
+
0,
|
|
1068
|
+
true
|
|
1069
|
+
);
|
|
1070
|
+
const cancelConnect = jest.fn((id: string) => direct.cancelConnect(id));
|
|
1071
|
+
Object.assign(native, {
|
|
1072
|
+
connectAsync: jest.fn((id: string) => direct.connectAsync(id)),
|
|
1073
|
+
cancelConnect,
|
|
1074
|
+
});
|
|
1075
|
+
const connecting = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT)?.({}, peripheral.id);
|
|
1076
|
+
await flushCallbacks();
|
|
1077
|
+
jest.advanceTimersByTime(1500);
|
|
1078
|
+
await flushCallbacks();
|
|
1079
|
+
jest.advanceTimersByTime(2000);
|
|
1080
|
+
await flushCallbacks();
|
|
1081
|
+
expect(await connecting).toMatchObject({ success: false });
|
|
1082
|
+
|
|
1083
|
+
direct._onConnect(peripheral.id, null);
|
|
1084
|
+
await flushCallbacks();
|
|
1085
|
+
expect(disconnect).toHaveBeenCalledTimes(1);
|
|
1086
|
+
expect(peripheral.state).toBe('disconnected');
|
|
1087
|
+
await sdk.disposeNobleBleSupport();
|
|
1088
|
+
expect(cancelConnect).not.toHaveBeenCalled();
|
|
1089
|
+
expect(native.stop).toHaveBeenCalledTimes(1);
|
|
1090
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
test('retries unavailable native IDs after cooldown without retaining connections', async () => {
|
|
1094
|
+
jest.useFakeTimers({ doNotFake: ['performance', 'setImmediate'] });
|
|
1095
|
+
const { sdk, native, handlers } = await setup();
|
|
1096
|
+
const Noble = jest.requireActual('@stoprocent/noble/lib/noble');
|
|
1097
|
+
const connect = jest.fn(() => {
|
|
1098
|
+
throw new Error('Peripheral could not be retrieved for connection');
|
|
1099
|
+
});
|
|
1100
|
+
const direct = new Noble({ connect, cancelConnect: jest.fn() });
|
|
1101
|
+
const cancelConnect = jest.fn((id: string) => direct.cancelConnect(id));
|
|
1102
|
+
Object.assign(native, {
|
|
1103
|
+
connectAsync: jest.fn((id: string) => direct.connectAsync(id)),
|
|
1104
|
+
cancelConnect,
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1108
|
+
const connecting = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT)?.(
|
|
1109
|
+
{},
|
|
1110
|
+
'aabbccddeeff'
|
|
1111
|
+
);
|
|
1112
|
+
await flushCallbacks();
|
|
1113
|
+
jest.advanceTimersByTime(1500);
|
|
1114
|
+
await flushCallbacks();
|
|
1115
|
+
expect(await connecting).toMatchObject({ success: false });
|
|
1116
|
+
expect(connect).toHaveBeenCalledTimes(attempt + 1);
|
|
1117
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
1118
|
+
jest.advanceTimersByTime(15_000);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const disposing = sdk.disposeNobleBleSupport();
|
|
1122
|
+
await flushCallbacks();
|
|
1123
|
+
expect(native.stop).toHaveBeenCalledTimes(1);
|
|
1124
|
+
await disposing;
|
|
1125
|
+
expect(cancelConnect).not.toHaveBeenCalled();
|
|
1126
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
test.each(['before', 'after'])(
|
|
1130
|
+
'retries a direct connection that fails before discovery %s the probe timeout',
|
|
1131
|
+
async phase => {
|
|
1132
|
+
jest.useFakeTimers({ doNotFake: ['performance', 'setImmediate'] });
|
|
1133
|
+
const { sdk, native, handlers } = await setup();
|
|
1134
|
+
const Noble = jest.requireActual('@stoprocent/noble/lib/noble');
|
|
1135
|
+
const bindings = Object.assign(new EventEmitter(), {
|
|
1136
|
+
connect: jest.fn(),
|
|
1137
|
+
cancelConnect: jest.fn(),
|
|
1138
|
+
});
|
|
1139
|
+
const direct = new Noble(bindings);
|
|
1140
|
+
direct._registerListeners();
|
|
1141
|
+
direct.removeAllListeners('warning');
|
|
1142
|
+
direct.on('warning', jest.fn());
|
|
1143
|
+
const connectAsync = jest.fn((id: string) => direct.connectAsync(id));
|
|
1144
|
+
Object.assign(native, {
|
|
1145
|
+
connectAsync,
|
|
1146
|
+
cancelConnect: jest.fn((id: string) => direct.cancelConnect(id)),
|
|
1147
|
+
});
|
|
1148
|
+
const deviceId = 'aabbccddeeff';
|
|
1149
|
+
let firstFinished = false;
|
|
1150
|
+
const connecting = Promise.resolve(
|
|
1151
|
+
handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT)?.({}, deviceId)
|
|
1152
|
+
).then(result => {
|
|
1153
|
+
firstFinished = true;
|
|
1154
|
+
return result;
|
|
1155
|
+
});
|
|
1156
|
+
await flushCallbacks();
|
|
1157
|
+
jest.advanceTimersByTime(1500);
|
|
1158
|
+
await flushCallbacks();
|
|
1159
|
+
expect(connectAsync).toHaveBeenCalledTimes(1);
|
|
1160
|
+
expect(direct._peripherals.has(deviceId)).toBe(false);
|
|
1161
|
+
if (phase === 'after') {
|
|
1162
|
+
jest.advanceTimersByTime(2000);
|
|
1163
|
+
await flushCallbacks();
|
|
1164
|
+
}
|
|
1165
|
+
bindings.emit('connect', deviceId, new Error('native async connection failed'));
|
|
1166
|
+
await flushCallbacks();
|
|
1167
|
+
expect(firstFinished).toBe(true);
|
|
1168
|
+
expect(await connecting).toMatchObject({ success: false });
|
|
1169
|
+
|
|
1170
|
+
jest.advanceTimersByTime(15_000);
|
|
1171
|
+
const retry = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT)?.({}, deviceId);
|
|
1172
|
+
await flushCallbacks();
|
|
1173
|
+
jest.advanceTimersByTime(1500);
|
|
1174
|
+
await flushCallbacks();
|
|
1175
|
+
expect(connectAsync).toHaveBeenCalledTimes(2);
|
|
1176
|
+
const disposing = sdk.disposeNobleBleSupport();
|
|
1177
|
+
await flushCallbacks();
|
|
1178
|
+
expect(native.stop).toHaveBeenCalledTimes(1);
|
|
1179
|
+
await disposing;
|
|
1180
|
+
expect(await retry).toMatchObject({ success: false });
|
|
1181
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
1182
|
+
}
|
|
1183
|
+
);
|
|
1184
|
+
|
|
1185
|
+
test('shares concurrent connections instead of cancelling a newer direct probe', async () => {
|
|
1186
|
+
jest.useFakeTimers({ doNotFake: ['performance', 'setImmediate'] });
|
|
1187
|
+
const { sdk, native, handlers } = await setup();
|
|
1188
|
+
const Noble = jest.requireActual('@stoprocent/noble/lib/noble');
|
|
1189
|
+
const direct = new Noble({ connect: jest.fn(), cancelConnect: jest.fn() });
|
|
1190
|
+
const connectAsync = jest.fn((id: string) => direct.connectAsync(id));
|
|
1191
|
+
const cancelConnect = jest.fn((id: string) => direct.cancelConnect(id));
|
|
1192
|
+
Object.assign(native, { connectAsync, cancelConnect });
|
|
1193
|
+
const connect = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT);
|
|
1194
|
+
const first = connect?.({}, 'aabbccddeeff');
|
|
1195
|
+
await flushCallbacks();
|
|
1196
|
+
jest.advanceTimersByTime(1500);
|
|
1197
|
+
await flushCallbacks();
|
|
1198
|
+
jest.advanceTimersByTime(500);
|
|
1199
|
+
const second = connect?.({}, 'aabbccddeeff');
|
|
1200
|
+
await flushCallbacks();
|
|
1201
|
+
expect(native.startScanning).toHaveBeenCalledTimes(1);
|
|
1202
|
+
jest.advanceTimersByTime(1500);
|
|
1203
|
+
await flushCallbacks();
|
|
1204
|
+
expect(await first).toMatchObject({ success: false });
|
|
1205
|
+
expect(await second).toMatchObject({ success: false });
|
|
1206
|
+
expect(connectAsync).toHaveBeenCalledTimes(1);
|
|
1207
|
+
expect(cancelConnect).not.toHaveBeenCalled();
|
|
1208
|
+
|
|
1209
|
+
const disposing = sdk.disposeNobleBleSupport();
|
|
1210
|
+
await flushCallbacks();
|
|
1211
|
+
expect(native.stop).toHaveBeenCalledTimes(1);
|
|
1212
|
+
await disposing;
|
|
1213
|
+
expect(cancelConnect).toHaveBeenCalledTimes(1);
|
|
1214
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
test('keeps late direct connect cleanup without native cancellation support', async () => {
|
|
1218
|
+
jest.useFakeTimers({ doNotFake: ['performance', 'setImmediate'] });
|
|
1219
|
+
const { sdk, native, handlers } = await setup();
|
|
1220
|
+
const peripheral = Object.assign(new EventEmitter(), {
|
|
1221
|
+
...createPeripheral('late-device', 'OneKey Pro'),
|
|
1222
|
+
disconnect: jest.fn(callback => callback?.()),
|
|
1223
|
+
});
|
|
1224
|
+
let finishConnect: ((value: typeof peripheral) => void) | undefined;
|
|
1225
|
+
Object.assign(native, {
|
|
1226
|
+
connectAsync: jest.fn(
|
|
1227
|
+
() =>
|
|
1228
|
+
new Promise<typeof peripheral>(resolve => {
|
|
1229
|
+
finishConnect = resolve;
|
|
1230
|
+
})
|
|
1231
|
+
),
|
|
1232
|
+
});
|
|
1233
|
+
const connecting = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT)?.({}, peripheral.id);
|
|
1234
|
+
await flushCallbacks();
|
|
1235
|
+
jest.advanceTimersByTime(1500);
|
|
1236
|
+
await flushCallbacks();
|
|
1237
|
+
jest.advanceTimersByTime(2000);
|
|
1238
|
+
await flushCallbacks();
|
|
1239
|
+
expect(await connecting).toMatchObject({
|
|
1240
|
+
success: false,
|
|
1241
|
+
error: { errorCode: HardwareErrorCode.DeviceNotFound },
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
peripheral.state = 'connected';
|
|
1245
|
+
finishConnect?.(peripheral);
|
|
1246
|
+
await flushCallbacks();
|
|
1247
|
+
expect(peripheral.disconnect).toHaveBeenCalledTimes(1);
|
|
1248
|
+
await sdk.disposeNobleBleSupport();
|
|
1249
|
+
expect(native.stop).toHaveBeenCalledTimes(1);
|
|
1250
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1018
1253
|
test('cancels native connects and waits for disconnect before release', async () => {
|
|
1019
1254
|
const { sdk, native, handlers } = await setup();
|
|
1020
1255
|
await handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK)?.({});
|
|
@@ -1446,3 +1681,76 @@ describe('Electron Noble BLE advertisement color', () => {
|
|
|
1446
1681
|
]);
|
|
1447
1682
|
});
|
|
1448
1683
|
});
|
|
1684
|
+
|
|
1685
|
+
describe('Noble synchronous native connection failures', () => {
|
|
1686
|
+
test.each(['direct', 'peripheral'] as const)(
|
|
1687
|
+
'releases %s async connection listeners and permits a subsequent retry',
|
|
1688
|
+
async entry => {
|
|
1689
|
+
const Noble = jest.requireActual('@stoprocent/noble/lib/noble');
|
|
1690
|
+
const error = new Error('Peripheral could not be retrieved for connection');
|
|
1691
|
+
const bindings = { connect: jest.fn() };
|
|
1692
|
+
const noble = new Noble(bindings);
|
|
1693
|
+
noble.removeAllListeners('warning');
|
|
1694
|
+
noble.on('warning', jest.fn());
|
|
1695
|
+
const deviceId = 'aabbccddeeff';
|
|
1696
|
+
const peripheral =
|
|
1697
|
+
entry === 'peripheral'
|
|
1698
|
+
? noble._createPeripheral(deviceId, '', '', true, {}, 0, true)
|
|
1699
|
+
: undefined;
|
|
1700
|
+
bindings.connect.mockImplementationOnce(() => {
|
|
1701
|
+
throw error;
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
await expect(
|
|
1705
|
+
entry === 'direct' ? noble.connectAsync(deviceId) : peripheral.connectAsync()
|
|
1706
|
+
).rejects.toBe(error);
|
|
1707
|
+
expect(noble.listenerCount(`connect:${deviceId}`)).toBe(0);
|
|
1708
|
+
expect(noble._exclusiveCallbacks?.size ?? 0).toBe(0);
|
|
1709
|
+
if (peripheral) {
|
|
1710
|
+
expect(peripheral.state).toBe('error');
|
|
1711
|
+
expect(peripheral.listenerCount('connect')).toBe(0);
|
|
1712
|
+
expect(peripheral._exclusiveCallbacks.size).toBe(0);
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
const retryPeripheral =
|
|
1716
|
+
peripheral ?? noble._createPeripheral(deviceId, '', '', true, {}, 0, true);
|
|
1717
|
+
const retry =
|
|
1718
|
+
entry === 'direct' ? noble.connectAsync(deviceId) : retryPeripheral.connectAsync();
|
|
1719
|
+
noble._onConnect(deviceId);
|
|
1720
|
+
await retry;
|
|
1721
|
+
expect(retryPeripheral.state).toBe('connected');
|
|
1722
|
+
expect(bindings.connect).toHaveBeenCalledTimes(2);
|
|
1723
|
+
expect(noble.listenerCount(`connect:${deviceId}`)).toBe(0);
|
|
1724
|
+
expect(retryPeripheral.listenerCount('connect')).toBe(0);
|
|
1725
|
+
}
|
|
1726
|
+
);
|
|
1727
|
+
|
|
1728
|
+
test.each(['direct', 'peripheral'] as const)(
|
|
1729
|
+
'settles the %s callback once when the binding throws',
|
|
1730
|
+
entry => {
|
|
1731
|
+
const Noble = jest.requireActual('@stoprocent/noble/lib/noble');
|
|
1732
|
+
const error = new Error('Peripheral could not be retrieved for connection');
|
|
1733
|
+
const noble = new Noble({
|
|
1734
|
+
connect: () => {
|
|
1735
|
+
throw error;
|
|
1736
|
+
},
|
|
1737
|
+
});
|
|
1738
|
+
const deviceId = 'aabbccddeeff';
|
|
1739
|
+
const peripheral = noble._createPeripheral(deviceId, '', '', true, {}, 0, true);
|
|
1740
|
+
const callback = jest.fn();
|
|
1741
|
+
|
|
1742
|
+
expect(() =>
|
|
1743
|
+
entry === 'direct'
|
|
1744
|
+
? noble.connect(deviceId, undefined, callback)
|
|
1745
|
+
: peripheral.connect(undefined, callback)
|
|
1746
|
+
).not.toThrow();
|
|
1747
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
1748
|
+
expect(callback.mock.calls[0][0]).toBe(error);
|
|
1749
|
+
expect(peripheral.state).toBe('error');
|
|
1750
|
+
expect(noble.listenerCount(`connect:${deviceId}`)).toBe(0);
|
|
1751
|
+
expect(peripheral.listenerCount('connect')).toBe(0);
|
|
1752
|
+
noble._onConnect(deviceId, error);
|
|
1753
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
1754
|
+
}
|
|
1755
|
+
);
|
|
1756
|
+
});
|
package/src/noble-ble-handler.ts
CHANGED
|
@@ -1698,11 +1698,13 @@ async function setupConnectionAndDiscoverServices(
|
|
|
1698
1698
|
}
|
|
1699
1699
|
}
|
|
1700
1700
|
|
|
1701
|
-
//
|
|
1701
|
+
// Bound probes while a native backend is still waiting for the device.
|
|
1702
1702
|
const DIRECT_CONNECT_TIMEOUT_MS = 2000;
|
|
1703
1703
|
// Floor after a timeout: do not pay the probe on every attempt.
|
|
1704
1704
|
const DIRECT_CONNECT_COOLDOWN_MS = 15_000;
|
|
1705
1705
|
const directConnectCooldownUntil = new Map<string, number>();
|
|
1706
|
+
const pendingDirectNativeConnections = new Set<string>();
|
|
1707
|
+
const deviceConnectionPromises = new Map<string, Promise<void>>();
|
|
1706
1708
|
|
|
1707
1709
|
/**
|
|
1708
1710
|
* Connect straight by id, skipping the ~650ms targeted scan. Both native
|
|
@@ -1721,6 +1723,8 @@ async function tryDirectConnectById(deviceId: string): Promise<Peripheral | unde
|
|
|
1721
1723
|
| null;
|
|
1722
1724
|
if (!nobleInstance?.connectAsync) return undefined;
|
|
1723
1725
|
|
|
1726
|
+
// Retain the original listener and late cleanup until the native attempt settles.
|
|
1727
|
+
if (pendingDirectNativeConnections.has(deviceId)) return undefined;
|
|
1724
1728
|
const cooldownUntil = directConnectCooldownUntil.get(deviceId) ?? 0;
|
|
1725
1729
|
if (Date.now() < cooldownUntil) return undefined;
|
|
1726
1730
|
|
|
@@ -1730,18 +1734,21 @@ async function tryDirectConnectById(deviceId: string): Promise<Peripheral | unde
|
|
|
1730
1734
|
try {
|
|
1731
1735
|
// The late-orphan guard must attach to THIS pending connect.
|
|
1732
1736
|
const directPromise = nobleInstance.connectAsync(deviceId);
|
|
1737
|
+
pendingDirectNativeConnections.add(deviceId);
|
|
1733
1738
|
trackNativeConnection(
|
|
1734
|
-
directPromise
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
peripheral.
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1739
|
+
directPromise
|
|
1740
|
+
.then(async late => {
|
|
1741
|
+
if ((!abandoned && !disposing) || nativeReleased) return;
|
|
1742
|
+
const peripheral = late ?? discoveredDevices.get(deviceId);
|
|
1743
|
+
if (peripheral?.state === 'connected' && !connectedDevices.has(deviceId)) {
|
|
1744
|
+
peripheral.removeAllListeners('disconnect');
|
|
1745
|
+
await runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1746
|
+
timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
|
|
1747
|
+
timeoutBehavior: 'resolve',
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
})
|
|
1751
|
+
.finally(() => pendingDirectNativeConnections.delete(deviceId)),
|
|
1745
1752
|
() => nobleInstance.cancelConnect?.(deviceId)
|
|
1746
1753
|
);
|
|
1747
1754
|
const raced = await Promise.race([
|
|
@@ -1781,7 +1788,18 @@ async function tryDirectConnectById(deviceId: string): Promise<Peripheral | unde
|
|
|
1781
1788
|
}
|
|
1782
1789
|
|
|
1783
1790
|
// Connect to device - supports both discovered and direct connection modes
|
|
1784
|
-
|
|
1791
|
+
function connectDevice(deviceId: string, webContents: WebContents): Promise<void> {
|
|
1792
|
+
// Share the whole attempt so a scan fallback cannot race another caller's native probe.
|
|
1793
|
+
const pending = deviceConnectionPromises.get(deviceId);
|
|
1794
|
+
if (pending) return pending;
|
|
1795
|
+
const connecting = connectDeviceOnce(deviceId, webContents).finally(() => {
|
|
1796
|
+
deviceConnectionPromises.delete(deviceId);
|
|
1797
|
+
});
|
|
1798
|
+
deviceConnectionPromises.set(deviceId, connecting);
|
|
1799
|
+
return connecting;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
async function connectDeviceOnce(deviceId: string, webContents: WebContents): Promise<void> {
|
|
1785
1803
|
await initializeNoble();
|
|
1786
1804
|
logger?.info('[NobleBLE] Connect device request:', {
|
|
1787
1805
|
deviceId,
|