@hla4ts/transport 0.1.0

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 ADDED
@@ -0,0 +1,386 @@
1
+ # @hla4ts/transport
2
+
3
+ **HLA 4 Federate Protocol Transport Layer**
4
+
5
+ This package provides the transport layer for the HLA 4 Federate Protocol, handling TCP/TLS connections, message framing, and low-level communication with HLA 4 RTIs.
6
+
7
+ ## Overview
8
+
9
+ The Federate Protocol uses a binary framing format over TCP or TLS connections. Each message has a 24-byte header followed by an optional payload. This package handles:
10
+
11
+ - **Connection management** - TCP and TLS socket connections
12
+ - **Message framing** - Encoding/decoding the 24-byte header
13
+ - **Stream handling** - Buffering partial messages, extracting multiple messages from a single chunk
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ bun add @hla4ts/transport
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```typescript
24
+ import {
25
+ TlsTransport,
26
+ MessageType,
27
+ encodeMessage,
28
+ FEDERATE_PROTOCOL_VERSION,
29
+ } from '@hla4ts/transport';
30
+
31
+ // Create a TLS transport
32
+ const transport = new TlsTransport({
33
+ host: 'rti.example.com',
34
+ port: 15165,
35
+ });
36
+
37
+ // Set up event handlers
38
+ transport.setEventHandlers({
39
+ onMessage: (msg) => {
40
+ console.log('Received:', MessageType[msg.header.messageType]);
41
+ console.log('Payload size:', msg.payload.length);
42
+ },
43
+ onClose: (hadError) => {
44
+ console.log('Connection closed', hadError ? '(with error)' : '');
45
+ },
46
+ onError: (err) => {
47
+ console.error('Transport error:', err);
48
+ },
49
+ });
50
+
51
+ // Connect
52
+ await transport.connect();
53
+
54
+ // Send a CTRL_NEW_SESSION message
55
+ const payload = new Uint8Array(4);
56
+ new DataView(payload.buffer).setUint32(0, FEDERATE_PROTOCOL_VERSION, false);
57
+
58
+ const message = encodeMessage(
59
+ 1, // sequence number
60
+ 0n, // session ID (0 for new session)
61
+ 0, // last received sequence number
62
+ MessageType.CTRL_NEW_SESSION, // message type
63
+ payload // payload
64
+ );
65
+
66
+ await transport.send(message);
67
+
68
+ // Later: disconnect
69
+ await transport.disconnect();
70
+ ```
71
+
72
+ ## Sequence Diagram
73
+
74
+ ```mermaid
75
+ sequenceDiagram
76
+ participant RTI
77
+ participant Transport as Tcp/TlsTransport
78
+ participant Decoder as FrameDecoder
79
+ participant App as Federate Code
80
+ RTI-->>Transport: TCP/TLS bytes
81
+ Transport-->>Decoder: push(chunk)
82
+ Decoder-->>App: onMessage(header, payload)
83
+ App->>Transport: send(encoded message)
84
+ Transport-->>RTI: TCP/TLS bytes
85
+ ```
86
+
87
+ ## Message Format
88
+
89
+ Every Federate Protocol message has a **24-byte header**:
90
+
91
+ ```
92
+ ┌─────────────────────────────────────────────────────────────────┐
93
+ │ Offset │ Size │ Field │ Description │
94
+ ├────────┼───────┼──────────────────────────┼─────────────────────┤
95
+ │ 0 │ 4 │ packetSize │ Total message size │
96
+ │ 4 │ 4 │ sequenceNumber │ Message sequence # │
97
+ │ 8 │ 8 │ sessionId │ Session identifier │
98
+ │ 16 │ 4 │ lastReceivedSequenceNum │ ACK for flow ctrl │
99
+ │ 20 │ 4 │ messageType │ Type of message │
100
+ ├────────┴───────┴──────────────────────────┴─────────────────────┤
101
+ │ 24 │ var │ payload │ Message payload │
102
+ └─────────────────────────────────────────────────────────────────┘
103
+ ```
104
+
105
+ All integers are **big-endian** (network byte order).
106
+
107
+ ## Message Types
108
+
109
+ ### Control Messages (Session Management)
110
+
111
+ | Code | Name | Direction | Description |
112
+ |------|------|-----------|-------------|
113
+ | 1 | `CTRL_NEW_SESSION` | C → S | Request new session |
114
+ | 2 | `CTRL_NEW_SESSION_STATUS` | S → C | Session creation result |
115
+ | 3 | `CTRL_HEARTBEAT` | Both | Keep-alive ping |
116
+ | 4 | `CTRL_HEARTBEAT_RESPONSE` | Both | Keep-alive pong |
117
+ | 5 | `CTRL_TERMINATE_SESSION` | C → S | Request session end |
118
+ | 6 | `CTRL_SESSION_TERMINATED` | S → C | Session ended |
119
+ | 10 | `CTRL_RESUME_REQUEST` | C → S | Resume dropped session |
120
+ | 11 | `CTRL_RESUME_STATUS` | S → C | Resume result |
121
+
122
+ ### HLA Messages
123
+
124
+ | Code | Name | Direction | Description |
125
+ |------|------|-----------|-------------|
126
+ | 20 | `HLA_CALL_REQUEST` | C → S | RTI service call (protobuf `CallRequest`) |
127
+ | 21 | `HLA_CALL_RESPONSE` | S → C | RTI response (protobuf `CallResponse`) |
128
+ | 22 | `HLA_CALLBACK_REQUEST` | S → C | Federate callback (protobuf `CallbackRequest`) |
129
+ | 23 | `HLA_CALLBACK_RESPONSE` | C → S | Callback ACK (protobuf `CallbackResponse`) |
130
+
131
+ ## API Reference
132
+
133
+ ### Transports
134
+
135
+ #### `TlsTransport`
136
+
137
+ Secure TLS connection (recommended for production):
138
+
139
+ ```typescript
140
+ import { TlsTransport, Ports } from '@hla4ts/transport';
141
+
142
+ const transport = new TlsTransport({
143
+ host: 'rti.example.com',
144
+ port: Ports.TLS, // 15165 (default)
145
+ connectionTimeout: 10000, // 10 seconds (default)
146
+ noDelay: true, // TCP_NODELAY (default)
147
+
148
+ // TLS options
149
+ rejectUnauthorized: true, // Verify server cert (default)
150
+ ca: '/path/to/ca.pem', // Custom CA (optional)
151
+ cert: '/path/to/client.pem', // Client cert for mTLS (optional)
152
+ key: '/path/to/client-key.pem', // Client key for mTLS (optional)
153
+ serverName: 'rti.example.com', // SNI hostname (optional)
154
+ });
155
+ ```
156
+
157
+ #### `TcpTransport`
158
+
159
+ Plain TCP connection (for development/testing only):
160
+
161
+ ```typescript
162
+ import { TcpTransport, Ports } from '@hla4ts/transport';
163
+
164
+ const transport = new TcpTransport({
165
+ host: 'localhost',
166
+ port: Ports.TCP, // 15164 (default)
167
+ });
168
+ ```
169
+
170
+ ### Transport Interface
171
+
172
+ Both transports implement the `Transport` interface:
173
+
174
+ ```typescript
175
+ interface Transport {
176
+ connect(): Promise<void>;
177
+ disconnect(): Promise<void>;
178
+ isConnected(): boolean;
179
+ send(data: Uint8Array): Promise<void>;
180
+ setEventHandlers(handlers: TransportEvents): void;
181
+ getProtocolName(): string;
182
+ getRemoteAddress(): string | null;
183
+ }
184
+
185
+ interface TransportEvents {
186
+ onMessage?: (message: ReceivedMessage) => void;
187
+ onClose?: (hadError: boolean) => void;
188
+ onError?: (error: Error) => void;
189
+ }
190
+
191
+ interface ReceivedMessage {
192
+ header: MessageHeader;
193
+ payload: Uint8Array;
194
+ }
195
+ ```
196
+
197
+ ### Message Header Functions
198
+
199
+ ```typescript
200
+ import {
201
+ encodeHeader,
202
+ decodeHeader,
203
+ encodeMessage,
204
+ getPayloadSize,
205
+ HEADER_SIZE, // 24
206
+ NO_SESSION_ID, // 0n
207
+ NO_SEQUENCE_NUMBER, // 0
208
+ } from '@hla4ts/transport';
209
+
210
+ // Encode just the header (24 bytes)
211
+ const header = encodeHeader(
212
+ payloadSize, // payload size in bytes
213
+ sequenceNumber, // sequence number
214
+ sessionId, // session ID (bigint)
215
+ lastReceivedSequenceNum, // ACK
216
+ messageType // MessageType enum
217
+ );
218
+
219
+ // Encode header + payload
220
+ const message = encodeMessage(
221
+ sequenceNumber,
222
+ sessionId,
223
+ lastReceivedSequenceNum,
224
+ messageType,
225
+ payload // optional Uint8Array
226
+ );
227
+
228
+ // Decode a header
229
+ const header = decodeHeader(buffer); // throws on invalid data
230
+ console.log(header.packetSize);
231
+ console.log(header.sequenceNumber);
232
+ console.log(header.sessionId);
233
+ console.log(header.messageType);
234
+
235
+ // Get payload size from header
236
+ const payloadSize = getPayloadSize(header);
237
+ ```
238
+
239
+ ### Frame Decoder
240
+
241
+ For advanced use cases, you can use the `FrameDecoder` directly:
242
+
243
+ ```typescript
244
+ import { FrameDecoder } from '@hla4ts/transport';
245
+
246
+ const decoder = new FrameDecoder();
247
+
248
+ decoder.onMessage = (msg) => {
249
+ console.log('Complete message received');
250
+ console.log('Type:', msg.header.messageType);
251
+ console.log('Payload:', msg.payload);
252
+ };
253
+
254
+ decoder.onError = (err) => {
255
+ console.error('Decode error:', err);
256
+ };
257
+
258
+ // Feed data as it arrives (handles fragmentation automatically)
259
+ socket.on('data', (chunk) => {
260
+ decoder.push(new Uint8Array(chunk));
261
+ });
262
+
263
+ // Check buffered data
264
+ console.log('Bytes waiting:', decoder.bufferedBytes);
265
+
266
+ // Reset after connection reset
267
+ decoder.reset();
268
+ ```
269
+
270
+ ### Constants
271
+
272
+ ```typescript
273
+ import {
274
+ FEDERATE_PROTOCOL_VERSION, // 1
275
+ Ports,
276
+ Protocol,
277
+ NewSessionStatus,
278
+ ResumeStatus,
279
+ } from '@hla4ts/transport';
280
+
281
+ // Default ports
282
+ Ports.TCP // 15164
283
+ Ports.TLS // 15165
284
+ Ports.WS // 80
285
+ Ports.WSS // 443
286
+
287
+ // Protocol names
288
+ Protocol.TCP // "tcp"
289
+ Protocol.TLS // "tls"
290
+ Protocol.WS // "websocket"
291
+ Protocol.WSS // "websocketsecure"
292
+
293
+ // Session status codes
294
+ NewSessionStatus.SUCCESS // 0
295
+ NewSessionStatus.UNSUPPORTED_PROTOCOL_VERSION // 1
296
+ NewSessionStatus.OUT_OF_RESOURCES // 2
297
+ NewSessionStatus.BAD_MESSAGE // 3
298
+ NewSessionStatus.OTHER_ERROR // 99
299
+
300
+ // Resume status codes
301
+ ResumeStatus.SUCCESS // 0
302
+ ResumeStatus.SESSION_NOT_FOUND // 1
303
+ ResumeStatus.NOT_ALLOWED // 2
304
+ ```
305
+
306
+ ## Session Protocol Flow
307
+
308
+ ```
309
+ ┌─────────────┐ ┌─────────────┐
310
+ │ Federate │ │ RTI │
311
+ └──────┬──────┘ └──────┬──────┘
312
+ │ │
313
+ │──── TCP/TLS Connect ──────────────────────>│
314
+ │ │
315
+ │──── CTRL_NEW_SESSION (version=1) ─────────>│
316
+ │ │
317
+ │<─── CTRL_NEW_SESSION_STATUS (sessionId) ───│
318
+ │ │
319
+ │──── HLA_CALL_REQUEST (Connect) ───────────>│
320
+ │<─── HLA_CALL_RESPONSE ─────────────────────│
321
+ │ │
322
+ │──── HLA_CALL_REQUEST (JoinFederation) ────>│
323
+ │<─── HLA_CALL_RESPONSE ─────────────────────│
324
+ │ │
325
+ │ ... HLA calls and callbacks ... │
326
+ │ │
327
+ │<─── HLA_CALLBACK_REQUEST (Discover...) ────│
328
+ │──── HLA_CALLBACK_RESPONSE ────────────────>│
329
+ │ │
330
+ │──── CTRL_HEARTBEAT ───────────────────────>│
331
+ │<─── CTRL_HEARTBEAT_RESPONSE ──────────────│
332
+ │ │
333
+ │──── CTRL_TERMINATE_SESSION ───────────────>│
334
+ │<─── CTRL_SESSION_TERMINATED ──────────────│
335
+ │ │
336
+ │──── TCP/TLS Disconnect ───────────────────>│
337
+ │ │
338
+ ```
339
+
340
+ ## Error Handling
341
+
342
+ ```typescript
343
+ try {
344
+ await transport.connect();
345
+ } catch (err) {
346
+ if (err.message.includes('Connection timeout')) {
347
+ console.error('RTI not reachable');
348
+ } else if (err.message.includes('certificate')) {
349
+ console.error('TLS certificate error');
350
+ }
351
+ }
352
+
353
+ transport.setEventHandlers({
354
+ onError: (err) => {
355
+ console.error('Transport error:', err);
356
+ // Attempt reconnection...
357
+ },
358
+ onClose: (hadError) => {
359
+ if (hadError) {
360
+ console.error('Connection lost unexpectedly');
361
+ }
362
+ },
363
+ });
364
+ ```
365
+
366
+ ## Testing
367
+
368
+ ```bash
369
+ cd packages/transport
370
+ bun test
371
+ ```
372
+
373
+ ## Related Packages
374
+
375
+ - [`@hla4ts/proto`](../proto) - Protocol buffer types
376
+ - [`@hla4ts/session`](../session) - Session management
377
+ - [`@hla4ts/hla-api`](../hla-api) - High-level HLA API facade
378
+
379
+ ## References
380
+
381
+ - [IEEE 1516-2025](https://standards.ieee.org/standard/1516-2025.html) - HLA 4 Standard
382
+ - [Pitch FedProClient](https://github.com/Pitch-Technologies/FedProClient) - Reference implementation
383
+
384
+ ## License
385
+
386
+ MIT
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@hla4ts/transport",
3
+ "version": "0.1.0",
4
+ "description": "HLA 4 Federate Protocol transport layer with TLS and message framing",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "files": [
9
+ "README.md",
10
+ "src"
11
+ ],
12
+ "exports": {
13
+ ".": "./src/index.ts"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "scripts": {
19
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun",
20
+ "clean": "rm -rf dist",
21
+ "typecheck": "tsc --noEmit",
22
+ "test": "bun test"
23
+ },
24
+ "dependencies": {
25
+ "@hla4ts/proto": "^0.1.0"
26
+ },
27
+ "devDependencies": {
28
+ "typescript": "^5.3.3"
29
+ }
30
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Federate Protocol Constants
3
+ */
4
+
5
+ /** Current Federate Protocol version */
6
+ export const FEDERATE_PROTOCOL_VERSION = 1;
7
+
8
+ /** Default ports for different transport protocols */
9
+ export const Ports = {
10
+ /** Plain TCP (unencrypted) */
11
+ TCP: 15164,
12
+ /** TLS over TCP (encrypted) */
13
+ TLS: 15165,
14
+ /** WebSocket (unencrypted) */
15
+ WS: 80,
16
+ /** WebSocket Secure (encrypted) */
17
+ WSS: 443,
18
+ } as const;
19
+
20
+ /** Transport protocol names */
21
+ export const Protocol = {
22
+ TCP: "tcp",
23
+ TLS: "tls",
24
+ WS: "websocket",
25
+ WSS: "websocketsecure",
26
+ } as const;
27
+
28
+ export type ProtocolType = (typeof Protocol)[keyof typeof Protocol];
29
+
30
+ /** New session status reason codes */
31
+ export const NewSessionStatus = {
32
+ /** Session created successfully */
33
+ SUCCESS: 0,
34
+ /** Server doesn't support the requested protocol version */
35
+ UNSUPPORTED_PROTOCOL_VERSION: 1,
36
+ /** Server is out of resources */
37
+ OUT_OF_RESOURCES: 2,
38
+ /** Bad message received */
39
+ BAD_MESSAGE: 3,
40
+ /** Other unspecified error */
41
+ OTHER_ERROR: 99,
42
+ } as const;
43
+
44
+ export type NewSessionStatusCode =
45
+ (typeof NewSessionStatus)[keyof typeof NewSessionStatus];
46
+
47
+ /** Resume status reason codes */
48
+ export const ResumeStatus = {
49
+ /** Resume successful */
50
+ SUCCESS: 0,
51
+ /** Session not found (expired or invalid) */
52
+ SESSION_NOT_FOUND: 1,
53
+ /** Resume not allowed in current state */
54
+ NOT_ALLOWED: 2,
55
+ } as const;
56
+
57
+ export type ResumeStatusCode = (typeof ResumeStatus)[keyof typeof ResumeStatus];
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Frame Decoder
3
+ *
4
+ * Handles accumulating partial data from the network and extracting
5
+ * complete framed messages. This is necessary because TCP is a stream
6
+ * protocol and messages may arrive in fragments or multiple messages
7
+ * may arrive in a single chunk.
8
+ */
9
+
10
+ import {
11
+ HEADER_SIZE,
12
+ decodeHeader,
13
+ type MessageHeader,
14
+ } from "./message-header.ts";
15
+ import type { ReceivedMessage } from "./transport.ts";
16
+
17
+ /** Maximum allowed message size (16 MB) - protection against malformed packets */
18
+ const MAX_MESSAGE_SIZE = 16 * 1024 * 1024;
19
+
20
+ /**
21
+ * Frame decoder for length-prefixed messages
22
+ *
23
+ * Usage:
24
+ * ```ts
25
+ * const decoder = new FrameDecoder();
26
+ * decoder.onMessage = (msg) => console.log('Got message:', msg);
27
+ *
28
+ * // Feed data as it arrives from the socket
29
+ * socket.on('data', (chunk) => decoder.push(chunk));
30
+ * ```
31
+ */
32
+ export class FrameDecoder {
33
+ private buffer: Uint8Array = new Uint8Array(0);
34
+ private pendingHeader: MessageHeader | null = null;
35
+
36
+ /** Callback for complete messages */
37
+ public onMessage: ((message: ReceivedMessage) => void) | null = null;
38
+
39
+ /** Callback for decode errors */
40
+ public onError: ((error: Error) => void) | null = null;
41
+
42
+ /**
43
+ * Push incoming data into the decoder
44
+ * @param chunk Data received from the network
45
+ */
46
+ push(chunk: Uint8Array): void {
47
+ // Append chunk to buffer
48
+ this.buffer = this.concat(this.buffer, chunk);
49
+
50
+ // Process as many complete messages as possible
51
+ this.processBuffer();
52
+ }
53
+
54
+ /**
55
+ * Reset the decoder state (e.g., after a connection reset)
56
+ */
57
+ reset(): void {
58
+ this.buffer = new Uint8Array(0);
59
+ this.pendingHeader = null;
60
+ }
61
+
62
+ /**
63
+ * Get the number of bytes currently buffered
64
+ */
65
+ get bufferedBytes(): number {
66
+ return this.buffer.length;
67
+ }
68
+
69
+ private processBuffer(): void {
70
+ while (true) {
71
+ // If we don't have a header yet, try to read one
72
+ if (this.pendingHeader === null) {
73
+ if (this.buffer.length < HEADER_SIZE) {
74
+ // Not enough data for header yet
75
+ return;
76
+ }
77
+
78
+ try {
79
+ this.pendingHeader = decodeHeader(this.buffer.subarray(0, HEADER_SIZE));
80
+ } catch (error) {
81
+ this.onError?.(error as Error);
82
+ // Skip one byte and try again (attempt to resync)
83
+ this.buffer = this.buffer.subarray(1);
84
+ continue;
85
+ }
86
+
87
+ // Validate packet size
88
+ if (this.pendingHeader.packetSize > MAX_MESSAGE_SIZE) {
89
+ this.onError?.(
90
+ new Error(
91
+ `Message too large: ${this.pendingHeader.packetSize} bytes (max: ${MAX_MESSAGE_SIZE})`
92
+ )
93
+ );
94
+ // Reset and skip this header
95
+ this.pendingHeader = null;
96
+ this.buffer = this.buffer.subarray(HEADER_SIZE);
97
+ continue;
98
+ }
99
+ }
100
+
101
+ // We have a header, check if we have the complete message
102
+ const totalSize = this.pendingHeader.packetSize;
103
+ if (this.buffer.length < totalSize) {
104
+ // Not enough data for complete message yet
105
+ return;
106
+ }
107
+
108
+ // Extract the complete message
109
+ const messageData = this.buffer.subarray(0, totalSize);
110
+ const payload = messageData.subarray(HEADER_SIZE);
111
+
112
+ // Create the received message
113
+ const message: ReceivedMessage = {
114
+ header: this.pendingHeader,
115
+ payload: new Uint8Array(payload), // Copy to avoid issues with buffer reuse
116
+ };
117
+
118
+ // Remove this message from buffer
119
+ this.buffer = this.buffer.subarray(totalSize);
120
+ this.pendingHeader = null;
121
+
122
+ // Emit the message
123
+ this.onMessage?.(message);
124
+ }
125
+ }
126
+
127
+ private concat(a: Uint8Array, b: Uint8Array): Uint8Array {
128
+ if (a.length === 0) return b;
129
+ if (b.length === 0) return a;
130
+
131
+ const result = new Uint8Array(a.length + b.length);
132
+ result.set(a, 0);
133
+ result.set(b, a.length);
134
+ return result;
135
+ }
136
+ }
package/src/index.ts ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @hla4ts/transport - HLA 4 Federate Protocol Transport Layer
3
+ *
4
+ * This package provides transport implementations for the HLA 4 Federate Protocol,
5
+ * including message framing, TLS support, and connection management.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { TlsTransport, MessageType, encodeMessage } from '@hla4ts/transport';
10
+ *
11
+ * // Create a TLS transport
12
+ * const transport = new TlsTransport({
13
+ * host: 'rti.example.com',
14
+ * port: 15165,
15
+ * });
16
+ *
17
+ * // Set up event handlers
18
+ * transport.setEventHandlers({
19
+ * onMessage: (msg) => {
20
+ * console.log('Received:', MessageType[msg.header.messageType]);
21
+ * },
22
+ * onClose: () => console.log('Connection closed'),
23
+ * onError: (err) => console.error('Error:', err),
24
+ * });
25
+ *
26
+ * // Connect and send a message
27
+ * await transport.connect();
28
+ * const message = encodeMessage(
29
+ * 1, // sequence number
30
+ * 0n, // session ID (0 for new session)
31
+ * 0, // last received seq num
32
+ * MessageType.CTRL_NEW_SESSION, // message type
33
+ * payload // optional payload
34
+ * );
35
+ * await transport.send(message);
36
+ * ```
37
+ *
38
+ * @packageDocumentation
39
+ */
40
+
41
+ // Message types
42
+ export { MessageType, isControlMessage, isHlaResponse, messageTypeName } from "./message-type.ts";
43
+
44
+ // Message header encoding/decoding
45
+ export {
46
+ HEADER_SIZE,
47
+ NO_SESSION_ID,
48
+ NO_SEQUENCE_NUMBER,
49
+ encodeHeader,
50
+ decodeHeader,
51
+ encodeMessage,
52
+ getPayloadSize,
53
+ type MessageHeader,
54
+ } from "./message-header.ts";
55
+
56
+ // Constants
57
+ export {
58
+ FEDERATE_PROTOCOL_VERSION,
59
+ Ports,
60
+ Protocol,
61
+ NewSessionStatus,
62
+ ResumeStatus,
63
+ type ProtocolType,
64
+ type NewSessionStatusCode,
65
+ type ResumeStatusCode,
66
+ } from "./constants.ts";
67
+
68
+ // Transport interface
69
+ export type {
70
+ Transport,
71
+ TransportOptions,
72
+ TlsOptions,
73
+ TransportEvents,
74
+ ReceivedMessage,
75
+ } from "./transport.ts";
76
+
77
+ // Frame decoder
78
+ export { FrameDecoder } from "./frame-decoder.ts";
79
+
80
+ // Transport implementations
81
+ export { TlsTransport } from "./tls-transport.ts";
82
+ export { TcpTransport } from "./tcp-transport.ts";
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * TCP Transport Implementation
3
+ *
4
+ * Provides a plain TCP connection for the Federate Protocol.
5
+ * Note: This is unencrypted and should only be used for local development/testing.
6
+ */
7
+
8
+ import { connect, type Socket, type SocketHandler } from "bun";
9
+ import { FrameDecoder } from "./frame-decoder.ts";
10
+ import { Ports, Protocol } from "./constants.ts";
11
+ import type {
12
+ Transport,
13
+ TransportEvents,
14
+ TransportOptions,
15
+ ReceivedMessage,
16
+ } from "./transport.ts";
17
+
18
+ /**
19
+ * Plain TCP Transport for Federate Protocol
20
+ *
21
+ * ⚠️ WARNING: This transport is unencrypted. Use TlsTransport for production.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * const transport = new TcpTransport({
26
+ * host: 'localhost',
27
+ * port: 15164,
28
+ * });
29
+ *
30
+ * await transport.connect();
31
+ * ```
32
+ */
33
+ export class TcpTransport implements Transport {
34
+ private readonly options: Required<TransportOptions>;
35
+ private socket: Socket<{ decoder: FrameDecoder }> | null = null;
36
+ private events: TransportEvents = {};
37
+ private decoder: FrameDecoder;
38
+ private connected = false;
39
+
40
+ constructor(options: TransportOptions) {
41
+ this.options = {
42
+ host: options.host,
43
+ port: options.port ?? Ports.TCP,
44
+ connectionTimeout: options.connectionTimeout ?? 10000,
45
+ noDelay: options.noDelay ?? true,
46
+ };
47
+
48
+ this.decoder = new FrameDecoder();
49
+ this.decoder.onMessage = (msg) => this.handleMessage(msg);
50
+ this.decoder.onError = (err) => this.handleError(err);
51
+ }
52
+
53
+ async connect(): Promise<void> {
54
+ if (this.socket) {
55
+ throw new Error("Already connected");
56
+ }
57
+
58
+ const decoder = this.decoder;
59
+ const transport = this;
60
+
61
+ return new Promise((resolve, reject) => {
62
+ const timeoutId = setTimeout(() => {
63
+ reject(new Error(`Connection timeout after ${this.options.connectionTimeout}ms`));
64
+ }, this.options.connectionTimeout);
65
+
66
+ const socketHandler: SocketHandler<{ decoder: FrameDecoder }> = {
67
+ data(_socket, data) {
68
+ decoder.push(new Uint8Array(data));
69
+ },
70
+ open(_socket) {
71
+ clearTimeout(timeoutId);
72
+ transport.connected = true;
73
+ resolve();
74
+ },
75
+ close(_socket) {
76
+ transport.connected = false;
77
+ transport.socket = null;
78
+ transport.events.onClose?.(false);
79
+ },
80
+ error(_socket, error) {
81
+ clearTimeout(timeoutId);
82
+ transport.connected = false;
83
+ const err = error instanceof Error ? error : new Error(String(error));
84
+ transport.events.onError?.(err);
85
+ reject(err);
86
+ },
87
+ connectError(_socket, error) {
88
+ clearTimeout(timeoutId);
89
+ const err = error instanceof Error ? error : new Error(String(error));
90
+ reject(err);
91
+ },
92
+ };
93
+
94
+ connect({
95
+ hostname: this.options.host,
96
+ port: this.options.port,
97
+ socket: socketHandler,
98
+ data: { decoder },
99
+ })
100
+ .then((socket) => {
101
+ this.socket = socket;
102
+ })
103
+ .catch(reject);
104
+ });
105
+ }
106
+
107
+ async disconnect(): Promise<void> {
108
+ if (this.socket) {
109
+ this.socket.end();
110
+ this.socket = null;
111
+ this.connected = false;
112
+ this.decoder.reset();
113
+ }
114
+ }
115
+
116
+ isConnected(): boolean {
117
+ return this.connected && this.socket !== null;
118
+ }
119
+
120
+ async send(data: Uint8Array): Promise<void> {
121
+ if (!this.socket || !this.connected) {
122
+ throw new Error("Not connected");
123
+ }
124
+
125
+ const written = this.socket.write(data);
126
+ if (written < data.length) {
127
+ throw new Error(
128
+ `Failed to write all data: wrote ${written} of ${data.length} bytes`
129
+ );
130
+ }
131
+ }
132
+
133
+ setEventHandlers(handlers: TransportEvents): void {
134
+ this.events = handlers;
135
+ }
136
+
137
+ getProtocolName(): string {
138
+ return Protocol.TCP;
139
+ }
140
+
141
+ getRemoteAddress(): string | null {
142
+ return this.socket
143
+ ? `${this.options.host}:${this.options.port}`
144
+ : null;
145
+ }
146
+
147
+ private handleMessage(message: ReceivedMessage): void {
148
+ this.events.onMessage?.(message);
149
+ }
150
+
151
+ private handleError(error: Error): void {
152
+ this.events.onError?.(error);
153
+ }
154
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * TLS Transport Implementation
3
+ *
4
+ * Provides a TLS-encrypted TCP connection for the Federate Protocol.
5
+ * Uses Bun's built-in TLS support.
6
+ */
7
+
8
+ import { connect, type Socket, type SocketHandler, type TLSOptions } from "bun";
9
+ import { FrameDecoder } from "./frame-decoder.ts";
10
+ import { Ports, Protocol } from "./constants.ts";
11
+ import type {
12
+ Transport,
13
+ TransportEvents,
14
+ TlsOptions,
15
+ ReceivedMessage,
16
+ } from "./transport.ts";
17
+
18
+ /**
19
+ * TLS Transport for Federate Protocol
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const transport = new TlsTransport({
24
+ * host: 'localhost',
25
+ * port: 15165,
26
+ * });
27
+ *
28
+ * transport.setEventHandlers({
29
+ * onMessage: (msg) => console.log('Received:', msg.header.messageType),
30
+ * onClose: () => console.log('Disconnected'),
31
+ * onError: (err) => console.error('Error:', err),
32
+ * });
33
+ *
34
+ * await transport.connect();
35
+ * await transport.send(someData);
36
+ * await transport.disconnect();
37
+ * ```
38
+ */
39
+ export class TlsTransport implements Transport {
40
+ private readonly options: Required<
41
+ Pick<TlsOptions, "host" | "port" | "connectionTimeout" | "noDelay">
42
+ > &
43
+ Omit<TlsOptions, "host" | "port" | "connectionTimeout" | "noDelay">;
44
+
45
+ private socket: Socket<{ decoder: FrameDecoder }> | null = null;
46
+ private events: TransportEvents = {};
47
+ private decoder: FrameDecoder;
48
+ private connected = false;
49
+
50
+ constructor(options: TlsOptions) {
51
+ this.options = {
52
+ host: options.host,
53
+ port: options.port ?? Ports.TLS,
54
+ connectionTimeout: options.connectionTimeout ?? 10000,
55
+ noDelay: options.noDelay ?? true,
56
+ ca: options.ca,
57
+ cert: options.cert,
58
+ key: options.key,
59
+ rejectUnauthorized: options.rejectUnauthorized ?? true,
60
+ serverName: options.serverName,
61
+ };
62
+
63
+ this.decoder = new FrameDecoder();
64
+ this.decoder.onMessage = (msg) => this.handleMessage(msg);
65
+ this.decoder.onError = (err) => this.handleError(err);
66
+ }
67
+
68
+ async connect(): Promise<void> {
69
+ if (this.socket) {
70
+ throw new Error("Already connected");
71
+ }
72
+
73
+ const decoder = this.decoder;
74
+ const transport = this;
75
+
76
+ return new Promise((resolve, reject) => {
77
+ const timeoutId = setTimeout(() => {
78
+ reject(new Error(`Connection timeout after ${this.options.connectionTimeout}ms`));
79
+ }, this.options.connectionTimeout);
80
+
81
+ const socketHandler: SocketHandler<{ decoder: FrameDecoder }> = {
82
+ data(_socket, data) {
83
+ decoder.push(new Uint8Array(data));
84
+ },
85
+ open(_socket) {
86
+ clearTimeout(timeoutId);
87
+ transport.connected = true;
88
+ resolve();
89
+ },
90
+ close(_socket) {
91
+ transport.connected = false;
92
+ transport.socket = null;
93
+ transport.events.onClose?.(false);
94
+ },
95
+ error(_socket, error) {
96
+ clearTimeout(timeoutId);
97
+ transport.connected = false;
98
+ const err = error instanceof Error ? error : new Error(String(error));
99
+ transport.events.onError?.(err);
100
+ reject(err);
101
+ },
102
+ connectError(_socket, error) {
103
+ clearTimeout(timeoutId);
104
+ const err = error instanceof Error ? error : new Error(String(error));
105
+ reject(err);
106
+ },
107
+ };
108
+
109
+ // Build TLS options
110
+ const tlsConfig: TLSOptions = {
111
+ rejectUnauthorized: this.options.rejectUnauthorized,
112
+ };
113
+
114
+ // Add optional TLS settings
115
+ if (this.options.ca) {
116
+ tlsConfig.ca = Bun.file(this.options.ca);
117
+ }
118
+ if (this.options.cert) {
119
+ tlsConfig.cert = Bun.file(this.options.cert);
120
+ }
121
+ if (this.options.key) {
122
+ tlsConfig.key = Bun.file(this.options.key);
123
+ }
124
+ if (this.options.serverName) {
125
+ tlsConfig.serverName = this.options.serverName;
126
+ }
127
+
128
+ connect({
129
+ hostname: this.options.host,
130
+ port: this.options.port,
131
+ socket: socketHandler,
132
+ data: { decoder },
133
+ tls: tlsConfig,
134
+ })
135
+ .then((socket) => {
136
+ this.socket = socket;
137
+ })
138
+ .catch(reject);
139
+ });
140
+ }
141
+
142
+ async disconnect(): Promise<void> {
143
+ if (this.socket) {
144
+ this.socket.end();
145
+ this.socket = null;
146
+ this.connected = false;
147
+ this.decoder.reset();
148
+ }
149
+ }
150
+
151
+ isConnected(): boolean {
152
+ return this.connected && this.socket !== null;
153
+ }
154
+
155
+ async send(data: Uint8Array): Promise<void> {
156
+ if (!this.socket || !this.connected) {
157
+ throw new Error("Not connected");
158
+ }
159
+
160
+ const written = this.socket.write(data);
161
+ if (written < data.length) {
162
+ // Bun's socket.write returns the number of bytes written
163
+ // If not all bytes were written, we need to wait for drain
164
+ // For simplicity, we'll throw an error here
165
+ // In production, you'd want to buffer and retry
166
+ throw new Error(
167
+ `Failed to write all data: wrote ${written} of ${data.length} bytes`
168
+ );
169
+ }
170
+ }
171
+
172
+ setEventHandlers(handlers: TransportEvents): void {
173
+ this.events = handlers;
174
+ }
175
+
176
+ getProtocolName(): string {
177
+ return Protocol.TLS;
178
+ }
179
+
180
+ getRemoteAddress(): string | null {
181
+ return this.socket
182
+ ? `${this.options.host}:${this.options.port}`
183
+ : null;
184
+ }
185
+
186
+ private handleMessage(message: ReceivedMessage): void {
187
+ this.events.onMessage?.(message);
188
+ }
189
+
190
+ private handleError(error: Error): void {
191
+ this.events.onError?.(error);
192
+ }
193
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Transport Interface
3
+ *
4
+ * Defines the contract for Federate Protocol transports (TCP, TLS, WebSocket).
5
+ */
6
+
7
+ import type { MessageHeader } from "./message-header.ts";
8
+
9
+ /**
10
+ * Transport connection options
11
+ */
12
+ export interface TransportOptions {
13
+ /** Server hostname or IP address */
14
+ host: string;
15
+ /** Server port (defaults to Ports.TCP for TCP, Ports.TLS for TLS) */
16
+ port?: number;
17
+ /** Connection timeout in milliseconds */
18
+ connectionTimeout?: number;
19
+ /** Enable TCP_NODELAY (disable Nagle's algorithm) */
20
+ noDelay?: boolean;
21
+ }
22
+
23
+ /**
24
+ * TLS-specific options
25
+ */
26
+ export interface TlsOptions extends TransportOptions {
27
+ /** Path to CA certificate file (for server verification) */
28
+ ca?: string;
29
+ /** Path to client certificate file (for mutual TLS) */
30
+ cert?: string;
31
+ /** Path to client private key file (for mutual TLS) */
32
+ key?: string;
33
+ /** Skip server certificate verification (INSECURE - for testing only) */
34
+ rejectUnauthorized?: boolean;
35
+ /** Server name for SNI */
36
+ serverName?: string;
37
+ }
38
+
39
+ /**
40
+ * Received message with header and payload
41
+ */
42
+ export interface ReceivedMessage {
43
+ header: MessageHeader;
44
+ payload: Uint8Array;
45
+ }
46
+
47
+ /**
48
+ * Transport event handlers
49
+ */
50
+ export interface TransportEvents {
51
+ /** Called when a complete message is received */
52
+ onMessage?: (message: ReceivedMessage) => void;
53
+ /** Called when the connection is closed */
54
+ onClose?: (hadError: boolean) => void;
55
+ /** Called when an error occurs */
56
+ onError?: (error: Error) => void;
57
+ }
58
+
59
+ /**
60
+ * Transport interface for Federate Protocol communication
61
+ */
62
+ export interface Transport {
63
+ /**
64
+ * Connect to the server
65
+ * @throws Error if connection fails
66
+ */
67
+ connect(): Promise<void>;
68
+
69
+ /**
70
+ * Disconnect from the server
71
+ */
72
+ disconnect(): Promise<void>;
73
+
74
+ /**
75
+ * Check if the transport is connected
76
+ */
77
+ isConnected(): boolean;
78
+
79
+ /**
80
+ * Send raw bytes to the server
81
+ * @param data The data to send
82
+ */
83
+ send(data: Uint8Array): Promise<void>;
84
+
85
+ /**
86
+ * Set event handlers
87
+ */
88
+ setEventHandlers(handlers: TransportEvents): void;
89
+
90
+ /**
91
+ * Get the protocol name (tcp, tls, websocket, websocketsecure)
92
+ */
93
+ getProtocolName(): string;
94
+
95
+ /**
96
+ * Get the remote address
97
+ */
98
+ getRemoteAddress(): string | null;
99
+ }