Files
OJ2/apps/web/src/oj/problem/composables/useSubmissionMonitor.ts
T
xuyueandClaude Opus 5.5 64994b9499
Deploy / deploy (push) Canceled after 0s
perf(判题结果): 全班一起交、判题排队时不再每人每 2 秒问一次结果
原来交上去 5 秒没结果就每 2 秒(流程图 3 秒)拉一次,分不清 WebSocket 断了还是队列长:
50 人一起交、队列排一分钟时每秒二十几个请求,全在和判题抢那两个核。

- 收到这一条的任何推送(服务端订阅时就回一帧当前状态)就认定 WebSocket 是通的,
  轮询退到 20 秒一次,只防推送丢失;连接断了才回快档,2 秒起按 1.5 倍退到 10 秒
- 拉失败不再永久停掉(掉线那几秒拉失败是常事),连着失败 5 次才放弃
- 页面在后台时不拉;比赛题目页、榜单、班级页的定时刷新也一样,切回来立刻补一次
- 代码判题和流程图评分共用 shared/composables/fallbackPoll.ts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:37:08 +08:00

153 lines
5.7 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, computed } from "vue"
import { getSubmission } from "oj/api"
import { SubmissionStatus } from "utils/constants"
import type { PendingAchievement, Submission } from "utils/types"
import { useAchievementStore } from "shared/store/achievement"
import { useSubmissionWebSocket, type SubmissionUpdate } from "shared/composables/websocket"
import { useFallbackPoll } from "shared/composables/fallbackPoll"
/**
* 判题监控 Composable
* 负责通过 WebSocket + 轮询双保险机制监控判题结果
*/
export function useSubmissionMonitor() {
// ==================== 状态 ====================
const submissionId = ref("")
const submission = ref<Submission>()
// ==================== WebSocket 处理 ====================
const handleSubmissionUpdate = (data: SubmissionUpdate) => {
// push_to_user 复用了 submission_update 这个 channel handler,
// 其他类型的消息会走同一条 WebSocket 帧进来,必须先分流
const frame = data as unknown as {
type: string
achievements?: PendingAchievement[]
}
if (frame.type === "achievement_unlocked") {
useAchievementStore().enqueue(frame.achievements ?? [])
return
}
if (frame.type !== "submission_update") {
return
}
console.log("[SubmissionMonitor] 收到WebSocket更新:", data)
if (data.submissionId !== submissionId.value) {
console.log("[SubmissionMonitor] 提交ID不匹配,忽略")
return
}
// 订阅后服务端立刻回的那一帧(还在排队也回)就算:WebSocket 是通的,轮询退到慢档
fallback.confirm()
if (!submission.value) {
submission.value = {} as Submission
}
submission.value.result = data.result as Submission["result"]
// 判题完成或出错,获取完整详情
if (data.status === "finished" || data.status === "error") {
console.log(`[SubmissionMonitor] 判题${data.status === "finished" ? "完成" : "出错"}`)
// 停止轮询(WebSocket已成功)
fallback.stop()
// 结果已经到手,别让重连再去重放这条早就判完的订阅
unsubscribe()
const id = submissionId.value
getSubmission(id).then((res) => {
if (id !== submissionId.value) return // 同上面轮询:换过题就扔掉
submission.value = res
// 15分钟无新提交则断开WebSocket(节省资源)
scheduleDisconnect(15 * 60 * 1000)
})
}
}
// 初始化 WebSocket
const { connect, subscribe, unsubscribe, scheduleDisconnect, cancelScheduledDisconnect, status } =
useSubmissionWebSocket(handleSubmissionUpdate)
// ==================== 轮询兜底 ====================
const fallback = useFallbackPoll({
status,
poll: async () => {
const id = submissionId.value
if (!id) return true
const res = await getSubmission(id)
// 请求在路上时换了题(reset)或者又交了一次:回来的是上一条,扔掉。
// 不扔的话上一道题判对了,会把新题标成已解决、弹点评
if (id !== submissionId.value) return false
submission.value = res
return res.result !== SubmissionStatus.judging && res.result !== SubmissionStatus.pending
},
})
// ==================== 启动监控 ====================
const startMonitoring = (id: string) => {
submissionId.value = id
submission.value = { id, result: SubmissionStatus.submitting } as Submission
// 取消之前的断开计划
cancelScheduledDisconnect()
// connect() 是幂等的:已经连着就直接返回,顺带把上一次空闲断开留下的状态清掉
connect()
// 直接订阅,不必先等 status 变成 connected:连接没就绪时 subscribe() 会把 id
// 记在 pendingSubmissionId 上,由 onConnected() 补发(断线重连后同样有效)。
//
// 原来这里是 watch(wsStatus, ..., { immediate: true }),而 immediate 的回调在
// watch() **返回之前**就同步跑了 —— 已经连着时 unwatch 还是 null,if 不成立,
// 这个 watcher 就永远停不掉:每提交一次泄漏一个,而且往后每次重连时它们都会
// 把各自那个早就判完的旧 submissionId 重新订阅一遍。
subscribe(id)
// 轮询兜底:WebSocket 通的话 20 秒才问一次,不通才快拉(见 fallbackPoll.ts)
fallback.start()
}
/**
* 换题的时候丢掉当前这条提交。题目页换题(顶栏题号直达、「下一题」)是同一个组件
* 复用、只换路由参数,不清的话上一道题的结果面板原样挂在新题上;而且上一道还在判的话,
* 判完触发的「通过」处理会把**新题**标成已解决。提交本身照常判完落库,只是这边不再跟。
*/
const reset = () => {
fallback.stop()
unsubscribe()
// 连接由 store 持有,离开题目页组件卸了它也还在。和判完之后一样,空闲 15 分钟断开;
// 不在这里立刻断,是因为刚交的那条判完之后还可能推成就过来
scheduleDisconnect(15 * 60 * 1000)
submissionId.value = ""
submission.value = undefined
}
// ==================== 计算属性 ====================
const judging = computed(() => submission.value?.result === SubmissionStatus.judging)
const pending = computed(() => submission.value?.result === SubmissionStatus.pending)
const submitting = computed(() => submission.value?.result === SubmissionStatus.submitting)
const isProcessing = computed(() => {
return judging.value || pending.value || submitting.value
})
return {
// 状态
submissionId,
submission,
// 计算属性
judging,
pending,
submitting,
isProcessing,
// 方法
startMonitoring,
reset,
}
}