@onekeyfe/hd-transport 1.2.0-alpha.18 → 1.2.0-alpha.181
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 +137 -32
- package/__tests__/protocol-v2-ble-frame-writer.test.js +119 -0
- package/__tests__/protocol-v2-link-manager.test.js +246 -9
- package/__tests__/protocol-v2-usb-transport-base.test.js +42 -6
- package/__tests__/protocol-v2.test.js +632 -62
- 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 +539 -285
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +718 -160
- package/dist/protocols/index.d.ts +17 -1
- 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 +11 -0
- package/dist/protocols/v2/decode.d.ts.map +1 -1
- package/dist/protocols/v2/errors.d.ts +16 -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 -0
- package/dist/protocols/v2/index.d.ts.map +1 -1
- package/dist/protocols/v2/link-manager.d.ts +6 -0
- package/dist/protocols/v2/link-manager.d.ts.map +1 -1
- package/dist/protocols/v2/session.d.ts +16 -4
- package/dist/protocols/v2/session.d.ts.map +1 -1
- package/dist/protocols/v2/usb-transport-base.d.ts +2 -0
- package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -1
- package/dist/serialization/protobuf/encode.d.ts.map +1 -1
- package/dist/types/messages.d.ts +217 -202
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/transport.d.ts +21 -2
- 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 +515 -540
- package/package.json +2 -2
- package/scripts/protobuf-types.js +7 -1
- package/src/constants.ts +30 -16
- package/src/index.ts +5 -1
- package/src/protocols/index.ts +114 -3
- package/src/protocols/v2/ble-frame-writer.ts +77 -0
- package/src/protocols/v2/crc8.ts +1 -1
- package/src/protocols/v2/decode.ts +49 -12
- package/src/protocols/v2/encode.ts +1 -1
- package/src/protocols/v2/errors.ts +54 -0
- package/src/protocols/v2/frame-assembler.ts +6 -4
- package/src/protocols/v2/index.ts +2 -0
- package/src/protocols/v2/link-manager.ts +110 -9
- package/src/protocols/v2/session.ts +260 -87
- package/src/protocols/v2/usb-transport-base.ts +42 -10
- package/src/serialization/protobuf/encode.ts +5 -1
- package/src/types/messages.ts +269 -258
- package/src/types/transport.ts +39 -7
- package/src/utils/transportLog.ts +102 -0
|
@@ -3,6 +3,7 @@ const {
|
|
|
3
3
|
createMessageFromType,
|
|
4
4
|
parseConfigure,
|
|
5
5
|
} = require('../src/serialization/protobuf/messages');
|
|
6
|
+
const { encode } = require('../src/serialization/protobuf/encode');
|
|
6
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');
|
|
@@ -101,61 +102,165 @@ describe('messages', () => {
|
|
|
101
102
|
expect(v2Messages.nested.MessageType.values).toMatchObject({
|
|
102
103
|
MessageType_DeviceStatusGet: 60602,
|
|
103
104
|
MessageType_DeviceStatus: 60603,
|
|
104
|
-
|
|
105
|
-
MessageType_DeviceSession:
|
|
106
|
-
MessageType_DeviceSessionAskPin:
|
|
105
|
+
MessageType_DeviceSessionGet: 61200,
|
|
106
|
+
MessageType_DeviceSession: 61201,
|
|
107
|
+
MessageType_DeviceSessionAskPin: 61202,
|
|
108
|
+
MessageType_DeviceSessionAskPassphrase: 61203,
|
|
107
109
|
});
|
|
108
110
|
expect(v2Messages.nested.DeviceStatusGet).toEqual({ fields: {} });
|
|
109
|
-
expect(v2Messages.nested.
|
|
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
|
+
});
|
|
121
|
+
expect(v2Messages.nested.DeviceSessionGet.fields.session_id).toMatchObject({
|
|
110
122
|
id: 1,
|
|
111
123
|
type: 'bytes',
|
|
112
124
|
});
|
|
113
|
-
expect(v2Messages.nested.
|
|
114
|
-
'
|
|
115
|
-
'
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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');
|
|
119
152
|
expect(v2Messages.nested).not.toHaveProperty('DeviceWalletType');
|
|
120
153
|
expect(v2Messages.nested).not.toHaveProperty('DeviceHiddenWalletSelect');
|
|
121
154
|
expect(v2Messages.nested.DeviceSession.fields).toMatchObject({
|
|
122
155
|
session_id: { id: 1, type: 'bytes' },
|
|
123
156
|
btc_test_address: { id: 2, type: 'string' },
|
|
124
157
|
});
|
|
125
|
-
expect(v2Messages.nested.DeviceSessionAskPin).
|
|
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
|
+
});
|
|
126
175
|
expect(v2Messages.nested.DeviceSessionAskPin_FailureSubCodes.values).toEqual({
|
|
127
176
|
UserCancel: 1,
|
|
128
177
|
});
|
|
129
178
|
expect(v2Messages.nested.MessageType.values).not.toHaveProperty(
|
|
130
179
|
'MessageType_DeviceSessionPinResult'
|
|
131
180
|
);
|
|
132
|
-
expect(v2Messages.nested.MessageType.values).not.toHaveProperty(
|
|
181
|
+
expect(v2Messages.nested.MessageType.values).not.toHaveProperty(
|
|
182
|
+
'MessageType_DeviceSessionOpen'
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
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
|
+
});
|
|
133
227
|
});
|
|
134
228
|
|
|
135
229
|
test('Protocol V2 onboarding status matches the current firmware-pro2 schema', () => {
|
|
136
|
-
expect(v2Messages.nested.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
expect(v2Messages.nested.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
230
|
+
expect(v2Messages.nested.MessageType.values).toMatchObject({
|
|
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' },
|
|
155
253
|
pin_set: { id: 4, type: 'bool' },
|
|
156
254
|
wallet_initialized: { id: 5, type: 'bool' },
|
|
157
255
|
});
|
|
158
|
-
expect(v2Messages.nested).not.toHaveProperty('
|
|
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' },
|
|
263
|
+
});
|
|
159
264
|
});
|
|
160
265
|
|
|
161
266
|
test('Protocol V2 does not restore retired unlock or passphrase ids', () => {
|
|
@@ -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
|
+
});
|
|
@@ -26,6 +26,9 @@ const protocolV2Messages = parseConfigure({
|
|
|
26
26
|
message: { type: 'string', id: 1 },
|
|
27
27
|
},
|
|
28
28
|
},
|
|
29
|
+
Cancel: {
|
|
30
|
+
fields: {},
|
|
31
|
+
},
|
|
29
32
|
Success: {
|
|
30
33
|
fields: {
|
|
31
34
|
message: { type: 'string', id: 1 },
|
|
@@ -35,6 +38,7 @@ const protocolV2Messages = parseConfigure({
|
|
|
35
38
|
values: {
|
|
36
39
|
MessageType_Ping: 60206,
|
|
37
40
|
MessageType_Success: 60207,
|
|
41
|
+
MessageType_Cancel: 60004,
|
|
38
42
|
},
|
|
39
43
|
},
|
|
40
44
|
},
|
|
@@ -55,7 +59,12 @@ const rewriteSeq = (frame, seq) => {
|
|
|
55
59
|
const createAdapterFactory = sentSeqs => {
|
|
56
60
|
const adapters = [];
|
|
57
61
|
let generation = 0;
|
|
58
|
-
const success = ProtocolV2.encodeFrame(
|
|
62
|
+
const success = ProtocolV2.encodeFrame(
|
|
63
|
+
schemas,
|
|
64
|
+
'Success',
|
|
65
|
+
{ message: 'ok' },
|
|
66
|
+
{ router: 1, packetSrc: 0, seq: 1 }
|
|
67
|
+
);
|
|
59
68
|
|
|
60
69
|
return {
|
|
61
70
|
adapters,
|
|
@@ -108,12 +117,15 @@ describe('ProtocolV2LinkManager', () => {
|
|
|
108
117
|
|
|
109
118
|
await manager.call('device-a', createAdapter, 'Ping', { message: '1' }, { timeoutMs: 123 });
|
|
110
119
|
|
|
111
|
-
expect(adapters[0].prepareCall).toHaveBeenCalledWith(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
120
|
+
expect(adapters[0].prepareCall).toHaveBeenCalledWith(
|
|
121
|
+
expect.objectContaining({
|
|
122
|
+
messageName: 'Ping',
|
|
123
|
+
timeoutMs: 123,
|
|
124
|
+
highThroughput: false,
|
|
125
|
+
generation: 1,
|
|
126
|
+
signal: expect.any(AbortSignal),
|
|
127
|
+
})
|
|
128
|
+
);
|
|
117
129
|
});
|
|
118
130
|
|
|
119
131
|
test('invalidates a link-fatal call before the next call creates a new link', async () => {
|
|
@@ -178,6 +190,54 @@ describe('ProtocolV2LinkManager', () => {
|
|
|
178
190
|
expect(adapters[1].reset).toHaveBeenCalledWith('schema changed');
|
|
179
191
|
});
|
|
180
192
|
|
|
193
|
+
test('waits for an in-flight native cleanup before a concurrent invalidation resolves', async () => {
|
|
194
|
+
const sentSeqs = [];
|
|
195
|
+
const { adapters, createAdapter } = createAdapterFactory(sentSeqs);
|
|
196
|
+
let markCleanupStarted;
|
|
197
|
+
let finishCleanup;
|
|
198
|
+
const cleanupStarted = new Promise(resolve => {
|
|
199
|
+
markCleanupStarted = resolve;
|
|
200
|
+
});
|
|
201
|
+
const cleanupBlocked = new Promise(resolve => {
|
|
202
|
+
finishCleanup = resolve;
|
|
203
|
+
});
|
|
204
|
+
const onLinkInvalidated = jest.fn(async () => {
|
|
205
|
+
markCleanupStarted();
|
|
206
|
+
await cleanupBlocked;
|
|
207
|
+
});
|
|
208
|
+
const manager = new ProtocolV2LinkManager({
|
|
209
|
+
getSchemas: () => schemas,
|
|
210
|
+
classifyError: () => 'recoverable',
|
|
211
|
+
onLinkInvalidated,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await manager.call('device-a', createAdapter, 'Ping', { message: '1' });
|
|
215
|
+
|
|
216
|
+
const release = manager.invalidateLink('device-a', 'release');
|
|
217
|
+
await cleanupStarted;
|
|
218
|
+
|
|
219
|
+
let acquireSettled = false;
|
|
220
|
+
const acquire = manager.invalidateLink('device-a', 'acquire').then(() => {
|
|
221
|
+
acquireSettled = true;
|
|
222
|
+
});
|
|
223
|
+
await new Promise(resolve => {
|
|
224
|
+
setImmediate(resolve);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(acquireSettled).toBe(false);
|
|
228
|
+
expect(adapters[0].reset).toHaveBeenCalledTimes(1);
|
|
229
|
+
expect(onLinkInvalidated).toHaveBeenCalledTimes(1);
|
|
230
|
+
|
|
231
|
+
finishCleanup();
|
|
232
|
+
await Promise.all([release, acquire]);
|
|
233
|
+
|
|
234
|
+
await manager.call('device-a', createAdapter, 'Ping', { message: '2' });
|
|
235
|
+
|
|
236
|
+
expect(acquireSettled).toBe(true);
|
|
237
|
+
expect(sentSeqs).toEqual([1, 2]);
|
|
238
|
+
expect(adapters).toHaveLength(2);
|
|
239
|
+
});
|
|
240
|
+
|
|
181
241
|
test('dispose clears active links and sequence cursors', async () => {
|
|
182
242
|
const sentSeqs = [];
|
|
183
243
|
const { adapters, createAdapter } = createAdapterFactory(sentSeqs);
|
|
@@ -200,7 +260,12 @@ describe('ProtocolV2LinkManager', () => {
|
|
|
200
260
|
const deviceABlocked = new Promise(resolve => {
|
|
201
261
|
releaseDeviceA = resolve;
|
|
202
262
|
});
|
|
203
|
-
const success = ProtocolV2.encodeFrame(
|
|
263
|
+
const success = ProtocolV2.encodeFrame(
|
|
264
|
+
schemas,
|
|
265
|
+
'Success',
|
|
266
|
+
{ message: 'ok' },
|
|
267
|
+
{ router: 1, packetSrc: 0, seq: 1 }
|
|
268
|
+
);
|
|
204
269
|
const createAdapter = key => {
|
|
205
270
|
let requestSeq = 0;
|
|
206
271
|
return {
|
|
@@ -252,6 +317,128 @@ describe('ProtocolV2LinkManager', () => {
|
|
|
252
317
|
]);
|
|
253
318
|
});
|
|
254
319
|
|
|
320
|
+
test('times out a queued call without sending it after the active call settles', async () => {
|
|
321
|
+
let releaseActiveRead;
|
|
322
|
+
let markActiveReadStarted;
|
|
323
|
+
const activeReadStarted = new Promise(resolve => {
|
|
324
|
+
markActiveReadStarted = resolve;
|
|
325
|
+
});
|
|
326
|
+
const activeReadBlocked = new Promise(resolve => {
|
|
327
|
+
releaseActiveRead = resolve;
|
|
328
|
+
});
|
|
329
|
+
const sentSeqs = [];
|
|
330
|
+
const success = ProtocolV2.encodeFrame(
|
|
331
|
+
schemas,
|
|
332
|
+
'Success',
|
|
333
|
+
{ message: 'ok' },
|
|
334
|
+
{ router: 1, packetSrc: 0, seq: 1 }
|
|
335
|
+
);
|
|
336
|
+
let requestSeq = 0;
|
|
337
|
+
let readCount = 0;
|
|
338
|
+
const adapter = {
|
|
339
|
+
router: 1,
|
|
340
|
+
generation: 1,
|
|
341
|
+
prepareCall: jest.fn(),
|
|
342
|
+
writeFrame: jest.fn(frame => {
|
|
343
|
+
[, , , , , , requestSeq] = frame;
|
|
344
|
+
sentSeqs.push(requestSeq);
|
|
345
|
+
return Promise.resolve();
|
|
346
|
+
}),
|
|
347
|
+
readFrame: jest.fn(async () => {
|
|
348
|
+
readCount += 1;
|
|
349
|
+
if (readCount === 1) {
|
|
350
|
+
markActiveReadStarted();
|
|
351
|
+
await activeReadBlocked;
|
|
352
|
+
}
|
|
353
|
+
return rewriteSeq(success, requestSeq);
|
|
354
|
+
}),
|
|
355
|
+
reset: jest.fn(),
|
|
356
|
+
createTimeoutError: (name, timeoutMs) =>
|
|
357
|
+
new Error(`response timeout after ${timeoutMs}ms for ${name}`),
|
|
358
|
+
};
|
|
359
|
+
const manager = new ProtocolV2LinkManager({
|
|
360
|
+
getSchemas: () => schemas,
|
|
361
|
+
classifyError: () => 'recoverable',
|
|
362
|
+
});
|
|
363
|
+
const createAdapter = jest.fn(() => adapter);
|
|
364
|
+
|
|
365
|
+
const activeCall = manager.call('device-a', createAdapter, 'Ping', { message: 'active' });
|
|
366
|
+
await activeReadStarted;
|
|
367
|
+
const queuedCall = manager.call(
|
|
368
|
+
'device-a',
|
|
369
|
+
createAdapter,
|
|
370
|
+
'Ping',
|
|
371
|
+
{ message: 'queued' },
|
|
372
|
+
{ timeoutMs: 20 }
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
await expect(queuedCall).rejects.toThrow('response timeout after 20ms for Ping');
|
|
376
|
+
expect(sentSeqs).toEqual([1]);
|
|
377
|
+
|
|
378
|
+
releaseActiveRead();
|
|
379
|
+
await activeCall;
|
|
380
|
+
await expect(
|
|
381
|
+
manager.call('device-a', createAdapter, 'Ping', { message: 'after-timeout' })
|
|
382
|
+
).resolves.toEqual({
|
|
383
|
+
type: 'Success',
|
|
384
|
+
message: { message: 'ok' },
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
expect(sentSeqs).toEqual([1, 2]);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test('writes flow control while the active call is waiting for its response', async () => {
|
|
391
|
+
const sentSeqs = [];
|
|
392
|
+
let releaseRead;
|
|
393
|
+
let markReadStarted;
|
|
394
|
+
const readStarted = new Promise(resolve => {
|
|
395
|
+
markReadStarted = resolve;
|
|
396
|
+
});
|
|
397
|
+
const readBlocked = new Promise(resolve => {
|
|
398
|
+
releaseRead = resolve;
|
|
399
|
+
});
|
|
400
|
+
const success = ProtocolV2.encodeFrame(
|
|
401
|
+
schemas,
|
|
402
|
+
'Success',
|
|
403
|
+
{ message: 'ok' },
|
|
404
|
+
{ router: 1, packetSrc: 0, seq: 1 }
|
|
405
|
+
);
|
|
406
|
+
let requestSeq = 0;
|
|
407
|
+
const adapter = {
|
|
408
|
+
router: 1,
|
|
409
|
+
generation: 1,
|
|
410
|
+
prepareCall: jest.fn(),
|
|
411
|
+
writeFrame: jest.fn(frame => {
|
|
412
|
+
[, , , , , , requestSeq] = frame;
|
|
413
|
+
sentSeqs.push(requestSeq);
|
|
414
|
+
return Promise.resolve();
|
|
415
|
+
}),
|
|
416
|
+
readFrame: jest.fn(async () => {
|
|
417
|
+
markReadStarted();
|
|
418
|
+
await readBlocked;
|
|
419
|
+
return rewriteSeq(success, 1);
|
|
420
|
+
}),
|
|
421
|
+
reset: jest.fn(),
|
|
422
|
+
};
|
|
423
|
+
const manager = new ProtocolV2LinkManager({
|
|
424
|
+
getSchemas: () => schemas,
|
|
425
|
+
classifyError: () => 'recoverable',
|
|
426
|
+
});
|
|
427
|
+
const createAdapter = jest.fn(() => adapter);
|
|
428
|
+
|
|
429
|
+
const activeCall = manager.call('device-a', createAdapter, 'Ping', { message: 'pending' });
|
|
430
|
+
await readStarted;
|
|
431
|
+
await expect(manager.sendFlowControl('device-a', createAdapter, 'Cancel', {})).resolves.toEqual(
|
|
432
|
+
{ type: 'WriteCompleted', message: {} }
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
expect(sentSeqs).toEqual([1, 2]);
|
|
436
|
+
expect(adapter.prepareCall).toHaveBeenCalledTimes(1);
|
|
437
|
+
expect(adapter.readFrame).toHaveBeenCalledTimes(1);
|
|
438
|
+
releaseRead();
|
|
439
|
+
await activeCall;
|
|
440
|
+
});
|
|
441
|
+
|
|
255
442
|
test('keeps a recoverable link after a call error', async () => {
|
|
256
443
|
const sentSeqs = [];
|
|
257
444
|
const { adapters, createAdapter } = createAdapterFactory(sentSeqs);
|
|
@@ -298,7 +485,12 @@ describe('ProtocolV2LinkManager', () => {
|
|
|
298
485
|
const writeBlocked = new Promise(resolve => {
|
|
299
486
|
releaseWrite = resolve;
|
|
300
487
|
});
|
|
301
|
-
const success = ProtocolV2.encodeFrame(
|
|
488
|
+
const success = ProtocolV2.encodeFrame(
|
|
489
|
+
schemas,
|
|
490
|
+
'Success',
|
|
491
|
+
{ message: 'ok' },
|
|
492
|
+
{ router: 1, packetSrc: 0, seq: 1 }
|
|
493
|
+
);
|
|
302
494
|
const adapter = {
|
|
303
495
|
router: 1,
|
|
304
496
|
generation: 1,
|
|
@@ -323,4 +515,49 @@ describe('ProtocolV2LinkManager', () => {
|
|
|
323
515
|
await expect(call).rejects.toThrow('device disconnected');
|
|
324
516
|
expect(adapter.readFrame).not.toHaveBeenCalled();
|
|
325
517
|
});
|
|
518
|
+
|
|
519
|
+
test('rejects calls queued before their link generation is invalidated', async () => {
|
|
520
|
+
let releaseWrite;
|
|
521
|
+
let markWriteStarted;
|
|
522
|
+
const writeStarted = new Promise(resolve => {
|
|
523
|
+
markWriteStarted = resolve;
|
|
524
|
+
});
|
|
525
|
+
const writeBlocked = new Promise(resolve => {
|
|
526
|
+
releaseWrite = resolve;
|
|
527
|
+
});
|
|
528
|
+
const success = ProtocolV2.encodeFrame(
|
|
529
|
+
schemas,
|
|
530
|
+
'Success',
|
|
531
|
+
{ message: 'ok' },
|
|
532
|
+
{ router: 1, packetSrc: 0, seq: 1 }
|
|
533
|
+
);
|
|
534
|
+
const adapter = {
|
|
535
|
+
router: 1,
|
|
536
|
+
generation: 1,
|
|
537
|
+
prepareCall: jest.fn(),
|
|
538
|
+
writeFrame: jest.fn(async () => {
|
|
539
|
+
markWriteStarted();
|
|
540
|
+
await writeBlocked;
|
|
541
|
+
}),
|
|
542
|
+
readFrame: jest.fn(() => Promise.resolve(success)),
|
|
543
|
+
reset: jest.fn(),
|
|
544
|
+
};
|
|
545
|
+
const createAdapter = jest.fn(() => adapter);
|
|
546
|
+
const manager = new ProtocolV2LinkManager({
|
|
547
|
+
getSchemas: () => schemas,
|
|
548
|
+
classifyError: () => 'recoverable',
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
const activeCall = manager.call('device-a', createAdapter, 'Ping', { message: 'active' });
|
|
552
|
+
await writeStarted;
|
|
553
|
+
const queuedCall = manager.call('device-a', createAdapter, 'Ping', { message: 'queued' });
|
|
554
|
+
await manager.invalidateLink('device-a', 'device released');
|
|
555
|
+
releaseWrite();
|
|
556
|
+
|
|
557
|
+
await expect(activeCall).rejects.toThrow('device released');
|
|
558
|
+
await expect(queuedCall).rejects.toThrow('device released');
|
|
559
|
+
expect(createAdapter).toHaveBeenCalledTimes(1);
|
|
560
|
+
expect(adapter.writeFrame).toHaveBeenCalledTimes(1);
|
|
561
|
+
expect(adapter.readFrame).not.toHaveBeenCalled();
|
|
562
|
+
});
|
|
326
563
|
});
|