XuqmGroup-Vue3SDK/src/im/ImClient.ts

148 行
4.3 KiB
TypeScript

2026-04-21 22:07:29 +08:00
import type { ImMessage, SendMessageParams, ImEventMap } from '../types'
import { getConfig, getToken, getUserId } from '../core/sdk'
2026-04-21 22:07:29 +08:00
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 {
const store = this.listeners as Record<string, Set<unknown>>
const listeners = (store[event] ?? new Set<EventListener<K>>()) as Set<EventListener<K>>
listeners.add(handler)
store[event] = listeners as Set<unknown>
2026-04-21 22:07:29 +08:00
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', this.normalizeMessage(frame.payload as ImMessage))
2026-04-21 22:07:29 +08:00
} 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): ImMessage {
const config = getConfig()
const userId = getUserId() ?? ''
const messageId = params.messageId ?? this.generateMessageId()
const outgoing: ImMessage = {
id: messageId,
appId: config.appKey,
fromUserId: userId,
fromId: userId,
toId: params.toId,
chatType: params.chatType,
msgType: params.msgType,
content: params.content,
status: 'SENDING',
mentionedUserIds: params.mentionedUserIds,
revoked: false,
createdAt: new Date().toISOString(),
}
2026-04-21 22:07:29 +08:00
if (this.ws?.readyState !== WebSocket.OPEN) {
return { ...outgoing, status: 'FAILED' }
2026-04-21 22:07:29 +08:00
}
this.ws.send(
JSON.stringify({
destination: '/app/chat.send',
payload: {
...params,
messageId,
},
2026-04-21 22:07:29 +08:00
})
)
return outgoing
2026-04-21 22:07:29 +08:00
}
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)
}
private generateMessageId(): string {
const cryptoId = globalThis.crypto?.randomUUID?.()
if (cryptoId) return cryptoId
return `msg_${Date.now()}_${Math.random().toString(16).slice(2)}`
}
private normalizeMessage(message: ImMessage): ImMessage {
return {
...message,
fromId: message.fromId ?? message.fromUserId,
revoked: message.revoked ?? message.status === 'REVOKED',
}
}
2026-04-21 22:07:29 +08:00
}