@onekeyfe/hd-transport-react-native 1.2.0-alpha.65 → 1.2.0-alpha.66
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.map +1 -1
- package/dist/index.d.ts +1 -79
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +109 -308
- package/jest.config.js +0 -5
- package/package.json +5 -5
- package/src/BleTransport.ts +6 -0
- package/src/__tests__/BleTransport.test.ts +54 -2
- package/src/__tests__/enumerate.test.ts +132 -0
- package/src/__tests__/protocolV2Link.test.ts +281 -48
- package/src/index.ts +112 -451
- package/src/__tests__/connectTimeout.test.ts +0 -282
- package/src/__tests__/protocolReprobe.test.ts +0 -133
- package/src/__tests__/staleCallTimeout.test.ts +0 -211
- package/src/__tests__/writePacketTimeout.test.ts +0 -306
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'events';
|
|
2
|
-
import { BleErrorCode } from 'react-native-ble-plx';
|
|
3
|
-
|
|
4
|
-
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
5
|
-
|
|
6
|
-
import ReactNativeBleTransport, {
|
|
7
|
-
BLE_CONNECT_TIMEOUT_MS,
|
|
8
|
-
BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD,
|
|
9
|
-
BLE_GATT_SETUP_TIMEOUT_MS,
|
|
10
|
-
} from '../index';
|
|
11
|
-
|
|
12
|
-
import messages from '@onekeyfe/hd-transport/messages.json';
|
|
13
|
-
|
|
14
|
-
jest.mock(
|
|
15
|
-
'react-native',
|
|
16
|
-
() => ({
|
|
17
|
-
Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
|
|
18
|
-
PermissionsAndroid: {
|
|
19
|
-
PERMISSIONS: {},
|
|
20
|
-
RESULTS: {},
|
|
21
|
-
request: jest.fn(),
|
|
22
|
-
requestMultiple: jest.fn(),
|
|
23
|
-
},
|
|
24
|
-
}),
|
|
25
|
-
{ virtual: true }
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
jest.mock('react-native-ble-plx', () => ({
|
|
29
|
-
BleATTErrorCode: { InvalidHandle: 1, UnlikelyError: 14 },
|
|
30
|
-
BleError: Error,
|
|
31
|
-
BleErrorCode: {
|
|
32
|
-
DeviceDisconnected: 201,
|
|
33
|
-
OperationStartFailed: 601,
|
|
34
|
-
DeviceMTUChangeFailed: 401,
|
|
35
|
-
OperationCancelled: 2,
|
|
36
|
-
OperationTimedOut: 3,
|
|
37
|
-
DeviceAlreadyConnected: 203,
|
|
38
|
-
},
|
|
39
|
-
BleManager: jest.fn(),
|
|
40
|
-
ScanMode: { LowLatency: 2 },
|
|
41
|
-
}));
|
|
42
|
-
|
|
43
|
-
jest.mock('@onekeyfe/react-native-ble-utils', () => ({
|
|
44
|
-
__esModule: true,
|
|
45
|
-
default: {
|
|
46
|
-
getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
47
|
-
getBondedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
48
|
-
pairDevice: jest.fn(() => Promise.resolve()),
|
|
49
|
-
},
|
|
50
|
-
}));
|
|
51
|
-
|
|
52
|
-
const UUID = 'stalled-connect-device';
|
|
53
|
-
|
|
54
|
-
const flush = () =>
|
|
55
|
-
new Promise(resolve => {
|
|
56
|
-
setImmediate(resolve);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
async function advanceUntil(settled: () => boolean, totalMs: number, stepMs = 250) {
|
|
60
|
-
for (let elapsed = 0; elapsed < totalMs; elapsed += stepMs) {
|
|
61
|
-
jest.advanceTimersByTime(stepMs);
|
|
62
|
-
// eslint-disable-next-line no-await-in-loop
|
|
63
|
-
await flush();
|
|
64
|
-
if (settled()) return;
|
|
65
|
-
}
|
|
66
|
-
throw new Error(`fake timers exhausted after ${totalMs}ms before the connect settled`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** A device whose native connect() never settles — the observed iOS failure mode. */
|
|
70
|
-
function createHarness(connectImpl: () => Promise<unknown>) {
|
|
71
|
-
const connect = jest.fn(connectImpl);
|
|
72
|
-
const writeCharacteristic = {
|
|
73
|
-
uuid: '00000002-0000-1000-8000-00805f9b34fb',
|
|
74
|
-
isWritableWithResponse: true,
|
|
75
|
-
};
|
|
76
|
-
const notifyCharacteristic = {
|
|
77
|
-
uuid: '00000003-0000-1000-8000-00805f9b34fb',
|
|
78
|
-
isNotifiable: true,
|
|
79
|
-
};
|
|
80
|
-
const device = {
|
|
81
|
-
id: UUID,
|
|
82
|
-
name: 'OneKey Classic',
|
|
83
|
-
localName: 'OneKey Classic',
|
|
84
|
-
serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
|
|
85
|
-
isConnected: jest.fn(() => Promise.resolve(false)),
|
|
86
|
-
cancelConnection: jest.fn(() => Promise.resolve()),
|
|
87
|
-
connect,
|
|
88
|
-
discoverAllServicesAndCharacteristics: jest.fn(() => Promise.resolve()),
|
|
89
|
-
characteristicsForService: jest.fn(() =>
|
|
90
|
-
Promise.resolve([writeCharacteristic, notifyCharacteristic])
|
|
91
|
-
),
|
|
92
|
-
services: jest.fn(() => Promise.resolve([])),
|
|
93
|
-
onDisconnected: jest.fn(() => ({ remove: jest.fn() })),
|
|
94
|
-
};
|
|
95
|
-
const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
|
|
96
|
-
const bleManager = {
|
|
97
|
-
devices: jest.fn(() => Promise.resolve([device])),
|
|
98
|
-
connectedDevices: jest.fn(() => Promise.resolve([])),
|
|
99
|
-
connectToDevice: jest.fn(connectImpl),
|
|
100
|
-
cancelTransaction: jest.fn(() => Promise.resolve()),
|
|
101
|
-
cancelDeviceConnection: jest.fn(() => Promise.resolve()),
|
|
102
|
-
onStateChange: jest.fn((listener: (state: string) => void) => {
|
|
103
|
-
// Dispatch asynchronously: subscribeBleOn wires its own cleanup after
|
|
104
|
-
// registering, so a synchronous callback would run before it is ready.
|
|
105
|
-
setImmediate(() => listener('PoweredOn'));
|
|
106
|
-
return { remove: jest.fn() };
|
|
107
|
-
}),
|
|
108
|
-
state: jest.fn(() => Promise.resolve('PoweredOn')),
|
|
109
|
-
startDeviceScan: jest.fn(),
|
|
110
|
-
stopDeviceScan: jest.fn(),
|
|
111
|
-
};
|
|
112
|
-
(transport as any).blePlxManager = bleManager;
|
|
113
|
-
transport.init(
|
|
114
|
-
{ debug: jest.fn(), error: jest.fn(), warn: jest.fn() } as any,
|
|
115
|
-
new EventEmitter()
|
|
116
|
-
);
|
|
117
|
-
transport.configure(messages);
|
|
118
|
-
return { transport, device, bleManager, connect };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
describe('BLE connect timeout', () => {
|
|
122
|
-
beforeAll(() => {
|
|
123
|
-
jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
afterAll(() => {
|
|
127
|
-
jest.useRealTimers();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
afterEach(() => {
|
|
131
|
-
jest.clearAllTimers();
|
|
132
|
-
jest.restoreAllMocks();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test('a native connect that never settles is bounded instead of blocking forever', async () => {
|
|
136
|
-
// iOS applies its own connect timeout on a serial queue; when that queue is busy
|
|
137
|
-
// the timeout never fires and acquire() blocks until the app-level 60s timeout.
|
|
138
|
-
const { transport } = createHarness(
|
|
139
|
-
() =>
|
|
140
|
-
new Promise(() => {
|
|
141
|
-
// never settles
|
|
142
|
-
})
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
const errors: Array<{ errorCode?: unknown }> = [];
|
|
146
|
-
let settled = false;
|
|
147
|
-
transport.acquire({ uuid: UUID }).catch(e => {
|
|
148
|
-
errors.push(e);
|
|
149
|
-
settled = true;
|
|
150
|
-
});
|
|
151
|
-
await flush();
|
|
152
|
-
|
|
153
|
-
await advanceUntil(() => settled, BLE_CONNECT_TIMEOUT_MS + 5000);
|
|
154
|
-
|
|
155
|
-
expect(errors).toHaveLength(1);
|
|
156
|
-
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleConnectedError);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
test('the connect budget leaves generous headroom over a healthy connect', () => {
|
|
160
|
-
// Healthy connects finish in ~2-3s (the native budget is 3s); this backstop only
|
|
161
|
-
// fires when the native timeout itself fails to.
|
|
162
|
-
expect(BLE_CONNECT_TIMEOUT_MS).toBeGreaterThanOrEqual(6000);
|
|
163
|
-
expect(BLE_CONNECT_TIMEOUT_MS).toBeLessThanOrEqual(12000);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test('a stalled connect is abandoned natively so the next attempt is not cancelled by it', async () => {
|
|
167
|
-
const { transport, bleManager } = createHarness(
|
|
168
|
-
() =>
|
|
169
|
-
new Promise(() => {
|
|
170
|
-
// never settles
|
|
171
|
-
})
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
let settled = false;
|
|
175
|
-
transport.acquire({ uuid: UUID }).catch(() => {
|
|
176
|
-
settled = true;
|
|
177
|
-
});
|
|
178
|
-
await flush();
|
|
179
|
-
await advanceUntil(() => settled, BLE_CONNECT_TIMEOUT_MS + 5000);
|
|
180
|
-
|
|
181
|
-
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(UUID);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test('repeated stalled connects recreate the BLE manager', async () => {
|
|
185
|
-
const { transport, bleManager } = createHarness(
|
|
186
|
-
() =>
|
|
187
|
-
new Promise(() => {
|
|
188
|
-
// never settles
|
|
189
|
-
})
|
|
190
|
-
);
|
|
191
|
-
const destroy = jest.fn();
|
|
192
|
-
(bleManager as unknown as { destroy: jest.Mock }).destroy = destroy;
|
|
193
|
-
|
|
194
|
-
for (let attempt = 0; attempt < BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD; attempt += 1) {
|
|
195
|
-
let settled = false;
|
|
196
|
-
transport.acquire({ uuid: UUID }).catch(() => {
|
|
197
|
-
settled = true;
|
|
198
|
-
});
|
|
199
|
-
// eslint-disable-next-line no-await-in-loop
|
|
200
|
-
await flush();
|
|
201
|
-
// eslint-disable-next-line no-await-in-loop
|
|
202
|
-
await advanceUntil(() => settled, BLE_CONNECT_TIMEOUT_MS + 5000);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
expect(destroy).toHaveBeenCalledTimes(1);
|
|
206
|
-
expect((transport as unknown as { blePlxManager?: unknown }).blePlxManager).toBeUndefined();
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test('native connect timeouts contribute to the same manager reset budget', async () => {
|
|
210
|
-
const { transport, bleManager } = createHarness(() => Promise.resolve());
|
|
211
|
-
const destroy = jest.fn();
|
|
212
|
-
(bleManager as unknown as { destroy: jest.Mock }).destroy = destroy;
|
|
213
|
-
const nativeTimeout = Object.assign(new Error('Operation timed out'), {
|
|
214
|
-
errorCode: BleErrorCode.OperationTimedOut,
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
for (let attempt = 0; attempt < BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD; attempt += 1) {
|
|
218
|
-
// eslint-disable-next-line no-await-in-loop
|
|
219
|
-
await expect(
|
|
220
|
-
(transport as any).connectWithTimeout(UUID, () => Promise.reject(nativeTimeout))
|
|
221
|
-
).rejects.toBe(nativeTimeout);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
expect(destroy).toHaveBeenCalledTimes(1);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
test('GATT discovery is bounded and abandons the native connection', async () => {
|
|
228
|
-
const { transport, device, bleManager } = createHarness(() => Promise.resolve());
|
|
229
|
-
device.discoverAllServicesAndCharacteristics.mockImplementation(
|
|
230
|
-
() =>
|
|
231
|
-
new Promise(() => {
|
|
232
|
-
// never settles
|
|
233
|
-
})
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
const errors: Array<{ errorCode?: unknown }> = [];
|
|
237
|
-
let settled = false;
|
|
238
|
-
(transport as any).resolveCharacteristicsWithTimeout(UUID, device).catch((error: unknown) => {
|
|
239
|
-
errors.push(error as { errorCode?: unknown });
|
|
240
|
-
settled = true;
|
|
241
|
-
});
|
|
242
|
-
await flush();
|
|
243
|
-
await advanceUntil(() => settled, BLE_GATT_SETUP_TIMEOUT_MS + 5000);
|
|
244
|
-
|
|
245
|
-
expect(errors).toHaveLength(1);
|
|
246
|
-
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleConnectedError);
|
|
247
|
-
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(UUID);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
test('a successful GATT retry clears the timeout budget before an abandoned call settles', async () => {
|
|
251
|
-
const { transport, device, bleManager } = createHarness(() => Promise.resolve());
|
|
252
|
-
let resolveAbandonedDiscovery: (() => void) | undefined;
|
|
253
|
-
device.discoverAllServicesAndCharacteristics
|
|
254
|
-
.mockImplementationOnce(
|
|
255
|
-
() =>
|
|
256
|
-
new Promise<void>(resolve => {
|
|
257
|
-
resolveAbandonedDiscovery = resolve;
|
|
258
|
-
})
|
|
259
|
-
)
|
|
260
|
-
.mockResolvedValueOnce(undefined);
|
|
261
|
-
|
|
262
|
-
let firstSettled = false;
|
|
263
|
-
(transport as any).resolveCharacteristicsWithTimeout(UUID, device).catch(() => {
|
|
264
|
-
firstSettled = true;
|
|
265
|
-
});
|
|
266
|
-
await flush();
|
|
267
|
-
await advanceUntil(() => firstSettled, BLE_GATT_SETUP_TIMEOUT_MS + 5000);
|
|
268
|
-
|
|
269
|
-
await expect(
|
|
270
|
-
(transport as any).resolveCharacteristicsWithTimeout(UUID, device)
|
|
271
|
-
).resolves.toMatchObject({
|
|
272
|
-
writeCharacteristic: expect.any(Object),
|
|
273
|
-
notifyCharacteristic: expect.any(Object),
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
resolveAbandonedDiscovery?.();
|
|
277
|
-
await flush();
|
|
278
|
-
|
|
279
|
-
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledTimes(1);
|
|
280
|
-
expect((transport as any).connectionSetupTimeoutCounts.has(UUID)).toBe(false);
|
|
281
|
-
});
|
|
282
|
-
});
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import ReactNativeBleTransport, { PROTOCOL_REPROBE_FALLBACK_ATTEMPTS } from '../index';
|
|
2
|
-
|
|
3
|
-
import messages from '@onekeyfe/hd-transport/messages.json';
|
|
4
|
-
|
|
5
|
-
jest.mock(
|
|
6
|
-
'react-native',
|
|
7
|
-
() => ({
|
|
8
|
-
Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
|
|
9
|
-
PermissionsAndroid: {
|
|
10
|
-
PERMISSIONS: {},
|
|
11
|
-
RESULTS: {},
|
|
12
|
-
request: jest.fn(),
|
|
13
|
-
requestMultiple: jest.fn(),
|
|
14
|
-
},
|
|
15
|
-
}),
|
|
16
|
-
{ virtual: true }
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
jest.mock('react-native-ble-plx', () => ({
|
|
20
|
-
BleATTErrorCode: { InvalidHandle: 1, UnlikelyError: 14 },
|
|
21
|
-
BleError: Error,
|
|
22
|
-
BleErrorCode: { DeviceDisconnected: 201, OperationStartFailed: 601 },
|
|
23
|
-
BleManager: jest.fn(),
|
|
24
|
-
ScanMode: { LowLatency: 2 },
|
|
25
|
-
}));
|
|
26
|
-
|
|
27
|
-
jest.mock('@onekeyfe/react-native-ble-utils', () => ({
|
|
28
|
-
__esModule: true,
|
|
29
|
-
default: {
|
|
30
|
-
getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
31
|
-
getBondedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
32
|
-
pairDevice: jest.fn(() => Promise.resolve()),
|
|
33
|
-
},
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
|
-
const UUID = 'reprobe-device';
|
|
37
|
-
|
|
38
|
-
/** Drive detectProtocol with stubbed probes so we can observe which are attempted. */
|
|
39
|
-
function createHarness({ v1, v2 }: { v1: boolean; v2: boolean }) {
|
|
40
|
-
const transport = new ReactNativeBleTransport({});
|
|
41
|
-
transport.configure(messages);
|
|
42
|
-
const probeV1 = jest.fn(() => {
|
|
43
|
-
if (v1) {
|
|
44
|
-
(transport as any).deviceProtocol.set(UUID, 'V1');
|
|
45
|
-
}
|
|
46
|
-
return Promise.resolve(v1);
|
|
47
|
-
});
|
|
48
|
-
const probeV2 = jest.fn(() => {
|
|
49
|
-
if (v2) {
|
|
50
|
-
(transport as any).deviceProtocol.set(UUID, 'V2');
|
|
51
|
-
}
|
|
52
|
-
return Promise.resolve(v2);
|
|
53
|
-
});
|
|
54
|
-
(transport as any).probeProtocolV1 = probeV1;
|
|
55
|
-
(transport as any).probeProtocolV2 = probeV2;
|
|
56
|
-
(transport as any).resetProbeStateAfterProtocolProbe = jest.fn(() => Promise.resolve());
|
|
57
|
-
return { transport, probeV1, probeV2 };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const detect = (transport: ReactNativeBleTransport) =>
|
|
61
|
-
(transport as any).detectProtocol(UUID, undefined, undefined, () =>
|
|
62
|
-
Promise.resolve()
|
|
63
|
-
) as Promise<string>;
|
|
64
|
-
|
|
65
|
-
describe('protocol re-probe after a known device goes away', () => {
|
|
66
|
-
test('an unknown device still probes both protocols', async () => {
|
|
67
|
-
const { transport, probeV1, probeV2 } = createHarness({ v1: false, v2: false });
|
|
68
|
-
|
|
69
|
-
await expect(detect(transport)).rejects.toBeDefined();
|
|
70
|
-
|
|
71
|
-
expect(probeV1).toHaveBeenCalledTimes(1);
|
|
72
|
-
expect(probeV2).toHaveBeenCalledTimes(1);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test('a device already known to speak V1 does not pay the V2 probe while it is away', async () => {
|
|
76
|
-
const known = createHarness({ v1: true, v2: false });
|
|
77
|
-
// First detection confirms V1 (this is the session that just talked to the device).
|
|
78
|
-
await expect(detect(known.transport)).resolves.toBe('V1');
|
|
79
|
-
|
|
80
|
-
// The device now reboots after a firmware install: V1 stops answering.
|
|
81
|
-
(known.transport as any).deviceProtocol.delete(UUID);
|
|
82
|
-
const probeV1 = jest.fn(() => Promise.resolve(false));
|
|
83
|
-
const probeV2 = jest.fn(() => Promise.resolve(false));
|
|
84
|
-
(known.transport as any).probeProtocolV1 = probeV1;
|
|
85
|
-
(known.transport as any).probeProtocolV2 = probeV2;
|
|
86
|
-
|
|
87
|
-
await expect(detect(known.transport)).rejects.toBeDefined();
|
|
88
|
-
|
|
89
|
-
expect(probeV1).toHaveBeenCalledTimes(1);
|
|
90
|
-
// The 10s Protocol V2 ping is pure waste for a device we just spoke V1 to.
|
|
91
|
-
expect(probeV2).not.toHaveBeenCalled();
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test('after repeated failures it re-probes every protocol again', async () => {
|
|
95
|
-
const known = createHarness({ v1: true, v2: false });
|
|
96
|
-
await expect(detect(known.transport)).resolves.toBe('V1');
|
|
97
|
-
|
|
98
|
-
(known.transport as any).deviceProtocol.delete(UUID);
|
|
99
|
-
const probeV1 = jest.fn(() => Promise.resolve(false));
|
|
100
|
-
const probeV2 = jest.fn(() => Promise.resolve(false));
|
|
101
|
-
(known.transport as any).probeProtocolV1 = probeV1;
|
|
102
|
-
(known.transport as any).probeProtocolV2 = probeV2;
|
|
103
|
-
|
|
104
|
-
for (let attempt = 0; attempt < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS; attempt += 1) {
|
|
105
|
-
// eslint-disable-next-line no-await-in-loop
|
|
106
|
-
await expect(detect(known.transport)).rejects.toBeDefined();
|
|
107
|
-
}
|
|
108
|
-
expect(probeV2).not.toHaveBeenCalled();
|
|
109
|
-
|
|
110
|
-
// The device may genuinely have changed protocol, so the shortcut must expire.
|
|
111
|
-
await expect(detect(known.transport)).rejects.toBeDefined();
|
|
112
|
-
expect(probeV2).toHaveBeenCalled();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test('a successful detection clears the failure streak', async () => {
|
|
116
|
-
const known = createHarness({ v1: true, v2: false });
|
|
117
|
-
await expect(detect(known.transport)).resolves.toBe('V1');
|
|
118
|
-
(known.transport as any).deviceProtocol.delete(UUID);
|
|
119
|
-
|
|
120
|
-
const failingV1 = jest.fn(() => Promise.resolve(false));
|
|
121
|
-
(known.transport as any).probeProtocolV1 = failingV1;
|
|
122
|
-
await expect(detect(known.transport)).rejects.toBeDefined();
|
|
123
|
-
|
|
124
|
-
// Device comes back.
|
|
125
|
-
(known.transport as any).probeProtocolV1 = jest.fn(() => {
|
|
126
|
-
(known.transport as any).deviceProtocol.set(UUID, 'V1');
|
|
127
|
-
return Promise.resolve(true);
|
|
128
|
-
});
|
|
129
|
-
await expect(detect(known.transport)).resolves.toBe('V1');
|
|
130
|
-
|
|
131
|
-
expect((known.transport as any).protocolReprobeFailures.get(UUID)).toBeUndefined();
|
|
132
|
-
});
|
|
133
|
-
});
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
-
|
|
3
|
-
import ReactNativeBleTransport from '../index';
|
|
4
|
-
|
|
5
|
-
import messages from '@onekeyfe/hd-transport/messages.json';
|
|
6
|
-
|
|
7
|
-
jest.mock(
|
|
8
|
-
'react-native',
|
|
9
|
-
() => ({
|
|
10
|
-
Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
|
|
11
|
-
PermissionsAndroid: {
|
|
12
|
-
PERMISSIONS: {},
|
|
13
|
-
RESULTS: {},
|
|
14
|
-
request: jest.fn(),
|
|
15
|
-
requestMultiple: jest.fn(),
|
|
16
|
-
},
|
|
17
|
-
}),
|
|
18
|
-
{ virtual: true }
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
jest.mock('react-native-ble-plx', () => ({
|
|
22
|
-
BleATTErrorCode: { InvalidHandle: 1 },
|
|
23
|
-
BleError: Error,
|
|
24
|
-
BleErrorCode: { DeviceDisconnected: 201, OperationStartFailed: 601 },
|
|
25
|
-
BleManager: jest.fn(),
|
|
26
|
-
ScanMode: { LowLatency: 2 },
|
|
27
|
-
}));
|
|
28
|
-
|
|
29
|
-
jest.mock('@onekeyfe/react-native-ble-utils', () => ({
|
|
30
|
-
__esModule: true,
|
|
31
|
-
default: {
|
|
32
|
-
getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
33
|
-
getBondedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
34
|
-
pairDevice: jest.fn(() => Promise.resolve()),
|
|
35
|
-
},
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
|
-
const UUID = 'stale-timeout-device';
|
|
39
|
-
|
|
40
|
-
const flush = () =>
|
|
41
|
-
new Promise(resolve => {
|
|
42
|
-
setImmediate(resolve);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
function createHarness() {
|
|
46
|
-
const t = new ReactNativeBleTransport({});
|
|
47
|
-
t.configure(messages);
|
|
48
|
-
(t as any).deviceProtocol.set(UUID, 'V1');
|
|
49
|
-
const writeWithoutResponse = jest.fn(() => Promise.resolve());
|
|
50
|
-
const fakeBleTransport = {
|
|
51
|
-
writeCharacteristic: { writeWithoutResponse },
|
|
52
|
-
writeWithRetry: jest.fn(() => Promise.resolve()),
|
|
53
|
-
};
|
|
54
|
-
(t as any).getCachedTransport = () => fakeBleTransport;
|
|
55
|
-
const disconnectSpy = jest.spyOn(t, 'disconnect').mockResolvedValue(undefined);
|
|
56
|
-
return { t, disconnectSpy, writeWithoutResponse };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
describe('Protocol V1 stale call timeout', () => {
|
|
60
|
-
beforeAll(() => {
|
|
61
|
-
jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
afterAll(() => {
|
|
65
|
-
jest.useRealTimers();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
afterEach(() => {
|
|
69
|
-
jest.clearAllTimers();
|
|
70
|
-
jest.restoreAllMocks();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test('superseded Initialize timeout does not tear down the shared transport', async () => {
|
|
74
|
-
const { t, disconnectSpy } = createHarness();
|
|
75
|
-
|
|
76
|
-
// Initialize #1: written while the device reboots, never answered.
|
|
77
|
-
const firstErrors: unknown[] = [];
|
|
78
|
-
const first = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
79
|
-
first.catch(e => firstErrors.push(e));
|
|
80
|
-
await flush();
|
|
81
|
-
|
|
82
|
-
// Initialize #2 (forceRun) supersedes #1 three seconds later.
|
|
83
|
-
jest.advanceTimersByTime(3000);
|
|
84
|
-
const secondErrors: unknown[] = [];
|
|
85
|
-
const second = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
86
|
-
second.catch(e => secondErrors.push(e));
|
|
87
|
-
await flush();
|
|
88
|
-
|
|
89
|
-
// The device answers Initialize #2 (settle its deferred at the transport seam).
|
|
90
|
-
expect(t.runPromise).not.toBeNull();
|
|
91
|
-
t.runPromise?.reject(new Error('settled by device response'));
|
|
92
|
-
await flush();
|
|
93
|
-
|
|
94
|
-
// FirmwareUpload is now the active call on the same transport, awaiting its response.
|
|
95
|
-
const uploadErrors: unknown[] = [];
|
|
96
|
-
const upload = t.call(UUID, 'FirmwareUpload', { payload: Buffer.alloc(300) });
|
|
97
|
-
upload.catch(e => uploadErrors.push(e));
|
|
98
|
-
await flush();
|
|
99
|
-
jest.advanceTimersByTime(100); // firmware upload flush delay
|
|
100
|
-
await flush();
|
|
101
|
-
|
|
102
|
-
// Initialize #1's 25s response timer elapses while the upload is in flight.
|
|
103
|
-
jest.advanceTimersByTime(25000);
|
|
104
|
-
await flush();
|
|
105
|
-
|
|
106
|
-
expect(disconnectSpy).not.toHaveBeenCalled();
|
|
107
|
-
expect(firstErrors).toHaveLength(1);
|
|
108
|
-
expect(uploadErrors).toHaveLength(0);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test('forceRun supersede settles the previous pending call immediately', async () => {
|
|
112
|
-
const { t } = createHarness();
|
|
113
|
-
|
|
114
|
-
const firstErrors: Array<{ errorCode?: unknown }> = [];
|
|
115
|
-
const first = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
116
|
-
first.catch(e => firstErrors.push(e));
|
|
117
|
-
await flush();
|
|
118
|
-
|
|
119
|
-
const second = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
120
|
-
second.catch(() => undefined);
|
|
121
|
-
await flush();
|
|
122
|
-
|
|
123
|
-
expect(firstErrors).toHaveLength(1);
|
|
124
|
-
expect(firstErrors[0]?.errorCode).toBe(HardwareErrorCode.BleForceCleanRunPromise);
|
|
125
|
-
|
|
126
|
-
t.runPromise?.reject(new Error('settle second call'));
|
|
127
|
-
await flush();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test('late write failure of a superseded call keeps the successor as owner', async () => {
|
|
131
|
-
const { t, disconnectSpy } = createHarness();
|
|
132
|
-
|
|
133
|
-
// First call's write hangs; its promise is controlled by the test.
|
|
134
|
-
let rejectFirstWrite: ((e: Error) => void) | undefined;
|
|
135
|
-
const fakeBleTransport = {
|
|
136
|
-
writeCharacteristic: {
|
|
137
|
-
writeWithoutResponse: jest
|
|
138
|
-
.fn()
|
|
139
|
-
.mockImplementationOnce(
|
|
140
|
-
() =>
|
|
141
|
-
new Promise((_resolve, reject) => {
|
|
142
|
-
rejectFirstWrite = reject;
|
|
143
|
-
})
|
|
144
|
-
)
|
|
145
|
-
.mockImplementation(() => Promise.resolve()),
|
|
146
|
-
},
|
|
147
|
-
writeWithRetry: jest.fn(() => Promise.resolve()),
|
|
148
|
-
};
|
|
149
|
-
(t as any).getCachedTransport = () => fakeBleTransport;
|
|
150
|
-
|
|
151
|
-
const firstErrors: unknown[] = [];
|
|
152
|
-
const first = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
153
|
-
first.catch(e => firstErrors.push(e));
|
|
154
|
-
await flush();
|
|
155
|
-
|
|
156
|
-
// forceRun successor takes ownership while the first call is stuck writing.
|
|
157
|
-
const secondErrors: unknown[] = [];
|
|
158
|
-
const second = t.call(UUID, 'Initialize', {}, { timeoutMs: 5000 });
|
|
159
|
-
second.catch(e => secondErrors.push(e));
|
|
160
|
-
await flush();
|
|
161
|
-
|
|
162
|
-
// The first call's write now fails late; it must not clear the successor's slot.
|
|
163
|
-
rejectFirstWrite?.(new Error('late write failure'));
|
|
164
|
-
await flush();
|
|
165
|
-
|
|
166
|
-
expect(t.runPromise).not.toBeNull();
|
|
167
|
-
|
|
168
|
-
// The successor's genuine timeout must still tear the connection down.
|
|
169
|
-
jest.advanceTimersByTime(5000);
|
|
170
|
-
await flush();
|
|
171
|
-
|
|
172
|
-
expect(disconnectSpy).toHaveBeenCalledTimes(1);
|
|
173
|
-
expect(secondErrors).toHaveLength(1);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test('orphan timer left behind by cancel() must not disconnect the transport', async () => {
|
|
177
|
-
const { t, disconnectSpy } = createHarness();
|
|
178
|
-
|
|
179
|
-
// cancel() nulls the ownership slot without settling the deferred, so the
|
|
180
|
-
// call's response timer stays armed (reachable via DeviceCommands.dispose).
|
|
181
|
-
const errors: Array<{ errorCode?: unknown }> = [];
|
|
182
|
-
const p = t.call(UUID, 'GetFeatures', {}, { timeoutMs: 5000 });
|
|
183
|
-
p.catch(e => errors.push(e));
|
|
184
|
-
await flush();
|
|
185
|
-
|
|
186
|
-
t.cancel();
|
|
187
|
-
|
|
188
|
-
jest.advanceTimersByTime(5000);
|
|
189
|
-
await flush();
|
|
190
|
-
|
|
191
|
-
expect(disconnectSpy).not.toHaveBeenCalled();
|
|
192
|
-
expect(errors).toHaveLength(1);
|
|
193
|
-
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleTimeoutError);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test('timeout on the active call still disconnects the transport', async () => {
|
|
197
|
-
const { t, disconnectSpy } = createHarness();
|
|
198
|
-
|
|
199
|
-
const errors: Array<{ errorCode?: unknown }> = [];
|
|
200
|
-
const p = t.call(UUID, 'GetFeatures', {}, { timeoutMs: 5000 });
|
|
201
|
-
p.catch(e => errors.push(e));
|
|
202
|
-
await flush();
|
|
203
|
-
|
|
204
|
-
jest.advanceTimersByTime(5000);
|
|
205
|
-
await flush();
|
|
206
|
-
|
|
207
|
-
expect(disconnectSpy).toHaveBeenCalledTimes(1);
|
|
208
|
-
expect(errors).toHaveLength(1);
|
|
209
|
-
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleTimeoutError);
|
|
210
|
-
});
|
|
211
|
-
});
|