Deploy / deploy (push) Canceled after 0s
原来交上去 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>
153 lines
5.7 KiB
TypeScript
153 lines
5.7 KiB
TypeScript
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,
|
||
}
|
||
}
|