XuqmGroup-Vue3SDK/src/im/ImClient.ts

113 行
3.2 KiB
TypeScript

2026-04-21 22:07:29 +08:00
import type { ImMessage, SendMessageParams, ImEventMap } from '../types'
import { getConfig, getToken } from '../core/sdk'
type EventListener<K extends keyof ImEventMap> = ImEventMap[K]
const MAX_RECONNECT_DELAY = 30_000
export class ImClient {
private ws: WebSocket | null = null
private reconnectDelay = 3_000
private reconnectTimer: ReturnType<typeof setTimeout> | null = null
private destroyed = false
private listeners: { [K in keyof ImEventMap]?: Set<EventListener<K>> } = {}
on<K extends keyof ImEventMap>(event: K, handler: EventListener<K>): this {
if (!this.listeners[event]) {
(this.listeners[event] as Set<EventListener<K>>) = new Set()
}
(this.listeners[event] as Set<EventListener<K>>).add(handler)
return this
}
off<K extends keyof ImEventMap>(event: K, handler: EventListener<K>): this {
(this.listeners[event] as Set<EventListener<K>> | undefined)?.delete(handler)
return this
}
private emit<K extends keyof ImEventMap>(event: K, ...args: Parameters<ImEventMap[K]>): 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)
}
}