jmri-client 4.2.0-beta.2 → 5.0.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 (58) hide show
  1. package/README.md +3 -1
  2. package/dist/cjs/index.js +2382 -31
  3. package/dist/esm/index.js +2333 -17
  4. package/docs/BROWSER.md +4 -4
  5. package/docs/MIGRATION.md +30 -1
  6. package/package.json +17 -18
  7. package/dist/cjs/client.js +0 -366
  8. package/dist/cjs/core/connection-state-manager.js +0 -84
  9. package/dist/cjs/core/heartbeat-manager.js +0 -79
  10. package/dist/cjs/core/index.js +0 -25
  11. package/dist/cjs/core/message-queue.js +0 -59
  12. package/dist/cjs/core/reconnection-manager.js +0 -97
  13. package/dist/cjs/core/websocket-adapter.js +0 -135
  14. package/dist/cjs/core/websocket-client.js +0 -388
  15. package/dist/cjs/managers/index.js +0 -25
  16. package/dist/cjs/managers/light-manager.js +0 -111
  17. package/dist/cjs/managers/power-manager.js +0 -90
  18. package/dist/cjs/managers/roster-manager.js +0 -118
  19. package/dist/cjs/managers/system-connections-manager.js +0 -28
  20. package/dist/cjs/managers/throttle-manager.js +0 -233
  21. package/dist/cjs/managers/turnout-manager.js +0 -111
  22. package/dist/cjs/mocks/index.js +0 -12
  23. package/dist/cjs/mocks/mock-data.js +0 -237
  24. package/dist/cjs/mocks/mock-response-manager.js +0 -290
  25. package/dist/cjs/types/client-options.js +0 -66
  26. package/dist/cjs/types/events.js +0 -16
  27. package/dist/cjs/types/index.js +0 -23
  28. package/dist/cjs/types/jmri-messages.js +0 -95
  29. package/dist/cjs/types/throttle.js +0 -19
  30. package/dist/cjs/utils/exponential-backoff.js +0 -40
  31. package/dist/cjs/utils/index.js +0 -21
  32. package/dist/cjs/utils/message-id.js +0 -40
  33. package/dist/esm/client.js +0 -362
  34. package/dist/esm/core/connection-state-manager.js +0 -80
  35. package/dist/esm/core/heartbeat-manager.js +0 -75
  36. package/dist/esm/core/index.js +0 -9
  37. package/dist/esm/core/message-queue.js +0 -55
  38. package/dist/esm/core/reconnection-manager.js +0 -93
  39. package/dist/esm/core/websocket-adapter.js +0 -98
  40. package/dist/esm/core/websocket-client.js +0 -384
  41. package/dist/esm/managers/index.js +0 -9
  42. package/dist/esm/managers/light-manager.js +0 -107
  43. package/dist/esm/managers/power-manager.js +0 -86
  44. package/dist/esm/managers/roster-manager.js +0 -114
  45. package/dist/esm/managers/system-connections-manager.js +0 -24
  46. package/dist/esm/managers/throttle-manager.js +0 -229
  47. package/dist/esm/managers/turnout-manager.js +0 -107
  48. package/dist/esm/mocks/index.js +0 -6
  49. package/dist/esm/mocks/mock-data.js +0 -234
  50. package/dist/esm/mocks/mock-response-manager.js +0 -286
  51. package/dist/esm/types/client-options.js +0 -62
  52. package/dist/esm/types/events.js +0 -13
  53. package/dist/esm/types/index.js +0 -7
  54. package/dist/esm/types/jmri-messages.js +0 -89
  55. package/dist/esm/types/throttle.js +0 -15
  56. package/dist/esm/utils/exponential-backoff.js +0 -36
  57. package/dist/esm/utils/index.js +0 -5
  58. package/dist/esm/utils/message-id.js +0 -36
package/dist/cjs/index.js CHANGED
@@ -1,32 +1,2383 @@
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": [],
585
+ "rosterGroups": []
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": [],
616
+ "rosterGroups": []
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": [],
648
+ "rosterGroups": []
649
+ },
650
+ "id": 3
651
+ }
652
+ ]
653
+ },
654
+ "throttle": {
655
+ "acquire": {
656
+ "success": {
657
+ "type": "throttle",
658
+ "data": {
659
+ "throttle": "{THROTTLE_ID}",
660
+ "address": "{ADDRESS}",
661
+ "speed": 0,
662
+ "forward": true,
663
+ "F0": false,
664
+ "F1": false,
665
+ "F2": false,
666
+ "F3": false,
667
+ "F4": false
668
+ }
669
+ }
670
+ },
671
+ "release": {
672
+ "success": {
673
+ "type": "throttle",
674
+ "data": {}
675
+ }
676
+ },
677
+ "control": {
678
+ "speed": {
679
+ "type": "throttle",
680
+ "data": {
681
+ "throttle": "{THROTTLE_ID}",
682
+ "speed": "{SPEED}"
683
+ }
684
+ },
685
+ "direction": {
686
+ "type": "throttle",
687
+ "data": {
688
+ "throttle": "{THROTTLE_ID}",
689
+ "forward": "{FORWARD}"
690
+ }
691
+ },
692
+ "function": {
693
+ "type": "throttle",
694
+ "data": {
695
+ "throttle": "{THROTTLE_ID}",
696
+ "{FUNCTION}": "{VALUE}"
697
+ }
698
+ }
699
+ }
700
+ },
701
+ "light": {
702
+ "list": [
703
+ { "type": "light", "data": { "name": "IL1", "userName": "Yard Light", "comment": null, "properties": [], "state": 4 } },
704
+ { "type": "light", "data": { "name": "IL2", "userName": "Platform Light", "comment": null, "properties": [], "state": 4 } },
705
+ { "type": "light", "data": { "name": "IL3", "userName": "Signal Lamp", "comment": null, "properties": [], "state": 2 } }
706
+ ]
707
+ },
708
+ "turnout": {
709
+ "list": [
710
+ { "type": "turnout", "data": { "name": "LT1", "userName": "Main Diverge", "state": 2 } },
711
+ { "type": "turnout", "data": { "name": "LT2", "userName": "Yard Lead", "state": 2 } },
712
+ { "type": "turnout", "data": { "name": "LT3", "userName": "Siding Entry", "state": 4 } }
713
+ ]
714
+ },
715
+ "ping": {
716
+ "type": "ping"
717
+ },
718
+ "pong": {
719
+ "type": "pong"
720
+ },
721
+ "goodbye": {
722
+ "type": "goodbye"
723
+ },
724
+ "error": {
725
+ "throttleNotFound": {
726
+ "type": "error",
727
+ "data": {
728
+ "code": 404,
729
+ "message": "Throttle not found"
730
+ }
731
+ },
732
+ "invalidSpeed": {
733
+ "type": "error",
734
+ "data": {
735
+ "code": 400,
736
+ "message": "Invalid speed value"
737
+ }
738
+ },
739
+ "connectionError": {
740
+ "type": "error",
741
+ "data": {
742
+ "code": 500,
743
+ "message": "Connection error"
744
+ }
745
+ }
746
+ }
747
+ };
748
+
749
+ // src/mocks/mock-response-manager.ts
750
+ var MockResponseManager = class {
751
+ constructor(options = {}) {
752
+ this.throttles = /* @__PURE__ */ new Map();
753
+ this.lights = /* @__PURE__ */ new Map([
754
+ ["IL1", 4 /* OFF */],
755
+ ["IL2", 4 /* OFF */],
756
+ ["IL3", 2 /* ON */]
757
+ ]);
758
+ this.turnouts = /* @__PURE__ */ new Map([
759
+ ["LT1", 2 /* CLOSED */],
760
+ ["LT2", 2 /* CLOSED */],
761
+ ["LT3", 4 /* THROWN */]
762
+ ]);
763
+ this.responseDelay = options.responseDelay ?? 50;
764
+ this.powerState = options.initialPowerState ?? 4 /* OFF */;
765
+ }
766
+ /**
767
+ * Get a mock response for a given message
768
+ */
769
+ async getMockResponse(message) {
770
+ if (this.responseDelay > 0) {
771
+ await this.delay(this.responseDelay);
772
+ }
773
+ switch (message.type) {
774
+ case "hello":
775
+ return this.getHelloResponse();
776
+ case "power":
777
+ return this.getPowerResponse(message);
778
+ case "roster":
779
+ return this.getRosterResponse(message);
780
+ case "throttle":
781
+ return this.getThrottleResponse(message);
782
+ case "light":
783
+ return this.getLightResponse(message);
784
+ case "turnout":
785
+ return this.getTurnoutResponse(message);
786
+ case "ping":
787
+ return this.getPingResponse();
788
+ case "goodbye":
789
+ return this.getGoodbyeResponse();
790
+ default:
791
+ return null;
792
+ }
793
+ }
794
+ /**
795
+ * Get hello response (connection establishment)
796
+ */
797
+ getHelloResponse() {
798
+ return JSON.parse(JSON.stringify(mockData.hello));
799
+ }
800
+ /**
801
+ * Get power response
802
+ */
803
+ getPowerResponse(message) {
804
+ if (message.data?.state !== void 0) {
805
+ this.powerState = message.data.state;
806
+ return {
807
+ type: "power",
808
+ data: { state: this.powerState }
809
+ };
810
+ }
811
+ return {
812
+ type: "power",
813
+ data: { state: this.powerState }
814
+ };
815
+ }
816
+ /**
817
+ * Get roster response
818
+ */
819
+ getRosterResponse(message) {
820
+ if (message.type === "roster" && message.method === "list") {
821
+ return {
822
+ type: "roster",
823
+ data: JSON.parse(JSON.stringify(mockData.roster.list))
824
+ };
825
+ }
826
+ return {
827
+ type: "roster",
828
+ data: []
829
+ };
830
+ }
831
+ /**
832
+ * Get throttle response
833
+ */
834
+ getThrottleResponse(message) {
835
+ const data = message.data || {};
836
+ if (data.address !== void 0 && !data.throttle) {
837
+ const throttleId = data.name || `MOCK-${data.address}`;
838
+ const throttleState = {
839
+ throttle: throttleId,
840
+ address: data.address,
841
+ speed: 0,
842
+ forward: true,
843
+ F0: false,
844
+ F1: false,
845
+ F2: false,
846
+ F3: false,
847
+ F4: false
848
+ };
849
+ this.throttles.set(throttleId, throttleState);
850
+ return {
851
+ type: "throttle",
852
+ data: { ...throttleState }
853
+ };
854
+ }
855
+ if (data.release !== void 0 && data.throttle) {
856
+ this.throttles.delete(data.throttle);
857
+ return {
858
+ type: "throttle",
859
+ data: {}
860
+ };
861
+ }
862
+ if (data.throttle) {
863
+ const throttleState = this.throttles.get(data.throttle);
864
+ if (!throttleState) {
865
+ const newState = {
866
+ throttle: data.throttle,
867
+ address: 0,
868
+ speed: 0,
869
+ forward: true
870
+ };
871
+ this.throttles.set(data.throttle, newState);
872
+ return {
873
+ type: "throttle",
874
+ data: { ...newState }
875
+ };
876
+ }
877
+ if (data.speed !== void 0) {
878
+ throttleState.speed = data.speed;
879
+ }
880
+ if (data.forward !== void 0) {
881
+ throttleState.forward = data.forward;
882
+ }
883
+ for (let i = 0; i <= 28; i++) {
884
+ const key = `F${i}`;
885
+ if (data[key] !== void 0) {
886
+ throttleState[key] = data[key];
887
+ }
888
+ }
889
+ return {
890
+ type: "throttle",
891
+ data: {}
892
+ };
893
+ }
894
+ return {
895
+ type: "throttle",
896
+ data: {}
897
+ };
898
+ }
899
+ /**
900
+ * Get light response
901
+ */
902
+ getLightResponse(message) {
903
+ if (message.method === "list") {
904
+ return {
905
+ type: "light",
906
+ data: JSON.parse(JSON.stringify(mockData.light.list))
907
+ };
908
+ }
909
+ const name = message.data?.name;
910
+ if (!name) {
911
+ return { type: "light", data: { name: "", state: 0 /* UNKNOWN */ } };
912
+ }
913
+ if (message.method === "post" && message.data?.state !== void 0) {
914
+ this.lights.set(name, message.data.state);
915
+ }
916
+ const state = this.lights.get(name) ?? 0 /* UNKNOWN */;
917
+ return { type: "light", data: { name, state } };
918
+ }
919
+ /**
920
+ * Get turnout response
921
+ */
922
+ getTurnoutResponse(message) {
923
+ if (message.method === "list") {
924
+ return {
925
+ type: "turnout",
926
+ data: JSON.parse(JSON.stringify(mockData.turnout.list))
927
+ };
928
+ }
929
+ const name = message.data?.name;
930
+ if (!name) {
931
+ return { type: "turnout", data: { name: "", state: 0 /* UNKNOWN */ } };
932
+ }
933
+ if (message.method === "post" && message.data?.state !== void 0) {
934
+ this.turnouts.set(name, message.data.state);
935
+ }
936
+ const state = this.turnouts.get(name) ?? 0 /* UNKNOWN */;
937
+ return { type: "turnout", data: { name, state } };
938
+ }
939
+ /**
940
+ * Get ping response (pong)
941
+ */
942
+ getPingResponse() {
943
+ return JSON.parse(JSON.stringify(mockData.pong));
944
+ }
945
+ /**
946
+ * Get goodbye response
947
+ */
948
+ getGoodbyeResponse() {
949
+ return JSON.parse(JSON.stringify(mockData.goodbye));
950
+ }
951
+ /**
952
+ * Get current power state
953
+ */
954
+ getPowerState() {
955
+ return this.powerState;
956
+ }
957
+ /**
958
+ * Set power state (for testing)
959
+ */
960
+ setPowerState(state) {
961
+ this.powerState = state;
962
+ }
963
+ /**
964
+ * Get all throttles (for testing)
965
+ */
966
+ getThrottles() {
967
+ return this.throttles;
968
+ }
969
+ /**
970
+ * Get all light states (for testing)
971
+ */
972
+ getLights() {
973
+ return this.lights;
974
+ }
975
+ /**
976
+ * Get all turnout states (for testing)
977
+ */
978
+ getTurnouts() {
979
+ return this.turnouts;
980
+ }
981
+ /**
982
+ * Reset all state (for testing)
983
+ */
984
+ reset() {
985
+ this.powerState = 4 /* OFF */;
986
+ this.throttles.clear();
987
+ this.lights = /* @__PURE__ */ new Map([
988
+ ["IL1", 4 /* OFF */],
989
+ ["IL2", 4 /* OFF */],
990
+ ["IL3", 2 /* ON */]
991
+ ]);
992
+ this.turnouts = /* @__PURE__ */ new Map([
993
+ ["LT1", 2 /* CLOSED */],
994
+ ["LT2", 2 /* CLOSED */],
995
+ ["LT3", 4 /* THROWN */]
996
+ ]);
997
+ }
998
+ /**
999
+ * Delay helper
1000
+ */
1001
+ delay(ms) {
1002
+ return new Promise((resolve) => setTimeout(resolve, ms));
1003
+ }
1004
+ };
1005
+ var mockResponseManager = new MockResponseManager({ responseDelay: 0 });
1006
+
1007
+ // src/core/websocket-client.ts
1008
+ var WebSocketClient = class extends import_eventemitter35.EventEmitter {
1009
+ constructor(options) {
1010
+ super();
1011
+ // Request/response tracking
1012
+ this.pendingRequests = /* @__PURE__ */ new Map();
1013
+ // Connection state
1014
+ this.isManualDisconnect = false;
1015
+ this.options = options;
1016
+ this.url = `${options.protocol}://${options.host}:${options.port}/json/`;
1017
+ this.messageIdGen = new MessageIdGenerator();
1018
+ this.messageQueue = new MessageQueue(options.messageQueueSize);
1019
+ this.stateManager = new ConnectionStateManager();
1020
+ this.heartbeatManager = new HeartbeatManager(options.heartbeat);
1021
+ this.reconnectionManager = new ReconnectionManager(options.reconnection);
1022
+ if (options.mock.enabled) {
1023
+ this.mockManager = new MockResponseManager({
1024
+ responseDelay: options.mock.responseDelay
1025
+ });
1026
+ }
1027
+ this.stateManager.on("stateChanged", (newState, prevState) => {
1028
+ this.emit("connectionStateChanged", newState, prevState);
1029
+ });
1030
+ this.heartbeatManager.on("timeout", () => {
1031
+ this.emit("heartbeat:timeout");
1032
+ this.handleHeartbeatTimeout();
1033
+ });
1034
+ this.heartbeatManager.on("pingSent", () => {
1035
+ this.emit("heartbeat:sent");
1036
+ });
1037
+ this.reconnectionManager.on("attemptScheduled", (attempt, delay) => {
1038
+ this.emit("reconnecting", attempt, delay);
1039
+ });
1040
+ this.reconnectionManager.on("success", () => {
1041
+ this.emit("reconnected");
1042
+ });
1043
+ this.reconnectionManager.on("maxAttemptsReached", (attempts) => {
1044
+ this.emit("reconnectionFailed", attempts);
1045
+ });
1046
+ this.reconnectionManager.on("debug", (message) => {
1047
+ this.emit("debug", message);
1048
+ });
1049
+ }
1050
+ /**
1051
+ * Connect to JMRI WebSocket server (or mock)
1052
+ */
1053
+ async connect() {
1054
+ if (this.stateManager.isConnected() || this.stateManager.isConnecting()) {
1055
+ return;
1056
+ }
1057
+ this.isManualDisconnect = false;
1058
+ this.stateManager.transition("connecting" /* CONNECTING */);
1059
+ if (this.mockManager) {
1060
+ return this.connectMock();
1061
+ }
1062
+ return new Promise(async (resolve, reject) => {
1063
+ try {
1064
+ this.ws = await createWebSocketAdapter(this.url);
1065
+ this.ws.on("open", () => {
1066
+ this.handleOpen();
1067
+ resolve();
1068
+ });
1069
+ this.ws.on("message", (data) => {
1070
+ this.handleMessage(data);
1071
+ });
1072
+ this.ws.on("close", (code, reason) => {
1073
+ if (this.stateManager.isConnecting()) {
1074
+ this.stateManager.transition("disconnected" /* DISCONNECTED */);
1075
+ const error = new Error(`WebSocket connection failed (code: ${code}${reason ? ", reason: " + reason : ""})`);
1076
+ this.emit("error", error);
1077
+ reject(error);
1078
+ this.handleClose(code, reason);
1079
+ } else {
1080
+ this.handleClose(code, reason);
1081
+ }
1082
+ });
1083
+ this.ws.on("error", (error) => {
1084
+ this.emit("error", error);
1085
+ if (this.stateManager.isConnecting()) {
1086
+ this.stateManager.transition("disconnected" /* DISCONNECTED */);
1087
+ reject(error);
1088
+ }
1089
+ });
1090
+ } catch (error) {
1091
+ this.stateManager.transition("disconnected" /* DISCONNECTED */);
1092
+ reject(error);
1093
+ }
1094
+ });
1095
+ }
1096
+ /**
1097
+ * Simulate connection in mock mode
1098
+ */
1099
+ async connectMock() {
1100
+ await this.delay(10);
1101
+ this.handleOpen();
1102
+ const helloResponse = await this.mockManager.getMockResponse({ type: "hello" });
1103
+ if (helloResponse) {
1104
+ this.processMessage(helloResponse);
1105
+ }
1106
+ }
1107
+ /**
1108
+ * Disconnect from JMRI WebSocket server
1109
+ */
1110
+ async disconnect() {
1111
+ if (this.stateManager.isDisconnected()) {
1112
+ return;
1113
+ }
1114
+ this.isManualDisconnect = true;
1115
+ this.reconnectionManager.stop();
1116
+ this.heartbeatManager.stop();
1117
+ if (this.stateManager.isConnected()) {
1118
+ try {
1119
+ await this.sendGoodbye();
1120
+ } catch (error) {
1121
+ }
1122
+ }
1123
+ if (this.ws) {
1124
+ this.ws.close();
1125
+ this.ws = void 0;
1126
+ }
1127
+ this.rejectAllPendingRequests(new Error("Client disconnected"));
1128
+ if (!this.stateManager.isDisconnected()) {
1129
+ this.stateManager.transition("disconnected" /* DISCONNECTED */);
1130
+ }
1131
+ }
1132
+ /**
1133
+ * Send message to JMRI (or mock)
1134
+ */
1135
+ send(message) {
1136
+ if (!this.stateManager.isConnected()) {
1137
+ this.messageQueue.enqueue(message);
1138
+ return;
1139
+ }
1140
+ if (this.mockManager) {
1141
+ this.emit("message:sent", message);
1142
+ return;
1143
+ }
1144
+ if (!this.ws) {
1145
+ throw new Error("WebSocket not initialized");
1146
+ }
1147
+ const json = JSON.stringify(message);
1148
+ this.ws.send(json);
1149
+ this.emit("message:sent", message);
1150
+ }
1151
+ /**
1152
+ * Send request and wait for response (or get mock response)
1153
+ */
1154
+ async request(message, timeout) {
1155
+ if (this.mockManager) {
1156
+ const response = await this.mockManager.getMockResponse(message);
1157
+ this.emit("message:sent", message);
1158
+ if (response) {
1159
+ this.processMessage(response);
1160
+ }
1161
+ return response;
1162
+ }
1163
+ const id = this.messageIdGen.next();
1164
+ message.id = id;
1165
+ return new Promise((resolve, reject) => {
1166
+ const timeoutMs = timeout || this.options.requestTimeout;
1167
+ const timeoutHandle = setTimeout(() => {
1168
+ this.pendingRequests.delete(id);
1169
+ reject(new Error(`Request timeout after ${timeoutMs}ms`));
1170
+ }, timeoutMs);
1171
+ const pendingRequest = {
1172
+ resolve,
1173
+ reject,
1174
+ timeout: timeoutHandle,
1175
+ messageType: message.type
1176
+ };
1177
+ if (message.type === "throttle" && message.data && "name" in message.data) {
1178
+ pendingRequest.matchKey = message.data.name;
1179
+ }
1180
+ this.pendingRequests.set(id, pendingRequest);
1181
+ try {
1182
+ this.send(message);
1183
+ } catch (error) {
1184
+ this.pendingRequests.delete(id);
1185
+ clearTimeout(timeoutHandle);
1186
+ reject(error);
1187
+ }
1188
+ });
1189
+ }
1190
+ /**
1191
+ * Get current connection state
1192
+ */
1193
+ getState() {
1194
+ return this.stateManager.getState();
1195
+ }
1196
+ /**
1197
+ * Check if connected
1198
+ */
1199
+ isConnected() {
1200
+ return this.stateManager.isConnected();
1201
+ }
1202
+ /**
1203
+ * Handle WebSocket open event
1204
+ */
1205
+ handleOpen() {
1206
+ this.stateManager.transition("connected" /* CONNECTED */);
1207
+ this.emit("connected");
1208
+ if (this.options.heartbeat.enabled) {
1209
+ this.heartbeatManager.start(() => this.sendPing());
1210
+ }
1211
+ const queuedMessages = this.messageQueue.flush();
1212
+ for (const message of queuedMessages) {
1213
+ this.send(message);
1214
+ }
1215
+ }
1216
+ /**
1217
+ * Handle WebSocket message event
1218
+ */
1219
+ handleMessage(data) {
1220
+ try {
1221
+ const message = JSON.parse(data);
1222
+ this.processMessage(message);
1223
+ } catch (error) {
1224
+ this.emit("error", new Error(`Failed to parse message: ${error}`));
1225
+ }
1226
+ }
1227
+ /**
1228
+ * Process a parsed message (called by both real and mock mode)
1229
+ */
1230
+ processMessage(message) {
1231
+ this.emit("message:received", message);
1232
+ if (message.type === "pong") {
1233
+ this.heartbeatManager.receivedPong();
1234
+ return;
1235
+ }
1236
+ if (message.type === "hello") {
1237
+ this.emit("hello", message.data);
1238
+ return;
1239
+ }
1240
+ if (message.id !== void 0) {
1241
+ const pending = this.pendingRequests.get(message.id);
1242
+ if (pending) {
1243
+ clearTimeout(pending.timeout);
1244
+ this.pendingRequests.delete(message.id);
1245
+ pending.resolve(message);
1246
+ return;
1247
+ }
1248
+ }
1249
+ if (message.id === void 0) {
1250
+ for (const [id, pending] of this.pendingRequests.entries()) {
1251
+ if (pending.messageType === message.type) {
1252
+ if (message.type === "throttle" && pending.matchKey) {
1253
+ const throttleName = message.data?.throttle || message.data?.name;
1254
+ if (throttleName === pending.matchKey) {
1255
+ clearTimeout(pending.timeout);
1256
+ this.pendingRequests.delete(id);
1257
+ pending.resolve(message);
1258
+ return;
1259
+ }
1260
+ } else {
1261
+ clearTimeout(pending.timeout);
1262
+ this.pendingRequests.delete(id);
1263
+ pending.resolve(message);
1264
+ return;
1265
+ }
1266
+ }
1267
+ }
1268
+ }
1269
+ this.emit("update", message);
1270
+ }
1271
+ /**
1272
+ * Handle WebSocket close event
1273
+ */
1274
+ handleClose(code, reason) {
1275
+ this.heartbeatManager.stop();
1276
+ const wasConnected = this.stateManager.isConnected();
1277
+ const isReconnecting = this.reconnectionManager.reconnecting();
1278
+ if (this.stateManager.isConnected() || this.stateManager.isConnecting()) {
1279
+ this.stateManager.transition("disconnected" /* DISCONNECTED */);
1280
+ }
1281
+ this.emit("disconnected", reason || `Connection closed (code: ${code})`);
1282
+ this.rejectAllPendingRequests(new Error("Connection closed"));
1283
+ if (!this.isManualDisconnect && (wasConnected || isReconnecting) && this.options.reconnection.enabled) {
1284
+ this.stateManager.forceState("reconnecting" /* RECONNECTING */);
1285
+ this.reconnectionManager.start(() => this.connect());
1286
+ }
1287
+ }
1288
+ /**
1289
+ * Handle heartbeat timeout
1290
+ */
1291
+ handleHeartbeatTimeout() {
1292
+ if (this.ws) {
1293
+ this.ws.close();
1294
+ }
1295
+ }
1296
+ /**
1297
+ * Send ping message
1298
+ */
1299
+ sendPing() {
1300
+ this.send({ type: "ping" });
1301
+ }
1302
+ /**
1303
+ * Send goodbye message
1304
+ */
1305
+ async sendGoodbye() {
1306
+ const message = { type: "goodbye" };
1307
+ this.send(message);
1308
+ await new Promise((resolve) => setTimeout(resolve, 100));
1309
+ }
1310
+ /**
1311
+ * Reject all pending requests
1312
+ */
1313
+ rejectAllPendingRequests(error) {
1314
+ for (const pending of this.pendingRequests.values()) {
1315
+ clearTimeout(pending.timeout);
1316
+ pending.reject(error);
1317
+ }
1318
+ this.pendingRequests.clear();
1319
+ }
1320
+ /**
1321
+ * Delay helper
1322
+ */
1323
+ delay(ms) {
1324
+ return new Promise((resolve) => setTimeout(resolve, ms));
1325
+ }
1326
+ };
1327
+
1328
+ // src/managers/power-manager.ts
1329
+ var import_eventemitter36 = require("eventemitter3");
1330
+ var PowerManager = class extends import_eventemitter36.EventEmitter {
1331
+ constructor(client) {
1332
+ super();
1333
+ this.currentState = 0 /* UNKNOWN */;
1334
+ this.client = client;
1335
+ this.client.on("update", (message) => {
1336
+ if (message.type === "power") {
1337
+ this.handlePowerUpdate(message);
1338
+ }
1339
+ });
1340
+ }
1341
+ /**
1342
+ * Get current track power state
1343
+ * @param prefix - Optional JMRI connection prefix to target a specific hardware connection
1344
+ */
1345
+ async getPower(prefix) {
1346
+ const message = {
1347
+ type: "power",
1348
+ ...prefix !== void 0 && { data: { state: 0 /* UNKNOWN */, prefix } }
1349
+ };
1350
+ const response = await this.client.request(message);
1351
+ if (response.data?.state !== void 0) {
1352
+ this.currentState = response.data.state;
1353
+ }
1354
+ return this.currentState;
1355
+ }
1356
+ /**
1357
+ * Set track power state
1358
+ * @param state - The desired power state
1359
+ * @param prefix - Optional JMRI connection prefix to target a specific hardware connection
1360
+ */
1361
+ async setPower(state, prefix) {
1362
+ const message = {
1363
+ type: "power",
1364
+ method: "post",
1365
+ data: { state, ...prefix !== void 0 && { prefix } }
1366
+ };
1367
+ await this.client.request(message);
1368
+ const oldState = this.currentState;
1369
+ this.currentState = state;
1370
+ if (oldState !== this.currentState) {
1371
+ this.emit("power:changed", this.currentState);
1372
+ }
1373
+ }
1374
+ /**
1375
+ * Turn track power on
1376
+ * @param prefix - Optional JMRI connection prefix to target a specific hardware connection
1377
+ */
1378
+ async powerOn(prefix) {
1379
+ await this.setPower(2 /* ON */, prefix);
1380
+ }
1381
+ /**
1382
+ * Turn track power off
1383
+ * @param prefix - Optional JMRI connection prefix to target a specific hardware connection
1384
+ */
1385
+ async powerOff(prefix) {
1386
+ await this.setPower(4 /* OFF */, prefix);
1387
+ }
1388
+ /**
1389
+ * Get cached power state (no network request)
1390
+ */
1391
+ getCachedState() {
1392
+ return this.currentState;
1393
+ }
1394
+ /**
1395
+ * Handle unsolicited power updates from JMRI
1396
+ */
1397
+ handlePowerUpdate(message) {
1398
+ if (message.data?.state !== void 0) {
1399
+ const oldState = this.currentState;
1400
+ this.currentState = message.data.state;
1401
+ if (oldState !== this.currentState) {
1402
+ this.emit("power:changed", this.currentState);
1403
+ }
1404
+ }
1405
+ }
1406
+ };
1407
+
1408
+ // src/managers/roster-manager.ts
1409
+ var RosterManager = class {
1410
+ constructor(client) {
1411
+ this.rosterCache = /* @__PURE__ */ new Map();
1412
+ this.client = client;
1413
+ }
1414
+ /**
1415
+ * Get all roster entries
1416
+ */
1417
+ async getRoster() {
1418
+ const message = {
1419
+ type: "roster",
1420
+ method: "list"
1421
+ };
1422
+ const response = await this.client.request(message);
1423
+ if (response.data) {
1424
+ this.updateCache(response.data);
1425
+ }
1426
+ return Array.from(this.rosterCache.values());
1427
+ }
1428
+ /**
1429
+ * Get roster entry by name
1430
+ */
1431
+ async getRosterEntryByName(name) {
1432
+ if (this.rosterCache.has(name)) {
1433
+ return this.rosterCache.get(name);
1434
+ }
1435
+ await this.getRoster();
1436
+ return this.rosterCache.get(name);
1437
+ }
1438
+ /**
1439
+ * Get roster entry by address
1440
+ */
1441
+ async getRosterEntryByAddress(address) {
1442
+ if (this.rosterCache.size === 0) {
1443
+ await this.getRoster();
1444
+ }
1445
+ const addressStr = address.toString();
1446
+ for (const wrapper of this.rosterCache.values()) {
1447
+ if (wrapper.data.address === addressStr) {
1448
+ return wrapper;
1449
+ }
1450
+ }
1451
+ return void 0;
1452
+ }
1453
+ /**
1454
+ * Search roster by partial name match
1455
+ */
1456
+ async searchRoster(query) {
1457
+ if (this.rosterCache.size === 0) {
1458
+ await this.getRoster();
1459
+ }
1460
+ const lowerQuery = query.toLowerCase();
1461
+ const results = [];
1462
+ for (const wrapper of this.rosterCache.values()) {
1463
+ const entry = wrapper.data;
1464
+ if (entry.name.toLowerCase().includes(lowerQuery) || entry.address.includes(query) || entry.road?.toLowerCase().includes(lowerQuery) || entry.number?.includes(query)) {
1465
+ results.push(wrapper);
1466
+ }
1467
+ }
1468
+ return results;
1469
+ }
1470
+ /**
1471
+ * Get cached roster (no network request)
1472
+ */
1473
+ getCachedRoster() {
1474
+ return Array.from(this.rosterCache.values());
1475
+ }
1476
+ /**
1477
+ * Clear roster cache
1478
+ */
1479
+ clearCache() {
1480
+ this.rosterCache.clear();
1481
+ }
1482
+ /**
1483
+ * Update internal cache from roster data
1484
+ */
1485
+ updateCache(rosterData) {
1486
+ this.rosterCache.clear();
1487
+ if (Array.isArray(rosterData)) {
1488
+ for (const wrapper of rosterData) {
1489
+ if (wrapper.type === "rosterEntry" && wrapper.data) {
1490
+ this.rosterCache.set(wrapper.data.name, wrapper);
1491
+ }
1492
+ }
1493
+ } else {
1494
+ let id = 1;
1495
+ for (const [name, entry] of Object.entries(rosterData)) {
1496
+ const wrapper = {
1497
+ type: "rosterEntry",
1498
+ data: entry,
1499
+ id: id++
1500
+ };
1501
+ this.rosterCache.set(name, wrapper);
1502
+ }
1503
+ }
1504
+ }
1505
+ };
1506
+
1507
+ // src/managers/throttle-manager.ts
1508
+ var import_eventemitter37 = require("eventemitter3");
1509
+
1510
+ // src/types/throttle.ts
1511
+ function isThrottleFunctionKey(key) {
1512
+ return /^F([0-9]|1[0-9]|2[0-8])$/.test(key);
1513
+ }
1514
+ function isValidSpeed(speed) {
1515
+ return typeof speed === "number" && speed >= 0 && speed <= 1;
1516
+ }
1517
+
1518
+ // src/managers/throttle-manager.ts
1519
+ var ThrottleManager = class extends import_eventemitter37.EventEmitter {
1520
+ constructor(client) {
1521
+ super();
1522
+ this.throttles = /* @__PURE__ */ new Map();
1523
+ this.client = client;
1524
+ this.clientId = `jmri-client-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1525
+ this.client.on("update", (message) => {
1526
+ if (message.type === "throttle") {
1527
+ this.handleThrottleUpdate(message);
1528
+ }
1529
+ });
1530
+ this.client.on("disconnected", () => {
1531
+ this.handleDisconnect();
1532
+ });
1533
+ }
1534
+ /**
1535
+ * Acquire a throttle for a locomotive
1536
+ */
1537
+ async acquireThrottle(options) {
1538
+ const throttleName = `${this.clientId}-${options.address}`;
1539
+ const message = {
1540
+ type: "throttle",
1541
+ data: {
1542
+ name: throttleName,
1543
+ address: options.address,
1544
+ ...options.prefix !== void 0 && { prefix: options.prefix }
1545
+ }
1546
+ };
1547
+ const response = await this.client.request(message);
1548
+ const throttleId = response.data?.throttle || throttleName;
1549
+ if (!throttleId) {
1550
+ throw new Error("Failed to acquire throttle: no throttle ID returned");
1551
+ }
1552
+ const state = {
1553
+ id: throttleId,
1554
+ address: options.address,
1555
+ speed: 0,
1556
+ forward: true,
1557
+ functions: /* @__PURE__ */ new Map(),
1558
+ acquired: true
1559
+ };
1560
+ this.throttles.set(throttleId, state);
1561
+ this.emit("throttle:acquired", throttleId);
1562
+ return throttleId;
1563
+ }
1564
+ /**
1565
+ * Release a throttle
1566
+ */
1567
+ async releaseThrottle(throttleId) {
1568
+ const state = this.throttles.get(throttleId);
1569
+ if (!state) {
1570
+ throw new Error(`Throttle not found: ${throttleId}`);
1571
+ }
1572
+ const message = {
1573
+ type: "throttle",
1574
+ data: {
1575
+ throttle: throttleId,
1576
+ release: null
1577
+ }
1578
+ };
1579
+ await this.client.request(message);
1580
+ this.throttles.delete(throttleId);
1581
+ this.emit("throttle:released", throttleId);
1582
+ }
1583
+ /**
1584
+ * Set throttle speed (0.0 to 1.0)
1585
+ */
1586
+ async setSpeed(throttleId, speed) {
1587
+ const state = this.throttles.get(throttleId);
1588
+ if (!state) {
1589
+ throw new Error(`Throttle not found: ${throttleId}`);
1590
+ }
1591
+ if (!isValidSpeed(speed)) {
1592
+ throw new Error(`Invalid speed: ${speed}. Must be between 0.0 and 1.0`);
1593
+ }
1594
+ const message = {
1595
+ type: "throttle",
1596
+ data: {
1597
+ throttle: throttleId,
1598
+ speed
1599
+ }
1600
+ };
1601
+ this.client.send(message);
1602
+ state.speed = speed;
1603
+ this.emit("throttle:updated", throttleId, { speed });
1604
+ }
1605
+ /**
1606
+ * Set throttle direction
1607
+ */
1608
+ async setDirection(throttleId, forward) {
1609
+ const state = this.throttles.get(throttleId);
1610
+ if (!state) {
1611
+ throw new Error(`Throttle not found: ${throttleId}`);
1612
+ }
1613
+ const message = {
1614
+ type: "throttle",
1615
+ data: {
1616
+ throttle: throttleId,
1617
+ forward
1618
+ }
1619
+ };
1620
+ this.client.send(message);
1621
+ state.forward = forward;
1622
+ this.emit("throttle:updated", throttleId, { forward });
1623
+ }
1624
+ /**
1625
+ * Set throttle function (F0-F28)
1626
+ */
1627
+ async setFunction(throttleId, functionKey, value) {
1628
+ const state = this.throttles.get(throttleId);
1629
+ if (!state) {
1630
+ throw new Error(`Throttle not found: ${throttleId}`);
1631
+ }
1632
+ if (!isThrottleFunctionKey(functionKey)) {
1633
+ throw new Error(`Invalid function key: ${functionKey}`);
1634
+ }
1635
+ const data = {
1636
+ throttle: throttleId,
1637
+ [functionKey]: value
1638
+ };
1639
+ const message = {
1640
+ type: "throttle",
1641
+ data
1642
+ };
1643
+ this.client.send(message);
1644
+ state.functions.set(functionKey, value);
1645
+ this.emit("throttle:updated", throttleId, { [functionKey]: value });
1646
+ }
1647
+ /**
1648
+ * Emergency stop for a throttle (speed to 0)
1649
+ */
1650
+ async emergencyStop(throttleId) {
1651
+ await this.setSpeed(throttleId, 0);
1652
+ }
1653
+ /**
1654
+ * Set throttle to idle (speed to 0, maintain direction)
1655
+ */
1656
+ async idle(throttleId) {
1657
+ await this.setSpeed(throttleId, 0);
1658
+ }
1659
+ /**
1660
+ * Get throttle state
1661
+ */
1662
+ getThrottleState(throttleId) {
1663
+ return this.throttles.get(throttleId);
1664
+ }
1665
+ /**
1666
+ * Get all throttle IDs
1667
+ */
1668
+ getThrottleIds() {
1669
+ return Array.from(this.throttles.keys());
1670
+ }
1671
+ /**
1672
+ * Get all throttle states
1673
+ */
1674
+ getAllThrottles() {
1675
+ return Array.from(this.throttles.values());
1676
+ }
1677
+ /**
1678
+ * Release all throttles
1679
+ */
1680
+ async releaseAllThrottles() {
1681
+ const throttleIds = this.getThrottleIds();
1682
+ for (const throttleId of throttleIds) {
1683
+ try {
1684
+ await this.releaseThrottle(throttleId);
1685
+ } catch (error) {
1686
+ this.emit("error", error);
1687
+ }
1688
+ }
1689
+ }
1690
+ /**
1691
+ * Handle unsolicited throttle updates from JMRI
1692
+ */
1693
+ handleThrottleUpdate(message) {
1694
+ const throttleId = message.data?.throttle;
1695
+ if (!throttleId) {
1696
+ return;
1697
+ }
1698
+ const state = this.throttles.get(throttleId);
1699
+ if (!state) {
1700
+ this.emit("throttle:lost", throttleId);
1701
+ return;
1702
+ }
1703
+ if (message.data.speed !== void 0) {
1704
+ state.speed = message.data.speed;
1705
+ }
1706
+ if (message.data.forward !== void 0) {
1707
+ state.forward = message.data.forward;
1708
+ }
1709
+ for (let i = 0; i <= 28; i++) {
1710
+ const key = `F${i}`;
1711
+ if (message.data[key] !== void 0) {
1712
+ state.functions.set(key, message.data[key]);
1713
+ }
1714
+ }
1715
+ this.emit("throttle:updated", throttleId, message.data);
1716
+ }
1717
+ /**
1718
+ * Handle disconnect - mark all throttles as not acquired
1719
+ */
1720
+ handleDisconnect() {
1721
+ for (const state of this.throttles.values()) {
1722
+ state.acquired = false;
1723
+ }
1724
+ }
1725
+ };
1726
+
1727
+ // src/managers/turnout-manager.ts
1728
+ var import_eventemitter38 = require("eventemitter3");
1729
+ var TurnoutManager = class extends import_eventemitter38.EventEmitter {
1730
+ constructor(client) {
1731
+ super();
1732
+ this.turnouts = /* @__PURE__ */ new Map();
1733
+ this.client = client;
1734
+ this.client.on("update", (message) => {
1735
+ if (message.type === "turnout") {
1736
+ this.handleTurnoutUpdate(message);
1737
+ }
1738
+ });
1739
+ }
1740
+ /**
1741
+ * Get the current state of a turnout.
1742
+ * Also registers a server-side listener so subsequent changes are pushed.
1743
+ */
1744
+ async getTurnout(name) {
1745
+ const message = {
1746
+ type: "turnout",
1747
+ data: { name }
1748
+ };
1749
+ const response = await this.client.request(message);
1750
+ const state = response.data?.state ?? 0 /* UNKNOWN */;
1751
+ this.turnouts.set(name, state);
1752
+ return state;
1753
+ }
1754
+ /**
1755
+ * Set a turnout to the given state
1756
+ */
1757
+ async setTurnout(name, state) {
1758
+ const message = {
1759
+ type: "turnout",
1760
+ method: "post",
1761
+ data: { name, state }
1762
+ };
1763
+ await this.client.request(message);
1764
+ const oldState = this.turnouts.get(name);
1765
+ this.turnouts.set(name, state);
1766
+ if (oldState !== state) {
1767
+ this.emit("turnout:changed", name, state);
1768
+ }
1769
+ }
1770
+ /**
1771
+ * Throw a turnout (diverging route)
1772
+ */
1773
+ async throwTurnout(name) {
1774
+ return this.setTurnout(name, 4 /* THROWN */);
1775
+ }
1776
+ /**
1777
+ * Close a turnout (straight through / normal)
1778
+ */
1779
+ async closeTurnout(name) {
1780
+ return this.setTurnout(name, 2 /* CLOSED */);
1781
+ }
1782
+ /**
1783
+ * List all turnouts known to JMRI
1784
+ */
1785
+ async listTurnouts() {
1786
+ const message = {
1787
+ type: "turnout",
1788
+ method: "list"
1789
+ };
1790
+ const response = await this.client.request(message);
1791
+ const entries = Array.isArray(response?.data) ? response.data.map((r) => r.data ?? r) : [];
1792
+ for (const entry of entries) {
1793
+ if (entry.name && entry.state !== void 0) {
1794
+ this.turnouts.set(entry.name, entry.state);
1795
+ }
1796
+ }
1797
+ return entries;
1798
+ }
1799
+ /**
1800
+ * Get cached turnout state without a network request
1801
+ */
1802
+ getTurnoutState(name) {
1803
+ return this.turnouts.get(name);
1804
+ }
1805
+ /**
1806
+ * Get all cached turnout states
1807
+ */
1808
+ getCachedTurnouts() {
1809
+ return new Map(this.turnouts);
1810
+ }
1811
+ /**
1812
+ * Handle unsolicited turnout state updates from JMRI
1813
+ */
1814
+ handleTurnoutUpdate(message) {
1815
+ const name = message.data?.name;
1816
+ const state = message.data?.state;
1817
+ if (!name || state === void 0) return;
1818
+ const oldState = this.turnouts.get(name);
1819
+ this.turnouts.set(name, state);
1820
+ if (oldState !== state) {
1821
+ this.emit("turnout:changed", name, state);
1822
+ }
1823
+ }
1824
+ };
1825
+
1826
+ // src/managers/light-manager.ts
1827
+ var import_eventemitter39 = require("eventemitter3");
1828
+ var LightManager = class extends import_eventemitter39.EventEmitter {
1829
+ constructor(client) {
1830
+ super();
1831
+ this.lights = /* @__PURE__ */ new Map();
1832
+ this.client = client;
1833
+ this.client.on("update", (message) => {
1834
+ if (message.type === "light") {
1835
+ this.handleLightUpdate(message);
1836
+ }
1837
+ });
1838
+ }
1839
+ /**
1840
+ * Get the current state of a light.
1841
+ * Also registers a server-side listener so subsequent changes are pushed.
1842
+ */
1843
+ async getLight(name) {
1844
+ const message = {
1845
+ type: "light",
1846
+ data: { name }
1847
+ };
1848
+ const response = await this.client.request(message);
1849
+ const state = response.data?.state ?? 0 /* UNKNOWN */;
1850
+ this.lights.set(name, state);
1851
+ return state;
1852
+ }
1853
+ /**
1854
+ * Set a light to the given state
1855
+ */
1856
+ async setLight(name, state) {
1857
+ const message = {
1858
+ type: "light",
1859
+ method: "post",
1860
+ data: { name, state }
1861
+ };
1862
+ await this.client.request(message);
1863
+ const oldState = this.lights.get(name);
1864
+ this.lights.set(name, state);
1865
+ if (oldState !== state) {
1866
+ this.emit("light:changed", name, state);
1867
+ }
1868
+ }
1869
+ /**
1870
+ * Turn a light on
1871
+ */
1872
+ async turnOnLight(name) {
1873
+ return this.setLight(name, 2 /* ON */);
1874
+ }
1875
+ /**
1876
+ * Turn a light off
1877
+ */
1878
+ async turnOffLight(name) {
1879
+ return this.setLight(name, 4 /* OFF */);
1880
+ }
1881
+ /**
1882
+ * List all lights known to JMRI
1883
+ */
1884
+ async listLights() {
1885
+ const message = {
1886
+ type: "light",
1887
+ method: "list"
1888
+ };
1889
+ const response = await this.client.request(message);
1890
+ const entries = Array.isArray(response?.data) ? response.data.map((r) => r.data ?? r) : [];
1891
+ for (const entry of entries) {
1892
+ if (entry.name && entry.state !== void 0) {
1893
+ this.lights.set(entry.name, entry.state);
1894
+ }
1895
+ }
1896
+ return entries;
1897
+ }
1898
+ /**
1899
+ * Get cached light state without a network request
1900
+ */
1901
+ getLightState(name) {
1902
+ return this.lights.get(name);
1903
+ }
1904
+ /**
1905
+ * Get all cached light states
1906
+ */
1907
+ getCachedLights() {
1908
+ return new Map(this.lights);
1909
+ }
1910
+ /**
1911
+ * Handle unsolicited light state updates from JMRI
1912
+ */
1913
+ handleLightUpdate(message) {
1914
+ const name = message.data?.name;
1915
+ const state = message.data?.state;
1916
+ if (!name || state === void 0) return;
1917
+ const oldState = this.lights.get(name);
1918
+ this.lights.set(name, state);
1919
+ if (oldState !== state) {
1920
+ this.emit("light:changed", name, state);
1921
+ }
1922
+ }
1923
+ };
1924
+
1925
+ // src/managers/system-connections-manager.ts
1926
+ var SystemConnectionsManager = class {
1927
+ constructor(client) {
1928
+ this.client = client;
1929
+ }
1930
+ /**
1931
+ * List all available JMRI system connections and their prefixes.
1932
+ * Use the returned prefix values with power and throttle commands to
1933
+ * target a specific hardware connection when multiple are configured.
1934
+ */
1935
+ async getSystemConnections() {
1936
+ const message = {
1937
+ type: "systemConnections",
1938
+ method: "list"
1939
+ };
1940
+ const response = await this.client.request(message);
1941
+ if (!response.data) {
1942
+ return [];
1943
+ }
1944
+ return Array.isArray(response.data) ? response.data : [response.data];
1945
+ }
1946
+ };
1947
+
1948
+ // src/types/client-options.ts
1949
+ var DEFAULT_CLIENT_OPTIONS = {
1950
+ host: "localhost",
1951
+ port: 12080,
1952
+ protocol: "ws",
1953
+ autoConnect: true,
1954
+ reconnection: {
1955
+ enabled: true,
1956
+ maxAttempts: 0,
1957
+ // infinite
1958
+ initialDelay: 1e3,
1959
+ maxDelay: 3e4,
1960
+ multiplier: 1.5,
1961
+ jitter: true
1962
+ },
1963
+ heartbeat: {
1964
+ enabled: true,
1965
+ interval: 3e4,
1966
+ timeout: 5e3
1967
+ },
1968
+ messageQueueSize: 100,
1969
+ requestTimeout: 1e4,
1970
+ mock: {
1971
+ enabled: false,
1972
+ responseDelay: 50
1973
+ }
1974
+ };
1975
+ function mergeOptions(userOptions) {
1976
+ const options = { ...DEFAULT_CLIENT_OPTIONS };
1977
+ if (!userOptions) {
1978
+ return options;
1979
+ }
1980
+ if (userOptions.host !== void 0) options.host = userOptions.host;
1981
+ if (userOptions.port !== void 0) options.port = userOptions.port;
1982
+ if (userOptions.protocol !== void 0) options.protocol = userOptions.protocol;
1983
+ if (userOptions.autoConnect !== void 0) options.autoConnect = userOptions.autoConnect;
1984
+ if (userOptions.messageQueueSize !== void 0) options.messageQueueSize = userOptions.messageQueueSize;
1985
+ if (userOptions.requestTimeout !== void 0) options.requestTimeout = userOptions.requestTimeout;
1986
+ if (userOptions.reconnection) {
1987
+ options.reconnection = { ...DEFAULT_CLIENT_OPTIONS.reconnection, ...userOptions.reconnection };
1988
+ }
1989
+ if (userOptions.heartbeat) {
1990
+ options.heartbeat = { ...DEFAULT_CLIENT_OPTIONS.heartbeat, ...userOptions.heartbeat };
1991
+ }
1992
+ if (userOptions.mock) {
1993
+ options.mock = { ...DEFAULT_CLIENT_OPTIONS.mock, ...userOptions.mock };
1994
+ }
1995
+ return options;
1996
+ }
1997
+
1998
+ // src/client.ts
1999
+ var JmriClient = class extends import_eventemitter310.EventEmitter {
2000
+ /**
2001
+ * Create a new JMRI client
2002
+ *
2003
+ * @param options - Client configuration options
2004
+ *
2005
+ * @example
2006
+ * ```typescript
2007
+ * const client = new JmriClient({
2008
+ * host: 'jmri.local',
2009
+ * port: 12080
2010
+ * });
2011
+ *
2012
+ * client.on('connected', () => console.log('Connected!'));
2013
+ * client.on('power:changed', (state) => console.log('Power:', state));
2014
+ * ```
2015
+ */
2016
+ constructor(options) {
2017
+ super();
2018
+ this.options = mergeOptions(options);
2019
+ this.wsClient = new WebSocketClient(this.options);
2020
+ this.powerManager = new PowerManager(this.wsClient);
2021
+ this.rosterManager = new RosterManager(this.wsClient);
2022
+ this.throttleManager = new ThrottleManager(this.wsClient);
2023
+ this.turnoutManager = new TurnoutManager(this.wsClient);
2024
+ this.lightManager = new LightManager(this.wsClient);
2025
+ this.systemConnectionsManager = new SystemConnectionsManager(this.wsClient);
2026
+ this.wsClient.on("connected", () => this.emit("connected"));
2027
+ this.wsClient.on("disconnected", (reason) => this.emit("disconnected", reason));
2028
+ this.wsClient.on(
2029
+ "reconnecting",
2030
+ (attempt, delay) => this.emit("reconnecting", attempt, delay)
2031
+ );
2032
+ this.wsClient.on("reconnected", () => this.emit("reconnected"));
2033
+ this.wsClient.on(
2034
+ "reconnectionFailed",
2035
+ (attempts) => this.emit("reconnectionFailed", attempts)
2036
+ );
2037
+ this.wsClient.on(
2038
+ "connectionStateChanged",
2039
+ (state) => this.emit("connectionStateChanged", state)
2040
+ );
2041
+ this.wsClient.on("error", (error) => this.emit("error", error));
2042
+ this.wsClient.on("heartbeat:sent", () => this.emit("heartbeat:sent"));
2043
+ this.wsClient.on("heartbeat:timeout", () => this.emit("heartbeat:timeout"));
2044
+ this.wsClient.on("hello", (data) => this.emit("hello", data));
2045
+ this.powerManager.on(
2046
+ "power:changed",
2047
+ (state) => this.emit("power:changed", state)
2048
+ );
2049
+ this.turnoutManager.on(
2050
+ "turnout:changed",
2051
+ (name, state) => this.emit("turnout:changed", name, state)
2052
+ );
2053
+ this.lightManager.on(
2054
+ "light:changed",
2055
+ (name, state) => this.emit("light:changed", name, state)
2056
+ );
2057
+ this.throttleManager.on(
2058
+ "throttle:acquired",
2059
+ (id) => this.emit("throttle:acquired", id)
2060
+ );
2061
+ this.throttleManager.on(
2062
+ "throttle:updated",
2063
+ (id, data) => this.emit("throttle:updated", id, data)
2064
+ );
2065
+ this.throttleManager.on(
2066
+ "throttle:released",
2067
+ (id) => this.emit("throttle:released", id)
2068
+ );
2069
+ this.throttleManager.on(
2070
+ "throttle:lost",
2071
+ (id) => this.emit("throttle:lost", id)
2072
+ );
2073
+ if (this.options.autoConnect) {
2074
+ this.connect().catch((error) => {
2075
+ this.emit("error", error);
2076
+ });
2077
+ }
2078
+ }
2079
+ /**
2080
+ * Connect to JMRI server
2081
+ */
2082
+ async connect() {
2083
+ return this.wsClient.connect();
2084
+ }
2085
+ /**
2086
+ * Disconnect from JMRI server
2087
+ * Releases all throttles and closes connection
2088
+ */
2089
+ async disconnect() {
2090
+ await this.throttleManager.releaseAllThrottles();
2091
+ return this.wsClient.disconnect();
2092
+ }
2093
+ /**
2094
+ * Get current connection state
2095
+ */
2096
+ getConnectionState() {
2097
+ return this.wsClient.getState();
2098
+ }
2099
+ /**
2100
+ * Check if connected to JMRI
2101
+ */
2102
+ isConnected() {
2103
+ return this.wsClient.isConnected();
2104
+ }
2105
+ // ============================================================================
2106
+ // Power Control
2107
+ // ============================================================================
2108
+ /**
2109
+ * Get current track power state
2110
+ * @param prefix - Optional JMRI connection prefix to target a specific hardware connection
2111
+ */
2112
+ async getPower(prefix) {
2113
+ return this.powerManager.getPower(prefix);
2114
+ }
2115
+ /**
2116
+ * Set track power state
2117
+ * @param state - The desired power state
2118
+ * @param prefix - Optional JMRI connection prefix to target a specific hardware connection
2119
+ */
2120
+ async setPower(state, prefix) {
2121
+ return this.powerManager.setPower(state, prefix);
2122
+ }
2123
+ /**
2124
+ * Turn track power on
2125
+ * @param prefix - Optional JMRI connection prefix to target a specific hardware connection
2126
+ */
2127
+ async powerOn(prefix) {
2128
+ return this.powerManager.powerOn(prefix);
2129
+ }
2130
+ /**
2131
+ * Turn track power off
2132
+ * @param prefix - Optional JMRI connection prefix to target a specific hardware connection
2133
+ */
2134
+ async powerOff(prefix) {
2135
+ return this.powerManager.powerOff(prefix);
2136
+ }
2137
+ // ============================================================================
2138
+ // System Connections
2139
+ // ============================================================================
2140
+ /**
2141
+ * List all available JMRI system connections and their prefixes.
2142
+ * Use the returned prefix values with power and throttle commands to
2143
+ * target a specific hardware connection when multiple are configured.
2144
+ *
2145
+ * @example
2146
+ * ```typescript
2147
+ * const connections = await client.getSystemConnections();
2148
+ * // [{ name: 'LocoNet', prefix: 'L' }, { name: 'DCC++', prefix: 'D' }]
2149
+ * await client.powerOn('L'); // power on via LocoNet only
2150
+ * ```
2151
+ */
2152
+ async getSystemConnections() {
2153
+ return this.systemConnectionsManager.getSystemConnections();
2154
+ }
2155
+ // ============================================================================
2156
+ // Roster Management
2157
+ // ============================================================================
2158
+ /**
2159
+ * Get all roster entries
2160
+ */
2161
+ async getRoster() {
2162
+ return this.rosterManager.getRoster();
2163
+ }
2164
+ /**
2165
+ * Get roster entry by name
2166
+ */
2167
+ async getRosterEntryByName(name) {
2168
+ return this.rosterManager.getRosterEntryByName(name);
2169
+ }
2170
+ /**
2171
+ * Get roster entry by address
2172
+ */
2173
+ async getRosterEntryByAddress(address) {
2174
+ return this.rosterManager.getRosterEntryByAddress(address);
2175
+ }
2176
+ /**
2177
+ * Search roster by partial name match
2178
+ */
2179
+ async searchRoster(query) {
2180
+ return this.rosterManager.searchRoster(query);
2181
+ }
2182
+ // ============================================================================
2183
+ // Turnout Control
2184
+ // ============================================================================
2185
+ /**
2186
+ * Get the current state of a turnout
2187
+ */
2188
+ async getTurnout(name) {
2189
+ return this.turnoutManager.getTurnout(name);
2190
+ }
2191
+ /**
2192
+ * Set a turnout to the given state
2193
+ */
2194
+ async setTurnout(name, state) {
2195
+ return this.turnoutManager.setTurnout(name, state);
2196
+ }
2197
+ /**
2198
+ * Throw a turnout (diverging route)
2199
+ */
2200
+ async throwTurnout(name) {
2201
+ return this.turnoutManager.throwTurnout(name);
2202
+ }
2203
+ /**
2204
+ * Close a turnout (straight through / normal)
2205
+ */
2206
+ async closeTurnout(name) {
2207
+ return this.turnoutManager.closeTurnout(name);
2208
+ }
2209
+ /**
2210
+ * List all turnouts known to JMRI
2211
+ */
2212
+ async listTurnouts() {
2213
+ return this.turnoutManager.listTurnouts();
2214
+ }
2215
+ /**
2216
+ * Get cached turnout state without a network request
2217
+ */
2218
+ getTurnoutState(name) {
2219
+ return this.turnoutManager.getTurnoutState(name);
2220
+ }
2221
+ /**
2222
+ * Get all cached turnout states
2223
+ */
2224
+ getCachedTurnouts() {
2225
+ return this.turnoutManager.getCachedTurnouts();
2226
+ }
2227
+ // ============================================================================
2228
+ // Light Control
2229
+ // ============================================================================
2230
+ /**
2231
+ * Get the current state of a light
2232
+ */
2233
+ async getLight(name) {
2234
+ return this.lightManager.getLight(name);
2235
+ }
2236
+ /**
2237
+ * Set a light to the given state
2238
+ */
2239
+ async setLight(name, state) {
2240
+ return this.lightManager.setLight(name, state);
2241
+ }
2242
+ /**
2243
+ * Turn a light on
2244
+ */
2245
+ async turnOnLight(name) {
2246
+ return this.lightManager.turnOnLight(name);
2247
+ }
2248
+ /**
2249
+ * Turn a light off
2250
+ */
2251
+ async turnOffLight(name) {
2252
+ return this.lightManager.turnOffLight(name);
2253
+ }
2254
+ /**
2255
+ * List all lights known to JMRI
2256
+ */
2257
+ async listLights() {
2258
+ return this.lightManager.listLights();
2259
+ }
2260
+ /**
2261
+ * Get cached light state without a network request
2262
+ */
2263
+ getLightState(name) {
2264
+ return this.lightManager.getLightState(name);
2265
+ }
2266
+ /**
2267
+ * Get all cached light states
2268
+ */
2269
+ getCachedLights() {
2270
+ return this.lightManager.getCachedLights();
2271
+ }
2272
+ // ============================================================================
2273
+ // Throttle Control
2274
+ // ============================================================================
2275
+ /**
2276
+ * Acquire a throttle for a locomotive
2277
+ *
2278
+ * @param options - Throttle acquisition options
2279
+ * @returns Throttle ID for use in other throttle methods
2280
+ *
2281
+ * @example
2282
+ * ```typescript
2283
+ * const throttleId = await client.acquireThrottle({ address: 3 });
2284
+ * await client.setThrottleSpeed(throttleId, 0.5); // Half speed
2285
+ * ```
2286
+ */
2287
+ async acquireThrottle(options) {
2288
+ return this.throttleManager.acquireThrottle(options);
2289
+ }
2290
+ /**
2291
+ * Release a throttle
2292
+ */
2293
+ async releaseThrottle(throttleId) {
2294
+ return this.throttleManager.releaseThrottle(throttleId);
2295
+ }
2296
+ /**
2297
+ * Set throttle speed (0.0 to 1.0)
2298
+ *
2299
+ * @param throttleId - Throttle ID from acquireThrottle
2300
+ * @param speed - Speed value between 0.0 (stopped) and 1.0 (full speed)
2301
+ */
2302
+ async setThrottleSpeed(throttleId, speed) {
2303
+ return this.throttleManager.setSpeed(throttleId, speed);
2304
+ }
2305
+ /**
2306
+ * Set throttle direction
2307
+ *
2308
+ * @param throttleId - Throttle ID from acquireThrottle
2309
+ * @param forward - True for forward, false for reverse
2310
+ */
2311
+ async setThrottleDirection(throttleId, forward) {
2312
+ return this.throttleManager.setDirection(throttleId, forward);
2313
+ }
2314
+ /**
2315
+ * Set throttle function (F0-F28)
2316
+ *
2317
+ * @param throttleId - Throttle ID from acquireThrottle
2318
+ * @param functionKey - Function key (F0-F28)
2319
+ * @param value - True to activate, false to deactivate
2320
+ *
2321
+ * @example
2322
+ * ```typescript
2323
+ * await client.setThrottleFunction(throttleId, 'F0', true); // Headlight on
2324
+ * await client.setThrottleFunction(throttleId, 'F2', true); // Horn
2325
+ * ```
2326
+ */
2327
+ async setThrottleFunction(throttleId, functionKey, value) {
2328
+ return this.throttleManager.setFunction(throttleId, functionKey, value);
2329
+ }
2330
+ /**
2331
+ * Emergency stop for a throttle (speed to 0)
2332
+ */
2333
+ async emergencyStop(throttleId) {
2334
+ return this.throttleManager.emergencyStop(throttleId);
2335
+ }
2336
+ /**
2337
+ * Set throttle to idle (speed to 0, maintain direction)
2338
+ */
2339
+ async idleThrottle(throttleId) {
2340
+ return this.throttleManager.idle(throttleId);
2341
+ }
2342
+ /**
2343
+ * Get throttle state
2344
+ */
2345
+ getThrottleState(throttleId) {
2346
+ return this.throttleManager.getThrottleState(throttleId);
2347
+ }
2348
+ /**
2349
+ * Get all throttle IDs
2350
+ */
2351
+ getThrottleIds() {
2352
+ return this.throttleManager.getThrottleIds();
2353
+ }
2354
+ /**
2355
+ * Get all throttle states
2356
+ */
2357
+ getAllThrottles() {
2358
+ return this.throttleManager.getAllThrottles();
2359
+ }
2360
+ /**
2361
+ * Release all throttles
2362
+ */
2363
+ async releaseAllThrottles() {
2364
+ return this.throttleManager.releaseAllThrottles();
2365
+ }
2366
+ };
2367
+ // Annotate the CommonJS export names for ESM import in node:
2368
+ 0 && (module.exports = {
2369
+ ConnectionState,
2370
+ JmriClient,
2371
+ LightState,
2372
+ MockResponseManager,
2373
+ PowerState,
2374
+ TurnoutState,
2375
+ WebSocketClient,
2376
+ isThrottleFunctionKey,
2377
+ isValidSpeed,
2378
+ lightStateToString,
2379
+ mockData,
2380
+ mockResponseManager,
2381
+ powerStateToString,
2382
+ turnoutStateToString
2383
+ });