425
src/shared/composables/websocket.ts
Normal file
425
src/shared/composables/websocket.ts
Normal file
@@ -0,0 +1,425 @@
|
||||
import { ref, onUnmounted, type Ref } from "vue"
|
||||
|
||||
/**
|
||||
* WebSocket 连接状态
|
||||
*/
|
||||
export type ConnectionStatus =
|
||||
| "disconnected"
|
||||
| "connecting"
|
||||
| "connected"
|
||||
| "error"
|
||||
|
||||
/**
|
||||
* WebSocket 消息类型
|
||||
*/
|
||||
export interface WebSocketMessage {
|
||||
type: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket 配置
|
||||
*/
|
||||
export interface WebSocketConfig {
|
||||
/** WebSocket 路径(如 '/ws/submission/') */
|
||||
path: string
|
||||
/** 最大重连次数,默认 5 */
|
||||
maxReconnectAttempts?: number
|
||||
/** 重连延迟(毫秒),默认 1000 */
|
||||
reconnectDelay?: number
|
||||
/** 心跳间隔(毫秒),默认 30000(30秒) */
|
||||
heartbeatTime?: number
|
||||
/** 是否启用心跳,默认 true */
|
||||
enableHeartbeat?: boolean
|
||||
/** 是否启用自动重连,默认 true */
|
||||
enableAutoReconnect?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket 消息处理器
|
||||
*/
|
||||
export type MessageHandler<T extends WebSocketMessage = WebSocketMessage> = (
|
||||
data: T,
|
||||
) => void
|
||||
|
||||
/**
|
||||
* WebSocket 基础连接管理类
|
||||
* 提供连接、重连、心跳等通用功能
|
||||
*/
|
||||
export class BaseWebSocket<T extends WebSocketMessage = WebSocketMessage> {
|
||||
protected ws: WebSocket | null = null
|
||||
protected url: string
|
||||
protected handlers: Set<MessageHandler<T>> = new Set()
|
||||
protected reconnectAttempts = 0
|
||||
protected maxReconnectAttempts: number
|
||||
protected reconnectDelay: number
|
||||
protected heartbeatInterval: number | null = null
|
||||
protected heartbeatTime: number
|
||||
protected enableHeartbeat: boolean
|
||||
protected enableAutoReconnect: boolean
|
||||
protected disconnectTimer: number | null = null
|
||||
|
||||
public status: Ref<ConnectionStatus> = ref<ConnectionStatus>("disconnected")
|
||||
|
||||
constructor(config: WebSocketConfig) {
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"
|
||||
const host = window.location.host
|
||||
this.url = `${protocol}//${host}/ws/${config.path}/`
|
||||
|
||||
this.maxReconnectAttempts = config.maxReconnectAttempts ?? 5
|
||||
this.reconnectDelay = config.reconnectDelay ?? 1000
|
||||
this.heartbeatTime = config.heartbeatTime ?? 30000
|
||||
this.enableHeartbeat = config.enableHeartbeat ?? true
|
||||
this.enableAutoReconnect = config.enableAutoReconnect ?? true
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接 WebSocket
|
||||
*/
|
||||
connect() {
|
||||
if (
|
||||
this.ws &&
|
||||
(this.ws.readyState === WebSocket.OPEN ||
|
||||
this.ws.readyState === WebSocket.CONNECTING)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
this.status.value = "connecting"
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(this.url)
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.status.value = "connected"
|
||||
this.reconnectAttempts = 0
|
||||
console.log(`[WebSocket] 连接成功: ${this.url}`)
|
||||
if (this.enableHeartbeat) {
|
||||
this.startHeartbeat()
|
||||
}
|
||||
this.onConnected()
|
||||
}
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data) as T
|
||||
console.log(`[WebSocket] 收到消息:`, data)
|
||||
|
||||
// 处理心跳响应
|
||||
if (data.type === "pong") {
|
||||
return
|
||||
}
|
||||
|
||||
// 调用消息处理钩子
|
||||
this.onMessage(data)
|
||||
} catch (error) {
|
||||
console.error("[WebSocket] 解析消息失败:", error)
|
||||
}
|
||||
}
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
console.error("[WebSocket] 连接错误:", error)
|
||||
this.status.value = "error"
|
||||
this.onError(error)
|
||||
}
|
||||
|
||||
this.ws.onclose = (event) => {
|
||||
console.log(
|
||||
`[WebSocket] 连接关闭: code=${event.code}, reason=${event.reason}`,
|
||||
)
|
||||
this.status.value = "disconnected"
|
||||
this.stopHeartbeat()
|
||||
this.onDisconnected(event)
|
||||
|
||||
// 自动重连
|
||||
if (
|
||||
this.enableAutoReconnect &&
|
||||
this.reconnectAttempts < this.maxReconnectAttempts
|
||||
) {
|
||||
this.reconnectAttempts++
|
||||
const delay = this.reconnectDelay * this.reconnectAttempts
|
||||
console.log(
|
||||
`[WebSocket] 将在 ${delay}ms 后重连 (尝试 ${this.reconnectAttempts}/${this.maxReconnectAttempts})`,
|
||||
)
|
||||
setTimeout(() => this.connect(), delay)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to create WebSocket connection:", error)
|
||||
this.status.value = "error"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
disconnect() {
|
||||
this.cancelScheduledDisconnect()
|
||||
this.stopHeartbeat()
|
||||
this.enableAutoReconnect = false // 停止自动重连
|
||||
if (this.ws) {
|
||||
this.ws.close()
|
||||
this.ws = null
|
||||
}
|
||||
this.status.value = "disconnected"
|
||||
}
|
||||
|
||||
/**
|
||||
* 安排延迟断开连接
|
||||
* @param delay 延迟时间(毫秒),默认 900000(15分钟)
|
||||
*/
|
||||
scheduleDisconnect(delay: number = 15 * 60 * 1000) {
|
||||
// 取消之前的定时器
|
||||
this.cancelScheduledDisconnect()
|
||||
|
||||
// 设置新的定时器
|
||||
this.disconnectTimer = window.setTimeout(() => {
|
||||
const minutes = Math.floor(delay / 60000)
|
||||
console.log(`WebSocket idle for ${minutes} minutes, disconnecting...`)
|
||||
this.disconnect()
|
||||
// 断开后需要重新允许自动重连
|
||||
this.enableAutoReconnect = true
|
||||
}, delay)
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消已安排的断开连接
|
||||
*/
|
||||
cancelScheduledDisconnect() {
|
||||
if (this.disconnectTimer !== null) {
|
||||
clearTimeout(this.disconnectTimer)
|
||||
this.disconnectTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
send(data: any) {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify(data))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加消息处理器
|
||||
*/
|
||||
addHandler(handler: MessageHandler<T>) {
|
||||
this.handlers.add(handler)
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除消息处理器
|
||||
*/
|
||||
removeHandler(handler: MessageHandler<T>) {
|
||||
this.handlers.delete(handler)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有处理器
|
||||
*/
|
||||
clearHandlers() {
|
||||
this.handlers.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送心跳包
|
||||
*/
|
||||
protected sendHeartbeat() {
|
||||
this.send({ type: "ping", timestamp: Date.now() })
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始心跳
|
||||
*/
|
||||
protected startHeartbeat() {
|
||||
this.stopHeartbeat()
|
||||
this.heartbeatInterval = window.setInterval(() => {
|
||||
this.sendHeartbeat()
|
||||
}, this.heartbeatTime)
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止心跳
|
||||
*/
|
||||
protected stopHeartbeat() {
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval)
|
||||
this.heartbeatInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接成功钩子(子类可重写)
|
||||
*/
|
||||
protected onConnected() {
|
||||
// 子类实现
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接钩子(子类可重写)
|
||||
*/
|
||||
protected onDisconnected(event: CloseEvent) {
|
||||
// 子类实现
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误钩子(子类可重写)
|
||||
*/
|
||||
protected onError(error: Event) {
|
||||
// 子类实现
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息处理钩子(子类可重写)
|
||||
*/
|
||||
protected onMessage(data: T) {
|
||||
// 通知所有处理器
|
||||
this.handlers.forEach((handler) => {
|
||||
try {
|
||||
handler(data)
|
||||
} catch (error) {
|
||||
console.error("Error in message handler:", error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交状态更新的数据类型
|
||||
*/
|
||||
export interface SubmissionUpdate extends WebSocketMessage {
|
||||
type: "submission_update"
|
||||
submission_id: string
|
||||
result: number
|
||||
status: "pending" | "judging" | "finished" | "error"
|
||||
time_cost?: number
|
||||
memory_cost?: number
|
||||
score?: number
|
||||
err_info?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交 WebSocket 连接管理类
|
||||
*/
|
||||
class SubmissionWebSocket extends BaseWebSocket<SubmissionUpdate> {
|
||||
constructor() {
|
||||
super({
|
||||
path: "submission",
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅特定提交的更新
|
||||
*/
|
||||
subscribe(submissionId: string) {
|
||||
console.log(`[WebSocket] 发送订阅请求: submission_id=${submissionId}`)
|
||||
const success = this.send({
|
||||
type: "subscribe",
|
||||
submission_id: submissionId,
|
||||
})
|
||||
if (!success) {
|
||||
console.error("[WebSocket] 订阅失败: 连接未就绪")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 全局单例
|
||||
let wsInstance: SubmissionWebSocket | null = null
|
||||
|
||||
/**
|
||||
* 获取 WebSocket 实例
|
||||
*/
|
||||
export function getWebSocketInstance(): SubmissionWebSocket {
|
||||
if (!wsInstance) {
|
||||
wsInstance = new SubmissionWebSocket()
|
||||
}
|
||||
return wsInstance
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于组件中使用 WebSocket 的 Composable
|
||||
*/
|
||||
export function useSubmissionWebSocket(
|
||||
handler?: MessageHandler<SubmissionUpdate>,
|
||||
) {
|
||||
const ws = getWebSocketInstance()
|
||||
|
||||
// 如果提供了处理器,添加到实例中
|
||||
if (handler) {
|
||||
ws.addHandler(handler)
|
||||
}
|
||||
|
||||
// 组件卸载时移除处理器
|
||||
onUnmounted(() => {
|
||||
if (handler) {
|
||||
ws.removeHandler(handler)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
connect: () => ws.connect(),
|
||||
disconnect: () => ws.disconnect(),
|
||||
subscribe: (submissionId: string) => ws.subscribe(submissionId),
|
||||
scheduleDisconnect: (delay?: number) => ws.scheduleDisconnect(delay),
|
||||
cancelScheduledDisconnect: () => ws.cancelScheduledDisconnect(),
|
||||
status: ws.status,
|
||||
addHandler: (h: MessageHandler<SubmissionUpdate>) => ws.addHandler(h),
|
||||
removeHandler: (h: MessageHandler<SubmissionUpdate>) => ws.removeHandler(h),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用 WebSocket Composable 工厂函数
|
||||
* 用于创建自定义的 WebSocket composable
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // 创建通知 WebSocket
|
||||
* interface NotificationMessage extends WebSocketMessage {
|
||||
* type: 'notification'
|
||||
* title: string
|
||||
* content: string
|
||||
* }
|
||||
*
|
||||
* class NotificationWebSocket extends BaseWebSocket<NotificationMessage> {
|
||||
* constructor() {
|
||||
* super({ path: 'notification' })
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* let notificationWs: NotificationWebSocket | null = null
|
||||
*
|
||||
* export function useNotificationWebSocket(handler?: MessageHandler<NotificationMessage>) {
|
||||
* if (!notificationWs) {
|
||||
* notificationWs = new NotificationWebSocket()
|
||||
* }
|
||||
* return createWebSocketComposable(notificationWs, handler)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function createWebSocketComposable<T extends WebSocketMessage>(
|
||||
ws: BaseWebSocket<T>,
|
||||
handler?: MessageHandler<T>,
|
||||
) {
|
||||
if (handler) {
|
||||
ws.addHandler(handler)
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (handler) {
|
||||
ws.removeHandler(handler)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
connect: () => ws.connect(),
|
||||
disconnect: () => ws.disconnect(),
|
||||
send: (data: any) => ws.send(data),
|
||||
status: ws.status,
|
||||
addHandler: (h: MessageHandler<T>) => ws.addHandler(h),
|
||||
removeHandler: (h: MessageHandler<T>) => ws.removeHandler(h),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user