@cnstra/devtools-server 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/index.cjs +998 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +995 -0
- package/dist/index.js.map +1 -0
- package/dist/start.d.ts +31 -0
- package/dist/start.d.ts.map +1 -0
- package/package.json +71 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,998 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var http = require('http');
|
|
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
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/index.ts
|
|
151
|
+
var CNSDevToolsServer = class _CNSDevToolsServer {
|
|
152
|
+
constructor(repository, options = {}) {
|
|
153
|
+
this.repository = repository;
|
|
154
|
+
this.options = options;
|
|
155
|
+
this.stopped = false;
|
|
156
|
+
this.appSockets = /* @__PURE__ */ new Map();
|
|
157
|
+
this.clientSockets = /* @__PURE__ */ new Set();
|
|
158
|
+
this.lastInitByApp = /* @__PURE__ */ new Map();
|
|
159
|
+
this.stimulationBuffer = [];
|
|
160
|
+
// Track app connection times
|
|
161
|
+
this.appFirstSeenAt = /* @__PURE__ */ new Map();
|
|
162
|
+
this.appLastSeenAt = /* @__PURE__ */ new Map();
|
|
163
|
+
this.replaysByApp = /* @__PURE__ */ new Map();
|
|
164
|
+
// server metrics sampling
|
|
165
|
+
this.lastCpuUsage = process.cpuUsage();
|
|
166
|
+
this.lastCpuTime = Date.now();
|
|
167
|
+
}
|
|
168
|
+
static sanitizeValue(value, depth = 0, limits = {
|
|
169
|
+
maxDepth: 5,
|
|
170
|
+
maxString: 2e3,
|
|
171
|
+
maxArray: 200,
|
|
172
|
+
maxObjectKeys: 200
|
|
173
|
+
}) {
|
|
174
|
+
if (value === null || value === void 0) return value;
|
|
175
|
+
if (depth >= limits.maxDepth) return "[MaxDepth]";
|
|
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;
|
|
200
|
+
}
|
|
201
|
+
if (t === "object") {
|
|
202
|
+
const out = {};
|
|
203
|
+
const entries = Object.entries(value);
|
|
204
|
+
const slice = entries.slice(0, limits.maxObjectKeys);
|
|
205
|
+
for (const [k, v] of slice) {
|
|
206
|
+
out[k] = this.sanitizeValue(v, depth + 1, limits);
|
|
207
|
+
}
|
|
208
|
+
if (entries.length > slice.length) {
|
|
209
|
+
out["\u2026"] = `+${entries.length - slice.length} keys`;
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
return value;
|
|
214
|
+
}
|
|
215
|
+
log(...args) {
|
|
216
|
+
if (this.options.consoleLogEnabled) {
|
|
217
|
+
console.log(...args);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
isNeuronResponseMessage(message) {
|
|
221
|
+
return message !== null && typeof message === "object" && "stimulationId" in message && "neuronId" in message && "appId" in message && "collateralName" in message && "timestamp" in message && typeof message.stimulationId === "string" && typeof message.neuronId === "string" && typeof message.appId === "string" && typeof message.collateralName === "string" && typeof message.timestamp === "number" && !("type" in message);
|
|
222
|
+
}
|
|
223
|
+
isMessageWithType(message) {
|
|
224
|
+
return message !== null && typeof message === "object" && "type" in message && typeof message.type === "string";
|
|
225
|
+
}
|
|
226
|
+
// Expose sockets map size for tests
|
|
227
|
+
/** @internal */
|
|
228
|
+
get connectedAppCount() {
|
|
229
|
+
return this.appSockets.size;
|
|
230
|
+
}
|
|
231
|
+
static applyWindowAndPaginate(items, params) {
|
|
232
|
+
const { fromTimestamp, toTimestamp } = params;
|
|
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;
|
|
245
|
+
}
|
|
246
|
+
async handleMessage(ws, message) {
|
|
247
|
+
if (!this.isMessageWithType(message)) {
|
|
248
|
+
if (this.options.consoleLogEnabled) {
|
|
249
|
+
console.warn("Unknown message format:", message);
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
switch (message.type) {
|
|
254
|
+
case "devtools-client-connect":
|
|
255
|
+
this.log("DevTools client connecting...");
|
|
256
|
+
this.addClient(ws);
|
|
257
|
+
if (!this.metricsTimer) {
|
|
258
|
+
this.startMetricsLoop();
|
|
259
|
+
}
|
|
260
|
+
ws.send(JSON.stringify({ type: "devtools-client-connected" }));
|
|
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;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async handleInit(ws, message) {
|
|
681
|
+
const baseAppId = message.appId || message.devToolsInstanceId;
|
|
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);
|
|
687
|
+
const now = Date.now();
|
|
688
|
+
if (!this.appFirstSeenAt.has(baseAppId)) {
|
|
689
|
+
this.appFirstSeenAt.set(baseAppId, now);
|
|
690
|
+
}
|
|
691
|
+
this.appLastSeenAt.set(baseAppId, now);
|
|
692
|
+
const app = {
|
|
693
|
+
appId: baseAppId,
|
|
694
|
+
appName: message.appName,
|
|
695
|
+
version: message.version,
|
|
696
|
+
firstSeenAt: this.appFirstSeenAt.get(baseAppId),
|
|
697
|
+
lastSeenAt: now
|
|
698
|
+
};
|
|
699
|
+
await this.repository.upsertApp(app);
|
|
700
|
+
this.appSockets.set(cnsId, ws);
|
|
701
|
+
const existingNeurons = await this.repository.getNeuronsByCns(cnsId);
|
|
702
|
+
const existingCollaterals = await this.repository.getCollateralsByCns(
|
|
703
|
+
cnsId
|
|
704
|
+
);
|
|
705
|
+
const existingDendrites = await this.repository.getDendritesByCns(
|
|
706
|
+
cnsId
|
|
707
|
+
);
|
|
708
|
+
for (const neuron of existingNeurons) {
|
|
709
|
+
if (this.repository.removeNeuron) {
|
|
710
|
+
await this.repository.removeNeuron(neuron.id);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
for (const collateral of existingCollaterals) {
|
|
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
|
|
739
|
+
});
|
|
740
|
+
this.log("Broadcasting apps:active to", this.clientSockets.size, "clients:", {
|
|
741
|
+
appsCount: apps.length,
|
|
742
|
+
appIds: apps.map((a) => a.appId)
|
|
743
|
+
});
|
|
744
|
+
this.broadcastToClients({
|
|
745
|
+
type: "apps:active",
|
|
746
|
+
apps
|
|
747
|
+
});
|
|
748
|
+
return {
|
|
749
|
+
type: "apps:active",
|
|
750
|
+
apps
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
async handleBatch(ws, message) {
|
|
754
|
+
this.log("Server handling batch with", message.items?.length || 0, "items");
|
|
755
|
+
if (!Array.isArray(message.items)) {
|
|
756
|
+
if (this.options.consoleLogEnabled) {
|
|
757
|
+
console.warn("Invalid batch message - no items array");
|
|
758
|
+
}
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const responses = [];
|
|
762
|
+
for (const item of message.items || []) {
|
|
763
|
+
const actualMessage = item.payload || item;
|
|
764
|
+
if (item.type === "response") {
|
|
765
|
+
responses.push(actualMessage);
|
|
766
|
+
} else {
|
|
767
|
+
this.log("Batch item (non-response):", {
|
|
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-");
|
|
790
|
+
});
|
|
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
|
+
}
|
|
811
|
+
async handleStimulate(ws, message) {
|
|
812
|
+
const cnsIdFromCmd = message.cnsId;
|
|
813
|
+
const appIdFromCmd = message.appId;
|
|
814
|
+
try {
|
|
815
|
+
try {
|
|
816
|
+
const entry = {
|
|
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);
|
|
830
|
+
} catch {
|
|
831
|
+
}
|
|
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
|
+
}
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
async handleDisconnect(ws) {
|
|
870
|
+
for (const [cnsId, socket] of Array.from(this.appSockets.entries())) {
|
|
871
|
+
if (socket === ws) {
|
|
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);
|
|
900
|
+
}
|
|
901
|
+
async handleStimulation(ws, message) {
|
|
902
|
+
await this.repository.saveStimulation(message);
|
|
903
|
+
this.stimulationBuffer.push(message);
|
|
904
|
+
this.broadcastToClients({
|
|
905
|
+
type: "stimulation-batch",
|
|
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
|
+
}
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
if (sentCount !== this.clientSockets.size && this.options.consoleLogEnabled) {
|
|
925
|
+
console.warn(
|
|
926
|
+
`Sent message to ${sentCount}/${this.clientSockets.size} clients`
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
startMetricsLoop() {
|
|
931
|
+
if (this.metricsTimer) {
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
this.lastCpuUsage = process.cpuUsage();
|
|
935
|
+
this.lastCpuTime = Date.now();
|
|
936
|
+
this.metricsTimer = setInterval(() => {
|
|
937
|
+
if (this.stopped) {
|
|
938
|
+
clearInterval(this.metricsTimer);
|
|
939
|
+
this.metricsTimer = void 0;
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
try {
|
|
943
|
+
const mem = process.memoryUsage();
|
|
944
|
+
const now = Date.now();
|
|
945
|
+
const cpu = process.cpuUsage(this.lastCpuUsage);
|
|
946
|
+
const elapsedMs = Math.max(1, now - this.lastCpuTime);
|
|
947
|
+
const userMs = cpu.user / 1e3;
|
|
948
|
+
const systemMs = cpu.system / 1e3;
|
|
949
|
+
const cpuPercent = Math.min(
|
|
950
|
+
100,
|
|
951
|
+
Math.round((userMs + systemMs) / elapsedMs * 100)
|
|
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
|
+
}
|
|
967
|
+
}, 1e3);
|
|
968
|
+
}
|
|
969
|
+
async getActiveApps() {
|
|
970
|
+
return await this.repository.listApps();
|
|
971
|
+
}
|
|
972
|
+
stop() {
|
|
973
|
+
this.log("Stopping server.");
|
|
974
|
+
this.stopped = true;
|
|
975
|
+
if (this.metricsTimer) {
|
|
976
|
+
clearInterval(this.metricsTimer);
|
|
977
|
+
this.metricsTimer = void 0;
|
|
978
|
+
}
|
|
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
|
+
}
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
exports.CNSDevToolsServer = CNSDevToolsServer;
|
|
996
|
+
exports.startDevTools = startDevTools;
|
|
997
|
+
//# sourceMappingURL=index.cjs.map
|
|
998
|
+
//# sourceMappingURL=index.cjs.map
|