@cnstra/devtools-server 1.1.0 → 1.1.2
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/index.cjs +194 -942
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +45 -83
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +195 -942
- package/dist/index.js.map +1 -1
- package/dist/start.d.ts +2 -6
- package/dist/start.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,995 +1,248 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, readFileSync } from 'fs';
|
|
3
|
-
import { join, dirname } from 'path';
|
|
4
|
-
import { createRequire } from 'module';
|
|
5
|
-
import { WebSocketServer, WebSocket } from 'ws';
|
|
6
|
-
|
|
7
|
-
// src/start.ts
|
|
8
|
-
var _require = createRequire(join(process.cwd(), "package.json"));
|
|
9
|
-
function findPanelUIDir() {
|
|
10
|
-
try {
|
|
11
|
-
const pkg = _require.resolve("@cnstra/devtools-panel-ui/package.json");
|
|
12
|
-
return join(dirname(pkg), "dist");
|
|
13
|
-
} catch {
|
|
14
|
-
}
|
|
15
|
-
try {
|
|
16
|
-
const here = dirname(_require.resolve("@cnstra/devtools-server/package.json"));
|
|
17
|
-
const monorepoPath = join(here, "..", "..", "devtools-panel-ui", "dist");
|
|
18
|
-
if (existsSync(join(monorepoPath, "index.html"))) return monorepoPath;
|
|
19
|
-
} catch {
|
|
20
|
-
}
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
function makeCNSAdapter(cns, registry) {
|
|
24
|
-
return {
|
|
25
|
-
addResponseListener: (l) => cns.addResponseListener(l),
|
|
26
|
-
stimulate: (signal, opts) => cns.stimulate(signal, opts),
|
|
27
|
-
getNeurons: () => [...registry.getNamedNeurons().entries()].map(([name, n]) => ({
|
|
28
|
-
name,
|
|
29
|
-
axon: n.axon,
|
|
30
|
-
dendrites: n.dendrites
|
|
31
|
-
})),
|
|
32
|
-
getCollaterals: () => {
|
|
33
|
-
const out = [];
|
|
34
|
-
for (const [, n] of registry.getNamedNeurons()) {
|
|
35
|
-
for (const [key, col] of Object.entries(n.axon)) {
|
|
36
|
-
out.push({ name: key, createSignal: (p) => col.createSignal(p) });
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return out;
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
var MIME = {
|
|
44
|
-
html: "text/html",
|
|
45
|
-
js: "application/javascript",
|
|
46
|
-
css: "text/css",
|
|
47
|
-
svg: "image/svg+xml",
|
|
48
|
-
png: "image/png",
|
|
49
|
-
woff2: "font/woff2"
|
|
50
|
-
};
|
|
51
|
-
function serveStatic(panelUIDir) {
|
|
52
|
-
return (_req, res) => {
|
|
53
|
-
const url = _req.url ?? "/";
|
|
54
|
-
const safe = url.split("?")[0].replace(/\.\./g, "");
|
|
55
|
-
const file = safe === "/" ? "/index.html" : safe;
|
|
56
|
-
const full = join(panelUIDir, file);
|
|
57
|
-
if (existsSync(full) && full.startsWith(panelUIDir)) {
|
|
58
|
-
const ext = full.split(".").pop() ?? "";
|
|
59
|
-
res.writeHead(200, { "Content-Type": MIME[ext] ?? "text/plain" });
|
|
60
|
-
res.end(readFileSync(full));
|
|
61
|
-
} else {
|
|
62
|
-
const idx = join(panelUIDir, "index.html");
|
|
63
|
-
if (existsSync(idx)) {
|
|
64
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
65
|
-
res.end(readFileSync(idx));
|
|
66
|
-
} else {
|
|
67
|
-
res.writeHead(503);
|
|
68
|
-
res.end("DevTools UI not built. Run: npm run build --workspace=@cnstra/devtools-panel-ui");
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
async function startDevTools(cns, registry, options = {}) {
|
|
74
|
-
const port = options.port ?? 3141;
|
|
75
|
-
const appId = options.appId ?? "app";
|
|
76
|
-
const logEnabled = options.consoleLogEnabled ?? false;
|
|
77
|
-
const panelUIDir = findPanelUIDir();
|
|
78
|
-
if (!panelUIDir) {
|
|
79
|
-
console.warn(
|
|
80
|
-
"\n\u26A0\uFE0F CNStra DevTools UI not found.\n WebSocket server will start but the UI will not be served.\n Install @cnstra/devtools-panel-ui or build it from the monorepo.\n"
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
const httpServer = createServer(
|
|
84
|
-
panelUIDir ? serveStatic(panelUIDir) : (_, res) => {
|
|
85
|
-
res.writeHead(503);
|
|
86
|
-
res.end("DevTools UI not available. See console for details.");
|
|
87
|
-
}
|
|
88
|
-
);
|
|
89
|
-
const wss = new WebSocketServer({ server: httpServer });
|
|
90
|
-
const { CNSDevToolsServerRepositoryInMemory } = await import('@cnstra/devtools-server-repository-in-memory');
|
|
91
|
-
const dtServer = new CNSDevToolsServer(
|
|
92
|
-
new CNSDevToolsServerRepositoryInMemory(),
|
|
93
|
-
{ consoleLogEnabled: logEnabled }
|
|
94
|
-
);
|
|
95
|
-
const clients = /* @__PURE__ */ new Set();
|
|
96
|
-
wss.on("connection", (ws) => {
|
|
97
|
-
clients.add(ws);
|
|
98
|
-
ws.on("message", async (data) => {
|
|
99
|
-
try {
|
|
100
|
-
const msg = JSON.parse(data.toString());
|
|
101
|
-
const result = await dtServer.handleMessage(ws, msg);
|
|
102
|
-
if (result) {
|
|
103
|
-
const payload = JSON.stringify(result);
|
|
104
|
-
clients.forEach((c) => {
|
|
105
|
-
if (c.readyState === WebSocket.OPEN) c.send(payload);
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
} catch {
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
ws.on("close", () => {
|
|
112
|
-
clients.delete(ws);
|
|
113
|
-
dtServer.handleDisconnect(ws);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
await new Promise((resolve) => httpServer.listen(port, resolve));
|
|
117
|
-
const url = `http://localhost:${port}`;
|
|
118
|
-
const { CNSDevTools } = await import('@cnstra/devtools');
|
|
119
|
-
const { CNSDevToolsTransportWs } = await import('@cnstra/devtools-transport-ws');
|
|
120
|
-
const transport = new CNSDevToolsTransportWs({
|
|
121
|
-
url: `ws://localhost:${port}`
|
|
122
|
-
});
|
|
123
|
-
const devtools = new CNSDevTools(appId, transport, {
|
|
124
|
-
devToolsInstanceName: appId,
|
|
125
|
-
consoleLogEnabled: logEnabled
|
|
126
|
-
});
|
|
127
|
-
devtools.registerCNS(makeCNSAdapter(cns, registry), "main");
|
|
128
|
-
if (logEnabled || process.stdout.isTTY) {
|
|
129
|
-
console.log(`
|
|
130
|
-
\u{1F9E0} CNStra DevTools: ${url}
|
|
131
|
-
`);
|
|
132
|
-
}
|
|
133
|
-
const shouldOpen = options.open ?? process.stdout.isTTY;
|
|
134
|
-
if (shouldOpen) {
|
|
135
|
-
const { exec } = await import('child_process');
|
|
136
|
-
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
137
|
-
exec(`${cmd} ${url}`);
|
|
138
|
-
}
|
|
139
|
-
return {
|
|
140
|
-
url,
|
|
141
|
-
stop: () => {
|
|
142
|
-
wss.close();
|
|
143
|
-
httpServer.close();
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
}
|
|
1
|
+
import { CNSDTOAppBatchMessageSchema, CNSDTOClientMessageSchema } from '@cnstra/devtools-dto';
|
|
147
2
|
|
|
148
3
|
// src/index.ts
|
|
149
|
-
var CNSDevToolsServer = class
|
|
150
|
-
constructor(repository
|
|
4
|
+
var CNSDevToolsServer = class {
|
|
5
|
+
constructor(repository) {
|
|
151
6
|
this.repository = repository;
|
|
152
|
-
|
|
153
|
-
this.stopped = false;
|
|
7
|
+
/** WebSocket → appId (for app clients) */
|
|
154
8
|
this.appSockets = /* @__PURE__ */ new Map();
|
|
155
|
-
|
|
156
|
-
this.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
this.appFirstSeenAt = /* @__PURE__ */ new Map();
|
|
160
|
-
this.appLastSeenAt = /* @__PURE__ */ new Map();
|
|
161
|
-
this.replaysByApp = /* @__PURE__ */ new Map();
|
|
162
|
-
// server metrics sampling
|
|
9
|
+
/** appId → WebSocket[] (one app may have multiple CNS instances) */
|
|
10
|
+
this.appWsByAppId = /* @__PURE__ */ new Map();
|
|
11
|
+
/** WebSocket clients that registered as UI clients */
|
|
12
|
+
this.uiClients = /* @__PURE__ */ new Set();
|
|
163
13
|
this.lastCpuUsage = process.cpuUsage();
|
|
164
14
|
this.lastCpuTime = Date.now();
|
|
165
15
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const t = typeof value;
|
|
175
|
-
if (t === "string") {
|
|
176
|
-
const s = value;
|
|
177
|
-
return s.length > limits.maxString ? s.slice(0, limits.maxString) + `\u2026[+${s.length - limits.maxString}]` : s;
|
|
178
|
-
}
|
|
179
|
-
if (t === "number" || t === "boolean" || t === "bigint" || t === "symbol")
|
|
180
|
-
return value;
|
|
181
|
-
if (t === "function") return "[Function]";
|
|
182
|
-
if (value instanceof Error) {
|
|
183
|
-
return {
|
|
184
|
-
name: value.name,
|
|
185
|
-
message: value.message,
|
|
186
|
-
stack: value.stack
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
if (Array.isArray(value)) {
|
|
190
|
-
const arr = value;
|
|
191
|
-
const slice = arr.slice(0, limits.maxArray);
|
|
192
|
-
const mapped = slice.map(
|
|
193
|
-
(v) => this.sanitizeValue(v, depth + 1, limits)
|
|
194
|
-
);
|
|
195
|
-
if (arr.length > slice.length)
|
|
196
|
-
mapped.push(`[\u2026+${arr.length - slice.length} items]`);
|
|
197
|
-
return mapped;
|
|
16
|
+
// ─── Message entry point ─────────────────────────────────────────────────
|
|
17
|
+
async handleMessage(ws, raw) {
|
|
18
|
+
if (typeof raw !== "string" && typeof raw !== "object") return;
|
|
19
|
+
let data;
|
|
20
|
+
try {
|
|
21
|
+
data = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
22
|
+
} catch {
|
|
23
|
+
return;
|
|
198
24
|
}
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
25
|
+
if (!data || typeof data !== "object" || !("type" in data)) return;
|
|
26
|
+
const type = data.type;
|
|
27
|
+
if (type === "batch") {
|
|
28
|
+
const result = CNSDTOAppBatchMessageSchema.safeParse(data);
|
|
29
|
+
if (!result.success) return;
|
|
30
|
+
for (const item of result.data.items) {
|
|
31
|
+
await this.handleAppBatchItem(ws, item);
|
|
205
32
|
}
|
|
206
|
-
|
|
207
|
-
out["\u2026"] = `+${entries.length - slice.length} keys`;
|
|
208
|
-
}
|
|
209
|
-
return out;
|
|
33
|
+
return;
|
|
210
34
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
35
|
+
const clientResult = CNSDTOClientMessageSchema.safeParse(data);
|
|
36
|
+
if (!clientResult.success) return;
|
|
37
|
+
const msg = clientResult.data;
|
|
38
|
+
switch (msg.type) {
|
|
39
|
+
case "client.connect":
|
|
40
|
+
await this.handleClientConnect(ws);
|
|
41
|
+
break;
|
|
42
|
+
case "apps.query":
|
|
43
|
+
await this.handleAppsQuery(ws, msg.requestId);
|
|
44
|
+
break;
|
|
45
|
+
case "topology.query":
|
|
46
|
+
await this.handleTopologyQuery(ws, msg.requestId, msg.cnsId, msg.appId);
|
|
47
|
+
break;
|
|
48
|
+
case "executions.query":
|
|
49
|
+
await this.handleExecutionsQuery(ws, msg);
|
|
50
|
+
break;
|
|
51
|
+
case "hops.query":
|
|
52
|
+
await this.handleHopsQuery(ws, msg);
|
|
53
|
+
break;
|
|
54
|
+
case "replay.start":
|
|
55
|
+
await this.handleReplayStart(ws, msg);
|
|
56
|
+
break;
|
|
216
57
|
}
|
|
217
58
|
}
|
|
218
|
-
|
|
219
|
-
|
|
59
|
+
addClient(ws) {
|
|
60
|
+
this.uiClients.add(ws);
|
|
61
|
+
if (this.uiClients.size === 1) this.startMetrics();
|
|
220
62
|
}
|
|
221
|
-
|
|
222
|
-
|
|
63
|
+
removeClient(ws) {
|
|
64
|
+
this.uiClients.delete(ws);
|
|
65
|
+
this.appSockets.delete(ws);
|
|
66
|
+
if (this.uiClients.size === 0) this.stopMetrics();
|
|
223
67
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
get connectedAppCount() {
|
|
227
|
-
return this.appSockets.size;
|
|
68
|
+
async getActiveApps() {
|
|
69
|
+
return this.repository.listApps();
|
|
228
70
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const offset = Math.max(0, params.offset ?? 0);
|
|
232
|
-
const limit = params.limit !== void 0 && params.limit > 0 ? params.limit : void 0;
|
|
233
|
-
const filtered = items.filter(
|
|
234
|
-
(item) => (fromTimestamp === void 0 || item.timestamp >= fromTimestamp) && (toTimestamp === void 0 || item.timestamp <= toTimestamp)
|
|
235
|
-
).slice().sort((a, b) => a.timestamp - b.timestamp);
|
|
236
|
-
if (offset > 0 || limit !== void 0) {
|
|
237
|
-
return filtered.slice(
|
|
238
|
-
offset,
|
|
239
|
-
limit !== void 0 ? offset + limit : void 0
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
return filtered;
|
|
71
|
+
stop() {
|
|
72
|
+
this.stopMetrics();
|
|
243
73
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
this.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
this.log("DevTools client connected, total clients:", this.clientSockets.size);
|
|
260
|
-
return { type: "devtools-client-connected" };
|
|
261
|
-
case "init":
|
|
262
|
-
return this.handleInit(ws, message);
|
|
263
|
-
case "batch":
|
|
264
|
-
return this.handleBatch(
|
|
265
|
-
ws,
|
|
266
|
-
message
|
|
267
|
-
);
|
|
268
|
-
case "neuron-response-batch":
|
|
269
|
-
case "response-batch":
|
|
270
|
-
return this.handleResponseBatch(
|
|
271
|
-
ws,
|
|
272
|
-
message
|
|
273
|
-
);
|
|
274
|
-
case "stimulate":
|
|
275
|
-
return this.handleStimulate(ws, message);
|
|
276
|
-
case "stimulate-accepted":
|
|
277
|
-
try {
|
|
278
|
-
const msg = message;
|
|
279
|
-
const cmdId = msg.stimulationCommandId;
|
|
280
|
-
const appId = msg.appId;
|
|
281
|
-
if (cmdId && appId) {
|
|
282
|
-
const arr = this.replaysByApp.get(appId) || [];
|
|
283
|
-
for (let i = arr.length - 1; i >= 0; i--) {
|
|
284
|
-
if (arr[i].stimulationCommandId === cmdId) {
|
|
285
|
-
arr[i].result = "accepted";
|
|
286
|
-
break;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
this.replaysByApp.set(appId, arr);
|
|
290
|
-
}
|
|
291
|
-
} catch {
|
|
292
|
-
}
|
|
293
|
-
return message;
|
|
294
|
-
case "stimulate-rejected":
|
|
295
|
-
try {
|
|
296
|
-
const msg = message;
|
|
297
|
-
const cmdId = msg.stimulationCommandId;
|
|
298
|
-
const appId = msg.appId;
|
|
299
|
-
if (cmdId && appId) {
|
|
300
|
-
const arr = this.replaysByApp.get(appId) || [];
|
|
301
|
-
for (let i = arr.length - 1; i >= 0; i--) {
|
|
302
|
-
if (arr[i].stimulationCommandId === cmdId) {
|
|
303
|
-
arr[i].result = "rejected";
|
|
304
|
-
break;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
this.replaysByApp.set(appId, arr);
|
|
308
|
-
}
|
|
309
|
-
} catch {
|
|
310
|
-
}
|
|
311
|
-
return message;
|
|
312
|
-
case "stimulation":
|
|
313
|
-
return this.handleStimulation(
|
|
314
|
-
ws,
|
|
315
|
-
message
|
|
316
|
-
);
|
|
317
|
-
case "apps:get-topology": {
|
|
318
|
-
const inits = Array.from(this.lastInitByApp.values());
|
|
319
|
-
return { type: "apps:topology", inits };
|
|
320
|
-
}
|
|
321
|
-
case "apps:get-stimulations": {
|
|
322
|
-
const msg = message;
|
|
323
|
-
const {
|
|
324
|
-
appId,
|
|
325
|
-
limit,
|
|
326
|
-
fromTimestamp,
|
|
327
|
-
toTimestamp,
|
|
328
|
-
offset,
|
|
329
|
-
hasError,
|
|
330
|
-
errorContains,
|
|
331
|
-
collateralName,
|
|
332
|
-
neuronId
|
|
333
|
-
} = msg;
|
|
334
|
-
let stimulations = [];
|
|
335
|
-
if (appId) {
|
|
336
|
-
stimulations = await this.repository.getStimulationsByApp(
|
|
337
|
-
appId,
|
|
338
|
-
{
|
|
339
|
-
fromTimestamp,
|
|
340
|
-
toTimestamp,
|
|
341
|
-
limit,
|
|
342
|
-
offset,
|
|
343
|
-
hasError,
|
|
344
|
-
errorContains,
|
|
345
|
-
collateralName,
|
|
346
|
-
neuronId
|
|
347
|
-
}
|
|
348
|
-
);
|
|
349
|
-
} else {
|
|
350
|
-
const apps = await this.repository.listApps();
|
|
351
|
-
for (const app of apps) {
|
|
352
|
-
const appStimulations = await this.repository.getStimulationsByApp(
|
|
353
|
-
app.appId,
|
|
354
|
-
{
|
|
355
|
-
fromTimestamp,
|
|
356
|
-
toTimestamp,
|
|
357
|
-
limit,
|
|
358
|
-
offset,
|
|
359
|
-
hasError,
|
|
360
|
-
errorContains,
|
|
361
|
-
collateralName,
|
|
362
|
-
neuronId
|
|
363
|
-
}
|
|
364
|
-
);
|
|
365
|
-
stimulations = stimulations.concat(appStimulations);
|
|
366
|
-
}
|
|
367
|
-
return {
|
|
368
|
-
type: "stimulation-batch",
|
|
369
|
-
stimulations
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
return {
|
|
373
|
-
type: "stimulation-batch",
|
|
374
|
-
stimulations
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
case "apps:get-replays": {
|
|
378
|
-
const msg = message;
|
|
379
|
-
const { appId, limit, fromTimestamp, toTimestamp, offset } = msg;
|
|
380
|
-
if (!appId) {
|
|
381
|
-
return { type: "apps:replays", replays: [] };
|
|
382
|
-
}
|
|
383
|
-
let list = this.replaysByApp.get(appId) || [];
|
|
384
|
-
if (fromTimestamp !== void 0) {
|
|
385
|
-
list = list.filter((r) => r.timestamp >= fromTimestamp);
|
|
386
|
-
}
|
|
387
|
-
if (toTimestamp !== void 0) {
|
|
388
|
-
list = list.filter((r) => r.timestamp <= toTimestamp);
|
|
389
|
-
}
|
|
390
|
-
list = list.sort((a, b) => b.timestamp - a.timestamp);
|
|
391
|
-
if (offset !== void 0 && offset > 0) {
|
|
392
|
-
list = list.slice(offset);
|
|
393
|
-
}
|
|
394
|
-
if (limit !== void 0 && limit > 0) {
|
|
395
|
-
list = list.slice(0, limit);
|
|
396
|
-
}
|
|
397
|
-
return { type: "apps:replays", appId, replays: list };
|
|
398
|
-
}
|
|
399
|
-
// Export endpoints (JSON export over WS)
|
|
400
|
-
case "apps:export-topology": {
|
|
401
|
-
const { appId } = message;
|
|
402
|
-
const inits = Array.from(this.lastInitByApp.values());
|
|
403
|
-
if (appId) {
|
|
404
|
-
const filtered = inits.filter((init) => {
|
|
405
|
-
const baseAppId = init.appId || init.devToolsInstanceId;
|
|
406
|
-
return baseAppId === appId;
|
|
407
|
-
});
|
|
408
|
-
return { type: "apps:topology", inits: filtered };
|
|
409
|
-
}
|
|
410
|
-
return { type: "apps:topology", inits };
|
|
411
|
-
}
|
|
412
|
-
case "apps:export-stimulations": {
|
|
413
|
-
const {
|
|
414
|
-
appId,
|
|
415
|
-
limit,
|
|
416
|
-
fromTimestamp,
|
|
417
|
-
toTimestamp,
|
|
418
|
-
offset,
|
|
419
|
-
hasError,
|
|
420
|
-
errorContains,
|
|
421
|
-
collateralName,
|
|
422
|
-
neuronId
|
|
423
|
-
} = message;
|
|
424
|
-
let stimulations = [];
|
|
425
|
-
if (appId) {
|
|
426
|
-
stimulations = await this.repository.getStimulationsByApp(
|
|
427
|
-
appId,
|
|
428
|
-
{
|
|
429
|
-
fromTimestamp,
|
|
430
|
-
toTimestamp,
|
|
431
|
-
limit,
|
|
432
|
-
offset,
|
|
433
|
-
hasError,
|
|
434
|
-
errorContains,
|
|
435
|
-
collateralName,
|
|
436
|
-
neuronId
|
|
437
|
-
}
|
|
438
|
-
);
|
|
439
|
-
} else {
|
|
440
|
-
const apps = await this.repository.listApps();
|
|
441
|
-
for (const app of apps) {
|
|
442
|
-
const appStimulations = await this.repository.getStimulationsByApp(
|
|
443
|
-
app.appId,
|
|
444
|
-
{
|
|
445
|
-
fromTimestamp,
|
|
446
|
-
toTimestamp,
|
|
447
|
-
limit,
|
|
448
|
-
offset,
|
|
449
|
-
hasError,
|
|
450
|
-
errorContains,
|
|
451
|
-
collateralName,
|
|
452
|
-
neuronId
|
|
453
|
-
}
|
|
454
|
-
);
|
|
455
|
-
stimulations = stimulations.concat(appStimulations);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
return {
|
|
459
|
-
type: "apps:export-stimulations",
|
|
460
|
-
stimulations
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
case "apps:export-snapshot": {
|
|
464
|
-
const { appId, limitResponses, limitStimulations } = message;
|
|
465
|
-
const inits = Array.from(this.lastInitByApp.values());
|
|
466
|
-
const topology = appId ? inits.filter(
|
|
467
|
-
(init) => (init.appId || init.devToolsInstanceId) === appId
|
|
468
|
-
) : inits;
|
|
469
|
-
let stimulations = [];
|
|
470
|
-
if (appId) {
|
|
471
|
-
stimulations = await this.repository.getStimulationsByApp(
|
|
472
|
-
appId
|
|
473
|
-
);
|
|
474
|
-
} else {
|
|
475
|
-
const apps = await this.repository.listApps();
|
|
476
|
-
for (const app of apps) {
|
|
477
|
-
const appStimulations = await this.repository.getStimulationsByApp(
|
|
478
|
-
app.appId
|
|
479
|
-
);
|
|
480
|
-
stimulations = stimulations.concat(appStimulations);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
const responses = [];
|
|
484
|
-
if (appId) {
|
|
485
|
-
const cnsIds = await this.repository.getCnsByApp(appId);
|
|
486
|
-
for (const cnsId of cnsIds) {
|
|
487
|
-
const cnsResponses = await this.repository.getResponsesByCns(cnsId);
|
|
488
|
-
responses.push(...cnsResponses);
|
|
489
|
-
}
|
|
490
|
-
} else {
|
|
491
|
-
const apps = await this.repository.listApps();
|
|
492
|
-
for (const app of apps) {
|
|
493
|
-
const cnsIds = await this.repository.getCnsByApp(
|
|
494
|
-
app.appId
|
|
495
|
-
);
|
|
496
|
-
for (const cnsId of cnsIds) {
|
|
497
|
-
const cnsResponses = await this.repository.getResponsesByCns(cnsId);
|
|
498
|
-
responses.push(...cnsResponses);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
const tail = (arr, n) => typeof n === "number" && n > 0 ? arr.slice(Math.max(0, arr.length - n)) : arr;
|
|
503
|
-
const sanitizedStim = tail(stimulations, limitStimulations).map(
|
|
504
|
-
(s) => ({
|
|
505
|
-
...s,
|
|
506
|
-
payload: _CNSDevToolsServer.sanitizeValue(
|
|
507
|
-
s.payload
|
|
508
|
-
)
|
|
509
|
-
})
|
|
510
|
-
);
|
|
511
|
-
const sanitizedResp = tail(responses, limitResponses).map(
|
|
512
|
-
(r) => ({
|
|
513
|
-
...r,
|
|
514
|
-
payload: _CNSDevToolsServer.sanitizeValue(
|
|
515
|
-
r.payload
|
|
516
|
-
),
|
|
517
|
-
contexts: _CNSDevToolsServer.sanitizeValue(
|
|
518
|
-
r.contexts
|
|
519
|
-
)
|
|
520
|
-
})
|
|
521
|
-
);
|
|
522
|
-
const snapshot = {
|
|
523
|
-
type: "apps:snapshot",
|
|
524
|
-
appId: appId || null,
|
|
525
|
-
topology,
|
|
526
|
-
stimulations: sanitizedStim,
|
|
527
|
-
responses: sanitizedResp,
|
|
528
|
-
createdAt: Date.now()
|
|
529
|
-
};
|
|
530
|
-
try {
|
|
531
|
-
const sizeBytes = Buffer.byteLength(
|
|
532
|
-
JSON.stringify(snapshot)
|
|
533
|
-
);
|
|
534
|
-
snapshot.sizeBytes = sizeBytes;
|
|
535
|
-
const maxBytes = 5 * 1024 * 1024;
|
|
536
|
-
if (sizeBytes > maxBytes) {
|
|
537
|
-
snapshot.warning = `Snapshot size ${sizeBytes} bytes exceeds ${maxBytes} bytes; consider narrowing filters.`;
|
|
538
|
-
}
|
|
539
|
-
} catch {
|
|
540
|
-
}
|
|
541
|
-
return snapshot;
|
|
542
|
-
}
|
|
543
|
-
case "cns:export-responses": {
|
|
544
|
-
const {
|
|
545
|
-
cnsId,
|
|
546
|
-
limit,
|
|
547
|
-
fromTimestamp,
|
|
548
|
-
toTimestamp,
|
|
549
|
-
offset,
|
|
550
|
-
hasError,
|
|
551
|
-
errorContains,
|
|
552
|
-
neuronId,
|
|
553
|
-
collateralName
|
|
554
|
-
} = message;
|
|
555
|
-
const responsesAll = await this.repository.getResponsesByCns(
|
|
556
|
-
cnsId,
|
|
557
|
-
{
|
|
558
|
-
fromTimestamp,
|
|
559
|
-
toTimestamp,
|
|
560
|
-
limit,
|
|
561
|
-
offset,
|
|
562
|
-
hasError,
|
|
563
|
-
errorContains,
|
|
564
|
-
neuronId,
|
|
565
|
-
collateralName
|
|
566
|
-
}
|
|
567
|
-
);
|
|
568
|
-
return {
|
|
569
|
-
type: "cns:export-responses",
|
|
570
|
-
cnsId,
|
|
571
|
-
responses: responsesAll
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
// REST-like over WS
|
|
575
|
-
case "apps:list": {
|
|
576
|
-
const apps = await this.repository.listApps();
|
|
577
|
-
return { type: "apps:list", apps };
|
|
578
|
-
}
|
|
579
|
-
case "apps:get-cns": {
|
|
580
|
-
const { appId } = message;
|
|
581
|
-
const cnsIds = await this.repository.getCnsByApp(appId);
|
|
582
|
-
const cns = cnsIds.map((cnsId) => ({ cnsId, appId }));
|
|
583
|
-
return { type: "apps:cns", appId, cns };
|
|
584
|
-
}
|
|
585
|
-
case "apps:get-responses": {
|
|
586
|
-
const {
|
|
587
|
-
appId,
|
|
588
|
-
limit,
|
|
589
|
-
fromTimestamp,
|
|
590
|
-
toTimestamp,
|
|
591
|
-
offset,
|
|
592
|
-
hasError,
|
|
593
|
-
errorContains,
|
|
594
|
-
neuronId,
|
|
595
|
-
collateralName
|
|
596
|
-
} = message;
|
|
597
|
-
let allResponses = [];
|
|
598
|
-
if (appId) {
|
|
599
|
-
const cnsIds = await this.repository.getCnsByApp(appId);
|
|
600
|
-
for (const cnsId of cnsIds) {
|
|
601
|
-
const responses = await this.repository.getResponsesByCns(cnsId, {
|
|
602
|
-
fromTimestamp,
|
|
603
|
-
toTimestamp,
|
|
604
|
-
hasError,
|
|
605
|
-
errorContains,
|
|
606
|
-
neuronId,
|
|
607
|
-
collateralName
|
|
608
|
-
});
|
|
609
|
-
allResponses = allResponses.concat(responses);
|
|
610
|
-
}
|
|
611
|
-
} else {
|
|
612
|
-
const apps = await this.repository.listApps();
|
|
613
|
-
for (const app of apps) {
|
|
614
|
-
const cnsIds = await this.repository.getCnsByApp(
|
|
615
|
-
app.appId
|
|
616
|
-
);
|
|
617
|
-
for (const cnsId of cnsIds) {
|
|
618
|
-
const responses = await this.repository.getResponsesByCns(cnsId, {
|
|
619
|
-
fromTimestamp,
|
|
620
|
-
toTimestamp,
|
|
621
|
-
hasError,
|
|
622
|
-
errorContains,
|
|
623
|
-
neuronId,
|
|
624
|
-
collateralName
|
|
625
|
-
});
|
|
626
|
-
allResponses = allResponses.concat(responses);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
allResponses.sort((a, b) => b.timestamp - a.timestamp);
|
|
631
|
-
const response = {
|
|
632
|
-
type: "apps:responses",
|
|
633
|
-
appId: appId || "all",
|
|
634
|
-
responses: allResponses,
|
|
635
|
-
total: allResponses.length,
|
|
636
|
-
offset: offset || 0,
|
|
637
|
-
limit: limit || allResponses.length
|
|
638
|
-
};
|
|
639
|
-
ws.send(JSON.stringify(response));
|
|
640
|
-
return response;
|
|
641
|
-
}
|
|
642
|
-
case "cns:get-responses": {
|
|
643
|
-
const msg = message;
|
|
644
|
-
const {
|
|
645
|
-
cnsId,
|
|
646
|
-
limit,
|
|
647
|
-
fromTimestamp,
|
|
648
|
-
toTimestamp,
|
|
649
|
-
offset,
|
|
650
|
-
hasError,
|
|
651
|
-
errorContains,
|
|
652
|
-
neuronId,
|
|
653
|
-
collateralName
|
|
654
|
-
} = msg;
|
|
655
|
-
if (!cnsId) {
|
|
656
|
-
return { type: "cns:responses", cnsId: "", responses: [] };
|
|
657
|
-
}
|
|
658
|
-
const responses = await this.repository.getResponsesByCns(
|
|
659
|
-
cnsId,
|
|
660
|
-
{
|
|
661
|
-
fromTimestamp,
|
|
662
|
-
toTimestamp,
|
|
663
|
-
limit,
|
|
664
|
-
offset,
|
|
665
|
-
hasError,
|
|
666
|
-
errorContains,
|
|
667
|
-
neuronId,
|
|
668
|
-
collateralName
|
|
669
|
-
}
|
|
670
|
-
);
|
|
671
|
-
return { type: "cns:responses", cnsId, responses };
|
|
672
|
-
}
|
|
673
|
-
default:
|
|
674
|
-
this.log("Unknown message type:", message.type);
|
|
675
|
-
return null;
|
|
74
|
+
// ─── App batch item handlers ──────────────────────────────────────────────
|
|
75
|
+
async handleAppBatchItem(ws, item) {
|
|
76
|
+
switch (item.type) {
|
|
77
|
+
case "topology":
|
|
78
|
+
await this.handleTopology(ws, item);
|
|
79
|
+
break;
|
|
80
|
+
case "execution.started":
|
|
81
|
+
await this.handleExecutionStarted(ws, item.execution);
|
|
82
|
+
break;
|
|
83
|
+
case "execution.hop":
|
|
84
|
+
await this.handleExecutionHop(item.hop);
|
|
85
|
+
break;
|
|
86
|
+
case "execution.completed":
|
|
87
|
+
await this.handleExecutionCompleted(item);
|
|
88
|
+
break;
|
|
676
89
|
}
|
|
677
90
|
}
|
|
678
|
-
async
|
|
679
|
-
const
|
|
680
|
-
const cnsId = message.cnsId || message.devToolsInstanceId;
|
|
681
|
-
if (!baseAppId || !cnsId) {
|
|
682
|
-
throw new Error("Invalid InitMessage: missing appId or cnsId");
|
|
683
|
-
}
|
|
684
|
-
this.lastInitByApp.set(cnsId, message);
|
|
91
|
+
async handleTopology(ws, msg) {
|
|
92
|
+
const { appId, cnsId, appName, version, timestamp, neurons, collaterals, dendrites } = msg;
|
|
685
93
|
const now = Date.now();
|
|
686
|
-
if (!this.appFirstSeenAt.has(baseAppId)) {
|
|
687
|
-
this.appFirstSeenAt.set(baseAppId, now);
|
|
688
|
-
}
|
|
689
|
-
this.appLastSeenAt.set(baseAppId, now);
|
|
690
94
|
const app = {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
version
|
|
694
|
-
|
|
95
|
+
id: appId,
|
|
96
|
+
name: appName,
|
|
97
|
+
version,
|
|
98
|
+
connectedAt: now,
|
|
695
99
|
lastSeenAt: now
|
|
696
100
|
};
|
|
697
101
|
await this.repository.upsertApp(app);
|
|
698
|
-
this.
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
);
|
|
703
|
-
const
|
|
704
|
-
|
|
102
|
+
await this.repository.addCnsToApp(appId, cnsId);
|
|
103
|
+
await this.repository.saveTopology({ appId, cnsId, appName, version, timestamp, neurons, collaterals, dendrites });
|
|
104
|
+
this.appSockets.set(ws, appId);
|
|
105
|
+
if (!this.appWsByAppId.has(appId)) this.appWsByAppId.set(appId, /* @__PURE__ */ new Set());
|
|
106
|
+
this.appWsByAppId.get(appId).add(ws);
|
|
107
|
+
const topologyBroadcast = {
|
|
108
|
+
type: "topology.result",
|
|
109
|
+
requestId: "__broadcast__",
|
|
110
|
+
snapshots: await this.repository.getTopology(cnsId)
|
|
111
|
+
};
|
|
112
|
+
this.broadcast({ type: "app.connected", app, topology: { cnsId, neurons, collaterals, dendrites } });
|
|
113
|
+
this.broadcastRaw(JSON.stringify(topologyBroadcast));
|
|
114
|
+
}
|
|
115
|
+
async handleExecutionStarted(ws, execution) {
|
|
116
|
+
const appId = this.appSockets.get(ws);
|
|
117
|
+
if (appId) {
|
|
118
|
+
const apps = await this.repository.listApps();
|
|
119
|
+
const app = apps.find((a) => a.id === appId);
|
|
120
|
+
if (app) await this.repository.upsertApp({ ...app, lastSeenAt: Date.now() });
|
|
121
|
+
}
|
|
122
|
+
await this.repository.saveExecution(execution);
|
|
123
|
+
this.broadcast({ type: "execution.started", execution });
|
|
124
|
+
}
|
|
125
|
+
async handleExecutionHop(hop) {
|
|
126
|
+
await this.repository.saveHop(hop);
|
|
127
|
+
this.broadcast({ type: "execution.hop", hop });
|
|
128
|
+
}
|
|
129
|
+
async handleExecutionCompleted(item) {
|
|
130
|
+
await this.repository.completeExecution(
|
|
131
|
+
item.executionId,
|
|
132
|
+
item.completedAt,
|
|
133
|
+
item.hopCount,
|
|
134
|
+
item.hasError
|
|
705
135
|
);
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
if (this.repository.removeCollateral) {
|
|
713
|
-
await this.repository.removeCollateral(collateral.id);
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
for (const dendrite of existingDendrites) {
|
|
717
|
-
if (this.repository.removeDendrite) {
|
|
718
|
-
await this.repository.removeDendrite(dendrite.id);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
for (const neuron of message.neurons || []) {
|
|
722
|
-
await this.repository.upsertNeuron(neuron);
|
|
723
|
-
}
|
|
724
|
-
for (const collateral of message.collaterals || []) {
|
|
725
|
-
await this.repository.upsertCollateral(collateral);
|
|
726
|
-
}
|
|
727
|
-
for (const dendrite of message.dendrites || []) {
|
|
728
|
-
await this.repository.upsertDendrite(dendrite);
|
|
729
|
-
}
|
|
730
|
-
await this.repository.addCnsToApp(app.appId, cnsId);
|
|
731
|
-
this.log(`App connected: ${message.appName} (${message.devToolsInstanceId})`);
|
|
732
|
-
this.log(`Topology: ${message.neurons.length} neurons, ${message.collaterals.length} collaterals, ${message.dendrites.length} dendrites`);
|
|
733
|
-
const apps = await this.repository.listApps();
|
|
734
|
-
this.broadcastToClients({
|
|
735
|
-
type: "app:added",
|
|
736
|
-
app
|
|
136
|
+
this.broadcast({
|
|
137
|
+
type: "execution.completed",
|
|
138
|
+
executionId: item.executionId,
|
|
139
|
+
completedAt: item.completedAt,
|
|
140
|
+
hopCount: item.hopCount,
|
|
141
|
+
hasError: item.hasError
|
|
737
142
|
});
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
this.
|
|
743
|
-
|
|
744
|
-
|
|
143
|
+
}
|
|
144
|
+
// ─── UI client query handlers ─────────────────────────────────────────────
|
|
145
|
+
async handleClientConnect(ws) {
|
|
146
|
+
this.addClient(ws);
|
|
147
|
+
this.send(ws, { type: "apps.result", requestId: "__init__", items: await this.repository.listApps() });
|
|
148
|
+
this.send(ws, {
|
|
149
|
+
type: "topology.result",
|
|
150
|
+
requestId: "__init__",
|
|
151
|
+
snapshots: await this.repository.getTopology()
|
|
745
152
|
});
|
|
746
|
-
return {
|
|
747
|
-
type: "apps:active",
|
|
748
|
-
apps
|
|
749
|
-
};
|
|
750
153
|
}
|
|
751
|
-
async
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
154
|
+
async handleAppsQuery(ws, requestId) {
|
|
155
|
+
const items = await this.repository.listApps();
|
|
156
|
+
this.send(ws, { type: "apps.result", requestId, items });
|
|
157
|
+
}
|
|
158
|
+
async handleTopologyQuery(ws, requestId, cnsId, appId) {
|
|
159
|
+
let snapshots = await this.repository.getTopology(cnsId);
|
|
160
|
+
if (appId) snapshots = snapshots.filter((s) => s.appId === appId);
|
|
161
|
+
this.send(ws, { type: "topology.result", requestId, snapshots });
|
|
162
|
+
}
|
|
163
|
+
async handleExecutionsQuery(ws, msg) {
|
|
164
|
+
const appId = msg.appId ?? (msg.cnsId ? await this.repository.findAppByCns(msg.cnsId) : void 0);
|
|
165
|
+
if (!appId) {
|
|
166
|
+
this.send(ws, { type: "executions.result", requestId: msg.requestId, items: [], total: 0, offset: 0 });
|
|
757
167
|
return;
|
|
758
168
|
}
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
type: actualMessage?.type
|
|
767
|
-
});
|
|
768
|
-
await this.handleMessage(ws, actualMessage);
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
if (responses.length > 0) {
|
|
772
|
-
this.log("Creating response-batch with", responses.length, "responses");
|
|
773
|
-
const responseBatch = {
|
|
774
|
-
type: "response-batch",
|
|
775
|
-
devToolsInstanceId: responses[0].cnsId || responses[0].appId || "unknown",
|
|
776
|
-
responses
|
|
777
|
-
};
|
|
778
|
-
await this.handleResponseBatch(
|
|
779
|
-
ws,
|
|
780
|
-
responseBatch
|
|
781
|
-
);
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
async handleResponseBatch(ws, message) {
|
|
785
|
-
const replayResponses = (message.responses || []).filter((r) => {
|
|
786
|
-
const stimId = r.stimulationId || "";
|
|
787
|
-
return typeof stimId === "string" && stimId.includes("-replay-");
|
|
169
|
+
const { items, total } = await this.repository.getExecutions(appId, msg.filter ?? {});
|
|
170
|
+
this.send(ws, {
|
|
171
|
+
type: "executions.result",
|
|
172
|
+
requestId: msg.requestId,
|
|
173
|
+
items,
|
|
174
|
+
total,
|
|
175
|
+
offset: msg.filter?.offset ?? 0
|
|
788
176
|
});
|
|
789
|
-
if (replayResponses.length > 0) {
|
|
790
|
-
this.log("[Server] Received REPLAY response-batch:", {
|
|
791
|
-
totalResponses: message.responses?.length || 0,
|
|
792
|
-
replayResponsesCount: replayResponses.length,
|
|
793
|
-
replayStimIds: replayResponses.slice(0, 3).map((r) => r.stimulationId),
|
|
794
|
-
replayAppIds: replayResponses.slice(0, 3).map((r) => r.appId)
|
|
795
|
-
});
|
|
796
|
-
}
|
|
797
|
-
if (message.responses && message.responses.length > 0) {
|
|
798
|
-
const appId = message.responses[0].appId;
|
|
799
|
-
if (appId) {
|
|
800
|
-
this.appLastSeenAt.set(appId, Date.now());
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
for (const response of message.responses) {
|
|
804
|
-
await this.repository.saveResponse(response);
|
|
805
|
-
}
|
|
806
|
-
this.broadcastToClients(message);
|
|
807
|
-
return message;
|
|
808
177
|
}
|
|
809
|
-
async
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
|
|
178
|
+
async handleHopsQuery(ws, msg) {
|
|
179
|
+
const items = await this.repository.getHops(msg.executionId);
|
|
180
|
+
this.send(ws, { type: "hops.result", requestId: msg.requestId, items });
|
|
181
|
+
}
|
|
182
|
+
async handleReplayStart(ws, msg) {
|
|
183
|
+
const appId = msg.appId ?? (msg.cnsId ? await this.repository.findAppByCns(msg.cnsId) : void 0);
|
|
184
|
+
const appSockets = appId ? this.appWsByAppId.get(appId) : void 0;
|
|
185
|
+
if (!appSockets || appSockets.size === 0) {
|
|
186
|
+
this.send(ws, { type: "replay.rejected", replayId: msg.replayId, reason: "App not connected" });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const newExecutionId = `${msg.executionId}-replay-${Date.now()}`;
|
|
190
|
+
this.send(ws, { type: "replay.accepted", replayId: msg.replayId, newExecutionId });
|
|
191
|
+
const payload = JSON.stringify(msg);
|
|
192
|
+
for (const appWs of appSockets) {
|
|
813
193
|
try {
|
|
814
|
-
|
|
815
|
-
stimulationCommandId: message.stimulationCommandId,
|
|
816
|
-
appId: message.appId,
|
|
817
|
-
cnsId: message.cnsId,
|
|
818
|
-
collateralName: message.collateralName,
|
|
819
|
-
payload: message.payload,
|
|
820
|
-
contexts: message.contexts,
|
|
821
|
-
options: message.options,
|
|
822
|
-
timestamp: Date.now()
|
|
823
|
-
};
|
|
824
|
-
const arr = this.replaysByApp.get(entry.appId) || [];
|
|
825
|
-
arr.push(entry);
|
|
826
|
-
if (arr.length > 1e3) arr.splice(0, arr.length - 1e3);
|
|
827
|
-
this.replaysByApp.set(entry.appId, arr);
|
|
194
|
+
appWs.send(payload);
|
|
828
195
|
} catch {
|
|
829
196
|
}
|
|
830
|
-
if (cnsIdFromCmd) {
|
|
831
|
-
const appWs = this.appSockets.get(cnsIdFromCmd);
|
|
832
|
-
if (appWs && appWs.readyState === 1) {
|
|
833
|
-
appWs.send(JSON.stringify(message));
|
|
834
|
-
this.log(`Stimulate forwarded to cns ${cnsIdFromCmd}`);
|
|
835
|
-
} else {
|
|
836
|
-
if (this.options.consoleLogEnabled) {
|
|
837
|
-
console.warn(`CNS socket not available for ${cnsIdFromCmd}`);
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
} else if (appIdFromCmd) {
|
|
841
|
-
const cnsIds = await this.repository.getCnsByApp(appIdFromCmd);
|
|
842
|
-
let count = 0;
|
|
843
|
-
for (const cnsId of cnsIds) {
|
|
844
|
-
const appWs = this.appSockets.get(cnsId);
|
|
845
|
-
if (appWs && appWs.readyState === 1) {
|
|
846
|
-
appWs.send(JSON.stringify(message));
|
|
847
|
-
count++;
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
this.log(`Stimulate forwarded to ${count} CNS of app ${appIdFromCmd}`);
|
|
851
|
-
} else {
|
|
852
|
-
let count = 0;
|
|
853
|
-
for (const [, appWs] of Array.from(this.appSockets.entries())) {
|
|
854
|
-
if (appWs.readyState === 1) {
|
|
855
|
-
appWs.send(JSON.stringify(message));
|
|
856
|
-
count++;
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
} catch (e) {
|
|
861
|
-
if (this.options.consoleLogEnabled) {
|
|
862
|
-
console.error("Failed to forward stimulate:", e);
|
|
863
|
-
}
|
|
864
197
|
}
|
|
865
|
-
return null;
|
|
866
198
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
this.appSockets.delete(cnsId);
|
|
871
|
-
this.log(`CNS disconnected: ${cnsId}`);
|
|
872
|
-
const sepIdx = cnsId.indexOf(":");
|
|
873
|
-
const baseAppId = sepIdx > 0 ? cnsId.slice(0, sepIdx) : cnsId;
|
|
874
|
-
this.lastInitByApp.delete(cnsId);
|
|
875
|
-
await this.repository.removeCnsFromApp(baseAppId, cnsId);
|
|
876
|
-
break;
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
this.clientSockets.delete(ws);
|
|
880
|
-
}
|
|
881
|
-
addClient(ws) {
|
|
882
|
-
this.clientSockets.add(ws);
|
|
883
|
-
if (!this.metricsTimer) {
|
|
884
|
-
this.startMetricsLoop();
|
|
885
|
-
}
|
|
886
|
-
const eventedWs = ws;
|
|
887
|
-
const remove = () => this.removeClient(ws);
|
|
888
|
-
if (eventedWs.on) {
|
|
889
|
-
eventedWs.on("close", remove);
|
|
890
|
-
eventedWs.on("error", remove);
|
|
891
|
-
} else if (eventedWs.addEventListener) {
|
|
892
|
-
eventedWs.addEventListener("close", remove);
|
|
893
|
-
eventedWs.addEventListener("error", remove);
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
removeClient(ws) {
|
|
897
|
-
this.clientSockets.delete(ws);
|
|
199
|
+
// ─── Broadcast helpers ────────────────────────────────────────────────────
|
|
200
|
+
broadcast(message) {
|
|
201
|
+
this.broadcastRaw(JSON.stringify(message));
|
|
898
202
|
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
stimulations: [message]
|
|
905
|
-
});
|
|
906
|
-
}
|
|
907
|
-
broadcastToClients(message) {
|
|
908
|
-
const messageStr = JSON.stringify(message);
|
|
909
|
-
let sentCount = 0;
|
|
910
|
-
this.clientSockets.forEach((clientWs) => {
|
|
911
|
-
if (clientWs.readyState === 1) {
|
|
912
|
-
try {
|
|
913
|
-
clientWs.send(messageStr);
|
|
914
|
-
sentCount++;
|
|
915
|
-
} catch (error) {
|
|
916
|
-
if (this.options.consoleLogEnabled) {
|
|
917
|
-
console.error("Failed to send message to client:", error);
|
|
918
|
-
}
|
|
919
|
-
}
|
|
203
|
+
broadcastRaw(payload) {
|
|
204
|
+
for (const client of this.uiClients) {
|
|
205
|
+
try {
|
|
206
|
+
if (client.readyState === 1) client.send(payload);
|
|
207
|
+
} catch {
|
|
920
208
|
}
|
|
921
|
-
});
|
|
922
|
-
if (sentCount !== this.clientSockets.size && this.options.consoleLogEnabled) {
|
|
923
|
-
console.warn(
|
|
924
|
-
`Sent message to ${sentCount}/${this.clientSockets.size} clients`
|
|
925
|
-
);
|
|
926
209
|
}
|
|
927
210
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
211
|
+
send(ws, message) {
|
|
212
|
+
try {
|
|
213
|
+
if (ws.readyState === 1) ws.send(JSON.stringify(message));
|
|
214
|
+
} catch {
|
|
931
215
|
}
|
|
932
|
-
|
|
933
|
-
|
|
216
|
+
}
|
|
217
|
+
// ─── Metrics ──────────────────────────────────────────────────────────────
|
|
218
|
+
startMetrics() {
|
|
219
|
+
if (this.metricsTimer) return;
|
|
934
220
|
this.metricsTimer = setInterval(() => {
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
);
|
|
951
|
-
this.lastCpuUsage = process.cpuUsage();
|
|
952
|
-
this.lastCpuTime = now;
|
|
953
|
-
const payload = {
|
|
954
|
-
type: "server:metrics",
|
|
955
|
-
timestamp: now,
|
|
956
|
-
rssMB: Math.round(mem.rss / 1024 / 1024 * 100) / 100,
|
|
957
|
-
heapUsedMB: Math.round(mem.heapUsed / 1024 / 1024 * 100) / 100,
|
|
958
|
-
heapTotalMB: Math.round(mem.heapTotal / 1024 / 1024 * 100) / 100,
|
|
959
|
-
externalMB: Math.round(mem.external / 1024 / 1024 * 100) / 100,
|
|
960
|
-
cpuPercent
|
|
961
|
-
};
|
|
962
|
-
this.broadcastToClients(payload);
|
|
963
|
-
} catch {
|
|
964
|
-
}
|
|
221
|
+
const mem = process.memoryUsage();
|
|
222
|
+
const now = Date.now();
|
|
223
|
+
const cpu = process.cpuUsage(this.lastCpuUsage);
|
|
224
|
+
const elapsed = (now - this.lastCpuTime) * 1e3;
|
|
225
|
+
const cpuPercent = elapsed > 0 ? (cpu.user + cpu.system) / elapsed * 100 : 0;
|
|
226
|
+
this.lastCpuUsage = process.cpuUsage();
|
|
227
|
+
this.lastCpuTime = now;
|
|
228
|
+
this.broadcast({
|
|
229
|
+
type: "server.metrics",
|
|
230
|
+
timestamp: now,
|
|
231
|
+
rssMB: mem.rss / 1024 / 1024,
|
|
232
|
+
heapUsedMB: mem.heapUsed / 1024 / 1024,
|
|
233
|
+
heapTotalMB: mem.heapTotal / 1024 / 1024,
|
|
234
|
+
cpuPercent
|
|
235
|
+
});
|
|
965
236
|
}, 1e3);
|
|
966
237
|
}
|
|
967
|
-
|
|
968
|
-
return await this.repository.listApps();
|
|
969
|
-
}
|
|
970
|
-
stop() {
|
|
971
|
-
this.log("Stopping server.");
|
|
972
|
-
this.stopped = true;
|
|
238
|
+
stopMetrics() {
|
|
973
239
|
if (this.metricsTimer) {
|
|
974
240
|
clearInterval(this.metricsTimer);
|
|
975
241
|
this.metricsTimer = void 0;
|
|
976
242
|
}
|
|
977
|
-
for (const ws of Array.from(this.appSockets.values())) {
|
|
978
|
-
ws.close();
|
|
979
|
-
}
|
|
980
|
-
this.appSockets.clear();
|
|
981
|
-
for (const ws of Array.from(this.clientSockets)) {
|
|
982
|
-
ws.close();
|
|
983
|
-
}
|
|
984
|
-
this.clientSockets.clear();
|
|
985
|
-
this.lastInitByApp.clear();
|
|
986
|
-
this.stimulationBuffer = [];
|
|
987
|
-
this.appFirstSeenAt.clear();
|
|
988
|
-
this.appLastSeenAt.clear();
|
|
989
|
-
this.replaysByApp.clear();
|
|
990
243
|
}
|
|
991
244
|
};
|
|
992
245
|
|
|
993
|
-
export { CNSDevToolsServer
|
|
246
|
+
export { CNSDevToolsServer };
|
|
994
247
|
//# sourceMappingURL=index.js.map
|
|
995
248
|
//# sourceMappingURL=index.js.map
|