@onekeyfe/hd-transport-react-native 1.2.3 → 1.2.4-alpha.1
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/bleIosStaleBond.d.ts +16 -0
- package/dist/bleIosStaleBond.d.ts.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +88 -1
- package/package.json +5 -5
- package/src/__tests__/iosPeerTermination.test.ts +303 -0
- package/src/bleIosStaleBond.ts +92 -0
- package/src/index.ts +41 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const IOS_PEER_TERMINATION_LINK_WINDOW_MS = 10000;
|
|
2
|
+
export declare const IOS_PEER_TERMINATION_REPEAT_WINDOW_MS = 60000;
|
|
3
|
+
export declare const isIosPeerTerminationError: (error: unknown) => boolean;
|
|
4
|
+
export declare class IosPeerTerminationTracker {
|
|
5
|
+
private linkStartedAt;
|
|
6
|
+
private terminatedAt;
|
|
7
|
+
private evidenceAt;
|
|
8
|
+
attemptStarted(deviceId: string): void;
|
|
9
|
+
linkStarted(deviceId: string): void;
|
|
10
|
+
note(deviceId: string, error: unknown): void;
|
|
11
|
+
linkProven(deviceId: string): void;
|
|
12
|
+
forget(deviceId: string): void;
|
|
13
|
+
attemptFailed(deviceId: string): boolean;
|
|
14
|
+
reset(): void;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=bleIosStaleBond.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bleIosStaleBond.d.ts","sourceRoot":"","sources":["../src/bleIosStaleBond.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,mCAAmC,QAAS,CAAC;AAE1D,eAAO,MAAM,qCAAqC,QAAS,CAAC;AAE5D,eAAO,MAAM,yBAAyB,UAAW,OAAO,KAAG,OAE9B,CAAC;AAE9B,qBAAa,yBAAyB;IAEpC,OAAO,CAAC,aAAa,CAA6B;IAGlD,OAAO,CAAC,YAAY,CAA6B;IAGjD,OAAO,CAAC,UAAU,CAA6B;IAE/C,cAAc,CAAC,QAAQ,EAAE,MAAM;IAK/B,WAAW,CAAC,QAAQ,EAAE,MAAM;IAM5B,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;IAMrC,UAAU,CAAC,QAAQ,EAAE,MAAM;IAI3B,MAAM,CAAC,QAAQ,EAAE,MAAM;IAOvB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAuBxC,KAAK;CAKN"}
|
package/dist/index.d.ts
CHANGED
|
@@ -175,6 +175,8 @@ declare class ReactNativeBleTransport {
|
|
|
175
175
|
private staleBondErrors;
|
|
176
176
|
/** When the current Android link attempt began; bounds which key-missing signals apply. */
|
|
177
177
|
private androidLinkStartedAt;
|
|
178
|
+
/** iOS acquire attempts whose new link the peer ended; two in a row mean a stale bond. */
|
|
179
|
+
private iosPeerTermination;
|
|
178
180
|
/** Strict or previously confirmed V2 target while acquire installs notifications. */
|
|
179
181
|
private acquiringProtocolV2;
|
|
180
182
|
private protocolV2Assemblers;
|
|
@@ -226,6 +228,11 @@ declare class ReactNativeBleTransport {
|
|
|
226
228
|
* connect is re-read as an invalid bond when that signal belongs to the same attempt.
|
|
227
229
|
*/
|
|
228
230
|
private resolveAndroidBondInvalid;
|
|
231
|
+
/**
|
|
232
|
+
* iPhones that do not report a lost bond only show the device ending each new link. One such
|
|
233
|
+
* attempt keeps its error so Core retries; the second in a row is the stale bond.
|
|
234
|
+
*/
|
|
235
|
+
private resolveIosBondInvalid;
|
|
229
236
|
/**
|
|
230
237
|
* Android 16+ encrypts a bonded link on its own right after connecting. Subscribing to
|
|
231
238
|
* notifications before that finishes gets the CCCD write rejected, and the framework's retry
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAGL,UAAU,IAAI,aAAa,EAG5B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,SAAS,EAAE,EAChB,KAAK,oBAAoB,EAIzB,KAAK,YAAY,EAKjB,KAAK,oBAAoB,EAI1B,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -494,6 +494,63 @@ const waitForAndroidLinkEncryption = ({ deviceId, linkStartedAt, resultTimeoutMs
|
|
|
494
494
|
});
|
|
495
495
|
};
|
|
496
496
|
|
|
497
|
+
const IOS_PERIPHERAL_DISCONNECTED$1 = 7;
|
|
498
|
+
const IOS_PEER_TERMINATION_LINK_WINDOW_MS = 10000;
|
|
499
|
+
const IOS_PEER_TERMINATION_REPEAT_WINDOW_MS = 60000;
|
|
500
|
+
const isIosPeerTerminationError = (error) => (error === null || error === void 0 ? void 0 : error.iosErrorCode) ===
|
|
501
|
+
IOS_PERIPHERAL_DISCONNECTED$1;
|
|
502
|
+
class IosPeerTerminationTracker {
|
|
503
|
+
constructor() {
|
|
504
|
+
this.linkStartedAt = new Map();
|
|
505
|
+
this.terminatedAt = new Map();
|
|
506
|
+
this.evidenceAt = new Map();
|
|
507
|
+
}
|
|
508
|
+
attemptStarted(deviceId) {
|
|
509
|
+
this.linkStartedAt.delete(deviceId);
|
|
510
|
+
this.terminatedAt.delete(deviceId);
|
|
511
|
+
}
|
|
512
|
+
linkStarted(deviceId) {
|
|
513
|
+
this.linkStartedAt.set(deviceId, Date.now());
|
|
514
|
+
this.terminatedAt.delete(deviceId);
|
|
515
|
+
}
|
|
516
|
+
note(deviceId, error) {
|
|
517
|
+
if (!isIosPeerTerminationError(error))
|
|
518
|
+
return;
|
|
519
|
+
if (!this.terminatedAt.has(deviceId))
|
|
520
|
+
this.terminatedAt.set(deviceId, Date.now());
|
|
521
|
+
}
|
|
522
|
+
linkProven(deviceId) {
|
|
523
|
+
this.forget(deviceId);
|
|
524
|
+
}
|
|
525
|
+
forget(deviceId) {
|
|
526
|
+
this.linkStartedAt.delete(deviceId);
|
|
527
|
+
this.terminatedAt.delete(deviceId);
|
|
528
|
+
this.evidenceAt.delete(deviceId);
|
|
529
|
+
}
|
|
530
|
+
attemptFailed(deviceId) {
|
|
531
|
+
const startedAt = this.linkStartedAt.get(deviceId);
|
|
532
|
+
const terminatedAt = this.terminatedAt.get(deviceId);
|
|
533
|
+
const previous = this.evidenceAt.get(deviceId);
|
|
534
|
+
this.forget(deviceId);
|
|
535
|
+
if (startedAt === undefined ||
|
|
536
|
+
terminatedAt === undefined ||
|
|
537
|
+
terminatedAt - startedAt > IOS_PEER_TERMINATION_LINK_WINDOW_MS) {
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
if (previous !== undefined &&
|
|
541
|
+
terminatedAt - previous <= IOS_PEER_TERMINATION_REPEAT_WINDOW_MS) {
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
this.evidenceAt.set(deviceId, terminatedAt);
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
reset() {
|
|
548
|
+
this.linkStartedAt.clear();
|
|
549
|
+
this.terminatedAt.clear();
|
|
550
|
+
this.evidenceAt.clear();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
497
554
|
const BLE_PLX_DEVICE_DISCONNECTED = 201;
|
|
498
555
|
const IOS_PERIPHERAL_DISCONNECTED = 7;
|
|
499
556
|
const nativeErrorText$1 = (error) => [error.reason, error.message]
|
|
@@ -859,6 +916,7 @@ class ReactNativeBleTransport {
|
|
|
859
916
|
this.androidGattCacheRefreshes = new Set();
|
|
860
917
|
this.staleBondErrors = new Map();
|
|
861
918
|
this.androidLinkStartedAt = new Map();
|
|
919
|
+
this.iosPeerTermination = new IosPeerTerminationTracker();
|
|
862
920
|
this.acquiringProtocolV2 = new Set();
|
|
863
921
|
this.protocolV2Assemblers = new Map();
|
|
864
922
|
this.protocolV2FrameQueues = new Map();
|
|
@@ -1301,7 +1359,7 @@ class ReactNativeBleTransport {
|
|
|
1301
1359
|
return yield this.acquireUnlocked(input);
|
|
1302
1360
|
}
|
|
1303
1361
|
catch (error) {
|
|
1304
|
-
throw yield this.resolveAndroidBondInvalid(uuid, error);
|
|
1362
|
+
throw this.resolveIosBondInvalid(input, yield this.resolveAndroidBondInvalid(uuid, error));
|
|
1305
1363
|
}
|
|
1306
1364
|
}));
|
|
1307
1365
|
});
|
|
@@ -1312,6 +1370,7 @@ class ReactNativeBleTransport {
|
|
|
1312
1370
|
if (this.stopped)
|
|
1313
1371
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
|
|
1314
1372
|
const { uuid, forceCleanRunPromise, expectedProtocol, skipProtocolProbe } = input;
|
|
1373
|
+
this.iosPeerTermination.attemptStarted(uuid);
|
|
1315
1374
|
const shouldMapProtocolV2StaleBond = expectedProtocol
|
|
1316
1375
|
? expectedProtocol === 'V2'
|
|
1317
1376
|
: this.confirmedProtocolV2.has(uuid);
|
|
@@ -1605,6 +1664,7 @@ class ReactNativeBleTransport {
|
|
|
1605
1664
|
}));
|
|
1606
1665
|
if (this.stopped)
|
|
1607
1666
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
|
|
1667
|
+
this.iosPeerTermination.linkProven(uuid);
|
|
1608
1668
|
const currentTransport = transportCache[uuid];
|
|
1609
1669
|
if (!currentTransport) {
|
|
1610
1670
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotFound);
|
|
@@ -1631,6 +1691,8 @@ class ReactNativeBleTransport {
|
|
|
1631
1691
|
var _a, _b, _c, _d, _e, _f;
|
|
1632
1692
|
const isCurrentMonitor = this.monitorTokens.get(uuid) === monitorToken;
|
|
1633
1693
|
if (error) {
|
|
1694
|
+
if (isCurrentMonitor)
|
|
1695
|
+
this.iosPeerTermination.note(uuid, error);
|
|
1634
1696
|
Log === null || Log === void 0 ? void 0 : Log.debug(`error monitor ${characteristic.uuid}, deviceId: ${characteristic.deviceID}: ${error}`);
|
|
1635
1697
|
if (this.firmwareUploadWriteRecoveryIds.has(uuid)) {
|
|
1636
1698
|
Log === null || Log === void 0 ? void 0 : Log.debug('notify error ignored during FirmwareUpload write recovery: ', uuid);
|
|
@@ -1849,6 +1911,24 @@ class ReactNativeBleTransport {
|
|
|
1849
1911
|
});
|
|
1850
1912
|
});
|
|
1851
1913
|
}
|
|
1914
|
+
resolveIosBondInvalid(input, error) {
|
|
1915
|
+
if (reactNative.Platform.OS !== 'ios')
|
|
1916
|
+
return error;
|
|
1917
|
+
const { uuid, skipProtocolProbe } = input;
|
|
1918
|
+
if (skipProtocolProbe || hdShared.isBleStaleBondHardwareError(error)) {
|
|
1919
|
+
this.iosPeerTermination.forget(uuid);
|
|
1920
|
+
return error;
|
|
1921
|
+
}
|
|
1922
|
+
if (!this.iosPeerTermination.attemptFailed(uuid))
|
|
1923
|
+
return error;
|
|
1924
|
+
Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] iOS peer ended two new links in a row, bond is invalid:', {
|
|
1925
|
+
connectIdSuffix: uuid.slice(-8),
|
|
1926
|
+
});
|
|
1927
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleBondInvalid, undefined, {
|
|
1928
|
+
phase: 'connect',
|
|
1929
|
+
reason: 'peer_disconnected',
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1852
1932
|
waitForAndroidLinkSecurity(uuid) {
|
|
1853
1933
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1854
1934
|
const linkStartedAt = this.androidLinkStartedAt.get(uuid);
|
|
@@ -2088,6 +2168,7 @@ class ReactNativeBleTransport {
|
|
|
2088
2168
|
this.androidPriorityResetTimers.clear();
|
|
2089
2169
|
this.androidHighPriorityDevices.clear();
|
|
2090
2170
|
this.androidLinkStartedAt.clear();
|
|
2171
|
+
this.iosPeerTermination.reset();
|
|
2091
2172
|
stopBleKeyMissingTracking();
|
|
2092
2173
|
stopBleEncryptionTracking();
|
|
2093
2174
|
const error = hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
|
|
@@ -2343,6 +2424,7 @@ class ReactNativeBleTransport {
|
|
|
2343
2424
|
}),
|
|
2344
2425
|
]);
|
|
2345
2426
|
succeeded = true;
|
|
2427
|
+
this.iosPeerTermination.linkStarted(uuid);
|
|
2346
2428
|
return result;
|
|
2347
2429
|
}
|
|
2348
2430
|
catch (error) {
|
|
@@ -2389,6 +2471,10 @@ class ReactNativeBleTransport {
|
|
|
2389
2471
|
return result;
|
|
2390
2472
|
}
|
|
2391
2473
|
catch (error) {
|
|
2474
|
+
this.iosPeerTermination.note(uuid, error);
|
|
2475
|
+
if (reactNative.Platform.OS === 'ios' && isNativeBleStaleBondError(error)) {
|
|
2476
|
+
throw toBleStaleBondHardwareError(error);
|
|
2477
|
+
}
|
|
2392
2478
|
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
2393
2479
|
const resetManager = this.abandonStalledConnection(uuid, timedOut ? 'gatt-backstop' : 'gatt-native');
|
|
2394
2480
|
if (resetManager) {
|
|
@@ -2467,6 +2553,7 @@ class ReactNativeBleTransport {
|
|
|
2467
2553
|
this.writeTimeoutCounts.delete(uuid);
|
|
2468
2554
|
}
|
|
2469
2555
|
catch (error) {
|
|
2556
|
+
this.iosPeerTermination.note(uuid, error);
|
|
2470
2557
|
if (timedOut) {
|
|
2471
2558
|
if (isCurrentOwner && !isCurrentOwner()) {
|
|
2472
2559
|
Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] stale BLE write timed out, link kept:', uuid);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport-react-native",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4-alpha.1",
|
|
4
4
|
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
"lint:fix": "eslint . --fix"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@onekeyfe/hd-core": "1.2.
|
|
24
|
-
"@onekeyfe/hd-shared": "1.2.
|
|
25
|
-
"@onekeyfe/hd-transport": "1.2.
|
|
23
|
+
"@onekeyfe/hd-core": "1.2.4-alpha.1",
|
|
24
|
+
"@onekeyfe/hd-shared": "1.2.4-alpha.1",
|
|
25
|
+
"@onekeyfe/hd-transport": "1.2.4-alpha.1",
|
|
26
26
|
"@onekeyfe/react-native-ble-utils": "0.1.9",
|
|
27
27
|
"react-native-ble-plx": "3.5.1"
|
|
28
28
|
},
|
|
29
|
-
"gitHead": "
|
|
29
|
+
"gitHead": "711c2e0c33c1316257fce02bc01e7c9bbdd8ce48"
|
|
30
30
|
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import EventEmitter from 'events';
|
|
2
|
+
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
3
|
+
|
|
4
|
+
import ReactNativeBleTransport from '../index';
|
|
5
|
+
import {
|
|
6
|
+
IOS_PEER_TERMINATION_LINK_WINDOW_MS,
|
|
7
|
+
IOS_PEER_TERMINATION_REPEAT_WINDOW_MS,
|
|
8
|
+
IosPeerTerminationTracker,
|
|
9
|
+
} from '../bleIosStaleBond';
|
|
10
|
+
import protocolV1Schema from './protocolV1SchemaFixture';
|
|
11
|
+
|
|
12
|
+
jest.mock(
|
|
13
|
+
'react-native',
|
|
14
|
+
() => ({
|
|
15
|
+
Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
|
|
16
|
+
PermissionsAndroid: {
|
|
17
|
+
PERMISSIONS: {},
|
|
18
|
+
RESULTS: {},
|
|
19
|
+
request: jest.fn(),
|
|
20
|
+
requestMultiple: jest.fn(),
|
|
21
|
+
},
|
|
22
|
+
}),
|
|
23
|
+
{ virtual: true }
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
jest.mock('react-native-ble-plx', () => ({
|
|
27
|
+
BleATTErrorCode: { InvalidHandle: 1, UnlikelyError: 14 },
|
|
28
|
+
BleError: Error,
|
|
29
|
+
BleErrorCode: {
|
|
30
|
+
DeviceDisconnected: 201,
|
|
31
|
+
OperationStartFailed: 601,
|
|
32
|
+
DeviceMTUChangeFailed: 401,
|
|
33
|
+
OperationCancelled: 2,
|
|
34
|
+
OperationTimedOut: 3,
|
|
35
|
+
DeviceAlreadyConnected: 203,
|
|
36
|
+
},
|
|
37
|
+
BleManager: jest.fn(),
|
|
38
|
+
ScanMode: { LowLatency: 2 },
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
jest.mock('@onekeyfe/react-native-ble-utils', () => ({
|
|
42
|
+
__esModule: true,
|
|
43
|
+
default: {
|
|
44
|
+
getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
45
|
+
getBondedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
46
|
+
pairDevice: jest.fn(() => Promise.resolve({ bonded: true, bonding: false })),
|
|
47
|
+
onDeviceBondState: jest.fn(),
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
const UUID = '2146E74A-21CC-2EA6-2CE3-B4258520FD0F';
|
|
52
|
+
|
|
53
|
+
// What ble-plx rejects the in-flight operation with when the device ends the link. An iPhone 17
|
|
54
|
+
// reports a wiped device this way instead of CBErrorPeerRemovedPairingInformation.
|
|
55
|
+
const nativeDisconnect = (iosErrorCode: number, reason: string) =>
|
|
56
|
+
Object.assign(new Error(`Device ${UUID} was disconnected`), {
|
|
57
|
+
errorCode: 201,
|
|
58
|
+
iosErrorCode,
|
|
59
|
+
reason,
|
|
60
|
+
});
|
|
61
|
+
const peerEndedLink = () => nativeDisconnect(7, 'The specified device has disconnected from us.');
|
|
62
|
+
const linkTimedOut = () => nativeDisconnect(6, 'The connection has timed out unexpectedly.');
|
|
63
|
+
|
|
64
|
+
function createHarness() {
|
|
65
|
+
const writeCharacteristic = {
|
|
66
|
+
uuid: '00000002-0000-1000-8000-00805f9b34fb',
|
|
67
|
+
isWritableWithResponse: true,
|
|
68
|
+
writeWithResponse: jest.fn(() => Promise.resolve()),
|
|
69
|
+
};
|
|
70
|
+
const notifyCharacteristic = {
|
|
71
|
+
uuid: '00000003-0000-1000-8000-00805f9b34fb',
|
|
72
|
+
isNotifiable: true,
|
|
73
|
+
monitor: jest.fn(() => ({ remove: jest.fn() })),
|
|
74
|
+
};
|
|
75
|
+
const device = {
|
|
76
|
+
id: UUID,
|
|
77
|
+
name: 'Pro 2 48E9',
|
|
78
|
+
localName: 'Pro 2 48E9',
|
|
79
|
+
mtu: 247,
|
|
80
|
+
serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
|
|
81
|
+
isConnected: jest.fn(() => Promise.resolve(false)),
|
|
82
|
+
cancelConnection: jest.fn(() => Promise.resolve()),
|
|
83
|
+
connect: jest.fn(),
|
|
84
|
+
discoverAllServicesAndCharacteristics: jest.fn(() => Promise.resolve()),
|
|
85
|
+
characteristicsForService: jest.fn(() =>
|
|
86
|
+
Promise.resolve([writeCharacteristic, notifyCharacteristic])
|
|
87
|
+
),
|
|
88
|
+
services: jest.fn(() => Promise.resolve([])),
|
|
89
|
+
onDisconnected: jest.fn(() => ({ remove: jest.fn() })),
|
|
90
|
+
};
|
|
91
|
+
device.connect.mockImplementation(() => Promise.resolve(device));
|
|
92
|
+
const transport = new ReactNativeBleTransport({});
|
|
93
|
+
(transport as any).blePlxManager = {
|
|
94
|
+
devices: jest.fn(() => Promise.resolve([device])),
|
|
95
|
+
connectedDevices: jest.fn(() => Promise.resolve([])),
|
|
96
|
+
connectToDevice: jest.fn(() => Promise.resolve(device)),
|
|
97
|
+
cancelTransaction: jest.fn(() => Promise.resolve()),
|
|
98
|
+
cancelDeviceConnection: jest.fn(() => Promise.resolve()),
|
|
99
|
+
onStateChange: jest.fn((listener: (state: string) => void) => {
|
|
100
|
+
setImmediate(() => listener('PoweredOn'));
|
|
101
|
+
return { remove: jest.fn() };
|
|
102
|
+
}),
|
|
103
|
+
state: jest.fn(() => Promise.resolve('PoweredOn')),
|
|
104
|
+
};
|
|
105
|
+
transport.init(
|
|
106
|
+
{ debug: jest.fn(), error: jest.fn(), warn: jest.fn() } as any,
|
|
107
|
+
new EventEmitter()
|
|
108
|
+
);
|
|
109
|
+
transport.configure(protocolV1Schema);
|
|
110
|
+
return { transport, device, writeCharacteristic, notifyCharacteristic };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type Outcome = { code?: unknown; params?: unknown; native?: unknown };
|
|
114
|
+
|
|
115
|
+
const flush = () =>
|
|
116
|
+
new Promise(resolve => {
|
|
117
|
+
setImmediate(resolve);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
/** Runs one acquire to its end, moving the fake clock past the transport's own waits. */
|
|
121
|
+
async function acquire(
|
|
122
|
+
transport: ReactNativeBleTransport,
|
|
123
|
+
input: Record<string, unknown> = {}
|
|
124
|
+
): Promise<Outcome> {
|
|
125
|
+
let outcome: Outcome | undefined;
|
|
126
|
+
transport.acquire({ uuid: UUID, ...input } as never).then(
|
|
127
|
+
() => {
|
|
128
|
+
outcome = {};
|
|
129
|
+
},
|
|
130
|
+
error => {
|
|
131
|
+
const { code, params } = ERRORS.serializeError({ error });
|
|
132
|
+
outcome = { code, params, native: (error as { iosErrorCode?: unknown }).iosErrorCode };
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
for (let i = 0; i < 400 && !outcome; i += 1) {
|
|
136
|
+
// eslint-disable-next-line no-await-in-loop
|
|
137
|
+
await flush();
|
|
138
|
+
if (!outcome) jest.advanceTimersByTime(100);
|
|
139
|
+
}
|
|
140
|
+
if (!outcome) throw new Error('acquire did not settle');
|
|
141
|
+
return outcome;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
describe('iOS link ended by the device right after connecting', () => {
|
|
145
|
+
beforeEach(() => {
|
|
146
|
+
jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance', 'nextTick'] });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
afterEach(() => {
|
|
150
|
+
jest.clearAllTimers();
|
|
151
|
+
jest.useRealTimers();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('keeps the first error so Core retries, then reports an invalid bond', async () => {
|
|
155
|
+
const { transport, device } = createHarness();
|
|
156
|
+
device.discoverAllServicesAndCharacteristics.mockImplementation(() =>
|
|
157
|
+
Promise.reject(peerEndedLink())
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Core retries an error it cannot classify, which is what makes the second attempt.
|
|
161
|
+
await expect(acquire(transport)).resolves.toEqual({ native: 7 });
|
|
162
|
+
await expect(acquire(transport)).resolves.toEqual({
|
|
163
|
+
code: HardwareErrorCode.BleBondInvalid,
|
|
164
|
+
params: { phase: 'connect', reason: 'peer_disconnected' },
|
|
165
|
+
});
|
|
166
|
+
// The verdict consumes the evidence, so the next one needs two attempts again.
|
|
167
|
+
await expect(acquire(transport)).resolves.toEqual({ native: 7 });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('counts a drop that interrupts the first write the same way', async () => {
|
|
171
|
+
const { transport, writeCharacteristic } = createHarness();
|
|
172
|
+
writeCharacteristic.writeWithResponse.mockImplementation(() => Promise.reject(peerEndedLink()));
|
|
173
|
+
|
|
174
|
+
await expect(acquire(transport)).resolves.toMatchObject({
|
|
175
|
+
code: HardwareErrorCode.BleDeviceNotBonded,
|
|
176
|
+
});
|
|
177
|
+
await expect(acquire(transport)).resolves.toMatchObject({
|
|
178
|
+
code: HardwareErrorCode.BleBondInvalid,
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('counts a drop that only the notification stream reports', async () => {
|
|
183
|
+
const { transport, notifyCharacteristic } = createHarness();
|
|
184
|
+
// The request went out before the link ended, so no write is there to fail.
|
|
185
|
+
notifyCharacteristic.monitor.mockImplementation(((
|
|
186
|
+
listener: (error: unknown, characteristic: unknown) => void
|
|
187
|
+
) => {
|
|
188
|
+
setTimeout(() => listener(peerEndedLink(), null), 50);
|
|
189
|
+
return { remove: jest.fn() };
|
|
190
|
+
}) as never);
|
|
191
|
+
|
|
192
|
+
await expect(acquire(transport)).resolves.not.toMatchObject({
|
|
193
|
+
code: HardwareErrorCode.BleBondInvalid,
|
|
194
|
+
});
|
|
195
|
+
await expect(acquire(transport)).resolves.toMatchObject({
|
|
196
|
+
code: HardwareErrorCode.BleBondInvalid,
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('needs the second drop soon after the first', async () => {
|
|
201
|
+
const { transport, device } = createHarness();
|
|
202
|
+
device.discoverAllServicesAndCharacteristics.mockImplementation(() =>
|
|
203
|
+
Promise.reject(peerEndedLink())
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
await acquire(transport);
|
|
207
|
+
jest.setSystemTime(Date.now() + IOS_PEER_TERMINATION_REPEAT_WINDOW_MS + 1);
|
|
208
|
+
|
|
209
|
+
await expect(acquire(transport)).resolves.toEqual({ native: 7 });
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('starts over after an attempt that failed for another reason', async () => {
|
|
213
|
+
const { transport, device } = createHarness();
|
|
214
|
+
device.discoverAllServicesAndCharacteristics
|
|
215
|
+
.mockImplementationOnce(() => Promise.reject(peerEndedLink()))
|
|
216
|
+
.mockImplementationOnce(() => Promise.reject(linkTimedOut()))
|
|
217
|
+
.mockImplementation(() => Promise.reject(peerEndedLink()));
|
|
218
|
+
|
|
219
|
+
await acquire(transport);
|
|
220
|
+
await expect(acquire(transport)).resolves.toEqual({ native: 6 });
|
|
221
|
+
await expect(acquire(transport)).resolves.toEqual({ native: 7 });
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('starts over once the device has answered', async () => {
|
|
225
|
+
const { transport, device } = createHarness();
|
|
226
|
+
(transport as any).detectProtocol = jest.fn(() => Promise.resolve('V1'));
|
|
227
|
+
device.discoverAllServicesAndCharacteristics
|
|
228
|
+
.mockImplementationOnce(() => Promise.reject(peerEndedLink()))
|
|
229
|
+
.mockImplementationOnce(() => Promise.resolve())
|
|
230
|
+
.mockImplementation(() => Promise.reject(peerEndedLink()));
|
|
231
|
+
|
|
232
|
+
await acquire(transport);
|
|
233
|
+
await expect(acquire(transport)).resolves.toEqual({});
|
|
234
|
+
|
|
235
|
+
await expect(acquire(transport)).resolves.toEqual({ native: 7 });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('never classifies a firmware-install reconnect', async () => {
|
|
239
|
+
const { transport, device } = createHarness();
|
|
240
|
+
device.discoverAllServicesAndCharacteristics.mockImplementation(() =>
|
|
241
|
+
Promise.reject(peerEndedLink())
|
|
242
|
+
);
|
|
243
|
+
const reconnect = { expectedProtocol: 'V2', skipProtocolProbe: true };
|
|
244
|
+
|
|
245
|
+
await expect(acquire(transport, reconnect)).resolves.toEqual({ native: 7 });
|
|
246
|
+
await expect(acquire(transport, reconnect)).resolves.toEqual({ native: 7 });
|
|
247
|
+
// The device was rebooting, so those drops are not evidence for a later attempt either.
|
|
248
|
+
await expect(acquire(transport)).resolves.toEqual({ native: 7 });
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('reports a lost bond that iOS names during GATT setup', async () => {
|
|
252
|
+
const { transport, device } = createHarness();
|
|
253
|
+
device.discoverAllServicesAndCharacteristics.mockImplementation(() =>
|
|
254
|
+
Promise.reject(nativeDisconnect(14, 'Peer removed pairing information'))
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
await expect(acquire(transport)).resolves.toMatchObject({
|
|
258
|
+
code: HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('IosPeerTerminationTracker', () => {
|
|
264
|
+
beforeEach(() => {
|
|
265
|
+
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
afterEach(() => {
|
|
269
|
+
jest.useRealTimers();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const dropLink = (tracker: IosPeerTerminationTracker, linkAgeMs = 0) => {
|
|
273
|
+
tracker.attemptStarted(UUID);
|
|
274
|
+
tracker.linkStarted(UUID);
|
|
275
|
+
jest.setSystemTime(Date.now() + linkAgeMs);
|
|
276
|
+
tracker.note(UUID, peerEndedLink());
|
|
277
|
+
return tracker.attemptFailed(UUID);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
test('ignores a drop on a link that was reused or had been up for a while', () => {
|
|
281
|
+
const tracker = new IosPeerTerminationTracker();
|
|
282
|
+
expect(dropLink(tracker)).toBe(false);
|
|
283
|
+
|
|
284
|
+
tracker.attemptStarted(UUID);
|
|
285
|
+
tracker.note(UUID, peerEndedLink());
|
|
286
|
+
expect(tracker.attemptFailed(UUID)).toBe(false);
|
|
287
|
+
|
|
288
|
+
expect(dropLink(tracker)).toBe(false);
|
|
289
|
+
expect(dropLink(tracker, IOS_PEER_TERMINATION_LINK_WINDOW_MS + 1)).toBe(false);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('ignores errors that do not say the peer ended the link', () => {
|
|
293
|
+
const tracker = new IosPeerTerminationTracker();
|
|
294
|
+
expect(dropLink(tracker)).toBe(false);
|
|
295
|
+
|
|
296
|
+
tracker.attemptStarted(UUID);
|
|
297
|
+
tracker.linkStarted(UUID);
|
|
298
|
+
tracker.note(UUID, linkTimedOut());
|
|
299
|
+
tracker.note(UUID, { errorCode: 201 });
|
|
300
|
+
tracker.note(UUID, undefined);
|
|
301
|
+
expect(tracker.attemptFailed(UUID)).toBe(false);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iOS reports a bond the device no longer holds as CBErrorPeerRemovedPairingInformation, but not
|
|
3
|
+
* on every iPhone. On the iPhone 17 family the device ends the link about a second after
|
|
4
|
+
* connecting instead (its firmware drops a link on the second key failure), and the app only sees
|
|
5
|
+
* CBErrorPeripheralDisconnected on whatever operation was in flight. The disconnect event carries
|
|
6
|
+
* no reason on iOS, so that error is the only place the code appears.
|
|
7
|
+
*
|
|
8
|
+
* A device that powers off or reboots right after connecting ends one link the same way, so a
|
|
9
|
+
* single drop proves nothing. iOS never re-pairs on its own, which makes a stale bond end every
|
|
10
|
+
* new link like that: two acquire attempts in a row, each dropped by the peer on a link it had
|
|
11
|
+
* just opened and before any response, is what this classifies as a stale bond.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** CBError.peripheralDisconnected: the peer ended the link. A link timeout is a different code. */
|
|
15
|
+
const IOS_PERIPHERAL_DISCONNECTED = 7;
|
|
16
|
+
|
|
17
|
+
/** The device drops a stale bond about a second after connecting; later drops mean something else. */
|
|
18
|
+
export const IOS_PEER_TERMINATION_LINK_WINDOW_MS = 10_000;
|
|
19
|
+
/** How long one dropped link stays usable as evidence for the next attempt. */
|
|
20
|
+
export const IOS_PEER_TERMINATION_REPEAT_WINDOW_MS = 60_000;
|
|
21
|
+
|
|
22
|
+
export const isIosPeerTerminationError = (error: unknown): boolean =>
|
|
23
|
+
(error as { iosErrorCode?: unknown } | null | undefined)?.iosErrorCode ===
|
|
24
|
+
IOS_PERIPHERAL_DISCONNECTED;
|
|
25
|
+
|
|
26
|
+
export class IosPeerTerminationTracker {
|
|
27
|
+
/** When the current acquire attempt opened its link; unset when it reuses an older one. */
|
|
28
|
+
private linkStartedAt = new Map<string, number>();
|
|
29
|
+
|
|
30
|
+
/** When the peer ended the current attempt's link. */
|
|
31
|
+
private terminatedAt = new Map<string, number>();
|
|
32
|
+
|
|
33
|
+
/** When the previous attempt ended that way. */
|
|
34
|
+
private evidenceAt = new Map<string, number>();
|
|
35
|
+
|
|
36
|
+
attemptStarted(deviceId: string) {
|
|
37
|
+
this.linkStartedAt.delete(deviceId);
|
|
38
|
+
this.terminatedAt.delete(deviceId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
linkStarted(deviceId: string) {
|
|
42
|
+
this.linkStartedAt.set(deviceId, Date.now());
|
|
43
|
+
this.terminatedAt.delete(deviceId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Reads the native error of a failed operation before it is mapped to a typed one. */
|
|
47
|
+
note(deviceId: string, error: unknown) {
|
|
48
|
+
if (!isIosPeerTerminationError(error)) return;
|
|
49
|
+
if (!this.terminatedAt.has(deviceId)) this.terminatedAt.set(deviceId, Date.now());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The device answered on this link, so its bond is valid. */
|
|
53
|
+
linkProven(deviceId: string) {
|
|
54
|
+
this.forget(deviceId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
forget(deviceId: string) {
|
|
58
|
+
this.linkStartedAt.delete(deviceId);
|
|
59
|
+
this.terminatedAt.delete(deviceId);
|
|
60
|
+
this.evidenceAt.delete(deviceId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Returns true when this failed attempt is the second in a row the peer ended on a new link. */
|
|
64
|
+
attemptFailed(deviceId: string): boolean {
|
|
65
|
+
const startedAt = this.linkStartedAt.get(deviceId);
|
|
66
|
+
const terminatedAt = this.terminatedAt.get(deviceId);
|
|
67
|
+
const previous = this.evidenceAt.get(deviceId);
|
|
68
|
+
this.forget(deviceId);
|
|
69
|
+
|
|
70
|
+
if (
|
|
71
|
+
startedAt === undefined ||
|
|
72
|
+
terminatedAt === undefined ||
|
|
73
|
+
terminatedAt - startedAt > IOS_PEER_TERMINATION_LINK_WINDOW_MS
|
|
74
|
+
) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (
|
|
78
|
+
previous !== undefined &&
|
|
79
|
+
terminatedAt - previous <= IOS_PEER_TERMINATION_REPEAT_WINDOW_MS
|
|
80
|
+
) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
this.evidenceAt.set(deviceId, terminatedAt);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
reset() {
|
|
88
|
+
this.linkStartedAt.clear();
|
|
89
|
+
this.terminatedAt.clear();
|
|
90
|
+
this.evidenceAt.clear();
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
stopBleEncryptionTracking,
|
|
60
60
|
waitForAndroidLinkEncryption,
|
|
61
61
|
} from './bleEncryption';
|
|
62
|
+
import { IosPeerTerminationTracker } from './bleIosStaleBond';
|
|
62
63
|
import { isNativeBleDisconnectError, toBleDisconnectHardwareError } from './bleNativeDisconnect';
|
|
63
64
|
import {
|
|
64
65
|
isBleStaleBondHardwareError,
|
|
@@ -588,6 +589,9 @@ export default class ReactNativeBleTransport {
|
|
|
588
589
|
/** When the current Android link attempt began; bounds which key-missing signals apply. */
|
|
589
590
|
private androidLinkStartedAt: Map<string, number> = new Map();
|
|
590
591
|
|
|
592
|
+
/** iOS acquire attempts whose new link the peer ended; two in a row mean a stale bond. */
|
|
593
|
+
private iosPeerTermination = new IosPeerTerminationTracker();
|
|
594
|
+
|
|
591
595
|
/** Strict or previously confirmed V2 target while acquire installs notifications. */
|
|
592
596
|
private acquiringProtocolV2 = new Set<string>();
|
|
593
597
|
|
|
@@ -1112,7 +1116,7 @@ export default class ReactNativeBleTransport {
|
|
|
1112
1116
|
try {
|
|
1113
1117
|
return await this.acquireUnlocked(input);
|
|
1114
1118
|
} catch (error) {
|
|
1115
|
-
throw await this.resolveAndroidBondInvalid(uuid, error);
|
|
1119
|
+
throw this.resolveIosBondInvalid(input, await this.resolveAndroidBondInvalid(uuid, error));
|
|
1116
1120
|
}
|
|
1117
1121
|
});
|
|
1118
1122
|
}
|
|
@@ -1120,6 +1124,7 @@ export default class ReactNativeBleTransport {
|
|
|
1120
1124
|
private async acquireUnlocked(input: FirmwareInstallBleAcquireInput) {
|
|
1121
1125
|
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1122
1126
|
const { uuid, forceCleanRunPromise, expectedProtocol, skipProtocolProbe } = input;
|
|
1127
|
+
this.iosPeerTermination.attemptStarted(uuid);
|
|
1123
1128
|
const shouldMapProtocolV2StaleBond = expectedProtocol
|
|
1124
1129
|
? expectedProtocol === 'V2'
|
|
1125
1130
|
: this.confirmedProtocolV2.has(uuid);
|
|
@@ -1492,6 +1497,7 @@ export default class ReactNativeBleTransport {
|
|
|
1492
1497
|
}
|
|
1493
1498
|
);
|
|
1494
1499
|
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1500
|
+
this.iosPeerTermination.linkProven(uuid);
|
|
1495
1501
|
const currentTransport = transportCache[uuid];
|
|
1496
1502
|
if (!currentTransport) {
|
|
1497
1503
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
@@ -1524,6 +1530,7 @@ export default class ReactNativeBleTransport {
|
|
|
1524
1530
|
const subscription = characteristic.monitor((error, c) => {
|
|
1525
1531
|
const isCurrentMonitor = this.monitorTokens.get(uuid) === monitorToken;
|
|
1526
1532
|
if (error) {
|
|
1533
|
+
if (isCurrentMonitor) this.iosPeerTermination.note(uuid, error);
|
|
1527
1534
|
Log?.debug(
|
|
1528
1535
|
`error monitor ${characteristic.uuid}, deviceId: ${characteristic.deviceID}: ${
|
|
1529
1536
|
error as unknown as string
|
|
@@ -1810,6 +1817,31 @@ export default class ReactNativeBleTransport {
|
|
|
1810
1817
|
});
|
|
1811
1818
|
}
|
|
1812
1819
|
|
|
1820
|
+
/**
|
|
1821
|
+
* iPhones that do not report a lost bond only show the device ending each new link. One such
|
|
1822
|
+
* attempt keeps its error so Core retries; the second in a row is the stale bond.
|
|
1823
|
+
*/
|
|
1824
|
+
private resolveIosBondInvalid(input: FirmwareInstallBleAcquireInput, error: unknown): unknown {
|
|
1825
|
+
if (Platform.OS !== 'ios') return error;
|
|
1826
|
+
const { uuid, skipProtocolProbe } = input;
|
|
1827
|
+
// A firmware-install reconnect expects the device to come and go.
|
|
1828
|
+
if (skipProtocolProbe || isBleStaleBondHardwareError(error)) {
|
|
1829
|
+
this.iosPeerTermination.forget(uuid);
|
|
1830
|
+
return error;
|
|
1831
|
+
}
|
|
1832
|
+
if (!this.iosPeerTermination.attemptFailed(uuid)) return error;
|
|
1833
|
+
Log?.debug(
|
|
1834
|
+
'[ReactNativeBleTransport] iOS peer ended two new links in a row, bond is invalid:',
|
|
1835
|
+
{
|
|
1836
|
+
connectIdSuffix: uuid.slice(-8),
|
|
1837
|
+
}
|
|
1838
|
+
);
|
|
1839
|
+
return ERRORS.TypedError(HardwareErrorCode.BleBondInvalid, undefined, {
|
|
1840
|
+
phase: 'connect',
|
|
1841
|
+
reason: 'peer_disconnected',
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1813
1845
|
/**
|
|
1814
1846
|
* Android 16+ encrypts a bonded link on its own right after connecting. Subscribing to
|
|
1815
1847
|
* notifications before that finishes gets the CCCD write rejected, and the framework's retry
|
|
@@ -2109,6 +2141,7 @@ export default class ReactNativeBleTransport {
|
|
|
2109
2141
|
this.androidPriorityResetTimers.clear();
|
|
2110
2142
|
this.androidHighPriorityDevices.clear();
|
|
2111
2143
|
this.androidLinkStartedAt.clear();
|
|
2144
|
+
this.iosPeerTermination.reset();
|
|
2112
2145
|
stopBleKeyMissingTracking();
|
|
2113
2146
|
stopBleEncryptionTracking();
|
|
2114
2147
|
const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
@@ -2426,6 +2459,7 @@ export default class ReactNativeBleTransport {
|
|
|
2426
2459
|
}),
|
|
2427
2460
|
]);
|
|
2428
2461
|
succeeded = true;
|
|
2462
|
+
this.iosPeerTermination.linkStarted(uuid);
|
|
2429
2463
|
return result;
|
|
2430
2464
|
} catch (error) {
|
|
2431
2465
|
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
@@ -2479,6 +2513,11 @@ export default class ReactNativeBleTransport {
|
|
|
2479
2513
|
succeeded = true;
|
|
2480
2514
|
return result;
|
|
2481
2515
|
} catch (error) {
|
|
2516
|
+
this.iosPeerTermination.note(uuid, error);
|
|
2517
|
+
if (Platform.OS === 'ios' && isNativeBleStaleBondError(error)) {
|
|
2518
|
+
// iOS can report the lost bond on the first request instead of on the connect.
|
|
2519
|
+
throw toBleStaleBondHardwareError(error);
|
|
2520
|
+
}
|
|
2482
2521
|
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
2483
2522
|
const resetManager = this.abandonStalledConnection(
|
|
2484
2523
|
uuid,
|
|
@@ -2596,6 +2635,7 @@ export default class ReactNativeBleTransport {
|
|
|
2596
2635
|
]);
|
|
2597
2636
|
this.writeTimeoutCounts.delete(uuid);
|
|
2598
2637
|
} catch (error) {
|
|
2638
|
+
this.iosPeerTermination.note(uuid, error);
|
|
2599
2639
|
if (timedOut) {
|
|
2600
2640
|
// A superseded call's late write must not tear down the link the current
|
|
2601
2641
|
// call is using; only the owner of the transport may declare it dead.
|