@rvncom/socket-bun-engine 1.0.1 → 1.0.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/README.md CHANGED
@@ -96,6 +96,12 @@ Default: `0` (unlimited)
96
96
 
97
97
  Maximum number of concurrent clients. New connections are rejected with HTTP 503 when the limit is reached.
98
98
 
99
+ ### `backpressureThreshold`
100
+
101
+ Default: `1048576` (1 MB)
102
+
103
+ WebSocket send buffer threshold in bytes. When `getBufferedAmount()` exceeds this value, writes are paused automatically and resumed when the buffer drains. Set to `0` to disable.
104
+
99
105
  ### `allowRequest`
100
106
 
101
107
  A function that receives the handshake/upgrade request and can reject it:
@@ -146,6 +152,56 @@ const engine = new Engine({
146
152
  });
147
153
  ```
148
154
 
155
+ ## Metrics
156
+
157
+ Built-in server metrics with zero dependencies:
158
+
159
+ ```ts
160
+ const snapshot = engine.metrics;
161
+ // {
162
+ // connections: 150, // total opened (cumulative)
163
+ // disconnections: 12, // total closed
164
+ // activeConnections: 138, // currently connected
165
+ // upgrades: 130, // polling → websocket
166
+ // bytesReceived: 524288,
167
+ // bytesSent: 1048576,
168
+ // errors: 2,
169
+ // avgRtt: 14 // average round-trip time (ms)
170
+ // }
171
+ ```
172
+
173
+ Per-socket RTT is also available:
174
+
175
+ ```ts
176
+ engine.on("connection", (socket) => {
177
+ socket.on("heartbeat", () => {
178
+ console.log(`RTT: ${socket.rtt}ms`);
179
+ });
180
+ });
181
+ ```
182
+
183
+ ## API
184
+
185
+ ### `server.clientsCount`
186
+
187
+ Number of currently connected clients.
188
+
189
+ ### `server.metrics`
190
+
191
+ Returns a `MetricsSnapshot` object with server-wide counters.
192
+
193
+ ### `server.sockets`
194
+
195
+ Iterator over all connected `Socket` instances.
196
+
197
+ ### `server.getSocket(id)`
198
+
199
+ Look up a specific socket by session ID.
200
+
201
+ ### `server.close()`
202
+
203
+ Returns a `Promise<void>` that resolves when all clients have disconnected.
204
+
149
205
  ## License
150
206
 
151
207
  [MIT](/LICENSE)
package/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export { Server, type ServerOptions } from "./server";
2
2
  export { Socket, type CloseReason } from "./socket";
3
3
  export { type RawData } from "./parser";
4
4
  export { type BunWebSocket, type WebSocketData } from "./transports/websocket";
5
+ export { type MetricsSnapshot } from "./metrics";
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@ export { Server } from "./server";
2
2
  export { Socket } from "./socket";
3
3
  export {} from "./parser";
4
4
  export {} from "./transports/websocket";
5
+ export {} from "./metrics";
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Built-in server metrics — lightweight counters with zero external dependencies.
3
+ */
4
+ export interface MetricsSnapshot {
5
+ connections: number;
6
+ disconnections: number;
7
+ activeConnections: number;
8
+ upgrades: number;
9
+ bytesReceived: number;
10
+ bytesSent: number;
11
+ errors: number;
12
+ avgRtt: number;
13
+ }
14
+ export declare class ServerMetrics {
15
+ connections: number;
16
+ disconnections: number;
17
+ upgrades: number;
18
+ bytesReceived: number;
19
+ bytesSent: number;
20
+ errors: number;
21
+ private rttSum;
22
+ private rttCount;
23
+ onConnection(): void;
24
+ onDisconnection(): void;
25
+ onUpgrade(): void;
26
+ onBytesReceived(bytes: number): void;
27
+ onBytesSent(bytes: number): void;
28
+ onError(): void;
29
+ onRtt(rtt: number): void;
30
+ snapshot(): MetricsSnapshot;
31
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Built-in server metrics — lightweight counters with zero external dependencies.
3
+ */
4
+ export class ServerMetrics {
5
+ connections = 0;
6
+ disconnections = 0;
7
+ upgrades = 0;
8
+ bytesReceived = 0;
9
+ bytesSent = 0;
10
+ errors = 0;
11
+ rttSum = 0;
12
+ rttCount = 0;
13
+ onConnection() {
14
+ this.connections++;
15
+ }
16
+ onDisconnection() {
17
+ this.disconnections++;
18
+ }
19
+ onUpgrade() {
20
+ this.upgrades++;
21
+ }
22
+ onBytesReceived(bytes) {
23
+ this.bytesReceived += bytes;
24
+ }
25
+ onBytesSent(bytes) {
26
+ this.bytesSent += bytes;
27
+ }
28
+ onError() {
29
+ this.errors++;
30
+ }
31
+ onRtt(rtt) {
32
+ this.rttSum += rtt;
33
+ this.rttCount++;
34
+ }
35
+ snapshot() {
36
+ return {
37
+ connections: this.connections,
38
+ disconnections: this.disconnections,
39
+ activeConnections: this.connections - this.disconnections,
40
+ upgrades: this.upgrades,
41
+ bytesReceived: this.bytesReceived,
42
+ bytesSent: this.bytesSent,
43
+ errors: this.errors,
44
+ avgRtt: this.rttCount > 0 ? Math.round(this.rttSum / this.rttCount) : 0,
45
+ };
46
+ }
47
+ }
package/dist/server.d.ts CHANGED
@@ -3,6 +3,7 @@ import { Socket } from "./socket";
3
3
  import { type BunWebSocket, type WebSocketData } from "./transports/websocket";
4
4
  import { type CorsOptions } from "./cors";
5
5
  import type { RawData } from "./parser";
6
+ import { type MetricsSnapshot } from "./metrics";
6
7
  export interface ServerOptions {
7
8
  /**
8
9
  * Name of the request path to handle
@@ -34,6 +35,11 @@ export interface ServerOptions {
34
35
  * @default 0
35
36
  */
36
37
  maxClients: number;
38
+ /**
39
+ * WebSocket send buffer threshold in bytes for backpressure. Set to 0 to disable.
40
+ * @default 1048576 (1 MB)
41
+ */
42
+ backpressureThreshold: number;
37
43
  /**
38
44
  * A function that receives a given handshake or upgrade request as its first parameter,
39
45
  * and can decide whether to continue or not.
@@ -65,7 +71,12 @@ interface ServerReservedEvents {
65
71
  export declare class Server extends EventEmitter<Record<never, never>, Record<never, never>, ServerReservedEvents> {
66
72
  readonly opts: ServerOptions;
67
73
  private clients;
74
+ private _metrics;
68
75
  get clientsCount(): number;
76
+ /**
77
+ * Returns a snapshot of server metrics.
78
+ */
79
+ get metrics(): MetricsSnapshot;
69
80
  constructor(opts?: Partial<ServerOptions>);
70
81
  /**
71
82
  * Handles an HTTP request.
package/dist/server.js CHANGED
@@ -5,6 +5,7 @@ import { WS, } from "./transports/websocket";
5
5
  import { addCorsHeaders } from "./cors";
6
6
  import { Transport } from "./transport";
7
7
  import { generateId } from "./util";
8
+ import { ServerMetrics } from "./metrics";
8
9
  import { debuglog } from "node:util";
9
10
  const debug = debuglog("engine.io");
10
11
  const TRANSPORTS = ["polling", "websocket"];
@@ -28,9 +29,16 @@ const ERROR_MESSAGES = new Map([
28
29
  export class Server extends EventEmitter {
29
30
  opts;
30
31
  clients = new Map();
32
+ _metrics = new ServerMetrics();
31
33
  get clientsCount() {
32
34
  return this.clients.size;
33
35
  }
36
+ /**
37
+ * Returns a snapshot of server metrics.
38
+ */
39
+ get metrics() {
40
+ return this._metrics.snapshot();
41
+ }
34
42
  constructor(opts = {}) {
35
43
  super();
36
44
  this.opts = Object.assign({
@@ -40,6 +48,7 @@ export class Server extends EventEmitter {
40
48
  upgradeTimeout: 10000,
41
49
  maxHttpBufferSize: 1e6,
42
50
  maxClients: 0,
51
+ backpressureThreshold: 1_048_576,
43
52
  }, opts);
44
53
  }
45
54
  /**
@@ -67,6 +76,7 @@ export class Server extends EventEmitter {
67
76
  catch (err) {
68
77
  const { code, context } = err;
69
78
  const message = ERROR_MESSAGES.get(code);
79
+ this._metrics.onError();
70
80
  this.emitReserved("connection_error", {
71
81
  req,
72
82
  code,
@@ -266,9 +276,31 @@ export class Server extends EventEmitter {
266
276
  },
267
277
  });
268
278
  this.clients.set(id, socket);
279
+ this._metrics.onConnection();
280
+ socket.on("data", (data) => {
281
+ this._metrics.onBytesReceived(typeof data === "string"
282
+ ? data.length
283
+ : data.byteLength);
284
+ });
285
+ socket.on("packetCreate", (packet) => {
286
+ if (packet.data != null) {
287
+ this._metrics.onBytesSent(typeof packet.data === "string"
288
+ ? packet.data.length
289
+ : packet.data.byteLength);
290
+ }
291
+ });
292
+ socket.on("heartbeat", () => {
293
+ if (socket.rtt > 0) {
294
+ this._metrics.onRtt(socket.rtt);
295
+ }
296
+ });
297
+ socket.on("upgrade", () => {
298
+ this._metrics.onUpgrade();
299
+ });
269
300
  socket.once("close", (reason) => {
270
301
  debug(`socket ${id} closed due to ${reason}`);
271
302
  this.clients.delete(id);
303
+ this._metrics.onDisconnection();
272
304
  });
273
305
  if (isUpgrade) {
274
306
  this.emitReserved("connection", socket, req, server);
package/dist/socket.d.ts CHANGED
@@ -34,6 +34,8 @@ export declare class Socket extends EventEmitter<Record<never, never>, Record<ne
34
34
  private writeBuffer;
35
35
  private pingIntervalTimer?;
36
36
  private pingTimeoutTimer?;
37
+ private _pingSentAt;
38
+ rtt: number;
37
39
  constructor(id: string, opts: ServerOptions, transport: Transport, req: HandshakeRequestReference);
38
40
  /**
39
41
  * Called upon transport considered open.
package/dist/socket.js CHANGED
@@ -18,6 +18,8 @@ export class Socket extends EventEmitter {
18
18
  */
19
19
  pingIntervalTimer;
20
20
  pingTimeoutTimer;
21
+ _pingSentAt = 0;
22
+ rtt = 0;
21
23
  constructor(id, opts, transport, req) {
22
24
  super();
23
25
  this.id = id;
@@ -60,6 +62,10 @@ export class Socket extends EventEmitter {
60
62
  switch (packet.type) {
61
63
  case "pong":
62
64
  debug("got pong");
65
+ if (this._pingSentAt > 0) {
66
+ this.rtt = Date.now() - this._pingSentAt;
67
+ this._pingSentAt = 0;
68
+ }
63
69
  clearTimeout(this.pingTimeoutTimer);
64
70
  this.schedulePing();
65
71
  this.emitReserved("heartbeat");
@@ -96,6 +102,7 @@ export class Socket extends EventEmitter {
96
102
  }
97
103
  this.pingIntervalTimer = setTimeout(() => {
98
104
  debug(`writing ping packet - expecting pong within ${this.opts.pingTimeout} ms`);
105
+ this._pingSentAt = Date.now();
99
106
  this.sendPacket("ping");
100
107
  this.resetPingTimeout();
101
108
  }, this.opts.pingInterval);
@@ -11,19 +11,34 @@ export class WS extends Transport {
11
11
  return [];
12
12
  }
13
13
  send(packets) {
14
- if (!this.writable || !this.socket || this.socket.readyState !== WebSocket.OPEN) {
14
+ if (!this.writable ||
15
+ !this.socket ||
16
+ this.socket.readyState !== WebSocket.OPEN) {
17
+ return;
18
+ }
19
+ // Backpressure: pause if send buffer is overloaded
20
+ const threshold = this.opts.backpressureThreshold;
21
+ if (threshold > 0 && this.socket.getBufferedAmount() > threshold) {
22
+ debug("backpressure: send buffer exceeded threshold, pausing writes");
23
+ this.writable = false;
15
24
  return;
16
25
  }
17
26
  if (packets.length === 1) {
18
27
  this.socket.send(Parser.encodePacket(packets[0], true));
19
- return;
20
28
  }
21
- // Batch multiple packets into a single syscall via cork()
22
- this.socket.cork(() => {
23
- for (const packet of packets) {
24
- this.socket.send(Parser.encodePacket(packet, true));
25
- }
26
- });
29
+ else {
30
+ // Batch multiple packets into a single syscall via cork()
31
+ this.socket.cork(() => {
32
+ for (const packet of packets) {
33
+ this.socket.send(Parser.encodePacket(packet, true));
34
+ }
35
+ });
36
+ }
37
+ // Check backpressure after send
38
+ if (threshold > 0 && this.socket.getBufferedAmount() > threshold) {
39
+ debug("backpressure: buffer full after send, pausing writes");
40
+ this.writable = false;
41
+ }
27
42
  }
28
43
  doClose() {
29
44
  this.socket?.close();
@@ -35,6 +50,15 @@ export class WS extends Transport {
35
50
  }
36
51
  onMessage(message) {
37
52
  debug("on message");
53
+ // Resume writes if backpressure cleared (client consuming data)
54
+ if (!this.writable && this.socket) {
55
+ const threshold = this.opts.backpressureThreshold;
56
+ if (threshold > 0 && this.socket.getBufferedAmount() <= threshold) {
57
+ debug("backpressure: buffer drained, resuming writes");
58
+ this.writable = true;
59
+ this.emitReserved("drain");
60
+ }
61
+ }
38
62
  this.onPacket(Parser.decodePacket(message));
39
63
  }
40
64
  onCloseEvent(_code, _message) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rvncom/socket-bun-engine",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Engine.IO server implementation for Bun runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -24,13 +24,13 @@
24
24
  "format:fix": "prettier --write \"lib/**/*.ts\" \"test/**/*.ts\""
25
25
  },
26
26
  "devDependencies": {
27
- "@types/bun": "^1.3.0",
27
+ "@types/bun": "^1.3.10",
28
28
  "oxlint": "latest",
29
- "prettier": "^3.6.2",
30
- "socket.io": "^4.8.1"
29
+ "prettier": "^3.8.1",
30
+ "socket.io": "^4.8.3"
31
31
  },
32
32
  "peerDependencies": {
33
- "typescript": "^5"
33
+ "typescript": "^5.9.2"
34
34
  },
35
35
  "license": "MIT",
36
36
  "homepage": "https://github.com/rvncom/socket-bun-engine#readme",