refactor(前端): websocket 合并三层继承与三份重复 composable,803 → 660 行
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
## 删掉的 - **六个从没人传过的配置项。** WebSocketConfig 上的 maxReconnectAttempts / reconnectDelay / maxReconnectDelay / heartbeatTime / enableHeartbeat / enableAutoReconnect,六个默认值、六处 ??,而三条通道从建起来到现在没有一个调用方 传过任何一个,也没在运行时改过。收成三个模块常量,连带三处恒真/恒不触发的分支 (if (enableHeartbeat)、!enableAutoReconnect、reconnectAttempts >= Infinity)。 - **一个没人用的工厂。** createWebSocketComposable 顶着二十五行 @example,示例里那条 通知通道并不存在,三个真实 composable 一个都没调它,各自手写一遍转发对象。 - **三个空壳子类。** SubmissionWebSocket / FlowchartWebSocket / ConfigWebSocket 的全部 内容就是在构造函数里拼 URL;前两个连 URL 都是同一条 /ws/submissions,两个类加两份 三十行 composable 的差异只有一个类型参数。改成 useChannel(path, handler) 一个工厂, 三个 useXxxWebSocket 各自只剩一行。 - SubscribingWebSocket 这一层并进基类:三层继承变两层,只剩 CollabWebSocket 还在扩展 (它确有自己的二进制缓冲与房间语义)。URL 拼接收进 channelUrl()。 行为一处没动:重连仍然不封顶(后端 deploy 重启一次就超过原来那 15 秒,这条注释保留)、 指数退避带抖动、30 秒上限、心跳 30 秒、force_logout、断线重连补订阅全部照旧。 ## 验证 起全栈用浏览器实跑: - 提交一发 Python3,/ws/submissions 连上并推回 judging → judging → finished, 控制台没有出现「WebSocket未及时响应,启动轮询保底」,即结果确实走的 WS 而非轮询; - kill 掉后端观察三条通道重连:682ms → 1.4s → 2.4s → 4.4s → …… → 15~26s 封顶, 指数退避与 30 秒上限均符合; - 后端起回来后**不刷新页面**自动恢复(/ws/config、/ws/collab 各自重连成功), 再提交一发仍然全程走 WS —— 这正是「重连不封顶」要保的场景; - vue-tsc --noEmit exit 0;vite build 通过。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012j1vgeDqay8wKCh8dPgPcH
This commit is contained in:
@@ -47,27 +47,23 @@ export interface WebSocketMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WebSocket 配置
|
* 重连与心跳的参数。
|
||||||
|
*
|
||||||
|
* 原来这三个(连同「最大重连次数」「是否心跳」「是否自动重连」)是构造函数上的六个
|
||||||
|
* 可选项,六个默认值、六处 `??` —— 而三条通道从建起来到现在,**没有一个调用方传过
|
||||||
|
* 任何一个**,也从没在运行时改过。所以它们是常量,不是配置。
|
||||||
|
*
|
||||||
|
* **重连不封顶**是有意的:原来默认 5 次、线性退避,加起来只有 15 秒 —— 后端 deploy
|
||||||
|
* 重启一次就超了,之后这条连接死到用户刷新页面为止。机房网络抖动同理。
|
||||||
*/
|
*/
|
||||||
export interface WebSocketConfig {
|
const RECONNECT_BASE_DELAY = 1000
|
||||||
/** 完整 URL。后端只认 /ws/submissions 和 /ws/config 两条,按当前页面的协议与 host 拼 */
|
const RECONNECT_MAX_DELAY = 30_000
|
||||||
url: string
|
const HEARTBEAT_INTERVAL = 30_000
|
||||||
/**
|
|
||||||
* 最大重连次数,默认不限。
|
/** 按当前页面的协议与 host 拼通道地址。后端只认 /ws/submissions、/ws/config、/ws/collab */
|
||||||
* 原来默认 5 次、线性退避,加起来只有 15 秒 —— 后端 deploy 重启一次就超了,
|
function channelUrl(path: string) {
|
||||||
* 之后这条连接死到用户刷新页面为止。机房网络抖动同理,所以默认不再封顶。
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"
|
||||||
*/
|
return `${protocol}//${window.location.host}${path}`
|
||||||
maxReconnectAttempts?: number
|
|
||||||
/** 首次重连延迟(毫秒),默认 1000。之后指数退避 */
|
|
||||||
reconnectDelay?: number
|
|
||||||
/** 重连延迟上限(毫秒),默认 30000 */
|
|
||||||
maxReconnectDelay?: number
|
|
||||||
/** 心跳间隔(毫秒),默认 30000(30秒) */
|
|
||||||
heartbeatTime?: number
|
|
||||||
/** 是否启用心跳,默认 true */
|
|
||||||
enableHeartbeat?: boolean
|
|
||||||
/** 是否启用自动重连,默认 true */
|
|
||||||
enableAutoReconnect?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,34 +82,22 @@ export class BaseWebSocket<T extends WebSocketMessage = WebSocketMessage> {
|
|||||||
protected url: string
|
protected url: string
|
||||||
protected handlers: Set<MessageHandler<T>> = new Set()
|
protected handlers: Set<MessageHandler<T>> = new Set()
|
||||||
protected reconnectAttempts = 0
|
protected reconnectAttempts = 0
|
||||||
protected maxReconnectAttempts: number
|
|
||||||
protected reconnectDelay: number
|
|
||||||
protected heartbeatInterval: number | null = null
|
protected heartbeatInterval: number | null = null
|
||||||
protected heartbeatTime: number
|
|
||||||
protected enableHeartbeat: boolean
|
|
||||||
protected enableAutoReconnect: boolean
|
|
||||||
protected maxReconnectDelay: number
|
|
||||||
protected disconnectTimer: number | null = null
|
protected disconnectTimer: number | null = null
|
||||||
protected reconnectTimer: number | null = null
|
protected reconnectTimer: number | null = null
|
||||||
/**
|
/**
|
||||||
* 「用户主动断开」的意图,和 enableAutoReconnect 这个**配置**分开存。
|
* 「用户主动断开」的意图。这是**唯一**能阻止自动重连的东西 ——
|
||||||
* 以前两者共用一个字段:disconnect() 把配置改成 false 来阻止重连,而 connect()
|
* 以前它和一个 enableAutoReconnect 配置项共用一个字段:disconnect() 把配置改成
|
||||||
* 从不改回 true —— 登出再登录后,这条连接就永远失去了自动重连能力。
|
* false 来阻止重连,而 connect() 从不改回 true —— 登出再登录后,这条连接就永远
|
||||||
|
* 失去了自动重连能力。
|
||||||
*/
|
*/
|
||||||
protected closedByUser = false
|
protected closedByUser = false
|
||||||
protected reviveBound = false
|
protected reviveBound = false
|
||||||
|
|
||||||
public status: Ref<ConnectionStatus> = ref<ConnectionStatus>("disconnected")
|
public status: Ref<ConnectionStatus> = ref<ConnectionStatus>("disconnected")
|
||||||
|
|
||||||
constructor(config: WebSocketConfig) {
|
constructor(path: string) {
|
||||||
this.url = config.url
|
this.url = channelUrl(path)
|
||||||
|
|
||||||
this.maxReconnectAttempts = config.maxReconnectAttempts ?? Number.POSITIVE_INFINITY
|
|
||||||
this.reconnectDelay = config.reconnectDelay ?? 1000
|
|
||||||
this.maxReconnectDelay = config.maxReconnectDelay ?? 30000
|
|
||||||
this.heartbeatTime = config.heartbeatTime ?? 30000
|
|
||||||
this.enableHeartbeat = config.enableHeartbeat ?? true
|
|
||||||
this.enableAutoReconnect = config.enableAutoReconnect ?? true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -147,9 +131,7 @@ export class BaseWebSocket<T extends WebSocketMessage = WebSocketMessage> {
|
|||||||
this.status.value = "connected"
|
this.status.value = "connected"
|
||||||
this.reconnectAttempts = 0
|
this.reconnectAttempts = 0
|
||||||
console.log(`[WebSocket] 连接成功: ${this.url}`)
|
console.log(`[WebSocket] 连接成功: ${this.url}`)
|
||||||
if (this.enableHeartbeat) {
|
this.startHeartbeat()
|
||||||
this.startHeartbeat()
|
|
||||||
}
|
|
||||||
this.onConnected()
|
this.onConnected()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,14 +202,13 @@ export class BaseWebSocket<T extends WebSocketMessage = WebSocketMessage> {
|
|||||||
* 别在同一毫秒一起冲回来把刚起来的后端再压趴一次。
|
* 别在同一毫秒一起冲回来把刚起来的后端再压趴一次。
|
||||||
*/
|
*/
|
||||||
protected scheduleReconnect() {
|
protected scheduleReconnect() {
|
||||||
if (this.closedByUser || !this.enableAutoReconnect) return
|
if (this.closedByUser) return
|
||||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) return
|
|
||||||
if (this.reconnectTimer !== null) return
|
if (this.reconnectTimer !== null) return
|
||||||
|
|
||||||
this.reconnectAttempts++
|
this.reconnectAttempts++
|
||||||
const base = Math.min(
|
const base = Math.min(
|
||||||
this.reconnectDelay * 2 ** (this.reconnectAttempts - 1),
|
RECONNECT_BASE_DELAY * 2 ** (this.reconnectAttempts - 1),
|
||||||
this.maxReconnectDelay,
|
RECONNECT_MAX_DELAY,
|
||||||
)
|
)
|
||||||
const delay = Math.round(base * (0.5 + Math.random() * 0.5))
|
const delay = Math.round(base * (0.5 + Math.random() * 0.5))
|
||||||
console.log(`[WebSocket] 将在 ${delay}ms 后重连 (第 ${this.reconnectAttempts} 次)`)
|
console.log(`[WebSocket] 将在 ${delay}ms 后重连 (第 ${this.reconnectAttempts} 次)`)
|
||||||
@@ -249,7 +230,7 @@ export class BaseWebSocket<T extends WebSocketMessage = WebSocketMessage> {
|
|||||||
* 退避到 30 秒后,用户切回页面却还要再干等半分钟是说不过去的。
|
* 退避到 30 秒后,用户切回页面却还要再干等半分钟是说不过去的。
|
||||||
*/
|
*/
|
||||||
protected readonly revive = () => {
|
protected readonly revive = () => {
|
||||||
if (this.closedByUser || !this.enableAutoReconnect) return
|
if (this.closedByUser) return
|
||||||
if (
|
if (
|
||||||
this.ws &&
|
this.ws &&
|
||||||
(this.ws.readyState === WebSocket.OPEN ||
|
(this.ws.readyState === WebSocket.OPEN ||
|
||||||
@@ -310,9 +291,9 @@ export class BaseWebSocket<T extends WebSocketMessage = WebSocketMessage> {
|
|||||||
this.disconnectTimer = null
|
this.disconnectTimer = null
|
||||||
const minutes = Math.floor(delay / 60000)
|
const minutes = Math.floor(delay / 60000)
|
||||||
console.log(`WebSocket idle for ${minutes} minutes, disconnecting...`)
|
console.log(`WebSocket idle for ${minutes} minutes, disconnecting...`)
|
||||||
// 这里**只断开**。原来断完紧接着一句 `enableAutoReconnect = true`,
|
// 这里**只断开**。原来断完紧接着一句 `enableAutoReconnect = true`(那个配置项
|
||||||
// 而 close 是异步的 —— 等 onclose 跑到时标志已经翻回来了,于是 1 秒后
|
// 已经不存在了),而 close 是异步的 —— 等 onclose 跑到时标志已经翻回来了,
|
||||||
// 又自动连上:这个「省资源」的空闲断开从来没有真正生效过。
|
// 于是 1 秒后又自动连上:这个「省资源」的空闲断开从来没有真正生效过。
|
||||||
// 下一次 connect()(新提交)会自己把 closedByUser 清掉,不需要在这里预置。
|
// 下一次 connect()(新提交)会自己把 closedByUser 清掉,不需要在这里预置。
|
||||||
this.disconnect()
|
this.disconnect()
|
||||||
}, delay)
|
}, delay)
|
||||||
@@ -392,7 +373,7 @@ export class BaseWebSocket<T extends WebSocketMessage = WebSocketMessage> {
|
|||||||
this.stopHeartbeat()
|
this.stopHeartbeat()
|
||||||
this.heartbeatInterval = window.setInterval(() => {
|
this.heartbeatInterval = window.setInterval(() => {
|
||||||
this.sendHeartbeat()
|
this.sendHeartbeat()
|
||||||
}, this.heartbeatTime)
|
}, HEARTBEAT_INTERVAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -405,13 +386,6 @@ export class BaseWebSocket<T extends WebSocketMessage = WebSocketMessage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 连接成功钩子(子类可重写)
|
|
||||||
*/
|
|
||||||
protected onConnected() {
|
|
||||||
// 子类实现
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 断开连接钩子(子类可重写)
|
* 断开连接钩子(子类可重写)
|
||||||
*/
|
*/
|
||||||
@@ -426,6 +400,45 @@ export class BaseWebSocket<T extends WebSocketMessage = WebSocketMessage> {
|
|||||||
// 子类实现
|
// 子类实现
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前在等结果的提交。**一直留着**,直到调用方 unsubscribe()。
|
||||||
|
*
|
||||||
|
* 原来这个字段叫 pendingSubmissionId,订阅一发成功就清空 —— 它只解决了
|
||||||
|
* 「还没连上就调 subscribe」,没解决断线重连。而真正会丢结果的恰恰是后者:
|
||||||
|
* 服务端收到 subscribe 会回一份当前状态,掉线期间错过的那条推送就是靠这次
|
||||||
|
* 重放补回来的。不重新订阅,重连后就只收得到「将来」的事件,可结果已经是过去式了。
|
||||||
|
*/
|
||||||
|
private subscribedId = ""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅特定提交的更新。连接没就绪也可以调,连上后会自动补发 ——
|
||||||
|
* 调用方不用关心此刻连上没有,断线重连后也会自动重新订阅。
|
||||||
|
*
|
||||||
|
* 只有 /ws/submissions 用得上;/ws/config 是单向广播、/ws/collab 自己有房间语义,
|
||||||
|
* 它们不调这两个方法,subscribedId 一直是空串,onConnected 里那一行就是空转。
|
||||||
|
*/
|
||||||
|
subscribe(submissionId: string) {
|
||||||
|
this.subscribedId = submissionId
|
||||||
|
this.sendSubscribe(submissionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 结果已经拿到,重连后不必再问一遍 */
|
||||||
|
unsubscribe() {
|
||||||
|
this.subscribedId = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendSubscribe(submissionId: string) {
|
||||||
|
return this.send({ type: "subscribe", submissionId })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接成功钩子。子类重写时**必须先调 super.onConnected()**,否则断线重连后
|
||||||
|
* 不会补订阅,那次判题的结果就再也回不来。
|
||||||
|
*/
|
||||||
|
protected onConnected() {
|
||||||
|
if (this.subscribedId) this.sendSubscribe(this.subscribedId)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息处理钩子(子类可重写)
|
* 消息处理钩子(子类可重写)
|
||||||
*/
|
*/
|
||||||
@@ -453,79 +466,28 @@ export interface SubmissionUpdate extends WebSocketMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 带「订阅意图」的连接。
|
* 三条通道共用的 composable。
|
||||||
*
|
*
|
||||||
* subscribe() 在连接还没就绪时先把 id 记下来,等 onConnected() 补发 —— 调用方
|
* 原来每条通道各有一个空壳子类(只在构造函数里拼 URL)加一个三十行的 composable,
|
||||||
* 不用关心此刻连上没有,断线重连后也会自动重新订阅。
|
* 而三份 composable 除了类型参数逐字相同 —— `SubmissionWebSocket` 和
|
||||||
|
* `FlowchartWebSocket` 甚至连 URL 都是同一条 `/ws/submissions`。中间还搁着一个
|
||||||
|
* 带二十五行示例注释的 `createWebSocketComposable` 工厂,示例里那条通知通道并不
|
||||||
|
* 存在,三个真实的 composable 一个都没用它。现在只剩这一个。
|
||||||
*
|
*
|
||||||
* 原来这套只有 SubmissionWebSocket 有,FlowchartWebSocket 是 send 失败就打一行
|
* 每次调用都新建一条连接(和原来一致,不是单例),并在组件卸载时摘掉 handler、断开。
|
||||||
* 日志了事:socket 一掉,那次评分的结果就再也回不来,页面永远转圈。提到基类上,
|
|
||||||
* 两条通道共用同一套语义。
|
|
||||||
*/
|
*/
|
||||||
class SubscribingWebSocket<
|
function useChannel<T extends WebSocketMessage>(
|
||||||
T extends WebSocketMessage,
|
path: string,
|
||||||
> extends BaseWebSocket<T> {
|
handler?: MessageHandler<T>,
|
||||||
/**
|
|
||||||
* 当前在等结果的提交。**一直留着**,直到调用方 unsubscribe()。
|
|
||||||
*
|
|
||||||
* 原来这个字段叫 pendingSubmissionId,订阅一发成功就清空 —— 它只解决了
|
|
||||||
* 「还没连上就调 subscribe」,没解决断线重连。而真正会丢结果的恰恰是后者:
|
|
||||||
* 服务端收到 subscribe 会回一份当前状态,掉线期间错过的那条推送就是靠这次
|
|
||||||
* 重放补回来的。不重新订阅,重连后就只收得到「将来」的事件,可结果已经是过去式了。
|
|
||||||
*/
|
|
||||||
private subscribedId = ""
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 订阅特定提交的更新。连接没就绪也可以调,连上后会自动补发。
|
|
||||||
*/
|
|
||||||
subscribe(submissionId: string) {
|
|
||||||
this.subscribedId = submissionId
|
|
||||||
this.sendSubscribe(submissionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 结果已经拿到,重连后不必再问一遍 */
|
|
||||||
unsubscribe() {
|
|
||||||
this.subscribedId = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
protected onConnected() {
|
|
||||||
if (this.subscribedId) this.sendSubscribe(this.subscribedId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private sendSubscribe(submissionId: string) {
|
|
||||||
return this.send({ type: "subscribe", submissionId })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提交 WebSocket 连接管理类
|
|
||||||
*/
|
|
||||||
class SubmissionWebSocket extends SubscribingWebSocket<SubmissionUpdate> {
|
|
||||||
constructor() {
|
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"
|
|
||||||
super({ url: `${protocol}//${window.location.host}/ws/submissions` })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用于组件中使用 WebSocket 的 Composable
|
|
||||||
* 每次调用创建新的 WebSocket 实例
|
|
||||||
*/
|
|
||||||
export function useSubmissionWebSocket(
|
|
||||||
handler?: MessageHandler<SubmissionUpdate>,
|
|
||||||
) {
|
) {
|
||||||
const ws = new SubmissionWebSocket()
|
const ws = new BaseWebSocket<T>(path)
|
||||||
|
|
||||||
// 如果提供了处理器,添加到实例中
|
// 同步注册,不放 onMounted:调用方(如 useConfigUpdate)在 setup 阶段就 connect(),
|
||||||
if (handler) {
|
// 中间那段窗口收到的广播没有任何 handler 接。窗口极小,但没有理由留着它。
|
||||||
ws.addHandler(handler)
|
if (handler) ws.addHandler(handler)
|
||||||
}
|
|
||||||
|
|
||||||
// 组件卸载时清理资源
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (handler) {
|
if (handler) ws.removeHandler(handler)
|
||||||
ws.removeHandler(handler)
|
|
||||||
}
|
|
||||||
ws.disconnect()
|
ws.disconnect()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -537,63 +499,25 @@ export function useSubmissionWebSocket(
|
|||||||
scheduleDisconnect: (delay?: number) => ws.scheduleDisconnect(delay),
|
scheduleDisconnect: (delay?: number) => ws.scheduleDisconnect(delay),
|
||||||
cancelScheduledDisconnect: () => ws.cancelScheduledDisconnect(),
|
cancelScheduledDisconnect: () => ws.cancelScheduledDisconnect(),
|
||||||
status: ws.status,
|
status: ws.status,
|
||||||
addHandler: (h: MessageHandler<SubmissionUpdate>) => ws.addHandler(h),
|
addHandler: (h: MessageHandler<T>) => ws.addHandler(h),
|
||||||
removeHandler: (h: MessageHandler<SubmissionUpdate>) => ws.removeHandler(h),
|
removeHandler: (h: MessageHandler<T>) => 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() {
|
|
||||||
* const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
||||||
* super({ url: `${protocol}//${window.location.host}/ws/notifications` })
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* 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>(
|
export interface SubmissionUpdate extends WebSocketMessage {
|
||||||
ws: BaseWebSocket<T>,
|
type: "submission_update"
|
||||||
handler?: MessageHandler<T>,
|
submissionId: string
|
||||||
) {
|
result: number
|
||||||
if (handler) {
|
status: "pending" | "judging" | "finished" | "error"
|
||||||
ws.addHandler(handler)
|
score?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnmounted(() => {
|
/** 判题进度。subscribe(submissionId) 认领,断线重连会自动补订阅 */
|
||||||
if (handler) {
|
export function useSubmissionWebSocket(handler?: MessageHandler<SubmissionUpdate>) {
|
||||||
ws.removeHandler(handler)
|
return useChannel<SubmissionUpdate>("/ws/submissions", 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),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -614,49 +538,13 @@ export interface FlowchartEvaluationUpdate extends WebSocketMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 流程图 WebSocket 连接管理类
|
* 流程图评分进度。和判题走的是**同一条** `/ws/submissions` ——
|
||||||
*/
|
* 服务端按 submissionId 分辨是代码提交还是流程图,这里只是换一套消息类型。
|
||||||
class FlowchartWebSocket extends SubscribingWebSocket<FlowchartEvaluationUpdate> {
|
|
||||||
constructor() {
|
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"
|
|
||||||
super({ url: `${protocol}//${window.location.host}/ws/submissions` })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用于组件中使用流程图 WebSocket 的 Composable
|
|
||||||
*/
|
*/
|
||||||
export function useFlowchartWebSocket(
|
export function useFlowchartWebSocket(
|
||||||
handler?: MessageHandler<FlowchartEvaluationUpdate>,
|
handler?: MessageHandler<FlowchartEvaluationUpdate>,
|
||||||
) {
|
) {
|
||||||
const ws = new FlowchartWebSocket()
|
return useChannel<FlowchartEvaluationUpdate>("/ws/submissions", handler)
|
||||||
|
|
||||||
// 如果提供了处理器,添加到实例中
|
|
||||||
if (handler) {
|
|
||||||
ws.addHandler(handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件卸载时清理资源
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (handler) {
|
|
||||||
ws.removeHandler(handler)
|
|
||||||
}
|
|
||||||
ws.disconnect()
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
connect: () => ws.connect(),
|
|
||||||
disconnect: () => ws.disconnect(),
|
|
||||||
subscribe: (submissionId: string) => ws.subscribe(submissionId),
|
|
||||||
unsubscribe: () => ws.unsubscribe(),
|
|
||||||
scheduleDisconnect: (delay?: number) => ws.scheduleDisconnect(delay),
|
|
||||||
cancelScheduledDisconnect: () => ws.cancelScheduledDisconnect(),
|
|
||||||
status: ws.status,
|
|
||||||
addHandler: (h: MessageHandler<FlowchartEvaluationUpdate>) =>
|
|
||||||
ws.addHandler(h),
|
|
||||||
removeHandler: (h: MessageHandler<FlowchartEvaluationUpdate>) =>
|
|
||||||
ws.removeHandler(h),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -669,45 +557,13 @@ export interface ConfigUpdate extends WebSocketMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 配置 WebSocket 连接管理类
|
* 站点配置广播。这条通道是**单向**的:只收后端广播。服务端的消息处理只认
|
||||||
*/
|
* ping / subscribe,客户端往这里推 config_update 会被回一个 error 帧 ——
|
||||||
class ConfigWebSocket extends BaseWebSocket<ConfigUpdate> {
|
* 别拿返回值里的 subscribe 往这条通道上发东西。配置变更走 POST /admin/website,
|
||||||
constructor() {
|
* 由后端广播给所有人。
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"
|
|
||||||
super({ url: `${protocol}//${window.location.host}/ws/config` })
|
|
||||||
}
|
|
||||||
// 这条通道是**单向**的:只收后端广播。服务端的消息处理只认 ping / subscribe,
|
|
||||||
// 客户端往这里推 config_update 会被回一个 error 帧 —— 别再加发送方法。
|
|
||||||
// 配置变更走 POST /admin/website,由后端广播给所有人。
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用于组件中使用配置 WebSocket 的 Composable
|
|
||||||
*/
|
*/
|
||||||
export function useConfigWebSocket(handler?: MessageHandler<ConfigUpdate>) {
|
export function useConfigWebSocket(handler?: MessageHandler<ConfigUpdate>) {
|
||||||
const ws = new ConfigWebSocket()
|
return useChannel<ConfigUpdate>("/ws/config", handler)
|
||||||
|
|
||||||
// 同步注册,和另外两个 composable 一致。原来放在 onMounted 里,而调用方
|
|
||||||
// (useConfigUpdate)在 setup 阶段就 connect() 了 —— 中间那段窗口收到的广播
|
|
||||||
// 没有任何 handler 接。窗口极小,但没有任何理由留着它。
|
|
||||||
if (handler) {
|
|
||||||
ws.addHandler(handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (handler) {
|
|
||||||
ws.removeHandler(handler)
|
|
||||||
}
|
|
||||||
ws.disconnect()
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
connect: () => ws.connect(),
|
|
||||||
disconnect: () => ws.disconnect(),
|
|
||||||
status: ws.status,
|
|
||||||
addHandler: (h: MessageHandler<ConfigUpdate>) => ws.addHandler(h),
|
|
||||||
removeHandler: (h: MessageHandler<ConfigUpdate>) => ws.removeHandler(h),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollabRequestItem {
|
export interface CollabRequestItem {
|
||||||
@@ -756,8 +612,7 @@ export class CollabWebSocket extends BaseWebSocket<CollabMessage> {
|
|||||||
private pendingBinary: ArrayBuffer[] = []
|
private pendingBinary: ArrayBuffer[] = []
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"
|
super("/ws/collab")
|
||||||
super({ url: `${protocol}//${window.location.host}/ws/collab` })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setBinaryHandler(handler: ((data: ArrayBuffer) => void) | null) {
|
setBinaryHandler(handler: ((data: ArrayBuffer) => void) | null) {
|
||||||
@@ -796,6 +651,8 @@ export class CollabWebSocket extends BaseWebSocket<CollabMessage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override onConnected() {
|
protected override onConnected() {
|
||||||
|
// 基类那一步是补订阅:collab 从不调 subscribe(),这里是空转,但按约定照调不误
|
||||||
|
super.onConnected()
|
||||||
// 新连接,旧连接攒下的帧一概作废
|
// 新连接,旧连接攒下的帧一概作废
|
||||||
this.pendingBinary = []
|
this.pendingBinary = []
|
||||||
this.connectHandler?.()
|
this.connectHandler?.()
|
||||||
|
|||||||
Reference in New Issue
Block a user