@hla4ts/transport 0.1.0 → 0.1.1
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 +386 -386
- package/package.json +2 -2
- package/src/constants.ts +57 -57
- package/src/frame-decoder.ts +136 -136
- package/src/index.ts +82 -82
- package/src/message-header.ts +141 -141
- package/src/message-type.ts +62 -62
- package/src/tcp-transport.ts +154 -154
- package/src/tls-transport.ts +193 -193
- package/src/transport.ts +99 -99
package/src/message-header.ts
CHANGED
|
@@ -1,141 +1,141 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Federate Protocol Message Header
|
|
3
|
-
*
|
|
4
|
-
* The message header is a 24-byte structure that precedes every message
|
|
5
|
-
* in the Federate Protocol. It contains:
|
|
6
|
-
*
|
|
7
|
-
* - packetSize (4 bytes): Total size of the packet including header
|
|
8
|
-
* - sequenceNumber (4 bytes): Monotonically increasing sequence number
|
|
9
|
-
* - sessionId (8 bytes): Unique session identifier
|
|
10
|
-
* - lastReceivedSequenceNumber (4 bytes): ACK for flow control
|
|
11
|
-
* - messageType (4 bytes): Type of message (see MessageType enum)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { MessageType } from "./message-type.ts";
|
|
15
|
-
|
|
16
|
-
/** Size of the message header in bytes */
|
|
17
|
-
export const HEADER_SIZE = 24;
|
|
18
|
-
|
|
19
|
-
/** Special value indicating no session ID */
|
|
20
|
-
export const NO_SESSION_ID = 0n;
|
|
21
|
-
|
|
22
|
-
/** Special value indicating no sequence number */
|
|
23
|
-
export const NO_SEQUENCE_NUMBER = 0;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Decoded message header
|
|
27
|
-
*/
|
|
28
|
-
export interface MessageHeader {
|
|
29
|
-
/** Total packet size including header */
|
|
30
|
-
packetSize: number;
|
|
31
|
-
/** Message sequence number */
|
|
32
|
-
sequenceNumber: number;
|
|
33
|
-
/** Session identifier */
|
|
34
|
-
sessionId: bigint;
|
|
35
|
-
/** Last received sequence number (for ACK) */
|
|
36
|
-
lastReceivedSequenceNumber: number;
|
|
37
|
-
/** Type of message */
|
|
38
|
-
messageType: MessageType;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Encode a message header into a 24-byte buffer
|
|
43
|
-
*/
|
|
44
|
-
export function encodeHeader(
|
|
45
|
-
payloadSize: number,
|
|
46
|
-
sequenceNumber: number,
|
|
47
|
-
sessionId: bigint,
|
|
48
|
-
lastReceivedSequenceNumber: number,
|
|
49
|
-
messageType: MessageType
|
|
50
|
-
): Uint8Array {
|
|
51
|
-
const packetSize = HEADER_SIZE + payloadSize;
|
|
52
|
-
const buffer = new ArrayBuffer(HEADER_SIZE);
|
|
53
|
-
const view = new DataView(buffer);
|
|
54
|
-
|
|
55
|
-
// All values are big-endian (network byte order)
|
|
56
|
-
view.setUint32(0, packetSize, false);
|
|
57
|
-
view.setUint32(4, sequenceNumber, false);
|
|
58
|
-
view.setBigUint64(8, sessionId, false);
|
|
59
|
-
view.setUint32(16, lastReceivedSequenceNumber, false);
|
|
60
|
-
view.setUint32(20, messageType, false);
|
|
61
|
-
|
|
62
|
-
return new Uint8Array(buffer);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Decode a message header from a 24-byte buffer
|
|
67
|
-
* @throws Error if buffer is too small or contains invalid data
|
|
68
|
-
*/
|
|
69
|
-
export function decodeHeader(buffer: Uint8Array): MessageHeader {
|
|
70
|
-
if (buffer.length < HEADER_SIZE) {
|
|
71
|
-
throw new Error(
|
|
72
|
-
`Buffer too small for header: expected ${HEADER_SIZE} bytes, got ${buffer.length}`
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const view = new DataView(
|
|
77
|
-
buffer.buffer,
|
|
78
|
-
buffer.byteOffset,
|
|
79
|
-
buffer.byteLength
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
const packetSize = view.getUint32(0, false);
|
|
83
|
-
if (packetSize < HEADER_SIZE) {
|
|
84
|
-
throw new Error(`Invalid packet size: ${packetSize} (minimum is ${HEADER_SIZE})`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const sequenceNumber = view.getUint32(4, false);
|
|
88
|
-
const sessionId = view.getBigUint64(8, false);
|
|
89
|
-
const lastReceivedSequenceNumber = view.getUint32(16, false);
|
|
90
|
-
const messageTypeValue = view.getUint32(20, false);
|
|
91
|
-
|
|
92
|
-
// Validate message type
|
|
93
|
-
if (!Object.values(MessageType).includes(messageTypeValue)) {
|
|
94
|
-
throw new Error(`Unknown message type: ${messageTypeValue}`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
packetSize,
|
|
99
|
-
sequenceNumber,
|
|
100
|
-
sessionId,
|
|
101
|
-
lastReceivedSequenceNumber,
|
|
102
|
-
messageType: messageTypeValue as MessageType,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Get the payload size from a header
|
|
108
|
-
*/
|
|
109
|
-
export function getPayloadSize(header: MessageHeader): number {
|
|
110
|
-
return header.packetSize - HEADER_SIZE;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Encode a complete message (header + payload)
|
|
115
|
-
*/
|
|
116
|
-
export function encodeMessage(
|
|
117
|
-
sequenceNumber: number,
|
|
118
|
-
sessionId: bigint,
|
|
119
|
-
lastReceivedSequenceNumber: number,
|
|
120
|
-
messageType: MessageType,
|
|
121
|
-
payload?: Uint8Array
|
|
122
|
-
): Uint8Array {
|
|
123
|
-
const payloadSize = payload?.length ?? 0;
|
|
124
|
-
const header = encodeHeader(
|
|
125
|
-
payloadSize,
|
|
126
|
-
sequenceNumber,
|
|
127
|
-
sessionId,
|
|
128
|
-
lastReceivedSequenceNumber,
|
|
129
|
-
messageType
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
if (payloadSize === 0) {
|
|
133
|
-
return header;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Combine header and payload
|
|
137
|
-
const message = new Uint8Array(HEADER_SIZE + payloadSize);
|
|
138
|
-
message.set(header, 0);
|
|
139
|
-
message.set(payload!, HEADER_SIZE);
|
|
140
|
-
return message;
|
|
141
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Federate Protocol Message Header
|
|
3
|
+
*
|
|
4
|
+
* The message header is a 24-byte structure that precedes every message
|
|
5
|
+
* in the Federate Protocol. It contains:
|
|
6
|
+
*
|
|
7
|
+
* - packetSize (4 bytes): Total size of the packet including header
|
|
8
|
+
* - sequenceNumber (4 bytes): Monotonically increasing sequence number
|
|
9
|
+
* - sessionId (8 bytes): Unique session identifier
|
|
10
|
+
* - lastReceivedSequenceNumber (4 bytes): ACK for flow control
|
|
11
|
+
* - messageType (4 bytes): Type of message (see MessageType enum)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { MessageType } from "./message-type.ts";
|
|
15
|
+
|
|
16
|
+
/** Size of the message header in bytes */
|
|
17
|
+
export const HEADER_SIZE = 24;
|
|
18
|
+
|
|
19
|
+
/** Special value indicating no session ID */
|
|
20
|
+
export const NO_SESSION_ID = 0n;
|
|
21
|
+
|
|
22
|
+
/** Special value indicating no sequence number */
|
|
23
|
+
export const NO_SEQUENCE_NUMBER = 0;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Decoded message header
|
|
27
|
+
*/
|
|
28
|
+
export interface MessageHeader {
|
|
29
|
+
/** Total packet size including header */
|
|
30
|
+
packetSize: number;
|
|
31
|
+
/** Message sequence number */
|
|
32
|
+
sequenceNumber: number;
|
|
33
|
+
/** Session identifier */
|
|
34
|
+
sessionId: bigint;
|
|
35
|
+
/** Last received sequence number (for ACK) */
|
|
36
|
+
lastReceivedSequenceNumber: number;
|
|
37
|
+
/** Type of message */
|
|
38
|
+
messageType: MessageType;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Encode a message header into a 24-byte buffer
|
|
43
|
+
*/
|
|
44
|
+
export function encodeHeader(
|
|
45
|
+
payloadSize: number,
|
|
46
|
+
sequenceNumber: number,
|
|
47
|
+
sessionId: bigint,
|
|
48
|
+
lastReceivedSequenceNumber: number,
|
|
49
|
+
messageType: MessageType
|
|
50
|
+
): Uint8Array {
|
|
51
|
+
const packetSize = HEADER_SIZE + payloadSize;
|
|
52
|
+
const buffer = new ArrayBuffer(HEADER_SIZE);
|
|
53
|
+
const view = new DataView(buffer);
|
|
54
|
+
|
|
55
|
+
// All values are big-endian (network byte order)
|
|
56
|
+
view.setUint32(0, packetSize, false);
|
|
57
|
+
view.setUint32(4, sequenceNumber, false);
|
|
58
|
+
view.setBigUint64(8, sessionId, false);
|
|
59
|
+
view.setUint32(16, lastReceivedSequenceNumber, false);
|
|
60
|
+
view.setUint32(20, messageType, false);
|
|
61
|
+
|
|
62
|
+
return new Uint8Array(buffer);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Decode a message header from a 24-byte buffer
|
|
67
|
+
* @throws Error if buffer is too small or contains invalid data
|
|
68
|
+
*/
|
|
69
|
+
export function decodeHeader(buffer: Uint8Array): MessageHeader {
|
|
70
|
+
if (buffer.length < HEADER_SIZE) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Buffer too small for header: expected ${HEADER_SIZE} bytes, got ${buffer.length}`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const view = new DataView(
|
|
77
|
+
buffer.buffer,
|
|
78
|
+
buffer.byteOffset,
|
|
79
|
+
buffer.byteLength
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const packetSize = view.getUint32(0, false);
|
|
83
|
+
if (packetSize < HEADER_SIZE) {
|
|
84
|
+
throw new Error(`Invalid packet size: ${packetSize} (minimum is ${HEADER_SIZE})`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const sequenceNumber = view.getUint32(4, false);
|
|
88
|
+
const sessionId = view.getBigUint64(8, false);
|
|
89
|
+
const lastReceivedSequenceNumber = view.getUint32(16, false);
|
|
90
|
+
const messageTypeValue = view.getUint32(20, false);
|
|
91
|
+
|
|
92
|
+
// Validate message type
|
|
93
|
+
if (!Object.values(MessageType).includes(messageTypeValue)) {
|
|
94
|
+
throw new Error(`Unknown message type: ${messageTypeValue}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
packetSize,
|
|
99
|
+
sequenceNumber,
|
|
100
|
+
sessionId,
|
|
101
|
+
lastReceivedSequenceNumber,
|
|
102
|
+
messageType: messageTypeValue as MessageType,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get the payload size from a header
|
|
108
|
+
*/
|
|
109
|
+
export function getPayloadSize(header: MessageHeader): number {
|
|
110
|
+
return header.packetSize - HEADER_SIZE;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Encode a complete message (header + payload)
|
|
115
|
+
*/
|
|
116
|
+
export function encodeMessage(
|
|
117
|
+
sequenceNumber: number,
|
|
118
|
+
sessionId: bigint,
|
|
119
|
+
lastReceivedSequenceNumber: number,
|
|
120
|
+
messageType: MessageType,
|
|
121
|
+
payload?: Uint8Array
|
|
122
|
+
): Uint8Array {
|
|
123
|
+
const payloadSize = payload?.length ?? 0;
|
|
124
|
+
const header = encodeHeader(
|
|
125
|
+
payloadSize,
|
|
126
|
+
sequenceNumber,
|
|
127
|
+
sessionId,
|
|
128
|
+
lastReceivedSequenceNumber,
|
|
129
|
+
messageType
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (payloadSize === 0) {
|
|
133
|
+
return header;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Combine header and payload
|
|
137
|
+
const message = new Uint8Array(HEADER_SIZE + payloadSize);
|
|
138
|
+
message.set(header, 0);
|
|
139
|
+
message.set(payload!, HEADER_SIZE);
|
|
140
|
+
return message;
|
|
141
|
+
}
|
package/src/message-type.ts
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Federate Protocol Message Types
|
|
3
|
-
*
|
|
4
|
-
* These message types are defined in the IEEE 1516-2025 Federate Protocol specification.
|
|
5
|
-
* They are used in the message header to indicate the type of message being sent.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export enum MessageType {
|
|
9
|
-
// Session Management (Control Messages)
|
|
10
|
-
/** Client → Server: Request to create a new session */
|
|
11
|
-
CTRL_NEW_SESSION = 1,
|
|
12
|
-
/** Server → Client: Response with session creation status */
|
|
13
|
-
CTRL_NEW_SESSION_STATUS = 2,
|
|
14
|
-
/** Bidirectional: Keep-alive ping */
|
|
15
|
-
CTRL_HEARTBEAT = 3,
|
|
16
|
-
/** Bidirectional: Keep-alive pong */
|
|
17
|
-
CTRL_HEARTBEAT_RESPONSE = 4,
|
|
18
|
-
/** Client → Server: Request to terminate session */
|
|
19
|
-
CTRL_TERMINATE_SESSION = 5,
|
|
20
|
-
/** Server → Client: Confirmation of session termination */
|
|
21
|
-
CTRL_SESSION_TERMINATED = 6,
|
|
22
|
-
|
|
23
|
-
// Reconnection
|
|
24
|
-
/** Client → Server: Request to resume a dropped session */
|
|
25
|
-
CTRL_RESUME_REQUEST = 10,
|
|
26
|
-
/** Server → Client: Response with resume status */
|
|
27
|
-
CTRL_RESUME_STATUS = 11,
|
|
28
|
-
|
|
29
|
-
// HLA Calls and Callbacks
|
|
30
|
-
/** Client → Server: HLA RTI service call (protobuf CallRequest) */
|
|
31
|
-
HLA_CALL_REQUEST = 20,
|
|
32
|
-
/** Server → Client: HLA RTI service response (protobuf CallResponse) */
|
|
33
|
-
HLA_CALL_RESPONSE = 21,
|
|
34
|
-
/** Server → Client: HLA callback to federate (protobuf CallbackRequest) */
|
|
35
|
-
HLA_CALLBACK_REQUEST = 22,
|
|
36
|
-
/** Client → Server: HLA callback acknowledgment (protobuf CallbackResponse) */
|
|
37
|
-
HLA_CALLBACK_RESPONSE = 23,
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Check if a message type is a control message (session management)
|
|
42
|
-
*/
|
|
43
|
-
export function isControlMessage(type: MessageType): boolean {
|
|
44
|
-
return type < MessageType.HLA_CALL_REQUEST;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Check if a message type is an HLA response
|
|
49
|
-
*/
|
|
50
|
-
export function isHlaResponse(type: MessageType): boolean {
|
|
51
|
-
return (
|
|
52
|
-
type === MessageType.HLA_CALL_RESPONSE ||
|
|
53
|
-
type === MessageType.HLA_CALLBACK_RESPONSE
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Get a human-readable name for a message type
|
|
59
|
-
*/
|
|
60
|
-
export function messageTypeName(type: MessageType): string {
|
|
61
|
-
return MessageType[type] ?? `UNKNOWN(${type})`;
|
|
62
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Federate Protocol Message Types
|
|
3
|
+
*
|
|
4
|
+
* These message types are defined in the IEEE 1516-2025 Federate Protocol specification.
|
|
5
|
+
* They are used in the message header to indicate the type of message being sent.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export enum MessageType {
|
|
9
|
+
// Session Management (Control Messages)
|
|
10
|
+
/** Client → Server: Request to create a new session */
|
|
11
|
+
CTRL_NEW_SESSION = 1,
|
|
12
|
+
/** Server → Client: Response with session creation status */
|
|
13
|
+
CTRL_NEW_SESSION_STATUS = 2,
|
|
14
|
+
/** Bidirectional: Keep-alive ping */
|
|
15
|
+
CTRL_HEARTBEAT = 3,
|
|
16
|
+
/** Bidirectional: Keep-alive pong */
|
|
17
|
+
CTRL_HEARTBEAT_RESPONSE = 4,
|
|
18
|
+
/** Client → Server: Request to terminate session */
|
|
19
|
+
CTRL_TERMINATE_SESSION = 5,
|
|
20
|
+
/** Server → Client: Confirmation of session termination */
|
|
21
|
+
CTRL_SESSION_TERMINATED = 6,
|
|
22
|
+
|
|
23
|
+
// Reconnection
|
|
24
|
+
/** Client → Server: Request to resume a dropped session */
|
|
25
|
+
CTRL_RESUME_REQUEST = 10,
|
|
26
|
+
/** Server → Client: Response with resume status */
|
|
27
|
+
CTRL_RESUME_STATUS = 11,
|
|
28
|
+
|
|
29
|
+
// HLA Calls and Callbacks
|
|
30
|
+
/** Client → Server: HLA RTI service call (protobuf CallRequest) */
|
|
31
|
+
HLA_CALL_REQUEST = 20,
|
|
32
|
+
/** Server → Client: HLA RTI service response (protobuf CallResponse) */
|
|
33
|
+
HLA_CALL_RESPONSE = 21,
|
|
34
|
+
/** Server → Client: HLA callback to federate (protobuf CallbackRequest) */
|
|
35
|
+
HLA_CALLBACK_REQUEST = 22,
|
|
36
|
+
/** Client → Server: HLA callback acknowledgment (protobuf CallbackResponse) */
|
|
37
|
+
HLA_CALLBACK_RESPONSE = 23,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if a message type is a control message (session management)
|
|
42
|
+
*/
|
|
43
|
+
export function isControlMessage(type: MessageType): boolean {
|
|
44
|
+
return type < MessageType.HLA_CALL_REQUEST;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if a message type is an HLA response
|
|
49
|
+
*/
|
|
50
|
+
export function isHlaResponse(type: MessageType): boolean {
|
|
51
|
+
return (
|
|
52
|
+
type === MessageType.HLA_CALL_RESPONSE ||
|
|
53
|
+
type === MessageType.HLA_CALLBACK_RESPONSE
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get a human-readable name for a message type
|
|
59
|
+
*/
|
|
60
|
+
export function messageTypeName(type: MessageType): string {
|
|
61
|
+
return MessageType[type] ?? `UNKNOWN(${type})`;
|
|
62
|
+
}
|