@onekeyfe/hd-transport-react-native 1.2.2-alpha.104 → 1.2.2-alpha.106

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
@@ -47,6 +47,7 @@ import {
47
47
  getInfosForServiceUuid,
48
48
  isSameBleUuid,
49
49
  } from './constants';
50
+ import { isNativeBleDisconnectError, toBleDisconnectHardwareError } from './bleNativeDisconnect';
50
51
  import {
51
52
  isBleStaleBondHardwareError,
52
53
  isNativeBleStaleBondError,
@@ -76,6 +77,8 @@ const { check, ProtocolV1, parseConfigure } = transport;
76
77
  const Log = bleLogger;
77
78
 
78
79
  const transportCache: Record<string, BleTransport> = {};
80
+ // ble-plx shares one manager across transport instances in this JS runtime.
81
+ let bleManagerResetPromise: Promise<void> | undefined;
79
82
  const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
80
83
  const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
81
84
  const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
@@ -168,6 +171,23 @@ const isWedgedWriteError = (error: unknown): boolean =>
168
171
  (error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleWriteCharacteristicError &&
169
172
  typeof (error as { message?: unknown })?.message === 'string' &&
170
173
  (error as { message: string }).message.startsWith(WEDGED_WRITE_MESSAGE);
174
+ const shouldRethrowProtocolProbeError = (error: unknown): boolean => {
175
+ const code = (error as { errorCode?: unknown })?.errorCode;
176
+ // Bonding and GATT failures are not evidence of a protocol mismatch. Preserve
177
+ // them instead of probing another protocol on an unusable connection.
178
+ // Native PLX disconnects (errorCode 201 / iOS 7) must match before they are
179
+ // mapped: Protocol V2 writes rethrow them unchanged unless normalized first.
180
+ return (
181
+ isBleStaleBondHardwareError(error) ||
182
+ isNativeBleDisconnectError(error) ||
183
+ code === HardwareErrorCode.BleDeviceNotBonded ||
184
+ code === HardwareErrorCode.BleDeviceBondedCanceled ||
185
+ code === HardwareErrorCode.BleDeviceDisconnected ||
186
+ code === HardwareErrorCode.BleCharacteristicNotifyError ||
187
+ code === HardwareErrorCode.BleCharacteristicNotifyChangeFailure ||
188
+ code === HardwareErrorCode.BleWriteCharacteristicError
189
+ );
190
+ };
171
191
  /** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
172
192
  export const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
173
193
  const DEVICE_SCAN_TIMEOUT_MS = 3000;
@@ -450,6 +470,10 @@ export default class ReactNativeBleTransport {
450
470
  /** Serializes transport lifecycle changes for the same physical device. */
451
471
  private lifecycleOperations: Map<string, Promise<void>> = new Map();
452
472
 
473
+ private stopPromise?: Promise<void>;
474
+
475
+ private scanCleanups = new Set<() => Promise<void>>();
476
+
453
477
  constructor(options: TransportOptions) {
454
478
  this.scanTimeout = options.scanTimeout ?? DEVICE_SCAN_TIMEOUT_MS;
455
479
  }
@@ -485,10 +509,34 @@ export default class ReactNativeBleTransport {
485
509
  // empty
486
510
  }
487
511
 
488
- getPlxManager(): Promise<BlePlxManager> {
489
- if (this.blePlxManager) return Promise.resolve(this.blePlxManager);
490
- this.blePlxManager = new BlePlxManager();
491
- return Promise.resolve(this.blePlxManager);
512
+ async getPlxManager(): Promise<BlePlxManager> {
513
+ while (bleManagerResetPromise) {
514
+ await this.waitForManagerReset();
515
+ }
516
+ if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
517
+ if (!this.blePlxManager) this.blePlxManager = new BlePlxManager();
518
+ return this.blePlxManager;
519
+ }
520
+
521
+ private async waitForManagerReset(): Promise<void> {
522
+ if (!bleManagerResetPromise) return;
523
+ let timeout: ReturnType<typeof setTimeout> | undefined;
524
+ try {
525
+ await Promise.race([
526
+ bleManagerResetPromise,
527
+ new Promise<never>((_, reject) => {
528
+ timeout = setTimeout(
529
+ () => reject(this.createWedgedBleSetupError()),
530
+ BLE_CONNECT_TIMEOUT_MS
531
+ );
532
+ }),
533
+ ]);
534
+ } catch {
535
+ // A timeout or failed destroy is not permission to reuse the old singleton.
536
+ throw this.createWedgedBleSetupError();
537
+ } finally {
538
+ if (timeout) clearTimeout(timeout);
539
+ }
492
540
  }
493
541
 
494
542
  async resolveCharacteristics(device: Device): Promise<ResolvedBleCharacteristics> {
@@ -677,35 +725,58 @@ export default class ReactNativeBleTransport {
677
725
  * @returns
678
726
  */
679
727
  async enumerate() {
680
- // eslint-disable-next-line no-async-promise-executor
681
- return new Promise<IOneKeyDevice[]>(async (resolve, reject) => {
682
- const deviceList: IOneKeyDevice[] = [];
683
- const blePlxManager = await this.getPlxManager();
684
- try {
685
- await subscribeBleOn(blePlxManager);
686
- } catch (error) {
687
- Log?.debug('subscribeBleOn error: ', error);
688
- reject(error);
689
- return;
690
- }
691
-
692
- if (Platform.OS === 'android' && Platform.Version >= 31) {
693
- Log?.debug('requesting permissions, please wait...');
728
+ const scanStartedAt = Date.now();
729
+ let firstDeviceMs: number | undefined;
730
+ const blePlxManager = await this.getPlxManager();
731
+ await subscribeBleOn(blePlxManager);
732
+ if (Platform.OS === 'android' && Platform.Version >= 31) {
733
+ Log?.debug('requesting permissions, please wait...');
694
734
 
695
- const resultConnect = await PermissionsAndroid.requestMultiple([
696
- PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
697
- PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
698
- ]);
735
+ const resultConnect = await PermissionsAndroid.requestMultiple([
736
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
737
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
738
+ ]);
699
739
 
700
- Log?.debug('requesting permissions, result: ', resultConnect);
701
- if (
702
- resultConnect[PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT] !== 'granted' ||
703
- resultConnect[PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN] !== 'granted'
704
- ) {
705
- reject(ERRORS.TypedError(HardwareErrorCode.BlePermissionError));
706
- return;
707
- }
740
+ Log?.debug('requesting permissions, result: ', resultConnect);
741
+ if (
742
+ resultConnect[PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT] !== 'granted' ||
743
+ resultConnect[PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN] !== 'granted'
744
+ ) {
745
+ throw ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
708
746
  }
747
+ }
748
+
749
+ if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
750
+ return new Promise<IOneKeyDevice[]>((resolve, reject) => {
751
+ const deviceList: IOneKeyDevice[] = [];
752
+ let finished = false;
753
+ let scanCleanup: Promise<void> | undefined;
754
+ const finishScan = (error?: unknown) => {
755
+ if (scanCleanup) return scanCleanup;
756
+ finished = true;
757
+ clearScanTimer();
758
+ scanCleanup = this.runNativeTeardown('scan', blePlxManager, async () => {
759
+ await blePlxManager.stopDeviceScan();
760
+ }).then(() => {
761
+ this.scanCleanups.delete(cancelScan);
762
+ if (error) reject(error);
763
+ else resolve(deviceList);
764
+ });
765
+ return scanCleanup;
766
+ };
767
+ const cancelScan = () =>
768
+ finishScan(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected));
769
+ this.scanCleanups.add(cancelScan);
770
+
771
+ const clearScanTimer = timer.timeout(() => {
772
+ Log?.debug('[ReactNativeBleTransport] scan completed', {
773
+ elapsedMs: Date.now() - scanStartedAt,
774
+ firstDeviceMs,
775
+ deviceCount: deviceList.length,
776
+ scanWindowMs: this.scanTimeout,
777
+ });
778
+ finishScan();
779
+ }, this.scanTimeout);
709
780
 
710
781
  blePlxManager.startDeviceScan(
711
782
  getBluetoothServiceUuids(),
@@ -721,18 +792,17 @@ export default class ReactNativeBleTransport {
721
792
  error.errorCode
722
793
  )
723
794
  ) {
724
- reject(ERRORS.TypedError(HardwareErrorCode.BlePermissionError));
795
+ finishScan(ERRORS.TypedError(HardwareErrorCode.BlePermissionError));
725
796
  } else if (error.errorCode === BleErrorCode.BluetoothUnauthorized) {
726
- reject(ERRORS.TypedError(HardwareErrorCode.BleLocationError));
797
+ finishScan(ERRORS.TypedError(HardwareErrorCode.BleLocationError));
727
798
  } else if (error.errorCode === BleErrorCode.LocationServicesDisabled) {
728
- reject(ERRORS.TypedError(HardwareErrorCode.BleLocationServicesDisabled));
799
+ finishScan(ERRORS.TypedError(HardwareErrorCode.BleLocationServicesDisabled));
729
800
  } else if (error.errorCode === BleErrorCode.ScanStartFailed) {
730
801
  // Android Bluetooth will report an error when the search frequency is too fast,
731
802
  // then nothing is processed and an empty array of devices is returned.
732
803
  // Then the next search will be back to normal
733
- timer.timeout(() => {}, this.scanTimeout);
734
804
  } else {
735
- reject(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.reason ?? ''));
805
+ finishScan(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.reason ?? ''));
736
806
  }
737
807
  return;
738
808
  }
@@ -764,6 +834,7 @@ export default class ReactNativeBleTransport {
764
834
  }
765
835
  );
766
836
 
837
+ if (finished) return;
767
838
  getConnectedDeviceIds(Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(
768
839
  devices => {
769
840
  for (const device of devices) {
@@ -783,11 +854,13 @@ export default class ReactNativeBleTransport {
783
854
  addDevice(device as unknown as Device);
784
855
  }
785
856
  }
786
- }
857
+ },
858
+ error => Log?.debug('search connected peripheral failed:', error)
787
859
  );
788
860
 
789
861
  const addDevice = (device: Device) => {
790
- if (deviceList.every(d => d.id !== device.id)) {
862
+ if (!finished && deviceList.every(d => d.id !== device.id)) {
863
+ firstDeviceMs ??= Date.now() - scanStartedAt;
791
864
  const displayName = getDeviceDisplayName(device) ?? 'Unknown BLE Device';
792
865
 
793
866
  deviceList.push({
@@ -802,11 +875,6 @@ export default class ReactNativeBleTransport {
802
875
  });
803
876
  }
804
877
  };
805
-
806
- timer.timeout(() => {
807
- blePlxManager.stopDeviceScan();
808
- resolve(deviceList);
809
- }, this.scanTimeout);
810
878
  });
811
879
  }
812
880
 
@@ -894,6 +962,7 @@ export default class ReactNativeBleTransport {
894
962
  }
895
963
 
896
964
  private async acquireUnlocked(input: FirmwareInstallBleAcquireInput) {
965
+ if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
897
966
  const { uuid, forceCleanRunPromise, expectedProtocol, skipProtocolProbe } = input;
898
967
  const shouldMapProtocolV2StaleBond = expectedProtocol
899
968
  ? expectedProtocol === 'V2'
@@ -1170,7 +1239,7 @@ export default class ReactNativeBleTransport {
1170
1239
  this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
1171
1240
  return { uuid, protocolType };
1172
1241
  } catch (error) {
1173
- if (isBleStaleBondHardwareError(error)) {
1242
+ if (isBleStaleBondHardwareError(error) || shouldRethrowProtocolProbeError(error)) {
1174
1243
  await this.disconnectUnlocked(uuid);
1175
1244
  } else {
1176
1245
  await this.releaseUnlocked(uuid, true);
@@ -1703,7 +1772,34 @@ export default class ReactNativeBleTransport {
1703
1772
  }
1704
1773
 
1705
1774
  stop() {
1775
+ if (this.stopPromise) return this.stopPromise;
1706
1776
  this.stopped = true;
1777
+ const deviceIds = new Set([
1778
+ ...this.monitorTokens.keys(),
1779
+ ...this.sessionProtocols.keys(),
1780
+ ...this.lifecycleOperations.keys(),
1781
+ ...(this.runPromiseDeviceId ? [this.runPromiseDeviceId] : []),
1782
+ ]);
1783
+ const scans = Array.from(this.scanCleanups, cleanup => cleanup());
1784
+ this.androidPriorityResetTimers.forEach(timeout => clearTimeout(timeout));
1785
+ this.androidPriorityResetTimers.clear();
1786
+ this.androidHighPriorityDevices.clear();
1787
+ const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1788
+ this.runPromise?.reject(error);
1789
+ this.runPromise = null;
1790
+ this.runPromiseDeviceId = null;
1791
+ deviceIds.forEach(uuid => this.rejectProtocolV2Frames(uuid, error));
1792
+ // Release only this transport's endpoints; other connectors may share ble-plx.
1793
+ this.stopPromise = Promise.all([
1794
+ ...scans,
1795
+ ...Array.from(deviceIds, uuid => this.disconnect(uuid)),
1796
+ ]).then(async () => {
1797
+ await this.protocolV2Links.invalidateAllLinks('React Native BLE transport stopped');
1798
+ await this.waitForManagerReset();
1799
+ this.blePlxManager = undefined;
1800
+ this.emitter = undefined;
1801
+ });
1802
+ return this.stopPromise;
1707
1803
  }
1708
1804
 
1709
1805
  async disconnect(session: string) {
@@ -1855,17 +1951,26 @@ export default class ReactNativeBleTransport {
1855
1951
  }
1856
1952
  }
1857
1953
 
1858
- cancel() {
1954
+ async cancel() {
1859
1955
  Log?.debug('transport-react-native transport cancel');
1860
- if (this.runPromise) {
1861
- // this.runPromise.reject(new Error('Transport_CallCanceled'));
1956
+ const pending = this.runPromise;
1957
+ const deviceId = this.runPromiseDeviceId;
1958
+ if (pending) {
1959
+ pending.reject(ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled));
1960
+ if (this.runPromise === pending) {
1961
+ this.runPromise = null;
1962
+ this.runPromiseDeviceId = null;
1963
+ }
1964
+ // A V1 read cannot be safely reused after abandoning its response.
1965
+ // Drain native teardown before DeviceCommands releases the operation.
1966
+ if (deviceId) await this.disconnect(deviceId);
1862
1967
  }
1863
- this.runPromise = null;
1864
- this.runPromiseDeviceId = null;
1865
1968
  }
1866
1969
 
1867
1970
  /** Run a native connect under the JS backstop budget. */
1868
1971
  private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
1972
+ const startedAt = Date.now();
1973
+ let succeeded = false;
1869
1974
  let timer: ReturnType<typeof setTimeout> | undefined;
1870
1975
  let timedOut = false;
1871
1976
  const pending = connect();
@@ -1887,6 +1992,7 @@ export default class ReactNativeBleTransport {
1887
1992
  }, BLE_CONNECT_TIMEOUT_MS);
1888
1993
  }),
1889
1994
  ]);
1995
+ succeeded = true;
1890
1996
  return result;
1891
1997
  } catch (error) {
1892
1998
  if (timedOut || isNativeOperationTimeoutError(error)) {
@@ -1901,6 +2007,12 @@ export default class ReactNativeBleTransport {
1901
2007
  throw error;
1902
2008
  } finally {
1903
2009
  if (timer) clearTimeout(timer);
2010
+ Log?.debug('[ReactNativeBleTransport] connect completed', {
2011
+ connectIdSuffix: uuid.slice(-8),
2012
+ elapsedMs: Date.now() - startedAt,
2013
+ succeeded,
2014
+ backstopExpired: timedOut,
2015
+ });
1904
2016
  }
1905
2017
  }
1906
2018
 
@@ -1909,6 +2021,8 @@ export default class ReactNativeBleTransport {
1909
2021
  uuid: string,
1910
2022
  device: Device
1911
2023
  ): Promise<ResolvedBleCharacteristics> {
2024
+ const startedAt = Date.now();
2025
+ let succeeded = false;
1912
2026
  let timer: ReturnType<typeof setTimeout> | undefined;
1913
2027
  let timedOut = false;
1914
2028
  const pending = this.resolveCharacteristics(device);
@@ -1929,6 +2043,7 @@ export default class ReactNativeBleTransport {
1929
2043
  }),
1930
2044
  ]);
1931
2045
  this.connectionSetupTimeoutCounts.delete(uuid);
2046
+ succeeded = true;
1932
2047
  return result;
1933
2048
  } catch (error) {
1934
2049
  if (timedOut || isNativeOperationTimeoutError(error)) {
@@ -1943,6 +2058,12 @@ export default class ReactNativeBleTransport {
1943
2058
  throw error;
1944
2059
  } finally {
1945
2060
  if (timer) clearTimeout(timer);
2061
+ Log?.debug('[ReactNativeBleTransport] GATT setup completed', {
2062
+ connectIdSuffix: uuid.slice(-8),
2063
+ elapsedMs: Date.now() - startedAt,
2064
+ succeeded,
2065
+ backstopExpired: timedOut,
2066
+ });
1946
2067
  }
1947
2068
  }
1948
2069
 
@@ -2086,6 +2207,7 @@ export default class ReactNativeBleTransport {
2086
2207
  }
2087
2208
 
2088
2209
  private resetPlxManager() {
2210
+ if (bleManagerResetPromise) return;
2089
2211
  const manager = this.blePlxManager;
2090
2212
  this.blePlxManager = undefined;
2091
2213
  const reason = 'React Native BLE manager reset';
@@ -2132,11 +2254,21 @@ export default class ReactNativeBleTransport {
2132
2254
  this.connectionSetupTimeoutCounts.clear();
2133
2255
  this.monitorTokens.clear();
2134
2256
  this.protocolV2Assemblers.clear();
2257
+ let reset: Promise<void>;
2135
2258
  try {
2136
- manager?.destroy();
2259
+ reset = Promise.resolve(manager?.destroy());
2137
2260
  } catch (error) {
2138
- Log?.debug('[ReactNativeBleTransport] BLE manager destroy failed (ignored):', error);
2261
+ reset = Promise.reject(error);
2139
2262
  }
2263
+ bleManagerResetPromise = reset;
2264
+ reset.then(
2265
+ () => {
2266
+ if (bleManagerResetPromise === reset) bleManagerResetPromise = undefined;
2267
+ },
2268
+ error => {
2269
+ Log?.error('[ReactNativeBleTransport] BLE manager destroy failed:', error);
2270
+ }
2271
+ );
2140
2272
  }
2141
2273
 
2142
2274
  private createProtocolMismatchError(expected: ProtocolType) {
@@ -2339,9 +2471,7 @@ export default class ReactNativeBleTransport {
2339
2471
  } catch (error) {
2340
2472
  this.clearProbeProtocol(uuid, 'V1');
2341
2473
  Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
2342
- // A wedged write already dropped the link, so probing another protocol on it
2343
- // would only fail against a torn-down transport: surface the real cause.
2344
- if (isWedgedWriteError(error)) {
2474
+ if (shouldRethrowProtocolProbeError(error)) {
2345
2475
  throw error;
2346
2476
  }
2347
2477
  return false;
@@ -2366,7 +2496,7 @@ export default class ReactNativeBleTransport {
2366
2496
  this.protocolV2Assemblers.get(uuid)?.reset();
2367
2497
  this.resetProtocolV2Frames(uuid);
2368
2498
  },
2369
- shouldRethrow: isBleStaleBondHardwareError,
2499
+ shouldRethrow: shouldRethrowProtocolProbeError,
2370
2500
  });
2371
2501
  if (!detected) {
2372
2502
  this.clearProbeProtocol(uuid, 'V2');
@@ -2513,6 +2643,9 @@ export default class ReactNativeBleTransport {
2513
2643
  this.rememberStaleBondError(uuid, bondError);
2514
2644
  throw bondError;
2515
2645
  }
2646
+ if (isNativeBleDisconnectError(error)) {
2647
+ throw toBleDisconnectHardwareError(error);
2648
+ }
2516
2649
  if (
2517
2650
  getFirmwareUploadWriteRetryType(error) !== 'congested' ||
2518
2651
  attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES