Files
czzj-app/src/utils/message-socket.ts
2026-10-13 16:23:43 +08:00

129 lines
3.1 KiB
TypeScript

import { getStoredToken } from '@/utils/auth-token'
import type { MessagePushPayload } from '@/types/message'
const HEARTBEAT_INTERVAL_MS = 25000
const RECONNECT_DELAY_MS = 25000
let socketTask: UniApp.SocketTask | null = null
let heartbeatTimer: ReturnType<typeof setInterval> | null = null
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
let connecting = false
let manualClosed = false
const listeners = new Set<(payload: MessagePushPayload) => void>()
function resolveMessageWsBase(): string {
const configured = import.meta.env.VITE_MESSAGE_WS_URL?.trim()
if (configured) {
return configured.replace(/[?#].*$/, '').replace(/\/$/, '')
}
const envBase = import.meta.env.VITE_API_BASE_URL?.trim() || ''
const fallback = import.meta.env.VITE_API_PROXY_TARGET?.trim() || 'http://192.168.0.222:8082'
const httpBase = envBase || fallback
const wsBase = httpBase.replace(/^https/i, 'wss').replace(/^http/i, 'ws')
return `${wsBase}/app/ws/message`
}
function resolveMessageWsUrl(token: string): string {
const base = resolveMessageWsBase()
return `${base}?token=${encodeURIComponent(token)}`
}
function stopHeartbeat() {
if (heartbeatTimer) clearInterval(heartbeatTimer)
heartbeatTimer = null
}
function startHeartbeat() {
stopHeartbeat()
heartbeatTimer = setInterval(() => {
socketTask?.send({ data: 'ping' })
}, HEARTBEAT_INTERVAL_MS)
}
function clearReconnectTimer() {
if (reconnectTimer) clearTimeout(reconnectTimer)
reconnectTimer = null
}
function scheduleReconnect() {
if (manualClosed || !getStoredToken()) return
clearReconnectTimer()
reconnectTimer = setTimeout(() => {
connectMessageSocket()
}, RECONNECT_DELAY_MS)
}
function cleanupSocketTask() {
stopHeartbeat()
socketTask = null
connecting = false
}
function handleSocketMessage(raw: string) {
if (!raw || raw === 'ping' || raw === 'pong') return
try {
const data = JSON.parse(raw) as MessagePushPayload
listeners.forEach((listener) => listener(data))
if (data.title) {
uni.showToast({ title: data.title, icon: 'none' })
}
} catch {
// ignore invalid payload
}
}
export function onMessagePush(listener: (payload: MessagePushPayload) => void) {
listeners.add(listener)
return () => listeners.delete(listener)
}
export function connectMessageSocket() {
const token = getStoredToken()
if (!token || connecting || socketTask) return
manualClosed = false
connecting = true
clearReconnectTimer()
const task = uni.connectSocket({
url: resolveMessageWsUrl(token),
complete: () => {
connecting = false
},
})
socketTask = task
task.onOpen(() => {
startHeartbeat()
})
task.onMessage((res) => {
const raw = typeof res.data === 'string' ? res.data : ''
handleSocketMessage(raw)
})
task.onClose(() => {
cleanupSocketTask()
scheduleReconnect()
})
task.onError(() => {
cleanupSocketTask()
scheduleReconnect()
})
}
export function disconnectMessageSocket() {
manualClosed = true
clearReconnectTimer()
stopHeartbeat()
if (socketTask) {
socketTask.close({})
}
socketTask = null
connecting = false
}