@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/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