story-kit: merge 138_bug_no_heartbeat_to_detect_stale_websocket_connections
This commit is contained in:
@@ -11,7 +11,8 @@ export type WsRequest =
|
||||
type: "permission_response";
|
||||
request_id: string;
|
||||
approved: boolean;
|
||||
};
|
||||
}
|
||||
| { type: "ping" };
|
||||
|
||||
export interface AgentAssignment {
|
||||
agent_name: string;
|
||||
@@ -60,7 +61,9 @@ export type WsResponse =
|
||||
}
|
||||
/** `.story_kit/project.toml` was modified; re-fetch the agent roster. */
|
||||
| { type: "agent_config_changed" }
|
||||
| { type: "tool_activity"; tool_name: string };
|
||||
| { type: "tool_activity"; tool_name: string }
|
||||
/** Heartbeat response confirming the connection is alive. */
|
||||
| { type: "pong" };
|
||||
|
||||
export interface ProviderConfig {
|
||||
provider: string;
|
||||
@@ -283,6 +286,30 @@ export class ChatWebSocket {
|
||||
private reconnectTimer?: number;
|
||||
private reconnectDelay = 1000;
|
||||
private shouldReconnect = false;
|
||||
private heartbeatInterval?: number;
|
||||
private heartbeatTimeout?: number;
|
||||
private static readonly HEARTBEAT_INTERVAL = 30_000;
|
||||
private static readonly HEARTBEAT_TIMEOUT = 5_000;
|
||||
|
||||
private _startHeartbeat(): void {
|
||||
this._stopHeartbeat();
|
||||
this.heartbeatInterval = window.setInterval(() => {
|
||||
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) return;
|
||||
const ping: WsRequest = { type: "ping" };
|
||||
this.socket.send(JSON.stringify(ping));
|
||||
this.heartbeatTimeout = window.setTimeout(() => {
|
||||
// No pong received within timeout; close socket to trigger reconnect.
|
||||
this.socket?.close();
|
||||
}, ChatWebSocket.HEARTBEAT_TIMEOUT);
|
||||
}, ChatWebSocket.HEARTBEAT_INTERVAL);
|
||||
}
|
||||
|
||||
private _stopHeartbeat(): void {
|
||||
window.clearInterval(this.heartbeatInterval);
|
||||
window.clearTimeout(this.heartbeatTimeout);
|
||||
this.heartbeatInterval = undefined;
|
||||
this.heartbeatTimeout = undefined;
|
||||
}
|
||||
|
||||
private _buildWsUrl(): string {
|
||||
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
|
||||
@@ -298,6 +325,7 @@ export class ChatWebSocket {
|
||||
if (!this.socket) return;
|
||||
this.socket.onopen = () => {
|
||||
this.reconnectDelay = 1000;
|
||||
this._startHeartbeat();
|
||||
};
|
||||
this.socket.onmessage = (event) => {
|
||||
try {
|
||||
@@ -327,6 +355,10 @@ export class ChatWebSocket {
|
||||
data.message,
|
||||
);
|
||||
if (data.type === "agent_config_changed") this.onAgentConfigChanged?.();
|
||||
if (data.type === "pong") {
|
||||
window.clearTimeout(this.heartbeatTimeout);
|
||||
this.heartbeatTimeout = undefined;
|
||||
}
|
||||
} catch (err) {
|
||||
this.onError?.(String(err));
|
||||
}
|
||||
@@ -420,6 +452,7 @@ export class ChatWebSocket {
|
||||
|
||||
close() {
|
||||
this.shouldReconnect = false;
|
||||
this._stopHeartbeat();
|
||||
window.clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = undefined;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user