create-harness-vibe-coding 0.8.18 → 0.8.19
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/CHANGELOG.md +13 -0
- package/README.md +1 -0
- package/package.json +5 -2
- package/src/index.js +383 -267
- package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js +17 -0
- package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js.map +1 -0
- package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js +7 -0
- package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js.map +1 -0
- package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js +12 -0
- package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js.map +1 -0
- package/src/ui/dist/assets/TaskList-BN4r8s1i.js +50 -0
- package/src/ui/dist/assets/TaskList-BN4r8s1i.js.map +1 -0
- package/src/ui/dist/assets/TerminalDrawer-6GBZ9nXN.css +32 -0
- package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js +74 -0
- package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js.map +1 -0
- package/src/ui/dist/assets/WorkflowRoute-BnuhLJ6X.css +1 -0
- package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js +39 -0
- package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js.map +1 -0
- package/src/ui/dist/assets/index-BOVYWntB.js +117 -0
- package/src/ui/dist/assets/index-BOVYWntB.js.map +1 -0
- package/src/ui/dist/assets/index-DaUObq0H.css +1 -0
- package/src/ui/dist/assets/maximize-2-LNrr_rKr.js +7 -0
- package/src/ui/dist/assets/maximize-2-LNrr_rKr.js.map +1 -0
- package/src/ui/dist/assets/plus-QuTJyoT3.js +7 -0
- package/src/ui/dist/assets/plus-QuTJyoT3.js.map +1 -0
- package/src/ui/dist/assets/proxy-D0vUbds5.js +2 -0
- package/src/ui/dist/assets/proxy-D0vUbds5.js.map +1 -0
- package/src/ui/dist/assets/refresh-cw-JNmExijd.js +7 -0
- package/src/ui/dist/assets/refresh-cw-JNmExijd.js.map +1 -0
- package/src/ui/dist/assets/terminal-BBQvnOAK.js +7 -0
- package/src/ui/dist/assets/terminal-BBQvnOAK.js.map +1 -0
- package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js +7 -0
- package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js.map +1 -0
- package/src/ui/dist/assets/x-DoIOGAJz.js +7 -0
- package/src/ui/dist/assets/x-DoIOGAJz.js.map +1 -0
- package/src/ui/dist/index.html +18 -0
- package/src/ui/index.html +17 -0
- package/src/ui/package.json +33 -0
- package/src/ui/pnpm-lock.yaml +1582 -0
- package/src/ui/pnpm-workspace.yaml +2 -0
- package/src/ui/src/App.tsx +86 -0
- package/src/ui/src/api.ts +93 -0
- package/src/ui/src/components/AgentsRoute.tsx +503 -0
- package/src/ui/src/components/Footer.tsx +69 -0
- package/src/ui/src/components/Header.tsx +175 -0
- package/src/ui/src/components/LoadingView.tsx +22 -0
- package/src/ui/src/components/RolesRoute.tsx +169 -0
- package/src/ui/src/components/SettingsRoute.tsx +166 -0
- package/src/ui/src/components/TaskList.tsx +388 -0
- package/src/ui/src/components/TerminalDrawer.tsx +611 -0
- package/src/ui/src/components/WorkflowRoute.tsx +670 -0
- package/src/ui/src/hooks/useReducedMotion.ts +17 -0
- package/src/ui/src/hooks/useServerConnection.ts +114 -0
- package/src/ui/src/index.css +281 -0
- package/src/ui/src/main.tsx +11 -0
- package/src/ui/src/types.ts +108 -0
- package/src/ui/tsconfig.json +21 -0
- package/src/ui/vite.config.ts +19 -0
- package/src/wf-ui-server/__tests__/a2a-store.test.mjs +79 -0
- package/src/wf-ui-server/__tests__/peer-capsule.test.mjs +268 -0
- package/src/wf-ui-server/__tests__/pty-adapter.test.mjs +70 -0
- package/src/wf-ui-server/__tests__/runtime-config.test.mjs +43 -0
- package/src/wf-ui-server/__tests__/runtime-detector.test.mjs +90 -0
- package/src/wf-ui-server/__tests__/security.test.mjs +59 -0
- package/src/wf-ui-server/__tests__/server.integration.test.mjs +150 -0
- package/src/wf-ui-server/__tests__/session-registry.test.mjs +238 -0
- package/src/wf-ui-server/__tests__/settings.test.mjs +135 -0
- package/src/wf-ui-server/__tests__/task-parser.test.mjs +200 -0
- package/src/wf-ui-server/__tests__/terminal-store.test.mjs +138 -0
- package/src/wf-ui-server/__tests__/token.test.mjs +48 -0
- package/src/wf-ui-server/__tests__/ws-events.integration.test.mjs +419 -0
- package/src/wf-ui-server/__tests__/ws-terminal.integration.test.mjs +383 -0
- package/src/wf-ui-server/a2a-store.mjs +296 -0
- package/src/wf-ui-server/peer-capsule.mjs +213 -0
- package/src/wf-ui-server/pty-adapter.mjs +155 -0
- package/src/wf-ui-server/runtime-config.mjs +153 -0
- package/src/wf-ui-server/runtime-detector.mjs +374 -0
- package/src/wf-ui-server/security.mjs +67 -0
- package/src/wf-ui-server/server.mjs +849 -0
- package/src/wf-ui-server/session-registry.mjs +204 -0
- package/src/wf-ui-server/settings.mjs +100 -0
- package/src/wf-ui-server/task-parser.mjs +133 -0
- package/src/wf-ui-server/terminal-store.mjs +225 -0
- package/src/wf-ui-server/token.mjs +41 -0
- package/src/wf-ui-server/ws-events.mjs +354 -0
- package/src/wf-ui-server/ws-terminal.mjs +473 -0
- package/templates/common/.claude/commands/wf-help.md +1 -0
- package/templates/common/.claude/commands/wf-task-list.md +2 -0
- package/templates/common/.claude/commands/wf-ui.md +26 -0
- package/templates/common/.claude/commands/wf-update.md +54 -9
- package/templates/common/.claude/rules/ecc/common.md +1 -1
- package/templates/common/.claude/skills/wf-auto/SKILL.md +3 -3
- package/templates/common/.claude/skills/wf-auto-spark/SKILL.md +2 -2
- package/templates/common/.claude/skills/wf-ui/SKILL.md +78 -0
- package/templates/common/.claude/skills/wf-update/SKILL.md +55 -58
- package/templates/common/.harness-version +63 -39
- package/templates/common/.opencode/commands/wf-help.md +1 -0
- package/templates/common/.opencode/commands/wf-task-list.md +2 -0
- package/templates/common/.opencode/commands/wf-ui.md +26 -0
- package/templates/common/.opencode/commands/wf-update.md +54 -9
- package/templates/common/CLAUDE.md +1 -1
- package/templates/common/Harness/MEMORY.md +2 -0
- package/templates/common/Harness/README.md +6 -4
- package/templates/common/Harness/a2a/role-graph.json +79 -0
- package/templates/common/Harness/a2a/runtime-registry.json +5 -0
- package/templates/common/Harness/a2a/skills/terminal-control.json +15 -0
- package/templates/common/Harness/ownership.manifest.json +57 -2
- package/templates/common/Harness/scripts/README.md +134 -0
- package/templates/common/Harness/scripts/a2a-terminal.mjs +191 -0
- package/templates/common/Harness/scripts/context-budget.mjs +1 -1
- package/templates/common/Harness/scripts/sync-host-global.mjs +278 -0
- package/templates/common/Harness/scripts/task-state.mjs +556 -23
- package/templates/common/Harness/scripts/validate-harness.mjs +229 -19
- package/templates/common/Harness/scripts/wf-remove.mjs +8 -3
- package/templates/common/Harness/scripts/wf-update-check.mjs +37 -9
- package/templates/common/Harness/scripts/wf-update-runner.mjs +325 -0
- package/templates/common/Harness/settings.json +35 -0
- package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +7 -2
- package/templates/common/Harness/specs/runtime/command-surface.json +14 -0
- package/templates/common/Harness/specs/workflows/WF-AUTO-SPARK.md +8 -8
- package/templates/common/Harness/specs/workflows/WF-AUTO.md +12 -12
- package/templates/common/Harness/tasks/_template/NAMING.md +16 -16
- package/templates/common/Harness/tasks/_template/PLAN.md +17 -60
- package/templates/common/Harness/tasks/_template/PROBLEM.md +18 -0
- package/templates/common/Harness/tasks/_template/PROGRESS.md +9 -14
- package/templates/common/Harness/tasks/_template/REFERENCES.md +26 -0
- package/templates/common/Harness/tasks/_template/ARTIFACTS.md +0 -3
- package/templates/common/Harness/tasks/_template/NOTES.md +0 -3
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parseTaskList } from './task-parser.mjs';
|
|
5
|
+
import { validateToken } from './token.mjs';
|
|
6
|
+
|
|
7
|
+
const KEEPALIVE_INTERVAL = 15000;
|
|
8
|
+
const PING_TIMEOUT = 5000;
|
|
9
|
+
const TICK_MS = Math.min(KEEPALIVE_INTERVAL, PING_TIMEOUT) / 2;
|
|
10
|
+
const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
11
|
+
|
|
12
|
+
// ── Frame helpers ──
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Write an unmasked text frame (opcode 0x1) to a socket.
|
|
16
|
+
* @param {import('net').Socket} socket
|
|
17
|
+
* @param {string} text
|
|
18
|
+
* @param {number} [opcode=0x1]
|
|
19
|
+
*/
|
|
20
|
+
function sendFrame(socket, text, opcode = 0x1) {
|
|
21
|
+
const data = Buffer.from(text, 'utf8');
|
|
22
|
+
const len = data.length;
|
|
23
|
+
let frame;
|
|
24
|
+
if (len < 126) {
|
|
25
|
+
frame = Buffer.alloc(2 + len);
|
|
26
|
+
frame[0] = 0x80 | opcode;
|
|
27
|
+
frame[1] = len;
|
|
28
|
+
data.copy(frame, 2);
|
|
29
|
+
} else if (len < 65536) {
|
|
30
|
+
frame = Buffer.alloc(4 + len);
|
|
31
|
+
frame[0] = 0x80 | opcode;
|
|
32
|
+
frame[1] = 126;
|
|
33
|
+
frame.writeUInt16BE(len, 2);
|
|
34
|
+
data.copy(frame, 4);
|
|
35
|
+
} else {
|
|
36
|
+
frame = Buffer.alloc(10 + len);
|
|
37
|
+
frame[0] = 0x80 | opcode;
|
|
38
|
+
frame[1] = 127;
|
|
39
|
+
frame.writeBigUInt64BE(BigInt(len), 2);
|
|
40
|
+
data.copy(frame, 10);
|
|
41
|
+
}
|
|
42
|
+
socket.write(frame);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Send a close frame with the given status code.
|
|
47
|
+
* @param {import('net').Socket} socket
|
|
48
|
+
* @param {number} [code=1000]
|
|
49
|
+
*/
|
|
50
|
+
function sendCloseFrame(socket, code = 1000) {
|
|
51
|
+
const payload = Buffer.alloc(2);
|
|
52
|
+
payload.writeUInt16BE(code, 0);
|
|
53
|
+
const header = Buffer.alloc(2);
|
|
54
|
+
header[0] = 0x88; // FIN + close
|
|
55
|
+
header[1] = 2;
|
|
56
|
+
socket.write(Buffer.concat([header, payload]));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Parse incoming WebSocket frames from a buffer of raw bytes.
|
|
61
|
+
* Handles masked client frames by unmasking the payload.
|
|
62
|
+
* @param {Buffer} buffer
|
|
63
|
+
* @returns {{ frames: Array<{ opcode: number, payload: Buffer }>, remainder: Buffer }}
|
|
64
|
+
*/
|
|
65
|
+
function parseFrames(buffer) {
|
|
66
|
+
const frames = [];
|
|
67
|
+
let offset = 0;
|
|
68
|
+
while (offset + 2 <= buffer.length) {
|
|
69
|
+
const b1 = buffer[offset];
|
|
70
|
+
const opcode = b1 & 0x0F;
|
|
71
|
+
const b2 = buffer[offset + 1];
|
|
72
|
+
const masked = (b2 & 0x80) !== 0;
|
|
73
|
+
let payloadLen = b2 & 0x7F;
|
|
74
|
+
let headerLen = 2;
|
|
75
|
+
if (payloadLen === 126) {
|
|
76
|
+
if (offset + 4 > buffer.length) break;
|
|
77
|
+
payloadLen = buffer.readUInt16BE(offset + 2);
|
|
78
|
+
headerLen = 4;
|
|
79
|
+
} else if (payloadLen === 127) {
|
|
80
|
+
if (offset + 10 > buffer.length) break;
|
|
81
|
+
payloadLen = Number(buffer.readBigUInt64BE(offset + 2));
|
|
82
|
+
headerLen = 10;
|
|
83
|
+
}
|
|
84
|
+
const maskLen = masked ? 4 : 0;
|
|
85
|
+
if (offset + headerLen + maskLen + payloadLen > buffer.length) break;
|
|
86
|
+
offset += headerLen;
|
|
87
|
+
let payload;
|
|
88
|
+
if (masked) {
|
|
89
|
+
const mask = buffer.slice(offset, offset + 4);
|
|
90
|
+
offset += 4;
|
|
91
|
+
payload = Buffer.allocUnsafe(payloadLen);
|
|
92
|
+
for (let i = 0; i < payloadLen; i++) {
|
|
93
|
+
payload[i] = buffer[offset + i] ^ mask[i % 4];
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
payload = buffer.slice(offset, offset + payloadLen);
|
|
97
|
+
}
|
|
98
|
+
offset += payloadLen;
|
|
99
|
+
frames.push({ opcode, payload });
|
|
100
|
+
}
|
|
101
|
+
return { frames, remainder: buffer.slice(offset) };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Send a short HTTP error JSON response, then close the socket.
|
|
106
|
+
* @param {import('net').Socket} sock
|
|
107
|
+
* @param {number} statusCode
|
|
108
|
+
* @param {string} message
|
|
109
|
+
*/
|
|
110
|
+
function sendHttpError(sock, statusCode, message) {
|
|
111
|
+
const body = JSON.stringify({ error: { code: statusCode === 401 ? 'UNAUTHORIZED' : 'ERROR', message } });
|
|
112
|
+
sock.write(
|
|
113
|
+
`HTTP/1.1 ${statusCode} ${statusCode === 401 ? 'Unauthorized' : 'Error'}\r\n` +
|
|
114
|
+
'Content-Type: application/json\r\n' +
|
|
115
|
+
`Content-Length: ${body.length}\r\n` +
|
|
116
|
+
'Connection: close\r\n\r\n' + body
|
|
117
|
+
);
|
|
118
|
+
sock.end();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Public API ──
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Attach a WebSocket /ws/events endpoint to an existing http.Server.
|
|
125
|
+
*
|
|
126
|
+
* On upgrade: validates `?token=` query param, upgrades to WS, sends
|
|
127
|
+
* `server.connected` handshake. Listens to `Harness/tasks/` for changes
|
|
128
|
+
* and broadcasts `task.updated` to all connected clients. Sends `ping`
|
|
129
|
+
* keepalive every 15s and force-closes clients that don't pong within 5s.
|
|
130
|
+
*
|
|
131
|
+
* @param {import('http').Server} httpServer
|
|
132
|
+
* @param {string} expectedToken - The token clients must present.
|
|
133
|
+
* @param {string} projectRoot - Absolute path to the project root.
|
|
134
|
+
* @param {{ keepaliveInterval?: number, pingTimeout?: number }} [options]
|
|
135
|
+
* @returns {{ wss: null, seq: number, close: () => Promise<void> }}
|
|
136
|
+
*/
|
|
137
|
+
export function attachEventsWs(httpServer, expectedToken, projectRoot, options = {}) {
|
|
138
|
+
const keepaliveInterval = options.keepaliveInterval || KEEPALIVE_INTERVAL;
|
|
139
|
+
const pingTimeoutMs = options.pingTimeout || PING_TIMEOUT;
|
|
140
|
+
const tickMs = Math.min(keepaliveInterval, pingTimeoutMs) / 2;
|
|
141
|
+
|
|
142
|
+
const clients = new Set();
|
|
143
|
+
let seq = 0;
|
|
144
|
+
let watcher = null;
|
|
145
|
+
let watchTimeout = null;
|
|
146
|
+
let keepaliveTimer = null;
|
|
147
|
+
|
|
148
|
+
// ── wire upgrade handler ──
|
|
149
|
+
httpServer.on('upgrade', (req, socket, head) => {
|
|
150
|
+
const url = new URL(req.url, `http://${req.headers.host || '127.0.0.1'}`);
|
|
151
|
+
if (url.pathname !== '/ws/events') {
|
|
152
|
+
// Not an events path — let another handler process it
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!validateToken(url.searchParams.get('token'), expectedToken)) {
|
|
157
|
+
sendHttpError(socket, 401, 'Invalid or missing token');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// WS upgrade handshake
|
|
162
|
+
const key = req.headers['sec-websocket-key'];
|
|
163
|
+
const digest = crypto.createHash('sha1').update(key + WS_GUID).digest('base64');
|
|
164
|
+
socket.write(
|
|
165
|
+
'HTTP/1.1 101 Switching Protocols\r\n' +
|
|
166
|
+
'Upgrade: websocket\r\n' +
|
|
167
|
+
'Connection: Upgrade\r\n' +
|
|
168
|
+
'Sec-WebSocket-Accept: ' + digest + '\r\n\r\n'
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const connectionId = crypto.randomBytes(16).toString('hex');
|
|
172
|
+
const client = {
|
|
173
|
+
socket,
|
|
174
|
+
connectionId,
|
|
175
|
+
lastActivity: Date.now(),
|
|
176
|
+
waitingPong: false,
|
|
177
|
+
pingSentAt: 0,
|
|
178
|
+
};
|
|
179
|
+
clients.add(client);
|
|
180
|
+
|
|
181
|
+
// Send handshake
|
|
182
|
+
seq = 0;
|
|
183
|
+
sendFrame(socket, JSON.stringify({
|
|
184
|
+
type: 'server.connected',
|
|
185
|
+
seq: 0,
|
|
186
|
+
ts: new Date().toISOString(),
|
|
187
|
+
connectionId,
|
|
188
|
+
}));
|
|
189
|
+
|
|
190
|
+
// Read incoming WS frames
|
|
191
|
+
let buffer = Buffer.alloc(0);
|
|
192
|
+
if (head && head.length > 0) buffer = Buffer.concat([buffer, head]);
|
|
193
|
+
|
|
194
|
+
socket.on('data', (chunk) => {
|
|
195
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
196
|
+
client.lastActivity = Date.now();
|
|
197
|
+
|
|
198
|
+
const result = parseFrames(buffer);
|
|
199
|
+
buffer = result.remainder;
|
|
200
|
+
for (const frame of result.frames) {
|
|
201
|
+
if (frame.opcode === 0x9) {
|
|
202
|
+
// Ping from client — respond with pong
|
|
203
|
+
sendFrame(socket, '', 0xA);
|
|
204
|
+
} else if (frame.opcode === 0xA) {
|
|
205
|
+
// Pong received — clear waiting flag
|
|
206
|
+
client.waitingPong = false;
|
|
207
|
+
} else if (frame.opcode === 0x8) {
|
|
208
|
+
// Close frame — echo back
|
|
209
|
+
sendCloseFrame(socket, 1000);
|
|
210
|
+
clients.delete(client);
|
|
211
|
+
socket.end();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
socket.on('close', () => {
|
|
218
|
+
clients.delete(client);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
socket.on('error', () => {
|
|
222
|
+
clients.delete(client);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// ── file watcher ──
|
|
227
|
+
const tasksRoot = path.join(projectRoot, 'Harness', 'tasks');
|
|
228
|
+
if (fs.existsSync(tasksRoot)) {
|
|
229
|
+
try {
|
|
230
|
+
watcher = fs.watch(tasksRoot, { recursive: true }, () => {
|
|
231
|
+
if (watchTimeout) clearTimeout(watchTimeout);
|
|
232
|
+
watchTimeout = setTimeout(() => {
|
|
233
|
+
seq++;
|
|
234
|
+
const payload = JSON.stringify({
|
|
235
|
+
type: 'task.updated',
|
|
236
|
+
seq,
|
|
237
|
+
ts: new Date().toISOString(),
|
|
238
|
+
taskId: null,
|
|
239
|
+
payload: { taskCount: parseTaskList(tasksRoot).length },
|
|
240
|
+
});
|
|
241
|
+
for (const c of clients) {
|
|
242
|
+
try { sendFrame(c.socket, payload); } catch { /* ignore */ }
|
|
243
|
+
}
|
|
244
|
+
}, 200);
|
|
245
|
+
});
|
|
246
|
+
} catch { /* watcher unavailable — degrade gracefully */ }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── keepalive timer ──
|
|
250
|
+
keepaliveTimer = setInterval(() => {
|
|
251
|
+
const now = Date.now();
|
|
252
|
+
for (const c of [...clients]) {
|
|
253
|
+
try {
|
|
254
|
+
if (c.waitingPong) {
|
|
255
|
+
if (now - c.pingSentAt >= pingTimeoutMs) {
|
|
256
|
+
sendCloseFrame(c.socket, 1001);
|
|
257
|
+
c.socket.end();
|
|
258
|
+
clients.delete(c);
|
|
259
|
+
}
|
|
260
|
+
} else if (now - c.lastActivity >= keepaliveInterval) {
|
|
261
|
+
sendFrame(c.socket, '', 0x9);
|
|
262
|
+
c.waitingPong = true;
|
|
263
|
+
c.pingSentAt = now;
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
clients.delete(c);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}, Math.max(tickMs, 100));
|
|
270
|
+
|
|
271
|
+
// ── close ──
|
|
272
|
+
function close() {
|
|
273
|
+
return new Promise((resolve) => {
|
|
274
|
+
if (keepaliveTimer) clearInterval(keepaliveTimer);
|
|
275
|
+
if (watchTimeout) clearTimeout(watchTimeout);
|
|
276
|
+
if (watcher) {
|
|
277
|
+
try { watcher.close(); } catch { /* ignore */ }
|
|
278
|
+
}
|
|
279
|
+
for (const c of [...clients]) {
|
|
280
|
+
try { sendCloseFrame(c.socket, 1001); c.socket.end(); } catch { /* ignore */ }
|
|
281
|
+
}
|
|
282
|
+
clients.clear();
|
|
283
|
+
resolve();
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── public broadcast helpers (invoked externally) ──
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Broadcast an arbitrary type+payload to all connected WebSocket clients.
|
|
291
|
+
* @param {string} type - Event type name
|
|
292
|
+
* @param {object} payload - Data fields (type and seq will be overridden)
|
|
293
|
+
*/
|
|
294
|
+
function broadcastAll(type, payload) {
|
|
295
|
+
seq++;
|
|
296
|
+
const msg = JSON.stringify({ type, ...payload, seq, ts: new Date().toISOString() });
|
|
297
|
+
for (const c of [...clients]) {
|
|
298
|
+
try { sendFrame(c.socket, msg); } catch { /* ignore */ }
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Broadcast a peer.started event to all connected WS clients.
|
|
304
|
+
* @param {string} peerId
|
|
305
|
+
* @param {string} runtime
|
|
306
|
+
* @param {string} taskId
|
|
307
|
+
*/
|
|
308
|
+
function broadcastPeerStartedFn(peerId, runtime, taskId) {
|
|
309
|
+
broadcastAll('peer.started', { peerId, runtime, taskId });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Broadcast a peer.blocked event to all connected WS clients.
|
|
314
|
+
* @param {string} peerId
|
|
315
|
+
* @param {string} reason
|
|
316
|
+
*/
|
|
317
|
+
function broadcastPeerBlockedFn(peerId, reason) {
|
|
318
|
+
broadcastAll('peer.blocked', { peerId, reason });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Broadcast a session.state event to all connected WS clients.
|
|
323
|
+
* @param {string} sessionId
|
|
324
|
+
* @param {string} state
|
|
325
|
+
*/
|
|
326
|
+
function broadcastSessionStateFn(sessionId, state) {
|
|
327
|
+
broadcastAll('session.state', { sessionId, state });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Get the current sequence number.
|
|
332
|
+
* @returns {number}
|
|
333
|
+
*/
|
|
334
|
+
function getSeq() { return seq; }
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Increment the sequence number and return the new value.
|
|
338
|
+
* @returns {number}
|
|
339
|
+
*/
|
|
340
|
+
function incrementSeq() { return ++seq; }
|
|
341
|
+
|
|
342
|
+
// ── return ──
|
|
343
|
+
return {
|
|
344
|
+
wss: null,
|
|
345
|
+
get seq() { return seq; },
|
|
346
|
+
close,
|
|
347
|
+
broadcast: broadcastAll,
|
|
348
|
+
broadcastPeerStarted: broadcastPeerStartedFn,
|
|
349
|
+
broadcastPeerBlocked: broadcastPeerBlockedFn,
|
|
350
|
+
broadcastSessionState: broadcastSessionStateFn,
|
|
351
|
+
getSeq,
|
|
352
|
+
incrementSeq,
|
|
353
|
+
};
|
|
354
|
+
}
|