@onekeyfe/hd-transport-react-native 1.2.0-alpha.67 → 1.2.0-alpha.69

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