@onekeyfe/hd-transport-react-native 1.2.0-alpha.56 → 1.2.0-alpha.62

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-react-native",
3
- "version": "1.2.0-alpha.56",
3
+ "version": "1.2.0-alpha.62",
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.0-alpha.56",
24
- "@onekeyfe/hd-shared": "1.2.0-alpha.56",
25
- "@onekeyfe/hd-transport": "1.2.0-alpha.56",
23
+ "@onekeyfe/hd-core": "1.2.0-alpha.62",
24
+ "@onekeyfe/hd-shared": "1.2.0-alpha.62",
25
+ "@onekeyfe/hd-transport": "1.2.0-alpha.62",
26
26
  "@onekeyfe/react-native-ble-utils": "^0.1.6",
27
27
  "react-native-ble-plx": "3.5.1"
28
28
  },
29
- "gitHead": "2c472748d67072307c21f8d33bc021cf18fe2ad7"
29
+ "gitHead": "f38f4fc8bf05aea42a4da5b2bd633544597ad958"
30
30
  }
@@ -33,6 +33,6 @@ export default class BleTransport {
33
33
  }
34
34
 
35
35
  async writeWithRetry(data: string): Promise<void> {
36
- await this.writeCharacteristic.writeWithResponse(data);
36
+ await this.writeCharacteristic.writeWithoutResponse(data);
37
37
  }
38
38
  }
@@ -24,7 +24,7 @@ describe('BleTransport side-effecting writes', () => {
24
24
  errorCode: BleErrorCode.DeviceDisconnected,
25
25
  });
26
26
  const writeCharacteristic = {
27
- writeWithResponse: jest.fn(() => Promise.reject(error)),
27
+ writeWithoutResponse: jest.fn(() => Promise.reject(error)),
28
28
  };
29
29
  const device = {
30
30
  id: 'classic-id',
@@ -35,7 +35,7 @@ describe('BleTransport side-effecting writes', () => {
35
35
 
36
36
  await expect(transport.writeWithRetry('payload')).rejects.toBe(error);
37
37
 
38
- expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
38
+ expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
39
39
  expect(device.connect).not.toHaveBeenCalled();
40
40
  });
41
41
  });
@@ -0,0 +1,192 @@
1
+ import { EventEmitter } from 'events';
2
+
3
+ import { HardwareErrorCode } from '@onekeyfe/hd-shared';
4
+
5
+ import ReactNativeBleTransport, {
6
+ BLE_CONNECT_TIMEOUT_MS,
7
+ BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD,
8
+ } from '../index';
9
+
10
+ import messages from '@onekeyfe/hd-transport/messages.json';
11
+
12
+ jest.mock(
13
+ 'react-native',
14
+ () => ({
15
+ Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
16
+ PermissionsAndroid: {
17
+ PERMISSIONS: {},
18
+ RESULTS: {},
19
+ request: jest.fn(),
20
+ requestMultiple: jest.fn(),
21
+ },
22
+ }),
23
+ { virtual: true }
24
+ );
25
+
26
+ jest.mock('react-native-ble-plx', () => ({
27
+ BleATTErrorCode: { InvalidHandle: 1, UnlikelyError: 14 },
28
+ BleError: Error,
29
+ BleErrorCode: {
30
+ DeviceDisconnected: 201,
31
+ OperationStartFailed: 601,
32
+ DeviceMTUChangeFailed: 401,
33
+ OperationCancelled: 2,
34
+ DeviceAlreadyConnected: 203,
35
+ },
36
+ BleManager: jest.fn(),
37
+ ScanMode: { LowLatency: 2 },
38
+ }));
39
+
40
+ jest.mock('@onekeyfe/react-native-ble-utils', () => ({
41
+ __esModule: true,
42
+ default: {
43
+ getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
44
+ getBondedPeripherals: jest.fn(() => Promise.resolve([])),
45
+ pairDevice: jest.fn(() => Promise.resolve()),
46
+ },
47
+ }));
48
+
49
+ const UUID = 'stalled-connect-device';
50
+
51
+ const flush = () =>
52
+ new Promise(resolve => {
53
+ setImmediate(resolve);
54
+ });
55
+
56
+ async function advanceUntil(settled: () => boolean, totalMs: number, stepMs = 250) {
57
+ for (let elapsed = 0; elapsed < totalMs; elapsed += stepMs) {
58
+ jest.advanceTimersByTime(stepMs);
59
+ // eslint-disable-next-line no-await-in-loop
60
+ await flush();
61
+ if (settled()) return;
62
+ }
63
+ throw new Error(`fake timers exhausted after ${totalMs}ms before the connect settled`);
64
+ }
65
+
66
+ /** A device whose native connect() never settles — the observed iOS failure mode. */
67
+ function createHarness(connectImpl: () => Promise<unknown>) {
68
+ const connect = jest.fn(connectImpl);
69
+ const device = {
70
+ id: UUID,
71
+ name: 'OneKey Classic',
72
+ localName: 'OneKey Classic',
73
+ serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
74
+ isConnected: jest.fn(() => Promise.resolve(false)),
75
+ cancelConnection: jest.fn(() => Promise.resolve()),
76
+ connect,
77
+ onDisconnected: jest.fn(() => ({ remove: jest.fn() })),
78
+ };
79
+ const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
80
+ const bleManager = {
81
+ devices: jest.fn(() => Promise.resolve([device])),
82
+ connectedDevices: jest.fn(() => Promise.resolve([])),
83
+ connectToDevice: jest.fn(connectImpl),
84
+ cancelTransaction: jest.fn(() => Promise.resolve()),
85
+ cancelDeviceConnection: jest.fn(() => Promise.resolve()),
86
+ onStateChange: jest.fn((listener: (state: string) => void) => {
87
+ // Dispatch asynchronously: subscribeBleOn wires its own cleanup after
88
+ // registering, so a synchronous callback would run before it is ready.
89
+ setImmediate(() => listener('PoweredOn'));
90
+ return { remove: jest.fn() };
91
+ }),
92
+ state: jest.fn(() => Promise.resolve('PoweredOn')),
93
+ startDeviceScan: jest.fn(),
94
+ stopDeviceScan: jest.fn(),
95
+ };
96
+ (transport as any).blePlxManager = bleManager;
97
+ transport.init(
98
+ { debug: jest.fn(), error: jest.fn(), warn: jest.fn() } as any,
99
+ new EventEmitter()
100
+ );
101
+ transport.configure(messages);
102
+ return { transport, device, bleManager, connect };
103
+ }
104
+
105
+ describe('BLE connect timeout', () => {
106
+ beforeAll(() => {
107
+ jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
108
+ });
109
+
110
+ afterAll(() => {
111
+ jest.useRealTimers();
112
+ });
113
+
114
+ afterEach(() => {
115
+ jest.clearAllTimers();
116
+ jest.restoreAllMocks();
117
+ });
118
+
119
+ test('a native connect that never settles is bounded instead of blocking forever', async () => {
120
+ // iOS applies its own connect timeout on a serial queue; when that queue is busy
121
+ // the timeout never fires and acquire() blocks until the app-level 60s timeout.
122
+ const { transport } = createHarness(
123
+ () =>
124
+ new Promise(() => {
125
+ // never settles
126
+ })
127
+ );
128
+
129
+ const errors: Array<{ errorCode?: unknown }> = [];
130
+ let settled = false;
131
+ transport.acquire({ uuid: UUID }).catch(e => {
132
+ errors.push(e);
133
+ settled = true;
134
+ });
135
+ await flush();
136
+
137
+ await advanceUntil(() => settled, BLE_CONNECT_TIMEOUT_MS + 5000);
138
+
139
+ expect(errors).toHaveLength(1);
140
+ expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleConnectedError);
141
+ });
142
+
143
+ test('the connect budget leaves generous headroom over a healthy connect', () => {
144
+ // Healthy connects finish in ~2-3s (the native budget is 3s); this backstop only
145
+ // fires when the native timeout itself fails to.
146
+ expect(BLE_CONNECT_TIMEOUT_MS).toBeGreaterThanOrEqual(6000);
147
+ expect(BLE_CONNECT_TIMEOUT_MS).toBeLessThanOrEqual(12000);
148
+ });
149
+
150
+ test('a stalled connect is abandoned natively so the next attempt is not cancelled by it', async () => {
151
+ const { transport, bleManager } = createHarness(
152
+ () =>
153
+ new Promise(() => {
154
+ // never settles
155
+ })
156
+ );
157
+
158
+ let settled = false;
159
+ transport.acquire({ uuid: UUID }).catch(() => {
160
+ settled = true;
161
+ });
162
+ await flush();
163
+ await advanceUntil(() => settled, BLE_CONNECT_TIMEOUT_MS + 5000);
164
+
165
+ expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(UUID);
166
+ });
167
+
168
+ test('repeated stalled connects recreate the BLE manager', async () => {
169
+ const { transport, bleManager } = createHarness(
170
+ () =>
171
+ new Promise(() => {
172
+ // never settles
173
+ })
174
+ );
175
+ const destroy = jest.fn();
176
+ (bleManager as unknown as { destroy: jest.Mock }).destroy = destroy;
177
+
178
+ for (let attempt = 0; attempt < BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD; attempt += 1) {
179
+ let settled = false;
180
+ transport.acquire({ uuid: UUID }).catch(() => {
181
+ settled = true;
182
+ });
183
+ // eslint-disable-next-line no-await-in-loop
184
+ await flush();
185
+ // eslint-disable-next-line no-await-in-loop
186
+ await advanceUntil(() => settled, BLE_CONNECT_TIMEOUT_MS + 5000);
187
+ }
188
+
189
+ expect(destroy).toHaveBeenCalledTimes(1);
190
+ expect((transport as unknown as { blePlxManager?: unknown }).blePlxManager).toBeUndefined();
191
+ });
192
+ });
@@ -3,6 +3,7 @@ import transportPackage, {
3
3
  PROTOCOL_V2_CHANNEL_BLE_UART,
4
4
  ProtocolV2,
5
5
  TRANSPORT_EVENT,
6
+ bytesToHex,
6
7
  } from '@onekeyfe/hd-transport';
7
8
  import { HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
8
9
 
@@ -45,11 +46,6 @@ jest.mock('../subscribeBleOn', () => ({
45
46
  subscribeBleOn: jest.fn(() => Promise.resolve()),
46
47
  }));
47
48
 
48
- const setPlatformOS = (os: 'ios' | 'android') => {
49
- const reactNative: { Platform: { OS: string } } = jest.requireMock('react-native');
50
- reactNative.Platform.OS = os;
51
- };
52
-
53
49
  const { parseConfigure } = transportPackage;
54
50
 
55
51
  const protocolV1Schema = {
@@ -73,13 +69,11 @@ const protocolV1Schema = {
73
69
 
74
70
  const protocolV2Schema = {
75
71
  nested: {
76
- ProtocolInfoRequest: { fields: {} },
77
72
  Ping: {
78
73
  fields: {
79
74
  message: { type: 'string', id: 1 },
80
75
  },
81
76
  },
82
- DeviceInfoGet: { fields: {} },
83
77
  FileWrite: { fields: {} },
84
78
  Success: {
85
79
  fields: {
@@ -88,10 +82,8 @@ const protocolV2Schema = {
88
82
  },
89
83
  MessageType: {
90
84
  values: {
91
- MessageType_ProtocolInfoRequest: 60200,
92
85
  MessageType_Ping: 60206,
93
86
  MessageType_Success: 60207,
94
- MessageType_DeviceInfoGet: 60600,
95
87
  MessageType_FileWrite: 60805,
96
88
  },
97
89
  },
@@ -103,13 +95,7 @@ const schemas = {
103
95
  protocolV2: parseConfigure(protocolV2Schema),
104
96
  };
105
97
 
106
- const createHarness = ({
107
- deviceName = 'OneKey Pro 2',
108
- isWritableWithResponse = true,
109
- }: {
110
- deviceName?: string;
111
- isWritableWithResponse?: boolean;
112
- } = {}) => {
98
+ const createHarness = () => {
113
99
  const uuid = 'rn-pro2-id';
114
100
  const sentSeqs: number[] = [];
115
101
  let responseSeq = 0;
@@ -148,15 +134,15 @@ const createHarness = ({
148
134
  const writeCharacteristic = {
149
135
  uuid: '0002',
150
136
  deviceID: uuid,
151
- isWritableWithResponse,
137
+ isWritableWithResponse: true,
152
138
  isWritableWithoutResponse: true,
153
139
  writeWithResponse: jest.fn(handleWrite),
154
140
  writeWithoutResponse: jest.fn(handleWrite),
155
141
  };
156
142
  const device = {
157
143
  id: uuid,
158
- name: deviceName,
159
- localName: deviceName,
144
+ name: 'OneKey Pro 2',
145
+ localName: 'OneKey Pro 2',
160
146
  serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
161
147
  isConnected: jest.fn(() => Promise.resolve(true)),
162
148
  cancelConnection: jest.fn(() => Promise.resolve()),
@@ -198,13 +184,7 @@ const createHarness = ({
198
184
  };
199
185
  };
200
186
 
201
- const createV1Harness = ({
202
- respondOnWriteCount = 1,
203
- isWritableWithResponse = true,
204
- }: {
205
- respondOnWriteCount?: number | number[];
206
- isWritableWithResponse?: boolean;
207
- } = {}) => {
187
+ const createV1Harness = () => {
208
188
  const uuid = 'rn-classic-id';
209
189
  const notifySubscriptionRemovers: jest.Mock[] = [];
210
190
  const disconnectSubscriptionRemovers: jest.Mock[] = [];
@@ -223,25 +203,20 @@ const createV1Harness = ({
223
203
  }),
224
204
  };
225
205
  let writeCount = 0;
226
- const responseWriteCounts = new Set(
227
- Array.isArray(respondOnWriteCount) ? respondOnWriteCount : [respondOnWriteCount]
228
- );
229
- const handleWrite = () => {
230
- writeCount += 1;
231
- if (responseWriteCounts.has(writeCount)) {
232
- notifyCallback?.(null, {
233
- value: Buffer.from('3f23230002000000040a026f6b', 'hex').toString('base64'),
234
- });
235
- }
236
- return Promise.resolve();
237
- };
238
206
  const writeCharacteristic = {
239
207
  uuid: '0002',
240
208
  deviceID: uuid,
241
- isWritableWithResponse,
209
+ isWritableWithResponse: true,
242
210
  isWritableWithoutResponse: true,
243
- writeWithResponse: jest.fn(handleWrite),
244
- writeWithoutResponse: jest.fn(handleWrite),
211
+ writeWithoutResponse: jest.fn(() => {
212
+ writeCount += 1;
213
+ if (writeCount === 1) {
214
+ notifyCallback?.(null, {
215
+ value: Buffer.from('3f23230002000000040a026f6b', 'hex').toString('base64'),
216
+ });
217
+ }
218
+ return Promise.resolve();
219
+ }),
245
220
  };
246
221
  const device = {
247
222
  id: uuid,
@@ -274,7 +249,6 @@ const createV1Harness = ({
274
249
  uuid,
275
250
  device,
276
251
  bleManager,
277
- writeCharacteristic,
278
252
  notifySubscriptionRemovers,
279
253
  disconnectSubscriptionRemovers,
280
254
  };
@@ -328,81 +302,12 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
328
302
  expect(new ReactNativeBleTransport({}).scanTimeout).toBe(3000);
329
303
  });
330
304
 
331
- test('uses withResponse for consecutive iOS Protocol V1 control commands without releasing', async () => {
332
- const { transport, uuid, writeCharacteristic } = createV1Harness({
333
- respondOnWriteCount: [1, 2],
334
- });
335
-
336
- await expect(transport.acquire({ uuid })).resolves.toEqual({
337
- uuid,
338
- protocolType: 'V1',
339
- });
340
- const releaseNative = jest.spyOn(transport as any, 'releaseNative');
341
- expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
342
- expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
343
-
344
- await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).resolves.toBeDefined();
345
- await expect(transport.call(uuid, 'GetFeatures', {}, { timeoutMs: 50 })).resolves.toBeDefined();
346
-
347
- expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(2);
348
- expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
349
- expect(releaseNative).not.toHaveBeenCalled();
350
- await transport.release(uuid, true);
351
- });
352
-
353
- test('falls back to withoutResponse for an iOS Protocol V1 control command when required', async () => {
354
- const { transport, uuid, writeCharacteristic } = createV1Harness({
355
- isWritableWithResponse: false,
356
- });
357
-
358
- await transport.acquire({ uuid });
359
- await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).resolves.toBeDefined();
360
-
361
- expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
362
- expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
363
- await transport.release(uuid, true);
364
- });
365
-
366
- test('does not resend a failed iOS Protocol V1 control write without response', async () => {
367
- const { transport, uuid, writeCharacteristic } = createV1Harness();
368
- const writeError = new Error('write with response failed');
369
-
370
- await transport.acquire({ uuid });
371
- writeCharacteristic.writeWithResponse.mockRejectedValueOnce(writeError);
372
-
373
- await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).rejects.toMatchObject({
374
- errorCode: HardwareErrorCode.BleWriteCharacteristicError,
375
- });
376
- expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
377
- expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
378
- await transport.release(uuid, true);
379
- });
380
-
381
- test('keeps the first Core command as the first iOS BLE request for a Protocol V2 device', async () => {
382
- const { transport, uuid, sentSeqs, writeCharacteristic } = createHarness({
383
- deviceName: 'Pro2 6E9E',
384
- });
385
-
386
- await expect(transport.acquire({ uuid })).resolves.toEqual({
387
- uuid,
388
- protocolType: 'V2',
389
- });
390
- expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
391
-
392
- await expect(
393
- transport.call(uuid, 'Ping', { message: 'first-core-command' })
394
- ).resolves.toBeDefined();
395
- expect(sentSeqs).toEqual([1]);
396
- await transport.release(uuid, true);
397
- });
398
-
399
305
  test('reconnects before falling back to Protocol V1 after a fatal V2 probe failure', async () => {
400
- setPlatformOS('android');
401
306
  const { transport, uuid, device, notifySubscriptionRemovers, disconnectSubscriptionRemovers } =
402
307
  createV1Harness();
403
308
  const probeProtocolV2 = jest
404
309
  .spyOn(transport as any, 'probeProtocolV2')
405
- .mockImplementation(async () => {
310
+ .mockImplementationOnce(async () => {
406
311
  await (transport as any).releaseNative(uuid, true);
407
312
  return false;
408
313
  });
@@ -428,9 +333,8 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
428
333
  });
429
334
 
430
335
  test('cleans the rebuilt transport when Protocol V1 fallback also fails', async () => {
431
- setPlatformOS('android');
432
336
  const { transport, uuid, device, bleManager, notifySubscriptionRemovers } = createV1Harness();
433
- jest.spyOn(transport as any, 'probeProtocolV2').mockImplementation(async () => {
337
+ jest.spyOn(transport as any, 'probeProtocolV2').mockImplementationOnce(async () => {
434
338
  await (transport as any).releaseNative(uuid, true);
435
339
  return false;
436
340
  });
@@ -449,9 +353,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
449
353
  });
450
354
 
451
355
  test('disconnects and invalidates a Protocol V1 link after a response timeout', async () => {
452
- const { transport, uuid, device } = createV1Harness({
453
- respondOnWriteCount: Number.POSITIVE_INFINITY,
454
- });
356
+ const { transport, uuid, device } = createV1Harness();
455
357
 
456
358
  await transport.acquire({ uuid, expectedProtocol: 'V1' });
457
359
  await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 5 })).rejects.toMatchObject({
@@ -463,17 +365,17 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
463
365
  });
464
366
 
465
367
  afterEach(() => {
466
- setPlatformOS('ios');
467
368
  resetProtocolV2BleTuning();
468
369
  });
469
370
 
470
- test('starts the Protocol V2 sequence with the first Core call on iOS', async () => {
371
+ test('keeps the Protocol V2 sequence across probe and the next call', async () => {
471
372
  const { transport, uuid, sentSeqs } = createHarness();
472
373
 
473
374
  await transport.acquire({ uuid });
474
- await transport.call(uuid, 'Ping', { message: 'first-core-command' });
375
+ await transport.call(uuid, 'Ping', { message: 'after-probe' });
475
376
 
476
- expect(sentSeqs).toEqual([1]);
377
+ expect(sentSeqs).toEqual([1, 2]);
378
+ expect(bytesToHex(new Uint8Array([sentSeqs[0], sentSeqs[1]]))).toBe('0102');
477
379
  await transport.release(uuid, true);
478
380
  });
479
381
 
@@ -484,10 +386,8 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
484
386
  harness.setShouldRespond(false);
485
387
 
486
388
  const call = transport.call(uuid, 'Ping', { message: 'wait-for-monitor' }, { timeoutMs: 50 });
487
- while (sentSeqs.length < 1) {
488
- await new Promise(resolve => {
489
- setTimeout(resolve, 0);
490
- });
389
+ while (sentSeqs.length < 2) {
390
+ await Promise.resolve();
491
391
  }
492
392
  await new Promise(resolve => {
493
393
  setTimeout(resolve, 0);
@@ -503,135 +403,30 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
503
403
  const { transport, uuid, sentSeqs } = createHarness();
504
404
 
505
405
  await transport.acquire({ uuid });
506
- await transport.call(uuid, 'Ping', { message: 'first-generation' });
507
406
  await transport.release(uuid, true);
508
407
  await transport.acquire({ uuid });
509
- await transport.call(uuid, 'Ping', { message: 'second-generation' });
510
408
 
511
409
  expect(sentSeqs).toEqual([1, 2]);
512
410
  await transport.release(uuid, true);
513
411
  });
514
412
 
515
- test('uses withResponse for consecutive iOS Protocol V2 control calls without releasing', async () => {
516
- const { transport, uuid, writeCharacteristic } = createHarness();
517
-
518
- await transport.acquire({ uuid });
519
- const releaseNative = jest.spyOn(transport as any, 'releaseNative');
520
- expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
521
- expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
522
-
523
- await transport.call(uuid, 'DeviceInfoGet', {});
524
- await transport.call(uuid, 'ProtocolInfoRequest', {});
525
-
526
- expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(2);
527
- expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
528
- expect(releaseNative).not.toHaveBeenCalled();
529
-
530
- await transport.release(uuid, true);
531
- });
532
-
533
- test('keeps iOS Protocol V2 high-volume calls on withoutResponse', async () => {
413
+ test('uses withoutResponse for normal and high-volume calls', async () => {
534
414
  const { transport, uuid, writeCharacteristic } = createHarness();
535
415
 
536
416
  await transport.acquire({ uuid });
537
-
538
- await transport.call(uuid, 'FileWrite', {});
539
417
  expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
540
418
  expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
541
- await transport.release(uuid, true);
542
- });
543
-
544
- test('uses withResponse for an iOS Protocol V2 firmware file write when requested', async () => {
545
- const { transport, uuid, writeCharacteristic } = createHarness();
546
-
547
- await transport.acquire({ uuid });
548
419
 
549
- await transport.call(uuid, 'FileWrite', {}, { writeWithResponse: true });
550
- expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
551
- expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
552
- await transport.release(uuid, true);
553
- });
554
-
555
- test('falls back to withoutResponse for an iOS Protocol V2 control call when required', async () => {
556
- const { transport, uuid, writeCharacteristic } = createHarness({
557
- isWritableWithResponse: false,
558
- });
559
-
560
- await transport.acquire({ uuid });
561
- await transport.call(uuid, 'ProtocolInfoRequest', {});
420
+ await transport.call(uuid, 'Ping', { message: 'normal' });
421
+ expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(2);
422
+ expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
562
423
 
424
+ await transport.call(uuid, 'FileWrite', {});
425
+ expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(3);
563
426
  expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
564
- expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
565
427
  await transport.release(uuid, true);
566
428
  });
567
429
 
568
- test('does not resend a failed iOS Protocol V2 control write without response', async () => {
569
- const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
570
- const writeError = new Error('write with response failed');
571
- const writeWithResponse = jest.fn().mockRejectedValue(writeError);
572
- const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
573
- const context = {
574
- messageName: 'ProtocolInfoRequest',
575
- timeoutMs: 1000,
576
- highVolume: false,
577
- generation: 1,
578
- signal: new AbortController().signal,
579
- };
580
-
581
- await expect(
582
- transport.writeProtocolV2Packet(
583
- {
584
- writeCharacteristic: {
585
- isWritableWithResponse: true,
586
- writeWithResponse,
587
- writeWithoutResponse,
588
- },
589
- },
590
- Buffer.from('control').toString('base64'),
591
- context,
592
- jest.fn()
593
- )
594
- ).rejects.toBe(writeError);
595
- expect(writeWithResponse).toHaveBeenCalledTimes(1);
596
- expect(writeWithoutResponse).not.toHaveBeenCalled();
597
- });
598
-
599
- test('paces a one-packet Protocol V2 control write on iOS', async () => {
600
- const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
601
- const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
602
- const bleTransport = {
603
- mtuSize: 23,
604
- writeCharacteristic: { writeWithoutResponse },
605
- };
606
- const context = {
607
- messageName: 'ProtocolInfoRequest',
608
- timeoutMs: 1000,
609
- highVolume: false,
610
- generation: 1,
611
- signal: new AbortController().signal,
612
- };
613
- configureProtocolV2BleTuning({ iosPacketLength: 20 });
614
- const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
615
-
616
- try {
617
- const call = transport.writeProtocolV2Frame(
618
- bleTransport,
619
- new Uint8Array(10),
620
- context,
621
- jest.fn()
622
- );
623
-
624
- await Promise.resolve();
625
- expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 5);
626
- expect(writeWithoutResponse).not.toHaveBeenCalled();
627
-
628
- await call;
629
- expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
630
- } finally {
631
- setTimeoutSpy.mockRestore();
632
- }
633
- });
634
-
635
430
  test('rejects an active Protocol V2 reader when disconnect resets the link', async () => {
636
431
  const harness = createHarness();
637
432
  const { transport, uuid, sentSeqs } = harness;
@@ -639,10 +434,8 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
639
434
  harness.setShouldRespond(false);
640
435
 
641
436
  const call = transport.call(uuid, 'Ping', { message: 'disconnect' }, { timeoutMs: 50 });
642
- while (sentSeqs.length < 1) {
643
- await new Promise(resolve => {
644
- setTimeout(resolve, 0);
645
- });
437
+ while (sentSeqs.length < 2) {
438
+ await Promise.resolve();
646
439
  }
647
440
 
648
441
  const rejection = expect(call).rejects.toThrow('React Native BLE transport disconnected');
@@ -689,6 +482,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
689
482
  configureProtocolV2BleTuning({ iosPacketLength: 20 });
690
483
 
691
484
  await transport.writeProtocolV2Frame(
485
+ 'device-uuid',
692
486
  bleTransport,
693
487
  new Uint8Array(30),
694
488
  context,
@@ -720,7 +514,13 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
720
514
  configureProtocolV2BleTuning({ iosPacketLength: 20 });
721
515
 
722
516
  await expect(
723
- transport.writeProtocolV2Frame(bleTransport, new Uint8Array(30), context, jest.fn())
517
+ transport.writeProtocolV2Frame(
518
+ 'device-uuid',
519
+ bleTransport,
520
+ new Uint8Array(30),
521
+ context,
522
+ jest.fn()
523
+ )
724
524
  ).rejects.toMatchObject({ errorCode: 205 });
725
525
  expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
726
526
  });