network-ai 4.15.3 → 5.0.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/INTEGRATION_GUIDE.md +11 -4
- package/QUICKSTART.md +31 -4
- package/README.md +37 -15
- package/bin/dashboard.ts +146 -0
- package/bin/mcp-server.ts +3 -2
- package/dist/adapters/adapter-registry.d.ts +33 -1
- package/dist/adapters/adapter-registry.d.ts.map +1 -1
- package/dist/adapters/adapter-registry.js +49 -0
- package/dist/adapters/adapter-registry.js.map +1 -1
- package/dist/adapters/anthropic-computer-use-adapter.d.ts +132 -0
- package/dist/adapters/anthropic-computer-use-adapter.d.ts.map +1 -0
- package/dist/adapters/anthropic-computer-use-adapter.js +180 -0
- package/dist/adapters/anthropic-computer-use-adapter.js.map +1 -0
- package/dist/adapters/browser-agent-adapter.d.ts +121 -0
- package/dist/adapters/browser-agent-adapter.d.ts.map +1 -0
- package/dist/adapters/browser-agent-adapter.js +219 -0
- package/dist/adapters/browser-agent-adapter.js.map +1 -0
- package/dist/adapters/copilot-adapter.d.ts +59 -0
- package/dist/adapters/copilot-adapter.d.ts.map +1 -0
- package/dist/adapters/copilot-adapter.js +132 -0
- package/dist/adapters/copilot-adapter.js.map +1 -0
- package/dist/adapters/index.d.ts +15 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +22 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/langgraph-adapter.d.ts +70 -0
- package/dist/adapters/langgraph-adapter.d.ts.map +1 -0
- package/dist/adapters/langgraph-adapter.js +119 -0
- package/dist/adapters/langgraph-adapter.js.map +1 -0
- package/dist/adapters/openai-agents-adapter.d.ts +100 -0
- package/dist/adapters/openai-agents-adapter.d.ts.map +1 -0
- package/dist/adapters/openai-agents-adapter.js +118 -0
- package/dist/adapters/openai-agents-adapter.js.map +1 -0
- package/dist/adapters/pydantic-ai-adapter.d.ts +104 -0
- package/dist/adapters/pydantic-ai-adapter.d.ts.map +1 -0
- package/dist/adapters/pydantic-ai-adapter.js +163 -0
- package/dist/adapters/pydantic-ai-adapter.js.map +1 -0
- package/dist/adapters/vertex-ai-adapter.d.ts +122 -0
- package/dist/adapters/vertex-ai-adapter.d.ts.map +1 -0
- package/dist/adapters/vertex-ai-adapter.js +166 -0
- package/dist/adapters/vertex-ai-adapter.js.map +1 -0
- package/dist/bin/dashboard.d.ts +11 -0
- package/dist/bin/dashboard.d.ts.map +1 -0
- package/dist/bin/dashboard.js +135 -0
- package/dist/bin/dashboard.js.map +1 -0
- package/dist/bin/mcp-server.js +3 -2
- package/dist/bin/mcp-server.js.map +1 -1
- package/dist/index.d.ts +103 -559
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +295 -1074
- package/dist/index.js.map +1 -1
- package/dist/lib/adapter-test-harness.d.ts +88 -0
- package/dist/lib/adapter-test-harness.d.ts.map +1 -0
- package/dist/lib/adapter-test-harness.js +118 -0
- package/dist/lib/adapter-test-harness.js.map +1 -0
- package/dist/lib/agent-conversation.d.ts +115 -0
- package/dist/lib/agent-conversation.d.ts.map +1 -0
- package/dist/lib/agent-conversation.js +155 -0
- package/dist/lib/agent-conversation.js.map +1 -0
- package/dist/lib/agent-debate.d.ts +115 -0
- package/dist/lib/agent-debate.d.ts.map +1 -0
- package/dist/lib/agent-debate.js +146 -0
- package/dist/lib/agent-debate.js.map +1 -0
- package/dist/lib/agent-memory.d.ts +157 -0
- package/dist/lib/agent-memory.d.ts.map +1 -0
- package/dist/lib/agent-memory.js +336 -0
- package/dist/lib/agent-memory.js.map +1 -0
- package/dist/lib/agent-vcr.d.ts +133 -0
- package/dist/lib/agent-vcr.d.ts.map +1 -0
- package/dist/lib/agent-vcr.js +218 -0
- package/dist/lib/agent-vcr.js.map +1 -0
- package/dist/lib/anomaly-detector.d.ts +112 -0
- package/dist/lib/anomaly-detector.d.ts.map +1 -0
- package/dist/lib/anomaly-detector.js +178 -0
- package/dist/lib/anomaly-detector.js.map +1 -0
- package/dist/lib/approval-inbox.d.ts +147 -0
- package/dist/lib/approval-inbox.d.ts.map +1 -0
- package/dist/lib/approval-inbox.js +385 -0
- package/dist/lib/approval-inbox.js.map +1 -0
- package/dist/lib/auth-guardian.d.ts +170 -0
- package/dist/lib/auth-guardian.d.ts.map +1 -0
- package/dist/lib/auth-guardian.js +604 -0
- package/dist/lib/auth-guardian.js.map +1 -0
- package/dist/lib/auth-validator.d.ts +70 -0
- package/dist/lib/auth-validator.d.ts.map +1 -0
- package/dist/lib/auth-validator.js +32 -0
- package/dist/lib/auth-validator.js.map +1 -0
- package/dist/lib/blackboard-validator.d.ts +56 -0
- package/dist/lib/blackboard-validator.d.ts.map +1 -1
- package/dist/lib/blackboard-validator.js +181 -4
- package/dist/lib/blackboard-validator.js.map +1 -1
- package/dist/lib/comparison-runner.d.ts +99 -0
- package/dist/lib/comparison-runner.d.ts.map +1 -0
- package/dist/lib/comparison-runner.js +138 -0
- package/dist/lib/comparison-runner.js.map +1 -0
- package/dist/lib/config-watcher.d.ts +109 -0
- package/dist/lib/config-watcher.d.ts.map +1 -0
- package/dist/lib/config-watcher.js +215 -0
- package/dist/lib/config-watcher.js.map +1 -0
- package/dist/lib/cost-governor.d.ts +105 -0
- package/dist/lib/cost-governor.d.ts.map +1 -0
- package/dist/lib/cost-governor.js +128 -0
- package/dist/lib/cost-governor.js.map +1 -0
- package/dist/lib/cost-heatmap.d.ts +104 -0
- package/dist/lib/cost-heatmap.d.ts.map +1 -0
- package/dist/lib/cost-heatmap.js +161 -0
- package/dist/lib/cost-heatmap.js.map +1 -0
- package/dist/lib/coverage-reporter.d.ts +92 -0
- package/dist/lib/coverage-reporter.d.ts.map +1 -0
- package/dist/lib/coverage-reporter.js +177 -0
- package/dist/lib/coverage-reporter.js.map +1 -0
- package/dist/lib/dashboard-server.d.ts +71 -0
- package/dist/lib/dashboard-server.d.ts.map +1 -0
- package/dist/lib/dashboard-server.js +403 -0
- package/dist/lib/dashboard-server.js.map +1 -0
- package/dist/lib/dry-run.d.ts +73 -0
- package/dist/lib/dry-run.d.ts.map +1 -0
- package/dist/lib/dry-run.js +130 -0
- package/dist/lib/dry-run.js.map +1 -0
- package/dist/lib/errors.d.ts +15 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +38 -0
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/event-bus.d.ts +167 -0
- package/dist/lib/event-bus.d.ts.map +1 -0
- package/dist/lib/event-bus.js +229 -0
- package/dist/lib/event-bus.js.map +1 -0
- package/dist/lib/explainability.d.ts +85 -0
- package/dist/lib/explainability.d.ts.map +1 -0
- package/dist/lib/explainability.js +102 -0
- package/dist/lib/explainability.js.map +1 -0
- package/dist/lib/goal-dsl.d.ts +157 -0
- package/dist/lib/goal-dsl.d.ts.map +1 -0
- package/dist/lib/goal-dsl.js +392 -0
- package/dist/lib/goal-dsl.js.map +1 -0
- package/dist/lib/job-queue.d.ts +183 -0
- package/dist/lib/job-queue.d.ts.map +1 -0
- package/dist/lib/job-queue.js +310 -0
- package/dist/lib/job-queue.js.map +1 -0
- package/dist/lib/learning-loop.d.ts +113 -0
- package/dist/lib/learning-loop.d.ts.map +1 -0
- package/dist/lib/learning-loop.js +181 -0
- package/dist/lib/learning-loop.js.map +1 -0
- package/dist/lib/lifecycle-hooks.d.ts +116 -0
- package/dist/lib/lifecycle-hooks.d.ts.map +1 -0
- package/dist/lib/lifecycle-hooks.js +148 -0
- package/dist/lib/lifecycle-hooks.js.map +1 -0
- package/dist/lib/locked-blackboard.d.ts.map +1 -1
- package/dist/lib/locked-blackboard.js +9 -5
- package/dist/lib/locked-blackboard.js.map +1 -1
- package/dist/lib/mcp-tool-consumer.d.ts +153 -0
- package/dist/lib/mcp-tool-consumer.d.ts.map +1 -0
- package/dist/lib/mcp-tool-consumer.js +320 -0
- package/dist/lib/mcp-tool-consumer.js.map +1 -0
- package/dist/lib/metrics.d.ts +119 -0
- package/dist/lib/metrics.d.ts.map +1 -0
- package/dist/lib/metrics.js +284 -0
- package/dist/lib/metrics.js.map +1 -0
- package/dist/lib/orchestrator-types.d.ts +309 -0
- package/dist/lib/orchestrator-types.d.ts.map +1 -0
- package/dist/lib/orchestrator-types.js +61 -0
- package/dist/lib/orchestrator-types.js.map +1 -0
- package/dist/lib/otel-bridge.d.ts +74 -0
- package/dist/lib/otel-bridge.d.ts.map +1 -0
- package/dist/lib/otel-bridge.js +167 -0
- package/dist/lib/otel-bridge.js.map +1 -0
- package/dist/lib/playground.d.ts +76 -0
- package/dist/lib/playground.d.ts.map +1 -0
- package/dist/lib/playground.js +224 -0
- package/dist/lib/playground.js.map +1 -0
- package/dist/lib/quadtree.d.ts +114 -0
- package/dist/lib/quadtree.d.ts.map +1 -0
- package/dist/lib/quadtree.js +259 -0
- package/dist/lib/quadtree.js.map +1 -0
- package/dist/lib/shared-blackboard.d.ts +101 -0
- package/dist/lib/shared-blackboard.d.ts.map +1 -0
- package/dist/lib/shared-blackboard.js +249 -0
- package/dist/lib/shared-blackboard.js.map +1 -0
- package/dist/lib/speculative-executor.d.ts +89 -0
- package/dist/lib/speculative-executor.d.ts.map +1 -0
- package/dist/lib/speculative-executor.js +107 -0
- package/dist/lib/speculative-executor.js.map +1 -0
- package/dist/lib/swarm-transport.d.ts +150 -0
- package/dist/lib/swarm-transport.d.ts.map +1 -0
- package/dist/lib/swarm-transport.js +307 -0
- package/dist/lib/swarm-transport.js.map +1 -0
- package/dist/lib/task-decomposer.d.ts +41 -0
- package/dist/lib/task-decomposer.d.ts.map +1 -0
- package/dist/lib/task-decomposer.js +272 -0
- package/dist/lib/task-decomposer.js.map +1 -0
- package/dist/lib/timeline-scrubber.d.ts +84 -0
- package/dist/lib/timeline-scrubber.d.ts.map +1 -0
- package/dist/lib/timeline-scrubber.js +173 -0
- package/dist/lib/timeline-scrubber.js.map +1 -0
- package/dist/lib/topology.d.ts +361 -0
- package/dist/lib/topology.d.ts.map +1 -0
- package/dist/lib/topology.js +591 -0
- package/dist/lib/topology.js.map +1 -0
- package/dist/security.d.ts +95 -0
- package/dist/security.d.ts.map +1 -1
- package/dist/security.js +266 -4
- package/dist/security.js.map +1 -1
- package/package.json +7 -5
- package/types/agent-adapter.d.ts +5 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CoverageReporter — Lightweight code coverage reporting without external deps
|
|
4
|
+
*
|
|
5
|
+
* Integrates with Node.js built-in V8 coverage (`NODE_V8_COVERAGE`).
|
|
6
|
+
* Parses V8 coverage JSON output into a structured report with file-level
|
|
7
|
+
* line and function coverage metrics. Supports thresholds and reporters.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* // Run tests with V8 coverage:
|
|
11
|
+
* // NODE_V8_COVERAGE=./coverage npx ts-node test.ts
|
|
12
|
+
*
|
|
13
|
+
* const reporter = new CoverageReporter({ coverageDir: './coverage' });
|
|
14
|
+
* const report = await reporter.collect();
|
|
15
|
+
* reporter.printSummary(report);
|
|
16
|
+
* reporter.enforce(report, { linePct: 80, branchPct: 70 });
|
|
17
|
+
*
|
|
18
|
+
* @module CoverageReporter
|
|
19
|
+
* @version 1.0.0
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.CoverageReporter = void 0;
|
|
23
|
+
const promises_1 = require("fs/promises");
|
|
24
|
+
const path_1 = require("path");
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// REPORTER
|
|
27
|
+
// ============================================================================
|
|
28
|
+
/**
|
|
29
|
+
* CoverageReporter — Collects and reports V8 code coverage data.
|
|
30
|
+
*/
|
|
31
|
+
class CoverageReporter {
|
|
32
|
+
config;
|
|
33
|
+
constructor(config) {
|
|
34
|
+
this.config = {
|
|
35
|
+
coverageDir: (0, path_1.resolve)(config.coverageDir),
|
|
36
|
+
projectRoot: config.projectRoot ?? process.cwd(),
|
|
37
|
+
include: config.include ?? ['.ts', '.js'],
|
|
38
|
+
exclude: config.exclude ?? ['node_modules', 'test-', 'dist/'],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Collect coverage data from V8 coverage directory.
|
|
43
|
+
*/
|
|
44
|
+
async collect() {
|
|
45
|
+
const files = [];
|
|
46
|
+
const coverageFiles = await this.findCoverageFiles();
|
|
47
|
+
for (const cf of coverageFiles) {
|
|
48
|
+
const raw = await (0, promises_1.readFile)(cf, 'utf-8');
|
|
49
|
+
const data = JSON.parse(raw);
|
|
50
|
+
for (const script of data.result) {
|
|
51
|
+
if (!this.shouldInclude(script.url))
|
|
52
|
+
continue;
|
|
53
|
+
const relPath = this.toRelativePath(script.url);
|
|
54
|
+
const existing = files.find((f) => f.file === relPath);
|
|
55
|
+
if (existing)
|
|
56
|
+
continue; // Deduplicate
|
|
57
|
+
const totalFunctions = script.functions.length;
|
|
58
|
+
const coveredFunctions = script.functions.filter((fn) => fn.ranges.some((r) => r.count > 0)).length;
|
|
59
|
+
let totalRanges = 0;
|
|
60
|
+
let coveredRanges = 0;
|
|
61
|
+
for (const fn of script.functions) {
|
|
62
|
+
for (const range of fn.ranges) {
|
|
63
|
+
totalRanges++;
|
|
64
|
+
if (range.count > 0)
|
|
65
|
+
coveredRanges++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
files.push({
|
|
69
|
+
file: relPath,
|
|
70
|
+
totalFunctions,
|
|
71
|
+
coveredFunctions,
|
|
72
|
+
functionPct: totalFunctions > 0 ? round((coveredFunctions / totalFunctions) * 100) : 100,
|
|
73
|
+
totalRanges,
|
|
74
|
+
coveredRanges,
|
|
75
|
+
rangePct: totalRanges > 0 ? round((coveredRanges / totalRanges) * 100) : 100,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
files.sort((a, b) => a.file.localeCompare(b.file));
|
|
80
|
+
const totalFns = files.reduce((s, f) => s + f.totalFunctions, 0);
|
|
81
|
+
const coveredFns = files.reduce((s, f) => s + f.coveredFunctions, 0);
|
|
82
|
+
const totalRng = files.reduce((s, f) => s + f.totalRanges, 0);
|
|
83
|
+
const coveredRng = files.reduce((s, f) => s + f.coveredRanges, 0);
|
|
84
|
+
return {
|
|
85
|
+
files,
|
|
86
|
+
totalFunctionPct: totalFns > 0 ? round((coveredFns / totalFns) * 100) : 100,
|
|
87
|
+
totalRangePct: totalRng > 0 ? round((coveredRng / totalRng) * 100) : 100,
|
|
88
|
+
fileCount: files.length,
|
|
89
|
+
generatedAt: Date.now(),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Print a summary table to console.
|
|
94
|
+
*/
|
|
95
|
+
printSummary(report) {
|
|
96
|
+
const header = 'File'.padEnd(50) + 'Functions'.padEnd(15) + 'Ranges';
|
|
97
|
+
console.log('─'.repeat(75));
|
|
98
|
+
console.log(header);
|
|
99
|
+
console.log('─'.repeat(75));
|
|
100
|
+
for (const f of report.files) {
|
|
101
|
+
const name = f.file.length > 48 ? '…' + f.file.slice(-47) : f.file;
|
|
102
|
+
const fnCol = `${f.coveredFunctions}/${f.totalFunctions} (${f.functionPct}%)`.padEnd(15);
|
|
103
|
+
const rgCol = `${f.coveredRanges}/${f.totalRanges} (${f.rangePct}%)`;
|
|
104
|
+
console.log(name.padEnd(50) + fnCol + rgCol);
|
|
105
|
+
}
|
|
106
|
+
console.log('─'.repeat(75));
|
|
107
|
+
console.log(`Total: ${report.fileCount} files | ` +
|
|
108
|
+
`Functions: ${report.totalFunctionPct}% | ` +
|
|
109
|
+
`Ranges: ${report.totalRangePct}%`);
|
|
110
|
+
console.log('─'.repeat(75));
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Enforce coverage thresholds. Throws if below threshold.
|
|
114
|
+
*/
|
|
115
|
+
enforce(report, thresholds) {
|
|
116
|
+
const failures = [];
|
|
117
|
+
if (thresholds.functionPct !== undefined && report.totalFunctionPct < thresholds.functionPct) {
|
|
118
|
+
failures.push(`Function coverage ${report.totalFunctionPct}% below threshold ${thresholds.functionPct}%`);
|
|
119
|
+
}
|
|
120
|
+
if (thresholds.rangePct !== undefined && report.totalRangePct < thresholds.rangePct) {
|
|
121
|
+
failures.push(`Range coverage ${report.totalRangePct}% below threshold ${thresholds.rangePct}%`);
|
|
122
|
+
}
|
|
123
|
+
if (thresholds.perFileFunctionPct !== undefined) {
|
|
124
|
+
for (const f of report.files) {
|
|
125
|
+
if (f.functionPct < thresholds.perFileFunctionPct) {
|
|
126
|
+
failures.push(`${f.file}: function coverage ${f.functionPct}% below per-file threshold ${thresholds.perFileFunctionPct}%`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (failures.length > 0) {
|
|
131
|
+
throw new Error(`Coverage thresholds not met:\n${failures.join('\n')}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// --------------------------------------------------------------------------
|
|
135
|
+
// Internal
|
|
136
|
+
// --------------------------------------------------------------------------
|
|
137
|
+
async findCoverageFiles() {
|
|
138
|
+
const results = [];
|
|
139
|
+
try {
|
|
140
|
+
const entries = await (0, promises_1.readdir)(this.config.coverageDir);
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
if (entry.endsWith('.json')) {
|
|
143
|
+
results.push((0, path_1.join)(this.config.coverageDir, entry));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Directory doesn't exist or not readable
|
|
149
|
+
}
|
|
150
|
+
return results;
|
|
151
|
+
}
|
|
152
|
+
shouldInclude(url) {
|
|
153
|
+
if (!url || url.startsWith('node:'))
|
|
154
|
+
return false;
|
|
155
|
+
const hasInclude = this.config.include.some((p) => url.includes(p));
|
|
156
|
+
const hasExclude = this.config.exclude.some((p) => url.includes(p));
|
|
157
|
+
return hasInclude && !hasExclude;
|
|
158
|
+
}
|
|
159
|
+
toRelativePath(url) {
|
|
160
|
+
let path = url;
|
|
161
|
+
// Strip file:// prefix
|
|
162
|
+
if (path.startsWith('file://')) {
|
|
163
|
+
path = path.slice(7);
|
|
164
|
+
}
|
|
165
|
+
// On Windows, strip leading slash before drive letter
|
|
166
|
+
if (/^\/[A-Za-z]:/.test(path)) {
|
|
167
|
+
path = path.slice(1);
|
|
168
|
+
}
|
|
169
|
+
return (0, path_1.relative)(this.config.projectRoot, path).replace(/\\/g, '/');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
exports.CoverageReporter = CoverageReporter;
|
|
173
|
+
/** Round to 1 decimal */
|
|
174
|
+
function round(n) {
|
|
175
|
+
return Math.round(n * 10) / 10;
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=coverage-reporter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coverage-reporter.js","sourceRoot":"","sources":["../../lib/coverage-reporter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Server — Serves the live agent topology dashboard
|
|
3
|
+
*
|
|
4
|
+
* A zero-dependency HTTP + WebSocket server that streams topology events
|
|
5
|
+
* to a browser-based visualization. Uses only Node.js built-in modules.
|
|
6
|
+
*
|
|
7
|
+
* @module DashboardServer
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
*/
|
|
10
|
+
import { EventEmitter } from 'events';
|
|
11
|
+
import type { TopologyTracker } from './topology';
|
|
12
|
+
/** Options for creating a DashboardServer */
|
|
13
|
+
export interface DashboardServerOptions {
|
|
14
|
+
/** TCP port to listen on (default: 4820) */
|
|
15
|
+
port?: number;
|
|
16
|
+
/** Hostname to bind to (default: '127.0.0.1') */
|
|
17
|
+
host?: string;
|
|
18
|
+
/** Whether to open the browser automatically (default: true) */
|
|
19
|
+
open?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* HTTP + WebSocket server for the live agent topology dashboard.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const topo = new TopologyTracker();
|
|
27
|
+
* const dashboard = new DashboardServer(topo, { port: 4820 });
|
|
28
|
+
* await dashboard.start();
|
|
29
|
+
* // Dashboard available at http://127.0.0.1:4820
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare class DashboardServer extends EventEmitter {
|
|
33
|
+
private readonly tracker;
|
|
34
|
+
private readonly port;
|
|
35
|
+
private readonly host;
|
|
36
|
+
private server;
|
|
37
|
+
private clients;
|
|
38
|
+
private pingInterval;
|
|
39
|
+
private eventHandler;
|
|
40
|
+
private clientCounter;
|
|
41
|
+
private deltaPending;
|
|
42
|
+
constructor(tracker: TopologyTracker, options?: DashboardServerOptions);
|
|
43
|
+
/**
|
|
44
|
+
* Start the dashboard server.
|
|
45
|
+
*/
|
|
46
|
+
start(): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Stop the dashboard server.
|
|
49
|
+
*/
|
|
50
|
+
stop(): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Number of connected WebSocket clients.
|
|
53
|
+
*/
|
|
54
|
+
clientCount(): number;
|
|
55
|
+
/**
|
|
56
|
+
* The URL the dashboard is serving on.
|
|
57
|
+
*/
|
|
58
|
+
get url(): string;
|
|
59
|
+
private handleHTTP;
|
|
60
|
+
private handleUpgrade;
|
|
61
|
+
private handleClientMessage;
|
|
62
|
+
private broadcast;
|
|
63
|
+
/**
|
|
64
|
+
* Broadcast a delta patch to all connected clients.
|
|
65
|
+
* Each client tracks its own lastSeq so we can compute per-client deltas.
|
|
66
|
+
* For simplicity, we use a single shared delta and reset after broadcast.
|
|
67
|
+
*/
|
|
68
|
+
private broadcastDelta;
|
|
69
|
+
private pingClients;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=dashboard-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard-server.d.ts","sourceRoot":"","sources":["../../lib/dashboard-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAItC,OAAO,KAAK,EAAE,eAAe,EAAkD,MAAM,YAAY,CAAC;AAMlG,6CAA6C;AAC7C,MAAM,WAAW,sBAAsB;IACrC,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AA+FD;;;;;;;;;;GAUG;AACH,qBAAa,eAAgB,SAAQ,YAAY;IAC/C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;IAC1C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,MAAM,CAAgD;IAC9D,OAAO,CAAC,OAAO,CAAoC;IACnD,OAAO,CAAC,YAAY,CAA+C;IACnE,OAAO,CAAC,YAAY,CAAiD;IACrE,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,YAAY,CAAS;gBAEjB,OAAO,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE,sBAAsB;IAOtE;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA+B5B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA6B3B;;OAEG;IACH,WAAW,IAAI,MAAM;IAIrB;;OAEG;IACH,IAAI,GAAG,IAAI,MAAM,CAEhB;IAMD,OAAO,CAAC,UAAU;IA8ClB,OAAO,CAAC,aAAa;IAoGrB,OAAO,CAAC,mBAAmB;IAY3B,OAAO,CAAC,SAAS;IAMjB;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAqCtB,OAAO,CAAC,WAAW;CAqBpB"}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Dashboard Server — Serves the live agent topology dashboard
|
|
4
|
+
*
|
|
5
|
+
* A zero-dependency HTTP + WebSocket server that streams topology events
|
|
6
|
+
* to a browser-based visualization. Uses only Node.js built-in modules.
|
|
7
|
+
*
|
|
8
|
+
* @module DashboardServer
|
|
9
|
+
* @version 1.0.0
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.DashboardServer = void 0;
|
|
13
|
+
const http_1 = require("http");
|
|
14
|
+
const crypto_1 = require("crypto");
|
|
15
|
+
const events_1 = require("events");
|
|
16
|
+
const fs_1 = require("fs");
|
|
17
|
+
const path_1 = require("path");
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// WEBSOCKET HELPERS (RFC 6455, minimal implementation)
|
|
20
|
+
// ============================================================================
|
|
21
|
+
const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
22
|
+
function computeAcceptKey(key) {
|
|
23
|
+
return (0, crypto_1.createHash)('sha1').update(key + WS_GUID).digest('base64');
|
|
24
|
+
}
|
|
25
|
+
function encodeWSFrame(data) {
|
|
26
|
+
const payload = Buffer.from(data, 'utf8');
|
|
27
|
+
const len = payload.length;
|
|
28
|
+
let header;
|
|
29
|
+
if (len < 126) {
|
|
30
|
+
header = Buffer.alloc(2);
|
|
31
|
+
header[0] = 0x81; // FIN + text opcode
|
|
32
|
+
header[1] = len;
|
|
33
|
+
}
|
|
34
|
+
else if (len < 65536) {
|
|
35
|
+
header = Buffer.alloc(4);
|
|
36
|
+
header[0] = 0x81;
|
|
37
|
+
header[1] = 126;
|
|
38
|
+
header.writeUInt16BE(len, 2);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
header = Buffer.alloc(10);
|
|
42
|
+
header[0] = 0x81;
|
|
43
|
+
header[1] = 127;
|
|
44
|
+
// Write as two 32-bit values (safe for strings < 4GB)
|
|
45
|
+
header.writeUInt32BE(0, 2);
|
|
46
|
+
header.writeUInt32BE(len, 6);
|
|
47
|
+
}
|
|
48
|
+
return Buffer.concat([header, payload]);
|
|
49
|
+
}
|
|
50
|
+
function decodeWSFrame(buf) {
|
|
51
|
+
if (buf.length < 2)
|
|
52
|
+
return null;
|
|
53
|
+
const opcode = buf[0] & 0x0f;
|
|
54
|
+
const masked = (buf[1] & 0x80) !== 0;
|
|
55
|
+
let payloadLen = buf[1] & 0x7f;
|
|
56
|
+
let offset = 2;
|
|
57
|
+
if (payloadLen === 126) {
|
|
58
|
+
if (buf.length < 4)
|
|
59
|
+
return null;
|
|
60
|
+
payloadLen = buf.readUInt16BE(2);
|
|
61
|
+
offset = 4;
|
|
62
|
+
}
|
|
63
|
+
else if (payloadLen === 127) {
|
|
64
|
+
if (buf.length < 10)
|
|
65
|
+
return null;
|
|
66
|
+
payloadLen = buf.readUInt32BE(6); // ignore high 32 bits
|
|
67
|
+
offset = 10;
|
|
68
|
+
}
|
|
69
|
+
if (masked) {
|
|
70
|
+
if (buf.length < offset + 4 + payloadLen)
|
|
71
|
+
return null;
|
|
72
|
+
const mask = buf.subarray(offset, offset + 4);
|
|
73
|
+
offset += 4;
|
|
74
|
+
const payload = Buffer.alloc(payloadLen);
|
|
75
|
+
for (let i = 0; i < payloadLen; i++) {
|
|
76
|
+
payload[i] = buf[offset + i] ^ mask[i % 4];
|
|
77
|
+
}
|
|
78
|
+
return { opcode, payload };
|
|
79
|
+
}
|
|
80
|
+
if (buf.length < offset + payloadLen)
|
|
81
|
+
return null;
|
|
82
|
+
return { opcode, payload: buf.subarray(offset, offset + payloadLen) };
|
|
83
|
+
}
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// DASHBOARD HTML (inline single-page app)
|
|
86
|
+
// ============================================================================
|
|
87
|
+
function getDashboardHTML(wsPort) {
|
|
88
|
+
// HTML loaded from the separate dashboard.html asset (Phase 1.2 extraction)
|
|
89
|
+
const htmlPath = (0, path_1.join)(__dirname, 'dashboard.html');
|
|
90
|
+
const html = (0, fs_1.readFileSync)(htmlPath, 'utf-8');
|
|
91
|
+
return html.replace('__WS_PORT__', String(wsPort));
|
|
92
|
+
}
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// DASHBOARD SERVER
|
|
95
|
+
// ============================================================================
|
|
96
|
+
/**
|
|
97
|
+
* HTTP + WebSocket server for the live agent topology dashboard.
|
|
98
|
+
*
|
|
99
|
+
* Usage:
|
|
100
|
+
* ```typescript
|
|
101
|
+
* const topo = new TopologyTracker();
|
|
102
|
+
* const dashboard = new DashboardServer(topo, { port: 4820 });
|
|
103
|
+
* await dashboard.start();
|
|
104
|
+
* // Dashboard available at http://127.0.0.1:4820
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
class DashboardServer extends events_1.EventEmitter {
|
|
108
|
+
tracker;
|
|
109
|
+
port;
|
|
110
|
+
host;
|
|
111
|
+
server = null;
|
|
112
|
+
clients = new Map();
|
|
113
|
+
pingInterval = null;
|
|
114
|
+
eventHandler = null;
|
|
115
|
+
clientCounter = 0;
|
|
116
|
+
deltaPending = false;
|
|
117
|
+
constructor(tracker, options) {
|
|
118
|
+
super();
|
|
119
|
+
this.tracker = tracker;
|
|
120
|
+
this.port = options?.port ?? 4820;
|
|
121
|
+
this.host = options?.host ?? '127.0.0.1';
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Start the dashboard server.
|
|
125
|
+
*/
|
|
126
|
+
async start() {
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
const server = (0, http_1.createServer)((req, res) => this.handleHTTP(req, res));
|
|
129
|
+
server.on('upgrade', (req, socket) => {
|
|
130
|
+
this.handleUpgrade(req, socket);
|
|
131
|
+
});
|
|
132
|
+
server.on('error', (err) => {
|
|
133
|
+
this.emit('error', err);
|
|
134
|
+
reject(err);
|
|
135
|
+
});
|
|
136
|
+
server.listen(this.port, this.host, () => {
|
|
137
|
+
this.server = server;
|
|
138
|
+
// Subscribe to topology events and broadcast deltas
|
|
139
|
+
this.eventHandler = (_event) => {
|
|
140
|
+
this.broadcastDelta();
|
|
141
|
+
};
|
|
142
|
+
this.tracker.on('event', this.eventHandler);
|
|
143
|
+
// Ping clients every 30s
|
|
144
|
+
this.pingInterval = setInterval(() => this.pingClients(), 30000);
|
|
145
|
+
this.emit('listening', { port: this.port, host: this.host });
|
|
146
|
+
resolve();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Stop the dashboard server.
|
|
152
|
+
*/
|
|
153
|
+
async stop() {
|
|
154
|
+
if (this.pingInterval) {
|
|
155
|
+
clearInterval(this.pingInterval);
|
|
156
|
+
this.pingInterval = null;
|
|
157
|
+
}
|
|
158
|
+
if (this.eventHandler) {
|
|
159
|
+
this.tracker.off('event', this.eventHandler);
|
|
160
|
+
this.eventHandler = null;
|
|
161
|
+
}
|
|
162
|
+
// Close all WebSocket clients
|
|
163
|
+
for (const client of this.clients.values()) {
|
|
164
|
+
client.close();
|
|
165
|
+
}
|
|
166
|
+
this.clients.clear();
|
|
167
|
+
return new Promise((resolve) => {
|
|
168
|
+
if (!this.server) {
|
|
169
|
+
resolve();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this.server.close(() => {
|
|
173
|
+
this.server = null;
|
|
174
|
+
resolve();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Number of connected WebSocket clients.
|
|
180
|
+
*/
|
|
181
|
+
clientCount() {
|
|
182
|
+
return this.clients.size;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The URL the dashboard is serving on.
|
|
186
|
+
*/
|
|
187
|
+
get url() {
|
|
188
|
+
return `http://${this.host}:${this.port}`;
|
|
189
|
+
}
|
|
190
|
+
// --------------------------------------------------------------------------
|
|
191
|
+
// HTTP HANDLER
|
|
192
|
+
// --------------------------------------------------------------------------
|
|
193
|
+
handleHTTP(req, res) {
|
|
194
|
+
const url = req.url ?? '/';
|
|
195
|
+
if (url === '/' || url === '/index.html') {
|
|
196
|
+
res.writeHead(200, {
|
|
197
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
198
|
+
'Cache-Control': 'no-cache',
|
|
199
|
+
'X-Content-Type-Options': 'nosniff',
|
|
200
|
+
});
|
|
201
|
+
res.end(getDashboardHTML(this.port));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (url === '/api/snapshot') {
|
|
205
|
+
const snapshot = this.tracker.snapshot();
|
|
206
|
+
res.writeHead(200, {
|
|
207
|
+
'Content-Type': 'application/json',
|
|
208
|
+
'Cache-Control': 'no-cache',
|
|
209
|
+
'X-Content-Type-Options': 'nosniff',
|
|
210
|
+
});
|
|
211
|
+
res.end(JSON.stringify(snapshot));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (url === '/api/health') {
|
|
215
|
+
res.writeHead(200, {
|
|
216
|
+
'Content-Type': 'application/json',
|
|
217
|
+
'X-Content-Type-Options': 'nosniff',
|
|
218
|
+
});
|
|
219
|
+
res.end(JSON.stringify({
|
|
220
|
+
status: 'ok',
|
|
221
|
+
clients: this.clients.size,
|
|
222
|
+
nodes: this.tracker.nodeCount(),
|
|
223
|
+
edges: this.tracker.edgeCount(),
|
|
224
|
+
}));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
228
|
+
res.end('Not Found');
|
|
229
|
+
}
|
|
230
|
+
// --------------------------------------------------------------------------
|
|
231
|
+
// WEBSOCKET UPGRADE
|
|
232
|
+
// --------------------------------------------------------------------------
|
|
233
|
+
handleUpgrade(req, socket) {
|
|
234
|
+
const key = req.headers['sec-websocket-key'];
|
|
235
|
+
if (!key) {
|
|
236
|
+
socket.destroy();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const acceptKey = computeAcceptKey(key);
|
|
240
|
+
const headers = [
|
|
241
|
+
'HTTP/1.1 101 Switching Protocols',
|
|
242
|
+
'Upgrade: websocket',
|
|
243
|
+
'Connection: Upgrade',
|
|
244
|
+
`Sec-WebSocket-Accept: ${acceptKey}`,
|
|
245
|
+
'',
|
|
246
|
+
'',
|
|
247
|
+
].join('\r\n');
|
|
248
|
+
socket.write(headers);
|
|
249
|
+
const clientId = `ws-${++this.clientCounter}`;
|
|
250
|
+
const client = {
|
|
251
|
+
id: clientId,
|
|
252
|
+
send: (data) => {
|
|
253
|
+
try {
|
|
254
|
+
socket.write(encodeWSFrame(data));
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Client disconnected
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
close: () => {
|
|
261
|
+
try {
|
|
262
|
+
// Send close frame
|
|
263
|
+
const closeFrame = Buffer.alloc(2);
|
|
264
|
+
closeFrame[0] = 0x88; // FIN + close
|
|
265
|
+
closeFrame[1] = 0x00;
|
|
266
|
+
socket.write(closeFrame);
|
|
267
|
+
socket.end();
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// Already closed
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
alive: true,
|
|
274
|
+
lastSeq: 0,
|
|
275
|
+
};
|
|
276
|
+
this.clients.set(clientId, client);
|
|
277
|
+
this.emit('client:connected', clientId);
|
|
278
|
+
// Send initial snapshot
|
|
279
|
+
const snapshot = this.tracker.snapshotQuiet();
|
|
280
|
+
client.send(JSON.stringify({ type: 'snapshot', data: snapshot }));
|
|
281
|
+
client.lastSeq = this.tracker.currentSeq();
|
|
282
|
+
// Handle incoming messages
|
|
283
|
+
let buffer = Buffer.alloc(0);
|
|
284
|
+
socket.on('data', (chunk) => {
|
|
285
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
286
|
+
const frame = decodeWSFrame(buffer);
|
|
287
|
+
if (!frame)
|
|
288
|
+
return;
|
|
289
|
+
buffer = Buffer.alloc(0);
|
|
290
|
+
if (frame.opcode === 0x08) {
|
|
291
|
+
// Close frame
|
|
292
|
+
this.clients.delete(clientId);
|
|
293
|
+
socket.end();
|
|
294
|
+
this.emit('client:disconnected', clientId);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (frame.opcode === 0x0a) {
|
|
298
|
+
// Pong
|
|
299
|
+
client.alive = true;
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (frame.opcode === 0x01) {
|
|
303
|
+
// Text message — handle commands
|
|
304
|
+
try {
|
|
305
|
+
const msg = JSON.parse(frame.payload.toString('utf8'));
|
|
306
|
+
this.handleClientMessage(client, msg);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
// Ignore malformed messages
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
socket.on('close', () => {
|
|
314
|
+
this.clients.delete(clientId);
|
|
315
|
+
this.emit('client:disconnected', clientId);
|
|
316
|
+
});
|
|
317
|
+
socket.on('error', () => {
|
|
318
|
+
this.clients.delete(clientId);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
// --------------------------------------------------------------------------
|
|
322
|
+
// WEBSOCKET MESSAGE HANDLING
|
|
323
|
+
// --------------------------------------------------------------------------
|
|
324
|
+
handleClientMessage(client, msg) {
|
|
325
|
+
if (msg.action === 'snapshot') {
|
|
326
|
+
const snapshot = this.tracker.snapshotQuiet();
|
|
327
|
+
snapshot.clusters = this.tracker.computeClusters();
|
|
328
|
+
client.send(JSON.stringify({ type: 'snapshot', data: snapshot }));
|
|
329
|
+
client.lastSeq = this.tracker.currentSeq();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
broadcast(data) {
|
|
333
|
+
for (const client of this.clients.values()) {
|
|
334
|
+
client.send(data);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Broadcast a delta patch to all connected clients.
|
|
339
|
+
* Each client tracks its own lastSeq so we can compute per-client deltas.
|
|
340
|
+
* For simplicity, we use a single shared delta and reset after broadcast.
|
|
341
|
+
*/
|
|
342
|
+
broadcastDelta() {
|
|
343
|
+
if (this.clients.size === 0)
|
|
344
|
+
return;
|
|
345
|
+
// Debounce: accumulate rapid events into one delta per tick
|
|
346
|
+
if (this.deltaPending)
|
|
347
|
+
return;
|
|
348
|
+
this.deltaPending = true;
|
|
349
|
+
// Use queueMicrotask so multiple sync events within one tick batch together
|
|
350
|
+
queueMicrotask(() => {
|
|
351
|
+
this.deltaPending = false;
|
|
352
|
+
if (this.clients.size === 0)
|
|
353
|
+
return;
|
|
354
|
+
// If there are many nodes (>200), send delta; otherwise send full snapshot
|
|
355
|
+
// for simplicity on small topologies
|
|
356
|
+
const nodeCount = this.tracker.nodeCount();
|
|
357
|
+
if (nodeCount <= 200) {
|
|
358
|
+
const snapshot = this.tracker.snapshotQuiet();
|
|
359
|
+
const data = JSON.stringify({ type: 'snapshot', data: snapshot });
|
|
360
|
+
for (const client of this.clients.values()) {
|
|
361
|
+
client.send(data);
|
|
362
|
+
client.lastSeq = this.tracker.currentSeq();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
// Delta protocol for large topologies
|
|
367
|
+
const minSeq = Math.min(...Array.from(this.clients.values()).map(c => c.lastSeq));
|
|
368
|
+
const delta = this.tracker.delta(minSeq);
|
|
369
|
+
delta.clusters = this.tracker.computeClusters();
|
|
370
|
+
const data = JSON.stringify({ type: 'delta', data: delta });
|
|
371
|
+
for (const client of this.clients.values()) {
|
|
372
|
+
client.send(data);
|
|
373
|
+
client.lastSeq = this.tracker.currentSeq();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
this.tracker.resetDelta();
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
pingClients() {
|
|
380
|
+
for (const [id, client] of this.clients.entries()) {
|
|
381
|
+
if (!client.alive) {
|
|
382
|
+
this.clients.delete(id);
|
|
383
|
+
client.close();
|
|
384
|
+
this.emit('client:disconnected', id);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
client.alive = false;
|
|
388
|
+
// Send ping frame
|
|
389
|
+
try {
|
|
390
|
+
const ping = Buffer.alloc(2);
|
|
391
|
+
ping[0] = 0x89; // FIN + ping
|
|
392
|
+
ping[1] = 0x00;
|
|
393
|
+
// We can't write directly to socket from here, so mark alive = false
|
|
394
|
+
// and wait for next data activity
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
// ignore
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
exports.DashboardServer = DashboardServer;
|
|
403
|
+
//# sourceMappingURL=dashboard-server.js.map
|