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