import type { ImMessage, SendMessageParams, ImEventMap } from '../types' import { getConfig, getToken } from '../core/sdk' type EventListener = ImEventMap[K] const MAX_RECONNECT_DELAY = 30_000 export class ImClient { private ws: WebSocket | null = null private reconnectDelay = 3_000 private reconnectTimer: ReturnType | null = null private destroyed = false private listeners: { [K in keyof ImEventMap]?: Set> } = {} on(event: K, handler: EventListener): this { if (!this.listeners[event]) { (this.listeners[event] as Set>) = new Set() } (this.listeners[event] as Set>).add(handler) return this } off(event: K, handler: EventListener): this { (this.listeners[event] as Set> | undefined)?.delete(handler) return this } private emit(event: K, ...args: Parameters): void { (this.listeners[event] as Set<(...a: unknown[]) => void> | undefined)?.forEach((h) => h(...(args as unknown[])) ) } connect(): void { if (this.destroyed) return const config = getConfig() const token = getToken() const url = `${config.imBaseUrl}/ws/im?token=${token ?? ''}` this.ws = new WebSocket(url) this.ws.onopen = () => { this.reconnectDelay = 3_000 if (config.debug) console.log('[ImClient] connected') this.emit('connected') } this.ws.onmessage = (event) => { try { const frame = JSON.parse(event.data as string) if (frame.type === 'MESSAGE') { this.emit('message', frame.payload as ImMessage) } else if (frame.type === 'REVOKE') { this.emit('revoke', frame.payload as { msgId: string; operatorId: string }) } } catch { // ignore malformed frames } } this.ws.onclose = (event) => { this.emit('disconnected', event.code, event.reason) if (!this.destroyed) this.scheduleReconnect() } this.ws.onerror = (event) => { this.emit('error', event) } } send(params: SendMessageParams): void { if (this.ws?.readyState !== WebSocket.OPEN) { throw new Error('WebSocket not connected') } this.ws.send( JSON.stringify({ destination: '/app/chat.send', payload: params, }) ) } revoke(msgId: string): void { if (this.ws?.readyState !== WebSocket.OPEN) { throw new Error('WebSocket not connected') } this.ws.send( JSON.stringify({ destination: '/app/chat.revoke', payload: { msgId }, }) ) } disconnect(): void { this.destroyed = true if (this.reconnectTimer) clearTimeout(this.reconnectTimer) this.ws?.close() this.ws = null } private scheduleReconnect(): void { if (this.destroyed) return if (getConfig().debug) { console.log(`[ImClient] reconnect in ${this.reconnectDelay}ms`) } this.reconnectTimer = setTimeout(() => { this.connect() this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY) }, this.reconnectDelay) } }