@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.
@@ -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
- });