@xalia/agent 0.5.8 → 0.6.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/README.md +23 -8
- package/dist/agent/src/agent/agent.js +173 -96
- package/dist/agent/src/agent/agentUtils.js +82 -53
- package/dist/agent/src/agent/compressingContextManager.js +102 -0
- package/dist/agent/src/agent/context.js +189 -0
- package/dist/agent/src/agent/dummyLLM.js +46 -5
- package/dist/agent/src/agent/iAgentEventHandler.js +2 -0
- package/dist/agent/src/agent/mcpServerManager.js +22 -23
- package/dist/agent/src/agent/nullAgentEventHandler.js +21 -0
- package/dist/agent/src/agent/nullPlatform.js +14 -0
- package/dist/agent/src/agent/openAILLMStreaming.js +12 -7
- package/dist/agent/src/agent/promptProvider.js +63 -0
- package/dist/agent/src/agent/repeatLLM.js +5 -5
- package/dist/agent/src/agent/sudoMcpServerManager.js +11 -9
- package/dist/agent/src/agent/tokenAuth.js +7 -7
- package/dist/agent/src/agent/tools.js +1 -1
- package/dist/agent/src/chat/client/chatClient.js +733 -0
- package/dist/agent/src/chat/client/connection.js +209 -0
- package/dist/agent/src/chat/client/connection.test.js +188 -0
- package/dist/agent/src/chat/client/constants.js +5 -0
- package/dist/agent/src/chat/client/index.js +15 -0
- package/dist/agent/src/chat/client/interfaces.js +2 -0
- package/dist/agent/src/chat/client/responseHandler.js +105 -0
- package/dist/agent/src/chat/client/sessionClient.js +331 -0
- package/dist/agent/src/chat/client/teamManager.js +2 -0
- package/dist/agent/src/chat/{apiKeyManager.js → data/apiKeyManager.js} +4 -0
- package/dist/agent/src/chat/data/dataModels.js +2 -0
- package/dist/agent/src/chat/data/database.js +749 -0
- package/dist/agent/src/chat/data/dbMcpServerConfigs.js +47 -0
- package/dist/agent/src/chat/protocol/connectionMessages.js +5 -0
- package/dist/agent/src/chat/protocol/constants.js +50 -0
- package/dist/agent/src/chat/protocol/errors.js +22 -0
- package/dist/agent/src/chat/protocol/messages.js +110 -0
- package/dist/agent/src/chat/server/chatContextManager.js +405 -0
- package/dist/agent/src/chat/server/connectionManager.js +352 -0
- package/dist/agent/src/chat/server/connectionManager.test.js +159 -0
- package/dist/agent/src/chat/server/conversation.js +198 -0
- package/dist/agent/src/chat/server/errorUtils.js +23 -0
- package/dist/agent/src/chat/server/openSession.js +869 -0
- package/dist/agent/src/chat/server/server.js +177 -0
- package/dist/agent/src/chat/server/sessionFileManager.js +161 -0
- package/dist/agent/src/chat/server/sessionRegistry.js +700 -0
- package/dist/agent/src/chat/server/sessionRegistry.test.js +97 -0
- package/dist/agent/src/chat/server/test-utils/mockFactories.js +307 -0
- package/dist/agent/src/chat/server/tools.js +243 -0
- package/dist/agent/src/chat/utils/agentSessionMap.js +66 -0
- package/dist/agent/src/chat/utils/approvalManager.js +85 -0
- package/dist/agent/src/{utils → chat/utils}/asyncLock.js +3 -3
- package/dist/agent/src/chat/{asyncQueue.js → utils/asyncQueue.js} +12 -2
- package/dist/agent/src/chat/utils/htmlToText.js +84 -0
- package/dist/agent/src/chat/utils/multiAsyncQueue.js +42 -0
- package/dist/agent/src/chat/utils/search.js +145 -0
- package/dist/agent/src/chat/utils/userResolver.js +46 -0
- package/dist/agent/src/chat/{websocket.js → utils/websocket.js} +2 -0
- package/dist/agent/src/test/agent.test.js +332 -0
- package/dist/agent/src/test/approvalManager.test.js +58 -0
- package/dist/agent/src/test/chatContextManager.test.js +392 -0
- package/dist/agent/src/test/clientServerConnection.test.js +158 -0
- package/dist/agent/src/test/compressingContextManager.test.js +65 -0
- package/dist/agent/src/test/context.test.js +83 -0
- package/dist/agent/src/test/conversation.test.js +89 -0
- package/dist/agent/src/test/db.test.js +262 -90
- package/dist/agent/src/test/dbMcpServerConfigs.test.js +72 -0
- package/dist/agent/src/test/dbTestTools.js +99 -0
- package/dist/agent/src/test/imageLoad.test.js +8 -7
- package/dist/agent/src/test/mcpServerManager.test.js +21 -18
- package/dist/agent/src/test/multiAsyncQueue.test.js +101 -0
- package/dist/agent/src/test/openaiStreaming.test.js +12 -11
- package/dist/agent/src/test/prompt.test.js +5 -4
- package/dist/agent/src/test/promptProvider.test.js +28 -0
- package/dist/agent/src/test/responseHandler.test.js +61 -0
- package/dist/agent/src/test/sudoMcpServerManager.test.js +14 -12
- package/dist/agent/src/test/testTools.js +109 -0
- package/dist/agent/src/test/tools.test.js +31 -0
- package/dist/agent/src/tool/agentChat.js +21 -10
- package/dist/agent/src/tool/agentMain.js +1 -1
- package/dist/agent/src/tool/chatMain.js +235 -58
- package/dist/agent/src/tool/commandPrompt.js +15 -9
- package/dist/agent/src/tool/files.js +20 -16
- package/dist/agent/src/tool/nodePlatform.js +47 -3
- package/dist/agent/src/tool/options.js +4 -4
- package/dist/agent/src/tool/prompt.js +19 -13
- package/eslint.config.mjs +14 -1
- package/package.json +14 -6
- package/scripts/chat_server +8 -0
- package/scripts/setup_chat +7 -2
- package/scripts/shutdown_chat_server +3 -0
- package/scripts/test_chat +135 -17
- package/src/agent/agent.ts +270 -135
- package/src/agent/agentUtils.ts +136 -95
- package/src/agent/compressingContextManager.ts +164 -0
- package/src/agent/context.ts +268 -0
- package/src/agent/dummyLLM.ts +76 -8
- package/src/agent/iAgentEventHandler.ts +54 -0
- package/src/agent/iplatform.ts +1 -0
- package/src/agent/mcpServerManager.ts +32 -30
- package/src/agent/nullAgentEventHandler.ts +20 -0
- package/src/agent/nullPlatform.ts +13 -0
- package/src/agent/openAILLMStreaming.ts +12 -6
- package/src/agent/promptProvider.ts +87 -0
- package/src/agent/repeatLLM.ts +5 -5
- package/src/agent/sudoMcpServerManager.ts +13 -11
- package/src/agent/tokenAuth.ts +7 -7
- package/src/agent/tools.ts +3 -1
- package/src/chat/client/chatClient.ts +900 -0
- package/src/chat/client/connection.test.ts +241 -0
- package/src/chat/client/connection.ts +276 -0
- package/src/chat/client/constants.ts +3 -0
- package/src/chat/client/index.ts +18 -0
- package/src/chat/client/interfaces.ts +34 -0
- package/src/chat/client/responseHandler.ts +131 -0
- package/src/chat/client/sessionClient.ts +443 -0
- package/src/chat/client/teamManager.ts +29 -0
- package/src/chat/{apiKeyManager.ts → data/apiKeyManager.ts} +6 -2
- package/src/chat/data/dataModels.ts +85 -0
- package/src/chat/data/database.ts +982 -0
- package/src/chat/data/dbMcpServerConfigs.ts +59 -0
- package/src/chat/protocol/connectionMessages.ts +49 -0
- package/src/chat/protocol/constants.ts +55 -0
- package/src/chat/protocol/errors.ts +16 -0
- package/src/chat/protocol/messages.ts +682 -0
- package/src/chat/server/README.md +127 -0
- package/src/chat/server/chatContextManager.ts +612 -0
- package/src/chat/server/connectionManager.test.ts +266 -0
- package/src/chat/server/connectionManager.ts +541 -0
- package/src/chat/server/conversation.ts +269 -0
- package/src/chat/server/errorUtils.ts +28 -0
- package/src/chat/server/openSession.ts +1332 -0
- package/src/chat/server/server.ts +177 -0
- package/src/chat/server/sessionFileManager.ts +239 -0
- package/src/chat/server/sessionRegistry.test.ts +138 -0
- package/src/chat/server/sessionRegistry.ts +1064 -0
- package/src/chat/server/test-utils/mockFactories.ts +422 -0
- package/src/chat/server/tools.ts +265 -0
- package/src/chat/utils/agentSessionMap.ts +76 -0
- package/src/chat/utils/approvalManager.ts +111 -0
- package/src/{utils → chat/utils}/asyncLock.ts +3 -3
- package/src/chat/{asyncQueue.ts → utils/asyncQueue.ts} +14 -3
- package/src/chat/utils/htmlToText.ts +61 -0
- package/src/chat/utils/multiAsyncQueue.ts +52 -0
- package/src/chat/utils/search.ts +139 -0
- package/src/chat/utils/userResolver.ts +48 -0
- package/src/chat/{websocket.ts → utils/websocket.ts} +2 -0
- package/src/test/agent.test.ts +487 -0
- package/src/test/approvalManager.test.ts +73 -0
- package/src/test/chatContextManager.test.ts +521 -0
- package/src/test/clientServerConnection.test.ts +207 -0
- package/src/test/compressingContextManager.test.ts +82 -0
- package/src/test/context.test.ts +105 -0
- package/src/test/conversation.test.ts +109 -0
- package/src/test/db.test.ts +351 -103
- package/src/test/dbMcpServerConfigs.test.ts +112 -0
- package/src/test/dbTestTools.ts +153 -0
- package/src/test/imageLoad.test.ts +7 -6
- package/src/test/mcpServerManager.test.ts +19 -14
- package/src/test/multiAsyncQueue.test.ts +125 -0
- package/src/test/openaiStreaming.test.ts +11 -10
- package/src/test/prompt.test.ts +4 -3
- package/src/test/promptProvider.test.ts +33 -0
- package/src/test/responseHandler.test.ts +78 -0
- package/src/test/sudoMcpServerManager.test.ts +22 -15
- package/src/test/testTools.ts +146 -0
- package/src/test/tools.test.ts +39 -0
- package/src/tool/agentChat.ts +26 -12
- package/src/tool/agentMain.ts +1 -1
- package/src/tool/chatMain.ts +283 -100
- package/src/tool/commandPrompt.ts +25 -9
- package/src/tool/files.ts +25 -19
- package/src/tool/nodePlatform.ts +52 -3
- package/src/tool/options.ts +4 -2
- package/src/tool/prompt.ts +22 -15
- package/test_data/dummyllm_script_crash.json +32 -0
- package/test_data/frog.png.b64 +1 -0
- package/vitest.config.ts +39 -0
- package/dist/agent/src/chat/client.js +0 -310
- package/dist/agent/src/chat/conversationManager.js +0 -502
- package/dist/agent/src/chat/db.js +0 -218
- package/dist/agent/src/chat/messages.js +0 -29
- package/dist/agent/src/chat/server.js +0 -158
- package/src/chat/client.ts +0 -445
- package/src/chat/conversationManager.ts +0 -730
- package/src/chat/db.ts +0 -304
- package/src/chat/messages.ts +0 -266
- package/src/chat/server.ts +0 -177
- /package/{frog.png → test_data/frog.png} +0 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import * as ws from "ws";
|
|
2
|
+
import { v4 as uuidv4 } from "uuid";
|
|
3
|
+
import { getLogger } from "@xalia/xmcp/sdk";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ServerConnectionReady,
|
|
7
|
+
ServerConnectionError,
|
|
8
|
+
ServerConnectionPing,
|
|
9
|
+
ClientConnectionMessage,
|
|
10
|
+
ServerConnectionMessage,
|
|
11
|
+
} from "../protocol/connectionMessages";
|
|
12
|
+
import { ChatErrorMessage, ChatFatalError } from "../protocol/errors";
|
|
13
|
+
import { ApiKeyManager } from "../data/apiKeyManager";
|
|
14
|
+
|
|
15
|
+
const logger = getLogger();
|
|
16
|
+
|
|
17
|
+
const HEARTBEAT_INTERVAL_MS: number = parseInt(
|
|
18
|
+
process.env["HEARTBEAT_INTERVAL_MS"] || String(30 * 1000)
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Interface for sending messages to clients.
|
|
23
|
+
*/
|
|
24
|
+
export interface IUserConnectionManager<ServerMsgT> {
|
|
25
|
+
/**
|
|
26
|
+
* Send message to all active connections of specific users.
|
|
27
|
+
* Handles user-to-connection routing internally.
|
|
28
|
+
*/
|
|
29
|
+
sendToUsers(userIds: Set<string>, message: ServerMsgT): void;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Send message to a specific connection.
|
|
33
|
+
*/
|
|
34
|
+
sendToConnection(connectionId: string, message: ServerMsgT): void;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get cached api key for a user. This key will not be available
|
|
38
|
+
* after the user disconnects.
|
|
39
|
+
*/
|
|
40
|
+
getLiveUserApiKey(userId: string): string | undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Interface for processing messages from the client.
|
|
45
|
+
*/
|
|
46
|
+
export interface IMessageProcessor<ClientMsgT> {
|
|
47
|
+
processMessage(
|
|
48
|
+
connectionId: string,
|
|
49
|
+
userId: string,
|
|
50
|
+
message: ClientMsgT
|
|
51
|
+
): Promise<void>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Handle user disconnect - clean up from all sessions.
|
|
55
|
+
* Called when a connection is closed to ensure proper cleanup.
|
|
56
|
+
*/
|
|
57
|
+
handleUserDisconnect(userId: string): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
class Connection {
|
|
61
|
+
public ws: ws.WebSocket;
|
|
62
|
+
public userId: string;
|
|
63
|
+
public apiKey: string;
|
|
64
|
+
public heartbeat: NodeJS.Timeout;
|
|
65
|
+
public lastMessageTime: number;
|
|
66
|
+
|
|
67
|
+
constructor(
|
|
68
|
+
ws: ws.WebSocket,
|
|
69
|
+
userId: string,
|
|
70
|
+
apiKey: string,
|
|
71
|
+
heartbeat: NodeJS.Timeout
|
|
72
|
+
) {
|
|
73
|
+
this.ws = ws;
|
|
74
|
+
this.userId = userId;
|
|
75
|
+
this.apiKey = apiKey;
|
|
76
|
+
this.heartbeat = heartbeat;
|
|
77
|
+
this.lastMessageTime = Date.now();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Manages WebSocket connections and handles protocol-level message routing.
|
|
83
|
+
*
|
|
84
|
+
* - Connection handshake
|
|
85
|
+
* - Connection lifecycle management
|
|
86
|
+
* - Message routing between connections and sessions
|
|
87
|
+
* - Connection-to-user
|
|
88
|
+
*/
|
|
89
|
+
export class ConnectionManager<ClientMsgT, ServerMsgT>
|
|
90
|
+
implements IUserConnectionManager<ServerMsgT>
|
|
91
|
+
{
|
|
92
|
+
// Connections by id
|
|
93
|
+
private connections: Map<string, Connection> = new Map();
|
|
94
|
+
// userId -> connectionIds
|
|
95
|
+
private userToConnections: Map<string, Set<string>> = new Map();
|
|
96
|
+
|
|
97
|
+
// this is a singleton object and can only be initialized by .init method
|
|
98
|
+
private constructor(
|
|
99
|
+
private apiKeyManager: ApiKeyManager,
|
|
100
|
+
private sessionMessageProcessor: IMessageProcessor<ClientMsgT>
|
|
101
|
+
) {}
|
|
102
|
+
|
|
103
|
+
static init<ClientMsgT, ServerMsgT>(
|
|
104
|
+
apiKeyManager: ApiKeyManager,
|
|
105
|
+
createMsgProcessor: (
|
|
106
|
+
cm: IUserConnectionManager<ServerMsgT>
|
|
107
|
+
) => IMessageProcessor<ClientMsgT>
|
|
108
|
+
): ConnectionManager<ClientMsgT, ServerMsgT> {
|
|
109
|
+
const connManager = new ConnectionManager(
|
|
110
|
+
apiKeyManager,
|
|
111
|
+
{} as IMessageProcessor<ClientMsgT>
|
|
112
|
+
);
|
|
113
|
+
connManager.sessionMessageProcessor = createMsgProcessor(connManager);
|
|
114
|
+
return connManager;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Handle new WebSocket connection with handshake protocol
|
|
119
|
+
* TODO: proper error code returning
|
|
120
|
+
*/
|
|
121
|
+
async handleConnection(
|
|
122
|
+
webSocket: ws.WebSocket,
|
|
123
|
+
apiKey: string,
|
|
124
|
+
req?: { headers: { [key: string]: string | string[] | undefined } }
|
|
125
|
+
): Promise<void> {
|
|
126
|
+
const connectionId = uuidv4();
|
|
127
|
+
logger.info(`[ConnectionManager] New connection: ${connectionId}`);
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
// Verify API key
|
|
131
|
+
const userData = await this.apiKeyManager.verifyApiKey(apiKey);
|
|
132
|
+
if (!userData) {
|
|
133
|
+
logger.error(`[ConnectionManager] Invalid API key: ${apiKey}`);
|
|
134
|
+
throw new ChatFatalError("invalid api key");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Heartbeat interval
|
|
138
|
+
const heartbeat = setInterval(
|
|
139
|
+
this.onHeartbeat.bind(this, connectionId),
|
|
140
|
+
HEARTBEAT_INTERVAL_MS
|
|
141
|
+
);
|
|
142
|
+
// Register connection
|
|
143
|
+
this.connections.set(
|
|
144
|
+
connectionId,
|
|
145
|
+
new Connection(webSocket, userData.uuid, apiKey, heartbeat)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Add to user connections
|
|
149
|
+
if (!this.userToConnections.has(userData.uuid)) {
|
|
150
|
+
this.userToConnections.set(userData.uuid, new Set());
|
|
151
|
+
}
|
|
152
|
+
const userConnections = this.userToConnections.get(userData.uuid);
|
|
153
|
+
if (userConnections) {
|
|
154
|
+
userConnections.add(connectionId);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Setup connection handlers
|
|
158
|
+
this.setupConnectionHandlers(connectionId, webSocket);
|
|
159
|
+
|
|
160
|
+
// Send connection_ready after WebSocket is fully open
|
|
161
|
+
const clientVersion =
|
|
162
|
+
(req?.headers["x-client-version"] as string) || "unknown";
|
|
163
|
+
|
|
164
|
+
const response: ServerConnectionReady = {
|
|
165
|
+
t: "ready",
|
|
166
|
+
c_id: connectionId,
|
|
167
|
+
user_uuid: userData.uuid,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Check if connection still exists before sending
|
|
171
|
+
if (this.connections.has(connectionId)) {
|
|
172
|
+
this.sendConnectionReadyMessage(connectionId, response);
|
|
173
|
+
} else {
|
|
174
|
+
logger.warn(
|
|
175
|
+
`[ConnectionManager] Connection ${connectionId} was closed` +
|
|
176
|
+
` before ready message could be sent`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
logger.info(
|
|
181
|
+
`[ConnectionManager] Connection ${connectionId} registered ` +
|
|
182
|
+
`for user ${userData.uuid}, version: ${clientVersion}`
|
|
183
|
+
);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
const errorMessage =
|
|
186
|
+
error instanceof Error ? error.message : String(error);
|
|
187
|
+
logger.error(
|
|
188
|
+
`[ConnectionManager] Failed to setup connection ${connectionId}:`,
|
|
189
|
+
errorMessage
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Send error to client if possible
|
|
193
|
+
this.sendErrorDirect(webSocket, connectionId, errorMessage);
|
|
194
|
+
|
|
195
|
+
// Force cleanup of any partial state
|
|
196
|
+
this.forceCleanupConnection(connectionId);
|
|
197
|
+
|
|
198
|
+
// Close WebSocket - this might trigger handlers if they were attached,
|
|
199
|
+
// but we've already cleaned up state above as a safeguard
|
|
200
|
+
try {
|
|
201
|
+
webSocket.close(4000, "Connection setup failed");
|
|
202
|
+
} catch (closeError) {
|
|
203
|
+
logger.error(
|
|
204
|
+
`[ConnectionManager] Error closing WebSocket ${connectionId}:`,
|
|
205
|
+
closeError
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Send connection_ready message with unified error handling
|
|
213
|
+
*/
|
|
214
|
+
private sendConnectionReadyMessage(
|
|
215
|
+
connectionId: string,
|
|
216
|
+
response: ServerConnectionReady
|
|
217
|
+
): void {
|
|
218
|
+
try {
|
|
219
|
+
this.sendConnectionMessage(connectionId, response);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
logger.error(
|
|
222
|
+
`[ConnectionManager] Failed connection_ready to ${connectionId}:`,
|
|
223
|
+
error
|
|
224
|
+
);
|
|
225
|
+
this.forceCleanupConnection(connectionId);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Send error message to connection directly.
|
|
231
|
+
* This is used when the connection is possibly not yet registered.
|
|
232
|
+
*/
|
|
233
|
+
private sendErrorDirect(
|
|
234
|
+
ws: ws.WebSocket,
|
|
235
|
+
connectionId: string,
|
|
236
|
+
errorMessage: string
|
|
237
|
+
): void {
|
|
238
|
+
const errorMsg: ServerConnectionError = {
|
|
239
|
+
t: "error",
|
|
240
|
+
e: errorMessage,
|
|
241
|
+
};
|
|
242
|
+
try {
|
|
243
|
+
if (ws.readyState === ws.OPEN) {
|
|
244
|
+
ws.send(JSON.stringify(errorMsg));
|
|
245
|
+
} else {
|
|
246
|
+
logger.error(
|
|
247
|
+
`[ConnectionManager] Conn not open sending error ${connectionId}`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
} catch (error) {
|
|
251
|
+
logger.error(
|
|
252
|
+
`[ConnectionManager] Failed to send error to connection` +
|
|
253
|
+
` ${connectionId}:`,
|
|
254
|
+
error
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Setup message and lifecycle handlers for a connection
|
|
261
|
+
*/
|
|
262
|
+
private setupConnectionHandlers(
|
|
263
|
+
connectionId: string,
|
|
264
|
+
webSocket: ws.WebSocket
|
|
265
|
+
): void {
|
|
266
|
+
// TODO: keep the Connection object in the closure here instead of always
|
|
267
|
+
// doing many (expensive) lookups.
|
|
268
|
+
|
|
269
|
+
// set message handler for incoming messages
|
|
270
|
+
webSocket.on("message", (data: ws.RawData) => {
|
|
271
|
+
void (async () => {
|
|
272
|
+
try {
|
|
273
|
+
const msgStr = (data as Buffer).toString("utf8");
|
|
274
|
+
logger.info(
|
|
275
|
+
`[ConnectionManager] Message from ${connectionId}: ${msgStr}`
|
|
276
|
+
);
|
|
277
|
+
const message = JSON.parse(
|
|
278
|
+
msgStr
|
|
279
|
+
) as ClientConnectionMessage<ClientMsgT>;
|
|
280
|
+
await this.routeMessage(connectionId, message);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
logger.error(`[ConnectionManager] Error processing message:`, error);
|
|
283
|
+
this.handleError(connectionId, error);
|
|
284
|
+
}
|
|
285
|
+
})();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
webSocket.on("close", () => {
|
|
289
|
+
logger.debug(`[ConnectionManager] Connection ${connectionId} closed`);
|
|
290
|
+
this.handleConnectionClose(connectionId);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
webSocket.on("error", (error) => {
|
|
294
|
+
logger.error(
|
|
295
|
+
`[ConnectionManager] WebSocket error on ${connectionId}:`,
|
|
296
|
+
error
|
|
297
|
+
);
|
|
298
|
+
this.handleConnectionClose(connectionId);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Route incoming message to appropriate handler based on message type
|
|
304
|
+
*/
|
|
305
|
+
private async routeMessage(
|
|
306
|
+
connectionId: string,
|
|
307
|
+
msg: ClientConnectionMessage<ClientMsgT>
|
|
308
|
+
): Promise<void> {
|
|
309
|
+
const conn = this.connections.get(connectionId);
|
|
310
|
+
if (!conn) {
|
|
311
|
+
throw new ChatErrorMessage(
|
|
312
|
+
`Connection ${connectionId} not found for message routing`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
conn.lastMessageTime = Date.now();
|
|
317
|
+
|
|
318
|
+
// Handle Connection-level messages
|
|
319
|
+
{
|
|
320
|
+
switch (msg.t) {
|
|
321
|
+
case "data":
|
|
322
|
+
await this.sessionMessageProcessor.processMessage(
|
|
323
|
+
connectionId,
|
|
324
|
+
conn.userId,
|
|
325
|
+
msg.d
|
|
326
|
+
);
|
|
327
|
+
break;
|
|
328
|
+
case "pong":
|
|
329
|
+
// The only operation is to update the `lastMessageTime`, done above
|
|
330
|
+
logger.debug(
|
|
331
|
+
`[ConnectionManager.routeMessage] pong from ${connectionId}`
|
|
332
|
+
);
|
|
333
|
+
break;
|
|
334
|
+
default: {
|
|
335
|
+
const _: never = msg;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private onHeartbeat(connectionId: string): void {
|
|
343
|
+
// TODO: move this and other handling to the Connection class so we can
|
|
344
|
+
// avoid all the per-event lookups.
|
|
345
|
+
|
|
346
|
+
const conn = this.connections.get(connectionId);
|
|
347
|
+
if (!conn) {
|
|
348
|
+
// TODO: We intentionally don't try to clean up here, to force
|
|
349
|
+
// correctness fo the real cleanup code.
|
|
350
|
+
logger.error(
|
|
351
|
+
`[onHeartbeat] unexpected callback for connId ${connectionId}`
|
|
352
|
+
);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const now = Date.now();
|
|
357
|
+
const timeSinceLast = now - conn.lastMessageTime;
|
|
358
|
+
logger.debug(
|
|
359
|
+
`[onHeartbeat] conn ${connectionId} since ${String(timeSinceLast)}`
|
|
360
|
+
);
|
|
361
|
+
if (timeSinceLast > 2 * HEARTBEAT_INTERVAL_MS) {
|
|
362
|
+
this.forceCleanupConnection(connectionId);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const msg: ServerConnectionPing = { t: "ping" };
|
|
367
|
+
this.sendConnectionMessage(connectionId, msg);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Force cleanup of connection state - used for error recovery.
|
|
372
|
+
* This method is safe to call multiple times and handles partial state.
|
|
373
|
+
*/
|
|
374
|
+
private forceCleanupConnection(connectionId: string): void {
|
|
375
|
+
const conn = this.connections.get(connectionId);
|
|
376
|
+
if (conn) {
|
|
377
|
+
// Cancel the heartbeat interval
|
|
378
|
+
clearInterval(conn.heartbeat);
|
|
379
|
+
|
|
380
|
+
// Remove from user connections
|
|
381
|
+
const userConnections = this.userToConnections.get(conn.userId);
|
|
382
|
+
if (userConnections) {
|
|
383
|
+
userConnections.delete(connectionId);
|
|
384
|
+
if (userConnections.size === 0) {
|
|
385
|
+
this.userToConnections.delete(conn.userId);
|
|
386
|
+
this.sessionMessageProcessor.handleUserDisconnect(conn.userId);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
this.connections.delete(connectionId);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
logger.debug(
|
|
394
|
+
`[ConnectionManager] Force cleaned up connection ${connectionId}`
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Handle connection close - cleanup all state.
|
|
400
|
+
* This is the normal cleanup path triggered by WebSocket events.
|
|
401
|
+
*/
|
|
402
|
+
private handleConnectionClose(connectionId: string): void {
|
|
403
|
+
this.forceCleanupConnection(connectionId);
|
|
404
|
+
logger.info(`[ConnectionManager] Connection ${connectionId} closed`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private sendConnectionMessage(
|
|
408
|
+
connectionId: string,
|
|
409
|
+
message: ServerConnectionMessage<ServerMsgT>
|
|
410
|
+
): void {
|
|
411
|
+
const conn = this.connections.get(connectionId);
|
|
412
|
+
if (!conn) {
|
|
413
|
+
logger.warn(
|
|
414
|
+
`[ConnectionManager] Cannot send to connection ${connectionId} - ` +
|
|
415
|
+
`not found`
|
|
416
|
+
);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const webSocket = conn.ws;
|
|
421
|
+
if (webSocket.readyState !== 1) {
|
|
422
|
+
// WebSocket.OPEN = 1
|
|
423
|
+
logger.warn(
|
|
424
|
+
`[ConnectionManager] Cannot send to connection ${connectionId} - ` +
|
|
425
|
+
`not open`
|
|
426
|
+
);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const msgString = JSON.stringify(message);
|
|
431
|
+
logger.debug(
|
|
432
|
+
`[ConnectionManager] Sending to ${connectionId}: ${msgString}`
|
|
433
|
+
);
|
|
434
|
+
webSocket.send(msgString);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Send message to specific connection.
|
|
439
|
+
*/
|
|
440
|
+
public sendToConnection(connectionId: string, message: ServerMsgT): void {
|
|
441
|
+
this.sendConnectionMessage(connectionId, { t: "data", d: message });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Send error message to connection
|
|
446
|
+
*/
|
|
447
|
+
private sendConnectionError(
|
|
448
|
+
connectionId: string,
|
|
449
|
+
errorMessage: string
|
|
450
|
+
): void {
|
|
451
|
+
const errorMsg: ServerConnectionError = {
|
|
452
|
+
t: "error",
|
|
453
|
+
e: errorMessage,
|
|
454
|
+
};
|
|
455
|
+
this.sendConnectionMessage(connectionId, errorMsg);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Handle errors during message processing
|
|
460
|
+
*/
|
|
461
|
+
private handleError(connectionId: string, error: unknown): void {
|
|
462
|
+
let message: string;
|
|
463
|
+
let shouldClose = false;
|
|
464
|
+
|
|
465
|
+
if (typeof error === "string") {
|
|
466
|
+
message = error;
|
|
467
|
+
} else if (error instanceof ChatFatalError) {
|
|
468
|
+
message = error.message;
|
|
469
|
+
shouldClose = true;
|
|
470
|
+
} else if (error instanceof ChatErrorMessage) {
|
|
471
|
+
message = error.message;
|
|
472
|
+
} else if (error instanceof Error) {
|
|
473
|
+
message = "Internal server error: " + error.message;
|
|
474
|
+
} else {
|
|
475
|
+
message = "Unknown error occurred";
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
logger.warn(
|
|
479
|
+
`[ConnectionManager] Error on connection ${connectionId}: ${message}`
|
|
480
|
+
);
|
|
481
|
+
this.sendConnectionError(connectionId, message);
|
|
482
|
+
|
|
483
|
+
if (shouldClose) {
|
|
484
|
+
const conn = this.connections.get(connectionId);
|
|
485
|
+
logger.info(
|
|
486
|
+
`[ConnectionManager] Closing connection ${connectionId} ` +
|
|
487
|
+
`due to error: ${message}`
|
|
488
|
+
);
|
|
489
|
+
if (conn) {
|
|
490
|
+
conn.ws.close(4000, message);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Implementation of IUserConnectionManager interface.
|
|
497
|
+
* Send message to all active connections of specific users.
|
|
498
|
+
*/
|
|
499
|
+
public sendToUsers(userIds: Set<string>, message: ServerMsgT): void {
|
|
500
|
+
for (const userId of userIds.values()) {
|
|
501
|
+
const connectionIds = this.getUserConnections(userId);
|
|
502
|
+
for (const connectionId of connectionIds) {
|
|
503
|
+
logger.debug(
|
|
504
|
+
`[ConnectionManager] sending to user ${userId} ` +
|
|
505
|
+
`connection ${connectionId}`
|
|
506
|
+
);
|
|
507
|
+
this.sendToConnection(connectionId, message);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Get all active connection IDs for a user.
|
|
514
|
+
* Helper method for sendToUsers implementation.
|
|
515
|
+
*/
|
|
516
|
+
private getUserConnections(userId: string): string[] {
|
|
517
|
+
const connections = this.userToConnections.get(userId);
|
|
518
|
+
return connections ? Array.from(connections) : [];
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Implementation of IUserConnectionManager interface.
|
|
523
|
+
* Get cached api key for a user. This key will not be available
|
|
524
|
+
* after the user disconnects.
|
|
525
|
+
*/
|
|
526
|
+
public getLiveUserApiKey(userId: string): string | undefined {
|
|
527
|
+
const connectionIds = this.getUserConnections(userId);
|
|
528
|
+
|
|
529
|
+
// Return API key from the first active connection
|
|
530
|
+
// (all connections for a user should have the same API key)
|
|
531
|
+
for (const connectionId of connectionIds) {
|
|
532
|
+
const conn = this.connections.get(connectionId);
|
|
533
|
+
if (conn) {
|
|
534
|
+
return conn.apiKey;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// No active connections for this user
|
|
539
|
+
return undefined;
|
|
540
|
+
}
|
|
541
|
+
}
|