@onekeyfe/hd-transport 1.2.0-alpha.9 → 1.2.0-alpha.90
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/__tests__/messages.test.js +169 -11
- package/__tests__/protocol-v2-ble-frame-writer.test.js +119 -0
- package/__tests__/protocol-v2-link-manager.test.js +120 -9
- package/__tests__/protocol-v2-usb-transport-base.test.js +36 -2
- package/__tests__/protocol-v2.test.js +438 -64
- package/__tests__/transport-log.test.js +79 -0
- package/dist/constants.d.ts +3 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +707 -242
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +597 -296
- package/dist/protocols/index.d.ts +13 -6
- package/dist/protocols/index.d.ts.map +1 -1
- package/dist/protocols/v2/ble-frame-writer.d.ts +15 -0
- package/dist/protocols/v2/ble-frame-writer.d.ts.map +1 -0
- package/dist/protocols/v2/decode.d.ts +12 -2
- package/dist/protocols/v2/decode.d.ts.map +1 -1
- package/dist/protocols/v2/encode.d.ts +2 -3
- package/dist/protocols/v2/encode.d.ts.map +1 -1
- package/dist/protocols/v2/errors.d.ts +8 -0
- package/dist/protocols/v2/errors.d.ts.map +1 -0
- package/dist/protocols/v2/frame-assembler.d.ts.map +1 -1
- package/dist/protocols/v2/index.d.ts +2 -1
- package/dist/protocols/v2/index.d.ts.map +1 -1
- package/dist/protocols/v2/link-manager.d.ts +4 -0
- package/dist/protocols/v2/link-manager.d.ts.map +1 -1
- package/dist/protocols/v2/session.d.ts +10 -4
- package/dist/protocols/v2/session.d.ts.map +1 -1
- package/dist/protocols/v2/usb-transport-base.d.ts +1 -0
- package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -1
- package/dist/serialization/protobuf/decode.d.ts.map +1 -1
- package/dist/serialization/protobuf/encode.d.ts.map +1 -1
- package/dist/types/messages.d.ts +370 -144
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/transport.d.ts +20 -3
- package/dist/types/transport.d.ts.map +1 -1
- package/dist/utils/transportLog.d.ts +9 -0
- package/dist/utils/transportLog.d.ts.map +1 -0
- package/messages-protocol-v2.json +1113 -547
- package/package.json +3 -3
- package/scripts/protobuf-build.sh +85 -253
- package/scripts/protobuf-patches/index.js +1 -0
- package/scripts/protobuf-types.js +19 -1
- package/src/constants.ts +30 -16
- package/src/index.ts +5 -1
- package/src/protocols/index.ts +37 -40
- package/src/protocols/v2/ble-frame-writer.ts +77 -0
- package/src/protocols/v2/crc8.ts +1 -1
- package/src/protocols/v2/decode.ts +50 -35
- package/src/protocols/v2/encode.ts +2 -28
- package/src/protocols/v2/errors.ts +25 -0
- package/src/protocols/v2/frame-assembler.ts +6 -4
- package/src/protocols/v2/index.ts +2 -1
- package/src/protocols/v2/link-manager.ts +54 -5
- package/src/protocols/v2/session.ts +151 -200
- package/src/protocols/v2/usb-transport-base.ts +29 -10
- package/src/serialization/protobuf/decode.ts +4 -1
- package/src/serialization/protobuf/encode.ts +5 -1
- package/src/types/messages.ts +450 -185
- package/src/types/transport.ts +28 -3
- package/src/utils/transportLog.ts +102 -0
- package/dist/protocols/v2/debug.d.ts +0 -13
- package/dist/protocols/v2/debug.d.ts.map +0 -1
- package/src/protocols/v2/debug.ts +0 -26
|
@@ -3,9 +3,11 @@ const {
|
|
|
3
3
|
createMessageFromType,
|
|
4
4
|
parseConfigure,
|
|
5
5
|
} = require('../src/serialization/protobuf/messages');
|
|
6
|
-
const
|
|
6
|
+
const { encode } = require('../src/serialization/protobuf/encode');
|
|
7
|
+
const v1Messages = require('../../core/src/data/messages/messages.json');
|
|
7
8
|
const v2Messages = require('../messages-protocol-v2.json');
|
|
8
9
|
const coreV2Messages = require('../../core/src/data/messages/messages-protocol-v2.json');
|
|
10
|
+
const generatedTypes = require('../src/types/messages');
|
|
9
11
|
|
|
10
12
|
// const json = require('./data/messages.json');
|
|
11
13
|
|
|
@@ -62,11 +64,12 @@ const json = {
|
|
|
62
64
|
};
|
|
63
65
|
|
|
64
66
|
describe('messages', () => {
|
|
65
|
-
test('V1 GetPassphraseState
|
|
67
|
+
test('V1 GetPassphraseState matches the current firmware schema', () => {
|
|
66
68
|
const { fields } = v1Messages.nested.GetPassphraseState;
|
|
67
69
|
|
|
68
|
-
expect(fields.
|
|
69
|
-
expect(fields
|
|
70
|
+
expect(fields.passphrase_state).toMatchObject({ id: 1, type: 'string' });
|
|
71
|
+
expect(fields).not.toHaveProperty('_only_main_pin');
|
|
72
|
+
expect(fields).not.toHaveProperty('allow_create_attach_pin');
|
|
70
73
|
});
|
|
71
74
|
|
|
72
75
|
test('Protocol V2 firmware targets match the current firmware-pro2 enum', () => {
|
|
@@ -85,35 +88,190 @@ describe('messages', () => {
|
|
|
85
88
|
});
|
|
86
89
|
});
|
|
87
90
|
|
|
91
|
+
test('Protocol V2 conflicting enums keep their own wire values', () => {
|
|
92
|
+
expect(generatedTypes.ProtocolV2FailureType).toMatchObject({
|
|
93
|
+
Failure_DataError: 4,
|
|
94
|
+
Failure_ProcessError: 5,
|
|
95
|
+
});
|
|
96
|
+
expect(generatedTypes.Enum_ProtocolV2Capability).toMatchObject({
|
|
97
|
+
Capability_AttachToPin: 18,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
88
101
|
test('Protocol V2 device status and session messages match firmware-pro2', () => {
|
|
89
102
|
expect(v2Messages.nested.MessageType.values).toMatchObject({
|
|
90
103
|
MessageType_DeviceStatusGet: 60602,
|
|
91
104
|
MessageType_DeviceStatus: 60603,
|
|
92
|
-
MessageType_DeviceSessionGet:
|
|
93
|
-
MessageType_DeviceSession:
|
|
105
|
+
MessageType_DeviceSessionGet: 61200,
|
|
106
|
+
MessageType_DeviceSession: 61201,
|
|
107
|
+
MessageType_DeviceSessionAskPin: 61202,
|
|
108
|
+
MessageType_DeviceSessionAskPassphrase: 61203,
|
|
94
109
|
});
|
|
95
110
|
expect(v2Messages.nested.DeviceStatusGet).toEqual({ fields: {} });
|
|
111
|
+
expect(v2Messages.nested.ProtocolInfoRequest.fields.eventless_wallet_session).toMatchObject({
|
|
112
|
+
id: 1,
|
|
113
|
+
type: 'bool',
|
|
114
|
+
options: { default: false },
|
|
115
|
+
});
|
|
116
|
+
expect(v2Messages.nested.ProtocolInfo.fields).toMatchObject({
|
|
117
|
+
version: { id: 1, type: 'uint32', rule: 'required' },
|
|
118
|
+
build_fingerprint: { id: 2, type: 'string', rule: 'required' },
|
|
119
|
+
supported_messages: { id: 3, type: 'uint32', rule: 'repeated' },
|
|
120
|
+
});
|
|
96
121
|
expect(v2Messages.nested.DeviceSessionGet.fields.session_id).toMatchObject({
|
|
97
122
|
id: 1,
|
|
98
123
|
type: 'bytes',
|
|
99
124
|
});
|
|
125
|
+
expect(v2Messages.nested.DeviceSessionGet.fields).toEqual({
|
|
126
|
+
session_id: { id: 1, type: 'bytes' },
|
|
127
|
+
btc_test_address: { id: 2, type: 'string' },
|
|
128
|
+
seed_domains: {
|
|
129
|
+
id: 3,
|
|
130
|
+
rule: 'repeated',
|
|
131
|
+
type: 'DeviceSessionSeedDomain',
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
expect(v2Messages.nested.DeviceSessionSeedDomain.values).toEqual({
|
|
135
|
+
SeedDomain_Standard: 1,
|
|
136
|
+
SeedDomain_Cardano: 2,
|
|
137
|
+
});
|
|
138
|
+
expect(v2Messages.nested.DeviceSessionPinType.values).toEqual({
|
|
139
|
+
Any: 1,
|
|
140
|
+
Main: 2,
|
|
141
|
+
AttachToPin: 3,
|
|
142
|
+
});
|
|
143
|
+
expect(v2Messages.nested.DeviceSessionErrorCode.values).toEqual({
|
|
144
|
+
DeviceSessionError_None: 0,
|
|
145
|
+
DeviceSessionError_UserCancelled: 1,
|
|
146
|
+
DeviceSessionError_InvalidSession: 2,
|
|
147
|
+
DeviceSessionError_AttachPinUnavailable: 3,
|
|
148
|
+
DeviceSessionError_PassphraseDisabled: 4,
|
|
149
|
+
DeviceSessionError_Busy: 5,
|
|
150
|
+
});
|
|
151
|
+
expect(v2Messages.nested).not.toHaveProperty('DeviceWalletSelect');
|
|
152
|
+
expect(v2Messages.nested).not.toHaveProperty('DeviceWalletType');
|
|
153
|
+
expect(v2Messages.nested).not.toHaveProperty('DeviceHiddenWalletSelect');
|
|
100
154
|
expect(v2Messages.nested.DeviceSession.fields).toMatchObject({
|
|
101
155
|
session_id: { id: 1, type: 'bytes' },
|
|
102
156
|
btc_test_address: { id: 2, type: 'string' },
|
|
103
157
|
});
|
|
158
|
+
expect(v2Messages.nested.DeviceSessionAskPin.fields.type).toMatchObject({
|
|
159
|
+
id: 1,
|
|
160
|
+
type: 'DeviceSessionPinType',
|
|
161
|
+
});
|
|
162
|
+
expect(v2Messages.nested.DeviceSessionAskPassphrase).toEqual({
|
|
163
|
+
fields: {
|
|
164
|
+
passphrase: {
|
|
165
|
+
type: 'string',
|
|
166
|
+
id: 1,
|
|
167
|
+
},
|
|
168
|
+
on_device: {
|
|
169
|
+
rule: 'required',
|
|
170
|
+
type: 'bool',
|
|
171
|
+
id: 2,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
expect(v2Messages.nested.DeviceSessionAskPin_FailureSubCodes.values).toEqual({
|
|
176
|
+
UserCancel: 1,
|
|
177
|
+
});
|
|
178
|
+
expect(v2Messages.nested.MessageType.values).not.toHaveProperty(
|
|
179
|
+
'MessageType_DeviceSessionPinResult'
|
|
180
|
+
);
|
|
181
|
+
expect(v2Messages.nested.MessageType.values).not.toHaveProperty(
|
|
182
|
+
'MessageType_DeviceSessionOpen'
|
|
183
|
+
);
|
|
104
184
|
});
|
|
105
185
|
|
|
106
|
-
test('Protocol V2
|
|
186
|
+
test('Protocol V2 passphrase selection explicitly selects host or device input on wire', () => {
|
|
187
|
+
const messages = parseConfigure(v2Messages);
|
|
188
|
+
const { Message } = createMessageFromName(messages, 'DeviceSessionAskPassphrase');
|
|
189
|
+
|
|
190
|
+
const standardWallet = encode(Message, { passphrase: '', on_device: false });
|
|
191
|
+
const onHost = Message.encode(
|
|
192
|
+
Message.create({ passphrase: 'host hidden wallet', on_device: false })
|
|
193
|
+
).finish();
|
|
194
|
+
const onDevice = Message.encode(Message.create({ on_device: true })).finish();
|
|
195
|
+
|
|
196
|
+
expect(standardWallet.toString('hex')).toBe('0a001000');
|
|
197
|
+
expect(Buffer.from(onHost).toString('hex')).toBe(
|
|
198
|
+
'0a12686f73742068696464656e2077616c6c65741000'
|
|
199
|
+
);
|
|
200
|
+
expect(Buffer.from(onDevice).toString('hex')).toBe('1001');
|
|
201
|
+
expect(Message.decode(onHost)).toMatchObject({
|
|
202
|
+
passphrase: 'host hidden wallet',
|
|
203
|
+
on_device: false,
|
|
204
|
+
});
|
|
205
|
+
expect(Message.decode(onDevice)).toMatchObject({ on_device: true });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('Protocol V2 wallet recovery carries the expected wallet and seed domains on wire', () => {
|
|
209
|
+
const messages = parseConfigure(v2Messages);
|
|
210
|
+
const { Message } = createMessageFromName(messages, 'DeviceSessionGet');
|
|
211
|
+
const payload = encode(Message, {
|
|
212
|
+
btc_test_address: 'tb1qwallet',
|
|
213
|
+
seed_domains: [
|
|
214
|
+
generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard,
|
|
215
|
+
generatedTypes.DeviceSessionSeedDomain.SeedDomain_Cardano,
|
|
216
|
+
],
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(payload.toString('hex')).toBe('120a7462317177616c6c65741a020102');
|
|
220
|
+
expect(Message.decode(payload.toBuffer())).toMatchObject({
|
|
221
|
+
btc_test_address: 'tb1qwallet',
|
|
222
|
+
seed_domains: [
|
|
223
|
+
generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard,
|
|
224
|
+
generatedTypes.DeviceSessionSeedDomain.SeedDomain_Cardano,
|
|
225
|
+
],
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('Protocol V2 onboarding status matches the current firmware-pro2 schema', () => {
|
|
107
230
|
expect(v2Messages.nested.MessageType.values).toMatchObject({
|
|
108
|
-
|
|
109
|
-
|
|
231
|
+
MessageType_OnboardingStatusGet: 61600,
|
|
232
|
+
MessageType_OnboardingStatus: 61601,
|
|
233
|
+
});
|
|
234
|
+
expect(v2Messages.nested.OnboardingStep.values).toMatchObject({
|
|
235
|
+
ONBOARDING_STEP_UNKNOWN: 0,
|
|
236
|
+
ONBOARDING_STEP_CHECKING: 1,
|
|
237
|
+
ONBOARDING_STEP_PERSONALIZATION: 2,
|
|
238
|
+
ONBOARDING_STEP_PIN: 3,
|
|
239
|
+
ONBOARDING_STEP_SETUP: 4,
|
|
240
|
+
ONBOARDING_STEP_DONE: 5,
|
|
241
|
+
});
|
|
242
|
+
expect(v2Messages.nested.OnboardingPhase.values).toBeDefined();
|
|
243
|
+
expect(v2Messages.nested.OnboardingSetupKind.values).toBeDefined();
|
|
244
|
+
expect(v2Messages.nested.OnboardingSetupMethod.values).toBeDefined();
|
|
245
|
+
expect(v2Messages.nested.OnboardingSetupStatus.fields).toMatchObject({
|
|
246
|
+
kind: { id: 1, type: 'OnboardingSetupKind' },
|
|
247
|
+
method: { id: 2, type: 'OnboardingSetupMethod' },
|
|
248
|
+
});
|
|
249
|
+
expect(v2Messages.nested.OnboardingStatus.fields).toMatchObject({
|
|
250
|
+
step: { id: 1, type: 'OnboardingStep' },
|
|
251
|
+
phase: { id: 2, type: 'OnboardingPhase' },
|
|
252
|
+
setup: { id: 3, type: 'OnboardingSetupStatus' },
|
|
253
|
+
pin_set: { id: 4, type: 'bool' },
|
|
254
|
+
wallet_initialized: { id: 5, type: 'bool' },
|
|
255
|
+
});
|
|
256
|
+
expect(v2Messages.nested).not.toHaveProperty('DevOnboardingStatus');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('Protocol V2 NFT update matches the current firmware-pro2 schema', () => {
|
|
260
|
+
expect(v2Messages.nested.MessageType.values.MessageType_NftUpdate).toBe(61500);
|
|
261
|
+
expect(v2Messages.nested.NftUpdate.fields).toEqual({
|
|
262
|
+
file_name_no_ext: { id: 1, type: 'string', rule: 'required' },
|
|
110
263
|
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('Protocol V2 does not restore retired unlock or passphrase ids', () => {
|
|
267
|
+
expect(v2Messages.nested.MessageType.values).not.toHaveProperty('MessageType_UnLockDevice');
|
|
268
|
+
expect(v2Messages.nested.MessageType.values).not.toHaveProperty(
|
|
269
|
+
'MessageType_UnLockDeviceResponse'
|
|
270
|
+
);
|
|
111
271
|
expect(v2Messages.nested.MessageType.values).not.toHaveProperty(
|
|
112
272
|
'MessageType_GetPassphraseState'
|
|
113
273
|
);
|
|
114
274
|
expect(v2Messages.nested.MessageType.values).not.toHaveProperty('MessageType_PassphraseState');
|
|
115
|
-
expect(v2Messages.nested.UnLockDevice).toBeDefined();
|
|
116
|
-
expect(v2Messages.nested.UnLockDeviceResponse).toBeDefined();
|
|
117
275
|
});
|
|
118
276
|
|
|
119
277
|
test('Protocol V2 transport and core schemas stay identical', () => {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
2
|
+
const { writeProtocolV2BleFrame } = require('../src');
|
|
3
|
+
|
|
4
|
+
describe('Protocol V2 BLE frame writer', () => {
|
|
5
|
+
test('splits one complete Protocol V2 frame without changing packet order', async () => {
|
|
6
|
+
const packets = [];
|
|
7
|
+
|
|
8
|
+
await writeProtocolV2BleFrame({
|
|
9
|
+
frame: new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
|
10
|
+
packetCapacity: 4,
|
|
11
|
+
writePacket: packet => {
|
|
12
|
+
packets.push(Array.from(packet));
|
|
13
|
+
return Promise.resolve();
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
expect(packets).toEqual([
|
|
18
|
+
[0, 1, 2, 3],
|
|
19
|
+
[4, 5, 6, 7],
|
|
20
|
+
[8, 9],
|
|
21
|
+
]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('stops before the next packet when the connection generation changes', async () => {
|
|
25
|
+
const packets = [];
|
|
26
|
+
let active = true;
|
|
27
|
+
|
|
28
|
+
await expect(
|
|
29
|
+
writeProtocolV2BleFrame({
|
|
30
|
+
frame: new Uint8Array(9),
|
|
31
|
+
packetCapacity: 4,
|
|
32
|
+
assertActive: () => {
|
|
33
|
+
if (!active) throw new Error('stale BLE generation');
|
|
34
|
+
},
|
|
35
|
+
writePacket: packet => {
|
|
36
|
+
packets.push(packet);
|
|
37
|
+
active = false;
|
|
38
|
+
return Promise.resolve();
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
).rejects.toThrow('stale BLE generation');
|
|
42
|
+
|
|
43
|
+
expect(packets).toHaveLength(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('does not write when the call is already aborted', async () => {
|
|
47
|
+
const controller = new AbortController();
|
|
48
|
+
const writePacket = jest.fn();
|
|
49
|
+
controller.abort();
|
|
50
|
+
|
|
51
|
+
await expect(
|
|
52
|
+
writeProtocolV2BleFrame({
|
|
53
|
+
frame: new Uint8Array(4),
|
|
54
|
+
packetCapacity: 4,
|
|
55
|
+
signal: controller.signal,
|
|
56
|
+
abortMessage: 'Protocol V2 BLE write aborted for Ping',
|
|
57
|
+
writePacket,
|
|
58
|
+
})
|
|
59
|
+
).rejects.toThrow('Protocol V2 BLE write aborted for Ping');
|
|
60
|
+
|
|
61
|
+
expect(writePacket).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('surfaces an abort that occurs while the final packet write is completing', async () => {
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const writePacket = jest.fn().mockImplementation(() => {
|
|
67
|
+
controller.abort();
|
|
68
|
+
return Promise.resolve();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await expect(
|
|
72
|
+
writeProtocolV2BleFrame({
|
|
73
|
+
frame: new Uint8Array(4),
|
|
74
|
+
packetCapacity: 4,
|
|
75
|
+
signal: controller.signal,
|
|
76
|
+
abortMessage: 'Protocol V2 BLE write aborted for Ping',
|
|
77
|
+
writePacket,
|
|
78
|
+
})
|
|
79
|
+
).rejects.toThrow('Protocol V2 BLE write aborted for Ping');
|
|
80
|
+
|
|
81
|
+
expect(writePacket).toHaveBeenCalledTimes(1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('applies bounded burst pauses and one final flush delay', async () => {
|
|
85
|
+
const waits = [];
|
|
86
|
+
const writePacket = jest.fn().mockResolvedValue(undefined);
|
|
87
|
+
|
|
88
|
+
await writeProtocolV2BleFrame({
|
|
89
|
+
frame: new Uint8Array(10),
|
|
90
|
+
packetCapacity: 3,
|
|
91
|
+
initialDelayMs: 3,
|
|
92
|
+
burstSize: 2,
|
|
93
|
+
burstPauseMs: 7,
|
|
94
|
+
flushDelayMs: 11,
|
|
95
|
+
wait: ms => {
|
|
96
|
+
waits.push(ms);
|
|
97
|
+
return Promise.resolve();
|
|
98
|
+
},
|
|
99
|
+
writePacket,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(writePacket).toHaveBeenCalledTimes(4);
|
|
103
|
+
expect(waits).toEqual([3, 7, 11]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('rejects an invalid packet capacity before writing', async () => {
|
|
107
|
+
const writePacket = jest.fn();
|
|
108
|
+
|
|
109
|
+
await expect(
|
|
110
|
+
writeProtocolV2BleFrame({
|
|
111
|
+
frame: new Uint8Array(4),
|
|
112
|
+
packetCapacity: 0,
|
|
113
|
+
writePacket,
|
|
114
|
+
})
|
|
115
|
+
).rejects.toThrow('Protocol V2 BLE packet capacity must be a positive integer');
|
|
116
|
+
|
|
117
|
+
expect(writePacket).not.toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -55,7 +55,12 @@ const rewriteSeq = (frame, seq) => {
|
|
|
55
55
|
const createAdapterFactory = sentSeqs => {
|
|
56
56
|
const adapters = [];
|
|
57
57
|
let generation = 0;
|
|
58
|
-
const success = ProtocolV2.encodeFrame(
|
|
58
|
+
const success = ProtocolV2.encodeFrame(
|
|
59
|
+
schemas,
|
|
60
|
+
'Success',
|
|
61
|
+
{ message: 'ok' },
|
|
62
|
+
{ router: 1, packetSrc: 0, seq: 1 }
|
|
63
|
+
);
|
|
59
64
|
|
|
60
65
|
return {
|
|
61
66
|
adapters,
|
|
@@ -108,12 +113,15 @@ describe('ProtocolV2LinkManager', () => {
|
|
|
108
113
|
|
|
109
114
|
await manager.call('device-a', createAdapter, 'Ping', { message: '1' }, { timeoutMs: 123 });
|
|
110
115
|
|
|
111
|
-
expect(adapters[0].prepareCall).toHaveBeenCalledWith(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
expect(adapters[0].prepareCall).toHaveBeenCalledWith(
|
|
117
|
+
expect.objectContaining({
|
|
118
|
+
messageName: 'Ping',
|
|
119
|
+
timeoutMs: 123,
|
|
120
|
+
highThroughput: false,
|
|
121
|
+
generation: 1,
|
|
122
|
+
signal: expect.any(AbortSignal),
|
|
123
|
+
})
|
|
124
|
+
);
|
|
117
125
|
});
|
|
118
126
|
|
|
119
127
|
test('invalidates a link-fatal call before the next call creates a new link', async () => {
|
|
@@ -178,6 +186,54 @@ describe('ProtocolV2LinkManager', () => {
|
|
|
178
186
|
expect(adapters[1].reset).toHaveBeenCalledWith('schema changed');
|
|
179
187
|
});
|
|
180
188
|
|
|
189
|
+
test('waits for an in-flight native cleanup before a concurrent invalidation resolves', async () => {
|
|
190
|
+
const sentSeqs = [];
|
|
191
|
+
const { adapters, createAdapter } = createAdapterFactory(sentSeqs);
|
|
192
|
+
let markCleanupStarted;
|
|
193
|
+
let finishCleanup;
|
|
194
|
+
const cleanupStarted = new Promise(resolve => {
|
|
195
|
+
markCleanupStarted = resolve;
|
|
196
|
+
});
|
|
197
|
+
const cleanupBlocked = new Promise(resolve => {
|
|
198
|
+
finishCleanup = resolve;
|
|
199
|
+
});
|
|
200
|
+
const onLinkInvalidated = jest.fn(async () => {
|
|
201
|
+
markCleanupStarted();
|
|
202
|
+
await cleanupBlocked;
|
|
203
|
+
});
|
|
204
|
+
const manager = new ProtocolV2LinkManager({
|
|
205
|
+
getSchemas: () => schemas,
|
|
206
|
+
classifyError: () => 'recoverable',
|
|
207
|
+
onLinkInvalidated,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await manager.call('device-a', createAdapter, 'Ping', { message: '1' });
|
|
211
|
+
|
|
212
|
+
const release = manager.invalidateLink('device-a', 'release');
|
|
213
|
+
await cleanupStarted;
|
|
214
|
+
|
|
215
|
+
let acquireSettled = false;
|
|
216
|
+
const acquire = manager.invalidateLink('device-a', 'acquire').then(() => {
|
|
217
|
+
acquireSettled = true;
|
|
218
|
+
});
|
|
219
|
+
await new Promise(resolve => {
|
|
220
|
+
setImmediate(resolve);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(acquireSettled).toBe(false);
|
|
224
|
+
expect(adapters[0].reset).toHaveBeenCalledTimes(1);
|
|
225
|
+
expect(onLinkInvalidated).toHaveBeenCalledTimes(1);
|
|
226
|
+
|
|
227
|
+
finishCleanup();
|
|
228
|
+
await Promise.all([release, acquire]);
|
|
229
|
+
|
|
230
|
+
await manager.call('device-a', createAdapter, 'Ping', { message: '2' });
|
|
231
|
+
|
|
232
|
+
expect(acquireSettled).toBe(true);
|
|
233
|
+
expect(sentSeqs).toEqual([1, 2]);
|
|
234
|
+
expect(adapters).toHaveLength(2);
|
|
235
|
+
});
|
|
236
|
+
|
|
181
237
|
test('dispose clears active links and sequence cursors', async () => {
|
|
182
238
|
const sentSeqs = [];
|
|
183
239
|
const { adapters, createAdapter } = createAdapterFactory(sentSeqs);
|
|
@@ -200,7 +256,12 @@ describe('ProtocolV2LinkManager', () => {
|
|
|
200
256
|
const deviceABlocked = new Promise(resolve => {
|
|
201
257
|
releaseDeviceA = resolve;
|
|
202
258
|
});
|
|
203
|
-
const success = ProtocolV2.encodeFrame(
|
|
259
|
+
const success = ProtocolV2.encodeFrame(
|
|
260
|
+
schemas,
|
|
261
|
+
'Success',
|
|
262
|
+
{ message: 'ok' },
|
|
263
|
+
{ router: 1, packetSrc: 0, seq: 1 }
|
|
264
|
+
);
|
|
204
265
|
const createAdapter = key => {
|
|
205
266
|
let requestSeq = 0;
|
|
206
267
|
return {
|
|
@@ -298,7 +359,12 @@ describe('ProtocolV2LinkManager', () => {
|
|
|
298
359
|
const writeBlocked = new Promise(resolve => {
|
|
299
360
|
releaseWrite = resolve;
|
|
300
361
|
});
|
|
301
|
-
const success = ProtocolV2.encodeFrame(
|
|
362
|
+
const success = ProtocolV2.encodeFrame(
|
|
363
|
+
schemas,
|
|
364
|
+
'Success',
|
|
365
|
+
{ message: 'ok' },
|
|
366
|
+
{ router: 1, packetSrc: 0, seq: 1 }
|
|
367
|
+
);
|
|
302
368
|
const adapter = {
|
|
303
369
|
router: 1,
|
|
304
370
|
generation: 1,
|
|
@@ -323,4 +389,49 @@ describe('ProtocolV2LinkManager', () => {
|
|
|
323
389
|
await expect(call).rejects.toThrow('device disconnected');
|
|
324
390
|
expect(adapter.readFrame).not.toHaveBeenCalled();
|
|
325
391
|
});
|
|
392
|
+
|
|
393
|
+
test('rejects calls queued before their link generation is invalidated', async () => {
|
|
394
|
+
let releaseWrite;
|
|
395
|
+
let markWriteStarted;
|
|
396
|
+
const writeStarted = new Promise(resolve => {
|
|
397
|
+
markWriteStarted = resolve;
|
|
398
|
+
});
|
|
399
|
+
const writeBlocked = new Promise(resolve => {
|
|
400
|
+
releaseWrite = resolve;
|
|
401
|
+
});
|
|
402
|
+
const success = ProtocolV2.encodeFrame(
|
|
403
|
+
schemas,
|
|
404
|
+
'Success',
|
|
405
|
+
{ message: 'ok' },
|
|
406
|
+
{ router: 1, packetSrc: 0, seq: 1 }
|
|
407
|
+
);
|
|
408
|
+
const adapter = {
|
|
409
|
+
router: 1,
|
|
410
|
+
generation: 1,
|
|
411
|
+
prepareCall: jest.fn(),
|
|
412
|
+
writeFrame: jest.fn(async () => {
|
|
413
|
+
markWriteStarted();
|
|
414
|
+
await writeBlocked;
|
|
415
|
+
}),
|
|
416
|
+
readFrame: jest.fn(() => Promise.resolve(success)),
|
|
417
|
+
reset: jest.fn(),
|
|
418
|
+
};
|
|
419
|
+
const createAdapter = jest.fn(() => adapter);
|
|
420
|
+
const manager = new ProtocolV2LinkManager({
|
|
421
|
+
getSchemas: () => schemas,
|
|
422
|
+
classifyError: () => 'recoverable',
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const activeCall = manager.call('device-a', createAdapter, 'Ping', { message: 'active' });
|
|
426
|
+
await writeStarted;
|
|
427
|
+
const queuedCall = manager.call('device-a', createAdapter, 'Ping', { message: 'queued' });
|
|
428
|
+
await manager.invalidateLink('device-a', 'device released');
|
|
429
|
+
releaseWrite();
|
|
430
|
+
|
|
431
|
+
await expect(activeCall).rejects.toThrow('device released');
|
|
432
|
+
await expect(queuedCall).rejects.toThrow('device released');
|
|
433
|
+
expect(createAdapter).toHaveBeenCalledTimes(1);
|
|
434
|
+
expect(adapter.writeFrame).toHaveBeenCalledTimes(1);
|
|
435
|
+
expect(adapter.readFrame).not.toHaveBeenCalled();
|
|
436
|
+
});
|
|
326
437
|
});
|
|
@@ -65,6 +65,7 @@ class FakeUsbTransport extends ProtocolV2UsbTransportBase {
|
|
|
65
65
|
this.nextReadError = undefined;
|
|
66
66
|
this.writeBlock = undefined;
|
|
67
67
|
this.readBlock = undefined;
|
|
68
|
+
this.coalesceNextResponses = false;
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
callDevice(key, message, timeoutMs) {
|
|
@@ -118,6 +119,10 @@ class FakeUsbTransport extends ProtocolV2UsbTransportBase {
|
|
|
118
119
|
return { started, release };
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
coalesceNextTwoResponses() {
|
|
123
|
+
this.coalesceNextResponses = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
121
126
|
getProtocolV2UsbSchemas() {
|
|
122
127
|
return schemas;
|
|
123
128
|
}
|
|
@@ -135,8 +140,22 @@ class FakeUsbTransport extends ProtocolV2UsbTransportBase {
|
|
|
135
140
|
{ message: 'ok' },
|
|
136
141
|
{ router: PROTOCOL_V2_CHANNEL_USB, seq }
|
|
137
142
|
);
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
if (this.coalesceNextResponses) {
|
|
144
|
+
this.coalesceNextResponses = false;
|
|
145
|
+
const nextResponse = ProtocolV2.encodeFrame(
|
|
146
|
+
schemas,
|
|
147
|
+
'Success',
|
|
148
|
+
{ message: 'ok' },
|
|
149
|
+
{ router: PROTOCOL_V2_CHANNEL_USB, seq: Number(seq) + 1 }
|
|
150
|
+
);
|
|
151
|
+
const packet = new Uint8Array(response.length + nextResponse.length);
|
|
152
|
+
packet.set(response);
|
|
153
|
+
packet.set(nextResponse, response.length);
|
|
154
|
+
this.packetQueues.set(key, [packet]);
|
|
155
|
+
} else {
|
|
156
|
+
const splitAt = Math.min(4, response.length);
|
|
157
|
+
this.packetQueues.set(key, [response.slice(0, splitAt), response.slice(splitAt)]);
|
|
158
|
+
}
|
|
140
159
|
|
|
141
160
|
const block = this.writeBlock;
|
|
142
161
|
if (block) {
|
|
@@ -215,6 +234,21 @@ describe('ProtocolV2UsbTransportBase', () => {
|
|
|
215
234
|
]);
|
|
216
235
|
});
|
|
217
236
|
|
|
237
|
+
test('keeps a coalesced response buffered for the next call', async () => {
|
|
238
|
+
const transport = new FakeUsbTransport();
|
|
239
|
+
await transport.rotate('device-a');
|
|
240
|
+
transport.coalesceNextTwoResponses();
|
|
241
|
+
|
|
242
|
+
await transport.callDevice('device-a', 'first');
|
|
243
|
+
await transport.callDevice('device-a', 'second');
|
|
244
|
+
|
|
245
|
+
expect(transport.readCounts.get('device-a')).toBe(1);
|
|
246
|
+
expect(transport.sentSeqs).toEqual([
|
|
247
|
+
['device-a', 1],
|
|
248
|
+
['device-a', 2],
|
|
249
|
+
]);
|
|
250
|
+
});
|
|
251
|
+
|
|
218
252
|
test('does not block a different USB path', async () => {
|
|
219
253
|
const transport = new FakeUsbTransport();
|
|
220
254
|
await transport.rotate('device-a');
|