fix: harden service websocket reconnect flows
Stabilize the service console and callback-host websocket paths so idle disconnects and mid-task client drops no longer wedge task execution or spam repeated commands. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -386,6 +386,17 @@
|
||||
};
|
||||
|
||||
let socket = null;
|
||||
let reconnectTimer = null;
|
||||
let connectTimeoutTimer = null;
|
||||
let heartbeatTimer = null;
|
||||
let shouldReconnect = false;
|
||||
let lastHeartbeatAt = 0;
|
||||
const reconnectDelayMs = 1500;
|
||||
const reconnectCloseCode = 4000;
|
||||
const reconnectCloseReason = "manual_disconnect";
|
||||
const heartbeatIntervalMs = 15000;
|
||||
const heartbeatTimeoutMs = 30000;
|
||||
const connectTimeoutMs = 5000;
|
||||
|
||||
function appendRow(kind, text) {
|
||||
if (elements.emptyState) {
|
||||
@@ -410,6 +421,59 @@
|
||||
elements.messageStream.scrollTop = elements.messageStream.scrollHeight;
|
||||
}
|
||||
|
||||
function clearReconnectTimer() {
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function clearConnectTimeoutTimer() {
|
||||
if (connectTimeoutTimer) {
|
||||
clearTimeout(connectTimeoutTimer);
|
||||
connectTimeoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function stopHeartbeat() {
|
||||
if (heartbeatTimer) {
|
||||
clearInterval(heartbeatTimer);
|
||||
heartbeatTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startHeartbeat() {
|
||||
stopHeartbeat();
|
||||
lastHeartbeatAt = Date.now();
|
||||
heartbeatTimer = setInterval(() => {
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
if (Date.now() - lastHeartbeatAt > heartbeatTimeoutMs) {
|
||||
appendRow("error", "heartbeat missed, forcing reconnect");
|
||||
const activeSocket = socket;
|
||||
socket = null;
|
||||
stopHeartbeat();
|
||||
clearConnectTimeoutTimer();
|
||||
activeSocket.close();
|
||||
scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
socket.send(JSON.stringify({ type: "ping" }));
|
||||
}, heartbeatIntervalMs);
|
||||
}
|
||||
|
||||
function scheduleReconnect() {
|
||||
clearReconnectTimer();
|
||||
clearConnectTimeoutTimer();
|
||||
if (!shouldReconnect) {
|
||||
return;
|
||||
}
|
||||
appendRow("status", "service websocket disconnected, retrying");
|
||||
reconnectTimer = setTimeout(() => connectOrDisconnectService(true), reconnectDelayMs);
|
||||
updateUiState();
|
||||
}
|
||||
|
||||
function setValidation(message) {
|
||||
elements.validationText.textContent = message;
|
||||
}
|
||||
@@ -417,7 +481,7 @@
|
||||
function updateUiState() {
|
||||
const readyState = socket ? socket.readyState : WebSocket.CLOSED;
|
||||
const connected = readyState === WebSocket.OPEN;
|
||||
const connecting = readyState === WebSocket.CONNECTING;
|
||||
const connecting = readyState === WebSocket.CONNECTING || Boolean(reconnectTimer);
|
||||
let stateText = "未连接";
|
||||
let stateValue = "disconnected";
|
||||
|
||||
@@ -435,35 +499,68 @@
|
||||
elements.connectionState.dataset.state = stateValue;
|
||||
}
|
||||
|
||||
function connectOrDisconnectService() {
|
||||
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
|
||||
socket.close();
|
||||
function connectOrDisconnectService(forceConnect = false) {
|
||||
if (!forceConnect && socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
|
||||
shouldReconnect = false;
|
||||
clearReconnectTimer();
|
||||
clearConnectTimeoutTimer();
|
||||
stopHeartbeat();
|
||||
socket.close(reconnectCloseCode, reconnectCloseReason);
|
||||
return;
|
||||
}
|
||||
|
||||
clearReconnectTimer();
|
||||
clearConnectTimeoutTimer();
|
||||
const url = elements.wsUrl.value.trim() || defaultWsUrl;
|
||||
elements.wsUrl.value = url;
|
||||
shouldReconnect = true;
|
||||
const nextSocket = new WebSocket(url);
|
||||
socket = nextSocket;
|
||||
updateUiState();
|
||||
|
||||
connectTimeoutTimer = setTimeout(() => {
|
||||
if (socket !== nextSocket || nextSocket.readyState !== WebSocket.CONNECTING) {
|
||||
return;
|
||||
}
|
||||
appendRow("error", "service websocket connect timed out");
|
||||
socket = null;
|
||||
nextSocket.close();
|
||||
scheduleReconnect();
|
||||
}, connectTimeoutMs);
|
||||
|
||||
nextSocket.addEventListener("open", () => {
|
||||
if (socket !== nextSocket) {
|
||||
return;
|
||||
}
|
||||
clearReconnectTimer();
|
||||
clearConnectTimeoutTimer();
|
||||
lastHeartbeatAt = Date.now();
|
||||
startHeartbeat();
|
||||
appendRow("status", "service websocket connected");
|
||||
updateUiState();
|
||||
});
|
||||
|
||||
nextSocket.addEventListener("close", () => {
|
||||
if (socket === nextSocket) {
|
||||
socket = null;
|
||||
nextSocket.addEventListener("close", (event) => {
|
||||
if (socket !== nextSocket) {
|
||||
return;
|
||||
}
|
||||
appendRow("status", "service websocket disconnected");
|
||||
updateUiState();
|
||||
socket = null;
|
||||
clearConnectTimeoutTimer();
|
||||
stopHeartbeat();
|
||||
const manualClose = event.code === reconnectCloseCode || event.reason === reconnectCloseReason;
|
||||
if (manualClose) {
|
||||
shouldReconnect = false;
|
||||
appendRow("status", "service websocket disconnected");
|
||||
updateUiState();
|
||||
return;
|
||||
}
|
||||
scheduleReconnect();
|
||||
});
|
||||
|
||||
nextSocket.addEventListener("error", () => {
|
||||
if (socket !== nextSocket) {
|
||||
return;
|
||||
}
|
||||
appendRow("error", "service websocket error");
|
||||
});
|
||||
|
||||
@@ -471,6 +568,7 @@
|
||||
}
|
||||
|
||||
function handleMessage(event) {
|
||||
lastHeartbeatAt = Date.now();
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(event.data);
|
||||
@@ -492,6 +590,8 @@
|
||||
case "busy":
|
||||
appendRow("error", message.message);
|
||||
break;
|
||||
case "pong":
|
||||
break;
|
||||
default:
|
||||
appendRow("error", "unknown service message: " + event.data);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user