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",
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.logger = server.logger;
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
- process.on('uncaughtException', handlers.uncaughtException);
18
- process.on('unhandledRejection', handlers.unhandledRejection);
19
- process.on('SIGINT', handlers.SIGINT);
20
- process.on('SIGTERM', handlers.SIGTERM);
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
- this.logger.error('WebSocketHandlers', `Uncaught exception: ${error.message}`);
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, promise) {
30
- this.logger.error('WebSocketHandlers', `Unhandled promise rejection: ${reason}`);
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.logger.info('WebSocketHandlers', 'Received SIGINT, shutting down gracefully');
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.logger.info('WebSocketHandlers', 'Received SIGTERM, shutting down gracefully');
41
- this.server.disconnect();
42
- setTimeout(() => process.exit(0), 1000);
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 ?? true;
14
- this.reconnectDelay = options.reconnectDelay ?? WebSocketConstants.DEFAULT_RECONNECT_DELAY;
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) => this.emit('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.success('ConnectionManager', 'WebSocket connection established');
109
+ this.logger.info('ConnectionManager', 'WebSocket connection established');
91
110
  }
92
111
 
93
112
  _handleClose(code, reason) {
94
- if (this.connected) {
95
- this.connected = false;
96
- }
113
+ this.connected = false;
114
+ const wasReconnecting = this.reconnecting;
115
+ this.reconnecting = false;
97
116
 
98
- const reasonStr = reason.toString();
117
+ const reasonStr = reason?.toString('utf8') || `Binary reason (length: ${reason?.length || 0})`;
99
118
  this.emit('disconnected', { code, reason: reasonStr });
100
- this.logger.warn('ConnectionManager', `Connection closed`, {
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
- reconnecting: this.reconnecting
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.metrics.increment('reconnects');
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: `${delay}ms (${Math.round(delay / 1000)}s)`,
153
- maxDelay: `${WebSocketConstants.MAX_RECONNECT_DELAY}ms`
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
- }, delay);
176
+ }, this.reconnectDelay);
164
177
  }
165
178
 
166
- disconnect(code = WebSocketConstants.ERROR_CODES.NORMAL_CLOSURE, reason = 'Manual disconnect') {
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
- const payload = typeof data === 'string' ? data : JSON.stringify(data);
187
- this.ws.send(payload);
199
+ try {
200
+ const payload = typeof data === 'string' ? data : JSON.stringify(data);
201
+ this.ws.send(payload);
188
202
 
189
- this.server.metrics.increment('messagesSent');
203
+ if (this.server?.metrics) {
204
+ this.server.metrics.increment('messagesSent');
205
+ }
190
206
 
191
- return true;
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
- MAX_RECONNECT_DELAY: 300000, // 5 minutes
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: {