@onekeyfe/hd-transport-react-native 1.2.0-alpha.69 → 1.2.0-alpha.70

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.
@@ -32,7 +32,6 @@ jest.mock('react-native-ble-plx', () => ({
32
32
  CharacteristicNotFound: 404,
33
33
  },
34
34
  BleManager: jest.fn(),
35
- ConnectionPriority: { Balanced: 0, High: 1, LowPower: 2 },
36
35
  ScanMode: { LowLatency: 2 },
37
36
  }));
38
37
 
@@ -158,7 +157,6 @@ const createHarness = ({
158
157
  id: uuid,
159
158
  name: deviceName,
160
159
  localName: deviceName,
161
- mtu: 247,
162
160
  serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
163
161
  isConnected: jest.fn(() => Promise.resolve(true)),
164
162
  cancelConnection: jest.fn(() => Promise.resolve()),
@@ -166,9 +164,7 @@ const createHarness = ({
166
164
  disconnectCallback = callback;
167
165
  return { remove: jest.fn() };
168
166
  }),
169
- } as any;
170
- device.requestMTU = jest.fn(() => Promise.resolve(device));
171
- device.requestConnectionPriority = jest.fn(() => Promise.resolve(device));
167
+ };
172
168
  const bleManager = {
173
169
  devices: jest.fn(() => Promise.resolve([device])),
174
170
  connectedDevices: jest.fn(() => Promise.resolve([])),
@@ -176,21 +172,18 @@ const createHarness = ({
176
172
  };
177
173
  const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
178
174
  const emitter = new EventEmitter();
179
- const logger = { debug: jest.fn(), error: jest.fn() };
180
175
  transport.blePlxManager = bleManager;
181
176
  transport.resolveCharacteristics = jest.fn(() =>
182
177
  Promise.resolve({ writeCharacteristic, notifyCharacteristic })
183
178
  );
184
- transport.init(logger, emitter);
179
+ transport.init({ debug: jest.fn(), error: jest.fn() }, emitter);
185
180
  transport.configure(protocolV1Schema);
186
181
  transport.configureProtocolV2(protocolV2Schema);
187
182
 
188
183
  return {
189
184
  transport,
190
185
  emitter,
191
- logger,
192
186
  uuid,
193
- device,
194
187
  sentSeqs,
195
188
  writeCharacteristic,
196
189
  setShouldRespond(value: boolean) {
@@ -254,7 +247,6 @@ const createV1Harness = ({
254
247
  id: uuid,
255
248
  name: 'OneKey Classic',
256
249
  localName: 'OneKey Classic',
257
- mtu: 247,
258
250
  serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
259
251
  isConnected: jest.fn(() => Promise.resolve(true)),
260
252
  cancelConnection: jest.fn(() => Promise.resolve()),
@@ -263,8 +255,7 @@ const createV1Harness = ({
263
255
  disconnectSubscriptionRemovers.push(remove);
264
256
  return { remove };
265
257
  }),
266
- } as any;
267
- device.requestMTU = jest.fn(() => Promise.resolve(device));
258
+ };
268
259
  const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
269
260
  const bleManager = {
270
261
  devices: jest.fn(() => Promise.resolve([device])),
@@ -401,7 +392,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
401
392
  });
402
393
 
403
394
  test('actively probes Protocol V2 on iOS when only a name-derived hint is available', async () => {
404
- const { transport, uuid, device, sentSeqs, writeCharacteristic } = createHarness({
395
+ const { transport, uuid, sentSeqs, writeCharacteristic } = createHarness({
405
396
  deviceName: 'Pro2 6E9E',
406
397
  });
407
398
 
@@ -409,7 +400,6 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
409
400
  uuid,
410
401
  protocolType: 'V2',
411
402
  });
412
- expect(device.requestMTU).toHaveBeenCalledWith(247);
413
403
  expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
414
404
 
415
405
  await expect(
@@ -439,57 +429,6 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
439
429
  await transport.release(uuid, true);
440
430
  });
441
431
 
442
- test('continues with the current MTU when the connected snapshot refresh fails', async () => {
443
- const { transport, uuid, device } = createHarness();
444
- const mtuError = new Error('MTU refresh failed');
445
- device.requestMTU.mockRejectedValueOnce(mtuError);
446
-
447
- await expect(transport.acquire({ uuid })).resolves.toEqual({
448
- uuid,
449
- protocolType: 'V2',
450
- });
451
- expect(device.requestMTU).toHaveBeenCalledTimes(1);
452
- expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(247);
453
- await transport.release(uuid, true);
454
- });
455
-
456
- test('refreshes a transient bootloader MTU after notifications are ready', async () => {
457
- const { transport, uuid, device } = createHarness();
458
- device.mtu = 23;
459
- device.requestMTU
460
- .mockResolvedValueOnce(device)
461
- .mockResolvedValueOnce(device)
462
- .mockImplementationOnce(() => {
463
- device.mtu = 247;
464
- return Promise.resolve(device);
465
- });
466
-
467
- await expect(transport.acquire({ uuid })).resolves.toEqual({
468
- uuid,
469
- protocolType: 'V2',
470
- });
471
- expect(device.requestMTU).toHaveBeenCalledTimes(3);
472
- expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(247);
473
- await transport.release(uuid, true);
474
- });
475
-
476
- test('continues with a low bootloader MTU when the bounded retry fails', async () => {
477
- const { transport, uuid, device } = createHarness();
478
- device.mtu = 23;
479
- device.requestMTU
480
- .mockResolvedValueOnce(device)
481
- .mockResolvedValueOnce(device)
482
- .mockRejectedValueOnce(new Error('bootloader MTU retry failed'));
483
-
484
- await expect(transport.acquire({ uuid })).resolves.toEqual({
485
- uuid,
486
- protocolType: 'V2',
487
- });
488
- expect(device.requestMTU).toHaveBeenCalledTimes(3);
489
- expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
490
- await transport.release(uuid, true);
491
- });
492
-
493
432
  test('reconnects before falling back to Protocol V1 after a fatal V2 probe failure', async () => {
494
433
  setPlatformOS('android');
495
434
  const { transport, uuid, device, notifySubscriptionRemovers, disconnectSubscriptionRemovers } =
@@ -625,38 +564,14 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
625
564
  });
626
565
 
627
566
  test('keeps iOS Protocol V2 high-volume calls on withoutResponse', async () => {
628
- const { transport, uuid, logger, writeCharacteristic } = createHarness();
567
+ const { transport, uuid, writeCharacteristic } = createHarness();
629
568
 
630
569
  await transport.acquire({ uuid, expectedProtocol: 'V2' });
631
570
 
632
571
  await transport.call(uuid, 'FileWrite', {});
633
- await transport.call(uuid, 'FileWrite', {});
634
- expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(2);
572
+ expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
635
573
  expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
636
- expect(
637
- logger.debug.mock.calls.filter(
638
- ([message]) =>
639
- message === '[ReactNativeBleTransport] Protocol V2 high-volume write configured'
640
- )
641
- ).toHaveLength(1);
642
- await transport.release(uuid, true);
643
- });
644
-
645
- test('uses Android 517 MTU and high connection priority during Protocol V2 high-volume calls', async () => {
646
- setPlatformOS('android');
647
- const { transport, uuid, device } = createHarness();
648
-
649
- await transport.acquire({ uuid, expectedProtocol: 'V2' });
650
- expect(device.requestMTU).toHaveBeenCalledWith(517);
651
-
652
- await transport.call(uuid, 'FileWrite', {});
653
- await transport.call(uuid, 'FileWrite', {});
654
-
655
- expect(device.requestConnectionPriority).toHaveBeenCalledTimes(1);
656
- expect(device.requestConnectionPriority).toHaveBeenCalledWith(1);
657
-
658
574
  await transport.release(uuid, true);
659
- expect(device.requestConnectionPriority).toHaveBeenLastCalledWith(0);
660
575
  });
661
576
 
662
577
  test('uses withResponse for an iOS Protocol V2 firmware file write when requested', async () => {
@@ -698,6 +613,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
698
613
 
699
614
  await expect(
700
615
  transport.writeProtocolV2Packet(
616
+ 'test-device',
701
617
  {
702
618
  writeCharacteristic: {
703
619
  isWritableWithResponse: true,
@@ -714,7 +630,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
714
630
  expect(writeWithoutResponse).not.toHaveBeenCalled();
715
631
  });
716
632
 
717
- test('does not pace a one-packet Protocol V2 control write on iOS', async () => {
633
+ test('paces a one-packet Protocol V2 control write on iOS', async () => {
718
634
  const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
719
635
  const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
720
636
  const bleTransport = {
@@ -732,9 +648,19 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
732
648
  const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
733
649
 
734
650
  try {
735
- await transport.writeProtocolV2Frame(bleTransport, new Uint8Array(10), context, jest.fn());
651
+ const call = transport.writeProtocolV2Frame(
652
+ 'test-device',
653
+ bleTransport,
654
+ new Uint8Array(10),
655
+ context,
656
+ jest.fn()
657
+ );
658
+
659
+ await Promise.resolve();
660
+ expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 5);
661
+ expect(writeWithoutResponse).not.toHaveBeenCalled();
736
662
 
737
- expect(setTimeoutSpy).not.toHaveBeenCalled();
663
+ await call;
738
664
  expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
739
665
  } finally {
740
666
  setTimeoutSpy.mockRestore();
@@ -798,6 +724,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
798
724
  configureProtocolV2BleTuning({ iosPacketLength: 20 });
799
725
 
800
726
  await transport.writeProtocolV2Frame(
727
+ 'device-uuid',
801
728
  bleTransport,
802
729
  new Uint8Array(30),
803
730
  context,
@@ -829,34 +756,14 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
829
756
  configureProtocolV2BleTuning({ iosPacketLength: 20 });
830
757
 
831
758
  await expect(
832
- transport.writeProtocolV2Frame(bleTransport, new Uint8Array(30), context, jest.fn())
759
+ transport.writeProtocolV2Frame(
760
+ 'device-uuid',
761
+ bleTransport,
762
+ new Uint8Array(30),
763
+ context,
764
+ jest.fn()
765
+ )
833
766
  ).rejects.toMatchObject({ errorCode: 205 });
834
767
  expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
835
768
  });
836
-
837
- test('does not apply fixed burst or flush pauses to high-volume Protocol V2 writes', async () => {
838
- const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
839
- const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
840
- const bleTransport = {
841
- mtuSize: 247,
842
- writeCharacteristic: { writeWithoutResponse },
843
- };
844
- const context = {
845
- messageName: 'FileWrite',
846
- timeoutMs: 1000,
847
- highVolume: true,
848
- generation: 1,
849
- signal: new AbortController().signal,
850
- };
851
- const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
852
-
853
- try {
854
- await transport.writeProtocolV2Frame(bleTransport, new Uint8Array(600), context, jest.fn());
855
-
856
- expect(writeWithoutResponse).toHaveBeenCalledTimes(3);
857
- expect(setTimeoutSpy).not.toHaveBeenCalled();
858
- } finally {
859
- setTimeoutSpy.mockRestore();
860
- }
861
- });
862
769
  });
@@ -0,0 +1,210 @@
1
+ import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+
3
+ import ReactNativeBleTransport from '../index';
4
+ import protocolV1Schema from './protocolV1SchemaFixture';
5
+
6
+ jest.mock(
7
+ 'react-native',
8
+ () => ({
9
+ Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
10
+ PermissionsAndroid: {
11
+ PERMISSIONS: {},
12
+ RESULTS: {},
13
+ request: jest.fn(),
14
+ requestMultiple: jest.fn(),
15
+ },
16
+ }),
17
+ { virtual: true }
18
+ );
19
+
20
+ jest.mock('react-native-ble-plx', () => ({
21
+ BleATTErrorCode: { InvalidHandle: 1 },
22
+ BleError: Error,
23
+ BleErrorCode: { DeviceDisconnected: 201, OperationStartFailed: 601 },
24
+ BleManager: jest.fn(),
25
+ ScanMode: { LowLatency: 2 },
26
+ }));
27
+
28
+ jest.mock('@onekeyfe/react-native-ble-utils', () => ({
29
+ __esModule: true,
30
+ default: {
31
+ getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
32
+ getBondedPeripherals: jest.fn(() => Promise.resolve([])),
33
+ pairDevice: jest.fn(() => Promise.resolve()),
34
+ },
35
+ }));
36
+
37
+ const UUID = 'stale-timeout-device';
38
+
39
+ const flush = () =>
40
+ new Promise(resolve => {
41
+ setImmediate(resolve);
42
+ });
43
+
44
+ function createHarness() {
45
+ const t = new ReactNativeBleTransport({});
46
+ t.configure(protocolV1Schema);
47
+ (t as any).deviceProtocol.set(UUID, 'V1');
48
+ const writeWithoutResponse = jest.fn(() => Promise.resolve());
49
+ const fakeBleTransport = {
50
+ writeCharacteristic: { writeWithoutResponse },
51
+ writeWithRetry: jest.fn(() => Promise.resolve()),
52
+ };
53
+ (t as any).getCachedTransport = () => fakeBleTransport;
54
+ const disconnectSpy = jest.spyOn(t, 'disconnect').mockResolvedValue(undefined);
55
+ return { t, disconnectSpy, writeWithoutResponse };
56
+ }
57
+
58
+ describe('Protocol V1 stale call timeout', () => {
59
+ beforeAll(() => {
60
+ jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
61
+ });
62
+
63
+ afterAll(() => {
64
+ jest.useRealTimers();
65
+ });
66
+
67
+ afterEach(() => {
68
+ jest.clearAllTimers();
69
+ jest.restoreAllMocks();
70
+ });
71
+
72
+ test('superseded Initialize timeout does not tear down the shared transport', async () => {
73
+ const { t, disconnectSpy } = createHarness();
74
+
75
+ // Initialize #1: written while the device reboots, never answered.
76
+ const firstErrors: unknown[] = [];
77
+ const first = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
78
+ first.catch(e => firstErrors.push(e));
79
+ await flush();
80
+
81
+ // Initialize #2 (forceRun) supersedes #1 three seconds later.
82
+ jest.advanceTimersByTime(3000);
83
+ const secondErrors: unknown[] = [];
84
+ const second = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
85
+ second.catch(e => secondErrors.push(e));
86
+ await flush();
87
+
88
+ // The device answers Initialize #2 (settle its deferred at the transport seam).
89
+ expect(t.runPromise).not.toBeNull();
90
+ t.runPromise?.reject(new Error('settled by device response'));
91
+ await flush();
92
+
93
+ // FirmwareUpload is now the active call on the same transport, awaiting its response.
94
+ const uploadErrors: unknown[] = [];
95
+ const upload = t.call(UUID, 'FirmwareUpload', { payload: Buffer.alloc(300) });
96
+ upload.catch(e => uploadErrors.push(e));
97
+ await flush();
98
+ jest.advanceTimersByTime(100); // firmware upload flush delay
99
+ await flush();
100
+
101
+ // Initialize #1's 25s response timer elapses while the upload is in flight.
102
+ jest.advanceTimersByTime(25000);
103
+ await flush();
104
+
105
+ expect(disconnectSpy).not.toHaveBeenCalled();
106
+ expect(firstErrors).toHaveLength(1);
107
+ expect(uploadErrors).toHaveLength(0);
108
+ });
109
+
110
+ test('forceRun supersede settles the previous pending call immediately', async () => {
111
+ const { t } = createHarness();
112
+
113
+ const firstErrors: Array<{ errorCode?: unknown }> = [];
114
+ const first = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
115
+ first.catch(e => firstErrors.push(e));
116
+ await flush();
117
+
118
+ const second = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
119
+ second.catch(() => undefined);
120
+ await flush();
121
+
122
+ expect(firstErrors).toHaveLength(1);
123
+ expect(firstErrors[0]?.errorCode).toBe(HardwareErrorCode.BleForceCleanRunPromise);
124
+
125
+ t.runPromise?.reject(new Error('settle second call'));
126
+ await flush();
127
+ });
128
+
129
+ test('late write failure of a superseded call keeps the successor as owner', async () => {
130
+ const { t, disconnectSpy } = createHarness();
131
+
132
+ // First call's write hangs; its promise is controlled by the test.
133
+ let rejectFirstWrite: ((e: Error) => void) | undefined;
134
+ const fakeBleTransport = {
135
+ writeCharacteristic: {
136
+ writeWithoutResponse: jest
137
+ .fn()
138
+ .mockImplementationOnce(
139
+ () =>
140
+ new Promise((_resolve, reject) => {
141
+ rejectFirstWrite = reject;
142
+ })
143
+ )
144
+ .mockImplementation(() => Promise.resolve()),
145
+ },
146
+ writeWithRetry: jest.fn(() => Promise.resolve()),
147
+ };
148
+ (t as any).getCachedTransport = () => fakeBleTransport;
149
+
150
+ const firstErrors: unknown[] = [];
151
+ const first = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
152
+ first.catch(e => firstErrors.push(e));
153
+ await flush();
154
+
155
+ // forceRun successor takes ownership while the first call is stuck writing.
156
+ const secondErrors: unknown[] = [];
157
+ const second = t.call(UUID, 'Initialize', {}, { timeoutMs: 5000 });
158
+ second.catch(e => secondErrors.push(e));
159
+ await flush();
160
+
161
+ // The first call's write now fails late; it must not clear the successor's slot.
162
+ rejectFirstWrite?.(new Error('late write failure'));
163
+ await flush();
164
+
165
+ expect(t.runPromise).not.toBeNull();
166
+
167
+ // The successor's genuine timeout must still tear the connection down.
168
+ jest.advanceTimersByTime(5000);
169
+ await flush();
170
+
171
+ expect(disconnectSpy).toHaveBeenCalledTimes(1);
172
+ expect(secondErrors).toHaveLength(1);
173
+ });
174
+
175
+ test('orphan timer left behind by cancel() must not disconnect the transport', async () => {
176
+ const { t, disconnectSpy } = createHarness();
177
+
178
+ // cancel() nulls the ownership slot without settling the deferred, so the
179
+ // call's response timer stays armed (reachable via DeviceCommands.dispose).
180
+ const errors: Array<{ errorCode?: unknown }> = [];
181
+ const p = t.call(UUID, 'GetFeatures', {}, { timeoutMs: 5000 });
182
+ p.catch(e => errors.push(e));
183
+ await flush();
184
+
185
+ t.cancel();
186
+
187
+ jest.advanceTimersByTime(5000);
188
+ await flush();
189
+
190
+ expect(disconnectSpy).not.toHaveBeenCalled();
191
+ expect(errors).toHaveLength(1);
192
+ expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleTimeoutError);
193
+ });
194
+
195
+ test('timeout on the active call still disconnects the transport', async () => {
196
+ const { t, disconnectSpy } = createHarness();
197
+
198
+ const errors: Array<{ errorCode?: unknown }> = [];
199
+ const p = t.call(UUID, 'GetFeatures', {}, { timeoutMs: 5000 });
200
+ p.catch(e => errors.push(e));
201
+ await flush();
202
+
203
+ jest.advanceTimersByTime(5000);
204
+ await flush();
205
+
206
+ expect(disconnectSpy).toHaveBeenCalledTimes(1);
207
+ expect(errors).toHaveLength(1);
208
+ expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleTimeoutError);
209
+ });
210
+ });