Files
ojnext/src/shared/composables/websocket.ts
yuetsh 657047793d
Some checks failed
Deploy / deploy (push) Has been cancelled
fix
2025-10-07 21:35:07 +08:00

424 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
/** 心跳间隔(毫秒),默认 3000030秒 */
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) {
this.url = `${import.meta.env.PUBLIC_WS_URL}/${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 延迟时间(毫秒),默认 90000015分钟
*/
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),
}
}