stelar-time-real 3.3.0 → 3.3.2

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": "stelar-time-real",
3
- "version": "3.3.0",
3
+ "version": "3.3.2",
4
4
  "description": "Zero-dependency production real-time library. Custom binary TCP + manual WebSocket. No ws package. Rate limiting (custom/per-event/per-client), hooks, custom IP tracker, custom health check, runtime config, rooms, ACKs, middleware, TLS/SSL.",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
package/src/client.d.ts CHANGED
@@ -1,6 +1,4 @@
1
- /**
2
- * @stelar-time-real Client — Browser WS / Node WS / binary TCP
3
- */
1
+ /** @stelar-time-real Client — Browser WS / Node WS / binary TCP */
4
2
  import { Logger, type LogLevel } from './logger.js';
5
3
  export interface StelarClientHooks {
6
4
  onBeforeEmit?: (i: {
@@ -48,6 +46,7 @@ export interface StelarClientOptions {
48
46
  tls?: boolean;
49
47
  rejectUnauthorized?: boolean;
50
48
  headers?: Record<string, string>;
49
+ compression?: boolean;
51
50
  customReconnectDelay?: (attempt: number, baseDelay: number, maxDelay: number) => number;
52
51
  hooks?: StelarClientHooks;
53
52
  }
@@ -80,6 +79,10 @@ declare class StelarClient {
80
79
  private _wsParser;
81
80
  private _tcpSock;
82
81
  private _tcpParser;
82
+ private _compress;
83
+ private _serverCompress;
84
+ private _writePaused;
85
+ private _writeQueue;
83
86
  private log;
84
87
  constructor(urlOrPort?: string | number, o?: StelarClientOptions);
85
88
  getState(): ConnectionState;
@@ -102,6 +105,7 @@ declare class StelarClient {
102
105
  mode: "ws" | "tcp";
103
106
  maxPayloadSize: number;
104
107
  messageQueueSize: number;
108
+ compression: boolean;
105
109
  hasCustomReconnectDelay: boolean;
106
110
  hooks: string[];
107
111
  }>;
@@ -135,6 +139,9 @@ declare class StelarClient {
135
139
  private _fullCleanup;
136
140
  private _tryReconnect;
137
141
  private _onConnected;
142
+ private _writeTCP;
143
+ private _writeNodeWS;
144
+ private _flushQueue;
138
145
  private _connectBrowser;
139
146
  private _handleBrowserMsg;
140
147
  private _connectNodeWS;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["client.ts"],"names":[],"mappings":"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"}
package/src/client.js CHANGED
@@ -1,11 +1,8 @@
1
- /**
2
- * @stelar-time-real Client — Browser WS / Node WS / binary TCP
3
- */
1
+ /** @stelar-time-real Client — Browser WS / Node WS / binary TCP */
4
2
  import { FrameParser, encodeJsonFrame, encodeBinaryFrame, encodeAckReqFrame, encodePingFrame, encodePongFrame, encodeJoinFrame, encodeLeaveFrame, FRAME_JSON, FRAME_BINARY, FRAME_PING, FRAME_PONG, FRAME_ACK_RES, FRAME_CONNECT, validateEventName, DEFAULT_MAX_FRAME_SIZE, } from './protocol.js';
5
- import { WSFrameParser, generateWSKey, createWSTextFrameMasked, createWSBinaryFrameMasked, createWSCloseFrameMasked, createWSPongFrameMasked, OP_TEXT, OP_BINARY, OP_CLOSE, OP_PING, OP_PONG, } from './websocket.js';
3
+ import { WSFrameParser, generateWSKey, createWSTextFrameMasked, createWSBinaryFrameMasked, createWSCloseFrameMasked, createWSPongFrameMasked, OP_TEXT, OP_BINARY, OP_CLOSE, OP_PING, OP_PONG, clientWantsCompression, } from './websocket.js';
6
4
  import { Logger, NULL_LOGGER } from './logger.js';
7
5
  const isNode = typeof process !== 'undefined' && process.versions?.node != null;
8
- /* Lazy-load Node modules for browser compat */
9
6
  let _http, _net, _tls, _https;
10
7
  async function loadModules() {
11
8
  if (!_http) {
@@ -26,6 +23,31 @@ class MsgQueue {
26
23
  get length() { return this.q.length; }
27
24
  clear() { this.q = []; }
28
25
  }
26
+ /** WS binary framing: [4B headerLen BE][header JSON][binary payload] — length-prefixed, not null-delimited */
27
+ function encodeWSBinary(event, data) {
28
+ const hdr = Buffer.from(JSON.stringify({ event }), 'utf8');
29
+ const payload = new Uint8Array(data);
30
+ const frame = Buffer.alloc(4 + hdr.length + payload.length);
31
+ frame.writeUInt32BE(hdr.length, 0);
32
+ hdr.copy(frame, 4);
33
+ frame.set(payload, 4 + hdr.length);
34
+ return frame;
35
+ }
36
+ function parseWSBinary(payload) {
37
+ if (payload.length < 4)
38
+ return null;
39
+ const hdrLen = payload.readUInt32BE(0);
40
+ if (hdrLen > payload.length - 4)
41
+ return null;
42
+ try {
43
+ const hdr = JSON.parse(payload.subarray(4, 4 + hdrLen).toString('utf8'));
44
+ const buf = payload.subarray(4 + hdrLen);
45
+ return { event: hdr.event, buffer: buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) };
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
29
51
  class StelarClient {
30
52
  constructor(urlOrPort = 'localhost:3000', o = {}) {
31
53
  this.events = new Map();
@@ -47,6 +69,10 @@ class StelarClient {
47
69
  this._wsParser = null;
48
70
  this._tcpSock = null;
49
71
  this._tcpParser = null;
72
+ this._compress = false;
73
+ this._serverCompress = false;
74
+ this._writePaused = false;
75
+ this._writeQueue = [];
50
76
  if (typeof urlOrPort === 'number')
51
77
  this.url = `ws://localhost:${urlOrPort}`;
52
78
  else if (urlOrPort.includes('://'))
@@ -61,6 +87,7 @@ class StelarClient {
61
87
  maxFrameSize: o.maxFrameSize || DEFAULT_MAX_FRAME_SIZE, messageQueueSize: o.messageQueueSize || 100,
62
88
  logger: o.logger !== undefined ? o.logger : 'warn', tls: o.tls || false,
63
89
  rejectUnauthorized: o.rejectUnauthorized !== false, headers: o.headers || {},
90
+ compression: o.compression || false,
64
91
  customReconnectDelay: o.customReconnectDelay, hooks: o.hooks || {},
65
92
  };
66
93
  this._mq = new MsgQueue(this.opts.messageQueueSize);
@@ -76,7 +103,7 @@ class StelarClient {
76
103
  getConnectTime() { return this._connTime; }
77
104
  setUrl(u) { this.url = u; return this; }
78
105
  updateOptions(o) {
79
- for (const k of ['reconnection', 'reconnectionAttempts', 'reconnectionDelay', 'maxReconnectionDelay', 'heartbeatInterval', 'ackTimeout', 'maxPayloadSize', 'maxFrameSize', 'messageQueueSize', 'headers'])
106
+ for (const k of ['reconnection', 'reconnectionAttempts', 'reconnectionDelay', 'maxReconnectionDelay', 'heartbeatInterval', 'ackTimeout', 'maxPayloadSize', 'maxFrameSize', 'messageQueueSize', 'headers', 'compression'])
80
107
  if (o[k] !== undefined)
81
108
  this.opts[k] = o[k];
82
109
  if (o.customReconnectDelay !== undefined)
@@ -91,6 +118,7 @@ class StelarClient {
91
118
  reconnectionDelay: this.opts.reconnectionDelay, maxReconnectionDelay: this.opts.maxReconnectionDelay,
92
119
  heartbeatInterval: this.opts.heartbeatInterval, ackTimeout: this.opts.ackTimeout, mode: this.opts.mode,
93
120
  maxPayloadSize: this.opts.maxPayloadSize, messageQueueSize: this.opts.messageQueueSize,
121
+ compression: this.opts.compression,
94
122
  hasCustomReconnectDelay: !!this.opts.customReconnectDelay, hooks: Object.keys(this.opts.hooks),
95
123
  });
96
124
  }
@@ -132,9 +160,9 @@ class StelarClient {
132
160
  try {
133
161
  const send = (wsPayload, tcpPayload) => {
134
162
  if (this.opts.mode === 'tcp' && this._tcpSock && !this._tcpSock.destroyed)
135
- this._tcpSock.write(tcpPayload());
163
+ this._writeTCP(tcpPayload());
136
164
  else if (this._nodeSock && !this._nodeSock.destroyed)
137
- this._nodeSock.write(wsPayload());
165
+ this._writeNodeWS(wsPayload());
138
166
  else if (this._ws && this._ws.readyState === WebSocket.OPEN)
139
167
  this._ws.send(JSON.stringify({ event, data, ...(opts.ack ? { _ackName: opts.ack } : {}), ...(opts._correlationId ? { _correlationId: opts._correlationId } : {}) }));
140
168
  else {
@@ -145,12 +173,12 @@ class StelarClient {
145
173
  };
146
174
  if (opts.ack) {
147
175
  send(() => { const p = { event, data, _ackName: opts.ack }; if (opts._correlationId)
148
- p._correlationId = opts._correlationId; return createWSTextFrameMasked(JSON.stringify(p)); }, () => { const d = { event, data }; if (opts._correlationId)
176
+ p._correlationId = opts._correlationId; return createWSTextFrameMasked(JSON.stringify(p), this._compress); }, () => { const d = { event, data }; if (opts._correlationId)
149
177
  d._correlationId = opts._correlationId; return encodeAckReqFrame(opts.ack, d, this.opts.maxFrameSize); });
150
178
  }
151
179
  else {
152
180
  send(() => { const p = { event, data }; if (opts._correlationId)
153
- p._correlationId = opts._correlationId; return createWSTextFrameMasked(JSON.stringify(p)); }, () => encodeJsonFrame(event, data, this.opts.maxFrameSize));
181
+ p._correlationId = opts._correlationId; return createWSTextFrameMasked(JSON.stringify(p), this._compress); }, () => encodeJsonFrame(event, data, this.opts.maxFrameSize));
154
182
  }
155
183
  }
156
184
  catch (e) {
@@ -170,24 +198,16 @@ class StelarClient {
170
198
  if (this._state !== 'connected')
171
199
  return this;
172
200
  try {
201
+ const safeCopy = Buffer.from(new Uint8Array(data));
173
202
  if (this.opts.mode === 'tcp' && this._tcpSock && !this._tcpSock.destroyed) {
174
- this._tcpSock.write(encodeBinaryFrame(event, new Uint8Array(data), this.opts.maxFrameSize));
203
+ this._writeTCP(encodeBinaryFrame(event, safeCopy, this.opts.maxFrameSize));
175
204
  }
176
205
  else if (this._nodeSock && !this._nodeSock.destroyed) {
177
- const hdr = Buffer.from(JSON.stringify({ event }), 'utf8');
178
- const c = Buffer.alloc(hdr.length + 1 + data.byteLength);
179
- hdr.copy(c, 0);
180
- c[hdr.length] = 0;
181
- c.set(new Uint8Array(data), hdr.length + 1);
182
- this._nodeSock.write(createWSBinaryFrameMasked(c));
206
+ this._writeNodeWS(createWSBinaryFrameMasked(encodeWSBinary(event, safeCopy)));
183
207
  }
184
208
  else if (this._ws && this._ws.readyState === WebSocket.OPEN) {
185
- const hdr = new TextEncoder().encode(JSON.stringify({ event }));
186
- const c = new Uint8Array(hdr.length + 1 + data.byteLength);
187
- c.set(hdr, 0);
188
- c[hdr.length] = 0;
189
- c.set(new Uint8Array(data), hdr.length + 1);
190
- this._ws.send(c);
209
+ const frame = encodeWSBinary(event, safeCopy);
210
+ this._ws.send(frame);
191
211
  }
192
212
  this._sent++;
193
213
  }
@@ -298,7 +318,7 @@ class StelarClient {
298
318
  catch { }
299
319
  else if (this._nodeSock && !this._nodeSock.destroyed)
300
320
  try {
301
- this._nodeSock.write(createWSTextFrameMasked(JSON.stringify({ event: 'pong', data: Date.now() })));
321
+ this._nodeSock.write(createWSTextFrameMasked(JSON.stringify({ event: 'pong', data: Date.now() }), this._compress));
302
322
  }
303
323
  catch { }
304
324
  else if (this._ws && this._ws.readyState === WebSocket.OPEN)
@@ -336,7 +356,7 @@ class StelarClient {
336
356
  this._tcpSock.write(m.opts.ack ? encodeAckReqFrame(m.opts.ack, p, this.opts.maxFrameSize) : encodeJsonFrame(m.event, m.data, this.opts.maxFrameSize));
337
357
  }
338
358
  else if (this._nodeSock && !this._nodeSock.destroyed) {
339
- this._nodeSock.write(createWSTextFrameMasked(JSON.stringify(p)));
359
+ this._nodeSock.write(createWSTextFrameMasked(JSON.stringify(p), this._compress));
340
360
  }
341
361
  else if (this._ws && this._ws.readyState === WebSocket.OPEN) {
342
362
  this._ws.send(JSON.stringify(p));
@@ -364,6 +384,8 @@ class StelarClient {
364
384
  this._tcpSock = null;
365
385
  this._tcpParser = null;
366
386
  this._ws = null;
387
+ this._writePaused = false;
388
+ this._writeQueue = [];
367
389
  }
368
390
  _tryReconnect(fn) {
369
391
  if (this._manualClose || !this.opts.reconnection)
@@ -389,6 +411,43 @@ class StelarClient {
389
411
  this._startHB();
390
412
  this._drain();
391
413
  }
414
+ /* ── Backpressure-aware writes ── */
415
+ _writeTCP(buf) {
416
+ if (!this._tcpSock || this._tcpSock.destroyed)
417
+ return;
418
+ if (this._writePaused) {
419
+ this._writeQueue.push(buf);
420
+ return;
421
+ }
422
+ const ok = this._tcpSock.write(buf);
423
+ if (!ok)
424
+ this._writePaused = true;
425
+ }
426
+ _writeNodeWS(buf) {
427
+ if (!this._nodeSock || this._nodeSock.destroyed)
428
+ return;
429
+ if (this._writePaused) {
430
+ this._writeQueue.push(buf);
431
+ return;
432
+ }
433
+ const ok = this._nodeSock.write(buf);
434
+ if (!ok)
435
+ this._writePaused = true;
436
+ }
437
+ _flushQueue() {
438
+ this._writePaused = false;
439
+ while (this._writeQueue.length) {
440
+ const buf = this._writeQueue.shift();
441
+ const sock = this.opts.mode === 'tcp' ? this._tcpSock : this._nodeSock;
442
+ if (sock && !sock.destroyed) {
443
+ const ok = sock.write(buf);
444
+ if (!ok) {
445
+ this._writePaused = true;
446
+ break;
447
+ }
448
+ }
449
+ }
450
+ }
392
451
  /* ── Browser WS ── */
393
452
  _connectBrowser() {
394
453
  try {
@@ -409,20 +468,13 @@ class StelarClient {
409
468
  _handleBrowserMsg(e) {
410
469
  try {
411
470
  if (e.data instanceof ArrayBuffer) {
412
- const v = new Uint8Array(e.data);
413
- let end = -1;
414
- for (let i = 0; i < v.length; i++)
415
- if (v[i] === 0) {
416
- end = i;
417
- break;
418
- }
419
- if (end === -1)
471
+ const buf = Buffer.from(e.data);
472
+ const parsed = parseWSBinary(buf);
473
+ if (!parsed)
420
474
  return;
421
- const hdr = JSON.parse(new TextDecoder().decode(v.slice(0, end)));
422
- const buf = v.slice(end + 1).buffer;
423
- this.opts.hooks.onMessage?.({ event: hdr.event, data: buf, isBinary: true });
424
- this.events.get(hdr.event)?.(buf);
425
- this._wild?.({ event: hdr.event, data: buf, isBinary: true, buffer: buf });
475
+ this.opts.hooks.onMessage?.({ event: parsed.event, data: parsed.buffer, isBinary: true });
476
+ this.events.get(parsed.event)?.(parsed.buffer);
477
+ this._wild?.({ event: parsed.event, data: parsed.buffer, isBinary: true, buffer: parsed.buffer });
426
478
  return;
427
479
  }
428
480
  const msg = JSON.parse(e.data), { event, data, _isAck } = msg;
@@ -454,10 +506,15 @@ class StelarClient {
454
506
  const parsed = new URL(this.url), secure = parsed.protocol === 'wss:' || this.opts.tls;
455
507
  const key = generateWSKey();
456
508
  const hdrs = { Upgrade: 'websocket', Connection: 'Upgrade', 'Sec-WebSocket-Key': key, 'Sec-WebSocket-Version': '13', ...this.opts.headers };
509
+ if (this.opts.compression)
510
+ hdrs['Sec-WebSocket-Extensions'] = 'permessage-deflate; client_no_context_takeover; server_no_context_takeover';
457
511
  const mod = secure && _https ? _https : _http;
458
512
  const req = mod.request({ hostname: parsed.hostname, port: parseInt(parsed.port) || (secure ? 443 : 80), path: parsed.pathname + parsed.search, method: 'GET', headers: hdrs, rejectUnauthorized: this.opts.rejectUnauthorized });
459
513
  req.setTimeout(this.opts.ackTimeout, () => req.destroy(new Error('Timeout')));
460
- req.on('upgrade', (_res, socket, head) => {
514
+ req.on('upgrade', (res, socket, head) => {
515
+ const extHeader = res.headers['sec-websocket-extensions'];
516
+ this._serverCompress = this.opts.compression && !!extHeader && clientWantsCompression(extHeader);
517
+ this._compress = this._serverCompress;
461
518
  this._nodeSock = socket;
462
519
  this._wsParser = new WSFrameParser(this.opts.maxFrameSize);
463
520
  if (head.length > 0)
@@ -465,8 +522,8 @@ class StelarClient {
465
522
  socket.on('data', (d) => this._processNodeWS(d));
466
523
  socket.on('close', () => { this._setState('disconnected'); this._fullCleanup(); this.events.get('disconnect')?.(undefined); this._tryReconnect(() => this._connectNodeWS()); });
467
524
  socket.on('error', (e) => { this._lastErr = e; this.events.get('error')?.(e); this.opts.hooks.onError?.({ error: e, context: 'node-ws' }); });
468
- socket.on('drain', () => socket.resume());
469
- this.log.info('Node WS connected', { secure });
525
+ socket.on('drain', () => this._flushQueue());
526
+ this.log.info('Node WS connected', { secure, compressed: this._compress });
470
527
  this._onConnected();
471
528
  });
472
529
  req.on('error', (e) => { this._lastErr = e; this.events.get('error')?.(e); this._tryReconnect(() => this._connectNodeWS()); });
@@ -536,22 +593,12 @@ class StelarClient {
536
593
  return;
537
594
  }
538
595
  if (f.opcode === OP_BINARY) {
539
- try {
540
- let end = -1;
541
- for (let i = 0; i < f.payload.length; i++)
542
- if (f.payload[i] === 0) {
543
- end = i;
544
- break;
545
- }
546
- if (end === -1)
547
- return;
548
- const hdr = JSON.parse(f.payload.subarray(0, end).toString('utf8'));
549
- const buf = f.payload.subarray(end + 1).buffer;
550
- this.opts.hooks.onMessage?.({ event: hdr.event, data: buf, isBinary: true });
551
- this.events.get(hdr.event)?.(buf);
552
- this._wild?.({ event: hdr.event, data: buf, isBinary: true, buffer: buf });
553
- }
554
- catch { }
596
+ const parsed = parseWSBinary(f.payload);
597
+ if (!parsed)
598
+ return;
599
+ this.opts.hooks.onMessage?.({ event: parsed.event, data: parsed.buffer, isBinary: true });
600
+ this.events.get(parsed.event)?.(parsed.buffer);
601
+ this._wild?.({ event: parsed.event, data: parsed.buffer, isBinary: true, buffer: parsed.buffer });
555
602
  }
556
603
  }
557
604
  /* ── TCP ── */
@@ -579,7 +626,7 @@ class StelarClient {
579
626
  } });
580
627
  socket.on('close', () => { this._setState('disconnected'); this._fullCleanup(); this.events.get('disconnect')?.(undefined); this._tryReconnect(() => this._connectTCP()); });
581
628
  socket.on('error', (e) => { this._lastErr = e; this.events.get('error')?.(e); this.opts.hooks.onError?.({ error: e, context: 'tcp' }); });
582
- socket.on('drain', () => socket.resume());
629
+ socket.on('drain', () => this._flushQueue());
583
630
  this._tcpSock = socket;
584
631
  }
585
632
  catch (e) {
package/src/client.ts CHANGED
@@ -1,6 +1,4 @@
1
- /**
2
- * @stelar-time-real Client — Browser WS / Node WS / binary TCP
3
- */
1
+ /** @stelar-time-real Client — Browser WS / Node WS / binary TCP */
4
2
 
5
3
  import {
6
4
  FrameParser, encodeJsonFrame, encodeBinaryFrame, encodeAckReqFrame,
@@ -13,7 +11,8 @@ import {
13
11
  WSFrameParser, generateWSKey, createWSTextFrameMasked,
14
12
  createWSBinaryFrameMasked, createWSCloseFrameMasked,
15
13
  createWSPongFrameMasked, OP_TEXT, OP_BINARY, OP_CLOSE, OP_PING, OP_PONG,
16
- CLOSE_NORMAL, DEFAULT_MAX_WS_FRAME_SIZE,
14
+ CLOSE_NORMAL, DEFAULT_MAX_WS_FRAME_SIZE, clientWantsCompression,
15
+ createWSTextFrame, buildUpgradeResponse,
17
16
  } from './websocket.js';
18
17
 
19
18
  import { Logger, NULL_LOGGER, type LogLevel } from './logger.js';
@@ -36,6 +35,7 @@ export interface StelarClientOptions {
36
35
  mode?: 'ws' | 'tcp'; maxPayloadSize?: number; maxFrameSize?: number;
37
36
  messageQueueSize?: number; logger?: Logger | LogLevel | false; tls?: boolean;
38
37
  rejectUnauthorized?: boolean; headers?: Record<string, string>;
38
+ compression?: boolean;
39
39
  customReconnectDelay?: (attempt: number, baseDelay: number, maxDelay: number) => number;
40
40
  hooks?: StelarClientHooks;
41
41
  }
@@ -44,7 +44,6 @@ export interface StelarEmitOptions { ack?: string; _correlationId?: string; }
44
44
  export type StelarEventHandler = (data: unknown) => void;
45
45
  export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
46
46
 
47
- /* Lazy-load Node modules for browser compat */
48
47
  let _http: typeof import('http') | null, _net: typeof import('net') | null,
49
48
  _tls: typeof import('tls') | null, _https: typeof import('https') | null;
50
49
 
@@ -63,6 +62,28 @@ class MsgQueue {
63
62
  clear() { this.q = []; }
64
63
  }
65
64
 
65
+ /** WS binary framing: [4B headerLen BE][header JSON][binary payload] — length-prefixed, not null-delimited */
66
+ function encodeWSBinary(event: string, data: Uint8Array | ArrayBuffer): Buffer {
67
+ const hdr = Buffer.from(JSON.stringify({ event }), 'utf8');
68
+ const payload = new Uint8Array(data);
69
+ const frame = Buffer.alloc(4 + hdr.length + payload.length);
70
+ frame.writeUInt32BE(hdr.length, 0);
71
+ hdr.copy(frame, 4);
72
+ frame.set(payload, 4 + hdr.length);
73
+ return frame;
74
+ }
75
+
76
+ function parseWSBinary(payload: Buffer): { event: string; buffer: ArrayBuffer } | null {
77
+ if (payload.length < 4) return null;
78
+ const hdrLen = payload.readUInt32BE(0);
79
+ if (hdrLen > payload.length - 4) return null;
80
+ try {
81
+ const hdr = JSON.parse(payload.subarray(4, 4 + hdrLen).toString('utf8'));
82
+ const buf = payload.subarray(4 + hdrLen);
83
+ return { event: hdr.event, buffer: buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer };
84
+ } catch { return null; }
85
+ }
86
+
66
87
  class StelarClient {
67
88
  private url: string;
68
89
  private opts: Required<Omit<StelarClientOptions, 'customReconnectDelay' | 'hooks'>> & {
@@ -85,6 +106,10 @@ class StelarClient {
85
106
  private _wsParser: WSFrameParser | null = null;
86
107
  private _tcpSock: InstanceType<typeof import('net').Socket> | null = null;
87
108
  private _tcpParser: FrameParser | null = null;
109
+ private _compress = false;
110
+ private _serverCompress = false;
111
+ private _writePaused = false;
112
+ private _writeQueue: Buffer[] = [];
88
113
  private log: Logger;
89
114
 
90
115
  constructor(urlOrPort: string | number = 'localhost:3000', o: StelarClientOptions = {}) {
@@ -99,6 +124,7 @@ class StelarClient {
99
124
  maxFrameSize: o.maxFrameSize || DEFAULT_MAX_FRAME_SIZE, messageQueueSize: o.messageQueueSize || 100,
100
125
  logger: o.logger !== undefined ? o.logger as any : 'warn', tls: o.tls || false,
101
126
  rejectUnauthorized: o.rejectUnauthorized !== false, headers: o.headers || {},
127
+ compression: o.compression || false,
102
128
  customReconnectDelay: o.customReconnectDelay, hooks: o.hooks || {},
103
129
  };
104
130
  this._mq = new MsgQueue(this.opts.messageQueueSize);
@@ -116,7 +142,7 @@ class StelarClient {
116
142
  setUrl(u: string) { this.url = u; return this; }
117
143
 
118
144
  updateOptions(o: Partial<StelarClientOptions>): this {
119
- for (const k of ['reconnection', 'reconnectionAttempts', 'reconnectionDelay', 'maxReconnectionDelay', 'heartbeatInterval', 'ackTimeout', 'maxPayloadSize', 'maxFrameSize', 'messageQueueSize', 'headers'] as const)
145
+ for (const k of ['reconnection', 'reconnectionAttempts', 'reconnectionDelay', 'maxReconnectionDelay', 'heartbeatInterval', 'ackTimeout', 'maxPayloadSize', 'maxFrameSize', 'messageQueueSize', 'headers', 'compression'] as const)
120
146
  if ((o as any)[k] !== undefined) (this.opts as any)[k] = (o as any)[k];
121
147
  if (o.customReconnectDelay !== undefined) this.opts.customReconnectDelay = o.customReconnectDelay;
122
148
  if (o.hooks !== undefined) this.opts.hooks = { ...this.opts.hooks, ...o.hooks };
@@ -129,6 +155,7 @@ class StelarClient {
129
155
  reconnectionDelay: this.opts.reconnectionDelay, maxReconnectionDelay: this.opts.maxReconnectionDelay,
130
156
  heartbeatInterval: this.opts.heartbeatInterval, ackTimeout: this.opts.ackTimeout, mode: this.opts.mode,
131
157
  maxPayloadSize: this.opts.maxPayloadSize, messageQueueSize: this.opts.messageQueueSize,
158
+ compression: this.opts.compression,
132
159
  hasCustomReconnectDelay: !!this.opts.customReconnectDelay, hooks: Object.keys(this.opts.hooks),
133
160
  });
134
161
  }
@@ -150,20 +177,20 @@ class StelarClient {
150
177
  }
151
178
  try {
152
179
  const send = (wsPayload: () => Buffer, tcpPayload: () => Buffer) => {
153
- if (this.opts.mode === 'tcp' && this._tcpSock && !this._tcpSock.destroyed) this._tcpSock.write(tcpPayload());
154
- else if (this._nodeSock && !this._nodeSock.destroyed) this._nodeSock.write(wsPayload());
180
+ if (this.opts.mode === 'tcp' && this._tcpSock && !this._tcpSock.destroyed) this._writeTCP(tcpPayload());
181
+ else if (this._nodeSock && !this._nodeSock.destroyed) this._writeNodeWS(wsPayload());
155
182
  else if (this._ws && this._ws.readyState === WebSocket.OPEN) this._ws.send(JSON.stringify({ event, data, ...(opts.ack ? { _ackName: opts.ack } : {}), ...(opts._correlationId ? { _correlationId: opts._correlationId } : {}) }));
156
183
  else { this._mq.push({ event, data, opts, ts: Date.now() }); return; }
157
184
  this._sent++;
158
185
  };
159
186
  if (opts.ack) {
160
187
  send(
161
- () => { const p: Record<string, unknown> = { event, data, _ackName: opts.ack }; if (opts._correlationId) p._correlationId = opts._correlationId; return createWSTextFrameMasked(JSON.stringify(p)); },
188
+ () => { const p: Record<string, unknown> = { event, data, _ackName: opts.ack }; if (opts._correlationId) p._correlationId = opts._correlationId; return createWSTextFrameMasked(JSON.stringify(p), this._compress); },
162
189
  () => { const d: Record<string, unknown> = { event, data }; if (opts._correlationId) d._correlationId = opts._correlationId; return encodeAckReqFrame(opts.ack!, d, this.opts.maxFrameSize); },
163
190
  );
164
191
  } else {
165
192
  send(
166
- () => { const p: Record<string, unknown> = { event, data }; if (opts._correlationId) p._correlationId = opts._correlationId; return createWSTextFrameMasked(JSON.stringify(p)); },
193
+ () => { const p: Record<string, unknown> = { event, data }; if (opts._correlationId) p._correlationId = opts._correlationId; return createWSTextFrameMasked(JSON.stringify(p), this._compress); },
167
194
  () => encodeJsonFrame(event, data, this.opts.maxFrameSize),
168
195
  );
169
196
  }
@@ -180,18 +207,14 @@ class StelarClient {
180
207
  if (this.opts.hooks.onBeforeEmit?.({ event, data }) === false) return this;
181
208
  if (this._state !== 'connected') return this;
182
209
  try {
210
+ const safeCopy = Buffer.from(new Uint8Array(data));
183
211
  if (this.opts.mode === 'tcp' && this._tcpSock && !this._tcpSock.destroyed) {
184
- this._tcpSock.write(encodeBinaryFrame(event, new Uint8Array(data), this.opts.maxFrameSize));
212
+ this._writeTCP(encodeBinaryFrame(event, safeCopy, this.opts.maxFrameSize));
185
213
  } else if (this._nodeSock && !this._nodeSock.destroyed) {
186
- const hdr = Buffer.from(JSON.stringify({ event }), 'utf8');
187
- const c = Buffer.alloc(hdr.length + 1 + data.byteLength);
188
- hdr.copy(c, 0); c[hdr.length] = 0; c.set(new Uint8Array(data), hdr.length + 1);
189
- this._nodeSock.write(createWSBinaryFrameMasked(c));
214
+ this._writeNodeWS(createWSBinaryFrameMasked(encodeWSBinary(event, safeCopy)));
190
215
  } else if (this._ws && this._ws.readyState === WebSocket.OPEN) {
191
- const hdr = new TextEncoder().encode(JSON.stringify({ event }));
192
- const c = new Uint8Array(hdr.length + 1 + data.byteLength);
193
- c.set(hdr, 0); c[hdr.length] = 0; c.set(new Uint8Array(data), hdr.length + 1);
194
- this._ws.send(c);
216
+ const frame = encodeWSBinary(event, safeCopy);
217
+ this._ws.send(frame);
195
218
  }
196
219
  this._sent++;
197
220
  } catch (e) { this.log.error('Binary emit error', { event, error: String(e) }); }
@@ -258,7 +281,7 @@ class StelarClient {
258
281
  this._stopHB();
259
282
  this._hb = setInterval(() => {
260
283
  if (this.opts.mode === 'tcp' && this._tcpSock && !this._tcpSock.destroyed) try { this._tcpSock.write(encodePingFrame()); } catch {}
261
- else if (this._nodeSock && !this._nodeSock.destroyed) try { this._nodeSock.write(createWSTextFrameMasked(JSON.stringify({ event: 'pong', data: Date.now() }))); } catch {}
284
+ else if (this._nodeSock && !this._nodeSock.destroyed) try { this._nodeSock.write(createWSTextFrameMasked(JSON.stringify({ event: 'pong', data: Date.now() }), this._compress)); } catch {}
262
285
  else if (this._ws && this._ws.readyState === WebSocket.OPEN) this._ws.send(JSON.stringify({ event: 'pong', data: Date.now() }));
263
286
  }, this.opts.heartbeatInterval);
264
287
  this._hb?.unref?.();
@@ -287,7 +310,7 @@ class StelarClient {
287
310
  if (this.opts.mode === 'tcp' && this._tcpSock && !this._tcpSock.destroyed) {
288
311
  this._tcpSock.write(m.opts.ack ? encodeAckReqFrame(m.opts.ack, p, this.opts.maxFrameSize) : encodeJsonFrame(m.event, m.data, this.opts.maxFrameSize));
289
312
  } else if (this._nodeSock && !this._nodeSock.destroyed) {
290
- this._nodeSock.write(createWSTextFrameMasked(JSON.stringify(p)));
313
+ this._nodeSock.write(createWSTextFrameMasked(JSON.stringify(p), this._compress));
291
314
  } else if (this._ws && this._ws.readyState === WebSocket.OPEN) {
292
315
  this._ws.send(JSON.stringify(p));
293
316
  }
@@ -303,6 +326,7 @@ class StelarClient {
303
326
  this._stopHB(); this._cleanupAcks();
304
327
  if (this._reconnTimer) { clearTimeout(this._reconnTimer); this._reconnTimer = null; }
305
328
  this._nodeSock = null; this._wsParser = null; this._tcpSock = null; this._tcpParser = null; this._ws = null;
329
+ this._writePaused = false; this._writeQueue = [];
306
330
  }
307
331
 
308
332
  private _tryReconnect(fn: () => void) {
@@ -320,6 +344,34 @@ class StelarClient {
320
344
  this.events.get('connect')?.(undefined); this._startHB(); this._drain();
321
345
  }
322
346
 
347
+ /* ── Backpressure-aware writes ── */
348
+
349
+ private _writeTCP(buf: Buffer) {
350
+ if (!this._tcpSock || this._tcpSock.destroyed) return;
351
+ if (this._writePaused) { this._writeQueue.push(buf); return; }
352
+ const ok = this._tcpSock.write(buf);
353
+ if (!ok) this._writePaused = true;
354
+ }
355
+
356
+ private _writeNodeWS(buf: Buffer) {
357
+ if (!this._nodeSock || this._nodeSock.destroyed) return;
358
+ if (this._writePaused) { this._writeQueue.push(buf); return; }
359
+ const ok = this._nodeSock.write(buf);
360
+ if (!ok) this._writePaused = true;
361
+ }
362
+
363
+ private _flushQueue() {
364
+ this._writePaused = false;
365
+ while (this._writeQueue.length) {
366
+ const buf = this._writeQueue.shift()!;
367
+ const sock = this.opts.mode === 'tcp' ? this._tcpSock : this._nodeSock;
368
+ if (sock && !sock.destroyed) {
369
+ const ok = sock.write(buf);
370
+ if (!ok) { this._writePaused = true; break; }
371
+ }
372
+ }
373
+ }
374
+
323
375
  /* ── Browser WS ── */
324
376
 
325
377
  private _connectBrowser() {
@@ -336,14 +388,12 @@ class StelarClient {
336
388
  private _handleBrowserMsg(e: MessageEvent) {
337
389
  try {
338
390
  if (e.data instanceof ArrayBuffer) {
339
- const v = new Uint8Array(e.data); let end = -1;
340
- for (let i = 0; i < v.length; i++) if (v[i] === 0) { end = i; break; }
341
- if (end === -1) return;
342
- const hdr = JSON.parse(new TextDecoder().decode(v.slice(0, end)));
343
- const buf = v.slice(end + 1).buffer as ArrayBuffer;
344
- this.opts.hooks.onMessage?.({ event: hdr.event, data: buf, isBinary: true });
345
- this.events.get(hdr.event)?.(buf);
346
- this._wild?.({ event: hdr.event, data: buf, isBinary: true, buffer: buf });
391
+ const buf = Buffer.from(e.data);
392
+ const parsed = parseWSBinary(buf);
393
+ if (!parsed) return;
394
+ this.opts.hooks.onMessage?.({ event: parsed.event, data: parsed.buffer, isBinary: true });
395
+ this.events.get(parsed.event)?.(parsed.buffer);
396
+ this._wild?.({ event: parsed.event, data: parsed.buffer, isBinary: true, buffer: parsed.buffer });
347
397
  return;
348
398
  }
349
399
  const msg = JSON.parse(e.data as string), { event, data, _isAck } = msg;
@@ -366,17 +416,21 @@ class StelarClient {
366
416
  const parsed = new URL(this.url), secure = parsed.protocol === 'wss:' || this.opts.tls;
367
417
  const key = generateWSKey();
368
418
  const hdrs: Record<string, string> = { Upgrade: 'websocket', Connection: 'Upgrade', 'Sec-WebSocket-Key': key, 'Sec-WebSocket-Version': '13', ...this.opts.headers };
419
+ if (this.opts.compression) hdrs['Sec-WebSocket-Extensions'] = 'permessage-deflate; client_no_context_takeover; server_no_context_takeover';
369
420
  const mod = secure && _https ? _https : _http;
370
421
  const req = mod.request({ hostname: parsed.hostname, port: parseInt(parsed.port) || (secure ? 443 : 80), path: parsed.pathname + parsed.search, method: 'GET', headers: hdrs, rejectUnauthorized: this.opts.rejectUnauthorized });
371
422
  req.setTimeout(this.opts.ackTimeout, () => req.destroy(new Error('Timeout')));
372
- req.on('upgrade', (_res, socket, head) => {
423
+ req.on('upgrade', (res, socket, head) => {
424
+ const extHeader = res.headers['sec-websocket-extensions'];
425
+ this._serverCompress = this.opts.compression && !!extHeader && clientWantsCompression(extHeader as string);
426
+ this._compress = this._serverCompress;
373
427
  this._nodeSock = socket; this._wsParser = new WSFrameParser(this.opts.maxFrameSize);
374
428
  if (head.length > 0) this._processNodeWS(head);
375
429
  socket.on('data', (d: Buffer) => this._processNodeWS(d));
376
430
  socket.on('close', () => { this._setState('disconnected'); this._fullCleanup(); this.events.get('disconnect')?.(undefined); this._tryReconnect(() => this._connectNodeWS()); });
377
431
  socket.on('error', (e: Error) => { this._lastErr = e; this.events.get('error')?.(e); this.opts.hooks.onError?.({ error: e, context: 'node-ws' }); });
378
- socket.on('drain', () => socket.resume());
379
- this.log.info('Node WS connected', { secure });
432
+ socket.on('drain', () => this._flushQueue());
433
+ this.log.info('Node WS connected', { secure, compressed: this._compress });
380
434
  this._onConnected();
381
435
  });
382
436
  req.on('error', (e: Error) => { this._lastErr = e; this.events.get('error')?.(e); this._tryReconnect(() => this._connectNodeWS()); });
@@ -411,15 +465,11 @@ class StelarClient {
411
465
  }
412
466
 
413
467
  if (f.opcode === OP_BINARY) {
414
- try {
415
- let end = -1; for (let i = 0; i < f.payload.length; i++) if (f.payload[i] === 0) { end = i; break; }
416
- if (end === -1) return;
417
- const hdr = JSON.parse(f.payload.subarray(0, end).toString('utf8'));
418
- const buf = f.payload.subarray(end + 1).buffer as ArrayBuffer;
419
- this.opts.hooks.onMessage?.({ event: hdr.event, data: buf, isBinary: true });
420
- this.events.get(hdr.event)?.(buf);
421
- this._wild?.({ event: hdr.event, data: buf, isBinary: true, buffer: buf });
422
- } catch {}
468
+ const parsed = parseWSBinary(f.payload);
469
+ if (!parsed) return;
470
+ this.opts.hooks.onMessage?.({ event: parsed.event, data: parsed.buffer, isBinary: true });
471
+ this.events.get(parsed.event)?.(parsed.buffer);
472
+ this._wild?.({ event: parsed.event, data: parsed.buffer, isBinary: true, buffer: parsed.buffer });
423
473
  }
424
474
  }
425
475
 
@@ -436,7 +486,7 @@ class StelarClient {
436
486
  socket.on('data', (d: Buffer) => { if (!this._tcpParser) return; let frames; try { frames = this._tcpParser.feed(d); } catch { this.log.error('TCP parse error'); socket.destroy(); return; } for (const f of frames) { this._recv++; this._handleTCPFrame(f); } });
437
487
  socket.on('close', () => { this._setState('disconnected'); this._fullCleanup(); this.events.get('disconnect')?.(undefined); this._tryReconnect(() => this._connectTCP()); });
438
488
  socket.on('error', (e: Error) => { this._lastErr = e; this.events.get('error')?.(e); this.opts.hooks.onError?.({ error: e, context: 'tcp' }); });
439
- socket.on('drain', () => socket.resume());
489
+ socket.on('drain', () => this._flushQueue());
440
490
  this._tcpSock = socket;
441
491
  } catch (e) { this._lastErr = e instanceof Error ? e : new Error(String(e)); this._tryReconnect(() => this._connectTCP()); }
442
492
  }