2026-04-14 11:37:37 +08:00
|
|
|
import { getToken } from '@/utils/auth'
|
|
|
|
|
|
|
|
|
|
let ws = null
|
|
|
|
|
let reconnectTimer = null
|
|
|
|
|
let manualClose = false
|
2026-04-15 08:32:42 +08:00
|
|
|
let retryCount = 0
|
2026-04-20 09:11:41 +08:00
|
|
|
const subscribers = new Set()
|
2026-04-14 11:37:37 +08:00
|
|
|
|
|
|
|
|
const WS_PATH = '/ws'
|
2026-04-15 08:32:42 +08:00
|
|
|
const MAX_RECONNECT_DELAY = 30000
|
2026-04-14 11:37:37 +08:00
|
|
|
|
|
|
|
|
function getWsBaseUrl() {
|
|
|
|
|
const baseApi = import.meta.env.VITE_APP_BASE_API || ''
|
|
|
|
|
|
|
|
|
|
if (/^https?:\/\//.test(baseApi)) {
|
|
|
|
|
return baseApi.replace(/^http/, 'ws').replace(/\/$/, '')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
|
|
|
return `${protocol}//${window.location.host}${baseApi}`.replace(/\/$/, '')
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:32:42 +08:00
|
|
|
function buildWsUrl() {
|
2026-04-14 11:37:37 +08:00
|
|
|
const url = new URL(`${getWsBaseUrl()}${WS_PATH}`)
|
|
|
|
|
const token = getToken()
|
|
|
|
|
|
|
|
|
|
if (token) {
|
|
|
|
|
url.searchParams.set('token', token)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return url.toString()
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:32:42 +08:00
|
|
|
export function connectWs(onMessage) {
|
2026-04-22 15:41:41 +08:00
|
|
|
console.trace('connectWs called')
|
2026-04-15 08:32:42 +08:00
|
|
|
if (!getToken()) {
|
|
|
|
|
console.warn('[WebSocket] missing token')
|
2026-04-14 11:37:37 +08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 09:11:41 +08:00
|
|
|
if (typeof onMessage === 'function') {
|
|
|
|
|
subscribers.add(onMessage)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 11:37:37 +08:00
|
|
|
manualClose = false
|
|
|
|
|
clearReconnectTimer()
|
|
|
|
|
|
|
|
|
|
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:32:42 +08:00
|
|
|
ws = new WebSocket(buildWsUrl())
|
2026-04-14 11:37:37 +08:00
|
|
|
|
|
|
|
|
ws.onopen = () => {
|
2026-04-15 08:32:42 +08:00
|
|
|
retryCount = 0
|
|
|
|
|
console.log('[WebSocket] connected')
|
2026-04-14 11:37:37 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ws.onmessage = (event) => {
|
|
|
|
|
let data = event.data
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
data = JSON.parse(event.data)
|
|
|
|
|
} catch (e) {
|
2026-04-15 08:32:42 +08:00
|
|
|
// Keep plain text messages as-is.
|
2026-04-14 11:37:37 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 09:11:41 +08:00
|
|
|
subscribers.forEach((handler) => {
|
|
|
|
|
try {
|
|
|
|
|
handler(data, event)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[WebSocket] subscriber error', error)
|
|
|
|
|
}
|
|
|
|
|
})
|
2026-04-14 11:37:37 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 15:41:41 +08:00
|
|
|
ws.onclose = (event) => {
|
|
|
|
|
console.log('[WebSocket] closed', { code: event.code, reason: event.reason })
|
2026-04-14 11:37:37 +08:00
|
|
|
ws = null
|
|
|
|
|
|
2026-04-22 15:41:41 +08:00
|
|
|
if (isNonRetryableClose(event)) {
|
|
|
|
|
console.warn('[WebSocket] stop reconnect because close reason is not retryable', {
|
|
|
|
|
code: event.code,
|
|
|
|
|
reason: event.reason
|
|
|
|
|
})
|
|
|
|
|
retryCount = 0
|
|
|
|
|
clearReconnectTimer()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 09:11:41 +08:00
|
|
|
if (!manualClose && subscribers.size > 0) {
|
2026-04-14 11:37:37 +08:00
|
|
|
reconnectTimer = setTimeout(() => {
|
2026-04-15 08:32:42 +08:00
|
|
|
console.log('[WebSocket] reconnecting')
|
2026-04-20 09:11:41 +08:00
|
|
|
connectWs()
|
2026-04-15 08:32:42 +08:00
|
|
|
}, getReconnectDelay())
|
2026-04-14 11:37:37 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ws.onerror = (error) => {
|
2026-04-15 08:32:42 +08:00
|
|
|
console.error('[WebSocket] error', error)
|
2026-04-14 11:37:37 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 09:11:41 +08:00
|
|
|
export function closeWs(onMessage) {
|
|
|
|
|
if (typeof onMessage === 'function') {
|
|
|
|
|
subscribers.delete(onMessage)
|
|
|
|
|
} else {
|
|
|
|
|
subscribers.clear()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (subscribers.size > 0) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 11:37:37 +08:00
|
|
|
manualClose = true
|
2026-04-15 08:32:42 +08:00
|
|
|
retryCount = 0
|
2026-04-14 11:37:37 +08:00
|
|
|
clearReconnectTimer()
|
|
|
|
|
if (ws) {
|
|
|
|
|
ws.close()
|
|
|
|
|
ws = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 09:11:41 +08:00
|
|
|
export function reconnectWs(onMessage) {
|
|
|
|
|
if (typeof onMessage === 'function') {
|
|
|
|
|
subscribers.add(onMessage)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
manualClose = false
|
|
|
|
|
retryCount = 0
|
|
|
|
|
clearReconnectTimer()
|
|
|
|
|
|
|
|
|
|
if (ws) {
|
|
|
|
|
ws.close()
|
|
|
|
|
ws = null
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:32:42 +08:00
|
|
|
manualClose = false
|
2026-04-20 09:11:41 +08:00
|
|
|
connectWs()
|
2026-04-15 08:32:42 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-14 11:37:37 +08:00
|
|
|
export function sendWs(data) {
|
|
|
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
2026-04-15 08:32:42 +08:00
|
|
|
console.warn('[WebSocket] send failed, socket is not open')
|
2026-04-14 11:37:37 +08:00
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ws.send(typeof data === 'string' ? data : JSON.stringify(data))
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getWsState() {
|
|
|
|
|
return ws ? ws.readyState : WebSocket.CLOSED
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:32:42 +08:00
|
|
|
function getReconnectDelay() {
|
|
|
|
|
retryCount += 1
|
|
|
|
|
return Math.min(5000 * retryCount, MAX_RECONNECT_DELAY)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 11:37:37 +08:00
|
|
|
function clearReconnectTimer() {
|
|
|
|
|
if (reconnectTimer) {
|
|
|
|
|
clearTimeout(reconnectTimer)
|
|
|
|
|
reconnectTimer = null
|
|
|
|
|
}
|
2026-04-15 08:32:42 +08:00
|
|
|
}
|
2026-04-22 15:41:41 +08:00
|
|
|
|
|
|
|
|
function isNonRetryableClose(event) {
|
|
|
|
|
const reason = (event?.reason || '').toLowerCase()
|
|
|
|
|
return (
|
|
|
|
|
reason.includes('invalid token') ||
|
|
|
|
|
reason.includes('missing token') ||
|
|
|
|
|
reason.includes('deptid is null')
|
|
|
|
|
)
|
|
|
|
|
}
|