jmri-client 4.2.0-beta.2 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/browser/jmri-client.js +88 -28
- package/dist/cjs/index.js +2442 -31
- package/dist/esm/index.js +2393 -17
- package/dist/types/client.d.ts +9 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/managers/roster-manager.d.ts +9 -1
- package/dist/types/mocks/mock-data.d.ts +30 -6
- package/dist/types/mocks/mock-response-manager.d.ts +7 -2
- package/dist/types/types/jmri-messages.d.ts +22 -0
- package/docs/API.md +8 -0
- package/docs/BROWSER.md +4 -4
- package/docs/MIGRATION.md +30 -1
- package/docs/MOCK_MODE.md +15 -9
- package/package.json +17 -18
- package/dist/cjs/client.js +0 -366
- package/dist/cjs/core/connection-state-manager.js +0 -84
- package/dist/cjs/core/heartbeat-manager.js +0 -79
- package/dist/cjs/core/index.js +0 -25
- package/dist/cjs/core/message-queue.js +0 -59
- package/dist/cjs/core/reconnection-manager.js +0 -97
- package/dist/cjs/core/websocket-adapter.js +0 -135
- package/dist/cjs/core/websocket-client.js +0 -388
- package/dist/cjs/managers/index.js +0 -25
- package/dist/cjs/managers/light-manager.js +0 -111
- package/dist/cjs/managers/power-manager.js +0 -90
- package/dist/cjs/managers/roster-manager.js +0 -118
- package/dist/cjs/managers/system-connections-manager.js +0 -28
- package/dist/cjs/managers/throttle-manager.js +0 -233
- package/dist/cjs/managers/turnout-manager.js +0 -111
- package/dist/cjs/mocks/index.js +0 -12
- package/dist/cjs/mocks/mock-data.js +0 -237
- package/dist/cjs/mocks/mock-response-manager.js +0 -290
- package/dist/cjs/types/client-options.js +0 -66
- package/dist/cjs/types/events.js +0 -16
- package/dist/cjs/types/index.js +0 -23
- package/dist/cjs/types/jmri-messages.js +0 -95
- package/dist/cjs/types/throttle.js +0 -19
- package/dist/cjs/utils/exponential-backoff.js +0 -40
- package/dist/cjs/utils/index.js +0 -21
- package/dist/cjs/utils/message-id.js +0 -40
- package/dist/esm/client.js +0 -362
- package/dist/esm/core/connection-state-manager.js +0 -80
- package/dist/esm/core/heartbeat-manager.js +0 -75
- package/dist/esm/core/index.js +0 -9
- package/dist/esm/core/message-queue.js +0 -55
- package/dist/esm/core/reconnection-manager.js +0 -93
- package/dist/esm/core/websocket-adapter.js +0 -98
- package/dist/esm/core/websocket-client.js +0 -384
- package/dist/esm/managers/index.js +0 -9
- package/dist/esm/managers/light-manager.js +0 -107
- package/dist/esm/managers/power-manager.js +0 -86
- package/dist/esm/managers/roster-manager.js +0 -114
- package/dist/esm/managers/system-connections-manager.js +0 -24
- package/dist/esm/managers/throttle-manager.js +0 -229
- package/dist/esm/managers/turnout-manager.js +0 -107
- package/dist/esm/mocks/index.js +0 -6
- package/dist/esm/mocks/mock-data.js +0 -234
- package/dist/esm/mocks/mock-response-manager.js +0 -286
- package/dist/esm/types/client-options.js +0 -62
- package/dist/esm/types/events.js +0 -13
- package/dist/esm/types/index.js +0 -7
- package/dist/esm/types/jmri-messages.js +0 -89
- package/dist/esm/types/throttle.js +0 -15
- package/dist/esm/utils/exponential-backoff.js +0 -36
- package/dist/esm/utils/index.js +0 -5
- package/dist/esm/utils/message-id.js +0 -36
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Automatic reconnection management with exponential backoff
|
|
4
|
-
*/
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.ReconnectionManager = void 0;
|
|
7
|
-
const eventemitter3_1 = require("eventemitter3");
|
|
8
|
-
const exponential_backoff_js_1 = require("../utils/exponential-backoff.js");
|
|
9
|
-
/**
|
|
10
|
-
* Manages automatic reconnection with exponential backoff
|
|
11
|
-
*/
|
|
12
|
-
class ReconnectionManager extends eventemitter3_1.EventEmitter {
|
|
13
|
-
constructor(options) {
|
|
14
|
-
super();
|
|
15
|
-
this.currentAttempt = 0;
|
|
16
|
-
this.isReconnecting = false;
|
|
17
|
-
this.options = options;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Start reconnection process
|
|
21
|
-
* @param reconnect - Callback to attempt reconnection
|
|
22
|
-
*/
|
|
23
|
-
start(reconnect) {
|
|
24
|
-
if (!this.options.enabled || this.isReconnecting) {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
this.isReconnecting = true;
|
|
28
|
-
this.currentAttempt = 0;
|
|
29
|
-
this.scheduleNextAttempt(reconnect);
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Schedule next reconnection attempt
|
|
33
|
-
*/
|
|
34
|
-
scheduleNextAttempt(reconnect) {
|
|
35
|
-
this.currentAttempt++;
|
|
36
|
-
// Check if should continue trying
|
|
37
|
-
if (!(0, exponential_backoff_js_1.shouldReconnect)(this.currentAttempt, this.options.maxAttempts)) {
|
|
38
|
-
this.emit('maxAttemptsReached', this.currentAttempt - 1);
|
|
39
|
-
this.stop();
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
// Calculate delay with backoff
|
|
43
|
-
const delay = (0, exponential_backoff_js_1.calculateBackoffDelay)(this.currentAttempt, this.options);
|
|
44
|
-
this.emit('attemptScheduled', this.currentAttempt, delay);
|
|
45
|
-
// Schedule attempt
|
|
46
|
-
this.reconnectTimeout = setTimeout(async () => {
|
|
47
|
-
this.emit('attempting', this.currentAttempt);
|
|
48
|
-
try {
|
|
49
|
-
await reconnect();
|
|
50
|
-
// Success - stop reconnection process
|
|
51
|
-
this.stop();
|
|
52
|
-
this.emit('success', this.currentAttempt);
|
|
53
|
-
}
|
|
54
|
-
catch (error) {
|
|
55
|
-
// Failure - schedule next attempt
|
|
56
|
-
this.emit('failed', this.currentAttempt, error);
|
|
57
|
-
this.scheduleNextAttempt(reconnect);
|
|
58
|
-
}
|
|
59
|
-
}, delay);
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Stop reconnection process
|
|
63
|
-
*/
|
|
64
|
-
stop() {
|
|
65
|
-
this.isReconnecting = false;
|
|
66
|
-
if (this.reconnectTimeout) {
|
|
67
|
-
clearTimeout(this.reconnectTimeout);
|
|
68
|
-
this.reconnectTimeout = undefined;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Reset attempt counter
|
|
73
|
-
*/
|
|
74
|
-
reset() {
|
|
75
|
-
this.stop();
|
|
76
|
-
this.currentAttempt = 0;
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Check if currently reconnecting
|
|
80
|
-
*/
|
|
81
|
-
reconnecting() {
|
|
82
|
-
return this.isReconnecting;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Get current attempt number
|
|
86
|
-
*/
|
|
87
|
-
getAttempt() {
|
|
88
|
-
return this.currentAttempt;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Update reconnection options
|
|
92
|
-
*/
|
|
93
|
-
updateOptions(options) {
|
|
94
|
-
this.options = { ...this.options, ...options };
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
exports.ReconnectionManager = ReconnectionManager;
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* WebSocket Adapter
|
|
4
|
-
* Provides a unified interface for WebSocket connections in both Node.js and browser environments
|
|
5
|
-
*/
|
|
6
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
-
if (k2 === undefined) k2 = k;
|
|
8
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
-
}
|
|
12
|
-
Object.defineProperty(o, k2, desc);
|
|
13
|
-
}) : (function(o, m, k, k2) {
|
|
14
|
-
if (k2 === undefined) k2 = k;
|
|
15
|
-
o[k2] = m[k];
|
|
16
|
-
}));
|
|
17
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
-
}) : function(o, v) {
|
|
20
|
-
o["default"] = v;
|
|
21
|
-
});
|
|
22
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
-
var ownKeys = function(o) {
|
|
24
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
-
var ar = [];
|
|
26
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
-
return ar;
|
|
28
|
-
};
|
|
29
|
-
return ownKeys(o);
|
|
30
|
-
};
|
|
31
|
-
return function (mod) {
|
|
32
|
-
if (mod && mod.__esModule) return mod;
|
|
33
|
-
var result = {};
|
|
34
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
-
__setModuleDefault(result, mod);
|
|
36
|
-
return result;
|
|
37
|
-
};
|
|
38
|
-
})();
|
|
39
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
-
exports.ReadyState = void 0;
|
|
41
|
-
exports.createWebSocketAdapter = createWebSocketAdapter;
|
|
42
|
-
const eventemitter3_1 = require("eventemitter3");
|
|
43
|
-
/**
|
|
44
|
-
* WebSocket ready states
|
|
45
|
-
*/
|
|
46
|
-
var ReadyState;
|
|
47
|
-
(function (ReadyState) {
|
|
48
|
-
ReadyState[ReadyState["CONNECTING"] = 0] = "CONNECTING";
|
|
49
|
-
ReadyState[ReadyState["OPEN"] = 1] = "OPEN";
|
|
50
|
-
ReadyState[ReadyState["CLOSING"] = 2] = "CLOSING";
|
|
51
|
-
ReadyState[ReadyState["CLOSED"] = 3] = "CLOSED";
|
|
52
|
-
})(ReadyState || (exports.ReadyState = ReadyState = {}));
|
|
53
|
-
/**
|
|
54
|
-
* Create a WebSocket adapter for the current environment
|
|
55
|
-
*/
|
|
56
|
-
async function createWebSocketAdapter(url, protocols) {
|
|
57
|
-
// Detect environment
|
|
58
|
-
const isBrowser = typeof window !== 'undefined' && typeof window.WebSocket !== 'undefined';
|
|
59
|
-
if (isBrowser) {
|
|
60
|
-
return new BrowserWebSocketAdapter(url, protocols);
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
return await NodeWebSocketAdapter.create(url, protocols);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Browser WebSocket adapter
|
|
68
|
-
* Wraps native browser WebSocket with EventEmitter interface
|
|
69
|
-
*/
|
|
70
|
-
class BrowserWebSocketAdapter extends eventemitter3_1.EventEmitter {
|
|
71
|
-
constructor(url, protocols) {
|
|
72
|
-
super();
|
|
73
|
-
this.ws = new WebSocket(url, protocols);
|
|
74
|
-
this.ws.onopen = () => {
|
|
75
|
-
this.emit('open');
|
|
76
|
-
};
|
|
77
|
-
this.ws.onmessage = (event) => {
|
|
78
|
-
this.emit('message', event.data);
|
|
79
|
-
};
|
|
80
|
-
this.ws.onerror = () => {
|
|
81
|
-
this.emit('error', new Error('WebSocket error'));
|
|
82
|
-
};
|
|
83
|
-
this.ws.onclose = (event) => {
|
|
84
|
-
this.emit('close', event.code, event.reason);
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
send(data) {
|
|
88
|
-
this.ws.send(data);
|
|
89
|
-
}
|
|
90
|
-
close(code, reason) {
|
|
91
|
-
this.ws.close(code, reason);
|
|
92
|
-
}
|
|
93
|
-
get readyState() {
|
|
94
|
-
return this.ws.readyState;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Node.js WebSocket adapter
|
|
99
|
-
* Wraps ws package with consistent interface
|
|
100
|
-
*/
|
|
101
|
-
class NodeWebSocketAdapter extends eventemitter3_1.EventEmitter {
|
|
102
|
-
constructor(ws) {
|
|
103
|
-
super();
|
|
104
|
-
this.ws = ws;
|
|
105
|
-
this.ws.on('open', () => {
|
|
106
|
-
this.emit('open');
|
|
107
|
-
});
|
|
108
|
-
this.ws.on('message', (data) => {
|
|
109
|
-
// Convert Buffer to string if needed
|
|
110
|
-
const message = typeof data === 'string' ? data : data.toString();
|
|
111
|
-
this.emit('message', message);
|
|
112
|
-
});
|
|
113
|
-
this.ws.on('error', (error) => {
|
|
114
|
-
this.emit('error', error);
|
|
115
|
-
});
|
|
116
|
-
this.ws.on('close', (code, reason) => {
|
|
117
|
-
this.emit('close', code, reason);
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
static async create(url, protocols) {
|
|
121
|
-
// Use dynamic import for ESM compatibility
|
|
122
|
-
const { default: WebSocket } = await Promise.resolve().then(() => __importStar(require('ws')));
|
|
123
|
-
const ws = new WebSocket(url, protocols);
|
|
124
|
-
return new NodeWebSocketAdapter(ws);
|
|
125
|
-
}
|
|
126
|
-
send(data) {
|
|
127
|
-
this.ws.send(data);
|
|
128
|
-
}
|
|
129
|
-
close(code, reason) {
|
|
130
|
-
this.ws.close(code, reason);
|
|
131
|
-
}
|
|
132
|
-
get readyState() {
|
|
133
|
-
return this.ws.readyState;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
@@ -1,388 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Core WebSocket client for JMRI communication
|
|
4
|
-
*/
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.WebSocketClient = void 0;
|
|
7
|
-
const eventemitter3_1 = require("eventemitter3");
|
|
8
|
-
const websocket_adapter_js_1 = require("./websocket-adapter.js");
|
|
9
|
-
const events_js_1 = require("../types/events.js");
|
|
10
|
-
const message_id_js_1 = require("../utils/message-id.js");
|
|
11
|
-
const message_queue_js_1 = require("./message-queue.js");
|
|
12
|
-
const connection_state_manager_js_1 = require("./connection-state-manager.js");
|
|
13
|
-
const heartbeat_manager_js_1 = require("./heartbeat-manager.js");
|
|
14
|
-
const reconnection_manager_js_1 = require("./reconnection-manager.js");
|
|
15
|
-
const index_js_1 = require("../mocks/index.js");
|
|
16
|
-
/**
|
|
17
|
-
* Core WebSocket client
|
|
18
|
-
*/
|
|
19
|
-
class WebSocketClient extends eventemitter3_1.EventEmitter {
|
|
20
|
-
constructor(options) {
|
|
21
|
-
super();
|
|
22
|
-
// Request/response tracking
|
|
23
|
-
this.pendingRequests = new Map();
|
|
24
|
-
// Connection state
|
|
25
|
-
this.isManualDisconnect = false;
|
|
26
|
-
this.options = options;
|
|
27
|
-
this.url = `${options.protocol}://${options.host}:${options.port}/json/`;
|
|
28
|
-
// Initialize sub-managers
|
|
29
|
-
this.messageIdGen = new message_id_js_1.MessageIdGenerator();
|
|
30
|
-
this.messageQueue = new message_queue_js_1.MessageQueue(options.messageQueueSize);
|
|
31
|
-
this.stateManager = new connection_state_manager_js_1.ConnectionStateManager();
|
|
32
|
-
this.heartbeatManager = new heartbeat_manager_js_1.HeartbeatManager(options.heartbeat);
|
|
33
|
-
this.reconnectionManager = new reconnection_manager_js_1.ReconnectionManager(options.reconnection);
|
|
34
|
-
// Initialize mock manager if mock mode is enabled
|
|
35
|
-
if (options.mock.enabled) {
|
|
36
|
-
this.mockManager = new index_js_1.MockResponseManager({
|
|
37
|
-
responseDelay: options.mock.responseDelay
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
// Wire up state manager events
|
|
41
|
-
this.stateManager.on('stateChanged', (newState, prevState) => {
|
|
42
|
-
this.emit('connectionStateChanged', newState, prevState);
|
|
43
|
-
});
|
|
44
|
-
// Wire up heartbeat events
|
|
45
|
-
this.heartbeatManager.on('timeout', () => {
|
|
46
|
-
this.emit('heartbeat:timeout');
|
|
47
|
-
this.handleHeartbeatTimeout();
|
|
48
|
-
});
|
|
49
|
-
this.heartbeatManager.on('pingSent', () => {
|
|
50
|
-
this.emit('heartbeat:sent');
|
|
51
|
-
});
|
|
52
|
-
// Wire up reconnection events
|
|
53
|
-
this.reconnectionManager.on('attemptScheduled', (attempt, delay) => {
|
|
54
|
-
this.emit('reconnecting', attempt, delay);
|
|
55
|
-
});
|
|
56
|
-
this.reconnectionManager.on('success', () => {
|
|
57
|
-
this.emit('reconnected');
|
|
58
|
-
});
|
|
59
|
-
this.reconnectionManager.on('maxAttemptsReached', (attempts) => {
|
|
60
|
-
this.emit('reconnectionFailed', attempts);
|
|
61
|
-
});
|
|
62
|
-
this.reconnectionManager.on('debug', (message) => {
|
|
63
|
-
this.emit('debug', message);
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Connect to JMRI WebSocket server (or mock)
|
|
68
|
-
*/
|
|
69
|
-
async connect() {
|
|
70
|
-
if (this.stateManager.isConnected() || this.stateManager.isConnecting()) {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
this.isManualDisconnect = false;
|
|
74
|
-
this.stateManager.transition(events_js_1.ConnectionState.CONNECTING);
|
|
75
|
-
// Mock mode - simulate connection
|
|
76
|
-
if (this.mockManager) {
|
|
77
|
-
return this.connectMock();
|
|
78
|
-
}
|
|
79
|
-
// Real WebSocket connection
|
|
80
|
-
return new Promise(async (resolve, reject) => {
|
|
81
|
-
try {
|
|
82
|
-
this.ws = await (0, websocket_adapter_js_1.createWebSocketAdapter)(this.url);
|
|
83
|
-
this.ws.on('open', () => {
|
|
84
|
-
this.handleOpen();
|
|
85
|
-
resolve();
|
|
86
|
-
});
|
|
87
|
-
this.ws.on('message', (data) => {
|
|
88
|
-
this.handleMessage(data);
|
|
89
|
-
});
|
|
90
|
-
this.ws.on('close', (code, reason) => {
|
|
91
|
-
// If close happens during connection attempt, treat as connection failure
|
|
92
|
-
if (this.stateManager.isConnecting()) {
|
|
93
|
-
// Transition to disconnected state so reconnection can proceed
|
|
94
|
-
this.stateManager.transition(events_js_1.ConnectionState.DISCONNECTED);
|
|
95
|
-
const error = new Error(`WebSocket connection failed (code: ${code}${reason ? ', reason: ' + reason : ''})`);
|
|
96
|
-
this.emit('error', error);
|
|
97
|
-
reject(error);
|
|
98
|
-
// Still need to handle close to trigger reconnection
|
|
99
|
-
this.handleClose(code, reason);
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
this.handleClose(code, reason);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
this.ws.on('error', (error) => {
|
|
106
|
-
this.emit('error', error);
|
|
107
|
-
if (this.stateManager.isConnecting()) {
|
|
108
|
-
this.stateManager.transition(events_js_1.ConnectionState.DISCONNECTED);
|
|
109
|
-
reject(error);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
catch (error) {
|
|
114
|
-
this.stateManager.transition(events_js_1.ConnectionState.DISCONNECTED);
|
|
115
|
-
reject(error);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Simulate connection in mock mode
|
|
121
|
-
*/
|
|
122
|
-
async connectMock() {
|
|
123
|
-
// Simulate connection delay
|
|
124
|
-
await this.delay(10);
|
|
125
|
-
// Transition to connected state
|
|
126
|
-
this.handleOpen();
|
|
127
|
-
// Send hello message in mock mode
|
|
128
|
-
const helloResponse = await this.mockManager.getMockResponse({ type: 'hello' });
|
|
129
|
-
if (helloResponse) {
|
|
130
|
-
this.processMessage(helloResponse);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Disconnect from JMRI WebSocket server
|
|
135
|
-
*/
|
|
136
|
-
async disconnect() {
|
|
137
|
-
// Already disconnected, nothing to do
|
|
138
|
-
if (this.stateManager.isDisconnected()) {
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
this.isManualDisconnect = true;
|
|
142
|
-
this.reconnectionManager.stop();
|
|
143
|
-
this.heartbeatManager.stop();
|
|
144
|
-
// Send goodbye message
|
|
145
|
-
if (this.stateManager.isConnected()) {
|
|
146
|
-
try {
|
|
147
|
-
await this.sendGoodbye();
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
// Ignore errors during goodbye
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
// Close WebSocket
|
|
154
|
-
if (this.ws) {
|
|
155
|
-
this.ws.close();
|
|
156
|
-
this.ws = undefined;
|
|
157
|
-
}
|
|
158
|
-
// Reject all pending requests
|
|
159
|
-
this.rejectAllPendingRequests(new Error('Client disconnected'));
|
|
160
|
-
if (!this.stateManager.isDisconnected()) {
|
|
161
|
-
this.stateManager.transition(events_js_1.ConnectionState.DISCONNECTED);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Send message to JMRI (or mock)
|
|
166
|
-
*/
|
|
167
|
-
send(message) {
|
|
168
|
-
if (!this.stateManager.isConnected()) {
|
|
169
|
-
// Queue message for later
|
|
170
|
-
this.messageQueue.enqueue(message);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
// Mock mode - send doesn't need to do anything
|
|
174
|
-
// Responses are generated in request()
|
|
175
|
-
if (this.mockManager) {
|
|
176
|
-
this.emit('message:sent', message);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
if (!this.ws) {
|
|
180
|
-
throw new Error('WebSocket not initialized');
|
|
181
|
-
}
|
|
182
|
-
const json = JSON.stringify(message);
|
|
183
|
-
this.ws.send(json);
|
|
184
|
-
this.emit('message:sent', message);
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Send request and wait for response (or get mock response)
|
|
188
|
-
*/
|
|
189
|
-
async request(message, timeout) {
|
|
190
|
-
// Mock mode - get response from mock manager
|
|
191
|
-
if (this.mockManager) {
|
|
192
|
-
const response = await this.mockManager.getMockResponse(message);
|
|
193
|
-
this.emit('message:sent', message);
|
|
194
|
-
if (response) {
|
|
195
|
-
this.processMessage(response);
|
|
196
|
-
}
|
|
197
|
-
return response;
|
|
198
|
-
}
|
|
199
|
-
// Real mode - send request and wait for response
|
|
200
|
-
// Assign message ID
|
|
201
|
-
const id = this.messageIdGen.next();
|
|
202
|
-
message.id = id;
|
|
203
|
-
return new Promise((resolve, reject) => {
|
|
204
|
-
// Set up timeout
|
|
205
|
-
const timeoutMs = timeout || this.options.requestTimeout;
|
|
206
|
-
const timeoutHandle = setTimeout(() => {
|
|
207
|
-
this.pendingRequests.delete(id);
|
|
208
|
-
reject(new Error(`Request timeout after ${timeoutMs}ms`));
|
|
209
|
-
}, timeoutMs);
|
|
210
|
-
// Track pending request with metadata for matching responses without IDs
|
|
211
|
-
const pendingRequest = {
|
|
212
|
-
resolve,
|
|
213
|
-
reject,
|
|
214
|
-
timeout: timeoutHandle,
|
|
215
|
-
messageType: message.type
|
|
216
|
-
};
|
|
217
|
-
// For throttle requests, store the throttle name for matching
|
|
218
|
-
if (message.type === 'throttle' && message.data && 'name' in message.data) {
|
|
219
|
-
pendingRequest.matchKey = message.data.name;
|
|
220
|
-
}
|
|
221
|
-
this.pendingRequests.set(id, pendingRequest);
|
|
222
|
-
// Send message
|
|
223
|
-
try {
|
|
224
|
-
this.send(message);
|
|
225
|
-
}
|
|
226
|
-
catch (error) {
|
|
227
|
-
this.pendingRequests.delete(id);
|
|
228
|
-
clearTimeout(timeoutHandle);
|
|
229
|
-
reject(error);
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Get current connection state
|
|
235
|
-
*/
|
|
236
|
-
getState() {
|
|
237
|
-
return this.stateManager.getState();
|
|
238
|
-
}
|
|
239
|
-
/**
|
|
240
|
-
* Check if connected
|
|
241
|
-
*/
|
|
242
|
-
isConnected() {
|
|
243
|
-
return this.stateManager.isConnected();
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Handle WebSocket open event
|
|
247
|
-
*/
|
|
248
|
-
handleOpen() {
|
|
249
|
-
this.stateManager.transition(events_js_1.ConnectionState.CONNECTED);
|
|
250
|
-
this.emit('connected');
|
|
251
|
-
// Start heartbeat
|
|
252
|
-
if (this.options.heartbeat.enabled) {
|
|
253
|
-
this.heartbeatManager.start(() => this.sendPing());
|
|
254
|
-
}
|
|
255
|
-
// Flush queued messages
|
|
256
|
-
const queuedMessages = this.messageQueue.flush();
|
|
257
|
-
for (const message of queuedMessages) {
|
|
258
|
-
this.send(message);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* Handle WebSocket message event
|
|
263
|
-
*/
|
|
264
|
-
handleMessage(data) {
|
|
265
|
-
try {
|
|
266
|
-
const message = JSON.parse(data);
|
|
267
|
-
this.processMessage(message);
|
|
268
|
-
}
|
|
269
|
-
catch (error) {
|
|
270
|
-
this.emit('error', new Error(`Failed to parse message: ${error}`));
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Process a parsed message (called by both real and mock mode)
|
|
275
|
-
*/
|
|
276
|
-
processMessage(message) {
|
|
277
|
-
this.emit('message:received', message);
|
|
278
|
-
// Handle pong
|
|
279
|
-
if (message.type === 'pong') {
|
|
280
|
-
this.heartbeatManager.receivedPong();
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
// Handle hello
|
|
284
|
-
if (message.type === 'hello') {
|
|
285
|
-
this.emit('hello', message.data);
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
// Handle response to request
|
|
289
|
-
if (message.id !== undefined) {
|
|
290
|
-
const pending = this.pendingRequests.get(message.id);
|
|
291
|
-
if (pending) {
|
|
292
|
-
clearTimeout(pending.timeout);
|
|
293
|
-
this.pendingRequests.delete(message.id);
|
|
294
|
-
pending.resolve(message);
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
// Handle responses without ID (like throttle responses from JMRI)
|
|
299
|
-
// Match by message type and data
|
|
300
|
-
if (message.id === undefined) {
|
|
301
|
-
for (const [id, pending] of this.pendingRequests.entries()) {
|
|
302
|
-
// Match by type
|
|
303
|
-
if (pending.messageType === message.type) {
|
|
304
|
-
// For throttle messages, also match by throttle name
|
|
305
|
-
if (message.type === 'throttle' && pending.matchKey) {
|
|
306
|
-
const throttleName = message.data?.throttle || message.data?.name;
|
|
307
|
-
if (throttleName === pending.matchKey) {
|
|
308
|
-
clearTimeout(pending.timeout);
|
|
309
|
-
this.pendingRequests.delete(id);
|
|
310
|
-
pending.resolve(message);
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
// For other message types, just match by type
|
|
316
|
-
clearTimeout(pending.timeout);
|
|
317
|
-
this.pendingRequests.delete(id);
|
|
318
|
-
pending.resolve(message);
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
// Handle unsolicited updates (auto-subscriptions)
|
|
325
|
-
this.emit('update', message);
|
|
326
|
-
}
|
|
327
|
-
/**
|
|
328
|
-
* Handle WebSocket close event
|
|
329
|
-
*/
|
|
330
|
-
handleClose(code, reason) {
|
|
331
|
-
this.heartbeatManager.stop();
|
|
332
|
-
const wasConnected = this.stateManager.isConnected();
|
|
333
|
-
const isReconnecting = this.reconnectionManager.reconnecting();
|
|
334
|
-
if (this.stateManager.isConnected() || this.stateManager.isConnecting()) {
|
|
335
|
-
this.stateManager.transition(events_js_1.ConnectionState.DISCONNECTED);
|
|
336
|
-
}
|
|
337
|
-
this.emit('disconnected', reason || `Connection closed (code: ${code})`);
|
|
338
|
-
// Reject all pending requests
|
|
339
|
-
this.rejectAllPendingRequests(new Error('Connection closed'));
|
|
340
|
-
// Attempt reconnection if not manual disconnect
|
|
341
|
-
// Continue reconnecting if we were connected OR reconnection manager is already active
|
|
342
|
-
if (!this.isManualDisconnect && (wasConnected || isReconnecting) && this.options.reconnection.enabled) {
|
|
343
|
-
this.stateManager.forceState(events_js_1.ConnectionState.RECONNECTING);
|
|
344
|
-
this.reconnectionManager.start(() => this.connect());
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Handle heartbeat timeout
|
|
349
|
-
*/
|
|
350
|
-
handleHeartbeatTimeout() {
|
|
351
|
-
// Connection appears dead, force reconnect
|
|
352
|
-
if (this.ws) {
|
|
353
|
-
this.ws.close();
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
/**
|
|
357
|
-
* Send ping message
|
|
358
|
-
*/
|
|
359
|
-
sendPing() {
|
|
360
|
-
this.send({ type: 'ping' });
|
|
361
|
-
}
|
|
362
|
-
/**
|
|
363
|
-
* Send goodbye message
|
|
364
|
-
*/
|
|
365
|
-
async sendGoodbye() {
|
|
366
|
-
const message = { type: 'goodbye' };
|
|
367
|
-
this.send(message);
|
|
368
|
-
// Give it a moment to send
|
|
369
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
370
|
-
}
|
|
371
|
-
/**
|
|
372
|
-
* Reject all pending requests
|
|
373
|
-
*/
|
|
374
|
-
rejectAllPendingRequests(error) {
|
|
375
|
-
for (const pending of this.pendingRequests.values()) {
|
|
376
|
-
clearTimeout(pending.timeout);
|
|
377
|
-
pending.reject(error);
|
|
378
|
-
}
|
|
379
|
-
this.pendingRequests.clear();
|
|
380
|
-
}
|
|
381
|
-
/**
|
|
382
|
-
* Delay helper
|
|
383
|
-
*/
|
|
384
|
-
delay(ms) {
|
|
385
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
exports.WebSocketClient = WebSocketClient;
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Manager exports
|
|
4
|
-
*/
|
|
5
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
-
if (k2 === undefined) k2 = k;
|
|
7
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
-
}
|
|
11
|
-
Object.defineProperty(o, k2, desc);
|
|
12
|
-
}) : (function(o, m, k, k2) {
|
|
13
|
-
if (k2 === undefined) k2 = k;
|
|
14
|
-
o[k2] = m[k];
|
|
15
|
-
}));
|
|
16
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
17
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
18
|
-
};
|
|
19
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
__exportStar(require("./power-manager.js"), exports);
|
|
21
|
-
__exportStar(require("./roster-manager.js"), exports);
|
|
22
|
-
__exportStar(require("./throttle-manager.js"), exports);
|
|
23
|
-
__exportStar(require("./turnout-manager.js"), exports);
|
|
24
|
-
__exportStar(require("./light-manager.js"), exports);
|
|
25
|
-
__exportStar(require("./system-connections-manager.js"), exports);
|