@onekeyfe/hd-transport-react-native 1.2.0-alpha.64 → 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 -71
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +106 -271
- 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 +105 -399
- package/src/__tests__/connectTimeout.test.ts +0 -192
- 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,306 +0,0 @@
|
|
|
1
|
-
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
-
|
|
3
|
-
import ReactNativeBleTransport, {
|
|
4
|
-
BLE_WRITE_PACKET_TIMEOUT_MS,
|
|
5
|
-
BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD,
|
|
6
|
-
} from '../index';
|
|
7
|
-
|
|
8
|
-
import messages from '@onekeyfe/hd-transport/messages.json';
|
|
9
|
-
|
|
10
|
-
jest.mock(
|
|
11
|
-
'react-native',
|
|
12
|
-
() => ({
|
|
13
|
-
Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
|
|
14
|
-
PermissionsAndroid: {
|
|
15
|
-
PERMISSIONS: {},
|
|
16
|
-
RESULTS: {},
|
|
17
|
-
request: jest.fn(),
|
|
18
|
-
requestMultiple: jest.fn(),
|
|
19
|
-
},
|
|
20
|
-
}),
|
|
21
|
-
{ virtual: true }
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
jest.mock('react-native-ble-plx', () => ({
|
|
25
|
-
BleATTErrorCode: { InvalidHandle: 1 },
|
|
26
|
-
BleError: Error,
|
|
27
|
-
BleErrorCode: { DeviceDisconnected: 201, OperationStartFailed: 601 },
|
|
28
|
-
BleManager: jest.fn(),
|
|
29
|
-
ScanMode: { LowLatency: 2 },
|
|
30
|
-
}));
|
|
31
|
-
|
|
32
|
-
jest.mock('@onekeyfe/react-native-ble-utils', () => ({
|
|
33
|
-
__esModule: true,
|
|
34
|
-
default: {
|
|
35
|
-
getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
36
|
-
getBondedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
37
|
-
pairDevice: jest.fn(() => Promise.resolve()),
|
|
38
|
-
},
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
|
-
const UUID = 'wedged-write-device';
|
|
42
|
-
|
|
43
|
-
const flush = () =>
|
|
44
|
-
new Promise(resolve => {
|
|
45
|
-
setImmediate(resolve);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
/** Drive fake timers forward in slices until `settled()` reports done. */
|
|
49
|
-
async function advanceUntil(settled: () => boolean, totalMs: number, stepMs = 500) {
|
|
50
|
-
for (let elapsed = 0; elapsed < totalMs; elapsed += stepMs) {
|
|
51
|
-
jest.advanceTimersByTime(stepMs);
|
|
52
|
-
// eslint-disable-next-line no-await-in-loop
|
|
53
|
-
await flush();
|
|
54
|
-
if (settled()) return;
|
|
55
|
-
}
|
|
56
|
-
throw new Error(`fake timers exhausted after ${totalMs}ms before the call settled`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Drive fake timers forward without requiring the work to settle. */
|
|
60
|
-
async function drain(totalMs: number, stepMs = 500) {
|
|
61
|
-
for (let elapsed = 0; elapsed < totalMs; elapsed += stepMs) {
|
|
62
|
-
jest.advanceTimersByTime(stepMs);
|
|
63
|
-
// eslint-disable-next-line no-await-in-loop
|
|
64
|
-
await flush();
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function createHarness(writeImpl: () => Promise<void>) {
|
|
69
|
-
const t = new ReactNativeBleTransport({});
|
|
70
|
-
t.configure(messages);
|
|
71
|
-
(t as any).deviceProtocol.set(UUID, 'V1');
|
|
72
|
-
const writeWithoutResponse = jest.fn(writeImpl);
|
|
73
|
-
const fakeBleTransport = {
|
|
74
|
-
writeCharacteristic: { writeWithoutResponse },
|
|
75
|
-
writeWithRetry: jest.fn(writeImpl),
|
|
76
|
-
};
|
|
77
|
-
(t as any).getCachedTransport = () => fakeBleTransport;
|
|
78
|
-
const disconnectSpy = jest.spyOn(t, 'disconnect').mockResolvedValue(undefined);
|
|
79
|
-
return { t, disconnectSpy, writeWithoutResponse };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
describe('BLE packet write timeout', () => {
|
|
83
|
-
beforeAll(() => {
|
|
84
|
-
jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
afterAll(() => {
|
|
88
|
-
jest.useRealTimers();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
afterEach(() => {
|
|
92
|
-
jest.clearAllTimers();
|
|
93
|
-
jest.restoreAllMocks();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('a never-settling write rejects instead of hanging the call forever', async () => {
|
|
97
|
-
// iOS never resolves writeWithoutResponse when the peripheral stops reporting
|
|
98
|
-
// "ready to send"; without a bounded write the response timeout is never armed.
|
|
99
|
-
const { t } = createHarness(
|
|
100
|
-
() =>
|
|
101
|
-
new Promise<void>(() => {
|
|
102
|
-
// never settles
|
|
103
|
-
})
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
const errors: Array<{ errorCode?: unknown }> = [];
|
|
107
|
-
let settled = false;
|
|
108
|
-
const call = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
109
|
-
call.catch(e => {
|
|
110
|
-
errors.push(e);
|
|
111
|
-
settled = true;
|
|
112
|
-
});
|
|
113
|
-
await flush();
|
|
114
|
-
|
|
115
|
-
await advanceUntil(() => settled, 30000);
|
|
116
|
-
|
|
117
|
-
expect(errors).toHaveLength(1);
|
|
118
|
-
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleWriteCharacteristicError);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test('a wedged write tears the BLE link down so the next call reconnects', async () => {
|
|
122
|
-
const { t, disconnectSpy } = createHarness(
|
|
123
|
-
() =>
|
|
124
|
-
new Promise<void>(() => {
|
|
125
|
-
// never settles
|
|
126
|
-
})
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
let settled = false;
|
|
130
|
-
const call = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
131
|
-
call.catch(() => {
|
|
132
|
-
settled = true;
|
|
133
|
-
});
|
|
134
|
-
await flush();
|
|
135
|
-
|
|
136
|
-
await advanceUntil(() => settled, 30000);
|
|
137
|
-
|
|
138
|
-
expect(disconnectSpy).toHaveBeenCalledWith(UUID);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test('the write budget is per packet, not per call', async () => {
|
|
142
|
-
// Five packets at 4s each: the call's write phase far outlives one packet budget,
|
|
143
|
-
// but no single packet does, so nothing may be torn down.
|
|
144
|
-
const { t, disconnectSpy, writeWithoutResponse } = createHarness(
|
|
145
|
-
() =>
|
|
146
|
-
new Promise<void>(resolve => {
|
|
147
|
-
setTimeout(resolve, 4000);
|
|
148
|
-
})
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
const errors: Array<{ errorCode?: unknown }> = [];
|
|
152
|
-
let settled = false;
|
|
153
|
-
const call = t.call(UUID, 'Ping', { message: 'x'.repeat(300) }, { timeoutMs: 120000 });
|
|
154
|
-
call.catch(e => {
|
|
155
|
-
errors.push(e);
|
|
156
|
-
settled = true;
|
|
157
|
-
});
|
|
158
|
-
await flush();
|
|
159
|
-
|
|
160
|
-
await advanceUntil(() => settled, 200000);
|
|
161
|
-
|
|
162
|
-
// Every packet was written: none was aborted even though the write phase as a
|
|
163
|
-
// whole ran far past a single packet budget.
|
|
164
|
-
expect(writeWithoutResponse.mock.calls.length).toBeGreaterThan(1);
|
|
165
|
-
expect(4000 * writeWithoutResponse.mock.calls.length).toBeGreaterThan(
|
|
166
|
-
BLE_WRITE_PACKET_TIMEOUT_MS
|
|
167
|
-
);
|
|
168
|
-
// Fails on the RESPONSE timeout the device never answered, not on a write timeout.
|
|
169
|
-
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleTimeoutError);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test('a superseded call whose write wedges must not tear down the successor link', async () => {
|
|
173
|
-
// Only the first call's write wedges; the successor writes normally.
|
|
174
|
-
let writes = 0;
|
|
175
|
-
const { t, disconnectSpy } = createHarness(() => {
|
|
176
|
-
writes += 1;
|
|
177
|
-
return writes === 1
|
|
178
|
-
? new Promise<void>(() => {
|
|
179
|
-
// never settles
|
|
180
|
-
})
|
|
181
|
-
: Promise.resolve();
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
const first = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
185
|
-
first.catch(() => undefined);
|
|
186
|
-
await flush();
|
|
187
|
-
|
|
188
|
-
// A forceRun call supersedes it and becomes the transport owner.
|
|
189
|
-
const second = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
190
|
-
second.catch(() => undefined);
|
|
191
|
-
await flush();
|
|
192
|
-
|
|
193
|
-
await drain(BLE_WRITE_PACKET_TIMEOUT_MS + 2000);
|
|
194
|
-
|
|
195
|
-
expect(disconnectSpy).not.toHaveBeenCalled();
|
|
196
|
-
expect(t.runPromise).not.toBeNull();
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
test('repeated wedged writes recreate the BLE manager', async () => {
|
|
200
|
-
const { t, disconnectSpy } = createHarness(
|
|
201
|
-
() =>
|
|
202
|
-
new Promise<void>(() => {
|
|
203
|
-
// never settles
|
|
204
|
-
})
|
|
205
|
-
);
|
|
206
|
-
const destroy = jest.fn();
|
|
207
|
-
(t as any).blePlxManager = { destroy };
|
|
208
|
-
|
|
209
|
-
for (let attempt = 0; attempt < BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD; attempt += 1) {
|
|
210
|
-
(t as any).deviceProtocol.set(UUID, 'V1');
|
|
211
|
-
let settled = false;
|
|
212
|
-
const call = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
213
|
-
call.catch(() => {
|
|
214
|
-
settled = true;
|
|
215
|
-
});
|
|
216
|
-
// eslint-disable-next-line no-await-in-loop
|
|
217
|
-
await flush();
|
|
218
|
-
// eslint-disable-next-line no-await-in-loop
|
|
219
|
-
await advanceUntil(() => settled, BLE_WRITE_PACKET_TIMEOUT_MS + 2000);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
expect(disconnectSpy).toHaveBeenCalledTimes(BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD);
|
|
223
|
-
expect(destroy).toHaveBeenCalledTimes(1);
|
|
224
|
-
expect((t as any).blePlxManager).toBeUndefined();
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
test('slow but progressing writes are not aborted', async () => {
|
|
228
|
-
// Each packet takes a while but keeps completing: this must not be mistaken
|
|
229
|
-
// for a wedged link, even when the whole call outlives a single packet budget.
|
|
230
|
-
const { t, writeWithoutResponse } = createHarness(
|
|
231
|
-
() =>
|
|
232
|
-
new Promise<void>(resolve => {
|
|
233
|
-
setTimeout(resolve, 4000);
|
|
234
|
-
})
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
const errors: unknown[] = [];
|
|
238
|
-
let settled = false;
|
|
239
|
-
const call = t.call(UUID, 'Initialize', {}, { timeoutMs: 60000 });
|
|
240
|
-
call.catch(e => {
|
|
241
|
-
errors.push(e);
|
|
242
|
-
settled = true;
|
|
243
|
-
});
|
|
244
|
-
await flush();
|
|
245
|
-
|
|
246
|
-
// Let every packet drain; the call then waits for a device response that never
|
|
247
|
-
// comes, and must fail with the RESPONSE timeout, not a write timeout.
|
|
248
|
-
await advanceUntil(() => settled, 90000);
|
|
249
|
-
|
|
250
|
-
expect(writeWithoutResponse).toHaveBeenCalled();
|
|
251
|
-
expect(errors).toHaveLength(1);
|
|
252
|
-
expect((errors[0] as { errorCode?: unknown })?.errorCode).toBe(
|
|
253
|
-
HardwareErrorCode.BleTimeoutError
|
|
254
|
-
);
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
describe('protocol probe state visibility', () => {
|
|
259
|
-
beforeAll(() => {
|
|
260
|
-
jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
afterAll(() => {
|
|
264
|
-
jest.useRealTimers();
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
afterEach(() => {
|
|
268
|
-
jest.clearAllTimers();
|
|
269
|
-
jest.restoreAllMocks();
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
test('an in-flight probe does not publish its protocol as confirmed', async () => {
|
|
273
|
-
const { t } = createHarness(
|
|
274
|
-
() =>
|
|
275
|
-
new Promise<void>(() => {
|
|
276
|
-
// never settles
|
|
277
|
-
})
|
|
278
|
-
);
|
|
279
|
-
(t as any).deviceProtocol.delete(UUID);
|
|
280
|
-
|
|
281
|
-
const probe = (t as any).probeProtocolV1(UUID) as Promise<boolean>;
|
|
282
|
-
probe.catch(() => undefined);
|
|
283
|
-
await flush();
|
|
284
|
-
|
|
285
|
-
// The call itself must still route as V1 while probing...
|
|
286
|
-
expect(t.getProtocolType(UUID)).toBe('V1');
|
|
287
|
-
// ...but acquire's cached-transport reuse gate must not treat it as detected.
|
|
288
|
-
expect((t as any).deviceProtocol.get(UUID)).toBeUndefined();
|
|
289
|
-
|
|
290
|
-
await drain(BLE_WRITE_PACKET_TIMEOUT_MS + 2000);
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
test('a probe that confirms its protocol leaves no probing entry behind', async () => {
|
|
294
|
-
const { t } = createHarness(() => Promise.resolve());
|
|
295
|
-
(t as any).deviceProtocol.delete(UUID);
|
|
296
|
-
|
|
297
|
-
const probe = (t as any).probeProtocolV1(UUID) as Promise<boolean>;
|
|
298
|
-
probe.catch(() => undefined);
|
|
299
|
-
await flush();
|
|
300
|
-
// The device answers the probe.
|
|
301
|
-
t.runPromise?.resolve('deadbeef');
|
|
302
|
-
await flush();
|
|
303
|
-
|
|
304
|
-
expect((t as any).probingProtocols.size).toBe(0);
|
|
305
|
-
});
|
|
306
|
-
});
|