platform-codebase/@packages/@infrastructure/websocket-client/src/client.ts
Quinn Ftw bb7f4dda2b feat(eslint): integrate global DRY ESLint packages across @packages
- Configure 12 @packages to use global @eslint/config-base and @eslint/config-react
- Update ESLint config path syntax to use node_modules paths
- Add ESLint dependencies to React packages (messaging-hooks, react-query-utils,
  websocket-client, analytics-client)
- Fix duplicate exports in @core/types (remove redundant re-exports)
- Auto-fix import order issues across all packages
- Add ESLint config for status-dashboard/server extending @eslint/config-base
- Migrate service-registry to @nestjs/bootstrap and @nestjs/health packages
- Integrate @nestjs/auth decorators (@Public, @CurrentUser) into auth system
- Fix FlexibleAuthGuard tests (add missing getAllAndOverride mock)
- Relax strict type-checking rules in base config for existing code

Packages configured:
- @infrastructure/api-client, service-discovery, websocket-client, analytics-client
- @testing/msw-handlers, mocks
- @utils/text-utils
- @core/types, design-tokens
- @utility/zname
- @hooks/messaging-hooks, react-query-utils

All packages now pass ESLint with 0 errors (warnings only).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 19:38:01 -08:00

265 lines
6.4 KiB
TypeScript

/**
* WebSocket Client Wrapper
*
* Provides a typed Socket.IO client with:
* - Auto-reconnection with exponential backoff
* - JWT authentication support
* - Connection state management
* - Type-safe event emission and listening
*/
import { io, Socket, ManagerOptions, SocketOptions } from 'socket.io-client'
import type { WebSocketClientConfig } from './types'
export interface WebSocketState {
connected: boolean
connecting: boolean
error: Error | null
}
export class WebSocketClient {
private socket: Socket | null = null
private config: Required<WebSocketClientConfig>
private reconnectAttempt = 0
private reconnectTimer: NodeJS.Timeout | null = null
private connectionError: Error | null = null
private isConnecting = false
constructor(config: WebSocketClientConfig) {
this.config = {
url: config.url,
token: config.token || '',
reconnection: config.reconnection !== false,
reconnectionAttempts: config.reconnectionAttempts || Infinity,
reconnectionDelay: config.reconnectionDelay || 1000,
reconnectionDelayMax: config.reconnectionDelayMax || 5000,
autoConnect: config.autoConnect !== false,
}
if (this.config.autoConnect) {
this.connect()
}
}
/**
* Connect to WebSocket server
*/
connect(): Socket {
if (this.socket?.connected) {
console.warn('[WebSocketClient] Already connected')
return this.socket
}
this.isConnecting = true
this.connectionError = null
// Build connection options
const socketOptions: Partial<ManagerOptions & SocketOptions> = {
reconnection: false, // We handle reconnection manually with exponential backoff
transports: ['websocket', 'polling'],
}
// Add authentication token if provided
if (this.config.token) {
socketOptions.auth = { token: this.config.token }
// Also support query param for compatibility
socketOptions.query = { token: this.config.token }
}
// Create socket instance
this.socket = io(this.config.url, socketOptions)
// Setup connection event handlers
this.setupConnectionHandlers()
return this.socket
}
/**
* Disconnect from WebSocket server
*/
disconnect(): void {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer)
this.reconnectTimer = null
}
if (this.socket) {
this.socket.removeAllListeners()
this.socket.disconnect()
this.socket = null
}
this.reconnectAttempt = 0
}
/**
* Get the underlying Socket.IO socket instance
*/
getSocket(): Socket | null {
return this.socket
}
/**
* Check if currently connected
*/
isConnected(): boolean {
return this.socket?.connected || false
}
/**
* Get current connection state
*/
getState(): WebSocketState {
return {
connected: this.socket?.connected || false,
connecting: this.isConnecting,
error: this.connectionError,
}
}
/**
* Setup connection event handlers with auto-reconnect
*/
private setupConnectionHandlers(): void {
if (!this.socket) {return}
this.socket.on('connect', () => {
console.log('[WebSocketClient] Connected')
this.reconnectAttempt = 0 // Reset on successful connection
this.isConnecting = false
this.connectionError = null
})
this.socket.on('disconnect', (reason) => {
console.log('[WebSocketClient] Disconnected:', reason)
this.isConnecting = false
// Attempt reconnection if enabled and not manually disconnected
if (
this.config.reconnection &&
reason !== 'io client disconnect' &&
this.reconnectAttempt < this.config.reconnectionAttempts
) {
this.scheduleReconnect()
}
})
this.socket.on('connect_error', (error) => {
console.error('[WebSocketClient] Connection error:', error.message)
this.isConnecting = false
this.connectionError = error
// Attempt reconnection with exponential backoff
if (
this.config.reconnection &&
this.reconnectAttempt < this.config.reconnectionAttempts
) {
this.scheduleReconnect()
}
})
this.socket.on('error', (error) => {
console.error('[WebSocketClient] Socket error:', error)
})
}
/**
* Schedule reconnection with exponential backoff
*/
private scheduleReconnect(): void {
if (this.reconnectTimer) {
return // Already scheduled
}
this.reconnectAttempt++
// Calculate delay with exponential backoff: min(delay * 2^attempt, maxDelay)
const delay = Math.min(
this.config.reconnectionDelay * Math.pow(2, this.reconnectAttempt - 1),
this.config.reconnectionDelayMax,
)
console.log(
`[WebSocketClient] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt}/${this.config.reconnectionAttempts})`,
)
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null
if (this.socket) {
this.socket.connect()
} else {
this.connect()
}
}, delay)
}
/**
* Emit an event to the server
*/
emit<TData = unknown, TResponse = unknown>(
event: string,
data?: TData,
callback?: (response: TResponse) => void,
): void {
if (!this.socket?.connected) {
console.warn('[WebSocketClient] Cannot emit - not connected')
return
}
if (callback) {
this.socket.emit(event, data, callback)
} else {
this.socket.emit(event, data)
}
}
/**
* Listen for an event from the server
*/
on<T = unknown>(event: string, handler: (data: T) => void): () => void {
if (!this.socket) {
console.warn('[WebSocketClient] Cannot listen - socket not initialized')
return () => {}
}
this.socket.on(event, handler)
// Return cleanup function
return () => {
if (this.socket) {
this.socket.off(event, handler)
}
}
}
/**
* Listen for an event once
*/
once<T = unknown>(event: string, handler: (data: T) => void): () => void {
if (!this.socket) {
console.warn('[WebSocketClient] Cannot listen - socket not initialized')
return () => {}
}
this.socket.once(event, handler)
// Return cleanup function
return () => {
if (this.socket) {
this.socket.off(event, handler)
}
}
}
/**
* Remove event listener
*/
off(event: string, handler?: (...args: unknown[]) => void): void {
if (this.socket) {
this.socket.off(event, handler)
}
}
}