jmri-client 4.2.0-beta.2 → 5.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.
Files changed (67) hide show
  1. package/README.md +3 -1
  2. package/dist/browser/jmri-client.js +88 -28
  3. package/dist/cjs/index.js +2442 -31
  4. package/dist/esm/index.js +2393 -17
  5. package/dist/types/client.d.ts +9 -1
  6. package/dist/types/index.d.ts +1 -1
  7. package/dist/types/managers/roster-manager.d.ts +9 -1
  8. package/dist/types/mocks/mock-data.d.ts +30 -6
  9. package/dist/types/mocks/mock-response-manager.d.ts +7 -2
  10. package/dist/types/types/jmri-messages.d.ts +22 -0
  11. package/docs/API.md +8 -0
  12. package/docs/BROWSER.md +4 -4
  13. package/docs/MIGRATION.md +30 -1
  14. package/docs/MOCK_MODE.md +15 -9
  15. package/package.json +17 -18
  16. package/dist/cjs/client.js +0 -366
  17. package/dist/cjs/core/connection-state-manager.js +0 -84
  18. package/dist/cjs/core/heartbeat-manager.js +0 -79
  19. package/dist/cjs/core/index.js +0 -25
  20. package/dist/cjs/core/message-queue.js +0 -59
  21. package/dist/cjs/core/reconnection-manager.js +0 -97
  22. package/dist/cjs/core/websocket-adapter.js +0 -135
  23. package/dist/cjs/core/websocket-client.js +0 -388
  24. package/dist/cjs/managers/index.js +0 -25
  25. package/dist/cjs/managers/light-manager.js +0 -111
  26. package/dist/cjs/managers/power-manager.js +0 -90
  27. package/dist/cjs/managers/roster-manager.js +0 -118
  28. package/dist/cjs/managers/system-connections-manager.js +0 -28
  29. package/dist/cjs/managers/throttle-manager.js +0 -233
  30. package/dist/cjs/managers/turnout-manager.js +0 -111
  31. package/dist/cjs/mocks/index.js +0 -12
  32. package/dist/cjs/mocks/mock-data.js +0 -237
  33. package/dist/cjs/mocks/mock-response-manager.js +0 -290
  34. package/dist/cjs/types/client-options.js +0 -66
  35. package/dist/cjs/types/events.js +0 -16
  36. package/dist/cjs/types/index.js +0 -23
  37. package/dist/cjs/types/jmri-messages.js +0 -95
  38. package/dist/cjs/types/throttle.js +0 -19
  39. package/dist/cjs/utils/exponential-backoff.js +0 -40
  40. package/dist/cjs/utils/index.js +0 -21
  41. package/dist/cjs/utils/message-id.js +0 -40
  42. package/dist/esm/client.js +0 -362
  43. package/dist/esm/core/connection-state-manager.js +0 -80
  44. package/dist/esm/core/heartbeat-manager.js +0 -75
  45. package/dist/esm/core/index.js +0 -9
  46. package/dist/esm/core/message-queue.js +0 -55
  47. package/dist/esm/core/reconnection-manager.js +0 -93
  48. package/dist/esm/core/websocket-adapter.js +0 -98
  49. package/dist/esm/core/websocket-client.js +0 -384
  50. package/dist/esm/managers/index.js +0 -9
  51. package/dist/esm/managers/light-manager.js +0 -107
  52. package/dist/esm/managers/power-manager.js +0 -86
  53. package/dist/esm/managers/roster-manager.js +0 -114
  54. package/dist/esm/managers/system-connections-manager.js +0 -24
  55. package/dist/esm/managers/throttle-manager.js +0 -229
  56. package/dist/esm/managers/turnout-manager.js +0 -107
  57. package/dist/esm/mocks/index.js +0 -6
  58. package/dist/esm/mocks/mock-data.js +0 -234
  59. package/dist/esm/mocks/mock-response-manager.js +0 -286
  60. package/dist/esm/types/client-options.js +0 -62
  61. package/dist/esm/types/events.js +0 -13
  62. package/dist/esm/types/index.js +0 -7
  63. package/dist/esm/types/jmri-messages.js +0 -89
  64. package/dist/esm/types/throttle.js +0 -15
  65. package/dist/esm/utils/exponential-backoff.js +0 -36
  66. package/dist/esm/utils/index.js +0 -5
  67. package/dist/esm/utils/message-id.js +0 -36
package/dist/cjs/index.js CHANGED
@@ -1,32 +1,2443 @@
1
1
  "use strict";
2
- /**
3
- * jmri-client v3.0
4
- * WebSocket client for JMRI with real-time updates and throttle control
5
- */
6
- Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.mockData = exports.mockResponseManager = exports.MockResponseManager = exports.lightStateToString = exports.turnoutStateToString = exports.powerStateToString = exports.isValidSpeed = exports.isThrottleFunctionKey = exports.ConnectionState = exports.LightState = exports.TurnoutState = exports.PowerState = exports.WebSocketClient = exports.JmriClient = void 0;
8
- var client_js_1 = require("./client.js");
9
- Object.defineProperty(exports, "JmriClient", { enumerable: true, get: function () { return client_js_1.JmriClient; } });
10
- var websocket_client_js_1 = require("./core/websocket-client.js");
11
- Object.defineProperty(exports, "WebSocketClient", { enumerable: true, get: function () { return websocket_client_js_1.WebSocketClient; } });
12
- // Export types
13
- var index_js_1 = require("./types/index.js");
14
- // JMRI message types
15
- Object.defineProperty(exports, "PowerState", { enumerable: true, get: function () { return index_js_1.PowerState; } });
16
- Object.defineProperty(exports, "TurnoutState", { enumerable: true, get: function () { return index_js_1.TurnoutState; } });
17
- Object.defineProperty(exports, "LightState", { enumerable: true, get: function () { return index_js_1.LightState; } });
18
- // Event types
19
- Object.defineProperty(exports, "ConnectionState", { enumerable: true, get: function () { return index_js_1.ConnectionState; } });
20
- // Export utility functions
21
- var throttle_js_1 = require("./types/throttle.js");
22
- Object.defineProperty(exports, "isThrottleFunctionKey", { enumerable: true, get: function () { return throttle_js_1.isThrottleFunctionKey; } });
23
- Object.defineProperty(exports, "isValidSpeed", { enumerable: true, get: function () { return throttle_js_1.isValidSpeed; } });
24
- var jmri_messages_js_1 = require("./types/jmri-messages.js");
25
- Object.defineProperty(exports, "powerStateToString", { enumerable: true, get: function () { return jmri_messages_js_1.powerStateToString; } });
26
- Object.defineProperty(exports, "turnoutStateToString", { enumerable: true, get: function () { return jmri_messages_js_1.turnoutStateToString; } });
27
- Object.defineProperty(exports, "lightStateToString", { enumerable: true, get: function () { return jmri_messages_js_1.lightStateToString; } });
28
- // Export mock system for testing and demo purposes
29
- var index_js_2 = require("./mocks/index.js");
30
- Object.defineProperty(exports, "MockResponseManager", { enumerable: true, get: function () { return index_js_2.MockResponseManager; } });
31
- Object.defineProperty(exports, "mockResponseManager", { enumerable: true, get: function () { return index_js_2.mockResponseManager; } });
32
- Object.defineProperty(exports, "mockData", { enumerable: true, get: function () { return index_js_2.mockData; } });
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ ConnectionState: () => ConnectionState,
34
+ JmriClient: () => JmriClient,
35
+ LightState: () => LightState,
36
+ MockResponseManager: () => MockResponseManager,
37
+ PowerState: () => PowerState,
38
+ TurnoutState: () => TurnoutState,
39
+ WebSocketClient: () => WebSocketClient,
40
+ isThrottleFunctionKey: () => isThrottleFunctionKey,
41
+ isValidSpeed: () => isValidSpeed,
42
+ lightStateToString: () => lightStateToString,
43
+ mockData: () => mockData,
44
+ mockResponseManager: () => mockResponseManager,
45
+ powerStateToString: () => powerStateToString,
46
+ turnoutStateToString: () => turnoutStateToString
47
+ });
48
+ module.exports = __toCommonJS(index_exports);
49
+
50
+ // src/client.ts
51
+ var import_eventemitter310 = require("eventemitter3");
52
+
53
+ // src/core/websocket-client.ts
54
+ var import_eventemitter35 = require("eventemitter3");
55
+
56
+ // src/core/websocket-adapter.ts
57
+ var import_eventemitter3 = require("eventemitter3");
58
+ async function createWebSocketAdapter(url, protocols) {
59
+ const isBrowser = typeof window !== "undefined" && typeof window.WebSocket !== "undefined";
60
+ if (isBrowser) {
61
+ return new BrowserWebSocketAdapter(url, protocols);
62
+ } else {
63
+ return await NodeWebSocketAdapter.create(url, protocols);
64
+ }
65
+ }
66
+ var BrowserWebSocketAdapter = class extends import_eventemitter3.EventEmitter {
67
+ constructor(url, protocols) {
68
+ super();
69
+ this.ws = new WebSocket(url, protocols);
70
+ this.ws.onopen = () => {
71
+ this.emit("open");
72
+ };
73
+ this.ws.onmessage = (event) => {
74
+ this.emit("message", event.data);
75
+ };
76
+ this.ws.onerror = () => {
77
+ this.emit("error", new Error("WebSocket error"));
78
+ };
79
+ this.ws.onclose = (event) => {
80
+ this.emit("close", event.code, event.reason);
81
+ };
82
+ }
83
+ send(data) {
84
+ this.ws.send(data);
85
+ }
86
+ close(code, reason) {
87
+ this.ws.close(code, reason);
88
+ }
89
+ get readyState() {
90
+ return this.ws.readyState;
91
+ }
92
+ };
93
+ var NodeWebSocketAdapter = class _NodeWebSocketAdapter extends import_eventemitter3.EventEmitter {
94
+ constructor(ws) {
95
+ super();
96
+ this.ws = ws;
97
+ this.ws.on("open", () => {
98
+ this.emit("open");
99
+ });
100
+ this.ws.on("message", (data) => {
101
+ const message = typeof data === "string" ? data : data.toString();
102
+ this.emit("message", message);
103
+ });
104
+ this.ws.on("error", (error) => {
105
+ this.emit("error", error);
106
+ });
107
+ this.ws.on("close", (code, reason) => {
108
+ this.emit("close", code, reason);
109
+ });
110
+ }
111
+ static async create(url, protocols) {
112
+ const { default: WebSocket2 } = await import("ws");
113
+ const ws = new WebSocket2(url, protocols);
114
+ return new _NodeWebSocketAdapter(ws);
115
+ }
116
+ send(data) {
117
+ this.ws.send(data);
118
+ }
119
+ close(code, reason) {
120
+ this.ws.close(code, reason);
121
+ }
122
+ get readyState() {
123
+ return this.ws.readyState;
124
+ }
125
+ };
126
+
127
+ // src/types/events.ts
128
+ var ConnectionState = /* @__PURE__ */ ((ConnectionState2) => {
129
+ ConnectionState2["DISCONNECTED"] = "disconnected";
130
+ ConnectionState2["CONNECTING"] = "connecting";
131
+ ConnectionState2["CONNECTED"] = "connected";
132
+ ConnectionState2["RECONNECTING"] = "reconnecting";
133
+ return ConnectionState2;
134
+ })(ConnectionState || {});
135
+
136
+ // src/utils/message-id.ts
137
+ var MessageIdGenerator = class {
138
+ constructor() {
139
+ this.currentId = 0;
140
+ this.maxId = Number.MAX_SAFE_INTEGER;
141
+ }
142
+ /**
143
+ * Generate next sequential ID
144
+ * Wraps around at MAX_SAFE_INTEGER
145
+ */
146
+ next() {
147
+ this.currentId++;
148
+ if (this.currentId >= this.maxId) {
149
+ this.currentId = 1;
150
+ }
151
+ return this.currentId;
152
+ }
153
+ /**
154
+ * Reset ID counter to 0
155
+ */
156
+ reset() {
157
+ this.currentId = 0;
158
+ }
159
+ /**
160
+ * Get current ID without incrementing
161
+ */
162
+ current() {
163
+ return this.currentId;
164
+ }
165
+ };
166
+
167
+ // src/core/message-queue.ts
168
+ var MessageQueue = class {
169
+ constructor(maxSize = 100) {
170
+ this.queue = [];
171
+ this.maxSize = maxSize;
172
+ }
173
+ /**
174
+ * Add message to queue
175
+ * If queue is full, oldest message is removed
176
+ */
177
+ enqueue(message) {
178
+ if (this.queue.length >= this.maxSize) {
179
+ this.queue.shift();
180
+ }
181
+ this.queue.push(message);
182
+ }
183
+ /**
184
+ * Get all queued messages and clear queue
185
+ */
186
+ flush() {
187
+ const messages = [...this.queue];
188
+ this.queue = [];
189
+ return messages;
190
+ }
191
+ /**
192
+ * Clear all queued messages without returning them
193
+ */
194
+ clear() {
195
+ this.queue = [];
196
+ }
197
+ /**
198
+ * Get number of queued messages
199
+ */
200
+ size() {
201
+ return this.queue.length;
202
+ }
203
+ /**
204
+ * Check if queue is empty
205
+ */
206
+ isEmpty() {
207
+ return this.queue.length === 0;
208
+ }
209
+ /**
210
+ * Check if queue is full
211
+ */
212
+ isFull() {
213
+ return this.queue.length >= this.maxSize;
214
+ }
215
+ };
216
+
217
+ // src/core/connection-state-manager.ts
218
+ var import_eventemitter32 = require("eventemitter3");
219
+ var VALID_TRANSITIONS = {
220
+ ["disconnected" /* DISCONNECTED */]: ["connecting" /* CONNECTING */],
221
+ ["connecting" /* CONNECTING */]: ["connected" /* CONNECTED */, "disconnected" /* DISCONNECTED */],
222
+ ["connected" /* CONNECTED */]: ["disconnected" /* DISCONNECTED */, "reconnecting" /* RECONNECTING */],
223
+ ["reconnecting" /* RECONNECTING */]: ["connecting" /* CONNECTING */, "connected" /* CONNECTED */, "disconnected" /* DISCONNECTED */]
224
+ };
225
+ var ConnectionStateManager = class extends import_eventemitter32.EventEmitter {
226
+ constructor() {
227
+ super(...arguments);
228
+ this.currentState = "disconnected" /* DISCONNECTED */;
229
+ }
230
+ /**
231
+ * Get current connection state
232
+ */
233
+ getState() {
234
+ return this.currentState;
235
+ }
236
+ /**
237
+ * Check if currently connected
238
+ */
239
+ isConnected() {
240
+ return this.currentState === "connected" /* CONNECTED */;
241
+ }
242
+ /**
243
+ * Check if currently connecting
244
+ */
245
+ isConnecting() {
246
+ return this.currentState === "connecting" /* CONNECTING */;
247
+ }
248
+ /**
249
+ * Check if currently disconnected
250
+ */
251
+ isDisconnected() {
252
+ return this.currentState === "disconnected" /* DISCONNECTED */;
253
+ }
254
+ /**
255
+ * Check if currently reconnecting
256
+ */
257
+ isReconnecting() {
258
+ return this.currentState === "reconnecting" /* RECONNECTING */;
259
+ }
260
+ /**
261
+ * Transition to new state
262
+ * Validates transition and emits event
263
+ */
264
+ transition(newState) {
265
+ const validTransitions = VALID_TRANSITIONS[this.currentState];
266
+ if (!validTransitions.includes(newState)) {
267
+ throw new Error(
268
+ `Invalid state transition: ${this.currentState} -> ${newState}`
269
+ );
270
+ }
271
+ const previousState = this.currentState;
272
+ this.currentState = newState;
273
+ this.emit("stateChanged", newState, previousState);
274
+ }
275
+ /**
276
+ * Force state without validation (use with caution)
277
+ */
278
+ forceState(newState) {
279
+ const previousState = this.currentState;
280
+ this.currentState = newState;
281
+ this.emit("stateChanged", newState, previousState);
282
+ }
283
+ /**
284
+ * Reset to disconnected state
285
+ */
286
+ reset() {
287
+ this.forceState("disconnected" /* DISCONNECTED */);
288
+ }
289
+ };
290
+
291
+ // src/core/heartbeat-manager.ts
292
+ var import_eventemitter33 = require("eventemitter3");
293
+ var HeartbeatManager = class extends import_eventemitter33.EventEmitter {
294
+ constructor(options) {
295
+ super();
296
+ this.isRunning = false;
297
+ this.options = options;
298
+ }
299
+ /**
300
+ * Start heartbeat monitoring
301
+ * @param sendPing - Callback to send ping message
302
+ */
303
+ start(sendPing) {
304
+ if (!this.options.enabled || this.isRunning) {
305
+ return;
306
+ }
307
+ this.isRunning = true;
308
+ this.pingInterval = setInterval(() => {
309
+ sendPing();
310
+ this.emit("pingSent");
311
+ this.pongTimeout = setTimeout(() => {
312
+ this.emit("timeout");
313
+ }, this.options.timeout);
314
+ }, this.options.interval);
315
+ }
316
+ /**
317
+ * Stop heartbeat monitoring
318
+ */
319
+ stop() {
320
+ this.isRunning = false;
321
+ if (this.pingInterval) {
322
+ clearInterval(this.pingInterval);
323
+ this.pingInterval = void 0;
324
+ }
325
+ if (this.pongTimeout) {
326
+ clearTimeout(this.pongTimeout);
327
+ this.pongTimeout = void 0;
328
+ }
329
+ }
330
+ /**
331
+ * Handle pong received from server
332
+ */
333
+ receivedPong() {
334
+ if (this.pongTimeout) {
335
+ clearTimeout(this.pongTimeout);
336
+ this.pongTimeout = void 0;
337
+ }
338
+ this.emit("pongReceived");
339
+ }
340
+ /**
341
+ * Check if heartbeat is running
342
+ */
343
+ running() {
344
+ return this.isRunning;
345
+ }
346
+ /**
347
+ * Update heartbeat options
348
+ */
349
+ updateOptions(options) {
350
+ const wasRunning = this.isRunning;
351
+ if (wasRunning) {
352
+ this.stop();
353
+ }
354
+ this.options = { ...this.options, ...options };
355
+ }
356
+ };
357
+
358
+ // src/core/reconnection-manager.ts
359
+ var import_eventemitter34 = require("eventemitter3");
360
+
361
+ // src/utils/exponential-backoff.ts
362
+ function calculateBackoffDelay(attempt, options) {
363
+ const exponentialDelay = options.initialDelay * Math.pow(options.multiplier, attempt - 1);
364
+ const cappedDelay = Math.min(exponentialDelay, options.maxDelay);
365
+ if (options.jitter) {
366
+ const jitterAmount = cappedDelay * 0.25;
367
+ const jitter = (Math.random() * 2 - 1) * jitterAmount;
368
+ return Math.max(0, Math.round(cappedDelay + jitter));
369
+ }
370
+ return Math.round(cappedDelay);
371
+ }
372
+ function shouldReconnect(attempt, maxAttempts) {
373
+ if (maxAttempts === 0) {
374
+ return true;
375
+ }
376
+ return attempt <= maxAttempts;
377
+ }
378
+
379
+ // src/core/reconnection-manager.ts
380
+ var ReconnectionManager = class extends import_eventemitter34.EventEmitter {
381
+ constructor(options) {
382
+ super();
383
+ this.currentAttempt = 0;
384
+ this.isReconnecting = false;
385
+ this.options = options;
386
+ }
387
+ /**
388
+ * Start reconnection process
389
+ * @param reconnect - Callback to attempt reconnection
390
+ */
391
+ start(reconnect) {
392
+ if (!this.options.enabled || this.isReconnecting) {
393
+ return;
394
+ }
395
+ this.isReconnecting = true;
396
+ this.currentAttempt = 0;
397
+ this.scheduleNextAttempt(reconnect);
398
+ }
399
+ /**
400
+ * Schedule next reconnection attempt
401
+ */
402
+ scheduleNextAttempt(reconnect) {
403
+ this.currentAttempt++;
404
+ if (!shouldReconnect(this.currentAttempt, this.options.maxAttempts)) {
405
+ this.emit("maxAttemptsReached", this.currentAttempt - 1);
406
+ this.stop();
407
+ return;
408
+ }
409
+ const delay = calculateBackoffDelay(this.currentAttempt, this.options);
410
+ this.emit("attemptScheduled", this.currentAttempt, delay);
411
+ this.reconnectTimeout = setTimeout(async () => {
412
+ this.emit("attempting", this.currentAttempt);
413
+ try {
414
+ await reconnect();
415
+ this.stop();
416
+ this.emit("success", this.currentAttempt);
417
+ } catch (error) {
418
+ this.emit("failed", this.currentAttempt, error);
419
+ this.scheduleNextAttempt(reconnect);
420
+ }
421
+ }, delay);
422
+ }
423
+ /**
424
+ * Stop reconnection process
425
+ */
426
+ stop() {
427
+ this.isReconnecting = false;
428
+ if (this.reconnectTimeout) {
429
+ clearTimeout(this.reconnectTimeout);
430
+ this.reconnectTimeout = void 0;
431
+ }
432
+ }
433
+ /**
434
+ * Reset attempt counter
435
+ */
436
+ reset() {
437
+ this.stop();
438
+ this.currentAttempt = 0;
439
+ }
440
+ /**
441
+ * Check if currently reconnecting
442
+ */
443
+ reconnecting() {
444
+ return this.isReconnecting;
445
+ }
446
+ /**
447
+ * Get current attempt number
448
+ */
449
+ getAttempt() {
450
+ return this.currentAttempt;
451
+ }
452
+ /**
453
+ * Update reconnection options
454
+ */
455
+ updateOptions(options) {
456
+ this.options = { ...this.options, ...options };
457
+ }
458
+ };
459
+
460
+ // src/types/jmri-messages.ts
461
+ var PowerState = /* @__PURE__ */ ((PowerState2) => {
462
+ PowerState2[PowerState2["UNKNOWN"] = 0] = "UNKNOWN";
463
+ PowerState2[PowerState2["ON"] = 2] = "ON";
464
+ PowerState2[PowerState2["OFF"] = 4] = "OFF";
465
+ return PowerState2;
466
+ })(PowerState || {});
467
+ function powerStateToString(state) {
468
+ switch (state) {
469
+ case 2 /* ON */:
470
+ return "ON";
471
+ case 4 /* OFF */:
472
+ return "OFF";
473
+ case 0 /* UNKNOWN */:
474
+ return "UNKNOWN";
475
+ default:
476
+ return "UNKNOWN";
477
+ }
478
+ }
479
+ var TurnoutState = /* @__PURE__ */ ((TurnoutState2) => {
480
+ TurnoutState2[TurnoutState2["UNKNOWN"] = 0] = "UNKNOWN";
481
+ TurnoutState2[TurnoutState2["CLOSED"] = 2] = "CLOSED";
482
+ TurnoutState2[TurnoutState2["THROWN"] = 4] = "THROWN";
483
+ TurnoutState2[TurnoutState2["INCONSISTENT"] = 8] = "INCONSISTENT";
484
+ return TurnoutState2;
485
+ })(TurnoutState || {});
486
+ function turnoutStateToString(state) {
487
+ switch (state) {
488
+ case 2 /* CLOSED */:
489
+ return "CLOSED";
490
+ case 4 /* THROWN */:
491
+ return "THROWN";
492
+ case 8 /* INCONSISTENT */:
493
+ return "INCONSISTENT";
494
+ case 0 /* UNKNOWN */:
495
+ default:
496
+ return "UNKNOWN";
497
+ }
498
+ }
499
+ var LightState = /* @__PURE__ */ ((LightState2) => {
500
+ LightState2[LightState2["UNKNOWN"] = 0] = "UNKNOWN";
501
+ LightState2[LightState2["ON"] = 2] = "ON";
502
+ LightState2[LightState2["OFF"] = 4] = "OFF";
503
+ return LightState2;
504
+ })(LightState || {});
505
+ function lightStateToString(state) {
506
+ switch (state) {
507
+ case 2 /* ON */:
508
+ return "ON";
509
+ case 4 /* OFF */:
510
+ return "OFF";
511
+ case 0 /* UNKNOWN */:
512
+ default:
513
+ return "UNKNOWN";
514
+ }
515
+ }
516
+
517
+ // src/mocks/mock-data.ts
518
+ var mockData = {
519
+ "hello": {
520
+ "type": "hello",
521
+ "data": {
522
+ "JMRI": "5.9.2",
523
+ "json": "5.0",
524
+ "version": "v5",
525
+ "heartbeat": 13500,
526
+ "railroad": "Demo Railroad",
527
+ "node": "jmri-server",
528
+ "activeProfile": "Demo Profile"
529
+ }
530
+ },
531
+ "power": {
532
+ "get": {
533
+ "on": {
534
+ "type": "power",
535
+ "data": {
536
+ "state": 2
537
+ }
538
+ },
539
+ "off": {
540
+ "type": "power",
541
+ "data": {
542
+ "state": 4
543
+ }
544
+ }
545
+ },
546
+ "post": {
547
+ "success": {
548
+ "type": "power",
549
+ "data": {
550
+ "state": 2
551
+ }
552
+ }
553
+ }
554
+ },
555
+ "roster": {
556
+ "list": [
557
+ {
558
+ "type": "rosterEntry",
559
+ "data": {
560
+ "name": "CSX754",
561
+ "address": "754",
562
+ "isLongAddress": true,
563
+ "road": "CSX",
564
+ "number": "754",
565
+ "mfg": "Athearn",
566
+ "decoderModel": "DH163D",
567
+ "decoderFamily": "Digitrax DH163",
568
+ "model": "GP38-2",
569
+ "comment": "Blue and yellow scheme",
570
+ "maxSpeedPct": 100,
571
+ "image": null,
572
+ "icon": "/roster/CSX754/icon",
573
+ "shuntingFunction": "",
574
+ "owner": "",
575
+ "dateModified": "2026-02-10T00:00:00.000+00:00",
576
+ "functionKeys": [
577
+ { "name": "F0", "label": "Headlight", "lockable": true, "icon": null, "selectedIcon": null },
578
+ { "name": "F1", "label": "Bell", "lockable": true, "icon": null, "selectedIcon": null },
579
+ { "name": "F2", "label": "Horn", "lockable": false, "icon": null, "selectedIcon": null },
580
+ { "name": "F3", "label": null, "lockable": false, "icon": null, "selectedIcon": null },
581
+ { "name": "F4", "label": "Dynamic Brake", "lockable": true, "icon": null, "selectedIcon": null },
582
+ { "name": "F5", "label": null, "lockable": false, "icon": null, "selectedIcon": null }
583
+ ],
584
+ "attributes": [{ "name": "RosterGroup:diesels", "value": "yes" }],
585
+ "rosterGroups": ["diesels"]
586
+ },
587
+ "id": 1
588
+ },
589
+ {
590
+ "type": "rosterEntry",
591
+ "data": {
592
+ "name": "UP3985",
593
+ "address": "3985",
594
+ "isLongAddress": true,
595
+ "road": "Union Pacific",
596
+ "number": "3985",
597
+ "mfg": "Rivarossi",
598
+ "decoderModel": "Sound decoder",
599
+ "decoderFamily": "ESU LokSound",
600
+ "model": "Challenger 4-6-6-4",
601
+ "comment": "Steam locomotive",
602
+ "maxSpeedPct": 100,
603
+ "image": null,
604
+ "icon": "/roster/UP3985/icon",
605
+ "shuntingFunction": "",
606
+ "owner": "",
607
+ "dateModified": "2026-02-10T00:00:00.000+00:00",
608
+ "functionKeys": [
609
+ { "name": "F0", "label": "Headlight", "lockable": true, "icon": null, "selectedIcon": null },
610
+ { "name": "F1", "label": "Bell", "lockable": true, "icon": null, "selectedIcon": null },
611
+ { "name": "F2", "label": "Whistle", "lockable": false, "icon": null, "selectedIcon": null },
612
+ { "name": "F3", "label": "Steam", "lockable": true, "icon": null, "selectedIcon": null },
613
+ { "name": "F4", "label": null, "lockable": false, "icon": null, "selectedIcon": null }
614
+ ],
615
+ "attributes": [{ "name": "RosterGroup:steam", "value": "yes" }],
616
+ "rosterGroups": ["steam"]
617
+ },
618
+ "id": 2
619
+ },
620
+ {
621
+ "type": "rosterEntry",
622
+ "data": {
623
+ "name": "BNSF5240",
624
+ "address": "5240",
625
+ "isLongAddress": true,
626
+ "road": "BNSF",
627
+ "number": "5240",
628
+ "mfg": "Kato",
629
+ "decoderModel": "DCC Sound",
630
+ "decoderFamily": "Kato",
631
+ "model": "SD40-2",
632
+ "comment": "Heritage II paint",
633
+ "maxSpeedPct": 100,
634
+ "image": null,
635
+ "icon": "/roster/BNSF5240/icon",
636
+ "shuntingFunction": "",
637
+ "owner": "",
638
+ "dateModified": "2026-02-10T00:00:00.000+00:00",
639
+ "functionKeys": [
640
+ { "name": "F0", "label": "Headlight", "lockable": true, "icon": null, "selectedIcon": null },
641
+ { "name": "F1", "label": "Bell", "lockable": true, "icon": null, "selectedIcon": null },
642
+ { "name": "F2", "label": "Horn", "lockable": false, "icon": null, "selectedIcon": null },
643
+ { "name": "F3", "label": "Dynamic Brake", "lockable": true, "icon": null, "selectedIcon": null },
644
+ { "name": "F4", "label": null, "lockable": false, "icon": null, "selectedIcon": null },
645
+ { "name": "F5", "label": "Mars Light", "lockable": true, "icon": null, "selectedIcon": null }
646
+ ],
647
+ "attributes": [{ "name": "RosterGroup:diesels", "value": "yes" }],
648
+ "rosterGroups": ["diesels"]
649
+ },
650
+ "id": 3
651
+ }
652
+ ]
653
+ },
654
+ "rosterGroup": {
655
+ "list": [
656
+ { "type": "rosterGroup", "data": { "name": "diesels", "length": 2 } },
657
+ { "type": "rosterGroup", "data": { "name": "steam", "length": 1 } }
658
+ ]
659
+ },
660
+ "throttle": {
661
+ "acquire": {
662
+ "success": {
663
+ "type": "throttle",
664
+ "data": {
665
+ "throttle": "{THROTTLE_ID}",
666
+ "address": "{ADDRESS}",
667
+ "speed": 0,
668
+ "forward": true,
669
+ "F0": false,
670
+ "F1": false,
671
+ "F2": false,
672
+ "F3": false,
673
+ "F4": false
674
+ }
675
+ }
676
+ },
677
+ "release": {
678
+ "success": {
679
+ "type": "throttle",
680
+ "data": {}
681
+ }
682
+ },
683
+ "control": {
684
+ "speed": {
685
+ "type": "throttle",
686
+ "data": {
687
+ "throttle": "{THROTTLE_ID}",
688
+ "speed": "{SPEED}"
689
+ }
690
+ },
691
+ "direction": {
692
+ "type": "throttle",
693
+ "data": {
694
+ "throttle": "{THROTTLE_ID}",
695
+ "forward": "{FORWARD}"
696
+ }
697
+ },
698
+ "function": {
699
+ "type": "throttle",
700
+ "data": {
701
+ "throttle": "{THROTTLE_ID}",
702
+ "{FUNCTION}": "{VALUE}"
703
+ }
704
+ }
705
+ }
706
+ },
707
+ "light": {
708
+ "list": [
709
+ { "type": "light", "data": { "name": "IL1", "userName": "Yard Light", "comment": null, "properties": [], "state": 4 } },
710
+ { "type": "light", "data": { "name": "IL2", "userName": "Platform Light", "comment": null, "properties": [], "state": 4 } },
711
+ { "type": "light", "data": { "name": "IL3", "userName": "Signal Lamp", "comment": null, "properties": [], "state": 2 } }
712
+ ]
713
+ },
714
+ "turnout": {
715
+ "list": [
716
+ { "type": "turnout", "data": { "name": "LT1", "userName": "Main Diverge", "state": 2 } },
717
+ { "type": "turnout", "data": { "name": "LT2", "userName": "Yard Lead", "state": 2 } },
718
+ { "type": "turnout", "data": { "name": "LT3", "userName": "Siding Entry", "state": 4 } }
719
+ ]
720
+ },
721
+ "ping": {
722
+ "type": "ping"
723
+ },
724
+ "pong": {
725
+ "type": "pong"
726
+ },
727
+ "goodbye": {
728
+ "type": "goodbye"
729
+ },
730
+ "error": {
731
+ "throttleNotFound": {
732
+ "type": "error",
733
+ "data": {
734
+ "code": 404,
735
+ "message": "Throttle not found"
736
+ }
737
+ },
738
+ "invalidSpeed": {
739
+ "type": "error",
740
+ "data": {
741
+ "code": 400,
742
+ "message": "Invalid speed value"
743
+ }
744
+ },
745
+ "connectionError": {
746
+ "type": "error",
747
+ "data": {
748
+ "code": 500,
749
+ "message": "Connection error"
750
+ }
751
+ }
752
+ }
753
+ };
754
+
755
+ // src/mocks/mock-response-manager.ts
756
+ var MockResponseManager = class {
757
+ constructor(options = {}) {
758
+ this.powerStateByPrefix = /* @__PURE__ */ new Map();
759
+ this.throttles = /* @__PURE__ */ new Map();
760
+ this.lights = /* @__PURE__ */ new Map([
761
+ ["IL1", 4 /* OFF */],
762
+ ["IL2", 4 /* OFF */],
763
+ ["IL3", 2 /* ON */]
764
+ ]);
765
+ this.turnouts = /* @__PURE__ */ new Map([
766
+ ["LT1", 2 /* CLOSED */],
767
+ ["LT2", 2 /* CLOSED */],
768
+ ["LT3", 4 /* THROWN */]
769
+ ]);
770
+ this.responseDelay = options.responseDelay ?? 50;
771
+ this.powerState = options.initialPowerState ?? 4 /* OFF */;
772
+ }
773
+ /**
774
+ * Get a mock response for a given message
775
+ */
776
+ async getMockResponse(message) {
777
+ if (this.responseDelay > 0) {
778
+ await this.delay(this.responseDelay);
779
+ }
780
+ switch (message.type) {
781
+ case "hello":
782
+ return this.getHelloResponse();
783
+ case "power":
784
+ return this.getPowerResponse(message);
785
+ case "roster":
786
+ return this.getRosterResponse(message);
787
+ case "rosterGroup":
788
+ return this.getRosterGroupResponse();
789
+ case "throttle":
790
+ return this.getThrottleResponse(message);
791
+ case "light":
792
+ return this.getLightResponse(message);
793
+ case "turnout":
794
+ return this.getTurnoutResponse(message);
795
+ case "ping":
796
+ return this.getPingResponse();
797
+ case "goodbye":
798
+ return this.getGoodbyeResponse();
799
+ default:
800
+ return null;
801
+ }
802
+ }
803
+ /**
804
+ * Get hello response (connection establishment)
805
+ */
806
+ getHelloResponse() {
807
+ return JSON.parse(JSON.stringify(mockData.hello));
808
+ }
809
+ /**
810
+ * Get power response, with optional per-prefix state tracking
811
+ */
812
+ getPowerResponse(message) {
813
+ const prefix = message.data?.prefix;
814
+ if (message.method === "post" && message.data?.state !== void 0) {
815
+ if (prefix !== void 0) {
816
+ this.powerStateByPrefix.set(prefix, message.data.state);
817
+ return { type: "power", data: { state: message.data.state, prefix } };
818
+ }
819
+ this.powerState = message.data.state;
820
+ return { type: "power", data: { state: this.powerState } };
821
+ }
822
+ if (prefix !== void 0) {
823
+ const state = this.powerStateByPrefix.get(prefix) ?? 0 /* UNKNOWN */;
824
+ return { type: "power", data: { state, prefix } };
825
+ }
826
+ return { type: "power", data: { state: this.powerState } };
827
+ }
828
+ /**
829
+ * Get roster response, optionally filtered by group
830
+ */
831
+ getRosterResponse(message) {
832
+ if (message.type === "roster" && message.method === "list") {
833
+ const all = JSON.parse(JSON.stringify(mockData.roster.list));
834
+ const group = message.params?.group;
835
+ if (group) {
836
+ return {
837
+ type: "roster",
838
+ data: all.filter((e) => e.data.rosterGroups?.includes(group))
839
+ };
840
+ }
841
+ return { type: "roster", data: all };
842
+ }
843
+ return { type: "roster", data: [] };
844
+ }
845
+ /**
846
+ * Get roster group response
847
+ */
848
+ getRosterGroupResponse() {
849
+ return {
850
+ type: "rosterGroup",
851
+ data: JSON.parse(JSON.stringify(mockData.rosterGroup.list))
852
+ };
853
+ }
854
+ /**
855
+ * Get throttle response
856
+ */
857
+ getThrottleResponse(message) {
858
+ const data = message.data || {};
859
+ if (data.address !== void 0 && !data.throttle) {
860
+ const throttleId = data.name || `MOCK-${data.address}`;
861
+ const throttleState = {
862
+ throttle: throttleId,
863
+ address: data.address,
864
+ speed: 0,
865
+ forward: true,
866
+ F0: false,
867
+ F1: false,
868
+ F2: false,
869
+ F3: false,
870
+ F4: false,
871
+ ...data.prefix !== void 0 && { prefix: data.prefix }
872
+ };
873
+ this.throttles.set(throttleId, throttleState);
874
+ return { type: "throttle", data: { ...throttleState } };
875
+ }
876
+ if (data.release !== void 0 && data.throttle) {
877
+ this.throttles.delete(data.throttle);
878
+ return {
879
+ type: "throttle",
880
+ data: {}
881
+ };
882
+ }
883
+ if (data.throttle) {
884
+ const throttleState = this.throttles.get(data.throttle);
885
+ if (!throttleState) {
886
+ const newState = {
887
+ throttle: data.throttle,
888
+ address: 0,
889
+ speed: 0,
890
+ forward: true
891
+ };
892
+ this.throttles.set(data.throttle, newState);
893
+ return {
894
+ type: "throttle",
895
+ data: { ...newState }
896
+ };
897
+ }
898
+ if (data.speed !== void 0) {
899
+ throttleState.speed = data.speed;
900
+ }
901
+ if (data.forward !== void 0) {
902
+ throttleState.forward = data.forward;
903
+ }
904
+ for (let i = 0; i <= 28; i++) {
905
+ const key = `F${i}`;
906
+ if (data[key] !== void 0) {
907
+ throttleState[key] = data[key];
908
+ }
909
+ }
910
+ return {
911
+ type: "throttle",
912
+ data: {}
913
+ };
914
+ }
915
+ return {
916
+ type: "throttle",
917
+ data: {}
918
+ };
919
+ }
920
+ /**
921
+ * Get light response
922
+ */
923
+ getLightResponse(message) {
924
+ if (message.method === "list") {
925
+ return {
926
+ type: "light",
927
+ data: JSON.parse(JSON.stringify(mockData.light.list))
928
+ };
929
+ }
930
+ const name = message.data?.name;
931
+ if (!name) {
932
+ return { type: "light", data: { name: "", state: 0 /* UNKNOWN */ } };
933
+ }
934
+ if (message.method === "post" && message.data?.state !== void 0) {
935
+ this.lights.set(name, message.data.state);
936
+ }
937
+ const state = this.lights.get(name) ?? 0 /* UNKNOWN */;
938
+ return { type: "light", data: { name, state } };
939
+ }
940
+ /**
941
+ * Get turnout response
942
+ */
943
+ getTurnoutResponse(message) {
944
+ if (message.method === "list") {
945
+ return {
946
+ type: "turnout",
947
+ data: JSON.parse(JSON.stringify(mockData.turnout.list))
948
+ };
949
+ }
950
+ const name = message.data?.name;
951
+ if (!name) {
952
+ return { type: "turnout", data: { name: "", state: 0 /* UNKNOWN */ } };
953
+ }
954
+ if (message.method === "post" && message.data?.state !== void 0) {
955
+ this.turnouts.set(name, message.data.state);
956
+ }
957
+ const state = this.turnouts.get(name) ?? 0 /* UNKNOWN */;
958
+ return { type: "turnout", data: { name, state } };
959
+ }
960
+ /**
961
+ * Get ping response (pong)
962
+ */
963
+ getPingResponse() {
964
+ return JSON.parse(JSON.stringify(mockData.pong));
965
+ }
966
+ /**
967
+ * Get goodbye response
968
+ */
969
+ getGoodbyeResponse() {
970
+ return JSON.parse(JSON.stringify(mockData.goodbye));
971
+ }
972
+ /**
973
+ * Get current power state
974
+ */
975
+ getPowerState() {
976
+ return this.powerState;
977
+ }
978
+ /**
979
+ * Set power state (for testing)
980
+ */
981
+ setPowerState(state) {
982
+ this.powerState = state;
983
+ }
984
+ /**
985
+ * Get all throttles (for testing)
986
+ */
987
+ getThrottles() {
988
+ return this.throttles;
989
+ }
990
+ /**
991
+ * Get all light states (for testing)
992
+ */
993
+ getLights() {
994
+ return this.lights;
995
+ }
996
+ /**
997
+ * Get all turnout states (for testing)
998
+ */
999
+ getTurnouts() {
1000
+ return this.turnouts;
1001
+ }
1002
+ /**
1003
+ * Reset all state (for testing)
1004
+ */
1005
+ reset() {
1006
+ this.powerState = 4 /* OFF */;
1007
+ this.powerStateByPrefix.clear();
1008
+ this.throttles.clear();
1009
+ this.lights = /* @__PURE__ */ new Map([
1010
+ ["IL1", 4 /* OFF */],
1011
+ ["IL2", 4 /* OFF */],
1012
+ ["IL3", 2 /* ON */]
1013
+ ]);
1014
+ this.turnouts = /* @__PURE__ */ new Map([
1015
+ ["LT1", 2 /* CLOSED */],
1016
+ ["LT2", 2 /* CLOSED */],
1017
+ ["LT3", 4 /* THROWN */]
1018
+ ]);
1019
+ }
1020
+ /**
1021
+ * Delay helper
1022
+ */
1023
+ delay(ms) {
1024
+ return new Promise((resolve) => setTimeout(resolve, ms));
1025
+ }
1026
+ };
1027
+ var mockResponseManager = new MockResponseManager({ responseDelay: 0 });
1028
+
1029
+ // src/core/websocket-client.ts
1030
+ var WebSocketClient = class extends import_eventemitter35.EventEmitter {
1031
+ constructor(options) {
1032
+ super();
1033
+ // Request/response tracking
1034
+ this.pendingRequests = /* @__PURE__ */ new Map();
1035
+ // Connection state
1036
+ this.isManualDisconnect = false;
1037
+ this.options = options;
1038
+ this.url = `${options.protocol}://${options.host}:${options.port}/json/`;
1039
+ this.messageIdGen = new MessageIdGenerator();
1040
+ this.messageQueue = new MessageQueue(options.messageQueueSize);
1041
+ this.stateManager = new ConnectionStateManager();
1042
+ this.heartbeatManager = new HeartbeatManager(options.heartbeat);
1043
+ this.reconnectionManager = new ReconnectionManager(options.reconnection);
1044
+ if (options.mock.enabled) {
1045
+ this.mockManager = new MockResponseManager({
1046
+ responseDelay: options.mock.responseDelay
1047
+ });
1048
+ }
1049
+ this.stateManager.on("stateChanged", (newState, prevState) => {
1050
+ this.emit("connectionStateChanged", newState, prevState);
1051
+ });
1052
+ this.heartbeatManager.on("timeout", () => {
1053
+ this.emit("heartbeat:timeout");
1054
+ this.handleHeartbeatTimeout();
1055
+ });
1056
+ this.heartbeatManager.on("pingSent", () => {
1057
+ this.emit("heartbeat:sent");
1058
+ });
1059
+ this.reconnectionManager.on("attemptScheduled", (attempt, delay) => {
1060
+ this.emit("reconnecting", attempt, delay);
1061
+ });
1062
+ this.reconnectionManager.on("success", () => {
1063
+ this.emit("reconnected");
1064
+ });
1065
+ this.reconnectionManager.on("maxAttemptsReached", (attempts) => {
1066
+ this.emit("reconnectionFailed", attempts);
1067
+ });
1068
+ this.reconnectionManager.on("debug", (message) => {
1069
+ this.emit("debug", message);
1070
+ });
1071
+ }
1072
+ /**
1073
+ * Connect to JMRI WebSocket server (or mock)
1074
+ */
1075
+ async connect() {
1076
+ if (this.stateManager.isConnected() || this.stateManager.isConnecting()) {
1077
+ return;
1078
+ }
1079
+ this.isManualDisconnect = false;
1080
+ this.stateManager.transition("connecting" /* CONNECTING */);
1081
+ if (this.mockManager) {
1082
+ return this.connectMock();
1083
+ }
1084
+ return new Promise(async (resolve, reject) => {
1085
+ try {
1086
+ this.ws = await createWebSocketAdapter(this.url);
1087
+ this.ws.on("open", () => {
1088
+ this.handleOpen();
1089
+ resolve();
1090
+ });
1091
+ this.ws.on("message", (data) => {
1092
+ this.handleMessage(data);
1093
+ });
1094
+ this.ws.on("close", (code, reason) => {
1095
+ if (this.stateManager.isConnecting()) {
1096
+ this.stateManager.transition("disconnected" /* DISCONNECTED */);
1097
+ const error = new Error(`WebSocket connection failed (code: ${code}${reason ? ", reason: " + reason : ""})`);
1098
+ this.emit("error", error);
1099
+ reject(error);
1100
+ this.handleClose(code, reason);
1101
+ } else {
1102
+ this.handleClose(code, reason);
1103
+ }
1104
+ });
1105
+ this.ws.on("error", (error) => {
1106
+ this.emit("error", error);
1107
+ if (this.stateManager.isConnecting()) {
1108
+ this.stateManager.transition("disconnected" /* DISCONNECTED */);
1109
+ reject(error);
1110
+ }
1111
+ });
1112
+ } catch (error) {
1113
+ this.stateManager.transition("disconnected" /* DISCONNECTED */);
1114
+ reject(error);
1115
+ }
1116
+ });
1117
+ }
1118
+ /**
1119
+ * Simulate connection in mock mode
1120
+ */
1121
+ async connectMock() {
1122
+ await this.delay(10);
1123
+ this.handleOpen();
1124
+ const helloResponse = await this.mockManager.getMockResponse({ type: "hello" });
1125
+ if (helloResponse) {
1126
+ this.processMessage(helloResponse);
1127
+ }
1128
+ }
1129
+ /**
1130
+ * Disconnect from JMRI WebSocket server
1131
+ */
1132
+ async disconnect() {
1133
+ if (this.stateManager.isDisconnected()) {
1134
+ return;
1135
+ }
1136
+ this.isManualDisconnect = true;
1137
+ this.reconnectionManager.stop();
1138
+ this.heartbeatManager.stop();
1139
+ if (this.stateManager.isConnected()) {
1140
+ try {
1141
+ await this.sendGoodbye();
1142
+ } catch (error) {
1143
+ }
1144
+ }
1145
+ if (this.ws) {
1146
+ this.ws.close();
1147
+ this.ws = void 0;
1148
+ }
1149
+ this.rejectAllPendingRequests(new Error("Client disconnected"));
1150
+ if (!this.stateManager.isDisconnected()) {
1151
+ this.stateManager.transition("disconnected" /* DISCONNECTED */);
1152
+ }
1153
+ }
1154
+ /**
1155
+ * Send message to JMRI (or mock)
1156
+ */
1157
+ send(message) {
1158
+ if (!this.stateManager.isConnected()) {
1159
+ this.messageQueue.enqueue(message);
1160
+ return;
1161
+ }
1162
+ if (this.mockManager) {
1163
+ this.emit("message:sent", message);
1164
+ return;
1165
+ }
1166
+ if (!this.ws) {
1167
+ throw new Error("WebSocket not initialized");
1168
+ }
1169
+ const json = JSON.stringify(message);
1170
+ this.ws.send(json);
1171
+ this.emit("message:sent", message);
1172
+ }
1173
+ /**
1174
+ * Send request and wait for response (or get mock response)
1175
+ */
1176
+ async request(message, timeout) {
1177
+ if (this.mockManager) {
1178
+ const response = await this.mockManager.getMockResponse(message);
1179
+ this.emit("message:sent", message);
1180
+ if (response) {
1181
+ this.processMessage(response);
1182
+ }
1183
+ return response;
1184
+ }
1185
+ const id = this.messageIdGen.next();
1186
+ message.id = id;
1187
+ return new Promise((resolve, reject) => {
1188
+ const timeoutMs = timeout || this.options.requestTimeout;
1189
+ const timeoutHandle = setTimeout(() => {
1190
+ this.pendingRequests.delete(id);
1191
+ reject(new Error(`Request timeout after ${timeoutMs}ms`));
1192
+ }, timeoutMs);
1193
+ const pendingRequest = {
1194
+ resolve,
1195
+ reject,
1196
+ timeout: timeoutHandle,
1197
+ messageType: message.type
1198
+ };
1199
+ if (message.type === "throttle" && message.data && "name" in message.data) {
1200
+ pendingRequest.matchKey = message.data.name;
1201
+ }
1202
+ this.pendingRequests.set(id, pendingRequest);
1203
+ try {
1204
+ this.send(message);
1205
+ } catch (error) {
1206
+ this.pendingRequests.delete(id);
1207
+ clearTimeout(timeoutHandle);
1208
+ reject(error);
1209
+ }
1210
+ });
1211
+ }
1212
+ /**
1213
+ * Get current connection state
1214
+ */
1215
+ getState() {
1216
+ return this.stateManager.getState();
1217
+ }
1218
+ /**
1219
+ * Check if connected
1220
+ */
1221
+ isConnected() {
1222
+ return this.stateManager.isConnected();
1223
+ }
1224
+ /**
1225
+ * Handle WebSocket open event
1226
+ */
1227
+ handleOpen() {
1228
+ this.stateManager.transition("connected" /* CONNECTED */);
1229
+ this.emit("connected");
1230
+ if (this.options.heartbeat.enabled) {
1231
+ this.heartbeatManager.start(() => this.sendPing());
1232
+ }
1233
+ const queuedMessages = this.messageQueue.flush();
1234
+ for (const message of queuedMessages) {
1235
+ this.send(message);
1236
+ }
1237
+ }
1238
+ /**
1239
+ * Handle WebSocket message event
1240
+ */
1241
+ handleMessage(data) {
1242
+ try {
1243
+ const message = JSON.parse(data);
1244
+ this.processMessage(message);
1245
+ } catch (error) {
1246
+ this.emit("error", new Error(`Failed to parse message: ${error}`));
1247
+ }
1248
+ }
1249
+ /**
1250
+ * Process a parsed message (called by both real and mock mode)
1251
+ */
1252
+ processMessage(message) {
1253
+ this.emit("message:received", message);
1254
+ if (message.type === "pong") {
1255
+ this.heartbeatManager.receivedPong();
1256
+ return;
1257
+ }
1258
+ if (message.type === "hello") {
1259
+ this.emit("hello", message.data);
1260
+ return;
1261
+ }
1262
+ if (message.id !== void 0) {
1263
+ const pending = this.pendingRequests.get(message.id);
1264
+ if (pending) {
1265
+ clearTimeout(pending.timeout);
1266
+ this.pendingRequests.delete(message.id);
1267
+ pending.resolve(message);
1268
+ return;
1269
+ }
1270
+ }
1271
+ if (message.id === void 0) {
1272
+ for (const [id, pending] of this.pendingRequests.entries()) {
1273
+ if (pending.messageType === message.type) {
1274
+ if (message.type === "throttle" && pending.matchKey) {
1275
+ const throttleName = message.data?.throttle || message.data?.name;
1276
+ if (throttleName === pending.matchKey) {
1277
+ clearTimeout(pending.timeout);
1278
+ this.pendingRequests.delete(id);
1279
+ pending.resolve(message);
1280
+ return;
1281
+ }
1282
+ } else {
1283
+ clearTimeout(pending.timeout);
1284
+ this.pendingRequests.delete(id);
1285
+ pending.resolve(message);
1286
+ return;
1287
+ }
1288
+ }
1289
+ }
1290
+ }
1291
+ this.emit("update", message);
1292
+ }
1293
+ /**
1294
+ * Handle WebSocket close event
1295
+ */
1296
+ handleClose(code, reason) {
1297
+ this.heartbeatManager.stop();
1298
+ const wasConnected = this.stateManager.isConnected();
1299
+ const isReconnecting = this.reconnectionManager.reconnecting();
1300
+ if (this.stateManager.isConnected() || this.stateManager.isConnecting()) {
1301
+ this.stateManager.transition("disconnected" /* DISCONNECTED */);
1302
+ }
1303
+ this.emit("disconnected", reason || `Connection closed (code: ${code})`);
1304
+ this.rejectAllPendingRequests(new Error("Connection closed"));
1305
+ if (!this.isManualDisconnect && (wasConnected || isReconnecting) && this.options.reconnection.enabled) {
1306
+ this.stateManager.forceState("reconnecting" /* RECONNECTING */);
1307
+ this.reconnectionManager.start(() => this.connect());
1308
+ }
1309
+ }
1310
+ /**
1311
+ * Handle heartbeat timeout
1312
+ */
1313
+ handleHeartbeatTimeout() {
1314
+ if (this.ws) {
1315
+ this.ws.close();
1316
+ }
1317
+ }
1318
+ /**
1319
+ * Send ping message
1320
+ */
1321
+ sendPing() {
1322
+ this.send({ type: "ping" });
1323
+ }
1324
+ /**
1325
+ * Send goodbye message
1326
+ */
1327
+ async sendGoodbye() {
1328
+ const message = { type: "goodbye" };
1329
+ this.send(message);
1330
+ await new Promise((resolve) => setTimeout(resolve, 100));
1331
+ }
1332
+ /**
1333
+ * Reject all pending requests
1334
+ */
1335
+ rejectAllPendingRequests(error) {
1336
+ for (const pending of this.pendingRequests.values()) {
1337
+ clearTimeout(pending.timeout);
1338
+ pending.reject(error);
1339
+ }
1340
+ this.pendingRequests.clear();
1341
+ }
1342
+ /**
1343
+ * Delay helper
1344
+ */
1345
+ delay(ms) {
1346
+ return new Promise((resolve) => setTimeout(resolve, ms));
1347
+ }
1348
+ };
1349
+
1350
+ // src/managers/power-manager.ts
1351
+ var import_eventemitter36 = require("eventemitter3");
1352
+ var PowerManager = class extends import_eventemitter36.EventEmitter {
1353
+ constructor(client) {
1354
+ super();
1355
+ this.currentState = 0 /* UNKNOWN */;
1356
+ this.client = client;
1357
+ this.client.on("update", (message) => {
1358
+ if (message.type === "power") {
1359
+ this.handlePowerUpdate(message);
1360
+ }
1361
+ });
1362
+ }
1363
+ /**
1364
+ * Get current track power state
1365
+ * @param prefix - Optional JMRI connection prefix to target a specific hardware connection
1366
+ */
1367
+ async getPower(prefix) {
1368
+ const message = {
1369
+ type: "power",
1370
+ ...prefix !== void 0 && { data: { state: 0 /* UNKNOWN */, prefix } }
1371
+ };
1372
+ const response = await this.client.request(message);
1373
+ if (response.data?.state !== void 0) {
1374
+ this.currentState = response.data.state;
1375
+ }
1376
+ return this.currentState;
1377
+ }
1378
+ /**
1379
+ * Set track power state
1380
+ * @param state - The desired power state
1381
+ * @param prefix - Optional JMRI connection prefix to target a specific hardware connection
1382
+ */
1383
+ async setPower(state, prefix) {
1384
+ const message = {
1385
+ type: "power",
1386
+ method: "post",
1387
+ data: { state, ...prefix !== void 0 && { prefix } }
1388
+ };
1389
+ await this.client.request(message);
1390
+ const oldState = this.currentState;
1391
+ this.currentState = state;
1392
+ if (oldState !== this.currentState) {
1393
+ this.emit("power:changed", this.currentState);
1394
+ }
1395
+ }
1396
+ /**
1397
+ * Turn track power on
1398
+ * @param prefix - Optional JMRI connection prefix to target a specific hardware connection
1399
+ */
1400
+ async powerOn(prefix) {
1401
+ await this.setPower(2 /* ON */, prefix);
1402
+ }
1403
+ /**
1404
+ * Turn track power off
1405
+ * @param prefix - Optional JMRI connection prefix to target a specific hardware connection
1406
+ */
1407
+ async powerOff(prefix) {
1408
+ await this.setPower(4 /* OFF */, prefix);
1409
+ }
1410
+ /**
1411
+ * Get cached power state (no network request)
1412
+ */
1413
+ getCachedState() {
1414
+ return this.currentState;
1415
+ }
1416
+ /**
1417
+ * Handle unsolicited power updates from JMRI
1418
+ */
1419
+ handlePowerUpdate(message) {
1420
+ if (message.data?.state !== void 0) {
1421
+ const oldState = this.currentState;
1422
+ this.currentState = message.data.state;
1423
+ if (oldState !== this.currentState) {
1424
+ this.emit("power:changed", this.currentState);
1425
+ }
1426
+ }
1427
+ }
1428
+ };
1429
+
1430
+ // src/managers/roster-manager.ts
1431
+ var RosterManager = class {
1432
+ constructor(client) {
1433
+ this.rosterCache = /* @__PURE__ */ new Map();
1434
+ this.client = client;
1435
+ }
1436
+ /**
1437
+ * Get all roster entries
1438
+ */
1439
+ async getRoster() {
1440
+ const message = {
1441
+ type: "roster",
1442
+ method: "list"
1443
+ };
1444
+ const response = await this.client.request(message);
1445
+ if (response.data) {
1446
+ this.updateCache(response.data);
1447
+ }
1448
+ return Array.from(this.rosterCache.values());
1449
+ }
1450
+ /**
1451
+ * Get roster entry by name
1452
+ */
1453
+ async getRosterEntryByName(name) {
1454
+ if (this.rosterCache.has(name)) {
1455
+ return this.rosterCache.get(name);
1456
+ }
1457
+ await this.getRoster();
1458
+ return this.rosterCache.get(name);
1459
+ }
1460
+ /**
1461
+ * Get roster entry by address
1462
+ */
1463
+ async getRosterEntryByAddress(address) {
1464
+ if (this.rosterCache.size === 0) {
1465
+ await this.getRoster();
1466
+ }
1467
+ const addressStr = address.toString();
1468
+ for (const wrapper of this.rosterCache.values()) {
1469
+ if (wrapper.data.address === addressStr) {
1470
+ return wrapper;
1471
+ }
1472
+ }
1473
+ return void 0;
1474
+ }
1475
+ /**
1476
+ * Search roster by partial name match
1477
+ */
1478
+ async searchRoster(query) {
1479
+ if (this.rosterCache.size === 0) {
1480
+ await this.getRoster();
1481
+ }
1482
+ const lowerQuery = query.toLowerCase();
1483
+ const results = [];
1484
+ for (const wrapper of this.rosterCache.values()) {
1485
+ const entry = wrapper.data;
1486
+ if (entry.name.toLowerCase().includes(lowerQuery) || entry.address.includes(query) || entry.road?.toLowerCase().includes(lowerQuery) || entry.number?.includes(query)) {
1487
+ results.push(wrapper);
1488
+ }
1489
+ }
1490
+ return results;
1491
+ }
1492
+ /**
1493
+ * Get all roster groups
1494
+ */
1495
+ async getRosterGroups() {
1496
+ const message = {
1497
+ type: "rosterGroup",
1498
+ method: "list"
1499
+ };
1500
+ const response = await this.client.request(message);
1501
+ if (!response.data) return [];
1502
+ return response.data.map((wrapper) => wrapper.data);
1503
+ }
1504
+ /**
1505
+ * Get roster entries belonging to a specific group
1506
+ */
1507
+ async getRosterEntriesByGroup(group) {
1508
+ const message = {
1509
+ type: "roster",
1510
+ method: "list",
1511
+ params: { group }
1512
+ };
1513
+ const response = await this.client.request(message);
1514
+ if (!response.data) return [];
1515
+ const entries = response.data;
1516
+ return entries.filter((e) => e.type === "rosterEntry");
1517
+ }
1518
+ /**
1519
+ * Get cached roster (no network request)
1520
+ */
1521
+ getCachedRoster() {
1522
+ return Array.from(this.rosterCache.values());
1523
+ }
1524
+ /**
1525
+ * Clear roster cache
1526
+ */
1527
+ clearCache() {
1528
+ this.rosterCache.clear();
1529
+ }
1530
+ /**
1531
+ * Update internal cache from roster data
1532
+ */
1533
+ updateCache(rosterData) {
1534
+ this.rosterCache.clear();
1535
+ if (Array.isArray(rosterData)) {
1536
+ for (const wrapper of rosterData) {
1537
+ if (wrapper.type === "rosterEntry" && wrapper.data) {
1538
+ this.rosterCache.set(wrapper.data.name, wrapper);
1539
+ }
1540
+ }
1541
+ } else {
1542
+ let id = 1;
1543
+ for (const [name, entry] of Object.entries(rosterData)) {
1544
+ const wrapper = {
1545
+ type: "rosterEntry",
1546
+ data: entry,
1547
+ id: id++
1548
+ };
1549
+ this.rosterCache.set(name, wrapper);
1550
+ }
1551
+ }
1552
+ }
1553
+ };
1554
+
1555
+ // src/managers/throttle-manager.ts
1556
+ var import_eventemitter37 = require("eventemitter3");
1557
+
1558
+ // src/types/throttle.ts
1559
+ function isThrottleFunctionKey(key) {
1560
+ return /^F([0-9]|1[0-9]|2[0-8])$/.test(key);
1561
+ }
1562
+ function isValidSpeed(speed) {
1563
+ return typeof speed === "number" && speed >= 0 && speed <= 1;
1564
+ }
1565
+
1566
+ // src/managers/throttle-manager.ts
1567
+ var ThrottleManager = class extends import_eventemitter37.EventEmitter {
1568
+ constructor(client) {
1569
+ super();
1570
+ this.throttles = /* @__PURE__ */ new Map();
1571
+ this.client = client;
1572
+ this.clientId = `jmri-client-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1573
+ this.client.on("update", (message) => {
1574
+ if (message.type === "throttle") {
1575
+ this.handleThrottleUpdate(message);
1576
+ }
1577
+ });
1578
+ this.client.on("disconnected", () => {
1579
+ this.handleDisconnect();
1580
+ });
1581
+ }
1582
+ /**
1583
+ * Acquire a throttle for a locomotive
1584
+ */
1585
+ async acquireThrottle(options) {
1586
+ const throttleName = `${this.clientId}-${options.address}`;
1587
+ const message = {
1588
+ type: "throttle",
1589
+ data: {
1590
+ name: throttleName,
1591
+ address: options.address,
1592
+ ...options.prefix !== void 0 && { prefix: options.prefix }
1593
+ }
1594
+ };
1595
+ const response = await this.client.request(message);
1596
+ const throttleId = response.data?.throttle || throttleName;
1597
+ if (!throttleId) {
1598
+ throw new Error("Failed to acquire throttle: no throttle ID returned");
1599
+ }
1600
+ const state = {
1601
+ id: throttleId,
1602
+ address: options.address,
1603
+ speed: 0,
1604
+ forward: true,
1605
+ functions: /* @__PURE__ */ new Map(),
1606
+ acquired: true
1607
+ };
1608
+ this.throttles.set(throttleId, state);
1609
+ this.emit("throttle:acquired", throttleId);
1610
+ return throttleId;
1611
+ }
1612
+ /**
1613
+ * Release a throttle
1614
+ */
1615
+ async releaseThrottle(throttleId) {
1616
+ const state = this.throttles.get(throttleId);
1617
+ if (!state) {
1618
+ throw new Error(`Throttle not found: ${throttleId}`);
1619
+ }
1620
+ const message = {
1621
+ type: "throttle",
1622
+ data: {
1623
+ throttle: throttleId,
1624
+ release: null
1625
+ }
1626
+ };
1627
+ await this.client.request(message);
1628
+ this.throttles.delete(throttleId);
1629
+ this.emit("throttle:released", throttleId);
1630
+ }
1631
+ /**
1632
+ * Set throttle speed (0.0 to 1.0)
1633
+ */
1634
+ async setSpeed(throttleId, speed) {
1635
+ const state = this.throttles.get(throttleId);
1636
+ if (!state) {
1637
+ throw new Error(`Throttle not found: ${throttleId}`);
1638
+ }
1639
+ if (!isValidSpeed(speed)) {
1640
+ throw new Error(`Invalid speed: ${speed}. Must be between 0.0 and 1.0`);
1641
+ }
1642
+ const message = {
1643
+ type: "throttle",
1644
+ data: {
1645
+ throttle: throttleId,
1646
+ speed
1647
+ }
1648
+ };
1649
+ this.client.send(message);
1650
+ state.speed = speed;
1651
+ this.emit("throttle:updated", throttleId, { speed });
1652
+ }
1653
+ /**
1654
+ * Set throttle direction
1655
+ */
1656
+ async setDirection(throttleId, forward) {
1657
+ const state = this.throttles.get(throttleId);
1658
+ if (!state) {
1659
+ throw new Error(`Throttle not found: ${throttleId}`);
1660
+ }
1661
+ const message = {
1662
+ type: "throttle",
1663
+ data: {
1664
+ throttle: throttleId,
1665
+ forward
1666
+ }
1667
+ };
1668
+ this.client.send(message);
1669
+ state.forward = forward;
1670
+ this.emit("throttle:updated", throttleId, { forward });
1671
+ }
1672
+ /**
1673
+ * Set throttle function (F0-F28)
1674
+ */
1675
+ async setFunction(throttleId, functionKey, value) {
1676
+ const state = this.throttles.get(throttleId);
1677
+ if (!state) {
1678
+ throw new Error(`Throttle not found: ${throttleId}`);
1679
+ }
1680
+ if (!isThrottleFunctionKey(functionKey)) {
1681
+ throw new Error(`Invalid function key: ${functionKey}`);
1682
+ }
1683
+ const data = {
1684
+ throttle: throttleId,
1685
+ [functionKey]: value
1686
+ };
1687
+ const message = {
1688
+ type: "throttle",
1689
+ data
1690
+ };
1691
+ this.client.send(message);
1692
+ state.functions.set(functionKey, value);
1693
+ this.emit("throttle:updated", throttleId, { [functionKey]: value });
1694
+ }
1695
+ /**
1696
+ * Emergency stop for a throttle (speed to 0)
1697
+ */
1698
+ async emergencyStop(throttleId) {
1699
+ await this.setSpeed(throttleId, 0);
1700
+ }
1701
+ /**
1702
+ * Set throttle to idle (speed to 0, maintain direction)
1703
+ */
1704
+ async idle(throttleId) {
1705
+ await this.setSpeed(throttleId, 0);
1706
+ }
1707
+ /**
1708
+ * Get throttle state
1709
+ */
1710
+ getThrottleState(throttleId) {
1711
+ return this.throttles.get(throttleId);
1712
+ }
1713
+ /**
1714
+ * Get all throttle IDs
1715
+ */
1716
+ getThrottleIds() {
1717
+ return Array.from(this.throttles.keys());
1718
+ }
1719
+ /**
1720
+ * Get all throttle states
1721
+ */
1722
+ getAllThrottles() {
1723
+ return Array.from(this.throttles.values());
1724
+ }
1725
+ /**
1726
+ * Release all throttles
1727
+ */
1728
+ async releaseAllThrottles() {
1729
+ const throttleIds = this.getThrottleIds();
1730
+ for (const throttleId of throttleIds) {
1731
+ try {
1732
+ await this.releaseThrottle(throttleId);
1733
+ } catch (error) {
1734
+ this.emit("error", error);
1735
+ }
1736
+ }
1737
+ }
1738
+ /**
1739
+ * Handle unsolicited throttle updates from JMRI
1740
+ */
1741
+ handleThrottleUpdate(message) {
1742
+ const throttleId = message.data?.throttle;
1743
+ if (!throttleId) {
1744
+ return;
1745
+ }
1746
+ const state = this.throttles.get(throttleId);
1747
+ if (!state) {
1748
+ this.emit("throttle:lost", throttleId);
1749
+ return;
1750
+ }
1751
+ if (message.data.speed !== void 0) {
1752
+ state.speed = message.data.speed;
1753
+ }
1754
+ if (message.data.forward !== void 0) {
1755
+ state.forward = message.data.forward;
1756
+ }
1757
+ for (let i = 0; i <= 28; i++) {
1758
+ const key = `F${i}`;
1759
+ if (message.data[key] !== void 0) {
1760
+ state.functions.set(key, message.data[key]);
1761
+ }
1762
+ }
1763
+ this.emit("throttle:updated", throttleId, message.data);
1764
+ }
1765
+ /**
1766
+ * Handle disconnect - mark all throttles as not acquired
1767
+ */
1768
+ handleDisconnect() {
1769
+ for (const state of this.throttles.values()) {
1770
+ state.acquired = false;
1771
+ }
1772
+ }
1773
+ };
1774
+
1775
+ // src/managers/turnout-manager.ts
1776
+ var import_eventemitter38 = require("eventemitter3");
1777
+ var TurnoutManager = class extends import_eventemitter38.EventEmitter {
1778
+ constructor(client) {
1779
+ super();
1780
+ this.turnouts = /* @__PURE__ */ new Map();
1781
+ this.client = client;
1782
+ this.client.on("update", (message) => {
1783
+ if (message.type === "turnout") {
1784
+ this.handleTurnoutUpdate(message);
1785
+ }
1786
+ });
1787
+ }
1788
+ /**
1789
+ * Get the current state of a turnout.
1790
+ * Also registers a server-side listener so subsequent changes are pushed.
1791
+ */
1792
+ async getTurnout(name) {
1793
+ const message = {
1794
+ type: "turnout",
1795
+ data: { name }
1796
+ };
1797
+ const response = await this.client.request(message);
1798
+ const state = response.data?.state ?? 0 /* UNKNOWN */;
1799
+ this.turnouts.set(name, state);
1800
+ return state;
1801
+ }
1802
+ /**
1803
+ * Set a turnout to the given state
1804
+ */
1805
+ async setTurnout(name, state) {
1806
+ const message = {
1807
+ type: "turnout",
1808
+ method: "post",
1809
+ data: { name, state }
1810
+ };
1811
+ await this.client.request(message);
1812
+ const oldState = this.turnouts.get(name);
1813
+ this.turnouts.set(name, state);
1814
+ if (oldState !== state) {
1815
+ this.emit("turnout:changed", name, state);
1816
+ }
1817
+ }
1818
+ /**
1819
+ * Throw a turnout (diverging route)
1820
+ */
1821
+ async throwTurnout(name) {
1822
+ return this.setTurnout(name, 4 /* THROWN */);
1823
+ }
1824
+ /**
1825
+ * Close a turnout (straight through / normal)
1826
+ */
1827
+ async closeTurnout(name) {
1828
+ return this.setTurnout(name, 2 /* CLOSED */);
1829
+ }
1830
+ /**
1831
+ * List all turnouts known to JMRI
1832
+ */
1833
+ async listTurnouts() {
1834
+ const message = {
1835
+ type: "turnout",
1836
+ method: "list"
1837
+ };
1838
+ const response = await this.client.request(message);
1839
+ const entries = Array.isArray(response?.data) ? response.data.map((r) => r.data ?? r) : [];
1840
+ for (const entry of entries) {
1841
+ if (entry.name && entry.state !== void 0) {
1842
+ this.turnouts.set(entry.name, entry.state);
1843
+ }
1844
+ }
1845
+ return entries;
1846
+ }
1847
+ /**
1848
+ * Get cached turnout state without a network request
1849
+ */
1850
+ getTurnoutState(name) {
1851
+ return this.turnouts.get(name);
1852
+ }
1853
+ /**
1854
+ * Get all cached turnout states
1855
+ */
1856
+ getCachedTurnouts() {
1857
+ return new Map(this.turnouts);
1858
+ }
1859
+ /**
1860
+ * Handle unsolicited turnout state updates from JMRI
1861
+ */
1862
+ handleTurnoutUpdate(message) {
1863
+ const name = message.data?.name;
1864
+ const state = message.data?.state;
1865
+ if (!name || state === void 0) return;
1866
+ const oldState = this.turnouts.get(name);
1867
+ this.turnouts.set(name, state);
1868
+ if (oldState !== state) {
1869
+ this.emit("turnout:changed", name, state);
1870
+ }
1871
+ }
1872
+ };
1873
+
1874
+ // src/managers/light-manager.ts
1875
+ var import_eventemitter39 = require("eventemitter3");
1876
+ var LightManager = class extends import_eventemitter39.EventEmitter {
1877
+ constructor(client) {
1878
+ super();
1879
+ this.lights = /* @__PURE__ */ new Map();
1880
+ this.client = client;
1881
+ this.client.on("update", (message) => {
1882
+ if (message.type === "light") {
1883
+ this.handleLightUpdate(message);
1884
+ }
1885
+ });
1886
+ }
1887
+ /**
1888
+ * Get the current state of a light.
1889
+ * Also registers a server-side listener so subsequent changes are pushed.
1890
+ */
1891
+ async getLight(name) {
1892
+ const message = {
1893
+ type: "light",
1894
+ data: { name }
1895
+ };
1896
+ const response = await this.client.request(message);
1897
+ const state = response.data?.state ?? 0 /* UNKNOWN */;
1898
+ this.lights.set(name, state);
1899
+ return state;
1900
+ }
1901
+ /**
1902
+ * Set a light to the given state
1903
+ */
1904
+ async setLight(name, state) {
1905
+ const message = {
1906
+ type: "light",
1907
+ method: "post",
1908
+ data: { name, state }
1909
+ };
1910
+ await this.client.request(message);
1911
+ const oldState = this.lights.get(name);
1912
+ this.lights.set(name, state);
1913
+ if (oldState !== state) {
1914
+ this.emit("light:changed", name, state);
1915
+ }
1916
+ }
1917
+ /**
1918
+ * Turn a light on
1919
+ */
1920
+ async turnOnLight(name) {
1921
+ return this.setLight(name, 2 /* ON */);
1922
+ }
1923
+ /**
1924
+ * Turn a light off
1925
+ */
1926
+ async turnOffLight(name) {
1927
+ return this.setLight(name, 4 /* OFF */);
1928
+ }
1929
+ /**
1930
+ * List all lights known to JMRI
1931
+ */
1932
+ async listLights() {
1933
+ const message = {
1934
+ type: "light",
1935
+ method: "list"
1936
+ };
1937
+ const response = await this.client.request(message);
1938
+ const entries = Array.isArray(response?.data) ? response.data.map((r) => r.data ?? r) : [];
1939
+ for (const entry of entries) {
1940
+ if (entry.name && entry.state !== void 0) {
1941
+ this.lights.set(entry.name, entry.state);
1942
+ }
1943
+ }
1944
+ return entries;
1945
+ }
1946
+ /**
1947
+ * Get cached light state without a network request
1948
+ */
1949
+ getLightState(name) {
1950
+ return this.lights.get(name);
1951
+ }
1952
+ /**
1953
+ * Get all cached light states
1954
+ */
1955
+ getCachedLights() {
1956
+ return new Map(this.lights);
1957
+ }
1958
+ /**
1959
+ * Handle unsolicited light state updates from JMRI
1960
+ */
1961
+ handleLightUpdate(message) {
1962
+ const name = message.data?.name;
1963
+ const state = message.data?.state;
1964
+ if (!name || state === void 0) return;
1965
+ const oldState = this.lights.get(name);
1966
+ this.lights.set(name, state);
1967
+ if (oldState !== state) {
1968
+ this.emit("light:changed", name, state);
1969
+ }
1970
+ }
1971
+ };
1972
+
1973
+ // src/managers/system-connections-manager.ts
1974
+ var SystemConnectionsManager = class {
1975
+ constructor(client) {
1976
+ this.client = client;
1977
+ }
1978
+ /**
1979
+ * List all available JMRI system connections and their prefixes.
1980
+ * Use the returned prefix values with power and throttle commands to
1981
+ * target a specific hardware connection when multiple are configured.
1982
+ */
1983
+ async getSystemConnections() {
1984
+ const message = {
1985
+ type: "systemConnections",
1986
+ method: "list"
1987
+ };
1988
+ const response = await this.client.request(message);
1989
+ if (!response.data) {
1990
+ return [];
1991
+ }
1992
+ return Array.isArray(response.data) ? response.data : [response.data];
1993
+ }
1994
+ };
1995
+
1996
+ // src/types/client-options.ts
1997
+ var DEFAULT_CLIENT_OPTIONS = {
1998
+ host: "localhost",
1999
+ port: 12080,
2000
+ protocol: "ws",
2001
+ autoConnect: true,
2002
+ reconnection: {
2003
+ enabled: true,
2004
+ maxAttempts: 0,
2005
+ // infinite
2006
+ initialDelay: 1e3,
2007
+ maxDelay: 3e4,
2008
+ multiplier: 1.5,
2009
+ jitter: true
2010
+ },
2011
+ heartbeat: {
2012
+ enabled: true,
2013
+ interval: 3e4,
2014
+ timeout: 5e3
2015
+ },
2016
+ messageQueueSize: 100,
2017
+ requestTimeout: 1e4,
2018
+ mock: {
2019
+ enabled: false,
2020
+ responseDelay: 50
2021
+ }
2022
+ };
2023
+ function mergeOptions(userOptions) {
2024
+ const options = { ...DEFAULT_CLIENT_OPTIONS };
2025
+ if (!userOptions) {
2026
+ return options;
2027
+ }
2028
+ if (userOptions.host !== void 0) options.host = userOptions.host;
2029
+ if (userOptions.port !== void 0) options.port = userOptions.port;
2030
+ if (userOptions.protocol !== void 0) options.protocol = userOptions.protocol;
2031
+ if (userOptions.autoConnect !== void 0) options.autoConnect = userOptions.autoConnect;
2032
+ if (userOptions.messageQueueSize !== void 0) options.messageQueueSize = userOptions.messageQueueSize;
2033
+ if (userOptions.requestTimeout !== void 0) options.requestTimeout = userOptions.requestTimeout;
2034
+ if (userOptions.reconnection) {
2035
+ options.reconnection = { ...DEFAULT_CLIENT_OPTIONS.reconnection, ...userOptions.reconnection };
2036
+ }
2037
+ if (userOptions.heartbeat) {
2038
+ options.heartbeat = { ...DEFAULT_CLIENT_OPTIONS.heartbeat, ...userOptions.heartbeat };
2039
+ }
2040
+ if (userOptions.mock) {
2041
+ options.mock = { ...DEFAULT_CLIENT_OPTIONS.mock, ...userOptions.mock };
2042
+ }
2043
+ return options;
2044
+ }
2045
+
2046
+ // src/client.ts
2047
+ var JmriClient = class extends import_eventemitter310.EventEmitter {
2048
+ /**
2049
+ * Create a new JMRI client
2050
+ *
2051
+ * @param options - Client configuration options
2052
+ *
2053
+ * @example
2054
+ * ```typescript
2055
+ * const client = new JmriClient({
2056
+ * host: 'jmri.local',
2057
+ * port: 12080
2058
+ * });
2059
+ *
2060
+ * client.on('connected', () => console.log('Connected!'));
2061
+ * client.on('power:changed', (state) => console.log('Power:', state));
2062
+ * ```
2063
+ */
2064
+ constructor(options) {
2065
+ super();
2066
+ this.options = mergeOptions(options);
2067
+ this.wsClient = new WebSocketClient(this.options);
2068
+ this.powerManager = new PowerManager(this.wsClient);
2069
+ this.rosterManager = new RosterManager(this.wsClient);
2070
+ this.throttleManager = new ThrottleManager(this.wsClient);
2071
+ this.turnoutManager = new TurnoutManager(this.wsClient);
2072
+ this.lightManager = new LightManager(this.wsClient);
2073
+ this.systemConnectionsManager = new SystemConnectionsManager(this.wsClient);
2074
+ this.wsClient.on("connected", () => this.emit("connected"));
2075
+ this.wsClient.on("disconnected", (reason) => this.emit("disconnected", reason));
2076
+ this.wsClient.on(
2077
+ "reconnecting",
2078
+ (attempt, delay) => this.emit("reconnecting", attempt, delay)
2079
+ );
2080
+ this.wsClient.on("reconnected", () => this.emit("reconnected"));
2081
+ this.wsClient.on(
2082
+ "reconnectionFailed",
2083
+ (attempts) => this.emit("reconnectionFailed", attempts)
2084
+ );
2085
+ this.wsClient.on(
2086
+ "connectionStateChanged",
2087
+ (state) => this.emit("connectionStateChanged", state)
2088
+ );
2089
+ this.wsClient.on("error", (error) => this.emit("error", error));
2090
+ this.wsClient.on("heartbeat:sent", () => this.emit("heartbeat:sent"));
2091
+ this.wsClient.on("heartbeat:timeout", () => this.emit("heartbeat:timeout"));
2092
+ this.wsClient.on("hello", (data) => this.emit("hello", data));
2093
+ this.powerManager.on(
2094
+ "power:changed",
2095
+ (state) => this.emit("power:changed", state)
2096
+ );
2097
+ this.turnoutManager.on(
2098
+ "turnout:changed",
2099
+ (name, state) => this.emit("turnout:changed", name, state)
2100
+ );
2101
+ this.lightManager.on(
2102
+ "light:changed",
2103
+ (name, state) => this.emit("light:changed", name, state)
2104
+ );
2105
+ this.throttleManager.on(
2106
+ "throttle:acquired",
2107
+ (id) => this.emit("throttle:acquired", id)
2108
+ );
2109
+ this.throttleManager.on(
2110
+ "throttle:updated",
2111
+ (id, data) => this.emit("throttle:updated", id, data)
2112
+ );
2113
+ this.throttleManager.on(
2114
+ "throttle:released",
2115
+ (id) => this.emit("throttle:released", id)
2116
+ );
2117
+ this.throttleManager.on(
2118
+ "throttle:lost",
2119
+ (id) => this.emit("throttle:lost", id)
2120
+ );
2121
+ if (this.options.autoConnect) {
2122
+ this.connect().catch((error) => {
2123
+ this.emit("error", error);
2124
+ });
2125
+ }
2126
+ }
2127
+ /**
2128
+ * Connect to JMRI server
2129
+ */
2130
+ async connect() {
2131
+ return this.wsClient.connect();
2132
+ }
2133
+ /**
2134
+ * Disconnect from JMRI server
2135
+ * Releases all throttles and closes connection
2136
+ */
2137
+ async disconnect() {
2138
+ await this.throttleManager.releaseAllThrottles();
2139
+ return this.wsClient.disconnect();
2140
+ }
2141
+ /**
2142
+ * Get current connection state
2143
+ */
2144
+ getConnectionState() {
2145
+ return this.wsClient.getState();
2146
+ }
2147
+ /**
2148
+ * Check if connected to JMRI
2149
+ */
2150
+ isConnected() {
2151
+ return this.wsClient.isConnected();
2152
+ }
2153
+ // ============================================================================
2154
+ // Power Control
2155
+ // ============================================================================
2156
+ /**
2157
+ * Get current track power state
2158
+ * @param prefix - Optional JMRI connection prefix to target a specific hardware connection
2159
+ */
2160
+ async getPower(prefix) {
2161
+ return this.powerManager.getPower(prefix);
2162
+ }
2163
+ /**
2164
+ * Set track power state
2165
+ * @param state - The desired power state
2166
+ * @param prefix - Optional JMRI connection prefix to target a specific hardware connection
2167
+ */
2168
+ async setPower(state, prefix) {
2169
+ return this.powerManager.setPower(state, prefix);
2170
+ }
2171
+ /**
2172
+ * Turn track power on
2173
+ * @param prefix - Optional JMRI connection prefix to target a specific hardware connection
2174
+ */
2175
+ async powerOn(prefix) {
2176
+ return this.powerManager.powerOn(prefix);
2177
+ }
2178
+ /**
2179
+ * Turn track power off
2180
+ * @param prefix - Optional JMRI connection prefix to target a specific hardware connection
2181
+ */
2182
+ async powerOff(prefix) {
2183
+ return this.powerManager.powerOff(prefix);
2184
+ }
2185
+ // ============================================================================
2186
+ // System Connections
2187
+ // ============================================================================
2188
+ /**
2189
+ * List all available JMRI system connections and their prefixes.
2190
+ * Use the returned prefix values with power and throttle commands to
2191
+ * target a specific hardware connection when multiple are configured.
2192
+ *
2193
+ * @example
2194
+ * ```typescript
2195
+ * const connections = await client.getSystemConnections();
2196
+ * // [{ name: 'LocoNet', prefix: 'L' }, { name: 'DCC++', prefix: 'D' }]
2197
+ * await client.powerOn('L'); // power on via LocoNet only
2198
+ * ```
2199
+ */
2200
+ async getSystemConnections() {
2201
+ return this.systemConnectionsManager.getSystemConnections();
2202
+ }
2203
+ // ============================================================================
2204
+ // Roster Management
2205
+ // ============================================================================
2206
+ /**
2207
+ * Get all roster entries
2208
+ */
2209
+ async getRoster() {
2210
+ return this.rosterManager.getRoster();
2211
+ }
2212
+ /**
2213
+ * Get roster entry by name
2214
+ */
2215
+ async getRosterEntryByName(name) {
2216
+ return this.rosterManager.getRosterEntryByName(name);
2217
+ }
2218
+ /**
2219
+ * Get roster entry by address
2220
+ */
2221
+ async getRosterEntryByAddress(address) {
2222
+ return this.rosterManager.getRosterEntryByAddress(address);
2223
+ }
2224
+ /**
2225
+ * Search roster by partial name match
2226
+ */
2227
+ async searchRoster(query) {
2228
+ return this.rosterManager.searchRoster(query);
2229
+ }
2230
+ /**
2231
+ * Get all roster groups
2232
+ */
2233
+ async getRosterGroups() {
2234
+ return this.rosterManager.getRosterGroups();
2235
+ }
2236
+ /**
2237
+ * Get roster entries belonging to a specific group
2238
+ */
2239
+ async getRosterEntriesByGroup(group) {
2240
+ return this.rosterManager.getRosterEntriesByGroup(group);
2241
+ }
2242
+ // ============================================================================
2243
+ // Turnout Control
2244
+ // ============================================================================
2245
+ /**
2246
+ * Get the current state of a turnout
2247
+ */
2248
+ async getTurnout(name) {
2249
+ return this.turnoutManager.getTurnout(name);
2250
+ }
2251
+ /**
2252
+ * Set a turnout to the given state
2253
+ */
2254
+ async setTurnout(name, state) {
2255
+ return this.turnoutManager.setTurnout(name, state);
2256
+ }
2257
+ /**
2258
+ * Throw a turnout (diverging route)
2259
+ */
2260
+ async throwTurnout(name) {
2261
+ return this.turnoutManager.throwTurnout(name);
2262
+ }
2263
+ /**
2264
+ * Close a turnout (straight through / normal)
2265
+ */
2266
+ async closeTurnout(name) {
2267
+ return this.turnoutManager.closeTurnout(name);
2268
+ }
2269
+ /**
2270
+ * List all turnouts known to JMRI
2271
+ */
2272
+ async listTurnouts() {
2273
+ return this.turnoutManager.listTurnouts();
2274
+ }
2275
+ /**
2276
+ * Get cached turnout state without a network request
2277
+ */
2278
+ getTurnoutState(name) {
2279
+ return this.turnoutManager.getTurnoutState(name);
2280
+ }
2281
+ /**
2282
+ * Get all cached turnout states
2283
+ */
2284
+ getCachedTurnouts() {
2285
+ return this.turnoutManager.getCachedTurnouts();
2286
+ }
2287
+ // ============================================================================
2288
+ // Light Control
2289
+ // ============================================================================
2290
+ /**
2291
+ * Get the current state of a light
2292
+ */
2293
+ async getLight(name) {
2294
+ return this.lightManager.getLight(name);
2295
+ }
2296
+ /**
2297
+ * Set a light to the given state
2298
+ */
2299
+ async setLight(name, state) {
2300
+ return this.lightManager.setLight(name, state);
2301
+ }
2302
+ /**
2303
+ * Turn a light on
2304
+ */
2305
+ async turnOnLight(name) {
2306
+ return this.lightManager.turnOnLight(name);
2307
+ }
2308
+ /**
2309
+ * Turn a light off
2310
+ */
2311
+ async turnOffLight(name) {
2312
+ return this.lightManager.turnOffLight(name);
2313
+ }
2314
+ /**
2315
+ * List all lights known to JMRI
2316
+ */
2317
+ async listLights() {
2318
+ return this.lightManager.listLights();
2319
+ }
2320
+ /**
2321
+ * Get cached light state without a network request
2322
+ */
2323
+ getLightState(name) {
2324
+ return this.lightManager.getLightState(name);
2325
+ }
2326
+ /**
2327
+ * Get all cached light states
2328
+ */
2329
+ getCachedLights() {
2330
+ return this.lightManager.getCachedLights();
2331
+ }
2332
+ // ============================================================================
2333
+ // Throttle Control
2334
+ // ============================================================================
2335
+ /**
2336
+ * Acquire a throttle for a locomotive
2337
+ *
2338
+ * @param options - Throttle acquisition options
2339
+ * @returns Throttle ID for use in other throttle methods
2340
+ *
2341
+ * @example
2342
+ * ```typescript
2343
+ * const throttleId = await client.acquireThrottle({ address: 3 });
2344
+ * await client.setThrottleSpeed(throttleId, 0.5); // Half speed
2345
+ * ```
2346
+ */
2347
+ async acquireThrottle(options) {
2348
+ return this.throttleManager.acquireThrottle(options);
2349
+ }
2350
+ /**
2351
+ * Release a throttle
2352
+ */
2353
+ async releaseThrottle(throttleId) {
2354
+ return this.throttleManager.releaseThrottle(throttleId);
2355
+ }
2356
+ /**
2357
+ * Set throttle speed (0.0 to 1.0)
2358
+ *
2359
+ * @param throttleId - Throttle ID from acquireThrottle
2360
+ * @param speed - Speed value between 0.0 (stopped) and 1.0 (full speed)
2361
+ */
2362
+ async setThrottleSpeed(throttleId, speed) {
2363
+ return this.throttleManager.setSpeed(throttleId, speed);
2364
+ }
2365
+ /**
2366
+ * Set throttle direction
2367
+ *
2368
+ * @param throttleId - Throttle ID from acquireThrottle
2369
+ * @param forward - True for forward, false for reverse
2370
+ */
2371
+ async setThrottleDirection(throttleId, forward) {
2372
+ return this.throttleManager.setDirection(throttleId, forward);
2373
+ }
2374
+ /**
2375
+ * Set throttle function (F0-F28)
2376
+ *
2377
+ * @param throttleId - Throttle ID from acquireThrottle
2378
+ * @param functionKey - Function key (F0-F28)
2379
+ * @param value - True to activate, false to deactivate
2380
+ *
2381
+ * @example
2382
+ * ```typescript
2383
+ * await client.setThrottleFunction(throttleId, 'F0', true); // Headlight on
2384
+ * await client.setThrottleFunction(throttleId, 'F2', true); // Horn
2385
+ * ```
2386
+ */
2387
+ async setThrottleFunction(throttleId, functionKey, value) {
2388
+ return this.throttleManager.setFunction(throttleId, functionKey, value);
2389
+ }
2390
+ /**
2391
+ * Emergency stop for a throttle (speed to 0)
2392
+ */
2393
+ async emergencyStop(throttleId) {
2394
+ return this.throttleManager.emergencyStop(throttleId);
2395
+ }
2396
+ /**
2397
+ * Set throttle to idle (speed to 0, maintain direction)
2398
+ */
2399
+ async idleThrottle(throttleId) {
2400
+ return this.throttleManager.idle(throttleId);
2401
+ }
2402
+ /**
2403
+ * Get throttle state
2404
+ */
2405
+ getThrottleState(throttleId) {
2406
+ return this.throttleManager.getThrottleState(throttleId);
2407
+ }
2408
+ /**
2409
+ * Get all throttle IDs
2410
+ */
2411
+ getThrottleIds() {
2412
+ return this.throttleManager.getThrottleIds();
2413
+ }
2414
+ /**
2415
+ * Get all throttle states
2416
+ */
2417
+ getAllThrottles() {
2418
+ return this.throttleManager.getAllThrottles();
2419
+ }
2420
+ /**
2421
+ * Release all throttles
2422
+ */
2423
+ async releaseAllThrottles() {
2424
+ return this.throttleManager.releaseAllThrottles();
2425
+ }
2426
+ };
2427
+ // Annotate the CommonJS export names for ESM import in node:
2428
+ 0 && (module.exports = {
2429
+ ConnectionState,
2430
+ JmriClient,
2431
+ LightState,
2432
+ MockResponseManager,
2433
+ PowerState,
2434
+ TurnoutState,
2435
+ WebSocketClient,
2436
+ isThrottleFunctionKey,
2437
+ isValidSpeed,
2438
+ lightStateToString,
2439
+ mockData,
2440
+ mockResponseManager,
2441
+ powerStateToString,
2442
+ turnoutStateToString
2443
+ });