add ws
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
2025-10-07 17:04:35 +08:00
parent 437da9d588
commit 389393b70d
3 changed files with 575 additions and 73 deletions

View 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
/** 心跳间隔(毫秒),默认 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) {
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 延迟时间(毫秒),默认 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),
}
}