@onekeyfe/hd-transport-react-native 1.2.3-alpha.3 → 1.2.3-alpha.5
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/README.md +1 -1
- package/dist/BleManager.d.ts +1 -1
- package/dist/BleManager.d.ts.map +1 -1
- package/dist/bleNativeDisconnect.d.ts +3 -0
- package/dist/bleNativeDisconnect.d.ts.map +1 -0
- package/dist/index.d.ts +35 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +634 -152
- package/dist/subscribeBleOn.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/BleManager.ts +73 -5
- package/src/__tests__/bleNativeDisconnect.test.ts +54 -0
- package/src/__tests__/connectTimeout.test.ts +465 -3
- package/src/__tests__/enumerate.test.ts +36 -1
- package/src/__tests__/protocolV2Link.test.ts +976 -62
- package/src/__tests__/staleCallTimeout.test.ts +34 -5
- package/src/__tests__/subscribeBleOn.test.ts +111 -0
- package/src/bleNativeDisconnect.ts +41 -0
- package/src/index.ts +647 -159
- package/src/subscribeBleOn.ts +26 -10
|
@@ -56,6 +56,34 @@ function createHarness() {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
describe('Protocol V1 stale call timeout', () => {
|
|
59
|
+
test('settles a cancelled read and waits for native teardown before completing cancel', async () => {
|
|
60
|
+
const { t, disconnectSpy } = createHarness();
|
|
61
|
+
let finishDisconnect!: () => void;
|
|
62
|
+
disconnectSpy.mockImplementationOnce(
|
|
63
|
+
() =>
|
|
64
|
+
new Promise<void>(resolve => {
|
|
65
|
+
finishDisconnect = resolve;
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
const call = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
69
|
+
const result = call.catch(error => error);
|
|
70
|
+
await flush();
|
|
71
|
+
let cancelled = false;
|
|
72
|
+
const cleanup = t.cancel().then(() => {
|
|
73
|
+
cancelled = true;
|
|
74
|
+
});
|
|
75
|
+
await flush();
|
|
76
|
+
expect(await result).toMatchObject({ errorCode: HardwareErrorCode.CallQueueActionCancelled });
|
|
77
|
+
expect(t.runPromise).toBeNull();
|
|
78
|
+
expect(disconnectSpy).toHaveBeenCalledWith(UUID);
|
|
79
|
+
expect(cancelled).toBe(false);
|
|
80
|
+
finishDisconnect();
|
|
81
|
+
await cleanup;
|
|
82
|
+
jest.advanceTimersByTime(25000);
|
|
83
|
+
await flush();
|
|
84
|
+
expect(disconnectSpy).toHaveBeenCalledTimes(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
59
87
|
beforeAll(() => {
|
|
60
88
|
jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
|
|
61
89
|
});
|
|
@@ -172,24 +200,25 @@ describe('Protocol V1 stale call timeout', () => {
|
|
|
172
200
|
expect(secondErrors).toHaveLength(1);
|
|
173
201
|
});
|
|
174
202
|
|
|
175
|
-
test('
|
|
203
|
+
test('a cancelled read cannot disconnect a later transport when its old deadline passes', async () => {
|
|
176
204
|
const { t, disconnectSpy } = createHarness();
|
|
177
205
|
|
|
178
|
-
// cancel() nulls the ownership slot without settling the deferred, so the
|
|
179
|
-
// call's response timer stays armed (reachable via DeviceCommands.dispose).
|
|
180
206
|
const errors: Array<{ errorCode?: unknown }> = [];
|
|
181
207
|
const p = t.call(UUID, 'GetFeatures', {}, { timeoutMs: 5000 });
|
|
182
208
|
p.catch(e => errors.push(e));
|
|
183
209
|
await flush();
|
|
184
210
|
|
|
185
|
-
t.cancel();
|
|
211
|
+
await t.cancel();
|
|
212
|
+
await flush();
|
|
213
|
+
expect(disconnectSpy).toHaveBeenCalledTimes(1);
|
|
214
|
+
disconnectSpy.mockClear();
|
|
186
215
|
|
|
187
216
|
jest.advanceTimersByTime(5000);
|
|
188
217
|
await flush();
|
|
189
218
|
|
|
190
219
|
expect(disconnectSpy).not.toHaveBeenCalled();
|
|
191
220
|
expect(errors).toHaveLength(1);
|
|
192
|
-
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.
|
|
221
|
+
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.CallQueueActionCancelled);
|
|
193
222
|
});
|
|
194
223
|
|
|
195
224
|
test('timeout on the active call still disconnects the transport', async () => {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
3
|
+
import { subscribeBleOn } from '../subscribeBleOn';
|
|
4
|
+
|
|
5
|
+
import type { BlePlxManager } from '../types';
|
|
6
|
+
|
|
7
|
+
type BleState = Parameters<Parameters<BlePlxManager['onStateChange']>[0]>[0];
|
|
8
|
+
|
|
9
|
+
const createBleManager = () => {
|
|
10
|
+
let stateListener: ((state: BleState) => void) | undefined;
|
|
11
|
+
const remove = jest.fn();
|
|
12
|
+
const bleManager = {
|
|
13
|
+
onStateChange: jest.fn(listener => {
|
|
14
|
+
stateListener = listener;
|
|
15
|
+
return { remove };
|
|
16
|
+
}),
|
|
17
|
+
} as unknown as BlePlxManager;
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
bleManager,
|
|
21
|
+
emitState: (state: BleState) => stateListener?.(state),
|
|
22
|
+
remove,
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe('subscribeBleOn', () => {
|
|
27
|
+
beforeAll(() => {
|
|
28
|
+
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterAll(() => {
|
|
32
|
+
jest.useRealTimers();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
jest.clearAllTimers();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('resolves when Bluetooth is powered on', async () => {
|
|
40
|
+
const { bleManager, emitState, remove } = createBleManager();
|
|
41
|
+
const result = subscribeBleOn(bleManager);
|
|
42
|
+
|
|
43
|
+
emitState('PoweredOn');
|
|
44
|
+
|
|
45
|
+
await expect(result).resolves.toBeUndefined();
|
|
46
|
+
expect(remove).toHaveBeenCalledTimes(1);
|
|
47
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('cleans up when the current state is emitted synchronously', async () => {
|
|
51
|
+
const remove = jest.fn();
|
|
52
|
+
const bleManager = {
|
|
53
|
+
onStateChange: jest.fn(listener => {
|
|
54
|
+
listener('PoweredOn');
|
|
55
|
+
return { remove };
|
|
56
|
+
}),
|
|
57
|
+
} as unknown as BlePlxManager;
|
|
58
|
+
|
|
59
|
+
await expect(subscribeBleOn(bleManager)).resolves.toBeUndefined();
|
|
60
|
+
expect(remove).toHaveBeenCalledTimes(1);
|
|
61
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test.each([
|
|
65
|
+
['PoweredOff', HardwareErrorCode.BlePoweredOff],
|
|
66
|
+
['Unsupported', HardwareErrorCode.BleUnsupported],
|
|
67
|
+
['Unauthorized', HardwareErrorCode.BlePermissionError],
|
|
68
|
+
] as const)('maps %s to hardware error %s', async (state, errorCode) => {
|
|
69
|
+
const { bleManager, emitState, remove } = createBleManager();
|
|
70
|
+
const result = subscribeBleOn(bleManager);
|
|
71
|
+
const rejection = expect(result).rejects.toMatchObject({ errorCode });
|
|
72
|
+
|
|
73
|
+
emitState(state);
|
|
74
|
+
|
|
75
|
+
await rejection;
|
|
76
|
+
expect(remove).toHaveBeenCalledTimes(1);
|
|
77
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('settles only once when terminal states repeat', async () => {
|
|
81
|
+
const { bleManager, emitState, remove } = createBleManager();
|
|
82
|
+
const result = subscribeBleOn(bleManager);
|
|
83
|
+
const rejection = expect(result).rejects.toMatchObject({
|
|
84
|
+
errorCode: HardwareErrorCode.BlePoweredOff,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
emitState('PoweredOff');
|
|
88
|
+
emitState('PoweredOn');
|
|
89
|
+
|
|
90
|
+
await rejection;
|
|
91
|
+
expect(remove).toHaveBeenCalledTimes(1);
|
|
92
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test.each(['Unknown', 'Resetting'] as const)(
|
|
96
|
+
'reports %s as a scan error if no stable state arrives before timeout',
|
|
97
|
+
async state => {
|
|
98
|
+
const { bleManager, emitState, remove } = createBleManager();
|
|
99
|
+
const result = subscribeBleOn(bleManager, 1000);
|
|
100
|
+
const rejection = expect(result).rejects.toMatchObject({
|
|
101
|
+
errorCode: HardwareErrorCode.BleScanError,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
emitState(state);
|
|
105
|
+
jest.advanceTimersByTime(1000);
|
|
106
|
+
|
|
107
|
+
await rejection;
|
|
108
|
+
expect(remove).toHaveBeenCalledTimes(1);
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
3
|
+
const BLE_PLX_DEVICE_DISCONNECTED = 201;
|
|
4
|
+
const IOS_PERIPHERAL_DISCONNECTED = 7;
|
|
5
|
+
|
|
6
|
+
type NativeBleErrorFields = {
|
|
7
|
+
errorCode?: unknown;
|
|
8
|
+
iosErrorCode?: unknown;
|
|
9
|
+
reason?: unknown;
|
|
10
|
+
message?: unknown;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const nativeErrorText = (error: NativeBleErrorFields) =>
|
|
14
|
+
[error.reason, error.message]
|
|
15
|
+
.filter((value): value is string => typeof value === 'string')
|
|
16
|
+
.join(' ');
|
|
17
|
+
|
|
18
|
+
export const isNativeBleDisconnectError = (error: unknown): boolean => {
|
|
19
|
+
if (!error || typeof error !== 'object') {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const nativeError = error as NativeBleErrorFields;
|
|
24
|
+
return (
|
|
25
|
+
nativeError.errorCode === BLE_PLX_DEVICE_DISCONNECTED ||
|
|
26
|
+
nativeError.iosErrorCode === IOS_PERIPHERAL_DISCONNECTED ||
|
|
27
|
+
nativeErrorText(nativeError).toLowerCase().includes('was disconnected')
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const toBleDisconnectHardwareError = (error: unknown) => {
|
|
32
|
+
if ((error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleDeviceDisconnected) {
|
|
33
|
+
return error as Error;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const nativeError = (error ?? {}) as NativeBleErrorFields;
|
|
37
|
+
return ERRORS.TypedError(
|
|
38
|
+
HardwareErrorCode.BleDeviceDisconnected,
|
|
39
|
+
nativeErrorText(nativeError) || undefined
|
|
40
|
+
);
|
|
41
|
+
};
|