@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.
- package/dist/BleTransport.d.ts +1 -1
- package/dist/BleTransport.d.ts.map +1 -1
- package/dist/bleStrategy.d.ts +0 -6
- package/dist/bleStrategy.d.ts.map +1 -1
- package/dist/constants.d.ts +1 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +80 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +341 -210
- package/jest.config.js +5 -0
- package/package.json +5 -5
- package/src/BleTransport.ts +1 -1
- package/src/__tests__/bleStrategy.test.ts +7 -77
- package/src/__tests__/connectTimeout.test.ts +281 -0
- package/src/__tests__/protocolReprobe.test.ts +132 -0
- package/src/__tests__/protocolV1SchemaFixture.ts +39 -0
- package/src/__tests__/protocolV2Link.test.ts +28 -121
- package/src/__tests__/staleCallTimeout.test.ts +210 -0
- package/src/__tests__/writePacketTimeout.test.ts +305 -0
- package/src/bleStrategy.ts +10 -23
- package/src/constants.ts +1 -2
- package/src/index.ts +486 -229
|
@@ -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
|
-
}
|
|
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(
|
|
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
|
-
}
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
+
});
|