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