@trentapps/manager-protocol 1.3.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/LICENSE +21 -0
- package/README.md +639 -0
- package/dist/analyzers/ArchitectureDetector.d.ts +44 -0
- package/dist/analyzers/ArchitectureDetector.d.ts.map +1 -0
- package/dist/analyzers/ArchitectureDetector.js +218 -0
- package/dist/analyzers/ArchitectureDetector.js.map +1 -0
- package/dist/analyzers/CSSAnalyzer.d.ts +284 -0
- package/dist/analyzers/CSSAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/CSSAnalyzer.js +1180 -0
- package/dist/analyzers/CSSAnalyzer.js.map +1 -0
- package/dist/analyzers/index.d.ts +5 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/index.js +5 -0
- package/dist/analyzers/index.js.map +1 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +174 -0
- package/dist/cli.js.map +1 -0
- package/dist/design-system/index.d.ts +6 -0
- package/dist/design-system/index.d.ts.map +1 -0
- package/dist/design-system/index.js +6 -0
- package/dist/design-system/index.js.map +1 -0
- package/dist/design-system/tokens.d.ts +106 -0
- package/dist/design-system/tokens.d.ts.map +1 -0
- package/dist/design-system/tokens.js +554 -0
- package/dist/design-system/tokens.js.map +1 -0
- package/dist/engine/AuditLogger.d.ts +506 -0
- package/dist/engine/AuditLogger.d.ts.map +1 -0
- package/dist/engine/AuditLogger.js +1491 -0
- package/dist/engine/AuditLogger.js.map +1 -0
- package/dist/engine/GitHubApprovalManager.d.ts +123 -0
- package/dist/engine/GitHubApprovalManager.d.ts.map +1 -0
- package/dist/engine/GitHubApprovalManager.js +347 -0
- package/dist/engine/GitHubApprovalManager.js.map +1 -0
- package/dist/engine/GitHubClient.d.ts +183 -0
- package/dist/engine/GitHubClient.d.ts.map +1 -0
- package/dist/engine/GitHubClient.js +411 -0
- package/dist/engine/GitHubClient.js.map +1 -0
- package/dist/engine/RateLimiter.d.ts +81 -0
- package/dist/engine/RateLimiter.d.ts.map +1 -0
- package/dist/engine/RateLimiter.js +215 -0
- package/dist/engine/RateLimiter.js.map +1 -0
- package/dist/engine/RuleDependencyAnalyzer.d.ts +73 -0
- package/dist/engine/RuleDependencyAnalyzer.d.ts.map +1 -0
- package/dist/engine/RuleDependencyAnalyzer.js +475 -0
- package/dist/engine/RuleDependencyAnalyzer.js.map +1 -0
- package/dist/engine/RulesEngine.d.ts +176 -0
- package/dist/engine/RulesEngine.d.ts.map +1 -0
- package/dist/engine/RulesEngine.js +705 -0
- package/dist/engine/RulesEngine.js.map +1 -0
- package/dist/engine/TaskManager.d.ts +174 -0
- package/dist/engine/TaskManager.d.ts.map +1 -0
- package/dist/engine/TaskManager.js +663 -0
- package/dist/engine/TaskManager.js.map +1 -0
- package/dist/engine/index.d.ts +11 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +13 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/architecture.d.ts +9 -0
- package/dist/rules/architecture.d.ts.map +1 -0
- package/dist/rules/architecture.js +322 -0
- package/dist/rules/architecture.js.map +1 -0
- package/dist/rules/azure.d.ts +7 -0
- package/dist/rules/azure.d.ts.map +1 -0
- package/dist/rules/azure.js +136 -0
- package/dist/rules/azure.js.map +1 -0
- package/dist/rules/compliance.d.ts +9 -0
- package/dist/rules/compliance.d.ts.map +1 -0
- package/dist/rules/compliance.js +286 -0
- package/dist/rules/compliance.js.map +1 -0
- package/dist/rules/condition-optimizer.d.ts +151 -0
- package/dist/rules/condition-optimizer.d.ts.map +1 -0
- package/dist/rules/condition-optimizer.js +479 -0
- package/dist/rules/condition-optimizer.js.map +1 -0
- package/dist/rules/css.d.ts +10 -0
- package/dist/rules/css.d.ts.map +1 -0
- package/dist/rules/css.js +1777 -0
- package/dist/rules/css.js.map +1 -0
- package/dist/rules/field-standards.d.ts +1172 -0
- package/dist/rules/field-standards.d.ts.map +1 -0
- package/dist/rules/field-standards.js +908 -0
- package/dist/rules/field-standards.js.map +1 -0
- package/dist/rules/flask.d.ts +7 -0
- package/dist/rules/flask.d.ts.map +1 -0
- package/dist/rules/flask.js +142 -0
- package/dist/rules/flask.js.map +1 -0
- package/dist/rules/index.d.ts +827 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +556 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/ml-ai.d.ts +7 -0
- package/dist/rules/ml-ai.d.ts.map +1 -0
- package/dist/rules/ml-ai.js +148 -0
- package/dist/rules/ml-ai.js.map +1 -0
- package/dist/rules/operational.d.ts +9 -0
- package/dist/rules/operational.d.ts.map +1 -0
- package/dist/rules/operational.js +318 -0
- package/dist/rules/operational.js.map +1 -0
- package/dist/rules/patterns.d.ts +568 -0
- package/dist/rules/patterns.d.ts.map +1 -0
- package/dist/rules/patterns.js +1359 -0
- package/dist/rules/patterns.js.map +1 -0
- package/dist/rules/security.d.ts +9 -0
- package/dist/rules/security.d.ts.map +1 -0
- package/dist/rules/security.js +848 -0
- package/dist/rules/security.js.map +1 -0
- package/dist/rules/shared-patterns.d.ts +268 -0
- package/dist/rules/shared-patterns.d.ts.map +1 -0
- package/dist/rules/shared-patterns.js +556 -0
- package/dist/rules/shared-patterns.js.map +1 -0
- package/dist/rules/storage.d.ts +13 -0
- package/dist/rules/storage.d.ts.map +1 -0
- package/dist/rules/storage.js +672 -0
- package/dist/rules/storage.js.map +1 -0
- package/dist/rules/stripe.d.ts +7 -0
- package/dist/rules/stripe.d.ts.map +1 -0
- package/dist/rules/stripe.js +133 -0
- package/dist/rules/stripe.js.map +1 -0
- package/dist/rules/testing.d.ts +7 -0
- package/dist/rules/testing.d.ts.map +1 -0
- package/dist/rules/testing.js +135 -0
- package/dist/rules/testing.js.map +1 -0
- package/dist/rules/ux.d.ts +9 -0
- package/dist/rules/ux.d.ts.map +1 -0
- package/dist/rules/ux.js +280 -0
- package/dist/rules/ux.js.map +1 -0
- package/dist/rules/websocket.d.ts +7 -0
- package/dist/rules/websocket.d.ts.map +1 -0
- package/dist/rules/websocket.js +128 -0
- package/dist/rules/websocket.js.map +1 -0
- package/dist/server.d.ts +43 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1967 -0
- package/dist/server.js.map +1 -0
- package/dist/supervisor/AgentSupervisor.d.ts +195 -0
- package/dist/supervisor/AgentSupervisor.d.ts.map +1 -0
- package/dist/supervisor/AgentSupervisor.js +569 -0
- package/dist/supervisor/AgentSupervisor.js.map +1 -0
- package/dist/supervisor/ManagedServerRegistry.d.ts +185 -0
- package/dist/supervisor/ManagedServerRegistry.d.ts.map +1 -0
- package/dist/supervisor/ManagedServerRegistry.js +729 -0
- package/dist/supervisor/ManagedServerRegistry.js.map +1 -0
- package/dist/supervisor/ProjectTracker.d.ts +210 -0
- package/dist/supervisor/ProjectTracker.d.ts.map +1 -0
- package/dist/supervisor/ProjectTracker.js +709 -0
- package/dist/supervisor/ProjectTracker.js.map +1 -0
- package/dist/supervisor/index.d.ts +6 -0
- package/dist/supervisor/index.d.ts.map +1 -0
- package/dist/supervisor/index.js +6 -0
- package/dist/supervisor/index.js.map +1 -0
- package/dist/testing/index.d.ts +11 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +12 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/rule-tester.d.ts +217 -0
- package/dist/testing/rule-tester.d.ts.map +1 -0
- package/dist/testing/rule-tester.examples.d.ts +57 -0
- package/dist/testing/rule-tester.examples.d.ts.map +1 -0
- package/dist/testing/rule-tester.examples.js +375 -0
- package/dist/testing/rule-tester.examples.js.map +1 -0
- package/dist/testing/rule-tester.js +381 -0
- package/dist/testing/rule-tester.js.map +1 -0
- package/dist/testing/rule-validator.d.ts +141 -0
- package/dist/testing/rule-validator.d.ts.map +1 -0
- package/dist/testing/rule-validator.js +640 -0
- package/dist/testing/rule-validator.js.map +1 -0
- package/dist/types/index.d.ts +1282 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +386 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/errors.d.ts +86 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +171 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +7 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/rate-limiting.d.ts +268 -0
- package/dist/utils/rate-limiting.d.ts.map +1 -0
- package/dist/utils/rate-limiting.js +403 -0
- package/dist/utils/rate-limiting.js.map +1 -0
- package/dist/utils/shared.d.ts +306 -0
- package/dist/utils/shared.d.ts.map +1 -0
- package/dist/utils/shared.js +464 -0
- package/dist/utils/shared.js.map +1 -0
- package/dist/utils/shell.d.ts +22 -0
- package/dist/utils/shell.d.ts.map +1 -0
- package/dist/utils/shell.js +29 -0
- package/dist/utils/shell.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed Server Registry
|
|
3
|
+
*
|
|
4
|
+
* Allows registering servers (ip + sourceDir) and checking online status.
|
|
5
|
+
* Features:
|
|
6
|
+
* - Connection pooling for TCP checks (reuse connections, timeout limits)
|
|
7
|
+
* - Caching for health check results (short TTL for success, no cache for failures)
|
|
8
|
+
* - Rate limiting with exponential backoff for repeated failures
|
|
9
|
+
*/
|
|
10
|
+
import { promises as fsp } from 'fs';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import net from 'net';
|
|
14
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
15
|
+
/**
|
|
16
|
+
* TCP Connection Pool
|
|
17
|
+
* Manages reusable TCP connections with automatic cleanup
|
|
18
|
+
*/
|
|
19
|
+
class TCPConnectionPool {
|
|
20
|
+
pools = new Map();
|
|
21
|
+
config;
|
|
22
|
+
cleanupTimer = null;
|
|
23
|
+
constructor(config = {}) {
|
|
24
|
+
this.config = {
|
|
25
|
+
maxConnectionsPerEndpoint: config.maxConnectionsPerEndpoint ?? 5,
|
|
26
|
+
connectionIdleTimeoutMs: config.connectionIdleTimeoutMs ?? 30000,
|
|
27
|
+
maxConnectionAgeMs: config.maxConnectionAgeMs ?? 60000,
|
|
28
|
+
cleanupIntervalMs: config.cleanupIntervalMs ?? 10000
|
|
29
|
+
};
|
|
30
|
+
this.startCleanupTimer();
|
|
31
|
+
}
|
|
32
|
+
getKey(host, port) {
|
|
33
|
+
return `${host}:${port}`;
|
|
34
|
+
}
|
|
35
|
+
startCleanupTimer() {
|
|
36
|
+
if (this.cleanupTimer)
|
|
37
|
+
return;
|
|
38
|
+
this.cleanupTimer = setInterval(() => {
|
|
39
|
+
this.cleanupStaleConnections();
|
|
40
|
+
}, this.config.cleanupIntervalMs);
|
|
41
|
+
// Don't block process exit
|
|
42
|
+
this.cleanupTimer.unref();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Clean up stale and idle connections
|
|
46
|
+
*/
|
|
47
|
+
cleanupStaleConnections() {
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
let cleaned = 0;
|
|
50
|
+
for (const [key, connections] of this.pools.entries()) {
|
|
51
|
+
const validConnections = [];
|
|
52
|
+
for (const conn of connections) {
|
|
53
|
+
const age = now - conn.createdAt;
|
|
54
|
+
const idle = now - conn.lastUsedAt;
|
|
55
|
+
// Remove if too old, too idle, or in error state
|
|
56
|
+
if (age > this.config.maxConnectionAgeMs ||
|
|
57
|
+
idle > this.config.connectionIdleTimeoutMs ||
|
|
58
|
+
conn.socket.destroyed) {
|
|
59
|
+
this.destroyConnection(conn);
|
|
60
|
+
cleaned++;
|
|
61
|
+
}
|
|
62
|
+
else if (!conn.inUse) {
|
|
63
|
+
validConnections.push(conn);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
validConnections.push(conn);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (validConnections.length === 0) {
|
|
70
|
+
this.pools.delete(key);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
this.pools.set(key, validConnections);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return cleaned;
|
|
77
|
+
}
|
|
78
|
+
destroyConnection(conn) {
|
|
79
|
+
try {
|
|
80
|
+
if (!conn.socket.destroyed) {
|
|
81
|
+
conn.socket.destroy();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Ignore destruction errors
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get or create a connection to the specified endpoint
|
|
90
|
+
*/
|
|
91
|
+
async getConnection(host, port, timeoutMs) {
|
|
92
|
+
const key = this.getKey(host, port);
|
|
93
|
+
const pool = this.pools.get(key) || [];
|
|
94
|
+
// Try to find an available connection
|
|
95
|
+
for (const conn of pool) {
|
|
96
|
+
if (!conn.inUse && !conn.socket.destroyed) {
|
|
97
|
+
conn.inUse = true;
|
|
98
|
+
conn.lastUsedAt = Date.now();
|
|
99
|
+
return { socket: conn.socket, reused: true };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Create new connection if under limit
|
|
103
|
+
if (pool.length < this.config.maxConnectionsPerEndpoint) {
|
|
104
|
+
const socket = await this.createConnection(host, port, timeoutMs);
|
|
105
|
+
const conn = {
|
|
106
|
+
socket,
|
|
107
|
+
host,
|
|
108
|
+
port,
|
|
109
|
+
createdAt: Date.now(),
|
|
110
|
+
lastUsedAt: Date.now(),
|
|
111
|
+
inUse: true
|
|
112
|
+
};
|
|
113
|
+
pool.push(conn);
|
|
114
|
+
this.pools.set(key, pool);
|
|
115
|
+
return { socket, reused: false };
|
|
116
|
+
}
|
|
117
|
+
// All connections in use, create a temporary one (won't be pooled)
|
|
118
|
+
const socket = await this.createConnection(host, port, timeoutMs);
|
|
119
|
+
return { socket, reused: false };
|
|
120
|
+
}
|
|
121
|
+
createConnection(host, port, timeoutMs) {
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const socket = new net.Socket();
|
|
124
|
+
let settled = false;
|
|
125
|
+
const settle = (error) => {
|
|
126
|
+
if (settled)
|
|
127
|
+
return;
|
|
128
|
+
settled = true;
|
|
129
|
+
if (error) {
|
|
130
|
+
try {
|
|
131
|
+
socket.destroy();
|
|
132
|
+
}
|
|
133
|
+
catch { /* noop */ }
|
|
134
|
+
reject(error);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
resolve(socket);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
socket.setTimeout(timeoutMs);
|
|
141
|
+
socket.once('connect', () => settle());
|
|
142
|
+
socket.once('timeout', () => settle(new Error('Connection timeout')));
|
|
143
|
+
socket.once('error', (err) => settle(err));
|
|
144
|
+
try {
|
|
145
|
+
socket.connect(port, host);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
settle(err instanceof Error ? err : new Error(String(err)));
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Release a connection back to the pool
|
|
154
|
+
*/
|
|
155
|
+
releaseConnection(host, port, socket, keepAlive = true) {
|
|
156
|
+
const key = this.getKey(host, port);
|
|
157
|
+
const pool = this.pools.get(key);
|
|
158
|
+
if (!pool) {
|
|
159
|
+
// Connection wasn't pooled, destroy it
|
|
160
|
+
try {
|
|
161
|
+
socket.destroy();
|
|
162
|
+
}
|
|
163
|
+
catch { /* noop */ }
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const conn = pool.find(c => c.socket === socket);
|
|
167
|
+
if (conn) {
|
|
168
|
+
conn.inUse = false;
|
|
169
|
+
conn.lastUsedAt = Date.now();
|
|
170
|
+
if (!keepAlive || socket.destroyed) {
|
|
171
|
+
// Remove from pool and destroy
|
|
172
|
+
const idx = pool.indexOf(conn);
|
|
173
|
+
if (idx !== -1)
|
|
174
|
+
pool.splice(idx, 1);
|
|
175
|
+
this.destroyConnection(conn);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// Not a pooled connection, destroy it
|
|
180
|
+
try {
|
|
181
|
+
socket.destroy();
|
|
182
|
+
}
|
|
183
|
+
catch { /* noop */ }
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Get pool statistics
|
|
188
|
+
*/
|
|
189
|
+
getStats() {
|
|
190
|
+
let total = 0;
|
|
191
|
+
let active = 0;
|
|
192
|
+
for (const pool of this.pools.values()) {
|
|
193
|
+
total += pool.length;
|
|
194
|
+
active += pool.filter(c => c.inUse).length;
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
totalConnections: total,
|
|
198
|
+
activeConnections: active,
|
|
199
|
+
endpoints: this.pools.size
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Destroy all connections and stop cleanup timer
|
|
204
|
+
*/
|
|
205
|
+
destroy() {
|
|
206
|
+
if (this.cleanupTimer) {
|
|
207
|
+
clearInterval(this.cleanupTimer);
|
|
208
|
+
this.cleanupTimer = null;
|
|
209
|
+
}
|
|
210
|
+
for (const pool of this.pools.values()) {
|
|
211
|
+
for (const conn of pool) {
|
|
212
|
+
this.destroyConnection(conn);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
this.pools.clear();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Health Check Cache
|
|
220
|
+
* Caches successful TCP check results with LRU eviction
|
|
221
|
+
*/
|
|
222
|
+
class HealthCheckCache {
|
|
223
|
+
cache = new Map();
|
|
224
|
+
config;
|
|
225
|
+
constructor(config = {}) {
|
|
226
|
+
this.config = {
|
|
227
|
+
successTtlMs: config.successTtlMs ?? 5000,
|
|
228
|
+
cacheFailures: config.cacheFailures ?? false,
|
|
229
|
+
failureTtlMs: config.failureTtlMs ?? 1000,
|
|
230
|
+
maxEntries: config.maxEntries ?? 1000
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
getKey(host, port) {
|
|
234
|
+
return `${host}:${port}`;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Get cached result if valid
|
|
238
|
+
*/
|
|
239
|
+
get(host, port) {
|
|
240
|
+
const key = this.getKey(host, port);
|
|
241
|
+
const entry = this.cache.get(key);
|
|
242
|
+
if (!entry)
|
|
243
|
+
return null;
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
const age = now - entry.timestamp;
|
|
246
|
+
const ttl = entry.result ? this.config.successTtlMs : this.config.failureTtlMs;
|
|
247
|
+
if (age > ttl) {
|
|
248
|
+
this.cache.delete(key);
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
// Update access tracking for LRU
|
|
252
|
+
entry.accessCount++;
|
|
253
|
+
entry.lastAccessedAt = now;
|
|
254
|
+
return { result: entry.result, ageMs: age };
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Store a result in cache
|
|
258
|
+
*/
|
|
259
|
+
set(host, port, result) {
|
|
260
|
+
// Don't cache failures unless configured to
|
|
261
|
+
if (!result && !this.config.cacheFailures) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
// Enforce max entries with LRU eviction
|
|
265
|
+
if (this.cache.size >= this.config.maxEntries) {
|
|
266
|
+
this.evictLRU();
|
|
267
|
+
}
|
|
268
|
+
const key = this.getKey(host, port);
|
|
269
|
+
const now = Date.now();
|
|
270
|
+
this.cache.set(key, {
|
|
271
|
+
result,
|
|
272
|
+
timestamp: now,
|
|
273
|
+
accessCount: 1,
|
|
274
|
+
lastAccessedAt: now
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Evict least recently used entries
|
|
279
|
+
*/
|
|
280
|
+
evictLRU() {
|
|
281
|
+
// Evict 10% of entries
|
|
282
|
+
const toEvict = Math.ceil(this.config.maxEntries * 0.1);
|
|
283
|
+
const entries = Array.from(this.cache.entries())
|
|
284
|
+
.sort((a, b) => a[1].lastAccessedAt - b[1].lastAccessedAt);
|
|
285
|
+
for (let i = 0; i < toEvict && i < entries.length; i++) {
|
|
286
|
+
this.cache.delete(entries[i][0]);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Invalidate cache for a specific endpoint
|
|
291
|
+
*/
|
|
292
|
+
invalidate(host, port) {
|
|
293
|
+
return this.cache.delete(this.getKey(host, port));
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Clear all cached entries
|
|
297
|
+
*/
|
|
298
|
+
clear() {
|
|
299
|
+
this.cache.clear();
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Get cache statistics
|
|
303
|
+
*/
|
|
304
|
+
getStats() {
|
|
305
|
+
let totalAccesses = 0;
|
|
306
|
+
for (const entry of this.cache.values()) {
|
|
307
|
+
totalAccesses += entry.accessCount;
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
entries: this.cache.size,
|
|
311
|
+
hitRate: this.cache.size > 0 ? totalAccesses / this.cache.size : undefined
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Rate Limiter with Exponential Backoff
|
|
317
|
+
* Prevents hammering unhealthy servers
|
|
318
|
+
*/
|
|
319
|
+
class TCPRateLimiter {
|
|
320
|
+
states = new Map();
|
|
321
|
+
config;
|
|
322
|
+
constructor(config = {}) {
|
|
323
|
+
this.config = {
|
|
324
|
+
initialDelayMs: config.initialDelayMs ?? 1000,
|
|
325
|
+
maxDelayMs: config.maxDelayMs ?? 60000,
|
|
326
|
+
backoffMultiplier: config.backoffMultiplier ?? 2,
|
|
327
|
+
failureThreshold: config.failureThreshold ?? 3,
|
|
328
|
+
failureWindowMs: config.failureWindowMs ?? 60000
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
getKey(host, port) {
|
|
332
|
+
return `${host}:${port}`;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Check if an endpoint is currently rate limited
|
|
336
|
+
* Returns remaining wait time in ms, or 0 if not limited
|
|
337
|
+
*/
|
|
338
|
+
isRateLimited(host, port) {
|
|
339
|
+
const key = this.getKey(host, port);
|
|
340
|
+
const state = this.states.get(key);
|
|
341
|
+
if (!state)
|
|
342
|
+
return 0;
|
|
343
|
+
const now = Date.now();
|
|
344
|
+
// Reset if outside failure window
|
|
345
|
+
if (now - state.lastFailureAt > this.config.failureWindowMs) {
|
|
346
|
+
this.states.delete(key);
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
// Check if still blocked
|
|
350
|
+
if (now < state.blockedUntil) {
|
|
351
|
+
return state.blockedUntil - now;
|
|
352
|
+
}
|
|
353
|
+
return 0;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Record a successful check - resets failure tracking
|
|
357
|
+
*/
|
|
358
|
+
recordSuccess(host, port) {
|
|
359
|
+
const key = this.getKey(host, port);
|
|
360
|
+
this.states.delete(key);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Record a failed check - may trigger rate limiting
|
|
364
|
+
*/
|
|
365
|
+
recordFailure(host, port) {
|
|
366
|
+
const key = this.getKey(host, port);
|
|
367
|
+
const now = Date.now();
|
|
368
|
+
let state = this.states.get(key);
|
|
369
|
+
if (!state) {
|
|
370
|
+
state = {
|
|
371
|
+
consecutiveFailures: 0,
|
|
372
|
+
lastFailureAt: now,
|
|
373
|
+
currentDelayMs: this.config.initialDelayMs,
|
|
374
|
+
blockedUntil: 0
|
|
375
|
+
};
|
|
376
|
+
this.states.set(key, state);
|
|
377
|
+
}
|
|
378
|
+
// Reset if outside failure window
|
|
379
|
+
if (now - state.lastFailureAt > this.config.failureWindowMs) {
|
|
380
|
+
state.consecutiveFailures = 0;
|
|
381
|
+
state.currentDelayMs = this.config.initialDelayMs;
|
|
382
|
+
}
|
|
383
|
+
state.consecutiveFailures++;
|
|
384
|
+
state.lastFailureAt = now;
|
|
385
|
+
// Apply rate limiting after threshold
|
|
386
|
+
if (state.consecutiveFailures >= this.config.failureThreshold) {
|
|
387
|
+
state.blockedUntil = now + state.currentDelayMs;
|
|
388
|
+
// Exponential backoff for next failure
|
|
389
|
+
state.currentDelayMs = Math.min(state.currentDelayMs * this.config.backoffMultiplier, this.config.maxDelayMs);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Get rate limit state for an endpoint
|
|
394
|
+
*/
|
|
395
|
+
getState(host, port) {
|
|
396
|
+
return this.states.get(this.getKey(host, port));
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Clear rate limiting for an endpoint
|
|
400
|
+
*/
|
|
401
|
+
clear(host, port) {
|
|
402
|
+
return this.states.delete(this.getKey(host, port));
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Clear all rate limiting states
|
|
406
|
+
*/
|
|
407
|
+
clearAll() {
|
|
408
|
+
this.states.clear();
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Get statistics
|
|
412
|
+
*/
|
|
413
|
+
getStats() {
|
|
414
|
+
const now = Date.now();
|
|
415
|
+
let rateLimited = 0;
|
|
416
|
+
for (const state of this.states.values()) {
|
|
417
|
+
if (now < state.blockedUntil) {
|
|
418
|
+
rateLimited++;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
trackedEndpoints: this.states.size,
|
|
423
|
+
rateLimitedEndpoints: rateLimited
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
export class ManagedServerRegistry {
|
|
428
|
+
filePath;
|
|
429
|
+
servers = new Map();
|
|
430
|
+
loaded = false;
|
|
431
|
+
// Connection pooling, caching, and rate limiting
|
|
432
|
+
connectionPool;
|
|
433
|
+
healthCache;
|
|
434
|
+
rateLimiter;
|
|
435
|
+
enablePooling;
|
|
436
|
+
enableCaching;
|
|
437
|
+
enableRateLimiting;
|
|
438
|
+
// Statistics tracking
|
|
439
|
+
stats = {
|
|
440
|
+
totalChecks: 0,
|
|
441
|
+
cacheHits: 0,
|
|
442
|
+
cacheMisses: 0,
|
|
443
|
+
pooledConnections: 0,
|
|
444
|
+
newConnections: 0,
|
|
445
|
+
rateLimitedChecks: 0
|
|
446
|
+
};
|
|
447
|
+
constructor(config = {}) {
|
|
448
|
+
this.filePath = config.filePath || path.join(process.cwd(), 'managed-servers.json');
|
|
449
|
+
// Initialize components with provided config or defaults
|
|
450
|
+
this.connectionPool = new TCPConnectionPool(config.connectionPool);
|
|
451
|
+
this.healthCache = new HealthCheckCache(config.cache);
|
|
452
|
+
this.rateLimiter = new TCPRateLimiter(config.rateLimit);
|
|
453
|
+
// Feature flags
|
|
454
|
+
this.enablePooling = config.enablePooling ?? true;
|
|
455
|
+
this.enableCaching = config.enableCaching ?? true;
|
|
456
|
+
this.enableRateLimiting = config.enableRateLimiting ?? true;
|
|
457
|
+
}
|
|
458
|
+
async ensureLoaded() {
|
|
459
|
+
if (this.loaded)
|
|
460
|
+
return;
|
|
461
|
+
try {
|
|
462
|
+
const data = await fsp.readFile(this.filePath, 'utf8');
|
|
463
|
+
const parsed = JSON.parse(data);
|
|
464
|
+
for (const s of parsed)
|
|
465
|
+
this.servers.set(s.id, s);
|
|
466
|
+
}
|
|
467
|
+
catch (err) {
|
|
468
|
+
// If file doesn't exist, start empty
|
|
469
|
+
if (err.code !== 'ENOENT') {
|
|
470
|
+
// eslint-disable-next-line no-console
|
|
471
|
+
console.error('Failed to load managed servers:', err);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
finally {
|
|
475
|
+
this.loaded = true;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async persist() {
|
|
479
|
+
const list = Array.from(this.servers.values());
|
|
480
|
+
await fsp.writeFile(this.filePath, JSON.stringify(list, null, 2), 'utf8');
|
|
481
|
+
}
|
|
482
|
+
async addServer(params) {
|
|
483
|
+
await this.ensureLoaded();
|
|
484
|
+
const server = {
|
|
485
|
+
id: uuidv4(),
|
|
486
|
+
ip: params.ip,
|
|
487
|
+
sourceDir: params.sourceDir,
|
|
488
|
+
name: params.name,
|
|
489
|
+
port: params.port,
|
|
490
|
+
createdAt: new Date().toISOString()
|
|
491
|
+
};
|
|
492
|
+
this.servers.set(server.id, server);
|
|
493
|
+
await this.persist();
|
|
494
|
+
return server;
|
|
495
|
+
}
|
|
496
|
+
async removeServer(id) {
|
|
497
|
+
await this.ensureLoaded();
|
|
498
|
+
const removed = this.servers.delete(id);
|
|
499
|
+
if (removed)
|
|
500
|
+
await this.persist();
|
|
501
|
+
return removed;
|
|
502
|
+
}
|
|
503
|
+
async listServers() {
|
|
504
|
+
await this.ensureLoaded();
|
|
505
|
+
return Array.from(this.servers.values());
|
|
506
|
+
}
|
|
507
|
+
async getServer(id) {
|
|
508
|
+
await this.ensureLoaded();
|
|
509
|
+
return this.servers.get(id);
|
|
510
|
+
}
|
|
511
|
+
async checkStatus(input) {
|
|
512
|
+
await this.ensureLoaded();
|
|
513
|
+
let ip = input.ip;
|
|
514
|
+
let sourceDir = input.sourceDir;
|
|
515
|
+
let id = input.id;
|
|
516
|
+
if (input.id) {
|
|
517
|
+
const found = this.servers.get(input.id);
|
|
518
|
+
if (!found) {
|
|
519
|
+
throw new Error(`Unknown managed server: ${input.id}`);
|
|
520
|
+
}
|
|
521
|
+
ip = found.ip;
|
|
522
|
+
sourceDir = found.sourceDir;
|
|
523
|
+
id = found.id;
|
|
524
|
+
}
|
|
525
|
+
if (!ip || !sourceDir) {
|
|
526
|
+
throw new Error('Must provide either id or both ip and sourceDir');
|
|
527
|
+
}
|
|
528
|
+
// Use custom port if available, otherwise default to common ports
|
|
529
|
+
let defaultPorts = [22, 80, 443];
|
|
530
|
+
if (input.id) {
|
|
531
|
+
const found = this.servers.get(input.id);
|
|
532
|
+
if (found?.port) {
|
|
533
|
+
defaultPorts = [found.port];
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const ports = input.ports && input.ports.length > 0 ? input.ports : defaultPorts;
|
|
537
|
+
const byPort = {};
|
|
538
|
+
let usedCache = false;
|
|
539
|
+
let maxCacheAge = 0;
|
|
540
|
+
// Try TCP connect to each port with short timeout
|
|
541
|
+
await Promise.all(ports.map(async (port) => {
|
|
542
|
+
const { result, cached, cacheAgeMs } = await this.tcpCheckWithFeatures(ip, port, 1000, input.skipCache);
|
|
543
|
+
byPort[port] = result;
|
|
544
|
+
if (cached) {
|
|
545
|
+
usedCache = true;
|
|
546
|
+
maxCacheAge = Math.max(maxCacheAge, cacheAgeMs || 0);
|
|
547
|
+
}
|
|
548
|
+
}));
|
|
549
|
+
const online = Object.values(byPort).some(Boolean);
|
|
550
|
+
const sourceDirExists = fs.existsSync(sourceDir);
|
|
551
|
+
return {
|
|
552
|
+
id,
|
|
553
|
+
ip,
|
|
554
|
+
online,
|
|
555
|
+
ports: byPort,
|
|
556
|
+
sourceDir,
|
|
557
|
+
sourceDirExists,
|
|
558
|
+
checkedAt: new Date().toISOString(),
|
|
559
|
+
cached: usedCache ? true : undefined,
|
|
560
|
+
cacheAgeMs: usedCache ? maxCacheAge : undefined
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* TCP check with connection pooling, caching, and rate limiting
|
|
565
|
+
*/
|
|
566
|
+
async tcpCheckWithFeatures(host, port, timeoutMs, skipCache) {
|
|
567
|
+
this.stats.totalChecks++;
|
|
568
|
+
// Check cache first (unless skipped)
|
|
569
|
+
if (this.enableCaching && !skipCache) {
|
|
570
|
+
const cached = this.healthCache.get(host, port);
|
|
571
|
+
if (cached !== null) {
|
|
572
|
+
this.stats.cacheHits++;
|
|
573
|
+
return { result: cached.result, cached: true, cacheAgeMs: cached.ageMs };
|
|
574
|
+
}
|
|
575
|
+
this.stats.cacheMisses++;
|
|
576
|
+
}
|
|
577
|
+
// Check rate limiting
|
|
578
|
+
if (this.enableRateLimiting) {
|
|
579
|
+
const waitTime = this.rateLimiter.isRateLimited(host, port);
|
|
580
|
+
if (waitTime > 0) {
|
|
581
|
+
this.stats.rateLimitedChecks++;
|
|
582
|
+
// Return false immediately for rate-limited endpoints
|
|
583
|
+
// Don't cache this - it's not a real check result
|
|
584
|
+
return { result: false, cached: false };
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
// Perform actual TCP check
|
|
588
|
+
let result;
|
|
589
|
+
if (this.enablePooling) {
|
|
590
|
+
result = await this.tcpCheckPooled(host, port, timeoutMs);
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
result = await this.tcpCheckDirect(host, port, timeoutMs);
|
|
594
|
+
}
|
|
595
|
+
// Update rate limiter
|
|
596
|
+
if (this.enableRateLimiting) {
|
|
597
|
+
if (result) {
|
|
598
|
+
this.rateLimiter.recordSuccess(host, port);
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
this.rateLimiter.recordFailure(host, port);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
// Cache the result
|
|
605
|
+
if (this.enableCaching) {
|
|
606
|
+
this.healthCache.set(host, port, result);
|
|
607
|
+
}
|
|
608
|
+
return { result, cached: false };
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* TCP check using connection pool
|
|
612
|
+
*/
|
|
613
|
+
async tcpCheckPooled(host, port, timeoutMs) {
|
|
614
|
+
try {
|
|
615
|
+
const { socket, reused } = await this.connectionPool.getConnection(host, port, timeoutMs);
|
|
616
|
+
if (reused) {
|
|
617
|
+
this.stats.pooledConnections++;
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
this.stats.newConnections++;
|
|
621
|
+
}
|
|
622
|
+
// For health check, we just need to verify connection works
|
|
623
|
+
// Release connection back to pool (keep alive for reuse)
|
|
624
|
+
this.connectionPool.releaseConnection(host, port, socket, true);
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Direct TCP check without pooling (original implementation)
|
|
633
|
+
*/
|
|
634
|
+
tcpCheckDirect(host, port, timeoutMs) {
|
|
635
|
+
this.stats.newConnections++;
|
|
636
|
+
return new Promise((resolve) => {
|
|
637
|
+
const socket = new net.Socket();
|
|
638
|
+
let done = false;
|
|
639
|
+
const finalize = (result) => {
|
|
640
|
+
if (done)
|
|
641
|
+
return;
|
|
642
|
+
done = true;
|
|
643
|
+
try {
|
|
644
|
+
socket.destroy();
|
|
645
|
+
}
|
|
646
|
+
catch { /* noop */ }
|
|
647
|
+
resolve(result);
|
|
648
|
+
};
|
|
649
|
+
socket.setTimeout(timeoutMs);
|
|
650
|
+
socket.once('connect', () => finalize(true));
|
|
651
|
+
socket.once('timeout', () => finalize(false));
|
|
652
|
+
socket.once('error', () => finalize(false));
|
|
653
|
+
// Some hosts may refuse connection quickly - still indicates reachability
|
|
654
|
+
socket.once('close', (hadError) => finalize(!hadError));
|
|
655
|
+
try {
|
|
656
|
+
socket.connect(port, host);
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
finalize(false);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Get comprehensive statistics about TCP checks
|
|
665
|
+
*/
|
|
666
|
+
getCheckStats() {
|
|
667
|
+
const totalConnections = this.stats.pooledConnections + this.stats.newConnections;
|
|
668
|
+
return {
|
|
669
|
+
...this.stats,
|
|
670
|
+
cacheHitRate: this.stats.totalChecks > 0
|
|
671
|
+
? this.stats.cacheHits / this.stats.totalChecks
|
|
672
|
+
: 0,
|
|
673
|
+
poolReuseRate: totalConnections > 0
|
|
674
|
+
? this.stats.pooledConnections / totalConnections
|
|
675
|
+
: 0,
|
|
676
|
+
pool: this.connectionPool.getStats(),
|
|
677
|
+
cache: this.healthCache.getStats(),
|
|
678
|
+
rateLimit: this.rateLimiter.getStats()
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Invalidate cache for a specific endpoint
|
|
683
|
+
*/
|
|
684
|
+
invalidateCache(host, port) {
|
|
685
|
+
return this.healthCache.invalidate(host, port);
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Clear all caches
|
|
689
|
+
*/
|
|
690
|
+
clearCache() {
|
|
691
|
+
this.healthCache.clear();
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Clear rate limiting for an endpoint
|
|
695
|
+
*/
|
|
696
|
+
clearRateLimit(host, port) {
|
|
697
|
+
return this.rateLimiter.clear(host, port);
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Clear all rate limiting
|
|
701
|
+
*/
|
|
702
|
+
clearAllRateLimits() {
|
|
703
|
+
this.rateLimiter.clearAll();
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Reset all statistics
|
|
707
|
+
*/
|
|
708
|
+
resetStats() {
|
|
709
|
+
this.stats = {
|
|
710
|
+
totalChecks: 0,
|
|
711
|
+
cacheHits: 0,
|
|
712
|
+
cacheMisses: 0,
|
|
713
|
+
pooledConnections: 0,
|
|
714
|
+
newConnections: 0,
|
|
715
|
+
rateLimitedChecks: 0
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Clean up resources (connection pool, timers)
|
|
720
|
+
*/
|
|
721
|
+
destroy() {
|
|
722
|
+
this.connectionPool.destroy();
|
|
723
|
+
this.healthCache.clear();
|
|
724
|
+
this.rateLimiter.clearAll();
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
// Singleton default registry
|
|
728
|
+
export const managedServerRegistry = new ManagedServerRegistry();
|
|
729
|
+
//# sourceMappingURL=ManagedServerRegistry.js.map
|