@onekeyfe/hd-transport 1.2.0-alpha.3 → 1.2.0-alpha.30
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/__tests__/messages.test.js +142 -0
- package/__tests__/protocol-v2-ble-frame-writer.test.js +119 -0
- package/__tests__/protocol-v2-link-manager.test.js +389 -0
- package/__tests__/protocol-v2-usb-transport-base.test.js +330 -0
- package/__tests__/protocol-v2.test.js +555 -119
- package/dist/constants.d.ts +6 -3
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +981 -278
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +759 -247
- package/dist/protocols/index.d.ts +14 -5
- 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/constants.d.ts +1 -0
- package/dist/protocols/v2/constants.d.ts.map +1 -1
- package/dist/protocols/v2/decode.d.ts +13 -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 +38 -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 +19 -5
- package/dist/protocols/v2/session.d.ts.map +1 -1
- package/dist/protocols/v2/usb-transport-base.d.ts +32 -0
- package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -0
- package/dist/serialization/protobuf/decode.d.ts.map +1 -1
- package/dist/types/messages.d.ts +523 -158
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/transport.d.ts +13 -3
- package/dist/types/transport.d.ts.map +1 -1
- package/messages-protocol-v2.json +796 -620
- package/package.json +3 -3
- package/scripts/protobuf-build.sh +96 -310
- package/scripts/protobuf-patches/index.js +1 -0
- package/scripts/protobuf-types.js +19 -1
- package/src/constants.ts +29 -15
- package/src/index.ts +11 -1
- package/src/protocols/index.ts +39 -40
- package/src/protocols/v2/ble-frame-writer.ts +77 -0
- package/src/protocols/v2/constants.ts +1 -0
- package/src/protocols/v2/crc8.ts +1 -1
- package/src/protocols/v2/decode.ts +71 -37
- 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 +186 -0
- package/src/protocols/v2/sequence-cursor.ts +10 -0
- package/src/protocols/v2/session.ts +157 -201
- package/src/protocols/v2/usb-transport-base.ts +213 -0
- package/src/serialization/protobuf/decode.ts +4 -1
- package/src/types/messages.ts +623 -181
- package/src/types/transport.ts +13 -3
- 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
package/src/protocols/index.ts
CHANGED
|
@@ -6,10 +6,14 @@ import { decodeMessage as decodeV1Message } from './v1/receive';
|
|
|
6
6
|
import { createMessageFromName, createMessageFromType } from '../serialization/protobuf/messages';
|
|
7
7
|
import { encode as encodeProtobuf } from '../serialization/protobuf/encode';
|
|
8
8
|
import { decode as decodeProtobuf } from '../serialization/protobuf/decode';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
decodeFrame as decodeV2Frame,
|
|
11
|
+
encodeProtobufFrame,
|
|
12
|
+
inspectFrameHeader,
|
|
13
|
+
isAckFrame,
|
|
14
|
+
} from './v2';
|
|
10
15
|
|
|
11
16
|
import type { Root } from 'protobufjs/light';
|
|
12
|
-
import type { ProtocolV2DebugLogger } from './v2';
|
|
13
17
|
|
|
14
18
|
export const PROTOCOL_V2_SYS_MESSAGE_THRESHOLD = 60000;
|
|
15
19
|
|
|
@@ -23,9 +27,6 @@ type ProtocolV2FrameOptions = {
|
|
|
23
27
|
router?: number;
|
|
24
28
|
/** Sequence number (1-255). Managed per-session by ProtocolV2Session. */
|
|
25
29
|
seq?: number;
|
|
26
|
-
logger?: ProtocolV2DebugLogger;
|
|
27
|
-
logPrefix?: string;
|
|
28
|
-
context?: string;
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
const resolveProtocolV2EncodeSchema = (name: string, schemas: ProtocolV2Schemas) => {
|
|
@@ -74,6 +75,24 @@ export const ProtocolV1 = {
|
|
|
74
75
|
};
|
|
75
76
|
|
|
76
77
|
export const ProtocolV2 = {
|
|
78
|
+
isAckFrame,
|
|
79
|
+
inspectFrameHeader,
|
|
80
|
+
|
|
81
|
+
inspectFrame(schemas: ProtocolV2Schemas, frame: Uint8Array) {
|
|
82
|
+
const { messageTypeId, pbPayload, seq, router, packetSrc, dataType } = decodeV2Frame(frame);
|
|
83
|
+
const { messageName } = createProtocolV2MessageFromType(messageTypeId, schemas);
|
|
84
|
+
return {
|
|
85
|
+
messageName,
|
|
86
|
+
messageTypeId,
|
|
87
|
+
pbPayload,
|
|
88
|
+
seq,
|
|
89
|
+
router,
|
|
90
|
+
packetSrc,
|
|
91
|
+
dataType,
|
|
92
|
+
type: messageName,
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
|
|
77
96
|
encodeFrame(
|
|
78
97
|
schemas: ProtocolV2Schemas,
|
|
79
98
|
name: string,
|
|
@@ -87,51 +106,31 @@ export const ProtocolV2 = {
|
|
|
87
106
|
const rawPbBuffer = pbBuffer.toBuffer() as unknown as ArrayBuffer;
|
|
88
107
|
const pbBytes = new Uint8Array(rawPbBuffer);
|
|
89
108
|
|
|
90
|
-
options.logger?.debug?.(`[${options.logPrefix ?? 'ProtocolV2'}] encode protobuf`, {
|
|
91
|
-
context: options.context ?? `encode:${name}`,
|
|
92
|
-
messageName: name,
|
|
93
|
-
messageTypeId,
|
|
94
|
-
pbPayloadLength: pbBytes.length,
|
|
95
|
-
packetSrc: options.packetSrc ?? 0,
|
|
96
|
-
router: options.router ?? 0,
|
|
97
|
-
});
|
|
98
|
-
|
|
99
109
|
return encodeProtobufFrame(
|
|
100
110
|
messageTypeId,
|
|
101
111
|
pbBytes,
|
|
102
112
|
options.packetSrc,
|
|
103
113
|
options.router,
|
|
104
|
-
{
|
|
105
|
-
logger: options.logger,
|
|
106
|
-
logPrefix: options.logPrefix,
|
|
107
|
-
context: options.context ?? `encode:${name}`,
|
|
108
|
-
messageName: name,
|
|
109
|
-
},
|
|
110
114
|
options.seq
|
|
111
115
|
);
|
|
112
116
|
},
|
|
113
117
|
|
|
114
|
-
decodeFrame(
|
|
115
|
-
schemas
|
|
116
|
-
|
|
117
|
-
options: Pick<ProtocolV2FrameOptions, 'logger' | 'logPrefix' | 'context'> = {}
|
|
118
|
-
) {
|
|
119
|
-
const { messageTypeId, pbPayload, seq } = decodeV2Frame(frame, {
|
|
120
|
-
logger: options.logger,
|
|
121
|
-
logPrefix: options.logPrefix,
|
|
122
|
-
context: options.context ?? 'decode',
|
|
123
|
-
});
|
|
124
|
-
const { Message, messageName } = createProtocolV2MessageFromType(messageTypeId, schemas);
|
|
118
|
+
decodeFrame(schemas: ProtocolV2Schemas, frame: Uint8Array) {
|
|
119
|
+
const { messageTypeId, pbPayload, seq, messageName } = this.inspectFrame(schemas, frame);
|
|
120
|
+
const { Message } = createProtocolV2MessageFromType(messageTypeId, schemas);
|
|
125
121
|
const rxByteBuffer = ByteBuffer.wrap(Buffer.from(pbPayload) as unknown as ArrayBuffer);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
122
|
+
let message: ReturnType<typeof decodeProtobuf>;
|
|
123
|
+
try {
|
|
124
|
+
message = decodeProtobuf(Message, rxByteBuffer);
|
|
125
|
+
} catch (cause) {
|
|
126
|
+
const error = new Error(
|
|
127
|
+
`Protocol V2 protobuf decode failed for "${messageName}" ` +
|
|
128
|
+
`(${messageTypeId}, ${pbPayload.length}-byte payload); ` +
|
|
129
|
+
'the payload is malformed or incompatible with the active SDK schema.'
|
|
130
|
+
);
|
|
131
|
+
(error as Error & { cause?: unknown }).cause = cause;
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
135
134
|
|
|
136
135
|
return {
|
|
137
136
|
message,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export type ProtocolV2BleFrameWriterOptions = {
|
|
2
|
+
frame: Uint8Array;
|
|
3
|
+
packetCapacity: number;
|
|
4
|
+
writePacket: (packet: Uint8Array, packetIndex: number) => Promise<void>;
|
|
5
|
+
assertActive?: () => void;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
abortMessage?: string;
|
|
8
|
+
initialDelayMs?: number;
|
|
9
|
+
burstSize?: number;
|
|
10
|
+
burstPauseMs?: number;
|
|
11
|
+
flushDelayMs?: number;
|
|
12
|
+
wait?: (timeoutMs: number) => Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const defaultWait = (timeoutMs: number) =>
|
|
16
|
+
new Promise<void>(resolve => {
|
|
17
|
+
setTimeout(resolve, timeoutMs);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export async function writeProtocolV2BleFrame({
|
|
21
|
+
frame,
|
|
22
|
+
packetCapacity,
|
|
23
|
+
writePacket,
|
|
24
|
+
assertActive,
|
|
25
|
+
signal,
|
|
26
|
+
abortMessage = 'Protocol V2 BLE write aborted',
|
|
27
|
+
initialDelayMs = 0,
|
|
28
|
+
burstSize,
|
|
29
|
+
burstPauseMs = 0,
|
|
30
|
+
flushDelayMs = 0,
|
|
31
|
+
wait = defaultWait,
|
|
32
|
+
}: ProtocolV2BleFrameWriterOptions): Promise<void> {
|
|
33
|
+
if (!Number.isInteger(packetCapacity) || packetCapacity <= 0) {
|
|
34
|
+
throw new Error('Protocol V2 BLE packet capacity must be a positive integer');
|
|
35
|
+
}
|
|
36
|
+
if (burstSize !== undefined && (!Number.isInteger(burstSize) || burstSize <= 0)) {
|
|
37
|
+
throw new Error('Protocol V2 BLE burst size must be a positive integer');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const assertWritable = () => {
|
|
41
|
+
assertActive?.();
|
|
42
|
+
if (signal?.aborted) {
|
|
43
|
+
throw new Error(abortMessage);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (frame.length > 0 && initialDelayMs > 0) {
|
|
48
|
+
assertWritable();
|
|
49
|
+
await wait(initialDelayMs);
|
|
50
|
+
assertWritable();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let packetsWritten = 0;
|
|
54
|
+
for (let offset = 0; offset < frame.length; offset += packetCapacity) {
|
|
55
|
+
assertWritable();
|
|
56
|
+
const packet = frame.slice(offset, offset + packetCapacity);
|
|
57
|
+
await writePacket(packet, packetsWritten);
|
|
58
|
+
packetsWritten += 1;
|
|
59
|
+
assertWritable();
|
|
60
|
+
|
|
61
|
+
const hasMorePackets = offset + packetCapacity < frame.length;
|
|
62
|
+
if (
|
|
63
|
+
hasMorePackets &&
|
|
64
|
+
burstSize !== undefined &&
|
|
65
|
+
packetsWritten % burstSize === 0 &&
|
|
66
|
+
burstPauseMs > 0
|
|
67
|
+
) {
|
|
68
|
+
await wait(burstPauseMs);
|
|
69
|
+
assertWritable();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (burstSize !== undefined && packetsWritten > burstSize && flushDelayMs > 0) {
|
|
74
|
+
await wait(flushDelayMs);
|
|
75
|
+
assertWritable();
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/protocols/v2/crc8.ts
CHANGED
|
@@ -22,7 +22,7 @@ export const CRC8_TABLE = new Uint8Array([
|
|
|
22
22
|
]);
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* Compute CRC-8 over the first len bytes using the firmware-compatible initial value.
|
|
26
26
|
*/
|
|
27
27
|
export function crc8(data: Uint8Array, len: number): number {
|
|
28
28
|
let crc = CRC8_INIT;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
PROTO_DATA_TYPE_ACK,
|
|
3
|
+
PROTO_DATA_TYPE_PACKET,
|
|
4
|
+
PROTO_HEAD_CRC_SIZE,
|
|
5
|
+
PROTO_HEAD_SOF,
|
|
6
|
+
} from './constants';
|
|
2
7
|
import { crc8 } from './crc8';
|
|
3
|
-
import { logProtocolV2Debug } from './debug';
|
|
4
|
-
|
|
5
|
-
import type { ProtocolV2FrameDebugOptions } from './debug';
|
|
6
8
|
|
|
7
9
|
export interface ProtoV2Frame {
|
|
8
10
|
/** Little-endian message type ID */
|
|
@@ -11,22 +13,23 @@ export interface ProtoV2Frame {
|
|
|
11
13
|
pbPayload: Uint8Array;
|
|
12
14
|
/** Sequence number from the frame header */
|
|
13
15
|
seq: number;
|
|
16
|
+
/** Routing channel from the frame header */
|
|
17
|
+
router: number;
|
|
18
|
+
/** Packet source from the frame header */
|
|
19
|
+
packetSrc: number;
|
|
20
|
+
/** Packet or ACK discriminator from the frame header */
|
|
21
|
+
dataType: number;
|
|
14
22
|
}
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
*/
|
|
26
|
-
export function decodeFrame(
|
|
27
|
-
data: Uint8Array,
|
|
28
|
-
debugOptions?: ProtocolV2FrameDebugOptions
|
|
29
|
-
): ProtoV2Frame {
|
|
24
|
+
export type ProtoV2FrameHeader = {
|
|
25
|
+
frameLen: number;
|
|
26
|
+
router: number;
|
|
27
|
+
packetSrc: number;
|
|
28
|
+
dataType: number;
|
|
29
|
+
seq: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function inspectFrameHeader(data: Uint8Array): ProtoV2FrameHeader {
|
|
30
33
|
if (data.length < PROTO_HEAD_CRC_SIZE) {
|
|
31
34
|
throw new Error(`Protocol V2 frame too short: ${data.length} bytes`);
|
|
32
35
|
}
|
|
@@ -39,11 +42,12 @@ export function decodeFrame(
|
|
|
39
42
|
|
|
40
43
|
const frameLen = data[1] + data[2] * 256;
|
|
41
44
|
|
|
42
|
-
if (data.length
|
|
43
|
-
throw new Error(
|
|
45
|
+
if (data.length !== frameLen) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Protocol V2 frame length mismatch: expected ${frameLen} bytes, got ${data.length}`
|
|
48
|
+
);
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
// Verify pre-header CRC (bytes 0-2)
|
|
47
51
|
const expectedHeaderCrc = crc8(data, 3);
|
|
48
52
|
if (data[3] !== expectedHeaderCrc) {
|
|
49
53
|
throw new Error(
|
|
@@ -53,7 +57,6 @@ export function decodeFrame(
|
|
|
53
57
|
);
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
// Verify frame CRC (all bytes except last)
|
|
57
60
|
const expectedFrameCrc = crc8(data, frameLen - 1);
|
|
58
61
|
if (data[frameLen - 1] !== expectedFrameCrc) {
|
|
59
62
|
throw new Error(
|
|
@@ -63,7 +66,52 @@ export function decodeFrame(
|
|
|
63
66
|
);
|
|
64
67
|
}
|
|
65
68
|
|
|
69
|
+
// eslint-disable-next-line no-bitwise
|
|
70
|
+
const dataType = data[5] & 0x03;
|
|
71
|
+
// eslint-disable-next-line no-bitwise
|
|
72
|
+
const packetSrc = (data[5] >> 2) & 0x0f;
|
|
66
73
|
const seq = data[6];
|
|
74
|
+
if (seq === 0) {
|
|
75
|
+
throw new Error('Invalid Protocol V2 sequence: 0 is reserved');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
frameLen,
|
|
80
|
+
router: data[4],
|
|
81
|
+
packetSrc,
|
|
82
|
+
dataType,
|
|
83
|
+
seq,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isAckFrame(data: Uint8Array): boolean {
|
|
88
|
+
// eslint-disable-next-line no-bitwise
|
|
89
|
+
if (data.length < 6 || (data[5] & 0x03) !== PROTO_DATA_TYPE_ACK) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const header = inspectFrameHeader(data);
|
|
94
|
+
if (header.frameLen !== PROTO_HEAD_CRC_SIZE) {
|
|
95
|
+
throw new Error(`Invalid Protocol V2 ACK frame length: ${header.frameLen}`);
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parse and validate a Protocol V2 response frame.
|
|
102
|
+
*
|
|
103
|
+
* Validates:
|
|
104
|
+
* - SOF byte (0x5A)
|
|
105
|
+
* - Header CRC (bytes 0-2)
|
|
106
|
+
* - Frame CRC (full frame except last byte)
|
|
107
|
+
*
|
|
108
|
+
* Returns the decoded messageTypeId, raw protobuf payload, and sequence number.
|
|
109
|
+
*/
|
|
110
|
+
export function decodeFrame(data: Uint8Array): ProtoV2Frame {
|
|
111
|
+
const { frameLen, router, packetSrc, dataType, seq } = inspectFrameHeader(data);
|
|
112
|
+
if (dataType !== PROTO_DATA_TYPE_PACKET) {
|
|
113
|
+
throw new Error(`Invalid Protocol V2 data type: expected packet, got ${dataType}`);
|
|
114
|
+
}
|
|
67
115
|
// Payload spans bytes 7 to frameLen-2 (inclusive), excluding final CRC byte
|
|
68
116
|
const payloadData = data.slice(7, frameLen - 1);
|
|
69
117
|
|
|
@@ -74,19 +122,5 @@ export function decodeFrame(
|
|
|
74
122
|
const messageTypeId = payloadData[0] + payloadData[1] * 256;
|
|
75
123
|
const pbPayload = payloadData.slice(2);
|
|
76
124
|
|
|
77
|
-
|
|
78
|
-
frameLen,
|
|
79
|
-
dataLength: data.length,
|
|
80
|
-
router: data[4],
|
|
81
|
-
attr: data[5],
|
|
82
|
-
seq,
|
|
83
|
-
headerCrc: data[3],
|
|
84
|
-
expectedHeaderCrc,
|
|
85
|
-
frameCrc: data[frameLen - 1],
|
|
86
|
-
expectedFrameCrc,
|
|
87
|
-
messageTypeId,
|
|
88
|
-
pbPayloadLength: pbPayload.length,
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
return { messageTypeId, pbPayload, seq };
|
|
125
|
+
return { messageTypeId, pbPayload, seq, router, packetSrc, dataType };
|
|
92
126
|
}
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { PROTO_DATA_TYPE_PACKET, PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF } from './constants';
|
|
2
2
|
import { crc8 } from './crc8';
|
|
3
|
-
import { logProtocolV2Debug } from './debug';
|
|
4
3
|
import { PROTOCOL_V2_FRAME_MAX_BYTES } from '../../constants';
|
|
5
4
|
|
|
6
|
-
import type { ProtocolV2FrameDebugOptions } from './debug';
|
|
7
|
-
|
|
8
5
|
// Default sequence number when callers do not manage one themselves.
|
|
9
6
|
// Stateful per-session sequencing lives in ProtocolV2Session (session.ts),
|
|
10
7
|
// which advances the counter via nextProtoSeq() and passes it in explicitly.
|
|
@@ -35,7 +32,6 @@ export function encodeFrame(
|
|
|
35
32
|
payload: Uint8Array | null,
|
|
36
33
|
packetSrc?: number,
|
|
37
34
|
router?: number,
|
|
38
|
-
debugOptions?: ProtocolV2FrameDebugOptions,
|
|
39
35
|
seq?: number
|
|
40
36
|
): Uint8Array {
|
|
41
37
|
const resolvedPacketSrc = packetSrc ?? 0;
|
|
@@ -47,7 +43,7 @@ export function encodeFrame(
|
|
|
47
43
|
const payloadLen = payload ? payload.length : 0;
|
|
48
44
|
const frameLen = payloadLen + PROTO_HEAD_CRC_SIZE;
|
|
49
45
|
if (frameLen > PROTOCOL_V2_FRAME_MAX_BYTES) {
|
|
50
|
-
throw new Error(`Protocol V2 frame too large: ${frameLen}`);
|
|
46
|
+
throw new Error(`Protocol V2 frame too large: ${frameLen} > ${PROTOCOL_V2_FRAME_MAX_BYTES}`);
|
|
51
47
|
}
|
|
52
48
|
const frame = new Uint8Array(frameLen);
|
|
53
49
|
|
|
@@ -69,17 +65,6 @@ export function encodeFrame(
|
|
|
69
65
|
// CRC8 over entire frame except last byte
|
|
70
66
|
frame[frameLen - 1] = crc8(frame, frameLen - 1);
|
|
71
67
|
|
|
72
|
-
logProtocolV2Debug(debugOptions, 'encode raw frame', {
|
|
73
|
-
frameLen,
|
|
74
|
-
payloadLen,
|
|
75
|
-
packetSrc: resolvedPacketSrc,
|
|
76
|
-
router: frame[4],
|
|
77
|
-
attr: frame[5],
|
|
78
|
-
seq: frame[6],
|
|
79
|
-
headerCrc: frame[3],
|
|
80
|
-
frameCrc: frame[frameLen - 1],
|
|
81
|
-
});
|
|
82
|
-
|
|
83
68
|
return frame;
|
|
84
69
|
}
|
|
85
70
|
|
|
@@ -95,22 +80,11 @@ export function encodeProtobufFrame(
|
|
|
95
80
|
pbPayload: Uint8Array,
|
|
96
81
|
packetSrc?: number,
|
|
97
82
|
router?: number,
|
|
98
|
-
debugOptions?: ProtocolV2FrameDebugOptions,
|
|
99
83
|
seq?: number
|
|
100
84
|
): Uint8Array {
|
|
101
85
|
const payload = new Uint8Array(2 + pbPayload.length);
|
|
102
86
|
payload[0] = messageTypeId % 256;
|
|
103
87
|
payload[1] = Math.floor(messageTypeId / 256) % 256;
|
|
104
88
|
payload.set(pbPayload, 2);
|
|
105
|
-
return encodeFrame(
|
|
106
|
-
payload,
|
|
107
|
-
packetSrc,
|
|
108
|
-
router,
|
|
109
|
-
{
|
|
110
|
-
...debugOptions,
|
|
111
|
-
messageTypeId,
|
|
112
|
-
pbPayloadLength: pbPayload.length,
|
|
113
|
-
},
|
|
114
|
-
seq
|
|
115
|
-
);
|
|
89
|
+
return encodeFrame(payload, packetSrc, router, seq);
|
|
116
90
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type ProtocolV2LinkErrorCode =
|
|
2
|
+
| 'response-timeout'
|
|
3
|
+
| 'io'
|
|
4
|
+
| 'generation'
|
|
5
|
+
| 'router'
|
|
6
|
+
| 'packet-source'
|
|
7
|
+
| 'ack-sequence'
|
|
8
|
+
| 'response-sequence'
|
|
9
|
+
| 'frame';
|
|
10
|
+
|
|
11
|
+
export class ProtocolV2LinkError extends Error {
|
|
12
|
+
readonly code: ProtocolV2LinkErrorCode;
|
|
13
|
+
|
|
14
|
+
readonly cause?: unknown;
|
|
15
|
+
|
|
16
|
+
constructor(code: ProtocolV2LinkErrorCode, message: string, cause?: unknown) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'ProtocolV2LinkError';
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.cause = cause;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const isProtocolV2LinkError = (error: unknown): error is ProtocolV2LinkError =>
|
|
25
|
+
error instanceof ProtocolV2LinkError;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PROTOCOL_V2_FRAME_MAX_BYTES } from '../../constants';
|
|
2
2
|
import { PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF, PROTO_PRE_HEAD_SIZE } from './constants';
|
|
3
3
|
import { crc8 } from './crc8';
|
|
4
|
+
import { ProtocolV2LinkError } from './errors';
|
|
4
5
|
|
|
5
6
|
export function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {
|
|
6
7
|
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
|
@@ -58,7 +59,7 @@ export class ProtocolV2FrameAssembler {
|
|
|
58
59
|
|
|
59
60
|
if (this.buffer[0] !== PROTO_HEAD_SOF) {
|
|
60
61
|
this.reset();
|
|
61
|
-
throw new
|
|
62
|
+
throw new ProtocolV2LinkError('frame', 'Invalid Protocol V2 SOF');
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
const expectedLen = this.buffer[1] + this.buffer[2] * 256;
|
|
@@ -67,11 +68,11 @@ export class ProtocolV2FrameAssembler {
|
|
|
67
68
|
// complete frame: without resetting, this poison prefix would stay in
|
|
68
69
|
// the buffer forever and deadlock the caller's drain loop.
|
|
69
70
|
this.reset();
|
|
70
|
-
throw new
|
|
71
|
+
throw new ProtocolV2LinkError('frame', `Protocol V2 frame length too small: ${expectedLen}`);
|
|
71
72
|
}
|
|
72
73
|
if (expectedLen > this.maxFrameBytes) {
|
|
73
74
|
this.reset();
|
|
74
|
-
throw new
|
|
75
|
+
throw new ProtocolV2LinkError('frame', `Protocol V2 frame too large: ${expectedLen}`);
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
if (this.buffer.length < PROTO_PRE_HEAD_SIZE) return undefined;
|
|
@@ -82,7 +83,8 @@ export class ProtocolV2FrameAssembler {
|
|
|
82
83
|
const expectedHeaderCrc = crc8(this.buffer, 3);
|
|
83
84
|
if (this.buffer[3] !== expectedHeaderCrc) {
|
|
84
85
|
this.reset();
|
|
85
|
-
throw new
|
|
86
|
+
throw new ProtocolV2LinkError(
|
|
87
|
+
'frame',
|
|
86
88
|
`Protocol V2 header CRC mismatch: expected 0x${expectedHeaderCrc
|
|
87
89
|
.toString(16)
|
|
88
90
|
.padStart(2, '0')}`
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { ProtocolV2SequenceCursor } from './sequence-cursor';
|
|
2
|
+
import { ProtocolV2LinkError } from './errors';
|
|
3
|
+
import { ProtocolV2Session, getErrorMessage } from './session';
|
|
4
|
+
|
|
5
|
+
import type { MessageFromOneKey, TransportCallOptions } from '../../types';
|
|
6
|
+
import type { ProtocolV2CallContext, ProtocolV2Schemas, ProtocolV2SessionOptions } from './session';
|
|
7
|
+
|
|
8
|
+
export type ProtocolV2LinkErrorClassification = 'link-fatal' | 'recoverable';
|
|
9
|
+
|
|
10
|
+
export interface ProtocolV2LinkAdapter {
|
|
11
|
+
router: number;
|
|
12
|
+
maxFrameBytes?: number;
|
|
13
|
+
generation: number;
|
|
14
|
+
prepareCall(context: ProtocolV2CallContext): Promise<void> | void;
|
|
15
|
+
writeFrame(frame: Uint8Array, context: ProtocolV2CallContext): Promise<void>;
|
|
16
|
+
readFrame(context: ProtocolV2CallContext): Promise<Uint8Array>;
|
|
17
|
+
reset(reason: string): Promise<void> | void;
|
|
18
|
+
logger?: ProtocolV2SessionOptions['logger'];
|
|
19
|
+
logPrefix?: string;
|
|
20
|
+
createTimeoutError?: ProtocolV2SessionOptions['createTimeoutError'];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ProtocolV2LinkManagerOptions<Key> = {
|
|
24
|
+
getSchemas: () => ProtocolV2Schemas;
|
|
25
|
+
classifyError: (error: unknown) => ProtocolV2LinkErrorClassification;
|
|
26
|
+
onLinkInvalidated?: (key: Key, reason: string) => Promise<void> | void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type ProtocolV2Link = {
|
|
30
|
+
adapter: ProtocolV2LinkAdapter;
|
|
31
|
+
session: ProtocolV2Session;
|
|
32
|
+
state: {
|
|
33
|
+
invalidatedReason?: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export class ProtocolV2LinkManager<Key> {
|
|
38
|
+
private readonly links = new Map<Key, ProtocolV2Link>();
|
|
39
|
+
|
|
40
|
+
private readonly sequences = new Map<Key, ProtocolV2SequenceCursor>();
|
|
41
|
+
|
|
42
|
+
private readonly callQueues = new Map<Key, Promise<unknown>>();
|
|
43
|
+
|
|
44
|
+
private readonly generations = new Map<Key, number>();
|
|
45
|
+
|
|
46
|
+
private readonly invalidationReasons = new Map<Key, { generation: number; reason: string }>();
|
|
47
|
+
|
|
48
|
+
private readonly options: ProtocolV2LinkManagerOptions<Key>;
|
|
49
|
+
|
|
50
|
+
constructor(options: ProtocolV2LinkManagerOptions<Key>) {
|
|
51
|
+
this.options = options;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
call(
|
|
55
|
+
key: Key,
|
|
56
|
+
createAdapter: () => ProtocolV2LinkAdapter,
|
|
57
|
+
name: string,
|
|
58
|
+
data: Record<string, unknown>,
|
|
59
|
+
options?: TransportCallOptions
|
|
60
|
+
): Promise<MessageFromOneKey> {
|
|
61
|
+
const generation = this.generations.get(key) ?? 0;
|
|
62
|
+
const run = () => {
|
|
63
|
+
this.assertCallGeneration(key, generation);
|
|
64
|
+
return this.executeCall(key, createAdapter, name, data, options);
|
|
65
|
+
};
|
|
66
|
+
const previous = this.callQueues.get(key) ?? Promise.resolve();
|
|
67
|
+
const result = previous.then(run, run);
|
|
68
|
+
const queue = result.catch(() => undefined);
|
|
69
|
+
this.callQueues.set(key, queue);
|
|
70
|
+
result
|
|
71
|
+
.then(
|
|
72
|
+
() => this.clearSettledCallQueue(key, queue),
|
|
73
|
+
() => this.clearSettledCallQueue(key, queue)
|
|
74
|
+
)
|
|
75
|
+
.catch(() => undefined);
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async invalidateLink(key: Key, reason: string): Promise<void> {
|
|
80
|
+
const generation = (this.generations.get(key) ?? 0) + 1;
|
|
81
|
+
this.generations.set(key, generation);
|
|
82
|
+
this.invalidationReasons.set(key, { generation, reason });
|
|
83
|
+
|
|
84
|
+
const link = this.links.get(key);
|
|
85
|
+
if (!link) return;
|
|
86
|
+
|
|
87
|
+
this.links.delete(key);
|
|
88
|
+
link.state.invalidatedReason = reason;
|
|
89
|
+
await link.adapter.reset(reason);
|
|
90
|
+
await this.options.onLinkInvalidated?.(key, reason);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async invalidateAllLinks(reason: string): Promise<void> {
|
|
94
|
+
const keys = new Set([...this.links.keys(), ...this.callQueues.keys()]);
|
|
95
|
+
await Promise.all(Array.from(keys, key => this.invalidateLink(key, reason)));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async dispose(reason: string): Promise<void> {
|
|
99
|
+
await this.invalidateAllLinks(reason);
|
|
100
|
+
this.sequences.clear();
|
|
101
|
+
this.callQueues.clear();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private getOrCreateLink(key: Key, createAdapter: () => ProtocolV2LinkAdapter): ProtocolV2Link {
|
|
105
|
+
const existing = this.links.get(key);
|
|
106
|
+
if (existing) return existing;
|
|
107
|
+
|
|
108
|
+
const adapter = createAdapter();
|
|
109
|
+
const state: ProtocolV2Link['state'] = {};
|
|
110
|
+
const assertLinkActive = () => {
|
|
111
|
+
if (state.invalidatedReason) {
|
|
112
|
+
throw new Error(state.invalidatedReason);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
let sequenceCursor = this.sequences.get(key);
|
|
116
|
+
if (!sequenceCursor) {
|
|
117
|
+
sequenceCursor = new ProtocolV2SequenceCursor();
|
|
118
|
+
this.sequences.set(key, sequenceCursor);
|
|
119
|
+
}
|
|
120
|
+
const session = new ProtocolV2Session({
|
|
121
|
+
schemas: this.options.getSchemas(),
|
|
122
|
+
router: adapter.router,
|
|
123
|
+
maxFrameBytes: adapter.maxFrameBytes,
|
|
124
|
+
generation: adapter.generation,
|
|
125
|
+
sequenceCursor,
|
|
126
|
+
prepareCall: async context => {
|
|
127
|
+
assertLinkActive();
|
|
128
|
+
await adapter.prepareCall(context);
|
|
129
|
+
assertLinkActive();
|
|
130
|
+
},
|
|
131
|
+
writeFrame: async (frame, context) => {
|
|
132
|
+
assertLinkActive();
|
|
133
|
+
await adapter.writeFrame(frame, context);
|
|
134
|
+
assertLinkActive();
|
|
135
|
+
},
|
|
136
|
+
readFrame: async context => {
|
|
137
|
+
assertLinkActive();
|
|
138
|
+
const frame = await adapter.readFrame(context);
|
|
139
|
+
assertLinkActive();
|
|
140
|
+
return frame;
|
|
141
|
+
},
|
|
142
|
+
logger: adapter.logger,
|
|
143
|
+
logPrefix: adapter.logPrefix,
|
|
144
|
+
createTimeoutError: adapter.createTimeoutError,
|
|
145
|
+
});
|
|
146
|
+
const link = { adapter, session, state };
|
|
147
|
+
this.links.set(key, link);
|
|
148
|
+
return link;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private async executeCall(
|
|
152
|
+
key: Key,
|
|
153
|
+
createAdapter: () => ProtocolV2LinkAdapter,
|
|
154
|
+
name: string,
|
|
155
|
+
data: Record<string, unknown>,
|
|
156
|
+
options?: TransportCallOptions
|
|
157
|
+
): Promise<MessageFromOneKey> {
|
|
158
|
+
try {
|
|
159
|
+
return await this.getOrCreateLink(key, createAdapter).session.call(name, data, options);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (this.options.classifyError(error) === 'link-fatal') {
|
|
162
|
+
const errorMessage = getErrorMessage(error) || 'unknown error';
|
|
163
|
+
await this.invalidateLink(key, `Protocol V2 link-fatal error: ${errorMessage}`);
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private clearSettledCallQueue(key: Key, queue: Promise<unknown>) {
|
|
170
|
+
if (this.callQueues.get(key) === queue) {
|
|
171
|
+
this.callQueues.delete(key);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private assertCallGeneration(key: Key, generation: number) {
|
|
176
|
+
const currentGeneration = this.generations.get(key) ?? 0;
|
|
177
|
+
if (currentGeneration === generation) return;
|
|
178
|
+
|
|
179
|
+
const invalidation = this.invalidationReasons.get(key);
|
|
180
|
+
const reason =
|
|
181
|
+
invalidation?.generation === currentGeneration
|
|
182
|
+
? invalidation.reason
|
|
183
|
+
: 'Protocol V2 link generation changed';
|
|
184
|
+
throw new ProtocolV2LinkError('generation', reason);
|
|
185
|
+
}
|
|
186
|
+
}
|