6f75e5ac21
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.
70 lines
1.8 KiB
TypeScript
70 lines
1.8 KiB
TypeScript
import WebSocket from 'ws'
|
|
import { DEFAULT_ORIGIN } from '../../Defaults'
|
|
import { AbstractSocketClient } from './types'
|
|
|
|
export class WebSocketClient extends AbstractSocketClient {
|
|
protected socket: WebSocket | null = null
|
|
|
|
get isOpen(): boolean {
|
|
return this.socket?.readyState === WebSocket.OPEN
|
|
}
|
|
get isClosed(): boolean {
|
|
return this.socket === null || this.socket?.readyState === WebSocket.CLOSED
|
|
}
|
|
get isClosing(): boolean {
|
|
return this.socket === null || this.socket?.readyState === WebSocket.CLOSING
|
|
}
|
|
get isConnecting(): boolean {
|
|
return this.socket?.readyState === WebSocket.CONNECTING
|
|
}
|
|
|
|
connect() {
|
|
if (this.socket) {
|
|
return
|
|
}
|
|
|
|
this.socket = new WebSocket(this.url, {
|
|
origin: DEFAULT_ORIGIN,
|
|
headers: this.config.options?.headers as {},
|
|
handshakeTimeout: this.config.connectTimeoutMs,
|
|
timeout: this.config.connectTimeoutMs,
|
|
agent: this.config.agent
|
|
})
|
|
|
|
// 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']
|
|
|
|
for (const event of events) {
|
|
this.socket?.on(event, (...args: any[]) => this.emit(event, ...args))
|
|
}
|
|
}
|
|
|
|
async close() {
|
|
if (!this.socket) {
|
|
return
|
|
}
|
|
|
|
const closePromise = new Promise<void>(resolve => {
|
|
this.socket?.once('close', resolve)
|
|
})
|
|
|
|
this.socket.close()
|
|
|
|
await closePromise
|
|
|
|
this.socket = null
|
|
}
|
|
send(str: string | Uint8Array, cb?: (err?: Error) => void): boolean {
|
|
this.socket?.send(str, cb)
|
|
|
|
return Boolean(this.socket)
|
|
}
|
|
}
|