@qodo/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +118 -0
- package/README.md +121 -0
- package/dist/api/agent.d.ts +69 -0
- package/dist/api/agent.d.ts.map +1 -0
- package/dist/api/agent.js +1034 -0
- package/dist/api/agent.js.map +1 -0
- package/dist/api/analytics.d.ts +43 -0
- package/dist/api/analytics.d.ts.map +1 -0
- package/dist/api/analytics.js +163 -0
- package/dist/api/analytics.js.map +1 -0
- package/dist/api/http.d.ts +5 -0
- package/dist/api/http.d.ts.map +1 -0
- package/dist/api/http.js +59 -0
- package/dist/api/http.js.map +1 -0
- package/dist/api/index.d.ts +12 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +17 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/taskTracking.d.ts +54 -0
- package/dist/api/taskTracking.d.ts.map +1 -0
- package/dist/api/taskTracking.js +208 -0
- package/dist/api/taskTracking.js.map +1 -0
- package/dist/api/types.d.ts +92 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +2 -0
- package/dist/api/types.js.map +1 -0
- package/dist/api/utils.d.ts +8 -0
- package/dist/api/utils.d.ts.map +1 -0
- package/dist/api/utils.js +54 -0
- package/dist/api/utils.js.map +1 -0
- package/dist/api/websocket.d.ts +74 -0
- package/dist/api/websocket.d.ts.map +1 -0
- package/dist/api/websocket.js +685 -0
- package/dist/api/websocket.js.map +1 -0
- package/dist/auth/index.d.ts +25 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +85 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/clients/index.d.ts +8 -0
- package/dist/clients/index.d.ts.map +1 -0
- package/dist/clients/index.js +7 -0
- package/dist/clients/index.js.map +1 -0
- package/dist/clients/info/InfoClient.d.ts +37 -0
- package/dist/clients/info/InfoClient.d.ts.map +1 -0
- package/dist/clients/info/InfoClient.js +69 -0
- package/dist/clients/info/InfoClient.js.map +1 -0
- package/dist/clients/info/index.d.ts +4 -0
- package/dist/clients/info/index.d.ts.map +1 -0
- package/dist/clients/info/index.js +2 -0
- package/dist/clients/info/index.js.map +1 -0
- package/dist/clients/info/types.d.ts +21 -0
- package/dist/clients/info/types.d.ts.map +1 -0
- package/dist/clients/info/types.js +2 -0
- package/dist/clients/info/types.js.map +1 -0
- package/dist/clients/sessions/SessionsClient.d.ts +34 -0
- package/dist/clients/sessions/SessionsClient.d.ts.map +1 -0
- package/dist/clients/sessions/SessionsClient.js +71 -0
- package/dist/clients/sessions/SessionsClient.js.map +1 -0
- package/dist/clients/sessions/index.d.ts +4 -0
- package/dist/clients/sessions/index.d.ts.map +1 -0
- package/dist/clients/sessions/index.js +2 -0
- package/dist/clients/sessions/index.js.map +1 -0
- package/dist/clients/sessions/types.d.ts +20 -0
- package/dist/clients/sessions/types.d.ts.map +1 -0
- package/dist/clients/sessions/types.js +2 -0
- package/dist/clients/sessions/types.js.map +1 -0
- package/dist/config/ConfigManager.d.ts +43 -0
- package/dist/config/ConfigManager.d.ts.map +1 -0
- package/dist/config/ConfigManager.js +472 -0
- package/dist/config/ConfigManager.js.map +1 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +7 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/urlConfig.d.ts +15 -0
- package/dist/config/urlConfig.d.ts.map +1 -0
- package/dist/config/urlConfig.js +75 -0
- package/dist/config/urlConfig.js.map +1 -0
- package/dist/constants/errors.d.ts +2 -0
- package/dist/constants/errors.d.ts.map +1 -0
- package/dist/constants/errors.js +2 -0
- package/dist/constants/errors.js.map +1 -0
- package/dist/constants/index.d.ts +7 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/constants/index.js +11 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/constants/tools.d.ts +4 -0
- package/dist/constants/tools.d.ts.map +1 -0
- package/dist/constants/tools.js +4 -0
- package/dist/constants/tools.js.map +1 -0
- package/dist/constants/versions.d.ts +2 -0
- package/dist/constants/versions.d.ts.map +1 -0
- package/dist/constants/versions.js +2 -0
- package/dist/constants/versions.js.map +1 -0
- package/dist/context/buildUserContext.d.ts +18 -0
- package/dist/context/buildUserContext.d.ts.map +1 -0
- package/dist/context/buildUserContext.js +34 -0
- package/dist/context/buildUserContext.js.map +1 -0
- package/dist/context/index.d.ts +9 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +9 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/messageManager.d.ts +42 -0
- package/dist/context/messageManager.d.ts.map +1 -0
- package/dist/context/messageManager.js +322 -0
- package/dist/context/messageManager.js.map +1 -0
- package/dist/context/taskFocus.d.ts +2 -0
- package/dist/context/taskFocus.d.ts.map +1 -0
- package/dist/context/taskFocus.js +26 -0
- package/dist/context/taskFocus.js.map +1 -0
- package/dist/context/userInput.d.ts +3 -0
- package/dist/context/userInput.d.ts.map +1 -0
- package/dist/context/userInput.js +20 -0
- package/dist/context/userInput.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/MCPManager.d.ts +125 -0
- package/dist/mcp/MCPManager.d.ts.map +1 -0
- package/dist/mcp/MCPManager.js +616 -0
- package/dist/mcp/MCPManager.js.map +1 -0
- package/dist/mcp/approvedTools.d.ts +4 -0
- package/dist/mcp/approvedTools.d.ts.map +1 -0
- package/dist/mcp/approvedTools.js +19 -0
- package/dist/mcp/approvedTools.js.map +1 -0
- package/dist/mcp/baseServer.d.ts +75 -0
- package/dist/mcp/baseServer.d.ts.map +1 -0
- package/dist/mcp/baseServer.js +107 -0
- package/dist/mcp/baseServer.js.map +1 -0
- package/dist/mcp/builtinServers.d.ts +15 -0
- package/dist/mcp/builtinServers.d.ts.map +1 -0
- package/dist/mcp/builtinServers.js +155 -0
- package/dist/mcp/builtinServers.js.map +1 -0
- package/dist/mcp/dynamicBEServer.d.ts +20 -0
- package/dist/mcp/dynamicBEServer.d.ts.map +1 -0
- package/dist/mcp/dynamicBEServer.js +52 -0
- package/dist/mcp/dynamicBEServer.js.map +1 -0
- package/dist/mcp/index.d.ts +19 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +24 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/mcpInitialization.d.ts +2 -0
- package/dist/mcp/mcpInitialization.d.ts.map +1 -0
- package/dist/mcp/mcpInitialization.js +56 -0
- package/dist/mcp/mcpInitialization.js.map +1 -0
- package/dist/mcp/servers/filesystem.d.ts +75 -0
- package/dist/mcp/servers/filesystem.d.ts.map +1 -0
- package/dist/mcp/servers/filesystem.js +992 -0
- package/dist/mcp/servers/filesystem.js.map +1 -0
- package/dist/mcp/servers/gerrit.d.ts +19 -0
- package/dist/mcp/servers/gerrit.d.ts.map +1 -0
- package/dist/mcp/servers/gerrit.js +515 -0
- package/dist/mcp/servers/gerrit.js.map +1 -0
- package/dist/mcp/servers/git.d.ts +18 -0
- package/dist/mcp/servers/git.d.ts.map +1 -0
- package/dist/mcp/servers/git.js +441 -0
- package/dist/mcp/servers/git.js.map +1 -0
- package/dist/mcp/servers/ripgrep.d.ts +34 -0
- package/dist/mcp/servers/ripgrep.d.ts.map +1 -0
- package/dist/mcp/servers/ripgrep.js +517 -0
- package/dist/mcp/servers/ripgrep.js.map +1 -0
- package/dist/mcp/servers/shell.d.ts +20 -0
- package/dist/mcp/servers/shell.d.ts.map +1 -0
- package/dist/mcp/servers/shell.js +603 -0
- package/dist/mcp/servers/shell.js.map +1 -0
- package/dist/mcp/serversRegistry.d.ts +55 -0
- package/dist/mcp/serversRegistry.d.ts.map +1 -0
- package/dist/mcp/serversRegistry.js +410 -0
- package/dist/mcp/serversRegistry.js.map +1 -0
- package/dist/mcp/toolProcessor.d.ts +42 -0
- package/dist/mcp/toolProcessor.d.ts.map +1 -0
- package/dist/mcp/toolProcessor.js +200 -0
- package/dist/mcp/toolProcessor.js.map +1 -0
- package/dist/mcp/types.d.ts +29 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/parser/index.d.ts +72 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +967 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/types.d.ts +153 -0
- package/dist/parser/types.d.ts.map +1 -0
- package/dist/parser/types.js +6 -0
- package/dist/parser/types.js.map +1 -0
- package/dist/parser/utils.d.ts +18 -0
- package/dist/parser/utils.d.ts.map +1 -0
- package/dist/parser/utils.js +64 -0
- package/dist/parser/utils.js.map +1 -0
- package/dist/sdk/QodoSDK.d.ts +152 -0
- package/dist/sdk/QodoSDK.d.ts.map +1 -0
- package/dist/sdk/QodoSDK.js +786 -0
- package/dist/sdk/QodoSDK.js.map +1 -0
- package/dist/sdk/bootstrap.d.ts +16 -0
- package/dist/sdk/bootstrap.d.ts.map +1 -0
- package/dist/sdk/bootstrap.js +21 -0
- package/dist/sdk/bootstrap.js.map +1 -0
- package/dist/sdk/builders.d.ts +54 -0
- package/dist/sdk/builders.d.ts.map +1 -0
- package/dist/sdk/builders.js +117 -0
- package/dist/sdk/builders.js.map +1 -0
- package/dist/sdk/defaults.d.ts +11 -0
- package/dist/sdk/defaults.d.ts.map +1 -0
- package/dist/sdk/defaults.js +39 -0
- package/dist/sdk/defaults.js.map +1 -0
- package/dist/sdk/discovery.d.ts +2 -0
- package/dist/sdk/discovery.d.ts.map +1 -0
- package/dist/sdk/discovery.js +25 -0
- package/dist/sdk/discovery.js.map +1 -0
- package/dist/sdk/events.d.ts +168 -0
- package/dist/sdk/events.d.ts.map +1 -0
- package/dist/sdk/events.js +52 -0
- package/dist/sdk/events.js.map +1 -0
- package/dist/sdk/index.d.ts +17 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +17 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/sdk/runner/AgentRunner.d.ts +22 -0
- package/dist/sdk/runner/AgentRunner.d.ts.map +1 -0
- package/dist/sdk/runner/AgentRunner.js +222 -0
- package/dist/sdk/runner/AgentRunner.js.map +1 -0
- package/dist/sdk/runner/finalize.d.ts +9 -0
- package/dist/sdk/runner/finalize.d.ts.map +1 -0
- package/dist/sdk/runner/finalize.js +115 -0
- package/dist/sdk/runner/finalize.js.map +1 -0
- package/dist/sdk/runner/formats.d.ts +7 -0
- package/dist/sdk/runner/formats.d.ts.map +1 -0
- package/dist/sdk/runner/formats.js +91 -0
- package/dist/sdk/runner/formats.js.map +1 -0
- package/dist/sdk/runner/index.d.ts +9 -0
- package/dist/sdk/runner/index.d.ts.map +1 -0
- package/dist/sdk/runner/index.js +9 -0
- package/dist/sdk/runner/index.js.map +1 -0
- package/dist/sdk/runner/progress.d.ts +3 -0
- package/dist/sdk/runner/progress.d.ts.map +1 -0
- package/dist/sdk/runner/progress.js +16 -0
- package/dist/sdk/runner/progress.js.map +1 -0
- package/dist/sdk/schemas.d.ts +50 -0
- package/dist/sdk/schemas.d.ts.map +1 -0
- package/dist/sdk/schemas.js +145 -0
- package/dist/sdk/schemas.js.map +1 -0
- package/dist/session/SessionContext.d.ts +86 -0
- package/dist/session/SessionContext.d.ts.map +1 -0
- package/dist/session/SessionContext.js +395 -0
- package/dist/session/SessionContext.js.map +1 -0
- package/dist/session/environment.d.ts +42 -0
- package/dist/session/environment.d.ts.map +1 -0
- package/dist/session/environment.js +27 -0
- package/dist/session/environment.js.map +1 -0
- package/dist/session/history.d.ts +3 -0
- package/dist/session/history.d.ts.map +1 -0
- package/dist/session/history.js +67 -0
- package/dist/session/history.js.map +1 -0
- package/dist/session/index.d.ts +10 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +9 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/serverData.d.ts +38 -0
- package/dist/session/serverData.d.ts.map +1 -0
- package/dist/session/serverData.js +241 -0
- package/dist/session/serverData.js.map +1 -0
- package/dist/tracking/Tracker.d.ts +55 -0
- package/dist/tracking/Tracker.d.ts.map +1 -0
- package/dist/tracking/Tracker.js +217 -0
- package/dist/tracking/Tracker.js.map +1 -0
- package/dist/tracking/index.d.ts +8 -0
- package/dist/tracking/index.d.ts.map +1 -0
- package/dist/tracking/index.js +8 -0
- package/dist/tracking/index.js.map +1 -0
- package/dist/tracking/schemas.d.ts +292 -0
- package/dist/tracking/schemas.d.ts.map +1 -0
- package/dist/tracking/schemas.js +91 -0
- package/dist/tracking/schemas.js.map +1 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/extractSetFlags.d.ts +6 -0
- package/dist/utils/extractSetFlags.d.ts.map +1 -0
- package/dist/utils/extractSetFlags.js +16 -0
- package/dist/utils/extractSetFlags.js.map +1 -0
- package/dist/utils/formatTimeAgo.d.ts +2 -0
- package/dist/utils/formatTimeAgo.d.ts.map +1 -0
- package/dist/utils/formatTimeAgo.js +20 -0
- package/dist/utils/formatTimeAgo.js.map +1 -0
- package/dist/utils/index.d.ts +12 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +12 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/machineId.d.ts +14 -0
- package/dist/utils/machineId.d.ts.map +1 -0
- package/dist/utils/machineId.js +66 -0
- package/dist/utils/machineId.js.map +1 -0
- package/dist/utils/pathUtils.d.ts +22 -0
- package/dist/utils/pathUtils.d.ts.map +1 -0
- package/dist/utils/pathUtils.js +54 -0
- package/dist/utils/pathUtils.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- package/package.json +93 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
import { v4 as uuid } from "uuid";
|
|
4
|
+
import { getAuthConfig } from "../auth/index.js";
|
|
5
|
+
import { ServerData } from "../session/index.js";
|
|
6
|
+
import { getCurrentEnvironment } from "../session/index.js";
|
|
7
|
+
// WebSocket-specific configuration
|
|
8
|
+
const WS_CONFIG = {
|
|
9
|
+
IDLE_TIMEOUT: 600000,
|
|
10
|
+
HEARTBEAT_INTERVAL: 30000,
|
|
11
|
+
HEARTBEAT_TIMEOUT: 60000,
|
|
12
|
+
CONNECTION_TIMEOUT: 15000,
|
|
13
|
+
INITIAL_RECONNECT_DELAY: 1000,
|
|
14
|
+
MAX_RECONNECT_DELAY: 30000,
|
|
15
|
+
RECONNECT_BACKOFF_FACTOR: 1.5,
|
|
16
|
+
MESSAGE_QUEUE_MAX_SIZE: 1000,
|
|
17
|
+
READY_TIMEOUT: 300000,
|
|
18
|
+
};
|
|
19
|
+
export var ConnectionState;
|
|
20
|
+
(function (ConnectionState) {
|
|
21
|
+
ConnectionState["DISCONNECTED"] = "DISCONNECTED";
|
|
22
|
+
ConnectionState["CONNECTING"] = "CONNECTING";
|
|
23
|
+
ConnectionState["CONNECTED"] = "CONNECTED";
|
|
24
|
+
ConnectionState["RECONNECTING"] = "RECONNECTING";
|
|
25
|
+
ConnectionState["DISCONNECTING"] = "DISCONNECTING";
|
|
26
|
+
ConnectionState["FAILED"] = "FAILED";
|
|
27
|
+
})(ConnectionState || (ConnectionState = {}));
|
|
28
|
+
export var ReadyState;
|
|
29
|
+
(function (ReadyState) {
|
|
30
|
+
ReadyState["IDLE"] = "IDLE";
|
|
31
|
+
ReadyState["WAITING_INITIAL_READY"] = "WAITING_INITIAL_READY";
|
|
32
|
+
ReadyState["READY"] = "READY";
|
|
33
|
+
ReadyState["MESSAGE_SENT"] = "MESSAGE_SENT";
|
|
34
|
+
ReadyState["WAITING_READY"] = "WAITING_READY";
|
|
35
|
+
ReadyState["CHECKPOINT_RECOVERY"] = "CHECKPOINT_RECOVERY";
|
|
36
|
+
})(ReadyState || (ReadyState = {}));
|
|
37
|
+
export class WebSocketClient extends EventEmitter {
|
|
38
|
+
ws;
|
|
39
|
+
shouldDebugLog() {
|
|
40
|
+
const env = getCurrentEnvironment();
|
|
41
|
+
// In SDK mode, default is silent unless sdkDebug=true or QODO_DEBUG=true.
|
|
42
|
+
if (env?.sdkMode) {
|
|
43
|
+
return !!env.sdkDebug || process.env.QODO_DEBUG === 'true';
|
|
44
|
+
}
|
|
45
|
+
return process.env.QODO_DEBUG === 'true';
|
|
46
|
+
}
|
|
47
|
+
debug(...args) {
|
|
48
|
+
if (this.shouldDebugLog()) {
|
|
49
|
+
// eslint-disable-next-line no-console
|
|
50
|
+
console.debug(...args);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
state = ConnectionState.DISCONNECTED;
|
|
54
|
+
sessionId;
|
|
55
|
+
requestId;
|
|
56
|
+
connectionId;
|
|
57
|
+
// Idle management
|
|
58
|
+
idleTimer;
|
|
59
|
+
autoDisconnected = false;
|
|
60
|
+
// Heartbeat management
|
|
61
|
+
heartbeatInterval;
|
|
62
|
+
lastPongReceived = Date.now();
|
|
63
|
+
lastPingSent = 0;
|
|
64
|
+
// Reconnection management
|
|
65
|
+
reconnectAttempts = 0;
|
|
66
|
+
reconnectTimer;
|
|
67
|
+
isReconnecting = false;
|
|
68
|
+
// Message queue for handling during disconnection
|
|
69
|
+
messageQueue = [];
|
|
70
|
+
isProcessingQueue = false;
|
|
71
|
+
// Ready protocol state management
|
|
72
|
+
readyState = ReadyState.IDLE;
|
|
73
|
+
latestCheckpointId;
|
|
74
|
+
lastSentMessage;
|
|
75
|
+
readyTimer;
|
|
76
|
+
pendingOutbox = [];
|
|
77
|
+
receivedResponsesSinceMessage = false; // Track if any responses received after sending message
|
|
78
|
+
hasReceivedResponsesSinceLastMessage() {
|
|
79
|
+
return this.receivedResponsesSinceMessage;
|
|
80
|
+
}
|
|
81
|
+
// Connection-timeout timer (ensure we clear/unref to avoid keeping event loop alive)
|
|
82
|
+
connectionTimeoutTimer;
|
|
83
|
+
constructor() {
|
|
84
|
+
super();
|
|
85
|
+
}
|
|
86
|
+
setState(newState) {
|
|
87
|
+
if (this.state !== newState) {
|
|
88
|
+
const oldState = this.state;
|
|
89
|
+
this.state = newState;
|
|
90
|
+
this.debug(`[WebSocketClient] Connection state transition: ${oldState} → ${newState} | ` +
|
|
91
|
+
`ReadyState: ${this.readyState} | ` +
|
|
92
|
+
`Session: ${this.sessionId?.substring(0, 8)}...`);
|
|
93
|
+
this.emit('stateChanged', newState);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
setReadyState(newState, context) {
|
|
97
|
+
if (this.readyState !== newState) {
|
|
98
|
+
const oldState = this.readyState;
|
|
99
|
+
this.readyState = newState;
|
|
100
|
+
this.debug(`[WebSocketClient] Ready state transition: ${oldState} → ${newState}` +
|
|
101
|
+
(context ? ` | Context: ${context}` : '') +
|
|
102
|
+
` | Session: ${this.sessionId?.substring(0, 8)}...` +
|
|
103
|
+
` | Checkpoint: ${this.latestCheckpointId?.substring(0, 8) || 'none'}`);
|
|
104
|
+
this.emit('readyStateChanged', { oldState, newState, context });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async handleReadyMessage(message) {
|
|
108
|
+
const checkpointId = message.data?.tool_args?.checkpoint_id;
|
|
109
|
+
const previousCheckpoint = this.latestCheckpointId;
|
|
110
|
+
const previousReadyState = this.readyState;
|
|
111
|
+
this.debug(`[WebSocketClient] Ready message received | ` +
|
|
112
|
+
`State: ${this.readyState} | ` +
|
|
113
|
+
`Checkpoint: ${checkpointId?.substring(0, 8) || 'none'} | ` +
|
|
114
|
+
`Previous: ${previousCheckpoint?.substring(0, 8) || 'none'}`);
|
|
115
|
+
// Update checkpoint if provided and changed
|
|
116
|
+
if (checkpointId && checkpointId !== previousCheckpoint) {
|
|
117
|
+
this.latestCheckpointId = checkpointId;
|
|
118
|
+
this.debug(`[WebSocketClient] Checkpoint ID updated: ${previousCheckpoint?.substring(0, 8) || 'none'} → ` +
|
|
119
|
+
`${checkpointId.substring(0, 8)}`);
|
|
120
|
+
}
|
|
121
|
+
// Handle based on current state
|
|
122
|
+
switch (this.readyState) {
|
|
123
|
+
case ReadyState.WAITING_INITIAL_READY:
|
|
124
|
+
this.clearReadyTimer();
|
|
125
|
+
this.setReadyState(ReadyState.READY, 'Initial Ready received after connection');
|
|
126
|
+
await this.processOutbox();
|
|
127
|
+
break;
|
|
128
|
+
case ReadyState.WAITING_READY:
|
|
129
|
+
this.clearReadyTimer();
|
|
130
|
+
this.setReadyState(ReadyState.READY, 'Ready received after responses completed');
|
|
131
|
+
await this.processOutbox();
|
|
132
|
+
break;
|
|
133
|
+
case ReadyState.MESSAGE_SENT:
|
|
134
|
+
// Ready in MESSAGE_SENT should only be accepted if responses were received
|
|
135
|
+
if (!this.receivedResponsesSinceMessage) {
|
|
136
|
+
this.debug(`[WebSocketClient] Unexpected Ready without responses | ` +
|
|
137
|
+
`Checkpoint: ${checkpointId?.substring(0, 8) || 'none'} | ` +
|
|
138
|
+
`This indicates server failed to process the message`);
|
|
139
|
+
// Trigger checkpoint recovery - server acknowledged but didn't process
|
|
140
|
+
this.clearReadyTimer();
|
|
141
|
+
await this.initiateCheckpointRecovery('Ready received without any responses (server processing failure)');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// Normal case: Ready after responses signals server finished processing
|
|
145
|
+
this.debug('[WebSocketClient] Ready received, responses complete');
|
|
146
|
+
this.clearReadyTimer();
|
|
147
|
+
this.setReadyState(ReadyState.READY, 'Ready received, server finished processing');
|
|
148
|
+
await this.processOutbox();
|
|
149
|
+
break;
|
|
150
|
+
case ReadyState.READY:
|
|
151
|
+
this.debug('[WebSocketClient] Ready received while already in READY state (duplicate)');
|
|
152
|
+
break;
|
|
153
|
+
case ReadyState.IDLE:
|
|
154
|
+
this.debug('[WebSocketClient] Ready received while IDLE - transitioning to READY');
|
|
155
|
+
this.setReadyState(ReadyState.READY, 'Ready received while idle');
|
|
156
|
+
break;
|
|
157
|
+
case ReadyState.CHECKPOINT_RECOVERY:
|
|
158
|
+
this.debug('[WebSocketClient] Ready received during recovery - resuming normal flow');
|
|
159
|
+
this.clearReadyTimer();
|
|
160
|
+
this.setReadyState(ReadyState.READY, 'Recovery completed, Ready received');
|
|
161
|
+
await this.processOutbox();
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
// Emit event for monitoring.
|
|
165
|
+
// Include previous ready state so listeners can distinguish initial Ready
|
|
166
|
+
// from completion Ready after MESSAGE_SENT.
|
|
167
|
+
this.emit('readyReceived', checkpointId, previousReadyState);
|
|
168
|
+
}
|
|
169
|
+
async connect(sessionId, requestId) {
|
|
170
|
+
// Update session info if provided
|
|
171
|
+
if (sessionId)
|
|
172
|
+
this.sessionId = sessionId;
|
|
173
|
+
if (requestId)
|
|
174
|
+
this.requestId = requestId;
|
|
175
|
+
if (this.state === ConnectionState.CONNECTED) {
|
|
176
|
+
this.resetIdleTimer();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// If we auto-disconnected due to idle, reconnect
|
|
180
|
+
if (this.autoDisconnected || this.state === ConnectionState.DISCONNECTED) {
|
|
181
|
+
this.autoDisconnected = false;
|
|
182
|
+
await this.establishConnection();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async sendMessage(type, data) {
|
|
186
|
+
// Reset idle timer on any outgoing message
|
|
187
|
+
this.resetIdleTimer();
|
|
188
|
+
const messageToSend = { type, data, timestamp: Date.now() };
|
|
189
|
+
this.debug(`[WebSocketClient] sendMessage called | ` +
|
|
190
|
+
`Type: ${type} | ` +
|
|
191
|
+
`ReadyState: ${this.readyState} | ` +
|
|
192
|
+
`ConnectionState: ${this.state}`);
|
|
193
|
+
// Save as last sent message for potential recovery
|
|
194
|
+
this.lastSentMessage = messageToSend;
|
|
195
|
+
this.debug(`[WebSocketClient] Last sent message cached | ` +
|
|
196
|
+
`Type: ${type} | ` +
|
|
197
|
+
`Timestamp: ${messageToSend.timestamp}`);
|
|
198
|
+
if (this.readyState === ReadyState.READY && this.ws && this.state === ConnectionState.CONNECTED) {
|
|
199
|
+
// Send immediately
|
|
200
|
+
this.receivedResponsesSinceMessage = false; // Reset flag when sending new message
|
|
201
|
+
await this.sendImmediately(messageToSend);
|
|
202
|
+
this.setReadyState(ReadyState.MESSAGE_SENT, `Sent ${type} message`);
|
|
203
|
+
this.startReadyTimer(); // Expect Ready after server processes message
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
// Queue until Ready
|
|
207
|
+
this.pendingOutbox.push(messageToSend);
|
|
208
|
+
this.debug(`[WebSocketClient] Message queued | ` +
|
|
209
|
+
`Type: ${type} | ` +
|
|
210
|
+
`Queue size: ${this.pendingOutbox.length} | ` +
|
|
211
|
+
`Reason: ReadyState=${this.readyState}, Connected=${this.state === ConnectionState.CONNECTED}`);
|
|
212
|
+
if (this.state === ConnectionState.DISCONNECTED) {
|
|
213
|
+
this.debug('[WebSocketClient] Initiating connection due to queued message');
|
|
214
|
+
await this.connect();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async sendImmediately(msg) {
|
|
219
|
+
const formatted = this.formatMessage(msg.type, msg.data);
|
|
220
|
+
this.ws.send(formatted);
|
|
221
|
+
this.debug(`[WebSocketClient] Message sent immediately | ` +
|
|
222
|
+
`Type: ${msg.type} | ` +
|
|
223
|
+
`Size: ${formatted.length} bytes`);
|
|
224
|
+
}
|
|
225
|
+
async processOutbox() {
|
|
226
|
+
this.debug(`[WebSocketClient] Processing outbox | ` +
|
|
227
|
+
`Queue size: ${this.pendingOutbox.length} | ` +
|
|
228
|
+
`ReadyState: ${this.readyState}`);
|
|
229
|
+
if (this.pendingOutbox.length > 0 && this.readyState === ReadyState.READY) {
|
|
230
|
+
const msg = this.pendingOutbox.shift();
|
|
231
|
+
this.debug(`[WebSocketClient] Sending queued message | ` +
|
|
232
|
+
`Type: ${msg.type} | ` +
|
|
233
|
+
`Queued at: ${msg.timestamp} | ` +
|
|
234
|
+
`Wait time: ${Date.now() - msg.timestamp}ms | ` +
|
|
235
|
+
`Remaining in queue: ${this.pendingOutbox.length}`);
|
|
236
|
+
this.receivedResponsesSinceMessage = false; // Reset flag when sending queued message
|
|
237
|
+
await this.sendImmediately(msg);
|
|
238
|
+
this.setReadyState(ReadyState.MESSAGE_SENT, `Sent queued ${msg.type} message`);
|
|
239
|
+
this.startReadyTimer(); // Expect Ready after server processes message
|
|
240
|
+
}
|
|
241
|
+
else if (this.pendingOutbox.length === 0) {
|
|
242
|
+
this.debug('[WebSocketClient] Outbox is empty, no messages to process');
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
this.debug(`[WebSocketClient] Cannot process outbox | ` +
|
|
246
|
+
`ReadyState: ${this.readyState} (expected READY)`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
startReadyTimer() {
|
|
250
|
+
this.clearReadyTimer();
|
|
251
|
+
this.readyTimer = setTimeout(async () => {
|
|
252
|
+
this.debug(`[WebSocketClient] Ready timeout expired | ` +
|
|
253
|
+
`State: ${this.readyState} | ` +
|
|
254
|
+
`Last message: ${this.lastSentMessage?.type} | ` +
|
|
255
|
+
`Checkpoint: ${this.latestCheckpointId?.substring(0, 8) || 'none'}`);
|
|
256
|
+
await this.initiateCheckpointRecovery('Ready timeout');
|
|
257
|
+
}, WS_CONFIG.READY_TIMEOUT);
|
|
258
|
+
// Do not keep the event loop alive solely because of this timer
|
|
259
|
+
this.readyTimer.unref?.();
|
|
260
|
+
}
|
|
261
|
+
clearReadyTimer() {
|
|
262
|
+
if (this.readyTimer) {
|
|
263
|
+
clearTimeout(this.readyTimer);
|
|
264
|
+
this.readyTimer = undefined;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async initiateCheckpointRecovery(reason) {
|
|
268
|
+
this.debug(`[WebSocketClient] ===== CHECKPOINT RECOVERY INITIATED ===== | ` +
|
|
269
|
+
`Reason: ${reason} | ` +
|
|
270
|
+
`Checkpoint: ${this.latestCheckpointId?.substring(0, 8) || 'none'} | ` +
|
|
271
|
+
`Last message type: ${this.lastSentMessage?.type} | ` +
|
|
272
|
+
`Current state: ${this.readyState}`);
|
|
273
|
+
this.setReadyState(ReadyState.CHECKPOINT_RECOVERY, reason);
|
|
274
|
+
// Disconnect current connection
|
|
275
|
+
this.debug('[WebSocketClient] Disconnecting for checkpoint recovery');
|
|
276
|
+
this.disconnect();
|
|
277
|
+
// Reconnect with checkpoint (URL will include checkpoint_id)
|
|
278
|
+
this.debug(`[WebSocketClient] Reconnecting with checkpoint | ` +
|
|
279
|
+
`Checkpoint ID: ${this.latestCheckpointId?.substring(0, 8) || 'none'} | ` +
|
|
280
|
+
`Session ID: ${this.sessionId?.substring(0, 8)}...`);
|
|
281
|
+
try {
|
|
282
|
+
await this.establishConnection();
|
|
283
|
+
this.debug('[WebSocketClient] Reconnection successful during recovery');
|
|
284
|
+
// Wait for Ready state is set in handleReadyMessage when initial Ready is received
|
|
285
|
+
// Resend last message once Ready received
|
|
286
|
+
if (this.lastSentMessage) {
|
|
287
|
+
this.pendingOutbox.unshift(this.lastSentMessage);
|
|
288
|
+
this.debug(`[WebSocketClient] Last message re-queued for recovery | ` +
|
|
289
|
+
`Type: ${this.lastSentMessage.type} | ` +
|
|
290
|
+
`Original timestamp: ${this.lastSentMessage.timestamp}`);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
this.debug('[WebSocketClient] No last message to resend during recovery');
|
|
294
|
+
}
|
|
295
|
+
// Emit event for monitoring
|
|
296
|
+
this.emit('checkpointRecovery', { reason, checkpoint: this.latestCheckpointId });
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
this.debug(`[WebSocketClient] Checkpoint recovery failed | ` +
|
|
300
|
+
`Error: ${error instanceof Error ? error.message : error}`);
|
|
301
|
+
this.setReadyState(ReadyState.IDLE, 'Recovery failed');
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
this.debug('[WebSocketClient] ===== CHECKPOINT RECOVERY COMPLETED =====');
|
|
305
|
+
}
|
|
306
|
+
disconnect() {
|
|
307
|
+
// Hold a reference to the current socket to close it safely
|
|
308
|
+
const wsRef = this.ws;
|
|
309
|
+
// Stop all timers and reconnection attempts
|
|
310
|
+
this.clearTimers();
|
|
311
|
+
this.autoDisconnected = false;
|
|
312
|
+
this.isReconnecting = false;
|
|
313
|
+
// Invalidate the current connection so any late events from wsRef are ignored
|
|
314
|
+
// Event handlers compare this.connectionId to their captured id
|
|
315
|
+
this.connectionId = uuid();
|
|
316
|
+
if (wsRef) {
|
|
317
|
+
try {
|
|
318
|
+
// Attempt a graceful close when open; otherwise force terminate to avoid races
|
|
319
|
+
if (wsRef.readyState === WebSocket.OPEN) {
|
|
320
|
+
this.setState(ConnectionState.DISCONNECTING);
|
|
321
|
+
wsRef.close(1000, 'Normal closure');
|
|
322
|
+
}
|
|
323
|
+
else if (wsRef.readyState === WebSocket.CONNECTING || wsRef.readyState === WebSocket.CLOSING) {
|
|
324
|
+
// Avoid an "open" event racing in after manual disconnect
|
|
325
|
+
try {
|
|
326
|
+
wsRef.terminate?.();
|
|
327
|
+
}
|
|
328
|
+
catch { }
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
catch { }
|
|
332
|
+
}
|
|
333
|
+
// Explicitly mark as disconnected and reset internal refs
|
|
334
|
+
this.setState(ConnectionState.DISCONNECTED);
|
|
335
|
+
this.ws = undefined;
|
|
336
|
+
this.setReadyState(ReadyState.IDLE, 'Manual disconnect');
|
|
337
|
+
this.lastPingSent = 0;
|
|
338
|
+
}
|
|
339
|
+
getState() {
|
|
340
|
+
return this.state;
|
|
341
|
+
}
|
|
342
|
+
keepAlive() {
|
|
343
|
+
if (this.state === ConnectionState.CONNECTED) {
|
|
344
|
+
this.resetIdleTimer();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// === Private Methods ===
|
|
348
|
+
async establishConnection() {
|
|
349
|
+
if (this.state === ConnectionState.CONNECTING) {
|
|
350
|
+
await this.waitForConnection();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
return new Promise((resolve, reject) => {
|
|
354
|
+
try {
|
|
355
|
+
this.setState(ConnectionState.CONNECTING);
|
|
356
|
+
this.connectionId = uuid();
|
|
357
|
+
const currentConnectionId = this.connectionId;
|
|
358
|
+
const wsUrl = this.buildWebSocketUrl();
|
|
359
|
+
const { token } = getAuthConfig();
|
|
360
|
+
this.debug(`Connecting to WebSocket: ${wsUrl}`);
|
|
361
|
+
const ws = new WebSocket(wsUrl, {
|
|
362
|
+
headers: {
|
|
363
|
+
'Authorization': `Bearer ${token}`,
|
|
364
|
+
},
|
|
365
|
+
handshakeTimeout: WS_CONFIG.CONNECTION_TIMEOUT,
|
|
366
|
+
});
|
|
367
|
+
this.ws = ws;
|
|
368
|
+
// Set up event handlers
|
|
369
|
+
ws.on('open', () => {
|
|
370
|
+
if (this.connectionId !== currentConnectionId) {
|
|
371
|
+
ws.close();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
// Clear pending connection timeout timer if any
|
|
375
|
+
if (this.connectionTimeoutTimer) {
|
|
376
|
+
clearTimeout(this.connectionTimeoutTimer);
|
|
377
|
+
this.connectionTimeoutTimer = undefined;
|
|
378
|
+
}
|
|
379
|
+
this.setState(ConnectionState.CONNECTED);
|
|
380
|
+
this.reconnectAttempts = 0;
|
|
381
|
+
this.emit('connected');
|
|
382
|
+
this.debug(`[WebSocketClient] WebSocket connected | ` +
|
|
383
|
+
`Session: ${this.sessionId?.substring(0, 8)}... | ` +
|
|
384
|
+
`Checkpoint: ${this.latestCheckpointId?.substring(0, 8) || 'none'}`);
|
|
385
|
+
// Transition to waiting for initial Ready
|
|
386
|
+
this.setReadyState(ReadyState.WAITING_INITIAL_READY, 'Connection opened, awaiting initial Ready');
|
|
387
|
+
this.startReadyTimer(); // Wait for initial Ready
|
|
388
|
+
// Start idle timer immediately upon connection
|
|
389
|
+
this.startIdleTimer();
|
|
390
|
+
// Start heartbeat
|
|
391
|
+
this.startHeartbeat();
|
|
392
|
+
// Process any queued messages (old mechanism, will be replaced by pendingOutbox)
|
|
393
|
+
this.processQueuedMessages();
|
|
394
|
+
this.debug('[WebSocketClient] Connection established, waiting for initial Ready signal');
|
|
395
|
+
resolve();
|
|
396
|
+
});
|
|
397
|
+
ws.on('message', (data) => {
|
|
398
|
+
if (this.connectionId !== currentConnectionId)
|
|
399
|
+
return;
|
|
400
|
+
this.resetIdleTimer();
|
|
401
|
+
this.lastPongReceived = Date.now(); // any message is also a ping
|
|
402
|
+
const message = data.toString();
|
|
403
|
+
// Try to detect Ready messages
|
|
404
|
+
try {
|
|
405
|
+
const parsed = JSON.parse(message);
|
|
406
|
+
if (parsed && parsed.data && parsed.data.tool === 'Ready') {
|
|
407
|
+
// Handle Ready message separately
|
|
408
|
+
this.handleReadyMessage(parsed);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
catch (e) {
|
|
413
|
+
// Not JSON or not a Ready message, continue with normal flow
|
|
414
|
+
}
|
|
415
|
+
// Track that we received a non-Ready message (response)
|
|
416
|
+
if (this.readyState === ReadyState.MESSAGE_SENT) {
|
|
417
|
+
this.receivedResponsesSinceMessage = true;
|
|
418
|
+
}
|
|
419
|
+
// Emit non-Ready messages to AgentAPI
|
|
420
|
+
this.emit('message', message);
|
|
421
|
+
});
|
|
422
|
+
ws.on('close', (code, reason) => {
|
|
423
|
+
if (this.connectionId !== currentConnectionId)
|
|
424
|
+
return;
|
|
425
|
+
// Clear pending connection-timeout timer
|
|
426
|
+
if (this.connectionTimeoutTimer) {
|
|
427
|
+
clearTimeout(this.connectionTimeoutTimer);
|
|
428
|
+
this.connectionTimeoutTimer = undefined;
|
|
429
|
+
}
|
|
430
|
+
this.handleClose(code, reason.toString());
|
|
431
|
+
if (this.state === ConnectionState.CONNECTING) {
|
|
432
|
+
reject(new Error(`Connection closed during setup: ${code} ${reason}`));
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
ws.on('error', (error) => {
|
|
436
|
+
if (this.connectionId !== currentConnectionId)
|
|
437
|
+
return;
|
|
438
|
+
// Clear pending connection-timeout timer
|
|
439
|
+
if (this.connectionTimeoutTimer) {
|
|
440
|
+
clearTimeout(this.connectionTimeoutTimer);
|
|
441
|
+
this.connectionTimeoutTimer = undefined;
|
|
442
|
+
}
|
|
443
|
+
this.debug('WebSocket error:', error);
|
|
444
|
+
this.emit('error', error);
|
|
445
|
+
if (this.state === ConnectionState.CONNECTING) {
|
|
446
|
+
reject(error);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
ws.on('pong', () => {
|
|
450
|
+
if (this.connectionId === currentConnectionId) {
|
|
451
|
+
this.lastPongReceived = Date.now();
|
|
452
|
+
this.debug('Pong received, heartbeat OK');
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
// Connection timeout
|
|
456
|
+
this.connectionTimeoutTimer = setTimeout(() => {
|
|
457
|
+
if (this.connectionId === currentConnectionId && this.state === ConnectionState.CONNECTING) {
|
|
458
|
+
ws.close();
|
|
459
|
+
reject(new Error('Connection timeout'));
|
|
460
|
+
}
|
|
461
|
+
}, WS_CONFIG.CONNECTION_TIMEOUT);
|
|
462
|
+
this.connectionTimeoutTimer.unref?.();
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
this.setState(ConnectionState.FAILED);
|
|
466
|
+
reject(error);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
async waitForConnection(timeout = WS_CONFIG.CONNECTION_TIMEOUT) {
|
|
471
|
+
const startTime = Date.now();
|
|
472
|
+
while (this.state === ConnectionState.CONNECTING || this.state === ConnectionState.RECONNECTING) {
|
|
473
|
+
if (Date.now() - startTime > timeout) {
|
|
474
|
+
throw new Error('Connection timeout while waiting');
|
|
475
|
+
}
|
|
476
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
477
|
+
}
|
|
478
|
+
if (this.state !== ConnectionState.CONNECTED) {
|
|
479
|
+
throw new Error('Failed to establish connection');
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
buildWebSocketUrl() {
|
|
483
|
+
const baseUrl = ServerData.getInstance().getBaseUrl();
|
|
484
|
+
const wsUrl = baseUrl.replace(/^http/, 'ws');
|
|
485
|
+
const trimmedBase = wsUrl.replace(/\/+$/, '');
|
|
486
|
+
const url = new URL(`${trimmedBase}/v2/agentic/ws/connect`);
|
|
487
|
+
// Use current session ID, or generate one if not set
|
|
488
|
+
const currentSessionId = this.sessionId || uuid();
|
|
489
|
+
url.searchParams.set('session_id', currentSessionId);
|
|
490
|
+
if (this.requestId) {
|
|
491
|
+
url.searchParams.set('request_id', this.requestId);
|
|
492
|
+
}
|
|
493
|
+
// Include checkpoint_id if available for recovery
|
|
494
|
+
if (this.latestCheckpointId && this.readyState === ReadyState.CHECKPOINT_RECOVERY) {
|
|
495
|
+
url.searchParams.set('checkpoint_id', this.latestCheckpointId);
|
|
496
|
+
this.debug(`[WebSocketClient] Building URL with checkpoint | ` +
|
|
497
|
+
`Checkpoint: ${this.latestCheckpointId.substring(0, 8)}...`);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
this.debug('[WebSocketClient] Building URL without checkpoint (fresh connection)');
|
|
501
|
+
}
|
|
502
|
+
return url.toString();
|
|
503
|
+
}
|
|
504
|
+
formatMessage(type, data) {
|
|
505
|
+
return `${type} ${JSON.stringify(data)}\n`;
|
|
506
|
+
}
|
|
507
|
+
handleClose(code, reason) {
|
|
508
|
+
this.clearTimers();
|
|
509
|
+
if (this.autoDisconnected) {
|
|
510
|
+
this.debug('WebSocket closed due to idle timeout');
|
|
511
|
+
this.setState(ConnectionState.DISCONNECTED);
|
|
512
|
+
this.emit('disconnected', { reason: 'idle', code });
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
this.debug(`WebSocket closed: ${code} ${reason}`);
|
|
516
|
+
// Determine if we should reconnect
|
|
517
|
+
if (code === 1000 || code === 1001) {
|
|
518
|
+
// Normal closure
|
|
519
|
+
this.setState(ConnectionState.DISCONNECTED);
|
|
520
|
+
this.emit('disconnected', { reason: 'normal', code });
|
|
521
|
+
}
|
|
522
|
+
else if (code === 1008) {
|
|
523
|
+
// Authentication failure - don't reconnect
|
|
524
|
+
this.setState(ConnectionState.FAILED);
|
|
525
|
+
this.emit('error', new Error('Authentication failed'));
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
// Unexpected closure - attempt reconnect
|
|
529
|
+
this.setState(ConnectionState.FAILED);
|
|
530
|
+
this.attemptReconnection().catch((error) => {
|
|
531
|
+
this.debug('Reconnection attempt failed:', error instanceof Error ? error.message : error);
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
async attemptReconnection() {
|
|
536
|
+
if (this.isReconnecting)
|
|
537
|
+
return;
|
|
538
|
+
this.isReconnecting = true;
|
|
539
|
+
this.setState(ConnectionState.RECONNECTING);
|
|
540
|
+
this.reconnectAttempts++;
|
|
541
|
+
const delay = this.calculateBackoffDelay(this.reconnectAttempts);
|
|
542
|
+
this.debug(`Attempting reconnection ${this.reconnectAttempts} in ${delay}ms`);
|
|
543
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
544
|
+
try {
|
|
545
|
+
await this.establishConnection();
|
|
546
|
+
this.isReconnecting = false;
|
|
547
|
+
this.emit('reconnected');
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
this.debug('Reconnection failed:', error instanceof Error ? error.message : error);
|
|
551
|
+
this.isReconnecting = false;
|
|
552
|
+
// Infinite retry - always attempt again
|
|
553
|
+
setTimeout(() => this.attemptReconnection(), 1000);
|
|
554
|
+
}
|
|
555
|
+
}, delay);
|
|
556
|
+
this.reconnectTimer.unref?.();
|
|
557
|
+
}
|
|
558
|
+
calculateBackoffDelay(attempt) {
|
|
559
|
+
const delay = WS_CONFIG.INITIAL_RECONNECT_DELAY *
|
|
560
|
+
Math.pow(WS_CONFIG.RECONNECT_BACKOFF_FACTOR, attempt - 1);
|
|
561
|
+
const jitter = delay * 0.25 * (Math.random() - 0.5);
|
|
562
|
+
return Math.min(delay + jitter, WS_CONFIG.MAX_RECONNECT_DELAY);
|
|
563
|
+
}
|
|
564
|
+
// === Idle Timer Management ===
|
|
565
|
+
startIdleTimer() {
|
|
566
|
+
this.clearIdleTimer();
|
|
567
|
+
this.idleTimer = setTimeout(() => {
|
|
568
|
+
this.debug('Connection idle for 10 minutes, disconnecting...');
|
|
569
|
+
this.autoDisconnected = true;
|
|
570
|
+
// Clean disconnect - ready for next request
|
|
571
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
572
|
+
this.ws.close(1000, 'Idle timeout');
|
|
573
|
+
}
|
|
574
|
+
this.setState(ConnectionState.DISCONNECTED);
|
|
575
|
+
this.emit('disconnected', { reason: 'idle' });
|
|
576
|
+
}, WS_CONFIG.IDLE_TIMEOUT);
|
|
577
|
+
// Let process exit if this is the only pending timer
|
|
578
|
+
this.idleTimer.unref?.();
|
|
579
|
+
}
|
|
580
|
+
clearIdleTimer() {
|
|
581
|
+
if (this.idleTimer) {
|
|
582
|
+
clearTimeout(this.idleTimer);
|
|
583
|
+
this.idleTimer = undefined;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
resetIdleTimer() {
|
|
587
|
+
this.startIdleTimer();
|
|
588
|
+
}
|
|
589
|
+
// === Heartbeat Management ===
|
|
590
|
+
startHeartbeat() {
|
|
591
|
+
if (this.heartbeatInterval) {
|
|
592
|
+
clearInterval(this.heartbeatInterval);
|
|
593
|
+
}
|
|
594
|
+
this.heartbeatInterval = setInterval(() => {
|
|
595
|
+
if (this.state === ConnectionState.CONNECTED && this.ws) {
|
|
596
|
+
const now = Date.now();
|
|
597
|
+
const timeSinceLastPong = now - this.lastPongReceived;
|
|
598
|
+
// If we haven't sent a ping recently, or if it's time to send another one
|
|
599
|
+
if (this.lastPingSent === 0 || (now - this.lastPingSent) >= WS_CONFIG.HEARTBEAT_INTERVAL) {
|
|
600
|
+
try {
|
|
601
|
+
this.ws.ping();
|
|
602
|
+
this.lastPingSent = now;
|
|
603
|
+
this.debug('Ping sent, waiting for pong...');
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
this.debug('Error sending ping:', error);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
// If we sent a ping and enough time has passed without a pong, timeout
|
|
610
|
+
else if (this.lastPingSent > 0 && timeSinceLastPong > WS_CONFIG.HEARTBEAT_TIMEOUT) {
|
|
611
|
+
this.debug('Heartbeat timeout, closing connection');
|
|
612
|
+
this.ws.close();
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}, WS_CONFIG.HEARTBEAT_INTERVAL);
|
|
617
|
+
// Allow process to exit if heartbeat is the last thing running
|
|
618
|
+
this.heartbeatInterval.unref?.();
|
|
619
|
+
}
|
|
620
|
+
stopHeartbeat() {
|
|
621
|
+
if (this.heartbeatInterval) {
|
|
622
|
+
clearInterval(this.heartbeatInterval);
|
|
623
|
+
this.heartbeatInterval = undefined;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
async processQueuedMessages() {
|
|
627
|
+
if (this.isProcessingQueue || this.messageQueue.length === 0)
|
|
628
|
+
return;
|
|
629
|
+
this.isProcessingQueue = true;
|
|
630
|
+
this.debug(`Processing ${this.messageQueue.length} queued messages`);
|
|
631
|
+
try {
|
|
632
|
+
while (this.messageQueue.length > 0 && this.state === ConnectionState.CONNECTED) {
|
|
633
|
+
const queuedMessage = this.messageQueue.shift();
|
|
634
|
+
try {
|
|
635
|
+
const message = this.formatMessage(queuedMessage.type, queuedMessage.data);
|
|
636
|
+
this.ws.send(message);
|
|
637
|
+
this.debug(`Sent queued ${queuedMessage.type} message`);
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
console.error('Error processing queued message:', error);
|
|
641
|
+
if (queuedMessage.retryCount < 3) {
|
|
642
|
+
queuedMessage.retryCount++;
|
|
643
|
+
this.messageQueue.unshift(queuedMessage);
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
finally {
|
|
650
|
+
this.isProcessingQueue = false;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
// === Cleanup ===
|
|
654
|
+
clearTimers() {
|
|
655
|
+
this.clearIdleTimer();
|
|
656
|
+
this.stopHeartbeat();
|
|
657
|
+
this.clearReadyTimer();
|
|
658
|
+
if (this.reconnectTimer) {
|
|
659
|
+
clearTimeout(this.reconnectTimer);
|
|
660
|
+
this.reconnectTimer = undefined;
|
|
661
|
+
}
|
|
662
|
+
if (this.connectionTimeoutTimer) {
|
|
663
|
+
clearTimeout(this.connectionTimeoutTimer);
|
|
664
|
+
this.connectionTimeoutTimer = undefined;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
cleanup() {
|
|
668
|
+
this.debug(`[WebSocketClient] Cleanup called | ` +
|
|
669
|
+
`State: ${this.state} | ` +
|
|
670
|
+
`ReadyState: ${this.readyState} | ` +
|
|
671
|
+
`Queued messages: ${this.messageQueue.length} | ` +
|
|
672
|
+
`Pending outbox: ${this.pendingOutbox.length}`);
|
|
673
|
+
this.clearTimers();
|
|
674
|
+
this.disconnect();
|
|
675
|
+
this.messageQueue = [];
|
|
676
|
+
this.pendingOutbox = [];
|
|
677
|
+
this.isReconnecting = false;
|
|
678
|
+
this.latestCheckpointId = undefined;
|
|
679
|
+
this.lastSentMessage = undefined;
|
|
680
|
+
this.setReadyState(ReadyState.IDLE, 'Cleanup performed');
|
|
681
|
+
this.removeAllListeners();
|
|
682
|
+
this.debug('[WebSocketClient] Cleanup completed');
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
//# sourceMappingURL=websocket.js.map
|