From 3afb8b80c53f0a925aa496262ce7d98229721911 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Jan 2026 05:19:22 +0000 Subject: [PATCH] feat(socket): add configurable maxListeners to prevent memory leaks Based on RSocket's battle-tested configuration: - Add maxWebSocketListeners config option (default: 20) - 8 base WS events + 10 dynamic listeners + 2 buffer slots - Add maxSocketClientListeners config option (default: 50) - Replace dangerous setMaxListeners(0) with configurable limits - Add warning log if user explicitly sets limit to 0 BREAKING: Previous behavior used setMaxListeners(0) which removed all limits. Now defaults to safe limits but can be overridden via config. --- src/Defaults/index.ts | 6 +++++- src/Socket/Client/types.ts | 8 +++++++- src/Socket/Client/websocket.ts | 8 +++++++- src/Types/Socket.ts | 15 +++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/Defaults/index.ts b/src/Defaults/index.ts index ea0c3c5d..006f9bb4 100644 --- a/src/Defaults/index.ts +++ b/src/Defaults/index.ts @@ -80,7 +80,11 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = { cachedGroupMetadata: async () => undefined, makeSignalRepository: makeLibSignalRepository, // Circuit breaker configuration - enableCircuitBreaker: true + enableCircuitBreaker: true, + // Listener limits (memory leak prevention) + // 8 base WS events + 10 dynamic listeners + 2 buffer slots + maxWebSocketListeners: 20, + maxSocketClientListeners: 50 } export const MEDIA_PATH_MAP: { [T in MediaType]?: string } = { diff --git a/src/Socket/Client/types.ts b/src/Socket/Client/types.ts index 3a6ee818..cc7c7024 100644 --- a/src/Socket/Client/types.ts +++ b/src/Socket/Client/types.ts @@ -13,7 +13,13 @@ export abstract class AbstractSocketClient extends EventEmitter { public config: SocketConfig ) { super() - this.setMaxListeners(0) + // Set max listeners from config (default: 50) + // WARNING: 0 disables limit and allows potential memory leaks + const maxListeners = this.config.maxSocketClientListeners ?? 50 + if (maxListeners === 0) { + this.config.logger?.warn('SocketClient setMaxListeners(0) allows UNLIMITED listeners - potential memory leak!') + } + this.setMaxListeners(maxListeners) } abstract connect(): void diff --git a/src/Socket/Client/websocket.ts b/src/Socket/Client/websocket.ts index 6aab1e3c..59632cd2 100644 --- a/src/Socket/Client/websocket.ts +++ b/src/Socket/Client/websocket.ts @@ -31,7 +31,13 @@ export class WebSocketClient extends AbstractSocketClient { agent: this.config.agent }) - this.socket.setMaxListeners(0) + // Set max listeners from config (default: 20) + // WARNING: 0 disables limit and allows potential memory leaks + const maxListeners = this.config.maxWebSocketListeners ?? 20 + if (maxListeners === 0) { + this.config.logger?.warn('WebSocket setMaxListeners(0) allows UNLIMITED listeners - potential memory leak!') + } + this.socket.setMaxListeners(maxListeners) const events = ['close', 'error', 'upgrade', 'message', 'open', 'ping', 'pong', 'unexpected-response'] diff --git a/src/Types/Socket.ts b/src/Types/Socket.ts index 62ab06b5..5d98c075 100644 --- a/src/Types/Socket.ts +++ b/src/Types/Socket.ts @@ -165,4 +165,19 @@ export type SocketConfig = { /** Circuit breaker configuration for message operations */ messageCircuitBreaker?: Partial + + // === Listener Limits (Memory Leak Prevention) === + + /** + * Max WebSocket event listeners (default: 20) + * Calculated as: 8 base WS events + 10 dynamic listeners + 2 buffer slots + * WARNING: Setting to 0 disables limit and allows potential memory leaks! + */ + maxWebSocketListeners?: number + + /** + * Max SocketClient event listeners (default: 50) + * WARNING: Setting to 0 disables limit and allows potential memory leaks! + */ + maxSocketClientListeners?: number }