@hla4ts/session 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.
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Timeout Timer
3
+ *
4
+ * Utility class to execute a callback after a timer expires.
5
+ * Supports extending the timeout and pausing/resuming.
6
+ *
7
+ * Can be configured as "eager" (may trigger early) or "lazy" (never triggers early).
8
+ */
9
+
10
+ /**
11
+ * Timer that executes a callback after a timeout.
12
+ * Supports extending, pausing, and resuming.
13
+ */
14
+ export class TimeoutTimer {
15
+ private readonly _timeoutDurationMs: number;
16
+ private readonly _checkIntervalMs: number;
17
+ private readonly _offsetTimeMs: number;
18
+ private _timeoutTimestampMs: number = Number.MAX_SAFE_INTEGER;
19
+ private _intervalHandle: ReturnType<typeof setInterval> | null = null;
20
+ private _onTimeout: (() => void) | null = null;
21
+ private _cancelled: boolean = false;
22
+
23
+ /**
24
+ * Create a new timeout timer.
25
+ *
26
+ * @param timeoutDurationMs - Duration until timeout in milliseconds
27
+ * @param eager - If true, timeout may trigger early but never late. If false, never triggers early.
28
+ */
29
+ private constructor(timeoutDurationMs: number, eager: boolean) {
30
+ this._timeoutDurationMs = timeoutDurationMs;
31
+
32
+ // Calculate check interval (check 6 times per timeout period)
33
+ const wakeUpTimesPerInterval = 6;
34
+ this._checkIntervalMs = Math.max(
35
+ 1,
36
+ Math.floor(timeoutDurationMs / wakeUpTimesPerInterval)
37
+ );
38
+
39
+ // For eager mode, trigger slightly before the full timeout
40
+ this._offsetTimeMs = eager
41
+ ? this._checkIntervalMs * (wakeUpTimesPerInterval - 1)
42
+ : timeoutDurationMs;
43
+ }
44
+
45
+ /**
46
+ * Create a lazy timeout timer (never triggers early, may trigger late)
47
+ */
48
+ static createLazy(timeoutDurationMs: number): TimeoutTimer {
49
+ return new TimeoutTimer(timeoutDurationMs, false);
50
+ }
51
+
52
+ /**
53
+ * Create an eager timeout timer (may trigger early, never triggers late)
54
+ */
55
+ static createEager(timeoutDurationMs: number): TimeoutTimer {
56
+ return new TimeoutTimer(timeoutDurationMs, true);
57
+ }
58
+
59
+ /**
60
+ * Get the configured timeout duration
61
+ */
62
+ get timeoutDurationMs(): number {
63
+ return this._timeoutDurationMs;
64
+ }
65
+
66
+ /**
67
+ * Check if the timer is currently running
68
+ */
69
+ get isRunning(): boolean {
70
+ return this._intervalHandle !== null && !this._cancelled;
71
+ }
72
+
73
+ /**
74
+ * Start the timer with a callback to execute on timeout.
75
+ *
76
+ * @param onTimeout - Callback to execute when the timer expires
77
+ */
78
+ start(onTimeout: () => void): void {
79
+ if (this._timeoutDurationMs === 0) {
80
+ // Zero timeout means no timeout checking
81
+ return;
82
+ }
83
+
84
+ if (this._cancelled) {
85
+ throw new Error("Cannot start a cancelled timer");
86
+ }
87
+
88
+ this._onTimeout = onTimeout;
89
+
90
+ // Start the periodic check
91
+ this._intervalHandle = setInterval(() => {
92
+ this._checkTimeout();
93
+ }, this._checkIntervalMs);
94
+
95
+ // Set initial timeout timestamp
96
+ this.extend();
97
+ }
98
+
99
+ /**
100
+ * Check if the timeout has been reached
101
+ */
102
+ private _checkTimeout(): void {
103
+ const now = Date.now();
104
+ if (now >= this._timeoutTimestampMs) {
105
+ this.pause();
106
+ this._onTimeout?.();
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Pause the timer (prevents timeout from triggering)
112
+ */
113
+ pause(): void {
114
+ this._timeoutTimestampMs = Number.MAX_SAFE_INTEGER;
115
+ }
116
+
117
+ /**
118
+ * Resume the timer after being paused
119
+ */
120
+ resume(): void {
121
+ this.extend();
122
+ }
123
+
124
+ /**
125
+ * Extend the timeout by resetting the timer
126
+ */
127
+ extend(): void {
128
+ this._timeoutTimestampMs = Date.now() + this._offsetTimeMs;
129
+ }
130
+
131
+ /**
132
+ * Cancel the timer permanently
133
+ */
134
+ cancel(): void {
135
+ this._cancelled = true;
136
+ if (this._intervalHandle !== null) {
137
+ clearInterval(this._intervalHandle);
138
+ this._intervalHandle = null;
139
+ }
140
+ this._onTimeout = null;
141
+ }
142
+
143
+ /**
144
+ * Get remaining time until timeout (approximate)
145
+ */
146
+ get remainingMs(): number {
147
+ const remaining = this._timeoutTimestampMs - Date.now();
148
+ return Math.max(0, remaining);
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Simple one-shot timer that executes a callback after a delay.
154
+ * Can be cancelled before execution.
155
+ */
156
+ export class OneShotTimer {
157
+ private _handle: ReturnType<typeof setTimeout> | null = null;
158
+ private _cancelled: boolean = false;
159
+
160
+ /**
161
+ * Schedule a callback to execute after the specified delay.
162
+ *
163
+ * @param callback - Callback to execute
164
+ * @param delayMs - Delay in milliseconds
165
+ */
166
+ schedule(callback: () => void, delayMs: number): void {
167
+ if (this._cancelled) {
168
+ throw new Error("Cannot schedule on a cancelled timer");
169
+ }
170
+
171
+ this.clear();
172
+ this._handle = setTimeout(() => {
173
+ this._handle = null;
174
+ if (!this._cancelled) {
175
+ callback();
176
+ }
177
+ }, delayMs);
178
+ }
179
+
180
+ /**
181
+ * Clear the scheduled callback without executing it
182
+ */
183
+ clear(): void {
184
+ if (this._handle !== null) {
185
+ clearTimeout(this._handle);
186
+ this._handle = null;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Cancel the timer permanently
192
+ */
193
+ cancel(): void {
194
+ this._cancelled = true;
195
+ this.clear();
196
+ }
197
+
198
+ /**
199
+ * Check if a callback is currently scheduled
200
+ */
201
+ get isScheduled(): boolean {
202
+ return this._handle !== null;
203
+ }
204
+ }
package/src/types.ts ADDED
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Session Types and State Machine
3
+ *
4
+ * Defines the session state machine, options, and event types for
5
+ * the HLA 4 Federate Protocol session layer.
6
+ */
7
+
8
+ /**
9
+ * Session states following the IEEE 1516-2025 specification.
10
+ *
11
+ * States can be divided into two categories:
12
+ * - **Stable states**: NEW, RUNNING, DROPPED, TERMINATED
13
+ * - **Transitory states**: STARTING, RESUMING, TERMINATING
14
+ *
15
+ * When in a transitory state, the thread that initiated the transition
16
+ * has exclusive access to the session.
17
+ */
18
+ export enum SessionState {
19
+ /**
20
+ * Session instance created but not yet started.
21
+ * Initial state after construction.
22
+ */
23
+ NEW = "NEW",
24
+
25
+ /**
26
+ * Session start() has been called, establishing connection with RTI.
27
+ * Transitory state - caller has exclusive access.
28
+ */
29
+ STARTING = "STARTING",
30
+
31
+ /**
32
+ * Session is connected and running normally.
33
+ * HLA calls and callbacks can be exchanged.
34
+ */
35
+ RUNNING = "RUNNING",
36
+
37
+ /**
38
+ * Session has experienced a connection loss.
39
+ * Can be resumed with resume() or terminated.
40
+ */
41
+ DROPPED = "DROPPED",
42
+
43
+ /**
44
+ * Session is attempting to resume after a connection loss.
45
+ * Transitory state - caller has exclusive access.
46
+ */
47
+ RESUMING = "RESUMING",
48
+
49
+ /**
50
+ * Session termination has been requested.
51
+ * Waiting for server acknowledgment.
52
+ */
53
+ TERMINATING = "TERMINATING",
54
+
55
+ /**
56
+ * Session is terminated and can no longer be used.
57
+ * Final state - session cannot be restarted.
58
+ */
59
+ TERMINATED = "TERMINATED",
60
+ }
61
+
62
+ /**
63
+ * Valid state transitions for the session state machine.
64
+ * Maps from current state to array of valid next states.
65
+ */
66
+ export const VALID_TRANSITIONS: Record<SessionState, SessionState[]> = {
67
+ [SessionState.NEW]: [SessionState.STARTING],
68
+ [SessionState.STARTING]: [SessionState.RUNNING, SessionState.TERMINATED],
69
+ [SessionState.RUNNING]: [SessionState.DROPPED, SessionState.TERMINATING],
70
+ [SessionState.DROPPED]: [
71
+ SessionState.RESUMING,
72
+ SessionState.TERMINATING,
73
+ SessionState.RUNNING, // Direct transition when resume succeeds quickly
74
+ ],
75
+ [SessionState.RESUMING]: [
76
+ SessionState.RUNNING,
77
+ SessionState.DROPPED,
78
+ SessionState.TERMINATED,
79
+ ],
80
+ [SessionState.TERMINATING]: [SessionState.TERMINATED],
81
+ [SessionState.TERMINATED]: [], // Terminal state
82
+ };
83
+
84
+ /**
85
+ * Check if a state transition is valid
86
+ */
87
+ export function isValidTransition(
88
+ from: SessionState,
89
+ to: SessionState
90
+ ): boolean {
91
+ return VALID_TRANSITIONS[from].includes(to);
92
+ }
93
+
94
+ /**
95
+ * Check if a state allows operations (sending messages)
96
+ */
97
+ export function isOperationalState(state: SessionState): boolean {
98
+ return (
99
+ state === SessionState.RUNNING ||
100
+ state === SessionState.DROPPED ||
101
+ state === SessionState.RESUMING
102
+ );
103
+ }
104
+
105
+ /**
106
+ * Session configuration options
107
+ */
108
+ export interface SessionOptions {
109
+ /**
110
+ * Timeout for initial connection response (milliseconds).
111
+ * @default 30000
112
+ */
113
+ connectionTimeout?: number;
114
+
115
+ /**
116
+ * Timeout for responses from the server (milliseconds).
117
+ * The session will be considered lost if no response is received within this time.
118
+ * @default 180000 (3 minutes)
119
+ */
120
+ responseTimeout?: number;
121
+
122
+ /**
123
+ * Maximum number of connection retry attempts.
124
+ * @default 3
125
+ */
126
+ maxRetryAttempts?: number;
127
+
128
+ /**
129
+ * Size of the message queue for outgoing messages.
130
+ * @default 1000
131
+ */
132
+ messageQueueSize?: number;
133
+
134
+ /**
135
+ * Enable rate limiting for outgoing messages.
136
+ * @default false
137
+ */
138
+ rateLimitEnabled?: boolean;
139
+
140
+ /**
141
+ * Interval between heartbeat checks (milliseconds).
142
+ * @default 10000 (10 seconds)
143
+ */
144
+ heartbeatInterval?: number;
145
+ }
146
+
147
+ /**
148
+ * Default session options
149
+ */
150
+ export const DEFAULT_SESSION_OPTIONS: Required<SessionOptions> = {
151
+ connectionTimeout: 30000,
152
+ responseTimeout: 180000,
153
+ maxRetryAttempts: 3,
154
+ messageQueueSize: 1000,
155
+ rateLimitEnabled: false,
156
+ heartbeatInterval: 10000,
157
+ };
158
+
159
+ /**
160
+ * Callback invoked when an HLA callback request is received from the RTI
161
+ */
162
+ export interface HlaCallbackRequestListener {
163
+ /**
164
+ * Handle an incoming HLA callback from the RTI.
165
+ *
166
+ * @param sequenceNumber - The sequence number of the callback request
167
+ * @param hlaCallback - The encoded callback data (protobuf CallbackRequest)
168
+ */
169
+ onHlaCallbackRequest(sequenceNumber: number, hlaCallback: Uint8Array): void;
170
+ }
171
+
172
+ /**
173
+ * Callback invoked when the session state changes
174
+ */
175
+ export interface SessionStateListener {
176
+ /**
177
+ * Handle a session state transition.
178
+ *
179
+ * @param oldState - The previous session state
180
+ * @param newState - The new session state
181
+ * @param reason - Description of why the transition occurred
182
+ */
183
+ onStateTransition(
184
+ oldState: SessionState,
185
+ newState: SessionState,
186
+ reason: string
187
+ ): void;
188
+ }
189
+
190
+ /**
191
+ * Callback invoked when a message is sent
192
+ */
193
+ export interface MessageSentListener {
194
+ /**
195
+ * Called when a message has been sent to the server.
196
+ * Can be used to implement heartbeat logic.
197
+ */
198
+ onMessageSent(): void;
199
+ }
200
+
201
+ /**
202
+ * Session statistics
203
+ */
204
+ export interface SessionStats {
205
+ /** Total HLA calls sent */
206
+ hlaCallCount: number;
207
+ /** Total HLA callbacks received */
208
+ hlaCallbackCount: number;
209
+ /** Number of times the session was resumed */
210
+ resumeCount: number;
211
+ /** Number of messages in the call request queue */
212
+ callRequestQueueLength: number;
213
+ /** Number of messages in the callback response queue */
214
+ callbackResponseQueueLength: number;
215
+ /** Number of HLA calls awaiting response */
216
+ pendingRequestCount: number;
217
+ }
218
+
219
+ /**
220
+ * New session status message payload structure
221
+ */
222
+ export interface NewSessionStatusPayload {
223
+ /** Status code (0 = success) */
224
+ status: number;
225
+ }
226
+
227
+ /**
228
+ * Resume status message payload structure
229
+ */
230
+ export interface ResumeStatusPayload {
231
+ /** Status code (0 = success) */
232
+ status: number;
233
+ /** Last sequence number received by the server */
234
+ lastReceivedFederateSequenceNumber: number;
235
+ }