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