@onekeyfe/hd-transport 1.1.34-alpha.2 → 1.1.34-alpha.3
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__/build-receive.test.js +6 -8
- package/__tests__/decode-features.test.js +3 -2
- package/__tests__/messages.test.js +131 -0
- package/__tests__/protocol-v2-link-manager.test.js +351 -0
- package/__tests__/protocol-v2-usb-transport-base.test.js +296 -0
- package/__tests__/protocol-v2.test.js +899 -0
- package/dist/constants.d.ts +16 -5
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +1398 -45
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1102 -85
- package/dist/protocols/index.d.ts +50 -0
- package/dist/protocols/index.d.ts.map +1 -0
- package/dist/protocols/v1/decode.d.ts +11 -0
- package/dist/protocols/v1/decode.d.ts.map +1 -0
- package/dist/protocols/v1/encode.d.ts +11 -0
- package/dist/protocols/v1/encode.d.ts.map +1 -0
- package/dist/protocols/v1/index.d.ts +5 -0
- package/dist/protocols/v1/index.d.ts.map +1 -0
- package/dist/protocols/v1/packets.d.ts +7 -0
- package/dist/protocols/v1/packets.d.ts.map +1 -0
- package/dist/{serialization → protocols/v1}/receive.d.ts +1 -1
- package/dist/protocols/v1/receive.d.ts.map +1 -0
- package/dist/protocols/v2/constants.d.ts +8 -0
- package/dist/protocols/v2/constants.d.ts.map +1 -0
- package/dist/protocols/v2/crc8.d.ts +3 -0
- package/dist/protocols/v2/crc8.d.ts.map +1 -0
- package/dist/protocols/v2/decode.d.ts +8 -0
- package/dist/protocols/v2/decode.d.ts.map +1 -0
- package/dist/protocols/v2/encode.d.ts +4 -0
- package/dist/protocols/v2/encode.d.ts.map +1 -0
- package/dist/protocols/v2/frame-assembler.d.ts +12 -0
- package/dist/protocols/v2/frame-assembler.d.ts.map +1 -0
- package/dist/protocols/v2/index.d.ts +6 -0
- package/dist/protocols/v2/index.d.ts.map +1 -0
- package/dist/protocols/v2/link-manager.d.ts +36 -0
- package/dist/protocols/v2/link-manager.d.ts.map +1 -0
- package/dist/protocols/v2/sequence-cursor.d.ts +5 -0
- package/dist/protocols/v2/sequence-cursor.d.ts.map +1 -0
- package/dist/protocols/v2/session.d.ts +63 -0
- package/dist/protocols/v2/session.d.ts.map +1 -0
- package/dist/protocols/v2/usb-transport-base.d.ts +31 -0
- package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -0
- package/dist/serialization/index.d.ts +6 -3
- package/dist/serialization/index.d.ts.map +1 -1
- package/dist/serialization/protobuf/decode.d.ts.map +1 -1
- package/dist/serialization/protobuf/messages.d.ts +1 -1
- package/dist/serialization/protobuf/messages.d.ts.map +1 -1
- package/dist/types/messages.d.ts +707 -12
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/transport.d.ts +19 -5
- package/dist/types/transport.d.ts.map +1 -1
- package/dist/utils/logBlockCommand.d.ts.map +1 -1
- package/messages-protocol-v2.json +13544 -0
- package/package.json +3 -3
- package/scripts/protobuf-patches/TxInputType.js +1 -0
- package/scripts/protobuf-patches/index.js +2 -0
- package/scripts/protobuf-types.js +233 -18
- package/src/constants.ts +48 -6
- package/src/index.ts +47 -11
- package/src/protocols/index.ts +124 -0
- package/src/{serialization/protocol → protocols/v1}/decode.ts +4 -4
- package/src/{serialization/protocol → protocols/v1}/encode.ts +18 -13
- package/src/protocols/v1/index.ts +4 -0
- package/src/protocols/v1/packets.ts +53 -0
- package/src/{serialization → protocols/v1}/receive.ts +5 -5
- package/src/protocols/v2/constants.ts +7 -0
- package/src/protocols/v2/crc8.ts +34 -0
- package/src/protocols/v2/decode.ts +89 -0
- package/src/protocols/v2/encode.ts +90 -0
- package/src/protocols/v2/frame-assembler.ts +98 -0
- package/src/protocols/v2/index.ts +5 -0
- package/src/protocols/v2/link-manager.ts +162 -0
- package/src/protocols/v2/sequence-cursor.ts +10 -0
- package/src/protocols/v2/session.ts +339 -0
- package/src/protocols/v2/usb-transport-base.ts +194 -0
- package/src/serialization/index.ts +6 -5
- package/src/serialization/protobuf/decode.ts +7 -0
- package/src/serialization/protobuf/messages.ts +14 -5
- package/src/types/messages.ts +903 -13
- package/src/types/transport.ts +29 -5
- package/src/utils/logBlockCommand.ts +9 -1
- package/dist/serialization/protocol/decode.d.ts +0 -11
- package/dist/serialization/protocol/decode.d.ts.map +0 -1
- package/dist/serialization/protocol/encode.d.ts +0 -11
- package/dist/serialization/protocol/encode.d.ts.map +0 -1
- package/dist/serialization/protocol/index.d.ts +0 -3
- package/dist/serialization/protocol/index.d.ts.map +0 -1
- package/dist/serialization/receive.d.ts.map +0 -1
- package/dist/serialization/send.d.ts +0 -7
- package/dist/serialization/send.d.ts.map +0 -1
- package/src/serialization/protocol/index.ts +0 -2
- package/src/serialization/send.ts +0 -58
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
const { parseConfigure } = require('../src/serialization/protobuf/messages');
|
|
2
|
-
const {
|
|
3
|
-
const { receiveOne } = require('../src/serialization/receive');
|
|
4
|
-
const { buildEncodeBuffers } = require('../src/serialization/send');
|
|
2
|
+
const { ProtocolV1 } = require('../src/protocols');
|
|
5
3
|
|
|
6
4
|
const messages = {
|
|
7
5
|
StellarPaymentOp: {
|
|
@@ -96,17 +94,17 @@ const parsedMessages = parseConfigure({
|
|
|
96
94
|
describe('encoding json -> protobuf -> json', () => {
|
|
97
95
|
fixtures.forEach(f => {
|
|
98
96
|
describe(f.name, () => {
|
|
99
|
-
test('
|
|
97
|
+
test('encodeEnvelope - decodeMessage', () => {
|
|
100
98
|
// encoded message
|
|
101
|
-
const encodedMessage =
|
|
99
|
+
const encodedMessage = ProtocolV1.encodeEnvelope(parsedMessages, f.name, f.in);
|
|
102
100
|
// then decode message and check, whether decoded message matches original json
|
|
103
|
-
const decodedMessage =
|
|
101
|
+
const decodedMessage = ProtocolV1.decodeMessage(parsedMessages, encodedMessage);
|
|
104
102
|
expect(decodedMessage.type).toEqual(f.name);
|
|
105
103
|
expect(decodedMessage.message).toEqual(f.in);
|
|
106
104
|
});
|
|
107
105
|
|
|
108
|
-
test('
|
|
109
|
-
const result =
|
|
106
|
+
test('encodeMessageChunks - receiveAndParse', () => {
|
|
107
|
+
const result = ProtocolV1.encodeMessageChunks(parsedMessages, f.name, f.in);
|
|
110
108
|
result.forEach(r => {
|
|
111
109
|
expect(r.byteLength).toBeLessThanOrEqual(63);
|
|
112
110
|
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
/* eslint-disable import/order */
|
|
1
2
|
const ProtoBuf = require('protobufjs/light');
|
|
2
3
|
const ByteBuffer = require('bytebuffer');
|
|
3
4
|
|
|
4
5
|
const { decode } = require('../src/serialization/protobuf/decode');
|
|
5
|
-
const {
|
|
6
|
+
const { decodeEnvelope } = require('../src/protocols/v1/decode');
|
|
6
7
|
|
|
7
8
|
// Reuse the messages.json already committed alongside @onekeyfe/hd-core
|
|
8
9
|
// (runtime data for DataManager). hd-transport's own messages.json is
|
|
@@ -61,7 +62,7 @@ describe('Fix messages decode', () => {
|
|
|
61
62
|
test('decode', () => {
|
|
62
63
|
// deserialize
|
|
63
64
|
const encoded = ByteBuffer.fromHex(f.encodeMessage);
|
|
64
|
-
const { buffer } =
|
|
65
|
+
const { buffer } = decodeEnvelope(encoded);
|
|
65
66
|
const decoded = decode(Message, buffer);
|
|
66
67
|
|
|
67
68
|
// filter null values
|
|
@@ -3,6 +3,11 @@ const {
|
|
|
3
3
|
createMessageFromType,
|
|
4
4
|
parseConfigure,
|
|
5
5
|
} = require('../src/serialization/protobuf/messages');
|
|
6
|
+
const v1Messages = require('../../core/src/data/messages/messages.json');
|
|
7
|
+
const v2Messages = require('../messages-protocol-v2.json');
|
|
8
|
+
const coreV2Messages = require('../../core/src/data/messages/messages-protocol-v2.json');
|
|
9
|
+
const generatedTypes = require('../src/types/messages');
|
|
10
|
+
|
|
6
11
|
// const json = require('./data/messages.json');
|
|
7
12
|
|
|
8
13
|
const json = {
|
|
@@ -58,6 +63,116 @@ const json = {
|
|
|
58
63
|
};
|
|
59
64
|
|
|
60
65
|
describe('messages', () => {
|
|
66
|
+
test('V1 GetPassphraseState matches the current firmware schema', () => {
|
|
67
|
+
const { fields } = v1Messages.nested.GetPassphraseState;
|
|
68
|
+
|
|
69
|
+
expect(fields.passphrase_state).toMatchObject({ id: 1, type: 'string' });
|
|
70
|
+
expect(fields).not.toHaveProperty('_only_main_pin');
|
|
71
|
+
expect(fields).not.toHaveProperty('allow_create_attach_pin');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('Protocol V2 firmware targets match the current firmware-pro2 enum', () => {
|
|
75
|
+
expect(v2Messages.nested.DeviceFirmwareTargetType.values).toEqual({
|
|
76
|
+
FW_MGMT_TARGET_INVALID: 0,
|
|
77
|
+
FW_MGMT_TARGET_CRATE: 1,
|
|
78
|
+
FW_MGMT_TARGET_ROMLOADER: 2,
|
|
79
|
+
FW_MGMT_TARGET_BOOTLOADER: 3,
|
|
80
|
+
FW_MGMT_TARGET_APPLICATION_P1: 4,
|
|
81
|
+
FW_MGMT_TARGET_APPLICATION_P2: 5,
|
|
82
|
+
FW_MGMT_TARGET_COPROCESSOR: 6,
|
|
83
|
+
FW_MGMT_TARGET_SE01: 7,
|
|
84
|
+
FW_MGMT_TARGET_SE02: 8,
|
|
85
|
+
FW_MGMT_TARGET_SE03: 9,
|
|
86
|
+
FW_MGMT_TARGET_SE04: 10,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('Protocol V2 conflicting enums keep their own wire values', () => {
|
|
91
|
+
expect(generatedTypes.ProtocolV2FailureType).toMatchObject({
|
|
92
|
+
Failure_DataError: 4,
|
|
93
|
+
Failure_ProcessError: 5,
|
|
94
|
+
});
|
|
95
|
+
expect(generatedTypes.Enum_ProtocolV2Capability).toMatchObject({
|
|
96
|
+
Capability_AttachToPin: 18,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('Protocol V2 device status and session messages match firmware-pro2', () => {
|
|
101
|
+
expect(v2Messages.nested.MessageType.values).toMatchObject({
|
|
102
|
+
MessageType_DeviceStatusGet: 60602,
|
|
103
|
+
MessageType_DeviceStatus: 60603,
|
|
104
|
+
MessageType_DeviceSessionOpen: 60606,
|
|
105
|
+
MessageType_DeviceSession: 60607,
|
|
106
|
+
MessageType_DeviceSessionAskPin: 60608,
|
|
107
|
+
});
|
|
108
|
+
expect(v2Messages.nested.DeviceStatusGet).toEqual({ fields: {} });
|
|
109
|
+
expect(v2Messages.nested.DeviceSessionResume.fields.session_id).toMatchObject({
|
|
110
|
+
id: 1,
|
|
111
|
+
type: 'bytes',
|
|
112
|
+
});
|
|
113
|
+
expect(v2Messages.nested.DeviceWalletSelect.oneofs.access.oneof).toEqual([
|
|
114
|
+
'host_passphrase',
|
|
115
|
+
'passphrase_on_device',
|
|
116
|
+
'attach_pin_on_device',
|
|
117
|
+
]);
|
|
118
|
+
expect(v2Messages.nested.DeviceSessionOpen.oneofs.mode.oneof).toEqual(['resume', 'select']);
|
|
119
|
+
expect(v2Messages.nested).not.toHaveProperty('DeviceWalletType');
|
|
120
|
+
expect(v2Messages.nested).not.toHaveProperty('DeviceHiddenWalletSelect');
|
|
121
|
+
expect(v2Messages.nested.DeviceSession.fields).toMatchObject({
|
|
122
|
+
session_id: { id: 1, type: 'bytes' },
|
|
123
|
+
btc_test_address: { id: 2, type: 'string' },
|
|
124
|
+
});
|
|
125
|
+
expect(v2Messages.nested.DeviceSessionAskPin).toEqual({ fields: {} });
|
|
126
|
+
expect(v2Messages.nested.DeviceSessionAskPin_FailureSubCodes.values).toEqual({
|
|
127
|
+
UserCancel: 1,
|
|
128
|
+
});
|
|
129
|
+
expect(v2Messages.nested.MessageType.values).not.toHaveProperty(
|
|
130
|
+
'MessageType_DeviceSessionPinResult'
|
|
131
|
+
);
|
|
132
|
+
expect(v2Messages.nested.MessageType.values).not.toHaveProperty('MessageType_DeviceSessionGet');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('Protocol V2 onboarding status matches the current firmware-pro2 schema', () => {
|
|
136
|
+
expect(v2Messages.nested.DevOnboardingStep.values).toMatchObject({
|
|
137
|
+
DEV_ONBOARDING_STEP_UNKNOWN: 0,
|
|
138
|
+
DEV_ONBOARDING_STEP_CHECKING: 1,
|
|
139
|
+
DEV_ONBOARDING_STEP_PERSONALIZATION: 2,
|
|
140
|
+
DEV_ONBOARDING_STEP_PIN: 3,
|
|
141
|
+
DEV_ONBOARDING_STEP_SETUP: 4,
|
|
142
|
+
DEV_ONBOARDING_STEP_DONE: 5,
|
|
143
|
+
});
|
|
144
|
+
expect(v2Messages.nested.DevOnboardingPhase.values).toBeDefined();
|
|
145
|
+
expect(v2Messages.nested.DevOnboardingSetupKind.values).toBeDefined();
|
|
146
|
+
expect(v2Messages.nested.DevOnboardingSetupMethod.values).toBeDefined();
|
|
147
|
+
expect(v2Messages.nested.DevOnboardingSetupStatus.fields).toMatchObject({
|
|
148
|
+
kind: { id: 1, type: 'DevOnboardingSetupKind' },
|
|
149
|
+
method: { id: 2, type: 'DevOnboardingSetupMethod' },
|
|
150
|
+
});
|
|
151
|
+
expect(v2Messages.nested.DevOnboardingStatus.fields).toMatchObject({
|
|
152
|
+
step: { id: 1, type: 'DevOnboardingStep' },
|
|
153
|
+
phase: { id: 2, type: 'DevOnboardingPhase' },
|
|
154
|
+
setup: { id: 3, type: 'DevOnboardingSetupStatus' },
|
|
155
|
+
pin_set: { id: 4, type: 'bool' },
|
|
156
|
+
wallet_initialized: { id: 5, type: 'bool' },
|
|
157
|
+
});
|
|
158
|
+
expect(v2Messages.nested).not.toHaveProperty('DevOnboardingStage');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('Protocol V2 does not restore retired unlock or passphrase ids', () => {
|
|
162
|
+
expect(v2Messages.nested.MessageType.values).not.toHaveProperty('MessageType_UnLockDevice');
|
|
163
|
+
expect(v2Messages.nested.MessageType.values).not.toHaveProperty(
|
|
164
|
+
'MessageType_UnLockDeviceResponse'
|
|
165
|
+
);
|
|
166
|
+
expect(v2Messages.nested.MessageType.values).not.toHaveProperty(
|
|
167
|
+
'MessageType_GetPassphraseState'
|
|
168
|
+
);
|
|
169
|
+
expect(v2Messages.nested.MessageType.values).not.toHaveProperty('MessageType_PassphraseState');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('Protocol V2 transport and core schemas stay identical', () => {
|
|
173
|
+
expect(coreV2Messages).toEqual(v2Messages);
|
|
174
|
+
});
|
|
175
|
+
|
|
61
176
|
test('createMessageFromName (common case)', () => {
|
|
62
177
|
const messages = parseConfigure(json);
|
|
63
178
|
const name = 'Initialize';
|
|
@@ -70,6 +185,14 @@ describe('messages', () => {
|
|
|
70
185
|
expect(() => createMessageFromType(messages, 0)).not.toThrow();
|
|
71
186
|
});
|
|
72
187
|
|
|
188
|
+
test('createMessageFromType throws a readable error for unknown ids', () => {
|
|
189
|
+
const messages = parseConfigure(json);
|
|
190
|
+
|
|
191
|
+
expect(() => createMessageFromType(messages, 99999)).toThrow(
|
|
192
|
+
'MessageType id "99999" is not defined in protobuf schema'
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
73
196
|
test('createMessageFromName (wire_type case)', () => {
|
|
74
197
|
const messages = parseConfigure(json);
|
|
75
198
|
const name = 'TxAckInput';
|
|
@@ -83,4 +206,12 @@ describe('messages', () => {
|
|
|
83
206
|
|
|
84
207
|
expect(() => createMessageFromName(messages, name)).not.toThrow();
|
|
85
208
|
});
|
|
209
|
+
|
|
210
|
+
test('createMessageFromName throws when message type id is missing', () => {
|
|
211
|
+
const messages = parseConfigure(json);
|
|
212
|
+
|
|
213
|
+
expect(() => createMessageFromName(messages, 'TxAckInputWrapper')).toThrow(
|
|
214
|
+
'MessageType for "TxAckInputWrapper" is not defined in protobuf schema'
|
|
215
|
+
);
|
|
216
|
+
});
|
|
86
217
|
});
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
2
|
+
const { ProtocolV2, ProtocolV2LinkManager } = require('../src');
|
|
3
|
+
const { parseConfigure } = require('../src/serialization/protobuf/messages');
|
|
4
|
+
const protocolV2 = require('../src/protocols/v2');
|
|
5
|
+
|
|
6
|
+
const protocolV1Messages = parseConfigure({
|
|
7
|
+
nested: {
|
|
8
|
+
Failure: {
|
|
9
|
+
fields: {
|
|
10
|
+
code: { type: 'uint32', id: 1 },
|
|
11
|
+
message: { type: 'string', id: 2 },
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
MessageType: {
|
|
15
|
+
values: {
|
|
16
|
+
MessageType_Failure: 3,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const protocolV2Messages = parseConfigure({
|
|
23
|
+
nested: {
|
|
24
|
+
Ping: {
|
|
25
|
+
fields: {
|
|
26
|
+
message: { type: 'string', id: 1 },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
Success: {
|
|
30
|
+
fields: {
|
|
31
|
+
message: { type: 'string', id: 1 },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
MessageType: {
|
|
35
|
+
values: {
|
|
36
|
+
MessageType_Ping: 60206,
|
|
37
|
+
MessageType_Success: 60207,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const schemas = {
|
|
44
|
+
protocolV1: protocolV1Messages,
|
|
45
|
+
protocolV2: protocolV2Messages,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const rewriteSeq = (frame, seq) => {
|
|
49
|
+
const copy = new Uint8Array(frame);
|
|
50
|
+
copy[6] = seq;
|
|
51
|
+
copy[copy.length - 1] = protocolV2.crc8(copy, copy.length - 1);
|
|
52
|
+
return copy;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const createAdapterFactory = sentSeqs => {
|
|
56
|
+
const adapters = [];
|
|
57
|
+
let generation = 0;
|
|
58
|
+
const success = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
adapters,
|
|
62
|
+
createAdapter: () => {
|
|
63
|
+
let requestSeq = 0;
|
|
64
|
+
const adapter = {
|
|
65
|
+
router: 1,
|
|
66
|
+
generation: ++generation,
|
|
67
|
+
prepareCall: jest.fn(),
|
|
68
|
+
writeFrame: jest.fn(frame => {
|
|
69
|
+
const [, , , , , , seq] = frame;
|
|
70
|
+
requestSeq = seq;
|
|
71
|
+
sentSeqs.push(requestSeq);
|
|
72
|
+
return Promise.resolve();
|
|
73
|
+
}),
|
|
74
|
+
readFrame: jest.fn(() => Promise.resolve(rewriteSeq(success, requestSeq))),
|
|
75
|
+
reset: jest.fn(() => Promise.resolve()),
|
|
76
|
+
};
|
|
77
|
+
adapters.push(adapter);
|
|
78
|
+
return adapter;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
describe('ProtocolV2LinkManager', () => {
|
|
84
|
+
test('retains the device sequence cursor when an active link is rebuilt', async () => {
|
|
85
|
+
const sentSeqs = [];
|
|
86
|
+
const { adapters, createAdapter } = createAdapterFactory(sentSeqs);
|
|
87
|
+
const manager = new ProtocolV2LinkManager({
|
|
88
|
+
getSchemas: () => schemas,
|
|
89
|
+
classifyError: () => 'recoverable',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await manager.call('device-a', createAdapter, 'Ping', { message: '1' });
|
|
93
|
+
await manager.invalidateLink('device-a', 'reconnect');
|
|
94
|
+
await manager.call('device-a', createAdapter, 'Ping', { message: '2' });
|
|
95
|
+
|
|
96
|
+
expect(sentSeqs).toEqual([1, 2]);
|
|
97
|
+
expect(adapters).toHaveLength(2);
|
|
98
|
+
expect(adapters[0].reset).toHaveBeenCalledWith('reconnect');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('prepares the active adapter with the same call context used for frame IO', async () => {
|
|
102
|
+
const sentSeqs = [];
|
|
103
|
+
const { adapters, createAdapter } = createAdapterFactory(sentSeqs);
|
|
104
|
+
const manager = new ProtocolV2LinkManager({
|
|
105
|
+
getSchemas: () => schemas,
|
|
106
|
+
classifyError: () => 'recoverable',
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await manager.call('device-a', createAdapter, 'Ping', { message: '1' }, { timeoutMs: 123 });
|
|
110
|
+
|
|
111
|
+
expect(adapters[0].prepareCall).toHaveBeenCalledWith({
|
|
112
|
+
messageName: 'Ping',
|
|
113
|
+
timeoutMs: 123,
|
|
114
|
+
highVolume: false,
|
|
115
|
+
generation: 1,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('invalidates a link-fatal call before the next call creates a new link', async () => {
|
|
120
|
+
const sentSeqs = [];
|
|
121
|
+
const onLinkInvalidated = jest.fn();
|
|
122
|
+
const { adapters, createAdapter: createWorkingAdapter } = createAdapterFactory(sentSeqs);
|
|
123
|
+
const createAdapter = jest
|
|
124
|
+
.fn()
|
|
125
|
+
.mockImplementationOnce(() => {
|
|
126
|
+
const adapter = createWorkingAdapter();
|
|
127
|
+
adapter.writeFrame.mockImplementationOnce(frame => {
|
|
128
|
+
sentSeqs.push(frame[6]);
|
|
129
|
+
return Promise.reject(new Error('transport write failed'));
|
|
130
|
+
});
|
|
131
|
+
return adapter;
|
|
132
|
+
})
|
|
133
|
+
.mockImplementation(() => createWorkingAdapter());
|
|
134
|
+
const manager = new ProtocolV2LinkManager({
|
|
135
|
+
getSchemas: () => schemas,
|
|
136
|
+
classifyError: () => 'link-fatal',
|
|
137
|
+
onLinkInvalidated,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await expect(manager.call('device-a', createAdapter, 'Ping', { message: '1' })).rejects.toThrow(
|
|
141
|
+
'transport write failed'
|
|
142
|
+
);
|
|
143
|
+
await expect(
|
|
144
|
+
manager.call('device-a', createAdapter, 'Ping', { message: '2' })
|
|
145
|
+
).resolves.toEqual({
|
|
146
|
+
type: 'Success',
|
|
147
|
+
message: { message: 'ok' },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(createAdapter).toHaveBeenCalledTimes(2);
|
|
151
|
+
expect(adapters[0].reset).toHaveBeenCalledWith(
|
|
152
|
+
expect.stringContaining('transport write failed')
|
|
153
|
+
);
|
|
154
|
+
expect(onLinkInvalidated).toHaveBeenCalledWith(
|
|
155
|
+
'device-a',
|
|
156
|
+
expect.stringContaining('transport write failed')
|
|
157
|
+
);
|
|
158
|
+
expect(sentSeqs).toEqual([1, 2]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('times out a stalled write and resets the link before releasing the call queue', async () => {
|
|
162
|
+
const adapter = {
|
|
163
|
+
router: 1,
|
|
164
|
+
generation: 1,
|
|
165
|
+
prepareCall: jest.fn(),
|
|
166
|
+
writeFrame: jest.fn(() => new Promise(() => {})),
|
|
167
|
+
readFrame: jest.fn(),
|
|
168
|
+
reset: jest.fn(() => Promise.resolve()),
|
|
169
|
+
writeTimeoutMs: 10,
|
|
170
|
+
};
|
|
171
|
+
const manager = new ProtocolV2LinkManager({
|
|
172
|
+
getSchemas: () => schemas,
|
|
173
|
+
classifyError: error =>
|
|
174
|
+
String(error).includes('write timeout') ? 'link-fatal' : 'recoverable',
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await expect(
|
|
178
|
+
manager.call('device-a', () => adapter, 'Ping', { message: 'pending' })
|
|
179
|
+
).rejects.toThrow('Protocol V2 write timeout after 10ms for Ping');
|
|
180
|
+
|
|
181
|
+
expect(adapter.reset).toHaveBeenCalledWith(expect.stringContaining('write timeout'));
|
|
182
|
+
expect(adapter.readFrame).not.toHaveBeenCalled();
|
|
183
|
+
expect(manager.callQueues.size).toBe(0);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('invalidates every active link while retaining per-device cursors', async () => {
|
|
187
|
+
const sentSeqs = [];
|
|
188
|
+
const { adapters, createAdapter } = createAdapterFactory(sentSeqs);
|
|
189
|
+
const manager = new ProtocolV2LinkManager({
|
|
190
|
+
getSchemas: () => schemas,
|
|
191
|
+
classifyError: () => 'recoverable',
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await manager.call('device-a', createAdapter, 'Ping', { message: 'a1' });
|
|
195
|
+
await manager.call('device-b', createAdapter, 'Ping', { message: 'b1' });
|
|
196
|
+
await manager.invalidateAllLinks('schema changed');
|
|
197
|
+
await manager.call('device-a', createAdapter, 'Ping', { message: 'a2' });
|
|
198
|
+
await manager.call('device-b', createAdapter, 'Ping', { message: 'b2' });
|
|
199
|
+
|
|
200
|
+
expect(sentSeqs).toEqual([1, 1, 2, 2]);
|
|
201
|
+
expect(adapters).toHaveLength(4);
|
|
202
|
+
expect(adapters[0].reset).toHaveBeenCalledWith('schema changed');
|
|
203
|
+
expect(adapters[1].reset).toHaveBeenCalledWith('schema changed');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('dispose clears active links and sequence cursors', async () => {
|
|
207
|
+
const sentSeqs = [];
|
|
208
|
+
const { adapters, createAdapter } = createAdapterFactory(sentSeqs);
|
|
209
|
+
const manager = new ProtocolV2LinkManager({
|
|
210
|
+
getSchemas: () => schemas,
|
|
211
|
+
classifyError: () => 'recoverable',
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await manager.call('device-a', createAdapter, 'Ping', { message: '1' });
|
|
215
|
+
await manager.dispose('transport disposed');
|
|
216
|
+
await manager.call('device-a', createAdapter, 'Ping', { message: '2' });
|
|
217
|
+
|
|
218
|
+
expect(sentSeqs).toEqual([1, 1]);
|
|
219
|
+
expect(adapters[0].reset).toHaveBeenCalledWith('transport disposed');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('serializes calls per device without blocking another device', async () => {
|
|
223
|
+
const events = [];
|
|
224
|
+
let releaseDeviceA;
|
|
225
|
+
const deviceABlocked = new Promise(resolve => {
|
|
226
|
+
releaseDeviceA = resolve;
|
|
227
|
+
});
|
|
228
|
+
const success = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
229
|
+
const createAdapter = key => {
|
|
230
|
+
let requestSeq = 0;
|
|
231
|
+
return {
|
|
232
|
+
router: 1,
|
|
233
|
+
generation: 1,
|
|
234
|
+
prepareCall: jest.fn(),
|
|
235
|
+
writeFrame: jest.fn(frame => {
|
|
236
|
+
const [, , , , , , seq] = frame;
|
|
237
|
+
requestSeq = seq;
|
|
238
|
+
events.push(`write:${key}:${requestSeq}`);
|
|
239
|
+
return Promise.resolve();
|
|
240
|
+
}),
|
|
241
|
+
readFrame: jest.fn(async () => {
|
|
242
|
+
if (key === 'device-a' && requestSeq === 1) {
|
|
243
|
+
await deviceABlocked;
|
|
244
|
+
}
|
|
245
|
+
events.push(`read:${key}:${requestSeq}`);
|
|
246
|
+
return rewriteSeq(success, requestSeq);
|
|
247
|
+
}),
|
|
248
|
+
reset: jest.fn(),
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
const manager = new ProtocolV2LinkManager({
|
|
252
|
+
getSchemas: () => schemas,
|
|
253
|
+
classifyError: () => 'recoverable',
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const firstA = manager.call('device-a', () => createAdapter('device-a'), 'Ping', {
|
|
257
|
+
message: 'a1',
|
|
258
|
+
});
|
|
259
|
+
const secondA = manager.call('device-a', () => createAdapter('device-a'), 'Ping', {
|
|
260
|
+
message: 'a2',
|
|
261
|
+
});
|
|
262
|
+
const firstB = manager.call('device-b', () => createAdapter('device-b'), 'Ping', {
|
|
263
|
+
message: 'b1',
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await firstB;
|
|
267
|
+
expect(events).toEqual(['write:device-a:1', 'write:device-b:1', 'read:device-b:1']);
|
|
268
|
+
releaseDeviceA();
|
|
269
|
+
await Promise.all([firstA, secondA]);
|
|
270
|
+
expect(events).toEqual([
|
|
271
|
+
'write:device-a:1',
|
|
272
|
+
'write:device-b:1',
|
|
273
|
+
'read:device-b:1',
|
|
274
|
+
'read:device-a:1',
|
|
275
|
+
'write:device-a:2',
|
|
276
|
+
'read:device-a:2',
|
|
277
|
+
]);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('keeps a recoverable link after a call error', async () => {
|
|
281
|
+
const sentSeqs = [];
|
|
282
|
+
const { adapters, createAdapter } = createAdapterFactory(sentSeqs);
|
|
283
|
+
const manager = new ProtocolV2LinkManager({
|
|
284
|
+
getSchemas: () => schemas,
|
|
285
|
+
classifyError: () => 'recoverable',
|
|
286
|
+
});
|
|
287
|
+
const adapter = createAdapter();
|
|
288
|
+
adapter.writeFrame.mockImplementationOnce(frame => {
|
|
289
|
+
sentSeqs.push(frame[6]);
|
|
290
|
+
return Promise.reject(new Error('application retryable error'));
|
|
291
|
+
});
|
|
292
|
+
const factory = jest.fn(() => adapter);
|
|
293
|
+
|
|
294
|
+
await expect(manager.call('device-a', factory, 'Ping', { message: '1' })).rejects.toThrow(
|
|
295
|
+
'application retryable error'
|
|
296
|
+
);
|
|
297
|
+
await manager.call('device-a', factory, 'Ping', { message: '2' });
|
|
298
|
+
|
|
299
|
+
expect(factory).toHaveBeenCalledTimes(1);
|
|
300
|
+
expect(adapter.reset).not.toHaveBeenCalled();
|
|
301
|
+
expect(sentSeqs).toEqual([1, 2]);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('removes a settled per-device call queue', async () => {
|
|
305
|
+
const sentSeqs = [];
|
|
306
|
+
const { createAdapter } = createAdapterFactory(sentSeqs);
|
|
307
|
+
const manager = new ProtocolV2LinkManager({
|
|
308
|
+
getSchemas: () => schemas,
|
|
309
|
+
classifyError: () => 'recoverable',
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
await manager.call('device-a', createAdapter, 'Ping', { message: 'done' });
|
|
313
|
+
|
|
314
|
+
expect(manager.callQueues.size).toBe(0);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('stops an in-flight call before reading when its link is invalidated during write', async () => {
|
|
318
|
+
let releaseWrite;
|
|
319
|
+
let markWriteStarted;
|
|
320
|
+
const writeStarted = new Promise(resolve => {
|
|
321
|
+
markWriteStarted = resolve;
|
|
322
|
+
});
|
|
323
|
+
const writeBlocked = new Promise(resolve => {
|
|
324
|
+
releaseWrite = resolve;
|
|
325
|
+
});
|
|
326
|
+
const success = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
327
|
+
const adapter = {
|
|
328
|
+
router: 1,
|
|
329
|
+
generation: 1,
|
|
330
|
+
prepareCall: jest.fn(),
|
|
331
|
+
writeFrame: jest.fn(async () => {
|
|
332
|
+
markWriteStarted();
|
|
333
|
+
await writeBlocked;
|
|
334
|
+
}),
|
|
335
|
+
readFrame: jest.fn(() => Promise.resolve(success)),
|
|
336
|
+
reset: jest.fn(),
|
|
337
|
+
};
|
|
338
|
+
const manager = new ProtocolV2LinkManager({
|
|
339
|
+
getSchemas: () => schemas,
|
|
340
|
+
classifyError: () => 'recoverable',
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const call = manager.call('device-a', () => adapter, 'Ping', { message: 'pending' });
|
|
344
|
+
await writeStarted;
|
|
345
|
+
await manager.invalidateLink('device-a', 'device disconnected');
|
|
346
|
+
releaseWrite();
|
|
347
|
+
|
|
348
|
+
await expect(call).rejects.toThrow('device disconnected');
|
|
349
|
+
expect(adapter.readFrame).not.toHaveBeenCalled();
|
|
350
|
+
});
|
|
351
|
+
});
|