@onekeyfe/hd-transport-react-native 1.2.4-alpha.0 → 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/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,
@@ -233,55 +234,6 @@ const isAndroidLinkLossError = (error: unknown): boolean => {
233
234
  code === HardwareErrorCode.BleConnectedError
234
235
  );
235
236
  };
236
-
237
- const logIosBleBondDiagnostic = (
238
- phase: string,
239
- error: unknown,
240
- outcome: 'fallback' | 'rethrow'
241
- ) => {
242
- if (Platform.OS !== 'ios') return;
243
-
244
- const nativeError = (error ?? {}) as {
245
- name?: unknown;
246
- errorCode?: unknown;
247
- attErrorCode?: unknown;
248
- iosErrorCode?: unknown;
249
- androidErrorCode?: unknown;
250
- };
251
- const scalar = (value: unknown) =>
252
- typeof value === 'string' || typeof value === 'number' ? value : undefined;
253
-
254
- Log?.debug('[ReactNativeBleTransport][BleBondDiagnostic] probe error', {
255
- phase,
256
- outcome,
257
- errorName: scalar(nativeError.name),
258
- errorCode: scalar(nativeError.errorCode),
259
- attErrorCode: scalar(nativeError.attErrorCode),
260
- iosErrorCode: scalar(nativeError.iosErrorCode),
261
- androidErrorCode: scalar(nativeError.androidErrorCode),
262
- nativeStaleBond: isNativeBleStaleBondError(error),
263
- hardwareStaleBond: isBleStaleBondHardwareError(error),
264
- nativeDisconnect: isNativeBleDisconnectError(error),
265
- });
266
- };
267
-
268
- type IosProbeDisconnectEvidence = {
269
- phase: 'protocol-v1-get-features' | 'protocol-v2-ping';
270
- errorCode?: string | number;
271
- recordedAt: number;
272
- };
273
-
274
- const IOS_PROBE_DISCONNECT_EVIDENCE_TTL_MS = 5 * 60_000;
275
-
276
- const isIosProbeDisconnectError = (error: unknown): boolean => {
277
- if (Platform.OS !== 'ios') return false;
278
- const code = (error as { errorCode?: unknown })?.errorCode;
279
- return (
280
- isNativeBleDisconnectError(error) ||
281
- code === HardwareErrorCode.BleDeviceNotBonded ||
282
- code === HardwareErrorCode.BleDeviceDisconnected
283
- );
284
- };
285
237
  /** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
286
238
  export const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
287
239
  const DEVICE_SCAN_TIMEOUT_MS = 3000;
@@ -624,14 +576,6 @@ export default class ReactNativeBleTransport {
624
576
  /** Endpoints whose last detection got no answer; 'woken' once their Initialize wake is spent. */
625
577
  private silentDetections = new Map<string, 'silent' | 'woken'>();
626
578
 
627
- /**
628
- * iOS can report a reset Pro2 as a generic peripheral disconnect instead of
629
- * CBErrorPeerRemovedPairingInformation. Keep that first probe failure across
630
- * the automatic cold reconnect and require a second failed probe before
631
- * classifying the OS bond as invalid.
632
- */
633
- private iosProbeDisconnectEvidence = new Map<string, IosProbeDisconnectEvidence>();
634
-
635
579
  /** Android endpoints whose cached GATT table is suspect; the next connect refreshes it. */
636
580
  private androidGattCacheRefreshes = new Set<string>();
637
581
 
@@ -645,6 +589,9 @@ export default class ReactNativeBleTransport {
645
589
  /** When the current Android link attempt began; bounds which key-missing signals apply. */
646
590
  private androidLinkStartedAt: Map<string, number> = new Map();
647
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
+
648
595
  /** Strict or previously confirmed V2 target while acquire installs notifications. */
649
596
  private acquiringProtocolV2 = new Set<string>();
650
597
 
@@ -1169,7 +1116,7 @@ export default class ReactNativeBleTransport {
1169
1116
  try {
1170
1117
  return await this.acquireUnlocked(input);
1171
1118
  } catch (error) {
1172
- throw await this.resolveAndroidBondInvalid(uuid, error);
1119
+ throw this.resolveIosBondInvalid(input, await this.resolveAndroidBondInvalid(uuid, error));
1173
1120
  }
1174
1121
  });
1175
1122
  }
@@ -1177,6 +1124,7 @@ export default class ReactNativeBleTransport {
1177
1124
  private async acquireUnlocked(input: FirmwareInstallBleAcquireInput) {
1178
1125
  if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1179
1126
  const { uuid, forceCleanRunPromise, expectedProtocol, skipProtocolProbe } = input;
1127
+ this.iosPeerTermination.attemptStarted(uuid);
1180
1128
  const shouldMapProtocolV2StaleBond = expectedProtocol
1181
1129
  ? expectedProtocol === 'V2'
1182
1130
  : this.confirmedProtocolV2.has(uuid);
@@ -1549,6 +1497,7 @@ export default class ReactNativeBleTransport {
1549
1497
  }
1550
1498
  );
1551
1499
  if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1500
+ this.iosPeerTermination.linkProven(uuid);
1552
1501
  const currentTransport = transportCache[uuid];
1553
1502
  if (!currentTransport) {
1554
1503
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
@@ -1581,6 +1530,7 @@ export default class ReactNativeBleTransport {
1581
1530
  const subscription = characteristic.monitor((error, c) => {
1582
1531
  const isCurrentMonitor = this.monitorTokens.get(uuid) === monitorToken;
1583
1532
  if (error) {
1533
+ if (isCurrentMonitor) this.iosPeerTermination.note(uuid, error);
1584
1534
  Log?.debug(
1585
1535
  `error monitor ${characteristic.uuid}, deviceId: ${characteristic.deviceID}: ${
1586
1536
  error as unknown as string
@@ -1867,6 +1817,31 @@ export default class ReactNativeBleTransport {
1867
1817
  });
1868
1818
  }
1869
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
+
1870
1845
  /**
1871
1846
  * Android 16+ encrypts a bonded link on its own right after connecting. Subscribing to
1872
1847
  * notifications before that finishes gets the CCCD write rejected, and the framework's retry
@@ -2166,9 +2141,9 @@ export default class ReactNativeBleTransport {
2166
2141
  this.androidPriorityResetTimers.clear();
2167
2142
  this.androidHighPriorityDevices.clear();
2168
2143
  this.androidLinkStartedAt.clear();
2144
+ this.iosPeerTermination.reset();
2169
2145
  stopBleKeyMissingTracking();
2170
2146
  stopBleEncryptionTracking();
2171
- this.iosProbeDisconnectEvidence.clear();
2172
2147
  const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
2173
2148
  this.runPromise?.reject(error);
2174
2149
  this.runPromise = null;
@@ -2484,6 +2459,7 @@ export default class ReactNativeBleTransport {
2484
2459
  }),
2485
2460
  ]);
2486
2461
  succeeded = true;
2462
+ this.iosPeerTermination.linkStarted(uuid);
2487
2463
  return result;
2488
2464
  } catch (error) {
2489
2465
  if (timedOut || isNativeOperationTimeoutError(error)) {
@@ -2537,6 +2513,11 @@ export default class ReactNativeBleTransport {
2537
2513
  succeeded = true;
2538
2514
  return result;
2539
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
+ }
2540
2521
  if (timedOut || isNativeOperationTimeoutError(error)) {
2541
2522
  const resetManager = this.abandonStalledConnection(
2542
2523
  uuid,
@@ -2654,6 +2635,7 @@ export default class ReactNativeBleTransport {
2654
2635
  ]);
2655
2636
  this.writeTimeoutCounts.delete(uuid);
2656
2637
  } catch (error) {
2638
+ this.iosPeerTermination.note(uuid, error);
2657
2639
  if (timedOut) {
2658
2640
  // A superseded call's late write must not tear down the link the current
2659
2641
  // call is using; only the owner of the transport may declare it dead.
@@ -2788,64 +2770,6 @@ export default class ReactNativeBleTransport {
2788
2770
  );
2789
2771
  }
2790
2772
 
2791
- private rememberIosProbeDisconnect(
2792
- uuid: string,
2793
- phase: IosProbeDisconnectEvidence['phase'],
2794
- error: unknown
2795
- ): Error | undefined {
2796
- if (!isIosProbeDisconnectError(error)) return undefined;
2797
-
2798
- const previous = this.getIosProbeDisconnectEvidence(uuid);
2799
- const errorCode = (error as { errorCode?: unknown })?.errorCode;
2800
- const evidence: IosProbeDisconnectEvidence = {
2801
- phase,
2802
- ...(typeof errorCode === 'string' || typeof errorCode === 'number' ? { errorCode } : {}),
2803
- recordedAt: Date.now(),
2804
- };
2805
- this.iosProbeDisconnectEvidence.set(uuid, evidence);
2806
- Log?.debug('[ReactNativeBleTransport][BleBondDiagnostic] probe disconnect recorded', {
2807
- phase,
2808
- errorCode: evidence.errorCode,
2809
- repeated: Boolean(previous),
2810
- });
2811
-
2812
- if (!previous) return undefined;
2813
- return this.confirmIosProbeDisconnect(uuid, 'repeated-probe-disconnect');
2814
- }
2815
-
2816
- private getIosProbeDisconnectEvidence(uuid: string): IosProbeDisconnectEvidence | undefined {
2817
- const evidence = this.iosProbeDisconnectEvidence.get(uuid);
2818
- if (!evidence) return undefined;
2819
-
2820
- const ageMs = Date.now() - evidence.recordedAt;
2821
- if (ageMs <= IOS_PROBE_DISCONNECT_EVIDENCE_TTL_MS) return evidence;
2822
-
2823
- this.iosProbeDisconnectEvidence.delete(uuid);
2824
- Log?.debug('[ReactNativeBleTransport][BleBondDiagnostic] probe disconnect expired', {
2825
- triggerPhase: evidence.phase,
2826
- triggerErrorCode: evidence.errorCode,
2827
- ageMs,
2828
- });
2829
- return undefined;
2830
- }
2831
-
2832
- private confirmIosProbeDisconnect(
2833
- uuid: string,
2834
- confirmation: 'repeated-probe-disconnect' | 'follow-up-no-response'
2835
- ): Error | undefined {
2836
- const evidence = this.getIosProbeDisconnectEvidence(uuid);
2837
- if (!evidence) return undefined;
2838
-
2839
- Log?.debug('[ReactNativeBleTransport][BleBondDiagnostic] stale bond confirmed', {
2840
- confirmation,
2841
- triggerPhase: evidence.phase,
2842
- triggerErrorCode: evidence.errorCode,
2843
- evidenceAgeMs: Date.now() - evidence.recordedAt,
2844
- finalErrorCode: HardwareErrorCode.BleBondInvalid,
2845
- });
2846
- return ERRORS.TypedError(HardwareErrorCode.BleBondInvalid);
2847
- }
2848
-
2849
2773
  private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
2850
2774
  if (this.probingProtocols.get(uuid) === protocol) {
2851
2775
  this.probingProtocols.delete(uuid);
@@ -2877,7 +2801,6 @@ export default class ReactNativeBleTransport {
2877
2801
  // received no response. skipProtocolProbe does not need it either — its only
2878
2802
  // caller is the V2 firmware-install reconnect.
2879
2803
  if (expectedProtocol === 'V1') {
2880
- this.iosProbeDisconnectEvidence.delete(uuid);
2881
2804
  this.deviceProtocol.set(uuid, 'V1');
2882
2805
  Log?.debug('[ReactNativeBleTransport] protocol selected', {
2883
2806
  deviceId: uuid,
@@ -2893,7 +2816,6 @@ export default class ReactNativeBleTransport {
2893
2816
 
2894
2817
  if (expectedProtocol === 'V2') {
2895
2818
  if (await this.probeProtocolV2(uuid)) {
2896
- this.iosProbeDisconnectEvidence.delete(uuid);
2897
2819
  this.deviceProtocol.set(uuid, 'V2');
2898
2820
  this.sessionProtocols.set(uuid, 'V2');
2899
2821
  this.confirmedProtocolV2.add(uuid);
@@ -2904,18 +2826,6 @@ export default class ReactNativeBleTransport {
2904
2826
  });
2905
2827
  return 'V2';
2906
2828
  }
2907
- const staleBondError = this.confirmIosProbeDisconnect(uuid, 'follow-up-no-response');
2908
- if (Platform.OS === 'ios') {
2909
- Log?.debug('[ReactNativeBleTransport][BleBondDiagnostic] protocol detection ended', {
2910
- outcome: 'expected-v2-no-response',
2911
- expectedProtocol,
2912
- protocolHint: protocolHint ?? 'none',
2913
- finalErrorCode: staleBondError
2914
- ? HardwareErrorCode.BleBondInvalid
2915
- : HardwareErrorCode.RuntimeError,
2916
- });
2917
- }
2918
- if (staleBondError) throw staleBondError;
2919
2829
  throw this.createProtocolMismatchError(expectedProtocol);
2920
2830
  }
2921
2831
 
@@ -2951,7 +2861,6 @@ export default class ReactNativeBleTransport {
2951
2861
  const detected =
2952
2862
  protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
2953
2863
  if (detected) {
2954
- this.iosProbeDisconnectEvidence.delete(uuid);
2955
2864
  this.deviceProtocol.set(uuid, protocol);
2956
2865
  this.sessionProtocols.set(uuid, protocol);
2957
2866
  if (protocol === 'V2') {
@@ -2981,25 +2890,6 @@ export default class ReactNativeBleTransport {
2981
2890
 
2982
2891
  this.deviceProtocol.delete(uuid);
2983
2892
  this.probingProtocols.delete(uuid);
2984
- const staleBondError = this.confirmIosProbeDisconnect(uuid, 'follow-up-no-response');
2985
- if (Platform.OS === 'ios') {
2986
- Log?.debug('[ReactNativeBleTransport][BleBondDiagnostic] protocol detection ended', {
2987
- outcome: 'all-probes-no-response',
2988
- expectedProtocol: expectedProtocol ?? 'none',
2989
- protocolHint: protocolHint ?? 'none',
2990
- sessionProtocol: sessionProtocol ?? 'none',
2991
- probeOrder: probeOrder.join(','),
2992
- trustSessionProtocol,
2993
- reprobeFailures,
2994
- transportCached: Boolean(transportCache[uuid]),
2995
- notifyActive: Boolean(transportCache[uuid]?.notifySubscription),
2996
- staleBondRecorded: this.iosProbeDisconnectEvidence.has(uuid),
2997
- finalErrorCode: staleBondError
2998
- ? HardwareErrorCode.BleBondInvalid
2999
- : HardwareErrorCode.BleTimeoutError,
3000
- });
3001
- }
3002
- if (staleBondError) throw staleBondError;
3003
2893
  throw this.createProtocolDetectionError();
3004
2894
  }
3005
2895
 
@@ -3098,19 +2988,7 @@ export default class ReactNativeBleTransport {
3098
2988
  } catch (error) {
3099
2989
  this.clearProbeProtocol(uuid, 'V1');
3100
2990
  Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
3101
- const shouldRethrow = shouldRethrowProtocolProbeError(error);
3102
- const staleBondError = this.rememberIosProbeDisconnect(
3103
- uuid,
3104
- 'protocol-v1-get-features',
3105
- error
3106
- );
3107
- logIosBleBondDiagnostic(
3108
- 'protocol-v1-get-features',
3109
- error,
3110
- shouldRethrow ? 'rethrow' : 'fallback'
3111
- );
3112
- if (staleBondError) throw staleBondError;
3113
- if (shouldRethrow) {
2991
+ if (shouldRethrowProtocolProbeError(error)) {
3114
2992
  throw error;
3115
2993
  }
3116
2994
  return false;
@@ -3125,26 +3003,18 @@ export default class ReactNativeBleTransport {
3125
3003
  this.probingProtocols.set(uuid, 'V2');
3126
3004
  this.protocolV2Assemblers.get(uuid)?.reset();
3127
3005
  this.throwIfStaleBondError(uuid);
3128
- let detected: boolean;
3129
- try {
3130
- detected = await probeProtocolV2Helper({
3131
- call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
3132
- this.callProtocolV2(uuid, name, data, options),
3133
- timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT_MS,
3134
- logger: Log,
3135
- logPrefix: 'ProtocolV2 RN-BLE',
3136
- onProbeFailed: () => {
3137
- this.protocolV2Assemblers.get(uuid)?.reset();
3138
- this.resetProtocolV2Frames(uuid);
3139
- },
3140
- shouldRethrow: shouldRethrowProtocolProbeError,
3141
- });
3142
- } catch (error) {
3143
- const staleBondError = this.rememberIosProbeDisconnect(uuid, 'protocol-v2-ping', error);
3144
- logIosBleBondDiagnostic('protocol-v2-ping', error, 'rethrow');
3145
- if (staleBondError) throw staleBondError;
3146
- throw error;
3147
- }
3006
+ const detected = await probeProtocolV2Helper({
3007
+ call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
3008
+ this.callProtocolV2(uuid, name, data, options),
3009
+ timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT_MS,
3010
+ logger: Log,
3011
+ logPrefix: 'ProtocolV2 RN-BLE',
3012
+ onProbeFailed: () => {
3013
+ this.protocolV2Assemblers.get(uuid)?.reset();
3014
+ this.resetProtocolV2Frames(uuid);
3015
+ },
3016
+ shouldRethrow: shouldRethrowProtocolProbeError,
3017
+ });
3148
3018
  if (!detected) {
3149
3019
  this.clearProbeProtocol(uuid, 'V2');
3150
3020
  } else {