@onekeyfe/hd-transport-react-native 1.2.2-alpha.110 → 1.2.2-alpha.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -420,10 +420,10 @@ function getDeviceDisplayName(device) {
420
420
  }
421
421
  const IOS_REQUEST_MTU = 247;
422
422
  const ANDROID_REQUEST_MTU = 517;
423
- const BLE_MTU_REFRESH_RETRY_DELAY_MS = 200;
424
423
  const ANDROID_HIGH_PRIORITY_IDLE_MS = 1000;
425
424
  const getRequestedBleMtu = () => reactNative.Platform.OS === 'android' ? ANDROID_REQUEST_MTU : IOS_REQUEST_MTU;
426
425
  const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
426
+ const BLE_MTU_REQUEST_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS;
427
427
  const connectOptions = {
428
428
  requestMTU: getRequestedBleMtu(),
429
429
  timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
@@ -457,14 +457,46 @@ const tryToGetConfiguration = (device) => {
457
457
  return null;
458
458
  return infos;
459
459
  };
460
- const requestNegotiatedMtu = (device, stage, attempt) => __awaiter(void 0, void 0, void 0, function* () {
460
+ const requestNegotiatedMtu = (device, stage, attempt, cancelTransaction) => __awaiter(void 0, void 0, void 0, function* () {
461
461
  if (reactNative.Platform.OS !== 'ios' && reactNative.Platform.OS !== 'android')
462
462
  return device;
463
+ const transactionId = `${device.id}:mtu:${stage}:${attempt}:${Date.now()}`;
464
+ let timeoutId;
465
+ let timedOut = false;
463
466
  try {
464
- const mtuDevice = yield device.requestMTU(getRequestedBleMtu());
467
+ const request = device.requestMTU(getRequestedBleMtu(), transactionId);
468
+ const mtuDevice = yield Promise.race([
469
+ request,
470
+ new Promise((_, reject) => {
471
+ timeoutId = setTimeout(() => {
472
+ timedOut = true;
473
+ reject(new Error(`BLE MTU request timeout after ${BLE_MTU_REQUEST_TIMEOUT_MS}ms`));
474
+ }, BLE_MTU_REQUEST_TIMEOUT_MS);
475
+ }),
476
+ ]);
465
477
  return mtuDevice;
466
478
  }
467
479
  catch (error) {
480
+ if (timedOut && cancelTransaction) {
481
+ try {
482
+ Promise.resolve(cancelTransaction(transactionId)).catch(cancelError => {
483
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] MTU cancellation failed', {
484
+ platform: reactNative.Platform.OS,
485
+ stage,
486
+ attempt,
487
+ error: cancelError instanceof Error ? cancelError.message : String(cancelError),
488
+ });
489
+ });
490
+ }
491
+ catch (cancelError) {
492
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] MTU cancellation failed', {
493
+ platform: reactNative.Platform.OS,
494
+ stage,
495
+ attempt,
496
+ error: cancelError instanceof Error ? cancelError.message : String(cancelError),
497
+ });
498
+ }
499
+ }
468
500
  Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] MTU refresh failed, continuing with current value', {
469
501
  platform: reactNative.Platform.OS,
470
502
  stage,
@@ -474,8 +506,14 @@ const requestNegotiatedMtu = (device, stage, attempt) => __awaiter(void 0, void
474
506
  });
475
507
  return device;
476
508
  }
509
+ finally {
510
+ if (timeoutId)
511
+ clearTimeout(timeoutId);
512
+ }
477
513
  });
478
- const resolveNegotiatedMtu = (device) => requestNegotiatedMtu(device, 'connected', 0);
514
+ const resolveNegotiatedMtu = (device, cancelTransaction) => shouldRefreshNegotiatedMtu(device.mtu)
515
+ ? requestNegotiatedMtu(device, 'connected', 0, cancelTransaction)
516
+ : Promise.resolve(device);
479
517
  function remapError(error) {
480
518
  var _a;
481
519
  if (error instanceof reactNativeBlePlx.BleError) {
@@ -529,11 +567,20 @@ class ReactNativeBleTransport {
529
567
  },
530
568
  classifyError: () => 'link-fatal',
531
569
  onLinkInvalidated: (uuid, reason) => __awaiter(this, void 0, void 0, function* () {
532
- var _b;
570
+ var _b, _c;
533
571
  (_b = this.protocolV2Assemblers.get(uuid)) === null || _b === void 0 ? void 0 : _b.reset();
534
572
  this.rejectProtocolV2Frames(uuid, new Error(reason));
535
573
  Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason);
536
574
  if (reason.startsWith('Protocol V2 link-fatal error:')) {
575
+ if (this.probingProtocols.get(uuid) !== 'V2') {
576
+ const transport = transportCache[uuid];
577
+ try {
578
+ this.emitDeviceDisconnect(uuid, (_c = transport === null || transport === void 0 ? void 0 : transport.device) === null || _c === void 0 ? void 0 : _c.name, transport === null || transport === void 0 ? void 0 : transport.monitorToken);
579
+ }
580
+ catch (_d) {
581
+ Log === null || Log === void 0 ? void 0 : Log.error('[ReactNativeBleTransport] Protocol V2 disconnect listener failed');
582
+ }
583
+ }
537
584
  yield this.releaseNative(uuid, true);
538
585
  }
539
586
  }),
@@ -921,38 +968,12 @@ class ReactNativeBleTransport {
921
968
  else if (reactNative.Platform.OS === 'android') {
922
969
  yield delay(ANDROID_NOTIFY_READY_DELAY_MS);
923
970
  }
924
- if (this.stopped)
925
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
926
- const initialMtu = transport$1.mtuSize;
927
- let refreshAttempts = 0;
928
- if ((reactNative.Platform.OS === 'ios' || reactNative.Platform.OS === 'android') &&
929
- shouldRefreshNegotiatedMtu(transport$1.mtuSize)) {
930
- refreshAttempts += 1;
931
- let refreshedDevice = yield requestNegotiatedMtu(transport$1.device, 'servicesAndNotifyReady', 1);
932
- if (this.stopped)
933
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
934
- transport$1.device = refreshedDevice;
935
- transport$1.mtuSize =
936
- typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport$1.mtuSize;
937
- if (shouldRefreshNegotiatedMtu(transport$1.mtuSize)) {
938
- yield delay(BLE_MTU_REFRESH_RETRY_DELAY_MS);
939
- if (this.stopped)
940
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
941
- refreshAttempts += 1;
942
- refreshedDevice = yield requestNegotiatedMtu(transport$1.device, 'servicesAndNotifyReady', 2);
943
- transport$1.device = refreshedDevice;
944
- transport$1.mtuSize =
945
- typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport$1.mtuSize;
946
- }
947
- }
948
971
  if (this.stopped)
949
972
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
950
973
  Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] BLE MTU ready', {
951
974
  platform: reactNative.Platform.OS,
952
975
  requested: getRequestedBleMtu(),
953
- initial: initialMtu,
954
976
  actual: transport$1.mtuSize,
955
- refreshAttempts,
956
977
  });
957
978
  return transport$1;
958
979
  });
@@ -1136,7 +1157,7 @@ class ReactNativeBleTransport {
1136
1157
  }
1137
1158
  if (this.stopped)
1138
1159
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
1139
- device = yield resolveNegotiatedMtu(device);
1160
+ device = yield resolveNegotiatedMtu(device, transactionId => blePlxManager.cancelTransaction(transactionId));
1140
1161
  if (this.stopped)
1141
1162
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
1142
1163
  const acquiredDevice = device;
@@ -1195,12 +1216,7 @@ class ReactNativeBleTransport {
1195
1216
  return { uuid, protocolType };
1196
1217
  }
1197
1218
  catch (error) {
1198
- if (hdShared.isBleStaleBondHardwareError(error) || shouldRethrowProtocolProbeError(error)) {
1199
- yield this.disconnectUnlocked(uuid);
1200
- }
1201
- else {
1202
- yield this.releaseUnlocked(uuid, true);
1203
- }
1219
+ yield this.disconnectUnlocked(uuid);
1204
1220
  throw error;
1205
1221
  }
1206
1222
  finally {
@@ -2378,6 +2394,7 @@ class ReactNativeBleTransport {
2378
2394
  if (!this._messages || !this._messagesV2) {
2379
2395
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
2380
2396
  }
2397
+ const isProtocolProbe = this.probingProtocols.get(uuid) === 'V2';
2381
2398
  const callOptions = options;
2382
2399
  const highThroughputWrite = transport.isProtocolV2HighThroughputCall(name);
2383
2400
  if (highThroughputWrite) {
@@ -2418,6 +2435,14 @@ class ReactNativeBleTransport {
2418
2435
  }
2419
2436
  catch (e) {
2420
2437
  Log === null || Log === void 0 ? void 0 : Log.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
2438
+ if (!isProtocolProbe &&
2439
+ (e === null || e === void 0 ? void 0 : e.errorCode) === hdShared.HardwareErrorCode.BleTimeoutError &&
2440
+ !this.monitorTokens.has(uuid)) {
2441
+ yield this.runLifecycleOperation(uuid, () => __awaiter(this, void 0, void 0, function* () {
2442
+ if (!this.monitorTokens.has(uuid))
2443
+ yield this.disconnectUnlocked(uuid);
2444
+ }));
2445
+ }
2421
2446
  throw e;
2422
2447
  }
2423
2448
  finally {
@@ -2432,7 +2457,7 @@ class ReactNativeBleTransport {
2432
2457
  const transport = this.getCachedTransport(uuid);
2433
2458
  if (!shouldRefreshNegotiatedMtu(transport.mtuSize))
2434
2459
  return;
2435
- const refreshedDevice = yield requestNegotiatedMtu(transport.device, 'highThroughput', 1);
2460
+ const refreshedDevice = yield requestNegotiatedMtu(transport.device, 'highThroughput', 1, transactionId => { var _a; return (_a = this.blePlxManager) === null || _a === void 0 ? void 0 : _a.cancelTransaction(transactionId); });
2436
2461
  transport.device = refreshedDevice;
2437
2462
  transport.mtuSize =
2438
2463
  typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-react-native",
3
- "version": "1.2.2-alpha.110",
3
+ "version": "1.2.2-alpha.112",
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.2-alpha.110",
24
- "@onekeyfe/hd-shared": "1.2.2-alpha.110",
25
- "@onekeyfe/hd-transport": "1.2.2-alpha.110",
23
+ "@onekeyfe/hd-core": "1.2.2-alpha.112",
24
+ "@onekeyfe/hd-shared": "1.2.2-alpha.112",
25
+ "@onekeyfe/hd-transport": "1.2.2-alpha.112",
26
26
  "@onekeyfe/react-native-ble-utils": "0.1.6",
27
27
  "react-native-ble-plx": "3.5.1"
28
28
  },
29
- "gitHead": "80a959fb1e9383ce948b911fddece1265b40051f"
29
+ "gitHead": "3b31cf365ea4042a5fb2e78dd7ede9a2d9d96e04"
30
30
  }
@@ -446,7 +446,7 @@ describe('BLE connect timeout', () => {
446
446
  const nativeOperation = createDeferred<typeof device>();
447
447
  const nativeDisconnect = createDeferred<void>();
448
448
  const requestMtu = jest.fn(() => Promise.resolve(device));
449
- Object.assign(device, { mtu: 247, requestMTU: requestMtu });
449
+ Object.assign(device, { mtu: stage === 'mtu' ? 23 : 247, requestMTU: requestMtu });
450
450
  device.isConnected.mockResolvedValue(true);
451
451
  if (stage === 'connect') device.isConnected.mockResolvedValueOnce(false);
452
452
  if (stage === 'mtu') requestMtu.mockImplementationOnce(() => nativeOperation.promise);
@@ -615,7 +615,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
615
615
  uuid,
616
616
  protocolType: 'V2',
617
617
  });
618
- expect(device.requestMTU).toHaveBeenCalledWith(247);
618
+ expect(device.requestMTU).not.toHaveBeenCalled();
619
619
  expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
620
620
  expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalled();
621
621
 
@@ -714,7 +714,72 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
714
714
  });
715
715
 
716
716
  test.each(['ios', 'android'] as const)(
717
- 'keeps a first expected Protocol V2 probe miss retryable on %s',
717
+ 'waits for native disconnect after failed protocol detection before reconnecting on %s',
718
+ async platform => {
719
+ setPlatformOS(platform);
720
+ const { transport, uuid, device, bleManager } = createHarness();
721
+ const probes = transport as unknown as {
722
+ probeProtocolV1(uuid: string): Promise<boolean>;
723
+ probeProtocolV2(uuid: string): Promise<boolean>;
724
+ releaseNative(uuid: string, onclose: boolean): Promise<void>;
725
+ };
726
+ jest.spyOn(probes, 'probeProtocolV1').mockResolvedValueOnce(false);
727
+ const probesFinished = createDeferred<void>();
728
+ const probeProtocolV2 = jest
729
+ .spyOn(probes, 'probeProtocolV2')
730
+ .mockImplementationOnce(async () => {
731
+ // A link-fatal V2 timeout removes the cached transport before acquire fails.
732
+ await probes.releaseNative(uuid, true);
733
+ probesFinished.resolve();
734
+ return false;
735
+ });
736
+ let connected = true;
737
+ device.isConnected.mockImplementation(() => Promise.resolve(connected));
738
+ device.connect = jest.fn(() => {
739
+ connected = true;
740
+ return Promise.resolve(device);
741
+ });
742
+ const disconnectGate = createDeferred<void>();
743
+ bleManager.cancelDeviceConnection.mockImplementation(async () => {
744
+ await disconnectGate.promise;
745
+ connected = false;
746
+ });
747
+
748
+ const acquiring = transport.acquire({ uuid });
749
+ const failure = expect(acquiring).rejects.toMatchObject({
750
+ errorCode: HardwareErrorCode.BleTimeoutError,
751
+ });
752
+ const reconnecting = transport.acquire({ uuid, expectedProtocol: 'V2' });
753
+
754
+ try {
755
+ await probesFinished.promise;
756
+ await new Promise(resolve => {
757
+ setImmediate(resolve);
758
+ });
759
+ expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
760
+ expect(device.connect).not.toHaveBeenCalled();
761
+ expect(probeProtocolV2).toHaveBeenCalledTimes(1);
762
+
763
+ disconnectGate.resolve();
764
+ await failure;
765
+ await expect(reconnecting).resolves.toEqual({ uuid, protocolType: 'V2' });
766
+ expect(device.connect).toHaveBeenCalledTimes(1);
767
+ await expect(
768
+ transport.call(uuid, 'Ping', { message: 'after-reconnect' })
769
+ ).resolves.toMatchObject({
770
+ type: 'Success',
771
+ message: { message: 'ok' },
772
+ });
773
+ } finally {
774
+ disconnectGate.resolve();
775
+ await Promise.allSettled([failure, reconnecting]);
776
+ await transport.release(uuid, true);
777
+ }
778
+ }
779
+ );
780
+
781
+ test.each(['ios', 'android'] as const)(
782
+ 'disconnects after a first expected Protocol V2 probe miss while keeping it retryable on %s',
718
783
  async platform => {
719
784
  setPlatformOS(platform);
720
785
  const { transport, uuid, device } = createHarness();
@@ -724,13 +789,13 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
724
789
  errorCode: HardwareErrorCode.RuntimeError,
725
790
  });
726
791
 
727
- expect(device.cancelConnection).not.toHaveBeenCalled();
792
+ expect(device.cancelConnection).toHaveBeenCalledTimes(1);
728
793
  expect(transport.getProtocolType(uuid)).toBeUndefined();
729
794
  }
730
795
  );
731
796
 
732
797
  test.each(['ios', 'android'] as const)(
733
- 'keeps a second expected Protocol V2 probe miss retryable on %s',
798
+ 'disconnects after each expected Protocol V2 probe miss while keeping it retryable on %s',
734
799
  async platform => {
735
800
  setPlatformOS(platform);
736
801
  const { transport, uuid, device } = createHarness();
@@ -743,7 +808,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
743
808
  errorCode: HardwareErrorCode.RuntimeError,
744
809
  });
745
810
 
746
- expect(device.cancelConnection).not.toHaveBeenCalled();
811
+ expect(device.cancelConnection).toHaveBeenCalledTimes(2);
747
812
  expect(transport.getProtocolType(uuid)).toBeUndefined();
748
813
  }
749
814
  );
@@ -778,7 +843,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
778
843
  );
779
844
 
780
845
  test.each(['ios', 'android'] as const)(
781
- 'keeps a confirmed Protocol V2 probe miss retryable on %s without native bond evidence',
846
+ 'disconnects after a confirmed Protocol V2 probe miss on %s without reporting a bond error',
782
847
  async platform => {
783
848
  setPlatformOS(platform);
784
849
  const { transport, uuid, device } = createHarness();
@@ -790,7 +855,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
790
855
  errorCode: HardwareErrorCode.RuntimeError,
791
856
  });
792
857
 
793
- expect(device.cancelConnection).not.toHaveBeenCalled();
858
+ expect(device.cancelConnection).toHaveBeenCalledTimes(1);
794
859
  expect(transport.getProtocolType(uuid)).toBeUndefined();
795
860
  }
796
861
  );
@@ -974,7 +1039,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
974
1039
  test.each(['V1', 'V2'] as const)(
975
1040
  'still falls back after a silent %s probe timeout',
976
1041
  async protocol => {
977
- const { transport, uuid } = createHarness();
1042
+ const { transport, uuid, device, bleManager } = createHarness();
978
1043
  jest
979
1044
  .spyOn(transport as any, protocol === 'V1' ? 'callProtocolV1' : 'callProtocolV2')
980
1045
  .mockRejectedValue(ERRORS.TypedError(HardwareErrorCode.BleTimeoutError));
@@ -988,6 +1053,8 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
988
1053
  });
989
1054
 
990
1055
  expect(otherProbe).toHaveBeenCalledTimes(1);
1056
+ expect(bleManager.cancelDeviceConnection).not.toHaveBeenCalled();
1057
+ expect(device.cancelConnection).not.toHaveBeenCalled();
991
1058
  await transport.release(uuid, true);
992
1059
  }
993
1060
  );
@@ -1012,57 +1079,69 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
1012
1079
  await transport.release(uuid, true);
1013
1080
  });
1014
1081
 
1015
- test('continues with the current MTU when the connected snapshot refresh fails', async () => {
1082
+ test('does not renegotiate an already usable MTU during acquire', async () => {
1016
1083
  const { transport, uuid, device } = createHarness();
1017
- const mtuError = new Error('MTU refresh failed');
1018
- device.requestMTU.mockRejectedValueOnce(mtuError);
1019
1084
 
1020
1085
  await expect(transport.acquire({ uuid })).resolves.toEqual({
1021
1086
  uuid,
1022
1087
  protocolType: 'V2',
1023
1088
  });
1024
- expect(device.requestMTU).toHaveBeenCalledTimes(1);
1089
+ expect(device.requestMTU).not.toHaveBeenCalled();
1025
1090
  expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(247);
1026
1091
  await transport.release(uuid, true);
1027
1092
  });
1028
1093
 
1029
- test('refreshes a transient bootloader MTU after notifications are ready', async () => {
1094
+ test('requests a low MTU once before protocol probing', async () => {
1030
1095
  const { transport, uuid, device } = createHarness();
1031
1096
  device.mtu = 23;
1032
- device.requestMTU
1033
- .mockResolvedValueOnce(device)
1034
- .mockResolvedValueOnce(device)
1035
- .mockImplementationOnce(() => {
1036
- device.mtu = 247;
1037
- return Promise.resolve(device);
1038
- });
1097
+ device.requestMTU.mockImplementationOnce(() => {
1098
+ device.mtu = 247;
1099
+ return Promise.resolve(device);
1100
+ });
1039
1101
 
1040
1102
  await expect(transport.acquire({ uuid })).resolves.toEqual({
1041
1103
  uuid,
1042
1104
  protocolType: 'V2',
1043
1105
  });
1044
- expect(device.requestMTU).toHaveBeenCalledTimes(3);
1106
+ expect(device.requestMTU).toHaveBeenCalledTimes(1);
1107
+ expect(device.requestMTU).toHaveBeenCalledWith(
1108
+ 247,
1109
+ expect.stringContaining(`${uuid}:mtu:connected:0:`)
1110
+ );
1045
1111
  expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(247);
1046
1112
  await transport.release(uuid, true);
1047
1113
  });
1048
1114
 
1049
- test('continues with a low bootloader MTU when the bounded retry fails', async () => {
1115
+ test('continues with a low MTU when the single negotiation fails', async () => {
1050
1116
  const { transport, uuid, device } = createHarness();
1051
1117
  device.mtu = 23;
1052
- device.requestMTU
1053
- .mockResolvedValueOnce(device)
1054
- .mockResolvedValueOnce(device)
1055
- .mockRejectedValueOnce(new Error('bootloader MTU retry failed'));
1118
+ device.requestMTU.mockRejectedValueOnce(new Error('MTU negotiation failed'));
1056
1119
 
1057
1120
  await expect(transport.acquire({ uuid })).resolves.toEqual({
1058
1121
  uuid,
1059
1122
  protocolType: 'V2',
1060
1123
  });
1061
- expect(device.requestMTU).toHaveBeenCalledTimes(3);
1124
+ expect(device.requestMTU).toHaveBeenCalledTimes(1);
1062
1125
  expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
1063
1126
  await transport.release(uuid, true);
1064
1127
  });
1065
1128
 
1129
+ test('bounds a stalled MTU negotiation and continues protocol probing', async () => {
1130
+ const { transport, uuid, device, bleManager } = createHarness();
1131
+ device.mtu = 23;
1132
+ device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
1133
+
1134
+ await expect(transport.acquire({ uuid })).resolves.toEqual({
1135
+ uuid,
1136
+ protocolType: 'V2',
1137
+ });
1138
+ const transactionId = device.requestMTU.mock.calls[0]?.[1];
1139
+ expect(transactionId).toEqual(expect.stringContaining(`${uuid}:mtu:connected:0:`));
1140
+ expect(bleManager.cancelTransaction).toHaveBeenCalledWith(transactionId);
1141
+ expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
1142
+ await transport.release(uuid, true);
1143
+ }, 10_000);
1144
+
1066
1145
  test('accepts a stable low MTU without the delayed refresh loop', async () => {
1067
1146
  const { transport, uuid, device } = createHarness();
1068
1147
  device.mtu = 185;
@@ -1071,7 +1150,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
1071
1150
  uuid,
1072
1151
  protocolType: 'V2',
1073
1152
  });
1074
- expect(device.requestMTU).toHaveBeenCalledTimes(1);
1153
+ expect(device.requestMTU).not.toHaveBeenCalled();
1075
1154
  expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(185);
1076
1155
  await transport.release(uuid, true);
1077
1156
  });
@@ -1084,7 +1163,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
1084
1163
  uuid,
1085
1164
  protocolType: 'V2',
1086
1165
  });
1087
- expect(device.requestMTU).toHaveBeenCalledTimes(3);
1166
+ expect(device.requestMTU).toHaveBeenCalledTimes(1);
1088
1167
  expect((transport as any).getCachedTransport(uuid).mtuSize).toBeUndefined();
1089
1168
  expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalled();
1090
1169
  await transport.release(uuid, true);
@@ -1102,7 +1181,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
1102
1181
  });
1103
1182
 
1104
1183
  await expect(transport.call(uuid, 'FileWrite', {})).resolves.toBeDefined();
1105
- expect(device.requestMTU).toHaveBeenCalledTimes(4);
1184
+ expect(device.requestMTU).toHaveBeenCalledTimes(2);
1106
1185
  expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(
1107
1186
  writesBeforeFileWrite + 1
1108
1187
  );
@@ -1119,7 +1198,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
1119
1198
  await expect(transport.call(uuid, 'FileWrite', {})).rejects.toMatchObject({
1120
1199
  errorCode: HardwareErrorCode.BleConnectedError,
1121
1200
  });
1122
- expect(device.requestMTU).toHaveBeenCalledTimes(4);
1201
+ expect(device.requestMTU).toHaveBeenCalledTimes(2);
1123
1202
  expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(writesBeforeFileWrite);
1124
1203
  await transport.release(uuid, true);
1125
1204
  });
@@ -1173,6 +1252,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
1173
1252
  expect(notifySubscriptionRemovers[0]).toHaveBeenCalledTimes(1);
1174
1253
  expect(notifySubscriptionRemovers[1]).toHaveBeenCalledTimes(1);
1175
1254
  expect(bleManager.cancelTransaction).toHaveBeenCalled();
1255
+ expect(device.cancelConnection).toHaveBeenCalledTimes(1);
1176
1256
  expect(transport.getProtocolType(uuid)).toBeUndefined();
1177
1257
  });
1178
1258
 
@@ -1227,6 +1307,115 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
1227
1307
  });
1228
1308
  });
1229
1309
 
1310
+ test.each(['ios', 'android'] as const)(
1311
+ 'disconnects after an active V2 timeout and reconnects even if a listener fails on %s',
1312
+ async platform => {
1313
+ setPlatformOS(platform);
1314
+ const harness = createHarness();
1315
+ const { transport, uuid, device, bleManager, sentSeqs, emitter } = harness;
1316
+ const disconnected = jest.fn(() => {
1317
+ throw new Error('Disconnect listener failed');
1318
+ });
1319
+ emitter.on(TRANSPORT_EVENT.DEVICE_DISCONNECT, disconnected);
1320
+ let connected = true;
1321
+ device.isConnected.mockImplementation(() => Promise.resolve(connected));
1322
+ device.connect = jest.fn(() => {
1323
+ connected = true;
1324
+ return Promise.resolve(device);
1325
+ });
1326
+ bleManager.cancelDeviceConnection.mockImplementation(() => {
1327
+ connected = false;
1328
+ return Promise.resolve();
1329
+ });
1330
+
1331
+ await transport.acquire({ uuid, expectedProtocol: 'V2' });
1332
+ harness.setShouldRespond(false);
1333
+ await expect(transport.call(uuid, 'Ping', {}, { timeoutMs: 10 })).rejects.toMatchObject({
1334
+ errorCode: HardwareErrorCode.BleTimeoutError,
1335
+ });
1336
+
1337
+ expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
1338
+ expect(connected).toBe(false);
1339
+ expect(transport.getProtocolType(uuid)).toBeUndefined();
1340
+ expect(disconnected).toHaveBeenCalledTimes(1);
1341
+ expect(disconnected).toHaveBeenCalledWith({
1342
+ id: uuid,
1343
+ connectId: uuid,
1344
+ name: device.name,
1345
+ });
1346
+
1347
+ harness.setShouldRespond(true);
1348
+ await transport.acquire({ uuid, expectedProtocol: 'V2' });
1349
+ await expect(transport.call(uuid, 'Ping', {})).resolves.toMatchObject({ type: 'Success' });
1350
+ expect(device.connect).toHaveBeenCalledTimes(1);
1351
+ expect(sentSeqs).toEqual([1, 2, 3, 4]);
1352
+ await transport.release(uuid, true);
1353
+ }
1354
+ );
1355
+
1356
+ test('does not disconnect an active Protocol V2 call when a queued call times out', async () => {
1357
+ const harness = createHarness();
1358
+ const { transport, uuid, bleManager } = harness;
1359
+ await transport.acquire({ uuid, expectedProtocol: 'V2' });
1360
+ harness.setShouldRespond(false);
1361
+ const active = transport.call(uuid, 'Ping', {}, { timeoutMs: 1000 }).catch(error => error);
1362
+ await new Promise(resolve => {
1363
+ setImmediate(resolve);
1364
+ });
1365
+
1366
+ try {
1367
+ await expect(transport.call(uuid, 'Ping', {}, { timeoutMs: 10 })).rejects.toMatchObject({
1368
+ errorCode: HardwareErrorCode.BleTimeoutError,
1369
+ });
1370
+ expect(bleManager.cancelDeviceConnection).not.toHaveBeenCalled();
1371
+ expect(transport.getProtocolType(uuid)).toBe('V2');
1372
+ } finally {
1373
+ await transport.disconnect(uuid);
1374
+ await active;
1375
+ }
1376
+ });
1377
+
1378
+ test('does not disconnect a newer acquire when V2 timeout cleanup waits for the lifecycle lock', async () => {
1379
+ const harness = createHarness();
1380
+ const { transport, uuid, bleManager } = harness;
1381
+ await transport.acquire({ uuid, expectedProtocol: 'V2' });
1382
+ const lifecycle = transport as unknown as {
1383
+ runLifecycleOperation(id: string, operation: () => Promise<void>): Promise<void>;
1384
+ releaseNative(id: string, onclose: boolean): Promise<boolean>;
1385
+ };
1386
+ const gate = createDeferred<void>();
1387
+ const holding = lifecycle.runLifecycleOperation(uuid, () => gate.promise);
1388
+ const reacquiring = transport.acquire({ uuid, expectedProtocol: 'V2' });
1389
+ const invalidated = createDeferred<void>();
1390
+ const releaseNative = lifecycle.releaseNative.bind(lifecycle);
1391
+ jest.spyOn(lifecycle, 'releaseNative').mockImplementationOnce(async (id, onclose) => {
1392
+ const result = await releaseNative(id, onclose);
1393
+ invalidated.resolve();
1394
+ return result;
1395
+ });
1396
+ harness.setShouldRespond(false);
1397
+ const failure = expect(
1398
+ transport.call(uuid, 'Ping', {}, { timeoutMs: 10 })
1399
+ ).rejects.toMatchObject({
1400
+ errorCode: HardwareErrorCode.BleTimeoutError,
1401
+ });
1402
+
1403
+ try {
1404
+ await invalidated.promise;
1405
+ harness.setShouldRespond(true);
1406
+ gate.resolve();
1407
+ await holding;
1408
+ await reacquiring;
1409
+ await failure;
1410
+ expect(bleManager.cancelDeviceConnection).not.toHaveBeenCalled();
1411
+ await expect(transport.call(uuid, 'Ping', {})).resolves.toMatchObject({ type: 'Success' });
1412
+ } finally {
1413
+ gate.resolve();
1414
+ await Promise.allSettled([holding, reacquiring, failure]);
1415
+ await transport.release(uuid, true);
1416
+ }
1417
+ });
1418
+
1230
1419
  test('retains the sequence cursor when a new monitor generation is acquired', async () => {
1231
1420
  const { transport, uuid, sentSeqs } = createHarness();
1232
1421
 
@@ -1279,9 +1468,17 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
1279
1468
  test('uses Android 517 MTU and high connection priority during Protocol V2 high-volume calls', async () => {
1280
1469
  setPlatformOS('android');
1281
1470
  const { transport, uuid, device } = createHarness();
1471
+ device.mtu = 23;
1472
+ device.requestMTU.mockImplementationOnce(() => {
1473
+ device.mtu = 517;
1474
+ return Promise.resolve(device);
1475
+ });
1282
1476
 
1283
1477
  await transport.acquire({ uuid, expectedProtocol: 'V2' });
1284
- expect(device.requestMTU).toHaveBeenCalledWith(517);
1478
+ expect(device.requestMTU).toHaveBeenCalledWith(
1479
+ 517,
1480
+ expect.stringContaining(`${uuid}:mtu:connected:0:`)
1481
+ );
1285
1482
 
1286
1483
  await transport.call(uuid, 'FileWrite', {});
1287
1484
  await transport.call(uuid, 'FileWrite', {});
package/src/index.ts CHANGED
@@ -242,13 +242,13 @@ function getDeviceDisplayName(device?: Device | null) {
242
242
 
243
243
  const IOS_REQUEST_MTU = 247;
244
244
  const ANDROID_REQUEST_MTU = 517;
245
- const BLE_MTU_REFRESH_RETRY_DELAY_MS = 200;
246
245
  const ANDROID_HIGH_PRIORITY_IDLE_MS = 1000;
247
246
 
248
247
  const getRequestedBleMtu = () =>
249
248
  Platform.OS === 'android' ? ANDROID_REQUEST_MTU : IOS_REQUEST_MTU;
250
249
 
251
250
  const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
251
+ const BLE_MTU_REQUEST_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS;
252
252
 
253
253
  const connectOptions: Record<string, unknown> = {
254
254
  requestMTU: getRequestedBleMtu(),
@@ -314,17 +314,50 @@ const tryToGetConfiguration = (device: Device) => {
314
314
 
315
315
  const requestNegotiatedMtu = async (
316
316
  device: Device,
317
- stage: 'connected' | 'servicesAndNotifyReady' | 'highThroughput',
318
- attempt: number
317
+ stage: 'connected' | 'highThroughput',
318
+ attempt: number,
319
+ cancelTransaction?: (transactionId: string) => Promise<void> | void
319
320
  ) => {
320
321
  if (Platform.OS !== 'ios' && Platform.OS !== 'android') return device;
321
322
 
323
+ const transactionId = `${device.id}:mtu:${stage}:${attempt}:${Date.now()}`;
324
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
325
+ let timedOut = false;
326
+
322
327
  try {
323
328
  // iOS ignores the requested value but react-native-ble-plx returns a fresh
324
329
  // Device snapshot whose MTU is derived from CoreBluetooth's maximum write length.
325
- const mtuDevice = await device.requestMTU(getRequestedBleMtu());
330
+ const request = device.requestMTU(getRequestedBleMtu(), transactionId);
331
+ const mtuDevice = await Promise.race([
332
+ request,
333
+ new Promise<never>((_, reject) => {
334
+ timeoutId = setTimeout(() => {
335
+ timedOut = true;
336
+ reject(new Error(`BLE MTU request timeout after ${BLE_MTU_REQUEST_TIMEOUT_MS}ms`));
337
+ }, BLE_MTU_REQUEST_TIMEOUT_MS);
338
+ }),
339
+ ]);
326
340
  return mtuDevice;
327
341
  } catch (error) {
342
+ if (timedOut && cancelTransaction) {
343
+ try {
344
+ Promise.resolve(cancelTransaction(transactionId)).catch(cancelError => {
345
+ Log?.debug('[ReactNativeBleTransport] MTU cancellation failed', {
346
+ platform: Platform.OS,
347
+ stage,
348
+ attempt,
349
+ error: cancelError instanceof Error ? cancelError.message : String(cancelError),
350
+ });
351
+ });
352
+ } catch (cancelError) {
353
+ Log?.debug('[ReactNativeBleTransport] MTU cancellation failed', {
354
+ platform: Platform.OS,
355
+ stage,
356
+ attempt,
357
+ error: cancelError instanceof Error ? cancelError.message : String(cancelError),
358
+ });
359
+ }
360
+ }
328
361
  Log?.debug('[ReactNativeBleTransport] MTU refresh failed, continuing with current value', {
329
362
  platform: Platform.OS,
330
363
  stage,
@@ -333,10 +366,18 @@ const requestNegotiatedMtu = async (
333
366
  error: error instanceof Error ? error.message : String(error),
334
367
  });
335
368
  return device;
369
+ } finally {
370
+ if (timeoutId) clearTimeout(timeoutId);
336
371
  }
337
372
  };
338
373
 
339
- const resolveNegotiatedMtu = (device: Device) => requestNegotiatedMtu(device, 'connected', 0);
374
+ const resolveNegotiatedMtu = (
375
+ device: Device,
376
+ cancelTransaction?: (transactionId: string) => Promise<void> | void
377
+ ) =>
378
+ shouldRefreshNegotiatedMtu(device.mtu)
379
+ ? requestNegotiatedMtu(device, 'connected', 0, cancelTransaction)
380
+ : Promise.resolve(device);
340
381
 
341
382
  type IOBleErrorRemap = Error | BleError | null | undefined;
342
383
 
@@ -452,6 +493,14 @@ export default class ReactNativeBleTransport {
452
493
  this.rejectProtocolV2Frames(uuid, new Error(reason));
453
494
  Log?.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason);
454
495
  if (reason.startsWith('Protocol V2 link-fatal error:')) {
496
+ if (this.probingProtocols.get(uuid) !== 'V2') {
497
+ const transport = transportCache[uuid];
498
+ try {
499
+ this.emitDeviceDisconnect(uuid, transport?.device?.name, transport?.monitorToken);
500
+ } catch {
501
+ Log?.error('[ReactNativeBleTransport] Protocol V2 disconnect listener failed');
502
+ }
503
+ }
455
504
  await this.releaseNative(uuid, true);
456
505
  }
457
506
  },
@@ -918,41 +967,10 @@ export default class ReactNativeBleTransport {
918
967
  }
919
968
  if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
920
969
 
921
- const initialMtu = transport.mtuSize;
922
- let refreshAttempts = 0;
923
- if (
924
- (Platform.OS === 'ios' || Platform.OS === 'android') &&
925
- shouldRefreshNegotiatedMtu(transport.mtuSize)
926
- ) {
927
- refreshAttempts += 1;
928
- let refreshedDevice = await requestNegotiatedMtu(
929
- transport.device,
930
- 'servicesAndNotifyReady',
931
- 1
932
- );
933
- if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
934
- transport.device = refreshedDevice;
935
- transport.mtuSize =
936
- typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
937
-
938
- if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
939
- await delay(BLE_MTU_REFRESH_RETRY_DELAY_MS);
940
- if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
941
- refreshAttempts += 1;
942
- refreshedDevice = await requestNegotiatedMtu(transport.device, 'servicesAndNotifyReady', 2);
943
- transport.device = refreshedDevice;
944
- transport.mtuSize =
945
- typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
946
- }
947
- }
948
-
949
- if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
950
970
  Log?.debug('[ReactNativeBleTransport] BLE MTU ready', {
951
971
  platform: Platform.OS,
952
972
  requested: getRequestedBleMtu(),
953
- initial: initialMtu,
954
973
  actual: transport.mtuSize,
955
- refreshAttempts,
956
974
  });
957
975
 
958
976
  return transport;
@@ -1180,7 +1198,9 @@ export default class ReactNativeBleTransport {
1180
1198
  throw ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'device is not connected');
1181
1199
  }
1182
1200
  if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1183
- device = await resolveNegotiatedMtu(device);
1201
+ device = await resolveNegotiatedMtu(device, transactionId =>
1202
+ blePlxManager.cancelTransaction(transactionId)
1203
+ );
1184
1204
  if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1185
1205
  const acquiredDevice = device;
1186
1206
  const { writeCharacteristic, notifyCharacteristic } =
@@ -1255,11 +1275,9 @@ export default class ReactNativeBleTransport {
1255
1275
  this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
1256
1276
  return { uuid, protocolType };
1257
1277
  } catch (error) {
1258
- if (isBleStaleBondHardwareError(error) || shouldRethrowProtocolProbeError(error)) {
1259
- await this.disconnectUnlocked(uuid);
1260
- } else {
1261
- await this.releaseUnlocked(uuid, true);
1262
- }
1278
+ // A failed acquire must retire the physical link before Core retries. Logical
1279
+ // release leaves GATT connected even when neither protocol receives a response.
1280
+ await this.disconnectUnlocked(uuid);
1263
1281
  throw error;
1264
1282
  } finally {
1265
1283
  this.acquiringProtocolV2.delete(uuid);
@@ -2738,6 +2756,7 @@ export default class ReactNativeBleTransport {
2738
2756
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
2739
2757
  }
2740
2758
 
2759
+ const isProtocolProbe = this.probingProtocols.get(uuid) === 'V2';
2741
2760
  const callOptions = options;
2742
2761
  const highThroughputWrite = isProtocolV2HighThroughputCall(name);
2743
2762
 
@@ -2789,6 +2808,19 @@ export default class ReactNativeBleTransport {
2789
2808
  );
2790
2809
  } catch (e) {
2791
2810
  Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
2811
+ if (
2812
+ !isProtocolProbe &&
2813
+ e?.errorCode === HardwareErrorCode.BleTimeoutError &&
2814
+ !this.monitorTokens.has(uuid)
2815
+ ) {
2816
+ // The failed link has finished invalidating. Disconnect outside that
2817
+ // callback to avoid waiting on its own invalidation or acquire lock.
2818
+ await this.runLifecycleOperation(uuid, async () => {
2819
+ // A queued timeout leaves its active monitor intact; a newer acquire
2820
+ // may also have installed one while cleanup waited for the lifecycle lock.
2821
+ if (!this.monitorTokens.has(uuid)) await this.disconnectUnlocked(uuid);
2822
+ });
2823
+ }
2792
2824
  throw e;
2793
2825
  } finally {
2794
2826
  if (highThroughputWrite) {
@@ -2801,7 +2833,12 @@ export default class ReactNativeBleTransport {
2801
2833
  const transport = this.getCachedTransport(uuid);
2802
2834
  if (!shouldRefreshNegotiatedMtu(transport.mtuSize)) return;
2803
2835
 
2804
- const refreshedDevice = await requestNegotiatedMtu(transport.device, 'highThroughput', 1);
2836
+ const refreshedDevice = await requestNegotiatedMtu(
2837
+ transport.device,
2838
+ 'highThroughput',
2839
+ 1,
2840
+ transactionId => this.blePlxManager?.cancelTransaction(transactionId)
2841
+ );
2805
2842
  transport.device = refreshedDevice;
2806
2843
  transport.mtuSize =
2807
2844
  typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;