update:新增比赛服断线重连机制

This commit is contained in:
2026-07-10 15:11:45 +08:00
parent b5373bee31
commit 98ca638b63
4 changed files with 339 additions and 38 deletions
+18
View File
@@ -80,6 +80,10 @@
// audioManager.play("射箭声音") // audioManager.play("射箭声音")
} }
function onNetworkStatusChange(status) {
matchWebsocket.handleMatchNetworkStatusChange(status);
}
function connectMatchServerFromMessage(content) { function connectMatchServerFromMessage(content) {
const messages = Array.isArray(content) ? content : [content]; const messages = Array.isArray(content) ? content : [content];
messages.forEach((message) => { messages.forEach((message) => {
@@ -108,6 +112,17 @@
uni.$on("update-online", emitUpdateOnline); uni.$on("update-online", emitUpdateOnline);
uni.$on("session-kicked-out", onSessionKickedOut); uni.$on("session-kicked-out", onSessionKickedOut);
uni.$on("device-bind-invalid", onDeviceBindInvalid); uni.$on("device-bind-invalid", onDeviceBindInvalid);
if (typeof uni.offNetworkStatusChange === "function") {
uni.offNetworkStatusChange(onNetworkStatusChange);
}
if (typeof uni.onNetworkStatusChange === "function") {
uni.onNetworkStatusChange(onNetworkStatusChange);
}
if (typeof uni.getNetworkType === "function") {
uni.getNetworkType({
success: onNetworkStatusChange
});
}
const token = uni.getStorageSync( const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token` `${uni.getAccountInfoSync().miniProgram.envVersion}_token`
); );
@@ -122,6 +137,9 @@
uni.$off("update-online", emitUpdateOnline); uni.$off("update-online", emitUpdateOnline);
uni.$off("session-kicked-out", onSessionKickedOut); uni.$off("session-kicked-out", onSessionKickedOut);
uni.$off("device-bind-invalid", onDeviceBindInvalid); uni.$off("device-bind-invalid", onDeviceBindInvalid);
if (typeof uni.offNetworkStatusChange === "function") {
uni.offNetworkStatusChange(onNetworkStatusChange);
}
matchWebsocket.closeMatchWebSocket({ matchWebsocket.closeMatchWebSocket({
reason: "app-hide" reason: "app-hide"
}); });
+232 -12
View File
@@ -24,11 +24,20 @@ let isConnecting = false;
let manualClose = false; let manualClose = false;
let audioAckListenerReady = false; let audioAckListenerReady = false;
let lastReadyRouteKey = ""; let lastReadyRouteKey = "";
let reconnectTimer = null;
let connectTimeoutTimer = null;
let livenessTimer = null;
let reconnectAttempt = 0;
let networkConnected = true;
let lastNetworkType = "";
// 后端要求非心跳消息必须等前端语音播报完成后再 ACK。 // 后端要求非心跳消息必须等前端语音播报完成后再 ACK。
const pendingAcks = []; const pendingAcks = [];
const ACK_AUDIO_TIMEOUT_MS = 9000; const ACK_AUDIO_TIMEOUT_MS = 9000;
const READY_ROUTE_FALLBACK_DELAY_MS = 300; const READY_ROUTE_FALLBACK_DELAY_MS = 300;
const CONNECT_TIMEOUT_MS = 8000;
const LIVENESS_TIMEOUT_MS = 30000;
const RECONNECT_DELAYS_MS = [1000, 2000, 4000, 8000, 15000];
const ROUND_AUDIO_NAMES = ["一", "二", "三", "四", "五"]; const ROUND_AUDIO_NAMES = ["一", "二", "三", "四", "五"];
const MATCH_STATUS_HALF_REST = 3; const MATCH_STATUS_HALF_REST = 3;
@@ -47,6 +56,7 @@ const BUSINESS_TYPE_BY_SERVER_TYPE = {
const MATCH_READY_SNAPSHOT_PREFIX = "match-ready-snapshot:"; const MATCH_READY_SNAPSHOT_PREFIX = "match-ready-snapshot:";
export const MATCH_WS_AUDIO_ACK_EVENT = "match-ws-audio-ack"; export const MATCH_WS_AUDIO_ACK_EVENT = "match-ws-audio-ack";
export const MATCH_WS_STATE_EVENT = "match-ws-state";
function normalizeShootData(shootData) { function normalizeShootData(shootData) {
if (!shootData || typeof shootData !== "object") return shootData; if (!shootData || typeof shootData !== "object") return shootData;
@@ -382,6 +392,166 @@ function normalizeServerUrl(serverAddr, token) {
return appendQuery(url, "token", token); return appendQuery(url, "token", token);
} }
function emitMatchSocketState(state, extra = {}) {
uni.$emit(MATCH_WS_STATE_EVENT, {
state,
matchId: currentContext?.matchId || "",
...extra,
});
}
function clearReconnectTimer() {
if (!reconnectTimer) return;
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
function clearConnectTimeout() {
if (!connectTimeoutTimer) return;
clearTimeout(connectTimeoutTimer);
connectTimeoutTimer = null;
}
function clearLivenessTimer() {
if (!livenessTimer) return;
clearTimeout(livenessTimer);
livenessTimer = null;
}
function clearSocketHealthTimers() {
clearConnectTimeout();
clearLivenessTimer();
}
function closeSocketTask(socketTask, reason) {
if (!socketTask) return;
try {
socketTask.close({ reason });
} catch (err) {
console.log("[match-ws] close socket task failed", reason, err);
}
}
function detachCurrentSocket(reason) {
const currentSocket = socket;
socket = null;
isConnecting = false;
clearSocketHealthTimers();
closeSocketTask(currentSocket, reason);
}
function scheduleReconnect(reason, { immediate = false } = {}) {
if (
manualClose ||
!currentContext ||
!networkConnected ||
reconnectTimer ||
socket ||
isConnecting
) {
return;
}
const delay = immediate
? 0
: RECONNECT_DELAYS_MS[
Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)
];
const attempt = reconnectAttempt + 1;
reconnectAttempt = attempt;
emitMatchSocketState("reconnecting", { reason, delay, attempt });
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
if (manualClose || !currentContext || !networkConnected) return;
connectMatchWebSocket({
...currentContext,
force: true,
reconnecting: true,
reconnectReason: reason,
});
}, delay);
}
function handleUnexpectedDisconnect(
socketTask,
reason,
detail,
{ closeTask = true } = {}
) {
if (socket !== socketTask) return;
socket = null;
isConnecting = false;
clearSocketHealthTimers();
if (closeTask) closeSocketTask(socketTask, reason);
console.log("[match-ws] unexpected disconnect", {
reason,
detail,
context: currentContext,
});
emitMatchSocketState(networkConnected ? "disconnected" : "offline", {
reason,
});
scheduleReconnect(reason);
}
function startConnectTimeout(socketTask) {
clearConnectTimeout();
connectTimeoutTimer = setTimeout(() => {
handleUnexpectedDisconnect(socketTask, "connect-timeout");
}, CONNECT_TIMEOUT_MS);
}
function resetLivenessTimer(socketTask) {
clearLivenessTimer();
livenessTimer = setTimeout(() => {
handleUnexpectedDisconnect(socketTask, "heartbeat-timeout");
}, LIVENESS_TIMEOUT_MS);
}
export function forceReconnectMatchWebSocket(reason = "force-reconnect") {
if (manualClose || !currentContext) return false;
clearReconnectTimer();
detachCurrentSocket(reason);
if (!networkConnected) {
emitMatchSocketState("offline", { reason });
return false;
}
reconnectAttempt = 0;
scheduleReconnect(reason, { immediate: true });
return true;
}
export function handleMatchNetworkStatusChange(status = {}) {
const previousConnected = networkConnected;
const networkType = String(status.networkType || "");
const nextConnected =
status.isConnected !== false && networkType.toLowerCase() !== "none";
const networkTypeChanged =
!!lastNetworkType && !!networkType && lastNetworkType !== networkType;
networkConnected = nextConnected;
if (networkType) lastNetworkType = networkType;
if (manualClose || !currentContext) return;
if (!nextConnected) {
clearReconnectTimer();
detachCurrentSocket("network-offline");
emitMatchSocketState("offline", { reason: "network-offline" });
return;
}
if (!previousConnected || networkTypeChanged) {
forceReconnectMatchWebSocket(
networkTypeChanged ? "network-type-changed" : "network-recovered"
);
}
}
// 统一发送二进制 protobuf 消息,并在日志里保留发送类型。 // 统一发送二进制 protobuf 消息,并在日志里保留发送类型。
function sendBuffer(buffer, label, clientMessage) { function sendBuffer(buffer, label, clientMessage) {
if (!socket || !buffer) return; if (!socket || !buffer) return;
@@ -395,6 +565,7 @@ function sendBuffer(buffer, label, clientMessage) {
fail: (err) => { fail: (err) => {
if (socket !== currentSocket) return; if (socket !== currentSocket) return;
console.log("比赛服 WebSocket 消息发送失败", label, clientMessage, err); console.log("比赛服 WebSocket 消息发送失败", label, clientMessage, err);
handleUnexpectedDisconnect(currentSocket, "send-failed", err);
}, },
}); });
} }
@@ -582,7 +753,16 @@ function sendLeave() {
export function connectMatchWebSocket(options = {}) { export function connectMatchWebSocket(options = {}) {
// 入口参数来自原 websocket 的比赛服地址通知,不影响原有 websocket 连接。 // 入口参数来自原 websocket 的比赛服地址通知,不影响原有 websocket 连接。
const { serverAddr, matchId, userId, token, mode } = options; const {
serverAddr,
matchId,
userId,
token,
mode,
force = false,
reconnecting = false,
reconnectReason = "",
} = options;
const url = normalizeServerUrl(serverAddr, token); const url = normalizeServerUrl(serverAddr, token);
const normalizedMatchId = normalizeId(matchId); const normalizedMatchId = normalizeId(matchId);
const normalizedUserId = normalizeId(userId); const normalizedUserId = normalizeId(userId);
@@ -593,11 +773,10 @@ export function connectMatchWebSocket(options = {}) {
return; return;
} }
if ( const isSameContext =
socket &&
currentContext?.url === url && currentContext?.url === url &&
currentContext?.matchId === normalizedMatchId currentContext?.matchId === normalizedMatchId;
) { if (!force && socket && isSameContext) {
// 同一场比赛同一地址重复通知时不重复建连。 // 同一场比赛同一地址重复通知时不重复建连。
console.log("[match-ws] already connected or connecting", { console.log("[match-ws] already connected or connecting", {
matchId: normalizedMatchId, matchId: normalizedMatchId,
@@ -606,22 +785,38 @@ export function connectMatchWebSocket(options = {}) {
return; return;
} }
// 切换比赛服地址时先关闭旧连接,不发送 LEAVE,避免误通知旧比赛离场。 if (force && isSameContext) {
closeMatchWebSocket({ sendLeave: false, reason: "switch" }); // 网络切换或假死重连时只销毁旧链路,保留比赛上下文且不发送 LEAVE。
detachCurrentSocket(reconnectReason || "force-reconnect");
} else if (!isSameContext) {
// 切换比赛服地址时先关闭旧连接,不发送 LEAVE,避免误通知旧比赛离场。
closeMatchWebSocket({ sendLeave: false, reason: "switch" });
}
clearReconnectTimer();
manualClose = false; manualClose = false;
isConnecting = true; isConnecting = true;
if (!isSameContext) reconnectAttempt = 0;
const connectedOnce = isSameContext && currentContext?.connectedOnce === true;
currentContext = { currentContext = {
serverAddr, serverAddr,
matchId: normalizedMatchId, matchId: normalizedMatchId,
userId: normalizedUserId, userId: normalizedUserId,
token,
url, url,
mode: Number.isFinite(normalizedMode) ? normalizedMode : undefined, mode: Number.isFinite(normalizedMode) ? normalizedMode : undefined,
isMelee: isMelee:
Number.isFinite(normalizedMode) ? normalizedMode > 3 : undefined, Number.isFinite(normalizedMode) ? normalizedMode > 3 : undefined,
meleeHalfRest: false, meleeHalfRest: isSameContext
? currentContext?.meleeHalfRest === true
: false,
connectedOnce,
}; };
ensureAudioAckListener(); ensureAudioAckListener();
emitMatchSocketState(reconnecting ? "reconnecting" : "connecting", {
reason: reconnectReason,
attempt: reconnectAttempt,
});
const socketTask = uni.connectSocket({ const socketTask = uni.connectSocket({
url, url,
@@ -636,40 +831,60 @@ export function connectMatchWebSocket(options = {}) {
socket = null; socket = null;
isConnecting = false; isConnecting = false;
console.log("[match-ws] connect failed", err); console.log("[match-ws] connect failed", err);
clearSocketHealthTimers();
emitMatchSocketState(networkConnected ? "disconnected" : "offline", {
reason: "connect-failed",
});
scheduleReconnect("connect-failed");
}, },
}); });
socket = socketTask; socket = socketTask;
startConnectTimeout(socketTask);
socketTask.onOpen(() => { socketTask.onOpen(() => {
if (socket !== socketTask) return; if (socket !== socketTask) return;
clearConnectTimeout();
isConnecting = false; isConnecting = false;
const wasReconnected =
currentContext?.connectedOnce === true || reconnecting || reconnectAttempt > 0;
if (currentContext) currentContext.connectedOnce = true;
reconnectAttempt = 0;
resetLivenessTimer(socketTask);
console.log("[match-ws] connected", { console.log("[match-ws] connected", {
matchId: normalizedMatchId, matchId: normalizedMatchId,
url, url,
reconnected: wasReconnected,
});
emitMatchSocketState("open", {
reason: reconnectReason,
reconnected: wasReconnected,
}); });
}); });
socketTask.onMessage((res) => { socketTask.onMessage((res) => {
if (socket !== socketTask) return; if (socket !== socketTask) return;
resetLivenessTimer(socketTask);
handleMessage(res.data); handleMessage(res.data);
}); });
socketTask.onError((err) => { socketTask.onError((err) => {
if (socket !== socketTask) return; if (socket !== socketTask) return;
console.log("[match-ws] socket error", err); console.log("[match-ws] socket error", err);
handleUnexpectedDisconnect(socketTask, "socket-error", err);
}); });
socketTask.onClose((result) => { socketTask.onClose((result) => {
if (socket !== socketTask) return; if (socket !== socketTask) return;
socket = null;
isConnecting = false;
console.log("[match-ws] closed", { console.log("[match-ws] closed", {
manualClose, manualClose,
result, result,
context: currentContext, context: currentContext,
}); });
if (!manualClose) removeAudioAckListener(); if (manualClose) return;
handleUnexpectedDisconnect(socketTask, "socket-close", result, {
closeTask: false,
});
}); });
} }
@@ -690,6 +905,9 @@ export function closeMatchWebSocket(options = {}) {
const { sendLeave: shouldSendLeave = true, reason = "manual" } = options; const { sendLeave: shouldSendLeave = true, reason = "manual" } = options;
manualClose = true; manualClose = true;
clearReconnectTimer();
clearSocketHealthTimers();
reconnectAttempt = 0;
pendingAcks.forEach((task) => { pendingAcks.forEach((task) => {
if (task?.timer) clearTimeout(task.timer); if (task?.timer) clearTimeout(task.timer);
}); });
@@ -700,7 +918,7 @@ export function closeMatchWebSocket(options = {}) {
if (shouldSendLeave) sendLeave(); if (shouldSendLeave) sendLeave();
socket = null; socket = null;
try { try {
currentSocket.close(); currentSocket.close({ reason });
} catch (err) { } catch (err) {
console.log("[match-ws] close failed", err); console.log("[match-ws] close failed", err);
} }
@@ -718,4 +936,6 @@ export default {
connectMatchWebSocket, connectMatchWebSocket,
connectMatchWebSocketFromNotice, connectMatchWebSocketFromNotice,
closeMatchWebSocket, closeMatchWebSocket,
forceReconnectMatchWebSocket,
handleMatchNetworkStatusChange,
}; };
+74 -26
View File
@@ -17,6 +17,7 @@ import {
closeMatchWebSocket, closeMatchWebSocket,
connectMatchWebSocket, connectMatchWebSocket,
MATCH_WS_AUDIO_ACK_EVENT, MATCH_WS_AUDIO_ACK_EVENT,
MATCH_WS_STATE_EVENT,
} from "@/matchWebsocket"; } from "@/matchWebsocket";
import { MESSAGETYPESV2 } from "@/constants"; import { MESSAGETYPESV2 } from "@/constants";
import { takeMatchReturnSnapshot } from "@/utils/matchReturn"; import { takeMatchReturnSnapshot } from "@/utils/matchReturn";
@@ -55,6 +56,7 @@ const showOfflineModal = ref(false);
const xRingStreaks = ref({}); const xRingStreaks = ref({});
let battleEnded = false; let battleEnded = false;
let skipNextRestoreOnShow = false; let skipNextRestoreOnShow = false;
let restoreGeneration = 0;
function clearHalfRestCountdown() { function clearHalfRestCountdown() {
if (halfRestTimer) { if (halfRestTimer) {
@@ -78,13 +80,24 @@ function getHalfRestSeconds(battleInfo) {
} }
} }
const endTime = Number(battleInfo?.halfRestEndTime ?? battleInfo?.restEndTime); const timeoutTime = normalizeTimestamp(battleInfo?.timeoutTime);
if (!Number.isFinite(endTime) || endTime <= 0) return HALF_REST_SECONDS; if (timeoutTime) {
const elapsedSeconds = Math.max(0, (Date.now() - timeoutTime) / 1000);
return Math.max(
0,
Math.min(HALF_REST_SECONDS, Math.ceil(HALF_REST_SECONDS - elapsedSeconds))
);
}
const timestamp = endTime < 1e12 ? endTime * 1000 : endTime; const endTime = normalizeTimestamp(
const diffSeconds = (timestamp - Date.now()) / 1000; battleInfo?.halfRestEndTime ?? battleInfo?.restEndTime
if (diffSeconds > 0 && diffSeconds <= HALF_REST_SECONDS) { );
return Math.ceil(diffSeconds); if (endTime) {
const diffSeconds = (endTime - Date.now()) / 1000;
return Math.max(
0,
Math.min(HALF_REST_SECONDS, Math.ceil(diffSeconds))
);
} }
return HALF_REST_SECONDS; return HALF_REST_SECONDS;
@@ -92,7 +105,14 @@ function getHalfRestSeconds(battleInfo) {
function startHalfRestCountdown(seconds = HALF_REST_SECONDS) { function startHalfRestCountdown(seconds = HALF_REST_SECONDS) {
clearHalfRestCountdown(); clearHalfRestCountdown();
halfRestRemain.value = Math.max(0, Math.ceil(Number(seconds) || HALF_REST_SECONDS)); const secondsValue = Number(seconds);
const normalizedSeconds = Number.isFinite(secondsValue)
? secondsValue
: HALF_REST_SECONDS;
halfRestRemain.value = Math.max(
0,
Math.min(HALF_REST_SECONDS, Math.ceil(normalizedSeconds))
);
if (halfRestRemain.value <= 0) return; if (halfRestRemain.value <= 0) return;
@@ -305,6 +325,47 @@ function recoverData(battleInfo, { force = false } = {}) {
} }
} }
async function restoreLatestBattle() {
if (!battleId.value) return;
const currentRestoreId = ++restoreGeneration;
let result = null;
try {
result = normalizeBattleInfo(await getBattleAPI(battleId.value));
} catch (err) {
console.log("restore latest melee battle failed:", err);
return;
}
if (currentRestoreId !== restoreGeneration || !result) return;
if (result.status === 2) {
battleEnded = true;
uni.showToast({
title: "比赛已结束",
icon: "none",
});
uni.navigateBack({
delta: 2,
});
return;
}
reconnectMatchServer(result);
recoverData(result, { force: true });
}
function onMatchSocketState(event) {
if (event?.state !== "open" || event?.reconnected !== true) return;
if (
event.matchId &&
battleId.value &&
String(event.matchId) !== String(battleId.value)
) {
return;
}
restoreLatestBattle();
}
onLoad(async (options) => { onLoad(async (options) => {
const returnSnapshot = options.fromReturn ? takeMatchReturnSnapshot() : null; const returnSnapshot = options.fromReturn ? takeMatchReturnSnapshot() : null;
skipNextRestoreOnShow = false; skipNextRestoreOnShow = false;
@@ -392,7 +453,7 @@ async function onReceiveMessage(msg) {
halfTimeTip.value = true; halfTimeTip.value = true;
halfRest.value = true; halfRest.value = true;
tips.value = "准备下半场"; tips.value = "准备下半场";
startHalfRestCountdown(); startHalfRestCountdown(getHalfRestSeconds(msg));
} else if (msg.type === MESSAGETYPESV2.BattleEnd) { } else if (msg.type === MESSAGETYPESV2.BattleEnd) {
battleEnded = true; battleEnded = true;
notifyMatchAudioAck(msg); notifyMatchAudioAck(msg);
@@ -409,14 +470,17 @@ onMounted(async () => {
keepScreenOn: true, keepScreenOn: true,
}); });
uni.$on("socket-inbox", onReceiveMessage); uni.$on("socket-inbox", onReceiveMessage);
uni.$on(MATCH_WS_STATE_EVENT, onMatchSocketState);
await laserCloseAPI(); await laserCloseAPI();
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
uni.setKeepScreenOn({ uni.setKeepScreenOn({
keepScreenOn: false, keepScreenOn: false,
}); });
restoreGeneration += 1;
clearHalfRestCountdown(); clearHalfRestCountdown();
uni.$off("socket-inbox", onReceiveMessage); uni.$off("socket-inbox", onReceiveMessage);
uni.$off(MATCH_WS_STATE_EVENT, onMatchSocketState);
closeBattleServer("melee-battle-unmount"); closeBattleServer("melee-battle-unmount");
audioManager.stopAll(); audioManager.stopAll();
}); });
@@ -425,28 +489,12 @@ onHide(() => {
closeBattleServer("melee-battle-hide"); closeBattleServer("melee-battle-hide");
}); });
onShow(async () => { onShow(() => {
if (skipNextRestoreOnShow) { if (skipNextRestoreOnShow) {
skipNextRestoreOnShow = false; skipNextRestoreOnShow = false;
return; return;
} }
if (battleId.value) { restoreLatestBattle();
const result = normalizeBattleInfo(await getBattleAPI(battleId.value));
if (!result) return;
if (result.status === 2) {
battleEnded = true;
uni.showToast({
title: "比赛已结束",
icon: "none",
});
uni.navigateBack({
delta: 2,
});
} else {
reconnectMatchServer(result);
recoverData(result, { force: true });
}
}
}); });
</script> </script>
+15
View File
@@ -17,6 +17,7 @@ import {
closeMatchWebSocket, closeMatchWebSocket,
connectMatchWebSocket, connectMatchWebSocket,
MATCH_WS_AUDIO_ACK_EVENT, MATCH_WS_AUDIO_ACK_EVENT,
MATCH_WS_STATE_EVENT,
} from "@/matchWebsocket"; } from "@/matchWebsocket";
import { MESSAGETYPESV2 } from "@/constants"; import { MESSAGETYPESV2 } from "@/constants";
import { getDirectionText } from "@/util"; import { getDirectionText } from "@/util";
@@ -1227,6 +1228,18 @@ function scheduleRestoreLatestBattle() {
}, RESTORE_EMPTY_ID_RETRY_DELAY); }, RESTORE_EMPTY_ID_RETRY_DELAY);
} }
function onMatchSocketState(event) {
if (event?.state !== "open" || event?.reconnected !== true) return;
if (
event.matchId &&
battleId.value &&
String(event.matchId) !== String(battleId.value)
) {
return;
}
scheduleRestoreLatestBattle();
}
// 页面统一的 socket 回调,只负责把消息送进战况队列。 // 页面统一的 socket 回调,只负责把消息送进战况队列。
function onReceiveMessage(message) { function onReceiveMessage(message) {
enqueueBattleMessage(message); enqueueBattleMessage(message);
@@ -1302,6 +1315,7 @@ onMounted(async () => {
uni.$on(AUDIO_INTERRUPTION_END_EVENT, handleBattleRecovered); uni.$on(AUDIO_INTERRUPTION_END_EVENT, handleBattleRecovered);
uni.$on(PROGRESS_ZERO_EVENT, onProgressZero); uni.$on(PROGRESS_ZERO_EVENT, onProgressZero);
uni.$on(COUNTDOWN_READY_EVENT, hideRestoreLoading); uni.$on(COUNTDOWN_READY_EVENT, hideRestoreLoading);
uni.$on(MATCH_WS_STATE_EVENT, onMatchSocketState);
await laserCloseAPI(); await laserCloseAPI();
}); });
@@ -1314,6 +1328,7 @@ onBeforeUnmount(() => {
uni.$off("audioEnded", onAudioEnded); uni.$off("audioEnded", onAudioEnded);
uni.$off(PROGRESS_ZERO_EVENT, onProgressZero); uni.$off(PROGRESS_ZERO_EVENT, onProgressZero);
uni.$off(COUNTDOWN_READY_EVENT, hideRestoreLoading); uni.$off(COUNTDOWN_READY_EVENT, hideRestoreLoading);
uni.$off(MATCH_WS_STATE_EVENT, onMatchSocketState);
if (pendingRestoreTimer) { if (pendingRestoreTimer) {
clearTimeout(pendingRestoreTimer); clearTimeout(pendingRestoreTimer);
pendingRestoreTimer = null; pendingRestoreTimer = null;