@gricha/perry 0.2.6 → 0.3.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/dist/agent/router.js +127 -0
- package/dist/agent/run.js +157 -99
- package/dist/agent/static.js +32 -0
- package/dist/agent/web/assets/index-CYo-1I5o.css +1 -0
- package/dist/agent/web/assets/index-CZjSxNrg.js +104 -0
- package/dist/agent/web/index.html +2 -2
- package/dist/chat/base-claude-session.js +48 -2
- package/dist/chat/opencode-server.js +241 -24
- package/dist/chat/session-monitor.js +186 -0
- package/dist/chat/session-utils.js +155 -0
- package/dist/client/api.js +19 -0
- package/dist/perry-worker +0 -0
- package/dist/session-manager/adapters/claude.js +256 -0
- package/dist/session-manager/adapters/index.js +2 -0
- package/dist/session-manager/adapters/opencode.js +317 -0
- package/dist/session-manager/bun-handler.js +175 -0
- package/dist/session-manager/index.js +3 -0
- package/dist/session-manager/manager.js +302 -0
- package/dist/session-manager/ring-buffer.js +66 -0
- package/dist/session-manager/types.js +1 -0
- package/dist/session-manager/websocket.js +153 -0
- package/dist/shared/base-websocket.js +39 -7
- package/dist/tailscale/index.js +20 -6
- package/dist/terminal/bun-handler.js +151 -0
- package/package.json +3 -3
- package/dist/agent/web/assets/index-BwItLEFi.css +0 -1
- package/dist/agent/web/assets/index-DhU_amC3.js +0 -104
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { execInContainer } from '../../docker';
|
|
2
|
+
const MESSAGE_TIMEOUT_MS = 30000;
|
|
3
|
+
const SSE_TIMEOUT_MS = 120000;
|
|
4
|
+
const serverPorts = new Map();
|
|
5
|
+
const serverStarting = new Map();
|
|
6
|
+
async function findAvailablePort(containerName) {
|
|
7
|
+
const script = `import socket; s=socket.socket(); s.bind(('', 0)); print(s.getsockname()[1]); s.close()`;
|
|
8
|
+
const result = await execInContainer(containerName, ['python3', '-c', script], {
|
|
9
|
+
user: 'workspace',
|
|
10
|
+
});
|
|
11
|
+
return parseInt(result.stdout.trim(), 10);
|
|
12
|
+
}
|
|
13
|
+
async function isServerRunning(containerName, port) {
|
|
14
|
+
try {
|
|
15
|
+
const result = await execInContainer(containerName, ['curl', '-s', '-o', '/dev/null', '-w', '%{http_code}', `http://localhost:${port}/session`], { user: 'workspace' });
|
|
16
|
+
return result.stdout.trim() === '200';
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function getServerLogs(containerName) {
|
|
23
|
+
try {
|
|
24
|
+
const result = await execInContainer(containerName, ['tail', '-20', '/tmp/opencode-server.log'], {
|
|
25
|
+
user: 'workspace',
|
|
26
|
+
});
|
|
27
|
+
return result.stdout;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return '(no logs available)';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function startServer(containerName) {
|
|
34
|
+
const existing = serverPorts.get(containerName);
|
|
35
|
+
if (existing && (await isServerRunning(containerName, existing))) {
|
|
36
|
+
return existing;
|
|
37
|
+
}
|
|
38
|
+
const starting = serverStarting.get(containerName);
|
|
39
|
+
if (starting) {
|
|
40
|
+
return starting;
|
|
41
|
+
}
|
|
42
|
+
const startPromise = (async () => {
|
|
43
|
+
const port = await findAvailablePort(containerName);
|
|
44
|
+
console.log(`[opencode] Starting server on port ${port} in ${containerName}`);
|
|
45
|
+
await execInContainer(containerName, [
|
|
46
|
+
'sh',
|
|
47
|
+
'-c',
|
|
48
|
+
`nohup opencode serve --port ${port} --hostname 127.0.0.1 > /tmp/opencode-server.log 2>&1 &`,
|
|
49
|
+
], { user: 'workspace' });
|
|
50
|
+
for (let i = 0; i < 30; i++) {
|
|
51
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
52
|
+
if (await isServerRunning(containerName, port)) {
|
|
53
|
+
console.log(`[opencode] Server ready on port ${port}`);
|
|
54
|
+
serverPorts.set(containerName, port);
|
|
55
|
+
serverStarting.delete(containerName);
|
|
56
|
+
return port;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
serverStarting.delete(containerName);
|
|
60
|
+
const logs = await getServerLogs(containerName);
|
|
61
|
+
throw new Error(`Failed to start OpenCode server. Logs:\n${logs}`);
|
|
62
|
+
})();
|
|
63
|
+
serverStarting.set(containerName, startPromise);
|
|
64
|
+
return startPromise;
|
|
65
|
+
}
|
|
66
|
+
export class OpenCodeAdapter {
|
|
67
|
+
agentType = 'opencode';
|
|
68
|
+
containerName;
|
|
69
|
+
agentSessionId;
|
|
70
|
+
model;
|
|
71
|
+
status = 'idle';
|
|
72
|
+
port;
|
|
73
|
+
sseProcess = null;
|
|
74
|
+
messageCallback;
|
|
75
|
+
statusCallback;
|
|
76
|
+
errorCallback;
|
|
77
|
+
onMessage(callback) {
|
|
78
|
+
this.messageCallback = callback;
|
|
79
|
+
}
|
|
80
|
+
onStatusChange(callback) {
|
|
81
|
+
this.statusCallback = callback;
|
|
82
|
+
}
|
|
83
|
+
onError(callback) {
|
|
84
|
+
this.errorCallback = callback;
|
|
85
|
+
}
|
|
86
|
+
async start(options) {
|
|
87
|
+
if (options.isHost) {
|
|
88
|
+
throw new Error('OpenCode adapter does not support host mode');
|
|
89
|
+
}
|
|
90
|
+
this.containerName = options.containerName;
|
|
91
|
+
this.agentSessionId = options.agentSessionId;
|
|
92
|
+
this.model = options.model;
|
|
93
|
+
try {
|
|
94
|
+
this.port = await startServer(this.containerName);
|
|
95
|
+
this.setStatus('idle');
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
this.emitError(err);
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async sendMessage(message) {
|
|
103
|
+
if (!this.containerName || !this.port) {
|
|
104
|
+
const err = new Error('Adapter not started');
|
|
105
|
+
this.emitError(err);
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
if (this.status === 'running') {
|
|
109
|
+
const err = new Error('Session is already processing a message');
|
|
110
|
+
this.emitError(err);
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
const baseUrl = `http://localhost:${this.port}`;
|
|
114
|
+
try {
|
|
115
|
+
if (!this.agentSessionId) {
|
|
116
|
+
this.agentSessionId = await this.createSession(baseUrl);
|
|
117
|
+
this.emit({ type: 'system', content: `Session: ${this.agentSessionId.slice(0, 8)}...` });
|
|
118
|
+
}
|
|
119
|
+
this.setStatus('running');
|
|
120
|
+
this.emit({ type: 'system', content: 'Processing...' });
|
|
121
|
+
await this.sendAndStream(baseUrl, message);
|
|
122
|
+
this.setStatus('idle');
|
|
123
|
+
this.emit({ type: 'done', content: 'Response complete' });
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
this.cleanup();
|
|
127
|
+
this.setStatus('error');
|
|
128
|
+
this.emitError(err);
|
|
129
|
+
this.emit({ type: 'error', content: err.message });
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async createSession(baseUrl) {
|
|
134
|
+
const payload = this.model ? JSON.stringify({ model: this.model }) : '{}';
|
|
135
|
+
const result = await execInContainer(this.containerName, [
|
|
136
|
+
'curl',
|
|
137
|
+
'-s',
|
|
138
|
+
'-f',
|
|
139
|
+
'--max-time',
|
|
140
|
+
String(MESSAGE_TIMEOUT_MS / 1000),
|
|
141
|
+
'-X',
|
|
142
|
+
'POST',
|
|
143
|
+
`${baseUrl}/session`,
|
|
144
|
+
'-H',
|
|
145
|
+
'Content-Type: application/json',
|
|
146
|
+
'-d',
|
|
147
|
+
payload,
|
|
148
|
+
], { user: 'workspace' });
|
|
149
|
+
if (result.exitCode !== 0) {
|
|
150
|
+
throw new Error(`Failed to create session: ${result.stderr || 'Unknown error'}`);
|
|
151
|
+
}
|
|
152
|
+
const session = JSON.parse(result.stdout);
|
|
153
|
+
return session.id;
|
|
154
|
+
}
|
|
155
|
+
async sendAndStream(baseUrl, message) {
|
|
156
|
+
const sseReady = this.startSSEStream();
|
|
157
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
158
|
+
const payload = JSON.stringify({ parts: [{ type: 'text', text: message }] });
|
|
159
|
+
const result = await execInContainer(this.containerName, [
|
|
160
|
+
'curl',
|
|
161
|
+
'-s',
|
|
162
|
+
'-f',
|
|
163
|
+
'--max-time',
|
|
164
|
+
String(MESSAGE_TIMEOUT_MS / 1000),
|
|
165
|
+
'-X',
|
|
166
|
+
'POST',
|
|
167
|
+
`${baseUrl}/session/${this.agentSessionId}/message`,
|
|
168
|
+
'-H',
|
|
169
|
+
'Content-Type: application/json',
|
|
170
|
+
'-d',
|
|
171
|
+
payload,
|
|
172
|
+
], { user: 'workspace' });
|
|
173
|
+
if (result.exitCode !== 0) {
|
|
174
|
+
throw new Error(`Failed to send message: ${result.stderr || 'Connection failed'}`);
|
|
175
|
+
}
|
|
176
|
+
await sseReady;
|
|
177
|
+
}
|
|
178
|
+
startSSEStream() {
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
const seenTools = new Set();
|
|
181
|
+
let resolved = false;
|
|
182
|
+
let receivedIdle = false;
|
|
183
|
+
const proc = Bun.spawn([
|
|
184
|
+
'docker',
|
|
185
|
+
'exec',
|
|
186
|
+
'-i',
|
|
187
|
+
this.containerName,
|
|
188
|
+
'curl',
|
|
189
|
+
'-s',
|
|
190
|
+
'-N',
|
|
191
|
+
'--max-time',
|
|
192
|
+
String(SSE_TIMEOUT_MS / 1000),
|
|
193
|
+
`http://localhost:${this.port}/event`,
|
|
194
|
+
], { stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' });
|
|
195
|
+
this.sseProcess = proc;
|
|
196
|
+
const decoder = new TextDecoder();
|
|
197
|
+
let buffer = '';
|
|
198
|
+
const finish = () => {
|
|
199
|
+
if (!resolved) {
|
|
200
|
+
resolved = true;
|
|
201
|
+
resolve();
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const timeout = setTimeout(() => {
|
|
205
|
+
proc.kill();
|
|
206
|
+
if (!resolved) {
|
|
207
|
+
resolved = true;
|
|
208
|
+
reject(new Error('SSE stream timeout'));
|
|
209
|
+
}
|
|
210
|
+
}, SSE_TIMEOUT_MS);
|
|
211
|
+
(async () => {
|
|
212
|
+
if (!proc.stdout) {
|
|
213
|
+
clearTimeout(timeout);
|
|
214
|
+
reject(new Error('Failed to start SSE stream'));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
for await (const chunk of proc.stdout) {
|
|
219
|
+
buffer += decoder.decode(chunk);
|
|
220
|
+
const lines = buffer.split('\n');
|
|
221
|
+
buffer = lines.pop() || '';
|
|
222
|
+
for (const line of lines) {
|
|
223
|
+
if (!line.startsWith('data: '))
|
|
224
|
+
continue;
|
|
225
|
+
const data = line.slice(6).trim();
|
|
226
|
+
if (!data)
|
|
227
|
+
continue;
|
|
228
|
+
try {
|
|
229
|
+
const event = JSON.parse(data);
|
|
230
|
+
if (event.type === 'session.idle') {
|
|
231
|
+
receivedIdle = true;
|
|
232
|
+
clearTimeout(timeout);
|
|
233
|
+
proc.kill();
|
|
234
|
+
finish();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (event.type === 'message.part.updated' && event.properties.part) {
|
|
238
|
+
const part = event.properties.part;
|
|
239
|
+
if (part.type === 'text' && event.properties.delta) {
|
|
240
|
+
this.emit({ type: 'assistant', content: event.properties.delta });
|
|
241
|
+
}
|
|
242
|
+
else if (part.type === 'tool' && part.tool && !seenTools.has(part.id)) {
|
|
243
|
+
seenTools.add(part.id);
|
|
244
|
+
this.emit({
|
|
245
|
+
type: 'tool_use',
|
|
246
|
+
content: JSON.stringify(part.state?.input, null, 2),
|
|
247
|
+
toolName: part.state?.title || part.tool,
|
|
248
|
+
toolId: part.id,
|
|
249
|
+
});
|
|
250
|
+
if (part.state?.status === 'completed' && part.state?.output) {
|
|
251
|
+
this.emit({
|
|
252
|
+
type: 'tool_result',
|
|
253
|
+
content: part.state.output,
|
|
254
|
+
toolId: part.id,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// Invalid JSON, skip
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
clearTimeout(timeout);
|
|
268
|
+
if (!resolved) {
|
|
269
|
+
resolved = true;
|
|
270
|
+
reject(err);
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
clearTimeout(timeout);
|
|
275
|
+
if (receivedIdle) {
|
|
276
|
+
finish();
|
|
277
|
+
}
|
|
278
|
+
else if (!resolved) {
|
|
279
|
+
resolved = true;
|
|
280
|
+
reject(new Error('SSE stream ended unexpectedly without session.idle'));
|
|
281
|
+
}
|
|
282
|
+
})();
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
cleanup() {
|
|
286
|
+
if (this.sseProcess) {
|
|
287
|
+
this.sseProcess.kill();
|
|
288
|
+
this.sseProcess = null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async interrupt() {
|
|
292
|
+
this.cleanup();
|
|
293
|
+
if (this.status === 'running') {
|
|
294
|
+
this.setStatus('interrupted');
|
|
295
|
+
this.emit({ type: 'system', content: 'Interrupted' });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async dispose() {
|
|
299
|
+
await this.interrupt();
|
|
300
|
+
}
|
|
301
|
+
getAgentSessionId() {
|
|
302
|
+
return this.agentSessionId;
|
|
303
|
+
}
|
|
304
|
+
getStatus() {
|
|
305
|
+
return this.status;
|
|
306
|
+
}
|
|
307
|
+
setStatus(status) {
|
|
308
|
+
this.status = status;
|
|
309
|
+
this.statusCallback?.(status);
|
|
310
|
+
}
|
|
311
|
+
emit(msg) {
|
|
312
|
+
this.messageCallback?.({ ...msg, timestamp: new Date().toISOString() });
|
|
313
|
+
}
|
|
314
|
+
emitError(error) {
|
|
315
|
+
this.errorCallback?.(error);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { sessionManager } from './manager';
|
|
2
|
+
import { HOST_WORKSPACE_NAME } from '../shared/client-types';
|
|
3
|
+
function safeSend(ws, data) {
|
|
4
|
+
try {
|
|
5
|
+
ws.send(data);
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class LiveChatHandler {
|
|
13
|
+
connections = new Map();
|
|
14
|
+
isHostAccessAllowed;
|
|
15
|
+
agentType;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.isHostAccessAllowed = options.isHostAccessAllowed || (() => false);
|
|
18
|
+
this.agentType = options.agentType;
|
|
19
|
+
}
|
|
20
|
+
handleOpen(ws, workspaceName) {
|
|
21
|
+
const isHostMode = workspaceName === HOST_WORKSPACE_NAME;
|
|
22
|
+
if (isHostMode && !this.isHostAccessAllowed()) {
|
|
23
|
+
ws.close(4003, 'Host access is disabled');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const connection = {
|
|
27
|
+
ws,
|
|
28
|
+
workspaceName,
|
|
29
|
+
sessionId: null,
|
|
30
|
+
clientId: null,
|
|
31
|
+
agentType: this.agentType,
|
|
32
|
+
};
|
|
33
|
+
this.connections.set(ws, connection);
|
|
34
|
+
safeSend(ws, JSON.stringify({
|
|
35
|
+
type: 'connected',
|
|
36
|
+
workspaceName,
|
|
37
|
+
agentType: this.agentType,
|
|
38
|
+
timestamp: new Date().toISOString(),
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
async handleMessage(ws, data) {
|
|
42
|
+
const connection = this.connections.get(ws);
|
|
43
|
+
if (!connection)
|
|
44
|
+
return;
|
|
45
|
+
try {
|
|
46
|
+
const message = JSON.parse(data);
|
|
47
|
+
if (message.type === 'connect') {
|
|
48
|
+
await this.handleConnect(connection, message);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (message.type === 'disconnect') {
|
|
52
|
+
this.handleDisconnect(connection);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (message.type === 'interrupt') {
|
|
56
|
+
if (connection.sessionId) {
|
|
57
|
+
await sessionManager.interrupt(connection.sessionId);
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (message.type === 'message' && message.content) {
|
|
62
|
+
await this.handleChatMessage(connection, message);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
safeSend(ws, JSON.stringify({
|
|
67
|
+
type: 'error',
|
|
68
|
+
content: err.message,
|
|
69
|
+
timestamp: new Date().toISOString(),
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
handleClose(ws, _code, _reason) {
|
|
74
|
+
const connection = this.connections.get(ws);
|
|
75
|
+
if (connection) {
|
|
76
|
+
this.handleDisconnect(connection);
|
|
77
|
+
}
|
|
78
|
+
this.connections.delete(ws);
|
|
79
|
+
}
|
|
80
|
+
handleError(ws, error) {
|
|
81
|
+
console.error('Live chat WebSocket error:', error);
|
|
82
|
+
const connection = this.connections.get(ws);
|
|
83
|
+
if (connection) {
|
|
84
|
+
this.handleDisconnect(connection);
|
|
85
|
+
}
|
|
86
|
+
this.connections.delete(ws);
|
|
87
|
+
}
|
|
88
|
+
async handleConnect(connection, message) {
|
|
89
|
+
const { ws, workspaceName } = connection;
|
|
90
|
+
const agentType = message.agentType || this.agentType;
|
|
91
|
+
if (message.sessionId) {
|
|
92
|
+
// Look up by internal sessionId or agentSessionId (Claude session ID)
|
|
93
|
+
const found = sessionManager.findSession(message.sessionId);
|
|
94
|
+
if (found) {
|
|
95
|
+
connection.sessionId = found.sessionId;
|
|
96
|
+
const sendFn = (msg) => {
|
|
97
|
+
safeSend(ws, JSON.stringify(msg));
|
|
98
|
+
};
|
|
99
|
+
const clientId = sessionManager.connectClient(found.sessionId, sendFn, {
|
|
100
|
+
resumeFromId: message.resumeFromId,
|
|
101
|
+
});
|
|
102
|
+
if (clientId) {
|
|
103
|
+
connection.clientId = clientId;
|
|
104
|
+
safeSend(ws, JSON.stringify({
|
|
105
|
+
type: 'session_joined',
|
|
106
|
+
sessionId: found.sessionId,
|
|
107
|
+
status: found.info.status,
|
|
108
|
+
agentSessionId: found.info.agentSessionId,
|
|
109
|
+
timestamp: new Date().toISOString(),
|
|
110
|
+
}));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const sessionId = await sessionManager.startSession({
|
|
116
|
+
workspaceName,
|
|
117
|
+
agentType,
|
|
118
|
+
sessionId: message.sessionId,
|
|
119
|
+
agentSessionId: message.agentSessionId,
|
|
120
|
+
model: message.model,
|
|
121
|
+
projectPath: message.projectPath,
|
|
122
|
+
});
|
|
123
|
+
connection.sessionId = sessionId;
|
|
124
|
+
const sendFn = (msg) => {
|
|
125
|
+
safeSend(ws, JSON.stringify(msg));
|
|
126
|
+
};
|
|
127
|
+
const clientId = sessionManager.connectClient(sessionId, sendFn);
|
|
128
|
+
connection.clientId = clientId;
|
|
129
|
+
safeSend(ws, JSON.stringify({
|
|
130
|
+
type: 'session_started',
|
|
131
|
+
sessionId,
|
|
132
|
+
timestamp: new Date().toISOString(),
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
async handleChatMessage(connection, message) {
|
|
136
|
+
if (!connection.sessionId) {
|
|
137
|
+
await this.handleConnect(connection, {
|
|
138
|
+
type: 'connect',
|
|
139
|
+
agentType: message.agentType || this.agentType,
|
|
140
|
+
agentSessionId: message.agentSessionId,
|
|
141
|
+
model: message.model,
|
|
142
|
+
projectPath: message.projectPath,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (!connection.sessionId) {
|
|
146
|
+
throw new Error('Failed to create session');
|
|
147
|
+
}
|
|
148
|
+
await sessionManager.sendMessage(connection.sessionId, message.content);
|
|
149
|
+
}
|
|
150
|
+
handleDisconnect(connection) {
|
|
151
|
+
if (connection.sessionId && connection.clientId) {
|
|
152
|
+
sessionManager.disconnectClient(connection.sessionId, connection.clientId);
|
|
153
|
+
connection.clientId = null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
getConnectionCount() {
|
|
157
|
+
return this.connections.size;
|
|
158
|
+
}
|
|
159
|
+
closeConnectionsForWorkspace(workspaceName) {
|
|
160
|
+
for (const [ws, connection] of this.connections) {
|
|
161
|
+
if (connection.workspaceName === workspaceName) {
|
|
162
|
+
this.handleDisconnect(connection);
|
|
163
|
+
ws.close(1001, 'Workspace stopped');
|
|
164
|
+
this.connections.delete(ws);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
close() {
|
|
169
|
+
for (const [ws, connection] of this.connections.entries()) {
|
|
170
|
+
this.handleDisconnect(connection);
|
|
171
|
+
ws.close(1001, 'Server shutting down');
|
|
172
|
+
}
|
|
173
|
+
this.connections.clear();
|
|
174
|
+
}
|
|
175
|
+
}
|