@cnstra/devtools-server 1.1.1 → 1.2.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/index.cjs CHANGED
@@ -1,998 +1,250 @@
1
1
  'use strict';
2
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
- }
3
+ var devtoolsDto = require('@cnstra/devtools-dto');
149
4
 
150
5
  // src/index.ts
151
- var CNSDevToolsServer = class _CNSDevToolsServer {
152
- constructor(repository, options = {}) {
6
+ var CNSDevToolsServer = class {
7
+ constructor(repository) {
153
8
  this.repository = repository;
154
- this.options = options;
155
- this.stopped = false;
9
+ /** WebSocket → appId (for app clients) */
156
10
  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
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
- 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;
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 (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);
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
- if (entries.length > slice.length) {
209
- out["\u2026"] = `+${entries.length - slice.length} keys`;
210
- }
211
- return out;
35
+ return;
212
36
  }
213
- return value;
214
- }
215
- log(...args) {
216
- if (this.options.consoleLogEnabled) {
217
- console.log(...args);
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 "stimulations.query":
51
+ await this.handleStimulationsQuery(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
- 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);
61
+ addClient(ws) {
62
+ this.uiClients.add(ws);
63
+ if (this.uiClients.size === 1) this.startMetrics();
222
64
  }
223
- isMessageWithType(message) {
224
- return message !== null && typeof message === "object" && "type" in message && typeof message.type === "string";
65
+ removeClient(ws) {
66
+ this.uiClients.delete(ws);
67
+ this.appSockets.delete(ws);
68
+ if (this.uiClients.size === 0) this.stopMetrics();
225
69
  }
226
- // Expose sockets map size for tests
227
- /** @internal */
228
- get connectedAppCount() {
229
- return this.appSockets.size;
70
+ async getActiveApps() {
71
+ return this.repository.listApps();
230
72
  }
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;
73
+ stop() {
74
+ this.stopMetrics();
245
75
  }
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;
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 "stimulation.started":
83
+ await this.handleStimulationStarted(ws, item.stimulation);
84
+ break;
85
+ case "stimulation.hop":
86
+ await this.handleStimulationHop(item.hop);
87
+ break;
88
+ case "stimulation.completed":
89
+ await this.handleStimulationCompleted(item);
90
+ break;
678
91
  }
679
92
  }
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);
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
- appId: baseAppId,
694
- appName: message.appName,
695
- version: message.version,
696
- firstSeenAt: this.appFirstSeenAt.get(baseAppId),
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.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
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 handleStimulationStarted(ws, stimulation) {
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.saveStimulation(stimulation);
125
+ this.broadcast({ type: "stimulation.started", stimulation });
126
+ }
127
+ async handleStimulationHop(hop) {
128
+ await this.repository.saveHop(hop);
129
+ this.broadcast({ type: "stimulation.hop", hop });
130
+ }
131
+ async handleStimulationCompleted(item) {
132
+ await this.repository.completeStimulation(
133
+ item.stimulationId,
134
+ item.completedAt,
135
+ item.hopCount,
136
+ item.hasError
707
137
  );
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
138
+ this.broadcast({
139
+ type: "stimulation.completed",
140
+ stimulationId: item.stimulationId,
141
+ completedAt: item.completedAt,
142
+ hopCount: item.hopCount,
143
+ hasError: item.hasError
739
144
  });
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
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 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
- }
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 handleStimulationsQuery(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: "stimulations.result", requestId: msg.requestId, items: [], total: 0, offset: 0 });
759
169
  return;
760
170
  }
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-");
171
+ const { items, total } = await this.repository.getStimulations(appId, msg.filter ?? {});
172
+ this.send(ws, {
173
+ type: "stimulations.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 handleStimulate(ws, message) {
812
- const cnsIdFromCmd = message.cnsId;
813
- const appIdFromCmd = message.appId;
814
- try {
180
+ async handleHopsQuery(ws, msg) {
181
+ const items = await this.repository.getHops(msg.stimulationId);
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 newStimulationId = `${msg.stimulationId}-replay-${Date.now()}`;
192
+ this.send(ws, { type: "replay.accepted", replayId: msg.replayId, newStimulationId });
193
+ const payload = JSON.stringify(msg);
194
+ for (const appWs of appSockets) {
815
195
  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);
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
- 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);
201
+ // ─── Broadcast helpers ────────────────────────────────────────────────────
202
+ broadcast(message) {
203
+ this.broadcastRaw(JSON.stringify(message));
900
204
  }
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
- }
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
- startMetricsLoop() {
931
- if (this.metricsTimer) {
932
- return;
213
+ send(ws, message) {
214
+ try {
215
+ if (ws.readyState === 1) ws.send(JSON.stringify(message));
216
+ } catch {
933
217
  }
934
- this.lastCpuUsage = process.cpuUsage();
935
- this.lastCpuTime = Date.now();
218
+ }
219
+ // ─── Metrics ──────────────────────────────────────────────────────────────
220
+ startMetrics() {
221
+ if (this.metricsTimer) return;
936
222
  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
- }
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
- async getActiveApps() {
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