stelar-time-real 3.2.1 → 3.3.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/src/index.js CHANGED
@@ -1,867 +1,625 @@
1
1
  /**
2
- * @stelar-time-real Server
3
- *
4
- * Dual-protocol real-time server: WebSocket (RFC 6455) + custom binary TCP.
5
- * Zero external dependencies — uses only Node.js built-in modules.
2
+ * @stelar-time-real Server — Dual-protocol: WebSocket (RFC 6455) + binary TCP
6
3
  */
7
- import { createServer as createHttpServer } from 'http';
8
- import { createServer as createTcpServer } from 'net';
4
+ import { createServer as createHttp } from 'http';
5
+ import { createServer as createTcp } from 'net';
9
6
  import { randomUUID } from 'crypto';
10
- import { createServer as createTlsServer } from 'tls';
7
+ import { createServer as createTls } from 'tls';
11
8
  import { FrameParser, encodeJsonFrame, encodeBinaryFrame, encodePingFrame, encodePongFrame, encodeAckResFrame, encodeConnectFrame, encodeDisconnectFrame, encodeErrorFrame, FRAME_JSON, FRAME_BINARY, FRAME_PING, FRAME_PONG, FRAME_ACK_REQ, FRAME_ACK_RES, FRAME_JOIN, FRAME_LEAVE, FRAME_CONNECT, ProtocolError, DEFAULT_MAX_FRAME_SIZE, } from './protocol.js';
12
- import { WSFrameParser, buildUpgradeResponse, validateWSKey, createWSTextFrame, createWSBinaryFrame, createWSCloseFrame, createWSPingFrame, createWSPongFrame, OP_TEXT, OP_BINARY, OP_CLOSE, OP_PING, OP_PONG, WebSocketError, CLOSE_POLICY_VIOLATION, CLOSE_MESSAGE_TOO_BIG, CLOSE_NORMAL, CLOSE_GOING_AWAY, DEFAULT_MAX_WS_FRAME_SIZE, } from './websocket.js';
9
+ import { WSFrameParser, buildUpgradeResponse, validateWSKey, createWSTextFrame, createWSBinaryFrame, createWSCloseFrame, createWSPingFrame, createWSPongFrame, OP_TEXT, OP_BINARY, OP_CLOSE, OP_PING, OP_PONG, WebSocketError, CLOSE_POLICY_VIOLATION, CLOSE_MESSAGE_TOO_BIG, CLOSE_GOING_AWAY, DEFAULT_MAX_WS_FRAME_SIZE, } from './websocket.js';
13
10
  import { Logger, NULL_LOGGER } from './logger.js';
14
11
  class RateLimiter {
15
- constructor(maxPoints = 100, windowMs = 1000) {
12
+ constructor(maxPts = 100, winMs = 1000) {
13
+ this.maxPts = maxPts;
14
+ this.winMs = winMs;
16
15
  this.limits = new Map();
17
- this.maxPoints = maxPoints;
18
- this.windowMs = windowMs;
19
16
  }
20
17
  check(id, cost = 1) {
21
18
  const now = Date.now();
22
- let entry = this.limits.get(id);
23
- if (!entry || now >= entry.resetTime) {
24
- entry = { count: 0, resetTime: now + this.windowMs };
25
- this.limits.set(id, entry);
19
+ let e = this.limits.get(id);
20
+ if (!e || now >= e.resetTime) {
21
+ e = { count: 0, resetTime: now + this.winMs };
22
+ this.limits.set(id, e);
26
23
  }
27
- if (entry.count + cost > this.maxPoints) {
24
+ if (e.count + cost > this.maxPts)
28
25
  return false;
29
- }
30
- entry.count += cost;
26
+ e.count += cost;
31
27
  return true;
32
28
  }
33
- cleanup() {
34
- const now = Date.now();
35
- for (const [id, entry] of this.limits) {
36
- if (now >= entry.resetTime) {
37
- this.limits.delete(id);
38
- }
39
- }
40
- }
41
- reset(id) {
42
- this.limits.delete(id);
43
- }
44
- size() {
45
- return this.limits.size;
46
- }
29
+ cleanup() { const now = Date.now(); for (const [id, e] of this.limits)
30
+ if (now >= e.resetTime)
31
+ this.limits.delete(id); }
32
+ reset(id) { this.limits.delete(id); }
33
+ size() { return this.limits.size; }
47
34
  }
48
- class IPConnectionTracker {
49
- constructor(maxPerIP = 50) {
50
- this.ipCounts = new Map();
51
- this.maxPerIP = maxPerIP;
52
- }
53
- check(ip) {
54
- const current = this.ipCounts.get(ip) || 0;
55
- return current < this.maxPerIP;
56
- }
57
- add(ip) {
58
- this.ipCounts.set(ip, (this.ipCounts.get(ip) || 0) + 1);
59
- }
60
- remove(ip) {
61
- const current = this.ipCounts.get(ip) || 0;
62
- if (current <= 1) {
63
- this.ipCounts.delete(ip);
64
- }
65
- else {
66
- this.ipCounts.set(ip, current - 1);
67
- }
68
- }
69
- getCount(ip) {
70
- return this.ipCounts.get(ip) || 0;
71
- }
72
- cleanup() {
73
- for (const [ip, count] of this.ipCounts) {
74
- if (count <= 0)
75
- this.ipCounts.delete(ip);
76
- }
77
- }
35
+ class IPTracker {
36
+ constructor(max = 50) {
37
+ this.max = max;
38
+ this.m = new Map();
39
+ }
40
+ check(ip) { return (this.m.get(ip) || 0) < this.max; }
41
+ add(ip) { this.m.set(ip, (this.m.get(ip) || 0) + 1); }
42
+ remove(ip) { const c = this.m.get(ip) || 0; c <= 1 ? this.m.delete(ip) : this.m.set(ip, c - 1); }
43
+ getCount(ip) { return this.m.get(ip) || 0; }
44
+ cleanup() { for (const [ip, c] of this.m)
45
+ if (c <= 0)
46
+ this.m.delete(ip); }
78
47
  }
48
+ /* ── Server ── */
79
49
  class StelarServer {
80
- constructor(options = {}) {
50
+ constructor(o = {}) {
81
51
  this.httpServer = null;
82
52
  this.tcpServer = null;
53
+ this.evRateLimits = new Map();
54
+ this.clientRates = new Map();
83
55
  this.clients = new Map();
84
- this.clientsById = new Map();
85
- this.rooms = new Map(); // room -> Set of client IDs
56
+ this.byId = new Map();
57
+ this.rooms = new Map();
86
58
  this.events = new Map();
87
- this.middlewares = [];
88
- this._hbTimer = null;
89
- this._rateCleanupTimer = null;
90
- this._wildcardHandler = null;
91
- this._connectionHandler = null;
59
+ this.mw = [];
60
+ this._hb = null;
61
+ this._rc = null;
62
+ this._wild = null;
63
+ this._connH = null;
92
64
  this._acks = new Map();
93
- this._externalServers = new WeakSet();
94
- this._upgradeHandler = null;
95
- this._requestHandler = null;
65
+ this._ext = new WeakSet();
66
+ this._upgH = null;
67
+ this._reqH = null;
96
68
  this._started = false;
97
69
  this._startTime = 0;
98
- this._shuttingDown = false;
99
- this._sigintHandler = null;
100
- this._sigtermHandler = null;
101
- this._totalConnections = 0;
102
- this._totalMessagesReceived = 0;
103
- this._totalMessagesSent = 0;
104
- this._shutdownCallbacks = [];
105
- this.port = options.port || 3000;
106
- this.httpServer = options.server || null;
107
- this.namespace = options.namespace || '/';
108
- this.heartbeatInterval = options.heartbeatInterval || 30000;
109
- this.heartbeatTimeout = options.heartbeatTimeout || this.heartbeatInterval * 2;
110
- this.tcpPort = options.tcpPort !== undefined ? options.tcpPort : false;
111
- this.maxConnections = options.maxConnections || 10000;
112
- this.maxRooms = options.maxRooms || 10000;
113
- this.maxRoomsPerClient = options.maxRoomsPerClient || 50;
114
- this.maxPayloadSize = options.maxPayloadSize || 10 * 1024 * 1024; // 10 MB
115
- this.maxFrameSize = options.maxFrameSize || DEFAULT_MAX_FRAME_SIZE;
116
- this.maxWSFrameSize = options.maxFrameSize || DEFAULT_MAX_WS_FRAME_SIZE;
117
- this.connectTimeout = options.connectTimeout || 10000;
118
- this.doGracefulShutdown = options.gracefulShutdown !== false;
119
- this.shutdownTimeout = options.shutdownTimeout || 10000;
120
- this.healthEndpoint = options.healthEndpoint !== undefined ? options.healthEndpoint : '/health';
121
- this.tlsOptions = options.tls;
122
- this.allowedOrigins = options.allowedOrigins || null;
123
- this._customRateLimiter = options.customRateLimiter || null;
124
- this._customIPTracker = options.customIPTracker || null;
125
- this._generateClientId = options.generateClientId || null;
126
- this._customHealthHandler = options.customHealthHandler || null;
127
- this.hooks = options.hooks || {};
128
- this.eventRateLimiters = new Map();
129
- this._clientRateOverrides = new Map();
130
- if (options.eventRateLimits) {
131
- for (const [event, config] of Object.entries(options.eventRateLimits)) {
132
- this.eventRateLimiters.set(event, new RateLimiter(config.maxPoints, config.windowMs));
133
- }
134
- }
135
- if (options.rateLimit === false && !this._customRateLimiter) {
70
+ this._shutting = false;
71
+ this._sigH = { int: null, term: null };
72
+ this._totalConns = 0;
73
+ this._totalRecv = 0;
74
+ this._totalSent = 0;
75
+ this._shutdownCbs = [];
76
+ this.port = o.port || 3000;
77
+ this.httpServer = o.server || null;
78
+ this.ns = o.namespace || '/';
79
+ this.hbInterval = o.heartbeatInterval || 30000;
80
+ this.hbTimeout = o.heartbeatTimeout || this.hbInterval * 2;
81
+ this.tcpPort = o.tcpPort !== undefined ? o.tcpPort : false;
82
+ this.maxConns = o.maxConnections || 10000;
83
+ this.maxRooms = o.maxRooms || 10000;
84
+ this.maxRoomsPerClient = o.maxRoomsPerClient || 50;
85
+ this.maxPayload = o.maxPayloadSize || 10 * 1024 * 1024;
86
+ this.maxFrame = o.maxFrameSize || DEFAULT_MAX_FRAME_SIZE;
87
+ this.maxWSFrame = o.maxFrameSize || DEFAULT_MAX_WS_FRAME_SIZE;
88
+ this.connTimeout = o.connectTimeout || 10000;
89
+ this.doGraceful = o.gracefulShutdown !== false;
90
+ this.shutdownMs = o.shutdownTimeout || 10000;
91
+ this.healthPath = o.healthEndpoint !== undefined ? o.healthEndpoint : '/health';
92
+ this.tlsOpts = o.tls;
93
+ this.origins = o.allowedOrigins || null;
94
+ this._crl = o.customRateLimiter || null;
95
+ this._cit = o.customIPTracker || null;
96
+ this._genId = o.generateClientId || null;
97
+ this._healthFn = o.customHealthHandler || null;
98
+ this.hooks = o.hooks || {};
99
+ if (o.eventRateLimits)
100
+ for (const [ev, c] of Object.entries(o.eventRateLimits))
101
+ this.evRateLimits.set(ev, new RateLimiter(c.maxPoints, c.windowMs));
102
+ const rl = o.rateLimit && typeof o.rateLimit === 'object' ? o.rateLimit : {};
103
+ this.rateLimiter = o.rateLimit === false && !this._crl ? null : this._crl ? null : new RateLimiter(rl.maxPoints || 100, rl.windowMs || 1000);
104
+ this.ipTracker = this._cit ? new IPTracker() : new IPTracker(o.maxConnectionsPerIP || 50);
105
+ this.log = o.logger === false ? NULL_LOGGER : o.logger instanceof Logger ? o.logger : new Logger({ level: o.logger || 'info', prefix: 'stelar:server' });
106
+ }
107
+ static of(path, o = {}) { return new StelarServer({ ...o, namespace: path }); }
108
+ /* ── Runtime config ── */
109
+ updateConfig(o) {
110
+ if (o.maxConnections !== undefined)
111
+ this.maxConns = o.maxConnections;
112
+ if (o.maxConnectionsPerIP !== undefined && !this._cit)
113
+ this.ipTracker = new IPTracker(o.maxConnectionsPerIP);
114
+ if (o.maxRooms !== undefined)
115
+ this.maxRooms = o.maxRooms;
116
+ if (o.maxRoomsPerClient !== undefined)
117
+ this.maxRoomsPerClient = o.maxRoomsPerClient;
118
+ if (o.maxPayloadSize !== undefined)
119
+ this.maxPayload = o.maxPayloadSize;
120
+ if (o.heartbeatInterval !== undefined)
121
+ this.hbInterval = o.heartbeatInterval;
122
+ if (o.heartbeatTimeout !== undefined)
123
+ this.hbTimeout = o.heartbeatTimeout;
124
+ if (o.allowedOrigins !== undefined)
125
+ this.origins = o.allowedOrigins;
126
+ if (o.rateLimit === false) {
136
127
  this.rateLimiter = null;
128
+ this._crl = null;
137
129
  }
138
- else if (!this._customRateLimiter) {
139
- const rl = options.rateLimit || {};
140
- this.rateLimiter = new RateLimiter(rl.maxPoints || 100, rl.windowMs || 1000);
141
- }
142
- else {
130
+ else if (o.rateLimit && !this._crl)
131
+ this.rateLimiter = new RateLimiter(o.rateLimit.maxPoints || 100, o.rateLimit.windowMs || 1000);
132
+ if (o.customRateLimiter !== undefined) {
133
+ this._crl = o.customRateLimiter;
143
134
  this.rateLimiter = null;
144
135
  }
145
- if (!this._customIPTracker) {
146
- this.ipTracker = new IPConnectionTracker(options.maxConnectionsPerIP || 50);
147
- }
148
- else {
149
- this.ipTracker = new IPConnectionTracker(50); // unused when custom tracker is set
150
- }
151
- if (options.logger === false) {
152
- this.log = NULL_LOGGER;
153
- }
154
- else if (options.logger instanceof Logger) {
155
- this.log = options.logger;
156
- }
157
- else {
158
- this.log = new Logger({
159
- level: options.logger || 'info',
160
- prefix: 'stelar:server',
161
- });
162
- }
163
- }
164
- static of(path, options = {}) {
165
- return new StelarServer({ ...options, namespace: path });
166
- }
167
- /** Update server configuration at runtime. */
168
- updateConfig(options) {
169
- if (options.maxConnections !== undefined)
170
- this.maxConnections = options.maxConnections;
171
- if (options.maxConnectionsPerIP !== undefined && !this._customIPTracker) {
172
- this.ipTracker = new IPConnectionTracker(options.maxConnectionsPerIP);
173
- }
174
- if (options.maxRooms !== undefined)
175
- this.maxRooms = options.maxRooms;
176
- if (options.maxRoomsPerClient !== undefined)
177
- this.maxRoomsPerClient = options.maxRoomsPerClient;
178
- if (options.maxPayloadSize !== undefined)
179
- this.maxPayloadSize = options.maxPayloadSize;
180
- if (options.heartbeatInterval !== undefined)
181
- this.heartbeatInterval = options.heartbeatInterval;
182
- if (options.heartbeatTimeout !== undefined)
183
- this.heartbeatTimeout = options.heartbeatTimeout;
184
- if (options.allowedOrigins !== undefined)
185
- this.allowedOrigins = options.allowedOrigins;
186
- if (options.rateLimit === false) {
187
- this.rateLimiter = null;
188
- this._customRateLimiter = null;
189
- }
190
- else if (options.rateLimit && !this._customRateLimiter) {
191
- const rl = options.rateLimit;
192
- this.rateLimiter = new RateLimiter(rl.maxPoints || 100, rl.windowMs || 1000);
193
- }
194
- if (options.customRateLimiter !== undefined) {
195
- this._customRateLimiter = options.customRateLimiter;
196
- this.rateLimiter = null;
197
- }
198
- if (options.customIPTracker !== undefined) {
199
- this._customIPTracker = options.customIPTracker;
200
- }
201
- if (options.generateClientId !== undefined) {
202
- this._generateClientId = options.generateClientId;
203
- }
204
- if (options.customHealthHandler !== undefined) {
205
- this._customHealthHandler = options.customHealthHandler;
206
- }
207
- if (options.hooks !== undefined) {
208
- this.hooks = { ...this.hooks, ...options.hooks };
209
- }
210
- if (options.eventRateLimits !== undefined) {
211
- this.eventRateLimiters.clear();
212
- for (const [event, config] of Object.entries(options.eventRateLimits)) {
213
- this.eventRateLimiters.set(event, new RateLimiter(config.maxPoints, config.windowMs));
214
- }
215
- }
216
- this.log.info('Server configuration updated');
136
+ if (o.customIPTracker !== undefined)
137
+ this._cit = o.customIPTracker;
138
+ if (o.generateClientId !== undefined)
139
+ this._genId = o.generateClientId;
140
+ if (o.customHealthHandler !== undefined)
141
+ this._healthFn = o.customHealthHandler;
142
+ if (o.hooks !== undefined)
143
+ this.hooks = { ...this.hooks, ...o.hooks };
144
+ if (o.eventRateLimits !== undefined) {
145
+ this.evRateLimits.clear();
146
+ for (const [ev, c] of Object.entries(o.eventRateLimits))
147
+ this.evRateLimits.set(ev, new RateLimiter(c.maxPoints, c.windowMs));
148
+ }
149
+ this.log.info('Config updated');
217
150
  return this;
218
151
  }
219
- /** Set a per-client rate limit override. */
220
- setClientRateLimit(clientId, config) {
221
- this._clientRateOverrides.set(clientId, new RateLimiter(config.maxPoints, config.windowMs));
222
- return this;
223
- }
224
- /** Remove a per-client rate limit override, falling back to the global limiter. */
225
- removeClientRateLimit(clientId) {
226
- this._clientRateOverrides.delete(clientId);
227
- return this;
228
- }
229
- /** Set a per-event rate limit. */
230
- setEventRateLimit(event, config) {
231
- this.eventRateLimiters.set(event, new RateLimiter(config.maxPoints, config.windowMs));
232
- return this;
233
- }
234
- /** Remove a per-event rate limit. */
235
- removeEventRateLimit(event) {
236
- this.eventRateLimiters.delete(event);
237
- return this;
238
- }
239
- /** Get the current server configuration as a read-only object. */
152
+ setClientRateLimit(id, c) { this.clientRates.set(id, new RateLimiter(c.maxPoints, c.windowMs)); return this; }
153
+ removeClientRateLimit(id) { this.clientRates.delete(id); return this; }
154
+ setEventRateLimit(ev, c) { this.evRateLimits.set(ev, new RateLimiter(c.maxPoints, c.windowMs)); return this; }
155
+ removeEventRateLimit(ev) { this.evRateLimits.delete(ev); return this; }
240
156
  getConfig() {
241
157
  return Object.freeze({
242
- maxConnections: this.maxConnections,
243
- maxConnectionsPerIP: this._customIPTracker ? -1 : this.ipTracker.maxPerIP || 50,
244
- maxRooms: this.maxRooms,
245
- maxRoomsPerClient: this.maxRoomsPerClient,
246
- maxPayloadSize: this.maxPayloadSize,
247
- heartbeatInterval: this.heartbeatInterval,
248
- heartbeatTimeout: this.heartbeatTimeout,
249
- connectTimeout: this.connectTimeout,
250
- shutdownTimeout: this.shutdownTimeout,
251
- hasCustomRateLimiter: this._customRateLimiter !== null,
252
- hasCustomIPTracker: this._customIPTracker !== null,
253
- hasCustomClientIdGenerator: this._generateClientId !== null,
254
- hasCustomHealthHandler: this._customHealthHandler !== null,
255
- eventRateLimits: Array.from(this.eventRateLimiters.keys()),
256
- hooks: Object.keys(this.hooks),
257
- allowedOrigins: this.allowedOrigins,
158
+ maxConnections: this.maxConns, maxConnectionsPerIP: this._cit ? -1 : 50,
159
+ maxRooms: this.maxRooms, maxRoomsPerClient: this.maxRoomsPerClient, maxPayloadSize: this.maxPayload,
160
+ heartbeatInterval: this.hbInterval, heartbeatTimeout: this.hbTimeout, connectTimeout: this.connTimeout,
161
+ shutdownTimeout: this.shutdownMs, hasCustomRateLimiter: this._crl !== null, hasCustomIPTracker: this._cit !== null,
162
+ hasCustomClientIdGenerator: this._genId !== null, hasCustomHealthHandler: this._healthFn !== null,
163
+ eventRateLimits: Array.from(this.evRateLimits.keys()), hooks: Object.keys(this.hooks), allowedOrigins: this.origins,
258
164
  });
259
165
  }
260
- use(middleware) {
261
- this.middlewares.push(middleware);
262
- return this;
263
- }
264
- on(event, handler) {
265
- this.events.set(event, handler);
266
- return this;
267
- }
268
- onAll(handler) {
269
- this._wildcardHandler = handler;
270
- return this;
271
- }
272
- onConnection(handler) {
273
- this._connectionHandler = handler;
274
- return this;
275
- }
276
- onDisconnect(handler) {
277
- this.events.set('disconnect', handler);
278
- return this;
279
- }
280
- onAck(name, handler) {
281
- this._acks.set(name, handler);
282
- return this;
283
- }
166
+ /* ── Event registration ── */
167
+ use(mw) { this.mw.push(mw); return this; }
168
+ on(ev, h) { this.events.set(ev, h); return this; }
169
+ onAll(h) { this._wild = h; return this; }
170
+ onConnection(h) { this._connH = h; return this; }
171
+ onDisconnect(h) { this.events.set('disconnect', h); return this; }
172
+ onAck(name, h) { this._acks.set(name, h); return this; }
173
+ /* ── Messaging ── */
284
174
  broadcast(event, data, excludeId) {
285
- if (this.hooks.onBeforeBroadcast) {
286
- const result = this.hooks.onBeforeBroadcast({ event, data, excludeId });
287
- if (result === false)
288
- return this;
289
- }
175
+ if (this.hooks.onBeforeBroadcast?.({ event, data, excludeId }) === false)
176
+ return this;
177
+ const wsF = createWSTextFrame(JSON.stringify({ event, data }));
178
+ const tcpF = encodeJsonFrame(event, data, this.maxFrame);
290
179
  let sent = 0;
291
- this.clients.forEach((record) => {
292
- if (excludeId && record.info.id === excludeId)
293
- return;
294
- if (this._sendJsonToClient(record, event, data))
295
- sent++;
296
- });
297
- this._totalMessagesSent += sent;
180
+ this.clients.forEach(r => { if (excludeId && r.info.id === excludeId)
181
+ return; if (this._write(r, wsF, tcpF))
182
+ sent++; });
183
+ this._totalSent += sent;
298
184
  return this;
299
185
  }
300
- broadcastBinary(event, buffer) {
301
- this.clients.forEach((record) => {
302
- this._sendBinaryRaw(record, event, buffer);
303
- });
304
- }
186
+ broadcastBinary(event, buf) { this.clients.forEach(r => this._sendBin(r, event, buf)); }
305
187
  to(room, event, data, excludeId) {
306
- const memberIds = this.rooms.get(room);
307
- if (!memberIds)
188
+ const ids = this.rooms.get(room);
189
+ if (!ids)
308
190
  return this;
191
+ const wsF = createWSTextFrame(JSON.stringify({ event, data }));
192
+ const tcpF = encodeJsonFrame(event, data, this.maxFrame);
309
193
  let sent = 0;
310
- for (const clientId of memberIds) {
311
- if (excludeId && clientId === excludeId)
194
+ for (const id of ids) {
195
+ if (excludeId && id === excludeId)
312
196
  continue;
313
- const record = this.clientsById.get(clientId);
314
- if (record && this._sendJsonToClient(record, event, data))
197
+ const r = this.byId.get(id);
198
+ if (r && this._write(r, wsF, tcpF))
315
199
  sent++;
316
200
  }
317
- this._totalMessagesSent += sent;
201
+ this._totalSent += sent;
318
202
  return this;
319
203
  }
320
204
  toId(id, event, data) {
321
- const record = this.clientsById.get(id);
322
- if (record && this._sendJsonToClient(record, event, data)) {
323
- this._totalMessagesSent++;
324
- }
205
+ const r = this.byId.get(id);
206
+ if (r && this._sendJson(r, event, data))
207
+ this._totalSent++;
325
208
  return this;
326
209
  }
327
210
  getClients(room) {
328
211
  const list = [];
329
- this.clients.forEach((record) => {
330
- if (!room || record.info.rooms.has(room)) {
331
- list.push({ id: record.info.id, rooms: Array.from(record.info.rooms) });
332
- }
333
- });
212
+ this.clients.forEach(r => { if (!room || r.info.rooms.has(room))
213
+ list.push({ id: r.info.id, rooms: [...r.info.rooms] }); });
334
214
  return list;
335
215
  }
336
- getRoomMembers(room) {
337
- const members = this.rooms.get(room);
338
- return members ? Array.from(members) : [];
339
- }
340
- getRooms() {
341
- return Array.from(this.rooms.keys());
342
- }
343
- getPort() {
344
- const address = this.httpServer?.address();
345
- if (address && typeof address === 'object') {
346
- return address.port;
347
- }
348
- return this.port;
349
- }
216
+ getRoomMembers(room) { return this.rooms.get(room) ? [...this.rooms.get(room)] : []; }
217
+ getRooms() { return [...this.rooms.keys()]; }
218
+ getPort() { const a = this.httpServer?.address(); return a && typeof a === 'object' ? a.port : this.port; }
350
219
  getStats() {
351
- let wsConns = 0;
352
- let tcpConns = 0;
353
- this.clients.forEach((r) => {
354
- if (r.protocol === 'ws')
355
- wsConns++;
356
- else
357
- tcpConns++;
358
- });
220
+ let ws = 0, tcp = 0;
221
+ this.clients.forEach(r => r.protocol === 'ws' ? ws++ : tcp++);
359
222
  return {
360
- totalConnections: this._totalConnections,
361
- activeConnections: this.clients.size,
362
- totalMessagesReceived: this._totalMessagesReceived,
363
- totalMessagesSent: this._totalMessagesSent,
364
- totalRooms: this.rooms.size,
365
- uptime: this._startTime ? Date.now() - this._startTime : 0,
366
- wsConnections: wsConns,
367
- tcpConnections: tcpConns,
368
- memoryUsage: process.memoryUsage(),
369
- rateLimiterEntries: this._getRateLimiterSize(),
223
+ totalConnections: this._totalConns, activeConnections: this.clients.size,
224
+ totalMessagesReceived: this._totalRecv, totalMessagesSent: this._totalSent,
225
+ totalRooms: this.rooms.size, uptime: this._startTime ? Date.now() - this._startTime : 0,
226
+ wsConnections: ws, tcpConnections: tcp, memoryUsage: process.memoryUsage(),
227
+ rateLimiterEntries: this._crl?.size() ?? this.rateLimiter?.size() ?? 0,
370
228
  };
371
229
  }
372
- _getRateLimiterSize() {
373
- if (this._customRateLimiter)
374
- return this._customRateLimiter.size();
375
- return this.rateLimiter?.size() || 0;
376
- }
377
- /** Check rate limit. Priority: per-client override > event-specific > custom/global. */
378
- _checkRateLimit(clientId, event) {
379
- const clientOverride = this._clientRateOverrides.get(clientId);
380
- if (clientOverride) {
381
- return clientOverride.check(clientId);
382
- }
383
- if (event && this.eventRateLimiters.has(event)) {
384
- const eventLimiter = this.eventRateLimiters.get(event);
385
- if (!eventLimiter.check(clientId))
386
- return false;
387
- }
388
- if (this._customRateLimiter) {
389
- return this._customRateLimiter.check(clientId);
230
+ onShutdown(cb) { this._shutdownCbs.push(cb); return this; }
231
+ /* ── Private: send helpers ── */
232
+ _sendJson(r, event, data) {
233
+ if (r.socket.destroyed || r.socket.writableEnded)
234
+ return false;
235
+ try {
236
+ r.socket.write(r.protocol === 'ws' ? createWSTextFrame(JSON.stringify({ event, data })) : encodeJsonFrame(event, data, this.maxFrame));
237
+ r.info.messagesSent++;
238
+ return true;
390
239
  }
391
- if (this.rateLimiter) {
392
- return this.rateLimiter.check(clientId);
240
+ catch {
241
+ return false;
393
242
  }
394
- return true;
395
243
  }
396
- _sendJsonToClient(record, event, data) {
397
- if (record.socket.destroyed || record.socket.writableEnded)
244
+ _write(r, wsF, tcpF) {
245
+ if (r.socket.destroyed || r.socket.writableEnded)
398
246
  return false;
399
247
  try {
400
- if (record.protocol === 'ws') {
401
- const json = JSON.stringify({ event, data });
402
- record.socket.write(createWSTextFrame(json));
403
- }
404
- else {
405
- record.socket.write(encodeJsonFrame(event, data, this.maxFrameSize));
406
- }
407
- record.info.messagesSent++;
248
+ r.socket.write(r.protocol === 'ws' ? wsF : tcpF);
249
+ r.info.messagesSent++;
408
250
  return true;
409
251
  }
410
- catch (err) {
411
- this.log.error('Send error', { clientId: record.info.id, error: String(err) });
252
+ catch {
412
253
  return false;
413
254
  }
414
255
  }
415
- _sendBinaryRaw(record, event, buffer) {
416
- if (record.socket.destroyed || record.socket.writableEnded)
256
+ _sendBin(r, event, buf) {
257
+ if (r.socket.destroyed || r.socket.writableEnded)
417
258
  return false;
418
259
  try {
419
- if (record.protocol === 'ws') {
420
- const header = JSON.stringify({ event, _binary: true });
421
- const headerBytes = Buffer.from(header, 'utf8');
422
- const combined = Buffer.alloc(headerBytes.length + 1 + buffer.byteLength);
423
- headerBytes.copy(combined, 0);
424
- combined[headerBytes.length] = 0;
425
- combined.set(new Uint8Array(buffer), headerBytes.length + 1);
426
- record.socket.write(createWSBinaryFrame(combined));
260
+ if (r.protocol === 'ws') {
261
+ const hdr = Buffer.from(JSON.stringify({ event, _binary: true }), 'utf8');
262
+ const combined = Buffer.alloc(hdr.length + 1 + buf.byteLength);
263
+ hdr.copy(combined, 0);
264
+ combined[hdr.length] = 0;
265
+ combined.set(new Uint8Array(buf), hdr.length + 1);
266
+ r.socket.write(createWSBinaryFrame(combined));
427
267
  }
428
268
  else {
429
- record.socket.write(encodeBinaryFrame(event, new Uint8Array(buffer), this.maxFrameSize));
269
+ r.socket.write(encodeBinaryFrame(event, new Uint8Array(buf), this.maxFrame));
430
270
  }
431
- record.info.messagesSent++;
271
+ r.info.messagesSent++;
432
272
  return true;
433
273
  }
434
- catch (err) {
435
- this.log.error('Binary send error', { clientId: record.info.id, error: String(err) });
274
+ catch {
436
275
  return false;
437
276
  }
438
277
  }
439
- _joinRoom(record, room) {
440
- if (this.hooks.onClientJoinRoom) {
441
- const result = this.hooks.onClientJoinRoom({
442
- clientId: record.info.id,
443
- room,
444
- metadata: record.info.metadata,
445
- });
446
- if (result === false) {
447
- this.log.info('Room join rejected by hook', { clientId: record.info.id, room });
448
- return;
278
+ _checkRate(cid, event) {
279
+ const co = this.clientRates.get(cid);
280
+ if (co)
281
+ return co.check(cid);
282
+ if (event && this.evRateLimits.has(event) && !this.evRateLimits.get(event).check(cid))
283
+ return false;
284
+ if (this._crl)
285
+ return this._crl.check(cid);
286
+ if (this.rateLimiter)
287
+ return this.rateLimiter.check(cid);
288
+ return true;
289
+ }
290
+ _getIP(socket, req) {
291
+ if (req) {
292
+ const fwd = req.headers['x-forwarded-for'];
293
+ if (typeof fwd === 'string')
294
+ return fwd.split(',')[0].trim();
295
+ }
296
+ return socket.remoteAddress || 'unknown';
297
+ }
298
+ /* ── Private: client lifecycle ── */
299
+ _register(socket, proto, req, parser) {
300
+ const ip = this._getIP(socket, req);
301
+ if (this.clients.size >= this.maxConns) {
302
+ this.hooks.onMaxConnectionsReached?.({ activeConnections: this.clients.size, max: this.maxConns, ip });
303
+ this.log.warn('Max connections reached', { active: this.clients.size, max: this.maxConns });
304
+ if (proto === 'ws')
305
+ try {
306
+ socket.write(createWSCloseFrame(CLOSE_POLICY_VIOLATION, 'Server full'));
307
+ }
308
+ catch { }
309
+ socket.destroy();
310
+ return null;
311
+ }
312
+ const tracker = this._cit || this.ipTracker;
313
+ if (!tracker.check(ip)) {
314
+ this.log.warn('Max connections per IP', { ip });
315
+ if (proto === 'ws')
316
+ try {
317
+ socket.write(createWSCloseFrame(CLOSE_POLICY_VIOLATION, 'Too many connections'));
318
+ }
319
+ catch { }
320
+ socket.destroy();
321
+ return null;
322
+ }
323
+ const id = this._genId ? this._genId() : randomUUID();
324
+ const info = { id, rooms: new Set(), lastPing: Date.now(), protocol: proto, connectedAt: Date.now(), metadata: new Map(), messagesReceived: 0, messagesSent: 0, remoteAddress: ip };
325
+ const record = { info, socket, parser, protocol: proto };
326
+ this.clients.set(socket, record);
327
+ this.byId.set(id, record);
328
+ tracker.add(ip);
329
+ this._totalConns++;
330
+ return record;
331
+ }
332
+ _unregister(r, ctx) {
333
+ this.hooks.onClientDisconnect?.({ clientId: r.info.id, ip: r.info.remoteAddress, protocol: r.info.protocol, rooms: new Set(r.info.rooms) });
334
+ for (const room of r.info.rooms) {
335
+ const m = this.rooms.get(room);
336
+ if (m) {
337
+ m.delete(r.info.id);
338
+ if (!m.size)
339
+ this.rooms.delete(room);
449
340
  }
450
341
  }
451
- if (record.info.rooms.size >= this.maxRoomsPerClient) {
452
- if (this.hooks.onMaxRoomsPerClientReached) {
453
- const result = this.hooks.onMaxRoomsPerClientReached({
454
- clientId: record.info.id,
455
- room,
456
- currentRooms: record.info.rooms.size,
457
- max: this.maxRoomsPerClient,
458
- });
459
- if (result === false)
460
- return;
342
+ r.info.rooms.clear();
343
+ this.byId.delete(r.info.id);
344
+ this.clients.delete(r.socket);
345
+ (this._cit || this.ipTracker).remove(r.info.remoteAddress);
346
+ if (this._crl)
347
+ this._crl.reset(r.info.id);
348
+ else
349
+ this.rateLimiter?.reset(r.info.id);
350
+ this.clientRates.delete(r.info.id);
351
+ const h = this.events.get('disconnect');
352
+ if (h)
353
+ try {
354
+ h({ ...ctx, event: 'disconnect' });
355
+ }
356
+ catch (e) {
357
+ this.log.error('Disconnect handler error', { error: String(e) });
461
358
  }
462
- this.log.warn('Client exceeded max rooms', { clientId: record.info.id, room, max: this.maxRoomsPerClient });
359
+ }
360
+ _joinRoom(r, room) {
361
+ if (this.hooks.onClientJoinRoom?.({ clientId: r.info.id, room, metadata: r.info.metadata }) === false)
362
+ return;
363
+ if (r.info.rooms.size >= this.maxRoomsPerClient) {
364
+ this.hooks.onMaxRoomsPerClientReached?.({ clientId: r.info.id, room, currentRooms: r.info.rooms.size, max: this.maxRoomsPerClient });
463
365
  return;
464
366
  }
465
367
  if (this.rooms.size >= this.maxRooms && !this.rooms.has(room)) {
466
- if (this.hooks.onMaxRoomsReached) {
467
- const result = this.hooks.onMaxRoomsReached({
468
- clientId: record.info.id,
469
- room,
470
- totalRooms: this.rooms.size,
471
- max: this.maxRooms,
472
- });
473
- if (result === false)
474
- return;
475
- }
476
- this.log.warn('Server exceeded max rooms', { room, max: this.maxRooms });
368
+ this.hooks.onMaxRoomsReached?.({ clientId: r.info.id, room, totalRooms: this.rooms.size, max: this.maxRooms });
477
369
  return;
478
370
  }
479
- record.info.rooms.add(room);
480
- if (!this.rooms.has(room)) {
371
+ r.info.rooms.add(room);
372
+ if (!this.rooms.has(room))
481
373
  this.rooms.set(room, new Set());
482
- }
483
- this.rooms.get(room).add(record.info.id);
484
- this._sendJsonToClient(record, 'joined-room', room);
374
+ this.rooms.get(room).add(r.info.id);
375
+ this._sendJson(r, 'joined-room', room);
485
376
  }
486
- _leaveRoom(record, room) {
487
- if (this.hooks.onClientLeaveRoom) {
488
- const result = this.hooks.onClientLeaveRoom({
489
- clientId: record.info.id,
490
- room,
491
- });
492
- if (result === false) {
493
- this.log.info('Room leave rejected by hook', { clientId: record.info.id, room });
494
- return;
495
- }
496
- }
497
- record.info.rooms.delete(room);
498
- const members = this.rooms.get(room);
499
- if (members) {
500
- members.delete(record.info.id);
501
- if (members.size === 0) {
377
+ _leaveRoom(r, room) {
378
+ if (this.hooks.onClientLeaveRoom?.({ clientId: r.info.id, room }) === false)
379
+ return;
380
+ r.info.rooms.delete(room);
381
+ const m = this.rooms.get(room);
382
+ if (m) {
383
+ m.delete(r.info.id);
384
+ if (!m.size)
502
385
  this.rooms.delete(room);
503
- }
504
- }
505
- this._sendJsonToClient(record, 'left-room', room);
506
- }
507
- _removeFromAllRooms(record) {
508
- for (const room of record.info.rooms) {
509
- const members = this.rooms.get(room);
510
- if (members) {
511
- members.delete(record.info.id);
512
- if (members.size === 0) {
513
- this.rooms.delete(room);
514
- }
515
- }
516
386
  }
517
- record.info.rooms.clear();
387
+ this._sendJson(r, 'left-room', room);
518
388
  }
519
- _buildCtx(record, req) {
520
- const self = this;
389
+ /* ── Private: context & middleware ── */
390
+ _buildCtx(r, req) {
391
+ const s = this;
521
392
  const ctx = {
522
- id: record.info.id,
523
- socket: record.socket,
524
- req,
525
- clientInfo: record.info,
526
- emit: (evt, d) => { if (self._sendJsonToClient(record, evt, d))
527
- self._totalMessagesSent++; },
528
- send: (respId, d) => { if (self._sendJsonToClient(record, respId, { data: d, _isAck: true }))
529
- self._totalMessagesSent++; },
530
- emitBinary: (evt, buf) => { if (self._sendBinaryRaw(record, evt, buf))
531
- self._totalMessagesSent++; },
532
- broadcast: (evt, d) => self.broadcast(evt, d, record.info.id),
533
- broadcastBinary: (evt, buf) => self.broadcastBinary(evt, buf),
534
- to: (room, evt, d) => self.to(room, evt, d, record.info.id),
535
- toId: (id, evt, d) => self.toId(id, evt, d),
536
- getClients: (room) => self.getClients(room),
537
- joinRoom: (room) => self._joinRoom(record, room),
538
- leaveRoom: (room) => self._leaveRoom(record, room),
539
- setMetadata: (key, value) => record.info.metadata.set(key, value),
540
- getMetadata: (key) => record.info.metadata.get(key),
541
- ack: (ackName, d) => {
542
- const ackHandler = self._acks.get(ackName);
543
- if (ackHandler) {
544
- const result = ackHandler({ ...ctx, data: d });
545
- if (result !== undefined) {
546
- try {
547
- if (record.protocol === 'ws') {
548
- record.socket.write(createWSTextFrame(JSON.stringify({ event: ackName, data: result, _isAck: true })));
549
- }
550
- else {
551
- record.socket.write(encodeAckResFrame(ackName, result, self.maxFrameSize));
552
- }
553
- self._totalMessagesSent++;
554
- }
555
- catch (err) {
556
- self.log.error('ACK send error', { ackName, error: String(err) });
557
- }
558
- }
393
+ id: r.info.id, socket: r.socket, req, clientInfo: r.info,
394
+ emit: (ev, d) => { if (s._sendJson(r, ev, d))
395
+ s._totalSent++; },
396
+ send: (rid, d) => { if (s._sendJson(r, rid, { data: d, _isAck: true }))
397
+ s._totalSent++; },
398
+ emitBinary: (ev, buf) => { if (s._sendBin(r, ev, buf))
399
+ s._totalSent++; },
400
+ broadcast: (ev, d) => s.broadcast(ev, d, r.info.id),
401
+ broadcastBinary: (ev, buf) => s.broadcastBinary(ev, buf),
402
+ to: (room, ev, d) => s.to(room, ev, d, r.info.id),
403
+ toId: (id, ev, d) => s.toId(id, ev, d),
404
+ getClients: (room) => s.getClients(room),
405
+ joinRoom: (room) => s._joinRoom(r, room),
406
+ leaveRoom: (room) => s._leaveRoom(r, room),
407
+ setMetadata: (k, v) => r.info.metadata.set(k, v),
408
+ getMetadata: (k) => r.info.metadata.get(k),
409
+ ack: (name, d) => {
410
+ const h = s._acks.get(name);
411
+ if (!h)
412
+ return;
413
+ let res;
414
+ try {
415
+ res = h({ ...ctx, data: d });
559
416
  }
560
- }
561
- };
562
- return ctx;
563
- }
564
- runMiddlewares(ctx, next) {
565
- const run = (i) => {
566
- if (i >= this.middlewares.length)
567
- return next();
568
- try {
569
- this.middlewares[i](ctx, () => run(i + 1));
570
- }
571
- catch (err) {
572
- this.log.error('Middleware error', { error: String(err), clientId: ctx.id });
573
- ctx.socket.destroy();
574
- }
575
- };
576
- run(0);
577
- }
578
- startHeartbeat() {
579
- this._hbTimer = setInterval(() => {
580
- const now = Date.now();
581
- this.clients.forEach((record) => {
582
- if (now - record.info.lastPing > this.heartbeatTimeout) {
583
- this.log.info('Client heartbeat timeout', { clientId: record.info.id });
584
- record.socket.destroy();
417
+ catch (e) {
418
+ s.log.error('ACK handler error', { name, error: String(e) });
419
+ return;
585
420
  }
586
- else {
421
+ if (res !== undefined) {
587
422
  try {
588
- if (record.protocol === 'ws') {
589
- record.socket.write(createWSPingFrame());
423
+ if (r.protocol === 'ws') {
424
+ const p = { event: name, data: res, _isAck: true };
425
+ if (ctx._correlationId)
426
+ p._correlationId = ctx._correlationId;
427
+ r.socket.write(createWSTextFrame(JSON.stringify(p)));
590
428
  }
591
429
  else {
592
- record.socket.write(encodePingFrame());
430
+ r.socket.write(ctx._correlationId
431
+ ? encodeAckResFrame(name, { data: res, _correlationId: ctx._correlationId }, s.maxFrame)
432
+ : encodeAckResFrame(name, res, s.maxFrame));
593
433
  }
434
+ s._totalSent++;
594
435
  }
595
- catch {
596
- // socket may have closed
436
+ catch (e) {
437
+ s.log.error('ACK send error', { name, error: String(e) });
597
438
  }
598
439
  }
599
- });
600
- }, this.heartbeatInterval);
601
- if (this._hbTimer && typeof this._hbTimer === 'object' && 'unref' in this._hbTimer) {
602
- this._hbTimer.unref();
603
- }
604
- }
605
- _getClientIP(socket, req) {
606
- if (req) {
607
- const forwarded = req.headers['x-forwarded-for'];
608
- if (typeof forwarded === 'string') {
609
- return forwarded.split(',')[0].trim();
610
- }
611
- }
612
- return socket.remoteAddress || 'unknown';
440
+ },
441
+ };
442
+ return ctx;
613
443
  }
614
- _registerClient(socket, protocol, req, parser) {
615
- if (this.clients.size >= this.maxConnections) {
616
- const clientIP = this._getClientIP(socket, req);
617
- if (this.hooks.onMaxConnectionsReached) {
618
- this.hooks.onMaxConnectionsReached({
619
- activeConnections: this.clients.size,
620
- max: this.maxConnections,
621
- ip: clientIP,
622
- });
623
- }
624
- this.log.warn('Max connections reached, rejecting', { active: this.clients.size, max: this.maxConnections });
625
- try {
626
- if (protocol === 'ws') {
627
- socket.write(createWSCloseFrame(CLOSE_POLICY_VIOLATION, 'Server full'));
628
- }
629
- }
630
- catch { /* ignore */ }
631
- socket.destroy();
632
- return null;
633
- }
634
- const clientIP = this._getClientIP(socket, req);
635
- const ipTracker = this._customIPTracker || this.ipTracker;
636
- if (!ipTracker.check(clientIP)) {
637
- this.log.warn('Max connections per IP reached, rejecting', { ip: clientIP });
638
- try {
639
- if (protocol === 'ws') {
640
- socket.write(createWSCloseFrame(CLOSE_POLICY_VIOLATION, 'Too many connections from this IP'));
641
- }
642
- }
643
- catch { /* ignore */ }
644
- socket.destroy();
645
- return null;
444
+ _runMw(ctx, next) {
445
+ const run = (i) => { if (i >= this.mw.length)
446
+ return next(); try {
447
+ this.mw[i](ctx, () => run(i + 1));
646
448
  }
647
- const clientId = this._generateClientId ? this._generateClientId() : randomUUID();
648
- const info = {
649
- id: clientId,
650
- rooms: new Set(),
651
- lastPing: Date.now(),
652
- protocol,
653
- connectedAt: Date.now(),
654
- metadata: new Map(),
655
- messagesReceived: 0,
656
- messagesSent: 0,
657
- remoteAddress: clientIP,
658
- };
659
- const record = { info, socket, parser, protocol };
660
- this.clients.set(socket, record);
661
- this.clientsById.set(clientId, record);
662
- ipTracker.add(clientIP);
663
- this._totalConnections++;
664
- return record;
449
+ catch {
450
+ ctx.socket.destroy();
451
+ } };
452
+ run(0);
665
453
  }
666
- _unregisterClient(record, ctx) {
667
- if (this.hooks.onClientDisconnect) {
668
- this.hooks.onClientDisconnect({
669
- clientId: record.info.id,
670
- ip: record.info.remoteAddress,
671
- protocol: record.info.protocol,
672
- rooms: new Set(record.info.rooms),
673
- });
454
+ /* ── Private: event dispatch (shared by WS & TCP) ── */
455
+ _dispatch(r, ctx, event, data, correlationId) {
456
+ if (event === 'pong') {
457
+ r.info.lastPing = Date.now();
458
+ return;
674
459
  }
675
- this._removeFromAllRooms(record);
676
- this.clientsById.delete(record.info.id);
677
- this.clients.delete(record.socket);
678
- const ipTracker = this._customIPTracker || this.ipTracker;
679
- ipTracker.remove(record.info.remoteAddress);
680
- if (this._customRateLimiter) {
681
- this._customRateLimiter.reset(record.info.id);
460
+ if (event === 'join-room') {
461
+ if (data)
462
+ this._joinRoom(r, String(data));
463
+ return;
682
464
  }
683
- else if (this.rateLimiter) {
684
- this.rateLimiter.reset(record.info.id);
465
+ if (event === 'leave-room') {
466
+ if (data)
467
+ this._leaveRoom(r, String(data));
468
+ return;
685
469
  }
686
- this._clientRateOverrides.delete(record.info.id);
687
- if (this.events.has('disconnect')) {
688
- const handler = this.events.get('disconnect');
470
+ const ectx = { ...ctx, data, event, _correlationId: correlationId };
471
+ const h = this.events.get(event);
472
+ if (h)
689
473
  try {
690
- handler({ ...ctx, event: 'disconnect' });
474
+ h(ectx);
691
475
  }
692
- catch (err) {
693
- this.log.error('Disconnect handler error', { error: String(err) });
476
+ catch (e) {
477
+ this.log.error('Event handler error', { event, error: String(e) });
478
+ }
479
+ if (this._wild)
480
+ try {
481
+ this._wild({ event, data: ectx });
482
+ }
483
+ catch (e) {
484
+ this.log.error('Wildcard error', { error: String(e) });
694
485
  }
695
- }
696
- }
697
- _checkOrigin(req) {
698
- if (!this.allowedOrigins)
699
- return true;
700
- const origin = req.headers['origin'];
701
- if (!origin)
702
- return true;
703
- return this.allowedOrigins.includes(origin);
704
486
  }
705
- handleWSUpgrade(req, socket, head) {
706
- const urlPath = new URL(req.url || '/', 'http://localhost').pathname;
707
- const nsPath = this.namespace === '/' ? '/' : this.namespace;
708
- if (nsPath !== '/' && urlPath !== nsPath) {
709
- this.log.debug('Rejected WS: wrong namespace', { path: urlPath, expected: nsPath });
487
+ /* ── Private: heartbeat ── */
488
+ _startHeartbeat() {
489
+ this._hb = setInterval(() => {
490
+ const now = Date.now();
491
+ this.clients.forEach(r => {
492
+ if (now - r.info.lastPing > this.hbTimeout) {
493
+ r.socket.destroy();
494
+ }
495
+ else
496
+ try {
497
+ r.socket.write(r.protocol === 'ws' ? createWSPingFrame() : encodePingFrame());
498
+ }
499
+ catch { }
500
+ });
501
+ }, this.hbInterval);
502
+ this._hb?.unref?.();
503
+ }
504
+ /* ── Private: WS upgrade ── */
505
+ _wsUpgrade(req, socket, head) {
506
+ const path = new URL(req.url || '/', 'http://localhost').pathname;
507
+ const nsPath = this.ns === '/' ? '/' : this.ns;
508
+ if (nsPath !== '/' && path !== nsPath) {
710
509
  socket.destroy();
711
510
  return;
712
511
  }
713
- if (!this._checkOrigin(req)) {
714
- this.log.warn('Rejected WS: origin not allowed', { origin: req.headers['origin'] });
512
+ if (this.origins && !this.origins.includes(req.headers['origin'] || '')) {
715
513
  socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
716
514
  socket.destroy();
717
515
  return;
718
516
  }
719
517
  const key = req.headers['sec-websocket-key'];
720
518
  if (!key || !validateWSKey(key)) {
721
- this.log.warn('Invalid WebSocket key');
722
519
  socket.destroy();
723
520
  return;
724
521
  }
725
522
  try {
726
- const extraHeaders = {};
523
+ const extra = {};
727
524
  const origin = req.headers['origin'];
728
- if (origin && this.allowedOrigins && this.allowedOrigins.includes(origin)) {
729
- extraHeaders['Access-Control-Allow-Origin'] = origin;
730
- }
731
- socket.write(buildUpgradeResponse(key, extraHeaders));
525
+ if (origin && this.origins?.includes(origin))
526
+ extra['Access-Control-Allow-Origin'] = origin;
527
+ socket.write(buildUpgradeResponse(key, extra));
732
528
  }
733
529
  catch {
734
530
  socket.destroy();
735
531
  return;
736
532
  }
737
- const connectTimer = setTimeout(() => {
738
- if (!this.clients.has(socket)) {
739
- this.log.warn('WS connect timeout');
740
- socket.destroy();
741
- }
742
- }, this.connectTimeout);
743
- connectTimer.unref();
744
- const record = this._registerClient(socket, 'ws', req, new WSFrameParser(this.maxWSFrameSize));
745
- if (!record) {
746
- clearTimeout(connectTimer);
533
+ const timer = setTimeout(() => { if (!this.clients.has(socket))
534
+ socket.destroy(); }, this.connTimeout);
535
+ timer.unref();
536
+ const r = this._register(socket, 'ws', req, new WSFrameParser(this.maxWSFrame));
537
+ if (!r) {
538
+ clearTimeout(timer);
747
539
  return;
748
540
  }
749
- const ctx = this._buildCtx(record, req);
750
- if (this.hooks.onClientConnect) {
751
- this.hooks.onClientConnect({
752
- clientId: record.info.id,
753
- ip: record.info.remoteAddress,
754
- protocol: 'ws',
755
- metadata: record.info.metadata,
756
- });
757
- }
758
- this.runMiddlewares(ctx, () => {
759
- if (this._connectionHandler) {
760
- try {
761
- this._connectionHandler(ctx);
762
- }
763
- catch (err) {
764
- this.log.error('Connection handler error', { error: String(err) });
765
- }
766
- }
767
- });
768
- this.log.info('WS client connected', { clientId: record.info.id, ip: record.info.remoteAddress });
769
- if (head.length > 0) {
770
- this._processWSData(record, head, ctx, req);
771
- }
772
- socket.on('data', (data) => {
773
- clearTimeout(connectTimer);
774
- this._processWSData(record, data, ctx, req);
775
- });
776
- socket.on('close', () => {
777
- clearTimeout(connectTimer);
778
- this.log.debug('WS client socket closed', { clientId: record.info.id });
779
- this._unregisterClient(record, ctx);
780
- });
781
- socket.on('error', (err) => {
782
- this.log.warn('WS client error', { clientId: record.info.id, error: err.message });
783
- this._handleError(record, ctx, err);
784
- });
785
- socket.on('drain', () => {
786
- socket.resume();
787
- });
788
- }
789
- _processWSData(record, data, ctx, req) {
541
+ const ctx = this._buildCtx(r, req);
542
+ this.hooks.onClientConnect?.({ clientId: r.info.id, ip: r.info.remoteAddress, protocol: 'ws', metadata: r.info.metadata });
543
+ this._runMw(ctx, () => { if (this._connH)
544
+ try {
545
+ this._connH(ctx);
546
+ }
547
+ catch (e) {
548
+ this.log.error('Connection handler error', { error: String(e) });
549
+ } });
550
+ this.log.info('WS connected', { clientId: r.info.id, ip: r.info.remoteAddress });
551
+ if (head.length > 0)
552
+ this._processWS(r, head, ctx);
553
+ socket.on('data', (d) => { clearTimeout(timer); this._processWS(r, d, ctx); });
554
+ socket.on('close', () => { clearTimeout(timer); this._unregister(r, ctx); });
555
+ socket.on('error', (e) => { this.log.warn('WS error', { clientId: r.info.id, error: e.message }); this._handleErr(r, ctx, e); });
556
+ socket.on('drain', () => socket.resume());
557
+ }
558
+ _processWS(r, data, ctx) {
790
559
  let frames;
791
560
  try {
792
- frames = record.parser.feed(data);
561
+ frames = r.parser.feed(data);
793
562
  }
794
- catch (err) {
795
- if (err instanceof WebSocketError) {
796
- this.log.warn('WS protocol error', { clientId: record.info.id, code: err.code, message: err.message });
563
+ catch (e) {
564
+ if (e instanceof WebSocketError) {
565
+ this.log.warn('WS protocol error', { code: e.code, message: e.message });
797
566
  try {
798
- record.socket.write(createWSCloseFrame(err.code, err.message));
567
+ r.socket.write(createWSCloseFrame(e.code, e.message));
799
568
  }
800
- catch { /* ignore */ }
801
- }
802
- else {
803
- this.log.error('WS frame parse error', { clientId: record.info.id, error: String(err) });
569
+ catch { }
804
570
  }
805
- record.socket.destroy();
571
+ else
572
+ this.log.error('WS parse error', { error: String(e) });
573
+ r.socket.destroy();
806
574
  return;
807
575
  }
808
- for (const frame of frames) {
809
- if (record.socket.destroyed)
810
- break;
811
- this._handleWSFrame(record, frame, ctx, req);
576
+ for (const f of frames) {
577
+ if (!r.socket.destroyed)
578
+ this._handleWSFrame(r, f, ctx);
812
579
  }
813
580
  }
814
- _handleWSFrame(record, frame, ctx, _req) {
581
+ _handleWSFrame(r, frame, ctx) {
815
582
  const { opcode, payload } = frame;
816
583
  if (opcode === OP_PING) {
817
584
  try {
818
- record.socket.write(createWSPongFrame(payload));
585
+ r.socket.write(createWSPongFrame(payload));
819
586
  }
820
- catch { /* ignore */ }
587
+ catch { }
821
588
  return;
822
589
  }
823
590
  if (opcode === OP_CLOSE) {
824
591
  try {
825
- record.socket.write(createWSCloseFrame(CLOSE_NORMAL));
592
+ r.socket.write(createWSCloseFrame());
826
593
  }
827
- catch { /* ignore */ }
828
- record.socket.end();
594
+ catch { }
595
+ r.socket.end();
829
596
  return;
830
597
  }
831
598
  if (opcode === OP_PONG) {
832
- record.info.lastPing = Date.now();
599
+ r.info.lastPing = Date.now();
833
600
  return;
834
601
  }
835
- if (!this._checkRateLimit(record.info.id)) {
836
- this.log.warn('Rate limit exceeded', { clientId: record.info.id });
837
- if (this.hooks.onRateLimitExceeded) {
838
- const result = this.hooks.onRateLimitExceeded({
839
- clientId: record.info.id,
840
- protocol: 'ws',
841
- });
842
- if (result === false)
843
- return;
844
- }
602
+ if (!this._checkRate(r.info.id)) {
603
+ this.log.warn('Rate limit exceeded', { clientId: r.info.id });
604
+ if (this.hooks.onRateLimitExceeded?.({ clientId: r.info.id, protocol: 'ws' }) === false)
605
+ return;
845
606
  try {
846
- record.socket.write(createWSCloseFrame(CLOSE_POLICY_VIOLATION, 'Rate limit exceeded'));
607
+ r.socket.write(createWSCloseFrame(CLOSE_POLICY_VIOLATION, 'Rate limit exceeded'));
847
608
  }
848
- catch { /* ignore */ }
849
- record.socket.destroy();
609
+ catch { }
610
+ r.socket.destroy();
850
611
  return;
851
612
  }
852
613
  if (opcode === OP_TEXT) {
853
- record.info.messagesReceived++;
854
- this._totalMessagesReceived++;
855
- if (payload.length > this.maxPayloadSize) {
856
- if (this.hooks.onPayloadTooLarge) {
857
- this.hooks.onPayloadTooLarge({ clientId: record.info.id, size: payload.length, max: this.maxPayloadSize });
858
- }
859
- this.log.warn('Payload too large', { clientId: record.info.id, size: payload.length });
614
+ r.info.messagesReceived++;
615
+ this._totalRecv++;
616
+ if (payload.length > this.maxPayload) {
617
+ this.hooks.onPayloadTooLarge?.({ clientId: r.info.id, size: payload.length, max: this.maxPayload });
860
618
  try {
861
- record.socket.write(createWSCloseFrame(CLOSE_MESSAGE_TOO_BIG, 'Payload too large'));
619
+ r.socket.write(createWSCloseFrame(CLOSE_MESSAGE_TOO_BIG));
862
620
  }
863
- catch { /* ignore */ }
864
- record.socket.destroy();
621
+ catch { }
622
+ r.socket.destroy();
865
623
  return;
866
624
  }
867
625
  let msg;
@@ -869,298 +627,190 @@ class StelarServer {
869
627
  msg = JSON.parse(payload.toString('utf8'));
870
628
  }
871
629
  catch {
872
- if (this.hooks.onInvalidMessage) {
873
- this.hooks.onInvalidMessage({ clientId: record.info.id, reason: 'Invalid JSON', protocol: 'ws' });
874
- }
875
- this.log.warn('Invalid JSON from client', { clientId: record.info.id });
630
+ this.hooks.onInvalidMessage?.({ clientId: r.info.id, reason: 'Invalid JSON', protocol: 'ws' });
876
631
  return;
877
632
  }
878
- const event = String(msg.event || '');
879
- const data = msg.data;
633
+ const event = String(msg.event || ''), data = msg.data, corrId = msg._correlationId ? String(msg._correlationId) : undefined;
880
634
  if (!event)
881
635
  return;
882
- if (event && !this._checkRateLimit(record.info.id, event)) {
883
- this.log.warn('Event rate limit exceeded', { clientId: record.info.id, event });
884
- if (this.hooks.onRateLimitExceeded) {
885
- const result = this.hooks.onRateLimitExceeded({
886
- clientId: record.info.id,
887
- event,
888
- protocol: 'ws',
889
- });
890
- if (result === false)
891
- return;
892
- }
636
+ if (!this._checkRate(r.info.id, event)) {
637
+ this.log.warn('Event rate limit', { clientId: r.info.id, event });
638
+ if (this.hooks.onRateLimitExceeded?.({ clientId: r.info.id, event, protocol: 'ws' }) === false)
639
+ return;
893
640
  try {
894
- record.socket.write(createWSCloseFrame(CLOSE_POLICY_VIOLATION, 'Rate limit exceeded'));
641
+ r.socket.write(createWSCloseFrame(CLOSE_POLICY_VIOLATION, 'Rate limit exceeded'));
895
642
  }
896
- catch { /* ignore */ }
897
- record.socket.destroy();
898
- return;
899
- }
900
- if (event === 'pong') {
901
- record.info.lastPing = Date.now();
902
- return;
903
- }
904
- if (event === 'join-room') {
905
- const room = String(data);
906
- if (room)
907
- this._joinRoom(record, room);
908
- return;
909
- }
910
- if (event === 'leave-room') {
911
- const room = String(data);
912
- if (room)
913
- this._leaveRoom(record, room);
643
+ catch { }
644
+ r.socket.destroy();
914
645
  return;
915
646
  }
916
647
  if (msg._ackName && this._acks.has(String(msg._ackName))) {
917
- const ackName = String(msg._ackName);
918
- const ackHandler = this._acks.get(ackName);
919
- try {
920
- const result = ackHandler({ ...ctx, data });
921
- if (result !== undefined) {
922
- record.socket.write(createWSTextFrame(JSON.stringify({ event: ackName, data: result, _isAck: true })));
923
- this._totalMessagesSent++;
924
- }
925
- }
926
- catch (err) {
927
- this.log.error('ACK handler error', { ackName, error: String(err) });
928
- }
929
- return;
930
- }
931
- const eventCtx = { ...ctx, data, event };
932
- const handler = this.events.get(event);
933
- if (handler) {
648
+ const name = String(msg._ackName), h = this._acks.get(name);
649
+ let res;
934
650
  try {
935
- handler(eventCtx);
651
+ res = h({ ...ctx, data, _correlationId: corrId });
936
652
  }
937
- catch (err) {
938
- this.log.error('Event handler error', { event, error: String(err) });
939
- }
940
- }
941
- if (this._wildcardHandler) {
942
- try {
943
- this._wildcardHandler({ event, data: eventCtx });
653
+ catch (e) {
654
+ this.log.error('ACK handler error', { name, error: String(e) });
655
+ return;
944
656
  }
945
- catch (err) {
946
- this.log.error('Wildcard handler error', { event, error: String(err) });
657
+ if (res !== undefined) {
658
+ const p = { event: name, data: res, _isAck: true };
659
+ if (corrId)
660
+ p._correlationId = corrId;
661
+ try {
662
+ r.socket.write(createWSTextFrame(JSON.stringify(p)));
663
+ this._totalSent++;
664
+ }
665
+ catch { }
947
666
  }
667
+ return;
948
668
  }
949
- return;
669
+ this._dispatch(r, ctx, event, data, corrId);
950
670
  }
951
671
  if (opcode === OP_BINARY) {
952
- record.info.messagesReceived++;
953
- this._totalMessagesReceived++;
954
- if (payload.length > this.maxPayloadSize) {
955
- if (this.hooks.onPayloadTooLarge) {
956
- this.hooks.onPayloadTooLarge({ clientId: record.info.id, size: payload.length, max: this.maxPayloadSize });
957
- }
958
- this.log.warn('Binary payload too large', { clientId: record.info.id, size: payload.length });
672
+ r.info.messagesReceived++;
673
+ this._totalRecv++;
674
+ if (payload.length > this.maxPayload) {
675
+ this.hooks.onPayloadTooLarge?.({ clientId: r.info.id, size: payload.length, max: this.maxPayload });
959
676
  return;
960
677
  }
961
678
  try {
962
- let headerEnd = -1;
963
- for (let i = 0; i < payload.length; i++) {
679
+ let end = -1;
680
+ for (let i = 0; i < payload.length; i++)
964
681
  if (payload[i] === 0) {
965
- headerEnd = i;
682
+ end = i;
966
683
  break;
967
684
  }
968
- }
969
- if (headerEnd === -1)
685
+ if (end === -1)
970
686
  return;
971
- const headerStr = payload.subarray(0, headerEnd).toString('utf8');
972
- const header = JSON.parse(headerStr);
973
- const buffer = payload.subarray(headerEnd + 1);
974
- if (header.event && !this._checkRateLimit(record.info.id, header.event)) {
975
- this.log.warn('Binary event rate limit exceeded', { clientId: record.info.id, event: header.event });
976
- if (this.hooks.onRateLimitExceeded) {
977
- const result = this.hooks.onRateLimitExceeded({ clientId: record.info.id, event: header.event, protocol: 'ws' });
978
- if (result === false)
979
- return;
980
- }
687
+ const hdr = JSON.parse(payload.subarray(0, end).toString('utf8'));
688
+ const buf = payload.subarray(end + 1);
689
+ if (hdr.event && !this._checkRate(r.info.id, hdr.event)) {
690
+ this.log.warn('Binary rate limit', { clientId: r.info.id, event: hdr.event });
691
+ if (this.hooks.onRateLimitExceeded?.({ clientId: r.info.id, event: hdr.event, protocol: 'ws' }) === false)
692
+ return;
981
693
  return;
982
694
  }
983
- const eventCtx = { ...ctx, data: buffer, buffer, isBinary: true, event: header.event };
984
- const handler = this.events.get(header.event);
985
- if (handler) {
695
+ const ectx = { ...ctx, data: buf, buffer: buf, isBinary: true, event: hdr.event };
696
+ const h = this.events.get(hdr.event);
697
+ if (h)
986
698
  try {
987
- handler(eventCtx);
988
- }
989
- catch (err) {
990
- this.log.error('Binary handler error', { error: String(err) });
699
+ h(ectx);
991
700
  }
992
- }
993
- if (this._wildcardHandler) {
701
+ catch { }
702
+ if (this._wild)
994
703
  try {
995
- this._wildcardHandler({ event: header.event, data: eventCtx });
996
- }
997
- catch (err) {
998
- this.log.error('Wildcard handler error', { error: String(err) });
704
+ this._wild({ event: hdr.event, data: ectx });
999
705
  }
1000
- }
706
+ catch { }
1001
707
  }
1002
708
  catch {
1003
- if (this.hooks.onInvalidMessage) {
1004
- this.hooks.onInvalidMessage({ clientId: record.info.id, reason: 'Invalid binary frame', protocol: 'ws' });
1005
- }
1006
- this.log.warn('Invalid binary frame from client', { clientId: record.info.id });
709
+ this.hooks.onInvalidMessage?.({ clientId: r.info.id, reason: 'Invalid binary frame', protocol: 'ws' });
1007
710
  }
1008
711
  }
1009
712
  }
1010
- handleTCPConnection(socket) {
1011
- const record = this._registerClient(socket, 'tcp', null, new FrameParser(this.maxFrameSize));
1012
- if (!record)
713
+ /* ── Private: TCP connection ── */
714
+ _tcpConnect(socket) {
715
+ const r = this._register(socket, 'tcp', null, new FrameParser(this.maxFrame));
716
+ if (!r)
1013
717
  return;
1014
- const ctx = this._buildCtx(record, null);
718
+ const ctx = this._buildCtx(r, null);
1015
719
  try {
1016
- socket.write(encodeConnectFrame(record.info.id));
720
+ socket.write(encodeConnectFrame(r.info.id));
1017
721
  }
1018
722
  catch {
1019
723
  socket.destroy();
1020
724
  return;
1021
725
  }
1022
- if (this.hooks.onClientConnect) {
1023
- this.hooks.onClientConnect({
1024
- clientId: record.info.id,
1025
- ip: record.info.remoteAddress,
1026
- protocol: 'tcp',
1027
- metadata: record.info.metadata,
1028
- });
1029
- }
1030
- this.runMiddlewares(ctx, () => {
1031
- if (this._connectionHandler) {
1032
- try {
1033
- this._connectionHandler(ctx);
1034
- }
1035
- catch (err) {
1036
- this.log.error('TCP connection handler error', { error: String(err) });
1037
- }
1038
- }
1039
- });
1040
- this.log.info('TCP client connected', { clientId: record.info.id, ip: record.info.remoteAddress });
1041
- socket.on('data', (data) => {
1042
- this._processTCPData(record, data, ctx);
1043
- });
1044
- socket.on('close', () => {
1045
- this.log.debug('TCP client socket closed', { clientId: record.info.id });
1046
- this._unregisterClient(record, ctx);
1047
- });
1048
- socket.on('error', (err) => {
1049
- this.log.warn('TCP client error', { clientId: record.info.id, error: err.message });
1050
- this._handleError(record, ctx, err);
1051
- });
1052
- socket.on('drain', () => {
1053
- socket.resume();
1054
- });
1055
- }
1056
- _processTCPData(record, data, ctx) {
726
+ this.hooks.onClientConnect?.({ clientId: r.info.id, ip: r.info.remoteAddress, protocol: 'tcp', metadata: r.info.metadata });
727
+ this._runMw(ctx, () => { if (this._connH)
728
+ try {
729
+ this._connH(ctx);
730
+ }
731
+ catch (e) {
732
+ this.log.error('TCP connection handler error', { error: String(e) });
733
+ } });
734
+ this.log.info('TCP connected', { clientId: r.info.id, ip: r.info.remoteAddress });
735
+ socket.on('data', (d) => this._processTCP(r, d, ctx));
736
+ socket.on('close', () => this._unregister(r, ctx));
737
+ socket.on('error', (e) => { this.log.warn('TCP error', { clientId: r.info.id, error: e.message }); this._handleErr(r, ctx, e); });
738
+ socket.on('drain', () => socket.resume());
739
+ }
740
+ _processTCP(r, data, ctx) {
1057
741
  let frames;
1058
742
  try {
1059
- frames = record.parser.feed(data);
743
+ frames = r.parser.feed(data);
1060
744
  }
1061
- catch (err) {
1062
- if (err instanceof ProtocolError) {
1063
- this.log.warn('TCP protocol error', { clientId: record.info.id, code: err.code, message: err.message });
745
+ catch (e) {
746
+ if (e instanceof ProtocolError) {
747
+ this.log.warn('TCP protocol error', { code: e.code, message: e.message });
1064
748
  try {
1065
- record.socket.write(encodeErrorFrame(err.message));
749
+ r.socket.write(encodeErrorFrame(e.message));
1066
750
  }
1067
- catch { /* ignore */ }
751
+ catch { }
1068
752
  }
1069
- record.socket.destroy();
753
+ r.socket.destroy();
1070
754
  return;
1071
755
  }
1072
- for (const frame of frames) {
1073
- if (record.socket.destroyed)
1074
- break;
1075
- this._handleTCPFrame(record, frame, ctx);
756
+ for (const f of frames) {
757
+ if (!r.socket.destroyed)
758
+ this._handleTCPFrame(r, f, ctx);
1076
759
  }
1077
760
  }
1078
- _handleTCPFrame(record, frame, ctx) {
761
+ _handleTCPFrame(r, frame, ctx) {
1079
762
  const { type, event, payload } = frame;
1080
763
  if (type === FRAME_PING) {
1081
764
  try {
1082
- record.socket.write(encodePongFrame());
765
+ r.socket.write(encodePongFrame());
1083
766
  }
1084
- catch { /* ignore */ }
1085
- record.info.lastPing = Date.now();
767
+ catch { }
768
+ r.info.lastPing = Date.now();
1086
769
  return;
1087
770
  }
1088
771
  if (type === FRAME_PONG) {
1089
- record.info.lastPing = Date.now();
772
+ r.info.lastPing = Date.now();
1090
773
  return;
1091
774
  }
1092
- if (!this._checkRateLimit(record.info.id, event)) {
1093
- this.log.warn('TCP rate limit exceeded', { clientId: record.info.id, event });
1094
- if (this.hooks.onRateLimitExceeded) {
1095
- const result = this.hooks.onRateLimitExceeded({
1096
- clientId: record.info.id,
1097
- event: event || undefined,
1098
- protocol: 'tcp',
1099
- });
1100
- if (result === false)
1101
- return;
1102
- }
775
+ if (type === FRAME_CONNECT)
776
+ return;
777
+ if (!this._checkRate(r.info.id, event)) {
778
+ this.log.warn('TCP rate limit', { clientId: r.info.id, event });
779
+ if (this.hooks.onRateLimitExceeded?.({ clientId: r.info.id, event: event || undefined, protocol: 'tcp' }) === false)
780
+ return;
1103
781
  try {
1104
- record.socket.write(encodeErrorFrame('Rate limit exceeded'));
782
+ r.socket.write(encodeErrorFrame('Rate limit exceeded'));
1105
783
  }
1106
- catch { /* ignore */ }
1107
- record.socket.destroy();
784
+ catch { }
785
+ r.socket.destroy();
1108
786
  return;
1109
787
  }
1110
788
  if (type === FRAME_JOIN) {
1111
- const room = payload.toString('utf8');
1112
- if (room)
1113
- this._joinRoom(record, room);
789
+ if (payload.toString('utf8'))
790
+ this._joinRoom(r, payload.toString('utf8'));
1114
791
  return;
1115
792
  }
1116
793
  if (type === FRAME_LEAVE) {
1117
- const room = payload.toString('utf8');
1118
- if (room)
1119
- this._leaveRoom(record, room);
794
+ if (payload.toString('utf8'))
795
+ this._leaveRoom(r, payload.toString('utf8'));
1120
796
  return;
1121
797
  }
1122
- if (type === FRAME_CONNECT) {
798
+ if (payload.length > this.maxPayload) {
799
+ this.hooks.onPayloadTooLarge?.({ clientId: r.info.id, event, size: payload.length, max: this.maxPayload });
1123
800
  return;
1124
801
  }
1125
- if (payload.length > this.maxPayloadSize) {
1126
- if (this.hooks.onPayloadTooLarge) {
1127
- this.hooks.onPayloadTooLarge({ clientId: record.info.id, event, size: payload.length, max: this.maxPayloadSize });
1128
- }
1129
- this.log.warn('TCP payload too large', { clientId: record.info.id, size: payload.length });
1130
- return;
1131
- }
1132
- record.info.messagesReceived++;
1133
- this._totalMessagesReceived++;
802
+ r.info.messagesReceived++;
803
+ this._totalRecv++;
1134
804
  if (type === FRAME_JSON) {
1135
805
  let data;
1136
806
  try {
1137
807
  data = JSON.parse(payload.toString('utf8'));
1138
808
  }
1139
809
  catch {
1140
- if (this.hooks.onInvalidMessage) {
1141
- this.hooks.onInvalidMessage({ clientId: record.info.id, reason: 'Invalid JSON', protocol: 'tcp' });
1142
- }
1143
- this.log.warn('Invalid TCP JSON', { clientId: record.info.id });
810
+ this.hooks.onInvalidMessage?.({ clientId: r.info.id, reason: 'Invalid JSON', protocol: 'tcp' });
1144
811
  return;
1145
812
  }
1146
- const eventCtx = { ...ctx, data, event };
1147
- const handler = this.events.get(event);
1148
- if (handler) {
1149
- try {
1150
- handler(eventCtx);
1151
- }
1152
- catch (err) {
1153
- this.log.error('TCP event handler error', { event, error: String(err) });
1154
- }
1155
- }
1156
- if (this._wildcardHandler) {
1157
- try {
1158
- this._wildcardHandler({ event, data: eventCtx });
1159
- }
1160
- catch (err) {
1161
- this.log.error('TCP wildcard handler error', { error: String(err) });
1162
- }
1163
- }
813
+ this._dispatch(r, ctx, event, data);
1164
814
  return;
1165
815
  }
1166
816
  if (type === FRAME_ACK_REQ) {
@@ -1168,80 +818,74 @@ class StelarServer {
1168
818
  try {
1169
819
  const parsed = JSON.parse(payload.toString('utf8'));
1170
820
  const data = parsed && typeof parsed === 'object' && 'data' in parsed ? parsed.data : parsed;
1171
- const ackHandler = this._acks.get(event);
1172
- const result = ackHandler({ ...ctx, data });
1173
- if (result !== undefined) {
1174
- record.socket.write(encodeAckResFrame(event, result, this.maxFrameSize));
1175
- this._totalMessagesSent++;
821
+ const corrId = parsed && typeof parsed === 'object' && '_correlationId' in parsed ? String(parsed._correlationId) : undefined;
822
+ const h = this._acks.get(event);
823
+ const res = h({ ...ctx, data, _correlationId: corrId });
824
+ if (res !== undefined) {
825
+ r.socket.write(corrId ? encodeAckResFrame(event, { data: res, _correlationId: corrId }, this.maxFrame) : encodeAckResFrame(event, res, this.maxFrame));
826
+ this._totalSent++;
1176
827
  }
1177
828
  }
1178
- catch (err) {
1179
- this.log.error('TCP ACK handler error', { event, error: String(err) });
829
+ catch (e) {
830
+ this.log.error('TCP ACK handler error', { event, error: String(e) });
1180
831
  }
1181
832
  }
1182
833
  return;
1183
834
  }
1184
835
  if (type === FRAME_ACK_RES) {
1185
- if (this._acks.has(event)) {
1186
- try {
1187
- const data = JSON.parse(payload.toString('utf8'));
1188
- const ackHandler = this._acks.get(event);
1189
- ackHandler({ ...ctx, data });
1190
- }
1191
- catch { /* ignore */ }
836
+ try {
837
+ const raw = JSON.parse(payload.toString('utf8'));
838
+ const data = raw && typeof raw === 'object' && 'data' in raw ? raw.data : raw;
839
+ const corrId = raw && typeof raw === 'object' && '_correlationId' in raw ? String(raw._correlationId) : undefined;
840
+ const h = this._acks.get(corrId || event);
841
+ if (h)
842
+ try {
843
+ h({ ...ctx, data });
844
+ }
845
+ catch { }
1192
846
  }
847
+ catch { }
1193
848
  return;
1194
849
  }
1195
850
  if (type === FRAME_BINARY) {
1196
- const eventCtx = { ...ctx, data: payload, buffer: payload, isBinary: true, event };
1197
- const handler = this.events.get(event);
1198
- if (handler) {
851
+ const ectx = { ...ctx, data: payload, buffer: payload, isBinary: true, event };
852
+ const h = this.events.get(event);
853
+ if (h)
1199
854
  try {
1200
- handler(eventCtx);
1201
- }
1202
- catch (err) {
1203
- this.log.error('TCP binary handler error', { event, error: String(err) });
855
+ h(ectx);
1204
856
  }
1205
- }
1206
- if (this._wildcardHandler) {
857
+ catch { }
858
+ if (this._wild)
1207
859
  try {
1208
- this._wildcardHandler({ event, data: eventCtx });
1209
- }
1210
- catch (err) {
1211
- this.log.error('TCP wildcard handler error', { error: String(err) });
860
+ this._wild({ event, data: ectx });
1212
861
  }
1213
- }
1214
- return;
862
+ catch { }
1215
863
  }
1216
864
  }
1217
- _handleError(record, ctx, err) {
1218
- if (this.events.has('error')) {
1219
- const handler = this.events.get('error');
865
+ _handleErr(r, ctx, err) {
866
+ const h = this.events.get('error');
867
+ if (h)
1220
868
  try {
1221
- handler({ ...ctx, error: err, event: 'error' });
1222
- }
1223
- catch (handlerErr) {
1224
- this.log.error('Error handler threw', { error: String(handlerErr) });
869
+ h({ ...ctx, error: err, event: 'error' });
1225
870
  }
1226
- }
871
+ catch { }
1227
872
  }
1228
- _handleHealthCheck(req, res) {
1229
- if (this._customHealthHandler) {
1230
- const stats = this.getStats();
873
+ /* ── Private: health check ── */
874
+ _health(req, res) {
875
+ if (this._healthFn) {
1231
876
  try {
1232
- this._customHealthHandler(req, res, stats);
877
+ this._healthFn(req, res, this.getStats());
1233
878
  }
1234
- catch (err) {
1235
- this.log.error('Custom health handler error', { error: String(err) });
879
+ catch {
1236
880
  if (!res.headersSent) {
1237
- res.writeHead(500, { 'Content-Type': 'application/json' });
1238
- res.end(JSON.stringify({ status: 'error', message: 'Health check handler failed' }));
881
+ res.writeHead(500);
882
+ res.end('{"status":"error"}');
1239
883
  }
1240
884
  }
1241
885
  return;
1242
886
  }
1243
887
  const origin = req.headers['origin'];
1244
- if (origin && (!this.allowedOrigins || this.allowedOrigins.includes(origin))) {
888
+ if (origin && (!this.origins || this.origins.includes(origin))) {
1245
889
  res.setHeader('Access-Control-Allow-Origin', origin);
1246
890
  res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
1247
891
  res.setHeader('Access-Control-Max-Age', '86400');
@@ -1251,267 +895,198 @@ class StelarServer {
1251
895
  res.end();
1252
896
  return;
1253
897
  }
1254
- if (this.healthEndpoint && req.url === this.healthEndpoint && req.method === 'GET') {
1255
- const stats = this.getStats();
898
+ if (this.healthPath && req.url === this.healthPath && req.method === 'GET') {
899
+ const s = this.getStats();
1256
900
  res.writeHead(200, { 'Content-Type': 'application/json' });
1257
- res.end(JSON.stringify({
1258
- status: 'ok',
1259
- ...stats,
1260
- uptimeSeconds: Math.floor(stats.uptime / 1000),
1261
- memoryMB: Math.round(stats.memoryUsage.heapUsed / 1024 / 1024 * 100) / 100,
1262
- }));
901
+ res.end(JSON.stringify({ status: 'ok', ...s, uptimeSeconds: Math.floor(s.uptime / 1000), memoryMB: Math.round(s.memoryUsage.heapUsed / 1024 / 1024 * 100) / 100 }));
1263
902
  return;
1264
903
  }
1265
904
  res.writeHead(200, { 'Content-Type': 'text/plain' });
1266
905
  res.end('Stelar Time Real v3 Server');
1267
906
  }
1268
- /** Register a callback for when graceful shutdown completes. */
1269
- onShutdown(callback) {
1270
- this._shutdownCallbacks.push(callback);
1271
- return this;
1272
- }
1273
- _emitShutdown(signal, force) {
1274
- for (const cb of this._shutdownCallbacks) {
1275
- try {
1276
- cb(signal, force);
1277
- }
1278
- catch { /* ignore */ }
1279
- }
1280
- if (this._shutdownCallbacks.length === 0) {
907
+ /* ── Private: graceful shutdown ── */
908
+ _emitShutdown(sig, force) {
909
+ if (!this._shutdownCbs.length) {
1281
910
  process.exit(force ? 1 : 0);
911
+ return;
1282
912
  }
913
+ for (const cb of this._shutdownCbs)
914
+ try {
915
+ cb(sig, force);
916
+ }
917
+ catch { }
1283
918
  }
1284
- _setupGracefulShutdown() {
1285
- if (!this.doGracefulShutdown)
919
+ _setupShutdown() {
920
+ if (!this.doGraceful)
1286
921
  return;
1287
- let isShuttingDown = false;
1288
- const shutdown = (signal) => {
1289
- if (isShuttingDown)
922
+ let done = false;
923
+ const shutdown = (sig) => {
924
+ if (done)
1290
925
  return;
1291
- isShuttingDown = true;
1292
- this._shuttingDown = true;
1293
- this.log.info(`Received ${signal}, shutting down gracefully...`);
926
+ done = true;
927
+ this._shutting = true;
928
+ this.log.info(`Received ${sig}, shutting down...`);
1294
929
  this.stop();
1295
- const clientCount = this.clients.size;
1296
- if (clientCount === 0) {
1297
- this.log.info('No active connections, shutdown complete');
1298
- this._emitShutdown(signal, false);
930
+ if (!this.clients.size) {
931
+ this.log.info('Shutdown complete');
932
+ this._emitShutdown(sig, false);
1299
933
  return;
1300
934
  }
1301
- this.log.info(`Waiting for ${clientCount} connections to close (timeout: ${this.shutdownTimeout}ms)`);
1302
- this.clients.forEach((record) => {
1303
- try {
1304
- if (record.protocol === 'ws') {
1305
- record.socket.write(createWSCloseFrame(CLOSE_GOING_AWAY, 'Server shutting down'));
1306
- }
1307
- else {
1308
- record.socket.write(encodeDisconnectFrame());
1309
- }
1310
- record.socket.end();
1311
- }
1312
- catch { /* ignore */ }
1313
- });
1314
- const forceTimeout = setTimeout(() => {
1315
- this.log.warn('Shutdown timeout reached, force closing remaining connections');
1316
- this.clients.forEach((record) => {
1317
- try {
1318
- record.socket.destroy();
1319
- }
1320
- catch { /* ignore */ }
1321
- });
1322
- this.clients.clear();
1323
- this.clientsById.clear();
1324
- this._emitShutdown(signal, true);
1325
- }, this.shutdownTimeout);
1326
- forceTimeout.unref();
1327
- const checkInterval = setInterval(() => {
1328
- if (this.clients.size === 0) {
1329
- clearInterval(checkInterval);
1330
- clearTimeout(forceTimeout);
1331
- this.log.info('All connections closed, shutdown complete');
1332
- this._emitShutdown(signal, false);
1333
- }
1334
- }, 100);
1335
- checkInterval.unref();
935
+ this.log.info(`Waiting for ${this.clients.size} connections (timeout: ${this.shutdownMs}ms)`);
936
+ this.clients.forEach(r => { try {
937
+ r.socket.write(r.protocol === 'ws' ? createWSCloseFrame(CLOSE_GOING_AWAY, 'Shutting down') : encodeDisconnectFrame());
938
+ r.socket.end();
939
+ }
940
+ catch { } });
941
+ const forceT = setTimeout(() => { this.clients.forEach(r => { try {
942
+ r.socket.destroy();
943
+ }
944
+ catch { } }); this.clients.clear(); this.byId.clear(); this._emitShutdown(sig, true); }, this.shutdownMs);
945
+ forceT.unref();
946
+ const check = setInterval(() => { if (!this.clients.size) {
947
+ clearInterval(check);
948
+ clearTimeout(forceT);
949
+ this._emitShutdown(sig, false);
950
+ } }, 100);
951
+ check.unref();
1336
952
  };
1337
- this._sigintHandler = () => shutdown('SIGINT');
1338
- this._sigtermHandler = () => shutdown('SIGTERM');
1339
- process.on('SIGINT', this._sigintHandler);
1340
- process.on('SIGTERM', this._sigtermHandler);
953
+ this._sigH.int = () => shutdown('SIGINT');
954
+ this._sigH.term = () => shutdown('SIGTERM');
955
+ process.on('SIGINT', this._sigH.int);
956
+ process.on('SIGTERM', this._sigH.term);
1341
957
  }
1342
- _removeSignalHandlers() {
1343
- if (this._sigintHandler) {
1344
- process.off('SIGINT', this._sigintHandler);
1345
- this._sigintHandler = null;
958
+ _removeSignals() {
959
+ if (this._sigH.int) {
960
+ process.off('SIGINT', this._sigH.int);
961
+ this._sigH.int = null;
1346
962
  }
1347
- if (this._sigtermHandler) {
1348
- process.off('SIGTERM', this._sigtermHandler);
1349
- this._sigtermHandler = null;
963
+ if (this._sigH.term) {
964
+ process.off('SIGTERM', this._sigH.term);
965
+ this._sigH.term = null;
1350
966
  }
1351
967
  }
1352
- start(callback) {
968
+ /* ── Start / Stop ── */
969
+ start(cb) {
1353
970
  if (this._started) {
1354
- const port = this.getPort();
1355
- if (callback)
1356
- callback(port);
1357
- return Promise.resolve(port);
971
+ const p = this.getPort();
972
+ cb?.(p);
973
+ return Promise.resolve(p);
1358
974
  }
1359
975
  this._started = true;
1360
976
  this._startTime = Date.now();
1361
- return new Promise((resolve) => {
1362
- const startHttpServer = (httpServer) => {
1363
- this.httpServer = httpServer;
1364
- this._requestHandler = (req, res) => {
1365
- this._handleHealthCheck(req, res);
1366
- };
1367
- httpServer.on('request', this._requestHandler);
1368
- this._upgradeHandler = (req, socket, head) => {
1369
- this.handleWSUpgrade(req, socket, head);
1370
- };
1371
- httpServer.on('upgrade', this._upgradeHandler);
1372
- this.startHeartbeat();
1373
- this._rateCleanupTimer = setInterval(() => {
1374
- if (this._customRateLimiter) {
1375
- this._customRateLimiter.cleanup();
1376
- }
1377
- else if (this.rateLimiter) {
1378
- this.rateLimiter.cleanup();
1379
- }
1380
- const ipTracker = this._customIPTracker || this.ipTracker;
1381
- ipTracker.cleanup();
1382
- for (const [clientId, limiter] of this._clientRateOverrides) {
1383
- limiter.cleanup();
1384
- if (!this.clientsById.has(clientId)) {
1385
- this._clientRateOverrides.delete(clientId);
1386
- }
1387
- }
1388
- for (const [, limiter] of this.eventRateLimiters) {
1389
- limiter.cleanup();
977
+ return new Promise(resolve => {
978
+ const onHttp = (srv) => {
979
+ this.httpServer = srv;
980
+ this._reqH = (req, res) => this._health(req, res);
981
+ this._upgH = (req, socket, head) => this._wsUpgrade(req, socket, head);
982
+ srv.on('request', this._reqH);
983
+ srv.on('upgrade', this._upgH);
984
+ this._startHeartbeat();
985
+ this._rc = setInterval(() => {
986
+ if (this._crl)
987
+ this._crl.cleanup();
988
+ else
989
+ this.rateLimiter?.cleanup();
990
+ (this._cit || this.ipTracker).cleanup();
991
+ for (const [id, l] of this.clientRates) {
992
+ l.cleanup();
993
+ if (!this.byId.has(id))
994
+ this.clientRates.delete(id);
1390
995
  }
996
+ for (const [, l] of this.evRateLimits)
997
+ l.cleanup();
1391
998
  }, 30000);
1392
- if (this._rateCleanupTimer && typeof this._rateCleanupTimer === 'object' && 'unref' in this._rateCleanupTimer) {
1393
- this._rateCleanupTimer.unref();
1394
- }
1395
- this._setupGracefulShutdown();
1396
- const finalPort = this.getPort();
1397
- this.log.info('Server started', { port: finalPort, namespace: this.namespace, tls: !!this.tlsOptions });
1398
- if (callback)
1399
- callback(finalPort);
1400
- resolve(finalPort);
999
+ this._rc?.unref?.();
1000
+ this._setupShutdown();
1001
+ const p = this.getPort();
1002
+ this.log.info('Server started', { port: p, namespace: this.ns, tls: !!this.tlsOpts });
1003
+ cb?.(p);
1004
+ resolve(p);
1401
1005
  };
1402
1006
  if (this.httpServer) {
1403
- this._externalServers.add(this.httpServer);
1404
- startHttpServer(this.httpServer);
1007
+ this._ext.add(this.httpServer);
1008
+ onHttp(this.httpServer);
1405
1009
  }
1406
1010
  else {
1407
1011
  const tryListen = (port) => {
1408
- const httpServer = this.tlsOptions
1409
- ? createHttpServer()
1410
- : createHttpServer();
1411
- httpServer.on('error', (err) => {
1412
- if (err.code === 'EADDRINUSE' && port < 65535) {
1413
- tryListen(port + 1);
1414
- }
1415
- else {
1416
- this.log.error('HTTP server error', { error: err.message });
1417
- }
1418
- });
1419
- httpServer.listen(port, () => {
1420
- this.port = port;
1421
- startHttpServer(httpServer);
1422
- });
1012
+ const srv = createHttp();
1013
+ srv.on('error', (e) => { if (e.code === 'EADDRINUSE' && port < 65535)
1014
+ tryListen(port + 1);
1015
+ else
1016
+ this.log.error('HTTP error', { error: e.message }); });
1017
+ srv.listen(port, () => { this.port = port; onHttp(srv); });
1423
1018
  };
1424
1019
  tryListen(this.port);
1425
1020
  }
1426
1021
  if (this.tcpPort !== false) {
1427
- const tcpPortNum = typeof this.tcpPort === 'number' ? this.tcpPort : this.port + 1;
1428
- this._startTCPServer(tcpPortNum);
1022
+ const p = typeof this.tcpPort === 'number' ? this.tcpPort : this.port + 1;
1023
+ this._startTCP(p);
1429
1024
  }
1430
1025
  });
1431
1026
  }
1432
- _startTCPServer(port, attempts = 0) {
1433
- const tcpHandler = (socket) => this.handleTCPConnection(socket);
1434
- if (this.tlsOptions) {
1027
+ _startTCP(port, attempts = 0) {
1028
+ const handler = (s) => this._tcpConnect(s);
1029
+ const startPlain = (p, a) => {
1030
+ const srv = createTcp(handler);
1031
+ srv.on('error', (e) => { if (e.code === 'EADDRINUSE' && a < 10) {
1032
+ this.tcpServer = null;
1033
+ this._startTCP(p + 1, a + 1);
1034
+ }
1035
+ else
1036
+ this.log.error('TCP error', { error: e.message }); });
1037
+ srv.listen(p, () => { this.tcpServer = srv; this.log.info('TCP started', { port: p }); });
1038
+ };
1039
+ if (this.tlsOpts) {
1435
1040
  try {
1436
- const tlsServer = createTlsServer(this.tlsOptions, tcpHandler);
1437
- this.tcpServer = tlsServer;
1438
- this.tcpServer.on('error', (err) => {
1439
- if (err.code === 'EADDRINUSE' && attempts < 10) {
1440
- this.log.info(`TLS TCP port ${port} in use, trying ${port + 1}`);
1441
- this.tcpServer = null;
1442
- this._startTCPServer(port + 1, attempts + 1);
1443
- }
1444
- else {
1445
- this.log.error('TLS TCP server error', { error: err.message });
1446
- }
1447
- });
1448
- this.tcpServer.listen(port, () => {
1449
- this.log.info('TLS TCP server started', { port });
1450
- });
1041
+ const srv = createTls(this.tlsOpts, handler);
1042
+ this.tcpServer = srv;
1043
+ this.tcpServer.on('error', (e) => { if (e.code === 'EADDRINUSE' && attempts < 10) {
1044
+ this.tcpServer = null;
1045
+ this._startTCP(port + 1, attempts + 1);
1046
+ }
1047
+ else
1048
+ this.log.error('TLS TCP error', { error: e.message }); });
1049
+ this.tcpServer.listen(port, () => this.log.info('TLS TCP started', { port }));
1451
1050
  }
1452
- catch (err) {
1453
- this.log.error('Failed to create TLS TCP server', { error: String(err) });
1454
- this._startPlainTCPServer(port, attempts, tcpHandler);
1051
+ catch {
1052
+ startPlain(port, attempts);
1455
1053
  }
1456
1054
  }
1457
- else {
1458
- this._startPlainTCPServer(port, attempts, tcpHandler);
1459
- }
1460
- }
1461
- _startPlainTCPServer(port, attempts, tcpHandler) {
1462
- this.tcpServer = createTcpServer(tcpHandler);
1463
- this.tcpServer.on('error', (err) => {
1464
- if (err.code === 'EADDRINUSE' && attempts < 10) {
1465
- this.log.info(`TCP port ${port} in use, trying ${port + 1}`);
1466
- this.tcpServer = null;
1467
- this._startTCPServer(port + 1, attempts + 1);
1468
- }
1469
- else {
1470
- this.log.error('TCP server error', { error: err.message });
1471
- }
1472
- });
1473
- this.tcpServer.listen(port, () => {
1474
- this.log.info('TCP server started', { port });
1475
- });
1055
+ else
1056
+ startPlain(port, attempts);
1476
1057
  }
1477
1058
  stop() {
1478
- if (this._hbTimer) {
1479
- clearInterval(this._hbTimer);
1480
- this._hbTimer = null;
1059
+ if (this._hb) {
1060
+ clearInterval(this._hb);
1061
+ this._hb = null;
1481
1062
  }
1482
- if (this._rateCleanupTimer) {
1483
- clearInterval(this._rateCleanupTimer);
1484
- this._rateCleanupTimer = null;
1063
+ if (this._rc) {
1064
+ clearInterval(this._rc);
1065
+ this._rc = null;
1485
1066
  }
1486
- this.clients.forEach((record) => {
1487
- if (!record.socket.destroyed) {
1488
- record.socket.destroy();
1489
- }
1490
- });
1067
+ this.clients.forEach(r => { if (!r.socket.destroyed)
1068
+ r.socket.destroy(); });
1491
1069
  this.clients.clear();
1492
- this.clientsById.clear();
1070
+ this.byId.clear();
1493
1071
  this.rooms.clear();
1494
- this._clientRateOverrides.clear();
1072
+ this.clientRates.clear();
1495
1073
  if (this.httpServer) {
1496
- if (this._upgradeHandler) {
1497
- this.httpServer.off('upgrade', this._upgradeHandler);
1498
- this._upgradeHandler = null;
1499
- }
1500
- if (this._requestHandler) {
1501
- this.httpServer.off('request', this._requestHandler);
1502
- this._requestHandler = null;
1503
- }
1504
- if (!this._externalServers.has(this.httpServer)) {
1074
+ if (this._upgH)
1075
+ this.httpServer.off('upgrade', this._upgH);
1076
+ if (this._reqH)
1077
+ this.httpServer.off('request', this._reqH);
1078
+ if (!this._ext.has(this.httpServer))
1505
1079
  this.httpServer.close();
1506
- }
1507
1080
  this.httpServer = null;
1081
+ this._upgH = null;
1082
+ this._reqH = null;
1508
1083
  }
1509
1084
  if (this.tcpServer) {
1510
1085
  this.tcpServer.close();
1511
1086
  this.tcpServer = null;
1512
1087
  }
1513
1088
  this._started = false;
1514
- this._removeSignalHandlers();
1089
+ this._removeSignals();
1515
1090
  this.log.info('Server stopped');
1516
1091
  return this;
1517
1092
  }