vantiv.io 1.0.3 → 1.0.5
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vantiv.io",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Enterprise WebSocket infrastructure for Highrise featuring spatial intelligence systems, memory-optimized architecture, and production-grade reliability for scalable application development",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"highrise",
|
|
@@ -1,53 +1,133 @@
|
|
|
1
|
-
const WebSocketConstants = require('vantiv.io/src/constants/WebSocketConstants');
|
|
2
|
-
|
|
3
1
|
class WebSocketHandlers {
|
|
2
|
+
static RESET = '\x1b[0m';
|
|
3
|
+
static ERROR = '\x1b[31m';
|
|
4
|
+
static WARN = '\x1b[33m';
|
|
5
|
+
static INFO = '\x1b[36m';
|
|
6
|
+
static MODULE = '\x1b[95m';
|
|
7
|
+
|
|
8
|
+
static prefix(levelColor, level) {
|
|
9
|
+
return `${WebSocketHandlers.MODULE}[WebSocketHandlers]${WebSocketHandlers.RESET} ${levelColor}${level}${WebSocketHandlers.RESET}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
4
12
|
constructor(server) {
|
|
5
13
|
this.server = server;
|
|
6
|
-
this.
|
|
14
|
+
this.isShuttingDown = false;
|
|
7
15
|
}
|
|
8
|
-
|
|
16
|
+
|
|
9
17
|
setupErrorHandlers() {
|
|
10
18
|
const handlers = {
|
|
11
19
|
uncaughtException: this._handleUncaughtException.bind(this),
|
|
12
20
|
unhandledRejection: this._handleUnhandledRejection.bind(this),
|
|
13
21
|
SIGINT: this._handleSIGINT.bind(this),
|
|
14
|
-
SIGTERM: this._handleSIGTERM.bind(this)
|
|
22
|
+
SIGTERM: this._handleSIGTERM.bind(this),
|
|
23
|
+
SIGUSR2: this._handleSIGUSR2.bind(this)
|
|
15
24
|
};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
|
|
26
|
+
this.activeHandlers = handlers;
|
|
27
|
+
|
|
28
|
+
Object.entries(handlers).forEach(([event, handler]) => {
|
|
29
|
+
process.on(event, handler);
|
|
30
|
+
});
|
|
31
|
+
|
|
22
32
|
return handlers;
|
|
23
33
|
}
|
|
24
|
-
|
|
34
|
+
|
|
25
35
|
_handleUncaughtException(error) {
|
|
26
|
-
|
|
36
|
+
console.error(
|
|
37
|
+
`${this.constructor.prefix(this.constructor.ERROR, 'ERROR')}: Uncaught exception — shutting down`,
|
|
38
|
+
'\n',
|
|
39
|
+
{
|
|
40
|
+
message: error.message,
|
|
41
|
+
name: error.name,
|
|
42
|
+
stack: error.stack
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
this._gracefulShutdown('uncaughtException').catch(err => {
|
|
47
|
+
console.error(
|
|
48
|
+
`${this.constructor.prefix(this.constructor.ERROR, 'ERROR')}: Shutdown failed after uncaught exception:`,
|
|
49
|
+
err.message
|
|
50
|
+
);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
});
|
|
27
53
|
}
|
|
28
|
-
|
|
29
|
-
_handleUnhandledRejection(reason,
|
|
30
|
-
|
|
54
|
+
|
|
55
|
+
_handleUnhandledRejection(reason, _promise) {
|
|
56
|
+
const reasonDetail = reason instanceof Error
|
|
57
|
+
? { message: reason.message, name: reason.name, stack: reason.stack }
|
|
58
|
+
: reason;
|
|
59
|
+
|
|
60
|
+
console.error(
|
|
61
|
+
`${this.constructor.prefix(this.constructor.ERROR, 'ERROR')}: Unhandled promise rejection`,
|
|
62
|
+
'\n',
|
|
63
|
+
{ reason: reasonDetail }
|
|
64
|
+
);
|
|
31
65
|
}
|
|
32
|
-
|
|
33
|
-
_handleSIGINT() {
|
|
34
|
-
this.
|
|
35
|
-
this.server.disconnect();
|
|
36
|
-
setTimeout(() => process.exit(0), 1000);
|
|
66
|
+
|
|
67
|
+
async _handleSIGINT() {
|
|
68
|
+
await this._gracefulShutdown('SIGINT');
|
|
37
69
|
}
|
|
38
|
-
|
|
39
|
-
_handleSIGTERM() {
|
|
40
|
-
this.
|
|
41
|
-
|
|
42
|
-
|
|
70
|
+
|
|
71
|
+
async _handleSIGTERM() {
|
|
72
|
+
await this._gracefulShutdown('SIGTERM');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async _handleSIGUSR2() {
|
|
76
|
+
await this._gracefulShutdown('SIGUSR2');
|
|
43
77
|
}
|
|
44
|
-
|
|
78
|
+
|
|
79
|
+
async _gracefulShutdown(signal) {
|
|
80
|
+
if (this.isShuttingDown) {
|
|
81
|
+
console.warn(
|
|
82
|
+
`${this.constructor.prefix(this.constructor.WARN, 'WARN')}: Shutdown already in progress for ${signal}`
|
|
83
|
+
);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.isShuttingDown = true;
|
|
88
|
+
console.info(
|
|
89
|
+
`\n${this.constructor.prefix(this.constructor.INFO, 'INFO')}: Starting graceful shutdown for ${signal}`
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
this.removeErrorHandlers(this.activeHandlers);
|
|
94
|
+
|
|
95
|
+
if (typeof this.server.disconnect === 'function') {
|
|
96
|
+
await Promise.race([
|
|
97
|
+
this.server.disconnect(),
|
|
98
|
+
new Promise((_, reject) =>
|
|
99
|
+
setTimeout(() => reject(new Error('Disconnect timeout')), 10000)
|
|
100
|
+
)
|
|
101
|
+
]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.info(
|
|
105
|
+
`${this.constructor.prefix(this.constructor.INFO, 'INFO')}: Graceful shutdown completed for ${signal}`
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(
|
|
110
|
+
`${this.constructor.prefix(this.constructor.ERROR, 'ERROR')}: Failed to shut down gracefully (${signal}):`,
|
|
111
|
+
error.message
|
|
112
|
+
);
|
|
113
|
+
} finally {
|
|
114
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
45
119
|
removeErrorHandlers(handlers) {
|
|
46
120
|
if (!handlers) return;
|
|
47
|
-
|
|
121
|
+
|
|
48
122
|
Object.entries(handlers).forEach(([event, handler]) => {
|
|
49
123
|
process.removeListener(event, handler);
|
|
50
124
|
});
|
|
125
|
+
|
|
126
|
+
this.activeHandlers = null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
isShutdownInProgress() {
|
|
130
|
+
return this.isShuttingDown;
|
|
51
131
|
}
|
|
52
132
|
}
|
|
53
133
|
|
|
@@ -44,11 +44,6 @@ class ChannelManager {
|
|
|
44
44
|
sent: true
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
this.logger.debug(method, 'Channel message sent', {
|
|
48
|
-
tags: normalizedTags,
|
|
49
|
-
messageLength: message.length
|
|
50
|
-
});
|
|
51
|
-
|
|
52
47
|
return true;
|
|
53
48
|
}
|
|
54
49
|
|
|
@@ -95,11 +90,6 @@ class ChannelManager {
|
|
|
95
90
|
this._tagIndex.get(tag).add(listenerId);
|
|
96
91
|
});
|
|
97
92
|
|
|
98
|
-
this.logger.debug(method, 'Listener registered', {
|
|
99
|
-
listenerId,
|
|
100
|
-
tags: normalizedTags
|
|
101
|
-
});
|
|
102
|
-
|
|
103
93
|
return listenerId;
|
|
104
94
|
|
|
105
95
|
} catch (error) {
|
|
@@ -138,7 +128,6 @@ class ChannelManager {
|
|
|
138
128
|
|
|
139
129
|
this._listeners.delete(listenerId);
|
|
140
130
|
|
|
141
|
-
this.logger.debug(method, 'Listener removed', { listenerId });
|
|
142
131
|
return true;
|
|
143
132
|
|
|
144
133
|
} catch (error) {
|
|
@@ -296,7 +285,6 @@ class ChannelManager {
|
|
|
296
285
|
this._messageHistory = [];
|
|
297
286
|
this._listeners.clear();
|
|
298
287
|
this._tagIndex.clear();
|
|
299
|
-
this.logger.info('ChannelManager', 'Cleared channel history and listeners');
|
|
300
288
|
}
|
|
301
289
|
}
|
|
302
290
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const WebSocketConstants = require('vantiv.io/src/constants/WebSocketConstants');
|
|
2
1
|
const EventEmitter = require('events');
|
|
3
2
|
const WebSocket = require('ws');
|
|
3
|
+
const WebSocketConstants = require('vantiv.io/src/constants/WebSocketConstants')
|
|
4
4
|
|
|
5
5
|
class ConnectionManager extends EventEmitter {
|
|
6
6
|
constructor(server, logger, options = {}) {
|
|
@@ -10,8 +10,9 @@ class ConnectionManager extends EventEmitter {
|
|
|
10
10
|
this.logger = logger;
|
|
11
11
|
this.ws = null;
|
|
12
12
|
|
|
13
|
-
this.autoReconnect = options.autoReconnect ??
|
|
14
|
-
this.reconnectDelay =
|
|
13
|
+
this.autoReconnect = options.autoReconnect ?? WebSocketConstants.DEFAULT_AUTO_RECONNECT;
|
|
14
|
+
this.reconnectDelay = WebSocketConstants.DEFAULT_RECONNECT_DELAY;
|
|
15
|
+
this.logger.info('ConnectionManager', `Reconnect delay set to ${this.reconnectDelay}ms`);
|
|
15
16
|
|
|
16
17
|
this.reconnectAttempts = 0;
|
|
17
18
|
this.reconnectTimeout = null;
|
|
@@ -23,12 +24,21 @@ class ConnectionManager extends EventEmitter {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
connect(token, roomId, events) {
|
|
27
|
+
if (!token || !roomId || !Array.isArray(events) || events.length === 0) {
|
|
28
|
+
throw new Error('Invalid connection parameters: token, roomId, and events array are required');
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
this.connectionAuth = { token, roomId, events };
|
|
27
32
|
this.isManualDisconnect = false;
|
|
28
33
|
this._establishConnection();
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
_establishConnection() {
|
|
37
|
+
if (this.reconnecting || this.connected) {
|
|
38
|
+
this.logger.debug('ConnectionManager', 'Connection attempt already in progress — skipping');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
if (!this.connectionAuth) {
|
|
33
43
|
throw new Error('No connection authentication available');
|
|
34
44
|
}
|
|
@@ -48,7 +58,8 @@ class ConnectionManager extends EventEmitter {
|
|
|
48
58
|
headers: {
|
|
49
59
|
[WebSocketConstants.HEADERS.API_TOKEN]: token,
|
|
50
60
|
[WebSocketConstants.HEADERS.ROOM_ID]: roomId
|
|
51
|
-
}
|
|
61
|
+
},
|
|
62
|
+
handshakeTimeout: WebSocketConstants.DEFAULT_HANDSHAKE_TIMEOUT
|
|
52
63
|
});
|
|
53
64
|
|
|
54
65
|
this._setupWebSocketHandlers();
|
|
@@ -60,7 +71,7 @@ class ConnectionManager extends EventEmitter {
|
|
|
60
71
|
|
|
61
72
|
if (this.ws.readyState !== WebSocket.CLOSED &&
|
|
62
73
|
this.ws.readyState !== WebSocket.CLOSING) {
|
|
63
|
-
this.ws.close();
|
|
74
|
+
this.ws.close(1000, 'Cleanup');
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
this.ws = null;
|
|
@@ -69,7 +80,15 @@ class ConnectionManager extends EventEmitter {
|
|
|
69
80
|
|
|
70
81
|
_setupWebSocketHandlers() {
|
|
71
82
|
this.ws.on('open', () => this._handleOpen());
|
|
72
|
-
this.ws.on('message', (data) =>
|
|
83
|
+
this.ws.on('message', (data) => {
|
|
84
|
+
try {
|
|
85
|
+
this.emit('message', data);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
this.logger.error('ConnectionManager', 'Error emitting message event', {
|
|
88
|
+
error: error.message
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
73
92
|
this.ws.on('close', (code, reason) => this._handleClose(code, reason));
|
|
74
93
|
this.ws.on('error', (error) => this._handleError(error));
|
|
75
94
|
}
|
|
@@ -87,27 +106,24 @@ class ConnectionManager extends EventEmitter {
|
|
|
87
106
|
this._clearReconnectTimeout();
|
|
88
107
|
|
|
89
108
|
this.emit('connected');
|
|
90
|
-
this.logger.
|
|
109
|
+
this.logger.info('ConnectionManager', 'WebSocket connection established');
|
|
91
110
|
}
|
|
92
111
|
|
|
93
112
|
_handleClose(code, reason) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
113
|
+
this.connected = false;
|
|
114
|
+
const wasReconnecting = this.reconnecting;
|
|
115
|
+
this.reconnecting = false;
|
|
97
116
|
|
|
98
|
-
const reasonStr = reason
|
|
117
|
+
const reasonStr = reason?.toString('utf8') || `Binary reason (length: ${reason?.length || 0})`;
|
|
99
118
|
this.emit('disconnected', { code, reason: reasonStr });
|
|
100
|
-
|
|
119
|
+
|
|
120
|
+
this.logger.warn('ConnectionManager', 'Connection closed', {
|
|
101
121
|
code,
|
|
102
122
|
reason: reasonStr,
|
|
103
|
-
reconnectAttempts: this.reconnectAttempts,
|
|
104
123
|
autoReconnect: this.autoReconnect,
|
|
105
|
-
|
|
124
|
+
manual: this.isManualDisconnect,
|
|
125
|
+
attempt: this.reconnectAttempts
|
|
106
126
|
});
|
|
107
|
-
|
|
108
|
-
const wasReconnecting = this.reconnecting;
|
|
109
|
-
this.reconnecting = false;
|
|
110
|
-
|
|
111
127
|
if (this.autoReconnect && !this.isManualDisconnect && !wasReconnecting) {
|
|
112
128
|
this._scheduleReconnect();
|
|
113
129
|
}
|
|
@@ -137,20 +153,17 @@ class ConnectionManager extends EventEmitter {
|
|
|
137
153
|
|
|
138
154
|
this._clearReconnectTimeout();
|
|
139
155
|
|
|
140
|
-
const delay = Math.min(
|
|
141
|
-
this.reconnectDelay * Math.pow(WebSocketConstants.RECONNECT_BACKOFF_FACTOR, this.reconnectAttempts),
|
|
142
|
-
WebSocketConstants.MAX_RECONNECT_DELAY
|
|
143
|
-
);
|
|
144
|
-
|
|
145
156
|
this.reconnectAttempts++;
|
|
146
157
|
this.reconnecting = true;
|
|
147
158
|
|
|
148
|
-
this.server
|
|
159
|
+
if (this.server?.metrics) {
|
|
160
|
+
this.server.metrics.increment('reconnects');
|
|
161
|
+
}
|
|
149
162
|
|
|
150
163
|
this.logger.info('ConnectionManager', `Scheduling reconnect...`, {
|
|
151
164
|
attempt: this.reconnectAttempts,
|
|
152
|
-
delay: `${
|
|
153
|
-
maxDelay:
|
|
165
|
+
delay: `${this.reconnectDelay}ms (5s)`,
|
|
166
|
+
maxDelay: '5s'
|
|
154
167
|
});
|
|
155
168
|
|
|
156
169
|
this.reconnectTimeout = setTimeout(() => {
|
|
@@ -160,10 +173,10 @@ class ConnectionManager extends EventEmitter {
|
|
|
160
173
|
} else {
|
|
161
174
|
this.reconnecting = false;
|
|
162
175
|
}
|
|
163
|
-
},
|
|
176
|
+
}, this.reconnectDelay);
|
|
164
177
|
}
|
|
165
178
|
|
|
166
|
-
disconnect(code =
|
|
179
|
+
disconnect(code = 1000, reason = 'Manual disconnect') {
|
|
167
180
|
this.autoReconnect = false;
|
|
168
181
|
this.isManualDisconnect = true;
|
|
169
182
|
this.reconnecting = false;
|
|
@@ -183,12 +196,19 @@ class ConnectionManager extends EventEmitter {
|
|
|
183
196
|
throw new Error('WebSocket is not connected');
|
|
184
197
|
}
|
|
185
198
|
|
|
186
|
-
|
|
187
|
-
|
|
199
|
+
try {
|
|
200
|
+
const payload = typeof data === 'string' ? data : JSON.stringify(data);
|
|
201
|
+
this.ws.send(payload);
|
|
188
202
|
|
|
189
|
-
|
|
203
|
+
if (this.server?.metrics) {
|
|
204
|
+
this.server.metrics.increment('messagesSent');
|
|
205
|
+
}
|
|
190
206
|
|
|
191
|
-
|
|
207
|
+
return true;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
this.logger.error('ConnectionManager', 'Failed to send message', { error: error.message });
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
192
212
|
}
|
|
193
213
|
|
|
194
214
|
isConnected() {
|
|
@@ -204,7 +224,8 @@ class ConnectionManager extends EventEmitter {
|
|
|
204
224
|
attempts: this.reconnectAttempts,
|
|
205
225
|
reconnecting: this.reconnecting,
|
|
206
226
|
autoReconnect: this.autoReconnect,
|
|
207
|
-
isManualDisconnect: this.isManualDisconnect
|
|
227
|
+
isManualDisconnect: this.isManualDisconnect,
|
|
228
|
+
connected: this.connected
|
|
208
229
|
};
|
|
209
230
|
}
|
|
210
231
|
|
|
@@ -213,6 +234,12 @@ class ConnectionManager extends EventEmitter {
|
|
|
213
234
|
this.removeAllListeners();
|
|
214
235
|
this.connectionAuth = null;
|
|
215
236
|
this._cleanupWebSocket();
|
|
237
|
+
this._clearReconnectTimeout();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
resetStats() {
|
|
241
|
+
this.reconnectAttempts = 0;
|
|
242
|
+
this.reconnecting = false;
|
|
216
243
|
}
|
|
217
244
|
}
|
|
218
245
|
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
const WebSocketConstants = {
|
|
2
2
|
// Connection
|
|
3
3
|
KEEPALIVE_INTERVAL: 15000,
|
|
4
|
-
|
|
5
|
-
RECONNECT_BACKOFF_FACTOR: 1.1,
|
|
6
|
-
DEFAULT_RECONNECT_DELAY: 5000,
|
|
4
|
+
DEFAULT_RECONNECT_DELAY: 10000,
|
|
7
5
|
DEFAULT_AUTO_RECONNECT: true,
|
|
8
6
|
MAX_RECONNECT_ATTEMPTS: 10,
|
|
7
|
+
DEFAULT_HANDSHAKE_TIMEOUT: 10000,
|
|
9
8
|
|
|
10
9
|
// Logger Options
|
|
11
10
|
DEFAULT_LOGGER_OPTIONS: {
|