@cnstra/devtools-server 1.1.1 → 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.js CHANGED
@@ -1,995 +1,248 @@
1
- import { createServer } from 'http';
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 _CNSDevToolsServer {
150
- constructor(repository, options = {}) {
4
+ var CNSDevToolsServer = class {
5
+ constructor(repository) {
151
6
  this.repository = repository;
152
- this.options = options;
153
- this.stopped = false;
7
+ /** WebSocket → appId (for app clients) */
154
8
  this.appSockets = /* @__PURE__ */ new Map();
155
- this.clientSockets = /* @__PURE__ */ new Set();
156
- this.lastInitByApp = /* @__PURE__ */ new Map();
157
- this.stimulationBuffer = [];
158
- // Track app connection times
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
- static sanitizeValue(value, depth = 0, limits = {
167
- maxDepth: 5,
168
- maxString: 2e3,
169
- maxArray: 200,
170
- maxObjectKeys: 200
171
- }) {
172
- if (value === null || value === void 0) return value;
173
- if (depth >= limits.maxDepth) return "[MaxDepth]";
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 (t === "object") {
200
- const out = {};
201
- const entries = Object.entries(value);
202
- const slice = entries.slice(0, limits.maxObjectKeys);
203
- for (const [k, v] of slice) {
204
- out[k] = this.sanitizeValue(v, depth + 1, limits);
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
- if (entries.length > slice.length) {
207
- out["\u2026"] = `+${entries.length - slice.length} keys`;
208
- }
209
- return out;
33
+ return;
210
34
  }
211
- return value;
212
- }
213
- log(...args) {
214
- if (this.options.consoleLogEnabled) {
215
- console.log(...args);
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
- isNeuronResponseMessage(message) {
219
- 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);
59
+ addClient(ws) {
60
+ this.uiClients.add(ws);
61
+ if (this.uiClients.size === 1) this.startMetrics();
220
62
  }
221
- isMessageWithType(message) {
222
- return message !== null && typeof message === "object" && "type" in message && typeof message.type === "string";
63
+ removeClient(ws) {
64
+ this.uiClients.delete(ws);
65
+ this.appSockets.delete(ws);
66
+ if (this.uiClients.size === 0) this.stopMetrics();
223
67
  }
224
- // Expose sockets map size for tests
225
- /** @internal */
226
- get connectedAppCount() {
227
- return this.appSockets.size;
68
+ async getActiveApps() {
69
+ return this.repository.listApps();
228
70
  }
229
- static applyWindowAndPaginate(items, params) {
230
- const { fromTimestamp, toTimestamp } = params;
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
- async handleMessage(ws, message) {
245
- if (!this.isMessageWithType(message)) {
246
- if (this.options.consoleLogEnabled) {
247
- console.warn("Unknown message format:", message);
248
- }
249
- return null;
250
- }
251
- switch (message.type) {
252
- case "devtools-client-connect":
253
- this.log("DevTools client connecting...");
254
- this.addClient(ws);
255
- if (!this.metricsTimer) {
256
- this.startMetricsLoop();
257
- }
258
- ws.send(JSON.stringify({ type: "devtools-client-connected" }));
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 handleInit(ws, message) {
679
- const baseAppId = message.appId || message.devToolsInstanceId;
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
- appId: baseAppId,
692
- appName: message.appName,
693
- version: message.version,
694
- firstSeenAt: this.appFirstSeenAt.get(baseAppId),
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.appSockets.set(cnsId, ws);
699
- const existingNeurons = await this.repository.getNeuronsByCns(cnsId);
700
- const existingCollaterals = await this.repository.getCollateralsByCns(
701
- cnsId
702
- );
703
- const existingDendrites = await this.repository.getDendritesByCns(
704
- cnsId
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
- for (const neuron of existingNeurons) {
707
- if (this.repository.removeNeuron) {
708
- await this.repository.removeNeuron(neuron.id);
709
- }
710
- }
711
- for (const collateral of existingCollaterals) {
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
- this.log("Broadcasting apps:active to", this.clientSockets.size, "clients:", {
739
- appsCount: apps.length,
740
- appIds: apps.map((a) => a.appId)
741
- });
742
- this.broadcastToClients({
743
- type: "apps:active",
744
- apps
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 handleBatch(ws, message) {
752
- this.log("Server handling batch with", message.items?.length || 0, "items");
753
- if (!Array.isArray(message.items)) {
754
- if (this.options.consoleLogEnabled) {
755
- console.warn("Invalid batch message - no items array");
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 responses = [];
760
- for (const item of message.items || []) {
761
- const actualMessage = item.payload || item;
762
- if (item.type === "response") {
763
- responses.push(actualMessage);
764
- } else {
765
- this.log("Batch item (non-response):", {
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 handleStimulate(ws, message) {
810
- const cnsIdFromCmd = message.cnsId;
811
- const appIdFromCmd = message.appId;
812
- try {
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
- const entry = {
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
- async handleDisconnect(ws) {
868
- for (const [cnsId, socket] of Array.from(this.appSockets.entries())) {
869
- if (socket === ws) {
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
- async handleStimulation(ws, message) {
900
- await this.repository.saveStimulation(message);
901
- this.stimulationBuffer.push(message);
902
- this.broadcastToClients({
903
- type: "stimulation-batch",
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
- startMetricsLoop() {
929
- if (this.metricsTimer) {
930
- return;
211
+ send(ws, message) {
212
+ try {
213
+ if (ws.readyState === 1) ws.send(JSON.stringify(message));
214
+ } catch {
931
215
  }
932
- this.lastCpuUsage = process.cpuUsage();
933
- this.lastCpuTime = Date.now();
216
+ }
217
+ // ─── Metrics ──────────────────────────────────────────────────────────────
218
+ startMetrics() {
219
+ if (this.metricsTimer) return;
934
220
  this.metricsTimer = setInterval(() => {
935
- if (this.stopped) {
936
- clearInterval(this.metricsTimer);
937
- this.metricsTimer = void 0;
938
- return;
939
- }
940
- try {
941
- const mem = process.memoryUsage();
942
- const now = Date.now();
943
- const cpu = process.cpuUsage(this.lastCpuUsage);
944
- const elapsedMs = Math.max(1, now - this.lastCpuTime);
945
- const userMs = cpu.user / 1e3;
946
- const systemMs = cpu.system / 1e3;
947
- const cpuPercent = Math.min(
948
- 100,
949
- Math.round((userMs + systemMs) / elapsedMs * 100)
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
- async getActiveApps() {
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, startDevTools };
246
+ export { CNSDevToolsServer };
994
247
  //# sourceMappingURL=index.js.map
995
248
  //# sourceMappingURL=index.js.map