@forinda/kickjs-http 0.6.0 → 0.7.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/chunk-PLKCXCBN.js +429 -0
- package/dist/chunk-PLKCXCBN.js.map +1 -0
- package/dist/devtools.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-YYT24FTH.js +0 -226
- package/dist/chunk-YYT24FTH.js.map +0 -1
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__name
|
|
3
|
+
} from "./chunk-WCQVDF3K.js";
|
|
4
|
+
|
|
5
|
+
// src/devtools.ts
|
|
6
|
+
import { Router } from "express";
|
|
7
|
+
|
|
8
|
+
// src/devtools/dashboard.ts
|
|
9
|
+
function renderDashboard(basePath) {
|
|
10
|
+
return `<!DOCTYPE html>
|
|
11
|
+
<html lang="en">
|
|
12
|
+
<head>
|
|
13
|
+
<meta charset="utf-8">
|
|
14
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
15
|
+
<title>KickJS DevTools</title>
|
|
16
|
+
<style>
|
|
17
|
+
${CSS}
|
|
18
|
+
</style>
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
${BODY}
|
|
22
|
+
<script>
|
|
23
|
+
const BASE = '${basePath}';
|
|
24
|
+
const POLL_MS = 30000;
|
|
25
|
+
${SCRIPT}
|
|
26
|
+
</script>
|
|
27
|
+
</body>
|
|
28
|
+
</html>`;
|
|
29
|
+
}
|
|
30
|
+
__name(renderDashboard, "renderDashboard");
|
|
31
|
+
var CSS = `
|
|
32
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
33
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 24px; }
|
|
34
|
+
h1 { font-size: 24px; margin-bottom: 8px; color: #38bdf8; }
|
|
35
|
+
.subtitle { color: #64748b; font-size: 14px; margin-bottom: 24px; }
|
|
36
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
|
37
|
+
.card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; }
|
|
38
|
+
.card h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 1px; color: #94a3b8; margin-bottom: 12px; }
|
|
39
|
+
.stat { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #334155; }
|
|
40
|
+
.stat:last-child { border-bottom: none; }
|
|
41
|
+
.stat-label { color: #94a3b8; }
|
|
42
|
+
.stat-value { font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
43
|
+
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: 600; }
|
|
44
|
+
.badge-green { background: #065f46; color: #6ee7b7; }
|
|
45
|
+
.badge-red { background: #7f1d1d; color: #fca5a5; }
|
|
46
|
+
.badge-blue { background: #1e3a5f; color: #93c5fd; }
|
|
47
|
+
.badge-yellow { background: #713f12; color: #fcd34d; }
|
|
48
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
49
|
+
th { text-align: left; padding: 8px; color: #94a3b8; border-bottom: 2px solid #334155; font-weight: 600; }
|
|
50
|
+
td { padding: 8px; border-bottom: 1px solid #1e293b; }
|
|
51
|
+
.method { font-weight: 700; font-size: 11px; }
|
|
52
|
+
.method-get { color: #34d399; }
|
|
53
|
+
.method-post { color: #60a5fa; }
|
|
54
|
+
.method-put { color: #fbbf24; }
|
|
55
|
+
.method-delete { color: #f87171; }
|
|
56
|
+
.method-patch { color: #a78bfa; }
|
|
57
|
+
.refresh-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
|
58
|
+
.refresh-info { font-size: 12px; color: #64748b; }
|
|
59
|
+
.pulse { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #34d399; margin-right: 6px; animation: pulse 2s infinite; }
|
|
60
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
|
61
|
+
.empty { color: #64748b; font-style: italic; padding: 12px 0; }
|
|
62
|
+
`;
|
|
63
|
+
var BODY = `
|
|
64
|
+
<h1>\u26A1 KickJS DevTools</h1>
|
|
65
|
+
<div class="refresh-bar">
|
|
66
|
+
<div class="subtitle">Development introspection dashboard</div>
|
|
67
|
+
<div class="refresh-info"><span class="pulse"></span>Auto-refresh every 30s \xB7 <span id="lastUpdate">loading...</span></div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="grid">
|
|
71
|
+
<div class="card">
|
|
72
|
+
<h2>Health</h2>
|
|
73
|
+
<div id="health"><div class="empty">Loading...</div></div>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="card">
|
|
76
|
+
<h2>Metrics</h2>
|
|
77
|
+
<div id="metrics"><div class="empty">Loading...</div></div>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="card">
|
|
80
|
+
<h2>WebSocket</h2>
|
|
81
|
+
<div id="ws"><div class="empty">Loading...</div></div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div class="card" style="margin-bottom: 16px;">
|
|
86
|
+
<h2>Routes (<span id="routeCount">0</span>)</h2>
|
|
87
|
+
<div id="routes" style="overflow-x: auto;"><div class="empty">Loading...</div></div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div class="card">
|
|
91
|
+
<h2>DI Container (<span id="diCount">0</span>)</h2>
|
|
92
|
+
<div id="container" style="overflow-x: auto;"><div class="empty">Loading...</div></div>
|
|
93
|
+
</div>
|
|
94
|
+
`;
|
|
95
|
+
var SCRIPT = `
|
|
96
|
+
async function fetchJSON(path) {
|
|
97
|
+
try { const r = await fetch(BASE + path); return r.ok ? r.json() : null; } catch { return null; }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function stat(label, value) {
|
|
101
|
+
return '<div class="stat"><span class="stat-label">' + label + '</span><span class="stat-value">' + value + '</span></div>';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function badge(text, type) {
|
|
105
|
+
return '<span class="badge badge-' + type + '">' + text + '</span>';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function methodClass(m) { return 'method method-' + m.toLowerCase(); }
|
|
109
|
+
|
|
110
|
+
async function refresh() {
|
|
111
|
+
const [health, metrics, routes, container, ws] = await Promise.all([
|
|
112
|
+
fetchJSON('/health'), fetchJSON('/metrics'), fetchJSON('/routes'),
|
|
113
|
+
fetchJSON('/container'), fetchJSON('/ws'),
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
if (health) {
|
|
117
|
+
const statusBadge = health.status === 'healthy' ? badge('healthy', 'green') : badge('degraded', 'red');
|
|
118
|
+
let html = stat('Status', statusBadge);
|
|
119
|
+
html += stat('Uptime', formatDuration(health.uptime));
|
|
120
|
+
html += stat('Error Rate', (health.errorRate * 100).toFixed(2) + '%');
|
|
121
|
+
if (health.adapters) {
|
|
122
|
+
Object.entries(health.adapters).forEach(function(e) {
|
|
123
|
+
html += stat(e[0], badge(e[1], e[1] === 'running' ? 'green' : 'yellow'));
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
document.getElementById('health').innerHTML = html;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (metrics) {
|
|
130
|
+
let html = stat('Total Requests', metrics.requests.toLocaleString());
|
|
131
|
+
html += stat('Server Errors (5xx)', metrics.serverErrors);
|
|
132
|
+
html += stat('Client Errors (4xx)', metrics.clientErrors);
|
|
133
|
+
html += stat('Error Rate', (metrics.errorRate * 100).toFixed(2) + '%');
|
|
134
|
+
html += stat('Uptime', formatDuration(metrics.uptimeSeconds));
|
|
135
|
+
html += stat('Started', new Date(metrics.startedAt).toLocaleTimeString());
|
|
136
|
+
document.getElementById('metrics').innerHTML = html;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (ws) {
|
|
140
|
+
if (!ws.enabled) {
|
|
141
|
+
document.getElementById('ws').innerHTML = '<div class="empty">No WsAdapter</div>';
|
|
142
|
+
} else {
|
|
143
|
+
let html = stat('Active Connections', ws.activeConnections);
|
|
144
|
+
html += stat('Total Connections', ws.totalConnections);
|
|
145
|
+
html += stat('Messages In', ws.messagesReceived);
|
|
146
|
+
html += stat('Messages Out', ws.messagesSent);
|
|
147
|
+
html += stat('Errors', ws.errors);
|
|
148
|
+
if (ws.namespaces) {
|
|
149
|
+
Object.entries(ws.namespaces).forEach(function(e) {
|
|
150
|
+
html += stat(e[0], e[1].connections + ' conn / ' + e[1].handlers + ' handlers');
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
document.getElementById('ws').innerHTML = html;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (routes) {
|
|
158
|
+
document.getElementById('routeCount').textContent = routes.routes.length;
|
|
159
|
+
if (routes.routes.length === 0) {
|
|
160
|
+
document.getElementById('routes').innerHTML = '<div class="empty">No routes registered</div>';
|
|
161
|
+
} else {
|
|
162
|
+
let html = '<table><tr><th>Method</th><th>Path</th><th>Controller</th><th>Handler</th><th>Middleware</th></tr>';
|
|
163
|
+
routes.routes.forEach(function(r) {
|
|
164
|
+
html += '<tr><td class="' + methodClass(r.method) + '">' + r.method + '</td>';
|
|
165
|
+
html += '<td><code>' + r.path + '</code></td>';
|
|
166
|
+
html += '<td>' + r.controller + '</td>';
|
|
167
|
+
html += '<td>' + r.handler + '</td>';
|
|
168
|
+
html += '<td>' + (r.middleware.length ? r.middleware.join(', ') : '\u2014') + '</td></tr>';
|
|
169
|
+
});
|
|
170
|
+
html += '</table>';
|
|
171
|
+
document.getElementById('routes').innerHTML = html;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (container) {
|
|
176
|
+
document.getElementById('diCount').textContent = container.count;
|
|
177
|
+
if (container.count === 0) {
|
|
178
|
+
document.getElementById('container').innerHTML = '<div class="empty">No DI registrations</div>';
|
|
179
|
+
} else {
|
|
180
|
+
let html = '<table><tr><th>Token</th><th>Scope</th><th>Instantiated</th></tr>';
|
|
181
|
+
container.registrations.forEach(function(r) {
|
|
182
|
+
html += '<tr><td><code>' + r.token + '</code></td>';
|
|
183
|
+
html += '<td>' + badge(r.scope, 'blue') + '</td>';
|
|
184
|
+
html += '<td>' + (r.instantiated ? badge('yes', 'green') : badge('no', 'yellow')) + '</td></tr>';
|
|
185
|
+
});
|
|
186
|
+
html += '</table>';
|
|
187
|
+
document.getElementById('container').innerHTML = html;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function formatDuration(seconds) {
|
|
195
|
+
if (seconds < 60) return seconds + 's';
|
|
196
|
+
if (seconds < 3600) return Math.floor(seconds / 60) + 'm ' + (seconds % 60) + 's';
|
|
197
|
+
var h = Math.floor(seconds / 3600);
|
|
198
|
+
var m = Math.floor((seconds % 3600) / 60);
|
|
199
|
+
return h + 'h ' + m + 'm';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
refresh();
|
|
203
|
+
setInterval(refresh, POLL_MS);
|
|
204
|
+
`;
|
|
205
|
+
|
|
206
|
+
// src/devtools.ts
|
|
207
|
+
import { METADATA, ref, computed, reactive, watch, createLogger } from "@forinda/kickjs-core";
|
|
208
|
+
var log = createLogger("DevTools");
|
|
209
|
+
var DevToolsAdapter = class {
|
|
210
|
+
static {
|
|
211
|
+
__name(this, "DevToolsAdapter");
|
|
212
|
+
}
|
|
213
|
+
name = "DevToolsAdapter";
|
|
214
|
+
basePath;
|
|
215
|
+
enabled;
|
|
216
|
+
exposeConfig;
|
|
217
|
+
configPrefixes;
|
|
218
|
+
errorRateThreshold;
|
|
219
|
+
// ── Reactive State ───────────────────────────────────────────────────
|
|
220
|
+
/** Total requests received */
|
|
221
|
+
requestCount;
|
|
222
|
+
/** Total responses with status >= 500 */
|
|
223
|
+
errorCount;
|
|
224
|
+
/** Total responses with status >= 400 and < 500 */
|
|
225
|
+
clientErrorCount;
|
|
226
|
+
/** Server start time */
|
|
227
|
+
startedAt;
|
|
228
|
+
/** Computed error rate (server errors / total requests) */
|
|
229
|
+
errorRate;
|
|
230
|
+
/** Computed uptime in seconds */
|
|
231
|
+
uptimeSeconds;
|
|
232
|
+
/** Per-route latency tracking */
|
|
233
|
+
routeLatency;
|
|
234
|
+
// ── Internal State ───────────────────────────────────────────────────
|
|
235
|
+
routes = [];
|
|
236
|
+
container = null;
|
|
237
|
+
adapterStatuses = {};
|
|
238
|
+
stopErrorWatch = null;
|
|
239
|
+
peerAdapters = [];
|
|
240
|
+
constructor(options = {}) {
|
|
241
|
+
this.basePath = options.basePath ?? "/_debug";
|
|
242
|
+
this.enabled = options.enabled ?? process.env.NODE_ENV !== "production";
|
|
243
|
+
this.exposeConfig = options.exposeConfig ?? false;
|
|
244
|
+
this.configPrefixes = options.configPrefixes ?? [
|
|
245
|
+
"APP_",
|
|
246
|
+
"NODE_ENV"
|
|
247
|
+
];
|
|
248
|
+
this.errorRateThreshold = options.errorRateThreshold ?? 0.5;
|
|
249
|
+
this.peerAdapters = options.adapters ?? [];
|
|
250
|
+
this.requestCount = ref(0);
|
|
251
|
+
this.errorCount = ref(0);
|
|
252
|
+
this.clientErrorCount = ref(0);
|
|
253
|
+
this.startedAt = ref(Date.now());
|
|
254
|
+
this.routeLatency = reactive({});
|
|
255
|
+
this.errorRate = computed(() => this.requestCount.value > 0 ? this.errorCount.value / this.requestCount.value : 0);
|
|
256
|
+
this.uptimeSeconds = computed(() => Math.floor((Date.now() - this.startedAt.value) / 1e3));
|
|
257
|
+
if (options.onErrorRateExceeded) {
|
|
258
|
+
const callback = options.onErrorRateExceeded;
|
|
259
|
+
const threshold = this.errorRateThreshold;
|
|
260
|
+
this.stopErrorWatch = watch(this.errorRate, (rate) => {
|
|
261
|
+
if (rate > threshold) {
|
|
262
|
+
callback(rate);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
} else {
|
|
266
|
+
this.stopErrorWatch = watch(this.errorRate, (rate) => {
|
|
267
|
+
if (rate > this.errorRateThreshold) {
|
|
268
|
+
log.warn(`Error rate elevated: ${(rate * 100).toFixed(1)}%`);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// ── Adapter Lifecycle ────────────────────────────────────────────────
|
|
274
|
+
beforeMount(app, container) {
|
|
275
|
+
if (!this.enabled) return;
|
|
276
|
+
this.container = container;
|
|
277
|
+
this.startedAt.value = Date.now();
|
|
278
|
+
this.routes = [];
|
|
279
|
+
this.adapterStatuses[this.name] = "running";
|
|
280
|
+
const router = Router();
|
|
281
|
+
router.get("/routes", (_req, res) => {
|
|
282
|
+
res.json({
|
|
283
|
+
routes: this.routes
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
router.get("/container", (_req, res) => {
|
|
287
|
+
const registrations = this.container?.getRegistrations() ?? [];
|
|
288
|
+
res.json({
|
|
289
|
+
registrations,
|
|
290
|
+
count: registrations.length
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
router.get("/metrics", (_req, res) => {
|
|
294
|
+
res.json({
|
|
295
|
+
requests: this.requestCount.value,
|
|
296
|
+
serverErrors: this.errorCount.value,
|
|
297
|
+
clientErrors: this.clientErrorCount.value,
|
|
298
|
+
errorRate: this.errorRate.value,
|
|
299
|
+
uptimeSeconds: this.uptimeSeconds.value,
|
|
300
|
+
startedAt: new Date(this.startedAt.value).toISOString(),
|
|
301
|
+
routeLatency: this.routeLatency
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
router.get("/health", (_req, res) => {
|
|
305
|
+
const healthy = this.errorRate.value < this.errorRateThreshold;
|
|
306
|
+
const status = healthy ? "healthy" : "degraded";
|
|
307
|
+
res.status(healthy ? 200 : 503).json({
|
|
308
|
+
status,
|
|
309
|
+
errorRate: this.errorRate.value,
|
|
310
|
+
uptime: this.uptimeSeconds.value,
|
|
311
|
+
adapters: this.adapterStatuses
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
router.get("/state", (_req, res) => {
|
|
315
|
+
const wsAdapter = this.peerAdapters.find((a) => a.name === "WsAdapter" && typeof a.getStats === "function");
|
|
316
|
+
res.json({
|
|
317
|
+
reactive: {
|
|
318
|
+
requestCount: this.requestCount.value,
|
|
319
|
+
errorCount: this.errorCount.value,
|
|
320
|
+
clientErrorCount: this.clientErrorCount.value,
|
|
321
|
+
errorRate: this.errorRate.value,
|
|
322
|
+
uptimeSeconds: this.uptimeSeconds.value,
|
|
323
|
+
startedAt: new Date(this.startedAt.value).toISOString()
|
|
324
|
+
},
|
|
325
|
+
routes: this.routes.length,
|
|
326
|
+
container: this.container?.getRegistrations().length ?? 0,
|
|
327
|
+
routeLatency: this.routeLatency,
|
|
328
|
+
...wsAdapter ? {
|
|
329
|
+
ws: wsAdapter.getStats()
|
|
330
|
+
} : {}
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
router.get("/ws", (_req, res) => {
|
|
334
|
+
const wsAdapter = this.peerAdapters.find((a) => a.name === "WsAdapter" && typeof a.getStats === "function");
|
|
335
|
+
if (!wsAdapter) {
|
|
336
|
+
res.json({
|
|
337
|
+
enabled: false,
|
|
338
|
+
message: "WsAdapter not found"
|
|
339
|
+
});
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
res.json({
|
|
343
|
+
enabled: true,
|
|
344
|
+
...wsAdapter.getStats()
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
if (this.exposeConfig) {
|
|
348
|
+
router.get("/config", (_req, res) => {
|
|
349
|
+
const config = {};
|
|
350
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
351
|
+
if (value === void 0) continue;
|
|
352
|
+
const allowed = this.configPrefixes.some((prefix) => key.startsWith(prefix));
|
|
353
|
+
config[key] = allowed ? value : "[REDACTED]";
|
|
354
|
+
}
|
|
355
|
+
res.json({
|
|
356
|
+
config
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
router.get("/", (_req, res) => {
|
|
361
|
+
res.type("html").send(renderDashboard(this.basePath));
|
|
362
|
+
});
|
|
363
|
+
app.use(this.basePath, router);
|
|
364
|
+
log.info(`DevTools mounted at ${this.basePath}`);
|
|
365
|
+
}
|
|
366
|
+
middleware() {
|
|
367
|
+
if (!this.enabled) return [];
|
|
368
|
+
return [
|
|
369
|
+
{
|
|
370
|
+
handler: /* @__PURE__ */ __name((req, res, next) => {
|
|
371
|
+
const start = Date.now();
|
|
372
|
+
this.requestCount.value++;
|
|
373
|
+
res.on("finish", () => {
|
|
374
|
+
if (res.statusCode >= 500) this.errorCount.value++;
|
|
375
|
+
else if (res.statusCode >= 400) this.clientErrorCount.value++;
|
|
376
|
+
const routeKey = `${req.method} ${req.route?.path ?? req.path}`;
|
|
377
|
+
const elapsed = Date.now() - start;
|
|
378
|
+
if (!this.routeLatency[routeKey]) {
|
|
379
|
+
this.routeLatency[routeKey] = {
|
|
380
|
+
count: 0,
|
|
381
|
+
totalMs: 0,
|
|
382
|
+
minMs: Infinity,
|
|
383
|
+
maxMs: 0
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
const stats = this.routeLatency[routeKey];
|
|
387
|
+
stats.count++;
|
|
388
|
+
stats.totalMs += elapsed;
|
|
389
|
+
stats.minMs = Math.min(stats.minMs, elapsed);
|
|
390
|
+
stats.maxMs = Math.max(stats.maxMs, elapsed);
|
|
391
|
+
});
|
|
392
|
+
next();
|
|
393
|
+
}, "handler"),
|
|
394
|
+
phase: "beforeGlobal"
|
|
395
|
+
}
|
|
396
|
+
];
|
|
397
|
+
}
|
|
398
|
+
onRouteMount(controllerClass, mountPath) {
|
|
399
|
+
if (!this.enabled) return;
|
|
400
|
+
const routes = Reflect.getMetadata(METADATA.ROUTES, controllerClass) ?? [];
|
|
401
|
+
const classMiddleware = Reflect.getMetadata(METADATA.CLASS_MIDDLEWARES, controllerClass) ?? [];
|
|
402
|
+
for (const route of routes) {
|
|
403
|
+
const methodMiddleware = Reflect.getMetadata(METADATA.METHOD_MIDDLEWARES, controllerClass.prototype, route.handlerName) ?? [];
|
|
404
|
+
this.routes.push({
|
|
405
|
+
method: route.method.toUpperCase(),
|
|
406
|
+
path: `${mountPath}${route.path === "/" ? "" : route.path}`,
|
|
407
|
+
controller: controllerClass.name,
|
|
408
|
+
handler: route.handlerName,
|
|
409
|
+
middleware: [
|
|
410
|
+
...classMiddleware.map((m) => m.name || "anonymous"),
|
|
411
|
+
...methodMiddleware.map((m) => m.name || "anonymous")
|
|
412
|
+
]
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
afterStart(_server, _container) {
|
|
417
|
+
if (!this.enabled) return;
|
|
418
|
+
log.info(`DevTools ready \u2014 ${this.routes.length} routes tracked, ${this.container?.getRegistrations().length ?? 0} DI bindings`);
|
|
419
|
+
}
|
|
420
|
+
shutdown() {
|
|
421
|
+
this.stopErrorWatch?.();
|
|
422
|
+
this.adapterStatuses[this.name] = "stopped";
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
export {
|
|
427
|
+
DevToolsAdapter
|
|
428
|
+
};
|
|
429
|
+
//# sourceMappingURL=chunk-PLKCXCBN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/devtools.ts","../src/devtools/dashboard.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport { Router } from 'express'\nimport { renderDashboard } from './devtools/dashboard'\nimport {\n type AppAdapter,\n type AdapterMiddleware,\n type Container,\n METADATA,\n ref,\n computed,\n reactive,\n watch,\n createLogger,\n type Ref,\n type ComputedRef,\n} from '@forinda/kickjs-core'\n\nconst log = createLogger('DevTools')\n\n/** Route metadata collected during mount */\ninterface RouteInfo {\n method: string\n path: string\n controller: string\n handler: string\n middleware: string[]\n}\n\n/** Per-route latency stats */\ninterface RouteStats {\n count: number\n totalMs: number\n minMs: number\n maxMs: number\n}\n\nexport interface DevToolsOptions {\n /** Base path for debug endpoints (default: '/_debug') */\n basePath?: string\n /** Only enable when this is true (default: process.env.NODE_ENV !== 'production') */\n enabled?: boolean\n /** Include environment variables (sanitized) at /_debug/config (default: false) */\n exposeConfig?: boolean\n /** Env var prefixes to expose (default: ['APP_', 'NODE_ENV']). Others are redacted. */\n configPrefixes?: string[]\n /** Callback when error rate exceeds threshold */\n onErrorRateExceeded?: (rate: number) => void\n /** Error rate threshold (default: 0.5 = 50%) */\n errorRateThreshold?: number\n /** Other adapters to discover stats from (e.g., WsAdapter) */\n adapters?: any[]\n}\n\n/**\n * DevToolsAdapter — Vue-style reactive introspection for KickJS applications.\n *\n * Exposes debug endpoints powered by reactive state (ref, computed, watch):\n * - `GET /_debug/routes` — all registered routes with middleware\n * - `GET /_debug/container` — DI registry with scopes and instantiation status\n * - `GET /_debug/metrics` — live request/error counts, error rate, uptime\n * - `GET /_debug/health` — deep health check with adapter status\n * - `GET /_debug/config` — sanitized environment variables (opt-in)\n * - `GET /_debug/state` — full reactive state snapshot\n *\n * @example\n * ```ts\n * import { DevToolsAdapter } from '@forinda/kickjs-http/devtools'\n *\n * bootstrap({\n * modules: [UserModule],\n * adapters: [\n * new DevToolsAdapter({\n * enabled: process.env.NODE_ENV !== 'production',\n * exposeConfig: true,\n * configPrefixes: ['APP_', 'DATABASE_'],\n * }),\n * ],\n * })\n * ```\n */\nexport class DevToolsAdapter implements AppAdapter {\n readonly name = 'DevToolsAdapter'\n\n private basePath: string\n private enabled: boolean\n private exposeConfig: boolean\n private configPrefixes: string[]\n private errorRateThreshold: number\n\n // ── Reactive State ───────────────────────────────────────────────────\n /** Total requests received */\n readonly requestCount: Ref<number>\n /** Total responses with status >= 500 */\n readonly errorCount: Ref<number>\n /** Total responses with status >= 400 and < 500 */\n readonly clientErrorCount: Ref<number>\n /** Server start time */\n readonly startedAt: Ref<number>\n /** Computed error rate (server errors / total requests) */\n readonly errorRate: ComputedRef<number>\n /** Computed uptime in seconds */\n readonly uptimeSeconds: ComputedRef<number>\n /** Per-route latency tracking */\n readonly routeLatency: Record<string, RouteStats>\n\n // ── Internal State ───────────────────────────────────────────────────\n private routes: RouteInfo[] = []\n private container: Container | null = null\n private adapterStatuses: Record<string, string> = {}\n private stopErrorWatch: (() => void) | null = null\n private peerAdapters: any[] = []\n\n constructor(options: DevToolsOptions = {}) {\n this.basePath = options.basePath ?? '/_debug'\n this.enabled = options.enabled ?? process.env.NODE_ENV !== 'production'\n this.exposeConfig = options.exposeConfig ?? false\n this.configPrefixes = options.configPrefixes ?? ['APP_', 'NODE_ENV']\n this.errorRateThreshold = options.errorRateThreshold ?? 0.5\n this.peerAdapters = options.adapters ?? []\n\n // Initialize reactive state\n this.requestCount = ref(0)\n this.errorCount = ref(0)\n this.clientErrorCount = ref(0)\n this.startedAt = ref(Date.now())\n this.routeLatency = reactive({})\n\n this.errorRate = computed(() =>\n this.requestCount.value > 0 ? this.errorCount.value / this.requestCount.value : 0,\n )\n\n this.uptimeSeconds = computed(() => Math.floor((Date.now() - this.startedAt.value) / 1000))\n\n // Watch error rate — log warnings when elevated\n if (options.onErrorRateExceeded) {\n const callback = options.onErrorRateExceeded\n const threshold = this.errorRateThreshold\n this.stopErrorWatch = watch(this.errorRate, (rate) => {\n if (rate > threshold) {\n callback(rate)\n }\n })\n } else {\n this.stopErrorWatch = watch(this.errorRate, (rate) => {\n if (rate > this.errorRateThreshold) {\n log.warn(`Error rate elevated: ${(rate * 100).toFixed(1)}%`)\n }\n })\n }\n }\n\n // ── Adapter Lifecycle ────────────────────────────────────────────────\n\n beforeMount(app: any, container: Container): void {\n if (!this.enabled) return\n\n this.container = container\n this.startedAt.value = Date.now()\n // Clear routes on rebuild/restart to prevent HMR duplication\n this.routes = []\n this.adapterStatuses[this.name] = 'running'\n\n const router = Router()\n\n router.get('/routes', (_req: Request, res: Response) => {\n res.json({ routes: this.routes })\n })\n\n router.get('/container', (_req: Request, res: Response) => {\n const registrations = this.container?.getRegistrations() ?? []\n res.json({ registrations, count: registrations.length })\n })\n\n router.get('/metrics', (_req: Request, res: Response) => {\n res.json({\n requests: this.requestCount.value,\n serverErrors: this.errorCount.value,\n clientErrors: this.clientErrorCount.value,\n errorRate: this.errorRate.value,\n uptimeSeconds: this.uptimeSeconds.value,\n startedAt: new Date(this.startedAt.value).toISOString(),\n routeLatency: this.routeLatency,\n })\n })\n\n router.get('/health', (_req: Request, res: Response) => {\n const healthy = this.errorRate.value < this.errorRateThreshold\n const status = healthy ? 'healthy' : 'degraded'\n\n res.status(healthy ? 200 : 503).json({\n status,\n errorRate: this.errorRate.value,\n uptime: this.uptimeSeconds.value,\n adapters: this.adapterStatuses,\n })\n })\n\n router.get('/state', (_req: Request, res: Response) => {\n const wsAdapter = this.peerAdapters.find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n res.json({\n reactive: {\n requestCount: this.requestCount.value,\n errorCount: this.errorCount.value,\n clientErrorCount: this.clientErrorCount.value,\n errorRate: this.errorRate.value,\n uptimeSeconds: this.uptimeSeconds.value,\n startedAt: new Date(this.startedAt.value).toISOString(),\n },\n routes: this.routes.length,\n container: this.container?.getRegistrations().length ?? 0,\n routeLatency: this.routeLatency,\n ...(wsAdapter ? { ws: wsAdapter.getStats() } : {}),\n })\n })\n\n router.get('/ws', (_req: Request, res: Response) => {\n const wsAdapter = this.peerAdapters.find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n if (!wsAdapter) {\n res.json({ enabled: false, message: 'WsAdapter not found' })\n return\n }\n res.json({ enabled: true, ...wsAdapter.getStats() })\n })\n\n if (this.exposeConfig) {\n router.get('/config', (_req: Request, res: Response) => {\n const config: Record<string, string> = {}\n for (const [key, value] of Object.entries(process.env)) {\n if (value === undefined) continue\n const allowed = this.configPrefixes.some((prefix) => key.startsWith(prefix))\n config[key] = allowed ? value : '[REDACTED]'\n }\n res.json({ config })\n })\n }\n\n // Dashboard UI — self-contained HTML that polls JSON endpoints\n router.get('/', (_req: Request, res: Response) => {\n res.type('html').send(renderDashboard(this.basePath))\n })\n\n app.use(this.basePath, router)\n log.info(`DevTools mounted at ${this.basePath}`)\n }\n\n middleware(): AdapterMiddleware[] {\n if (!this.enabled) return []\n\n return [\n {\n handler: (req: Request, res: Response, next: NextFunction) => {\n const start = Date.now()\n this.requestCount.value++\n\n res.on('finish', () => {\n if (res.statusCode >= 500) this.errorCount.value++\n else if (res.statusCode >= 400) this.clientErrorCount.value++\n\n // Track per-route latency\n const routeKey = `${req.method} ${req.route?.path ?? req.path}`\n const elapsed = Date.now() - start\n\n if (!this.routeLatency[routeKey]) {\n this.routeLatency[routeKey] = {\n count: 0,\n totalMs: 0,\n minMs: Infinity,\n maxMs: 0,\n }\n }\n const stats = this.routeLatency[routeKey]\n stats.count++\n stats.totalMs += elapsed\n stats.minMs = Math.min(stats.minMs, elapsed)\n stats.maxMs = Math.max(stats.maxMs, elapsed)\n })\n\n next()\n },\n phase: 'beforeGlobal',\n },\n ]\n }\n\n onRouteMount(controllerClass: any, mountPath: string): void {\n if (!this.enabled) return\n\n const routes: Array<{ method: string; path: string; handlerName: string }> =\n Reflect.getMetadata(METADATA.ROUTES, controllerClass) ?? []\n\n const classMiddleware: any[] =\n Reflect.getMetadata(METADATA.CLASS_MIDDLEWARES, controllerClass) ?? []\n\n for (const route of routes) {\n const methodMiddleware: any[] =\n Reflect.getMetadata(\n METADATA.METHOD_MIDDLEWARES,\n controllerClass.prototype,\n route.handlerName,\n ) ?? []\n\n this.routes.push({\n method: route.method.toUpperCase(),\n path: `${mountPath}${route.path === '/' ? '' : route.path}`,\n controller: controllerClass.name,\n handler: route.handlerName,\n middleware: [\n ...classMiddleware.map((m: any) => m.name || 'anonymous'),\n ...methodMiddleware.map((m: any) => m.name || 'anonymous'),\n ],\n })\n }\n }\n\n afterStart(_server: any, _container: Container): void {\n if (!this.enabled) return\n log.info(\n `DevTools ready — ${this.routes.length} routes tracked, ` +\n `${this.container?.getRegistrations().length ?? 0} DI bindings`,\n )\n }\n\n shutdown(): void {\n this.stopErrorWatch?.()\n this.adapterStatuses[this.name] = 'stopped'\n }\n}\n","/**\n * Generates the self-contained HTML dashboard for DevTools.\n * Served at GET /_debug — dark-themed, auto-refreshes every 30s.\n */\nexport function renderDashboard(basePath: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>KickJS DevTools</title>\n<style>\n${CSS}\n</style>\n</head>\n<body>\n${BODY}\n<script>\nconst BASE = '${basePath}';\nconst POLL_MS = 30000;\n${SCRIPT}\n</script>\n</body>\n</html>`\n}\n\n// ── Styles ──────────────────────────────────────────────────────────────\n\nconst CSS = `\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 24px; }\n h1 { font-size: 24px; margin-bottom: 8px; color: #38bdf8; }\n .subtitle { color: #64748b; font-size: 14px; margin-bottom: 24px; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; margin-bottom: 24px; }\n .card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; }\n .card h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 1px; color: #94a3b8; margin-bottom: 12px; }\n .stat { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #334155; }\n .stat:last-child { border-bottom: none; }\n .stat-label { color: #94a3b8; }\n .stat-value { font-weight: 600; font-variant-numeric: tabular-nums; }\n .badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: 600; }\n .badge-green { background: #065f46; color: #6ee7b7; }\n .badge-red { background: #7f1d1d; color: #fca5a5; }\n .badge-blue { background: #1e3a5f; color: #93c5fd; }\n .badge-yellow { background: #713f12; color: #fcd34d; }\n table { width: 100%; border-collapse: collapse; font-size: 13px; }\n th { text-align: left; padding: 8px; color: #94a3b8; border-bottom: 2px solid #334155; font-weight: 600; }\n td { padding: 8px; border-bottom: 1px solid #1e293b; }\n .method { font-weight: 700; font-size: 11px; }\n .method-get { color: #34d399; }\n .method-post { color: #60a5fa; }\n .method-put { color: #fbbf24; }\n .method-delete { color: #f87171; }\n .method-patch { color: #a78bfa; }\n .refresh-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }\n .refresh-info { font-size: 12px; color: #64748b; }\n .pulse { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #34d399; margin-right: 6px; animation: pulse 2s infinite; }\n @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }\n .empty { color: #64748b; font-style: italic; padding: 12px 0; }\n`\n\n// ── HTML Body ───────────────────────────────────────────────────────────\n\nconst BODY = `\n<h1>⚡ KickJS DevTools</h1>\n<div class=\"refresh-bar\">\n <div class=\"subtitle\">Development introspection dashboard</div>\n <div class=\"refresh-info\"><span class=\"pulse\"></span>Auto-refresh every 30s · <span id=\"lastUpdate\">loading...</span></div>\n</div>\n\n<div class=\"grid\">\n <div class=\"card\">\n <h2>Health</h2>\n <div id=\"health\"><div class=\"empty\">Loading...</div></div>\n </div>\n <div class=\"card\">\n <h2>Metrics</h2>\n <div id=\"metrics\"><div class=\"empty\">Loading...</div></div>\n </div>\n <div class=\"card\">\n <h2>WebSocket</h2>\n <div id=\"ws\"><div class=\"empty\">Loading...</div></div>\n </div>\n</div>\n\n<div class=\"card\" style=\"margin-bottom: 16px;\">\n <h2>Routes (<span id=\"routeCount\">0</span>)</h2>\n <div id=\"routes\" style=\"overflow-x: auto;\"><div class=\"empty\">Loading...</div></div>\n</div>\n\n<div class=\"card\">\n <h2>DI Container (<span id=\"diCount\">0</span>)</h2>\n <div id=\"container\" style=\"overflow-x: auto;\"><div class=\"empty\">Loading...</div></div>\n</div>\n`\n\n// ── Client-side Script ──────────────────────────────────────────────────\n\nconst SCRIPT = `\nasync function fetchJSON(path) {\n try { const r = await fetch(BASE + path); return r.ok ? r.json() : null; } catch { return null; }\n}\n\nfunction stat(label, value) {\n return '<div class=\"stat\"><span class=\"stat-label\">' + label + '</span><span class=\"stat-value\">' + value + '</span></div>';\n}\n\nfunction badge(text, type) {\n return '<span class=\"badge badge-' + type + '\">' + text + '</span>';\n}\n\nfunction methodClass(m) { return 'method method-' + m.toLowerCase(); }\n\nasync function refresh() {\n const [health, metrics, routes, container, ws] = await Promise.all([\n fetchJSON('/health'), fetchJSON('/metrics'), fetchJSON('/routes'),\n fetchJSON('/container'), fetchJSON('/ws'),\n ]);\n\n if (health) {\n const statusBadge = health.status === 'healthy' ? badge('healthy', 'green') : badge('degraded', 'red');\n let html = stat('Status', statusBadge);\n html += stat('Uptime', formatDuration(health.uptime));\n html += stat('Error Rate', (health.errorRate * 100).toFixed(2) + '%');\n if (health.adapters) {\n Object.entries(health.adapters).forEach(function(e) {\n html += stat(e[0], badge(e[1], e[1] === 'running' ? 'green' : 'yellow'));\n });\n }\n document.getElementById('health').innerHTML = html;\n }\n\n if (metrics) {\n let html = stat('Total Requests', metrics.requests.toLocaleString());\n html += stat('Server Errors (5xx)', metrics.serverErrors);\n html += stat('Client Errors (4xx)', metrics.clientErrors);\n html += stat('Error Rate', (metrics.errorRate * 100).toFixed(2) + '%');\n html += stat('Uptime', formatDuration(metrics.uptimeSeconds));\n html += stat('Started', new Date(metrics.startedAt).toLocaleTimeString());\n document.getElementById('metrics').innerHTML = html;\n }\n\n if (ws) {\n if (!ws.enabled) {\n document.getElementById('ws').innerHTML = '<div class=\"empty\">No WsAdapter</div>';\n } else {\n let html = stat('Active Connections', ws.activeConnections);\n html += stat('Total Connections', ws.totalConnections);\n html += stat('Messages In', ws.messagesReceived);\n html += stat('Messages Out', ws.messagesSent);\n html += stat('Errors', ws.errors);\n if (ws.namespaces) {\n Object.entries(ws.namespaces).forEach(function(e) {\n html += stat(e[0], e[1].connections + ' conn / ' + e[1].handlers + ' handlers');\n });\n }\n document.getElementById('ws').innerHTML = html;\n }\n }\n\n if (routes) {\n document.getElementById('routeCount').textContent = routes.routes.length;\n if (routes.routes.length === 0) {\n document.getElementById('routes').innerHTML = '<div class=\"empty\">No routes registered</div>';\n } else {\n let html = '<table><tr><th>Method</th><th>Path</th><th>Controller</th><th>Handler</th><th>Middleware</th></tr>';\n routes.routes.forEach(function(r) {\n html += '<tr><td class=\"' + methodClass(r.method) + '\">' + r.method + '</td>';\n html += '<td><code>' + r.path + '</code></td>';\n html += '<td>' + r.controller + '</td>';\n html += '<td>' + r.handler + '</td>';\n html += '<td>' + (r.middleware.length ? r.middleware.join(', ') : '—') + '</td></tr>';\n });\n html += '</table>';\n document.getElementById('routes').innerHTML = html;\n }\n }\n\n if (container) {\n document.getElementById('diCount').textContent = container.count;\n if (container.count === 0) {\n document.getElementById('container').innerHTML = '<div class=\"empty\">No DI registrations</div>';\n } else {\n let html = '<table><tr><th>Token</th><th>Scope</th><th>Instantiated</th></tr>';\n container.registrations.forEach(function(r) {\n html += '<tr><td><code>' + r.token + '</code></td>';\n html += '<td>' + badge(r.scope, 'blue') + '</td>';\n html += '<td>' + (r.instantiated ? badge('yes', 'green') : badge('no', 'yellow')) + '</td></tr>';\n });\n html += '</table>';\n document.getElementById('container').innerHTML = html;\n }\n }\n\n document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString();\n}\n\nfunction formatDuration(seconds) {\n if (seconds < 60) return seconds + 's';\n if (seconds < 3600) return Math.floor(seconds / 60) + 'm ' + (seconds % 60) + 's';\n var h = Math.floor(seconds / 3600);\n var m = Math.floor((seconds % 3600) / 60);\n return h + 'h ' + m + 'm';\n}\n\nrefresh();\nsetInterval(refresh, POLL_MS);\n`\n"],"mappings":"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","names":["Router","renderDashboard","basePath","CSS","BODY","SCRIPT","METADATA","ref","computed","reactive","watch","createLogger","log","createLogger","DevToolsAdapter","name","basePath","enabled","exposeConfig","configPrefixes","errorRateThreshold","requestCount","errorCount","clientErrorCount","startedAt","errorRate","uptimeSeconds","routeLatency","routes","container","adapterStatuses","stopErrorWatch","peerAdapters","options","process","env","NODE_ENV","adapters","ref","Date","now","reactive","computed","value","Math","floor","onErrorRateExceeded","callback","threshold","watch","rate","warn","toFixed","beforeMount","app","router","Router","get","_req","res","json","registrations","getRegistrations","count","length","requests","serverErrors","clientErrors","toISOString","healthy","status","uptime","wsAdapter","find","a","getStats","ws","message","config","key","Object","entries","undefined","allowed","some","prefix","startsWith","type","send","renderDashboard","use","info","middleware","handler","req","next","start","on","statusCode","routeKey","method","route","path","elapsed","totalMs","minMs","Infinity","maxMs","stats","min","max","phase","onRouteMount","controllerClass","mountPath","Reflect","getMetadata","METADATA","ROUTES","classMiddleware","CLASS_MIDDLEWARES","methodMiddleware","METHOD_MIDDLEWARES","prototype","handlerName","push","toUpperCase","controller","map","m","afterStart","_server","_container","shutdown"]}
|
package/dist/devtools.js
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forinda/kickjs-http",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Express 5 integration, router builder, RequestContext, and middleware for KickJS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kickjs",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"cookie-parser": "^1.4.7",
|
|
99
99
|
"multer": "^2.1.1",
|
|
100
100
|
"reflect-metadata": "^0.2.2",
|
|
101
|
-
"@forinda/kickjs-core": "0.
|
|
101
|
+
"@forinda/kickjs-core": "0.7.0"
|
|
102
102
|
},
|
|
103
103
|
"peerDependencies": {
|
|
104
104
|
"express": "^5.1.0"
|
package/dist/chunk-YYT24FTH.js
DELETED
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
__name
|
|
3
|
-
} from "./chunk-WCQVDF3K.js";
|
|
4
|
-
|
|
5
|
-
// src/devtools.ts
|
|
6
|
-
import { Router } from "express";
|
|
7
|
-
import { METADATA, ref, computed, reactive, watch, createLogger } from "@forinda/kickjs-core";
|
|
8
|
-
var log = createLogger("DevTools");
|
|
9
|
-
var DevToolsAdapter = class {
|
|
10
|
-
static {
|
|
11
|
-
__name(this, "DevToolsAdapter");
|
|
12
|
-
}
|
|
13
|
-
name = "DevToolsAdapter";
|
|
14
|
-
basePath;
|
|
15
|
-
enabled;
|
|
16
|
-
exposeConfig;
|
|
17
|
-
configPrefixes;
|
|
18
|
-
errorRateThreshold;
|
|
19
|
-
// ── Reactive State ───────────────────────────────────────────────────
|
|
20
|
-
/** Total requests received */
|
|
21
|
-
requestCount;
|
|
22
|
-
/** Total responses with status >= 500 */
|
|
23
|
-
errorCount;
|
|
24
|
-
/** Total responses with status >= 400 and < 500 */
|
|
25
|
-
clientErrorCount;
|
|
26
|
-
/** Server start time */
|
|
27
|
-
startedAt;
|
|
28
|
-
/** Computed error rate (server errors / total requests) */
|
|
29
|
-
errorRate;
|
|
30
|
-
/** Computed uptime in seconds */
|
|
31
|
-
uptimeSeconds;
|
|
32
|
-
/** Per-route latency tracking */
|
|
33
|
-
routeLatency;
|
|
34
|
-
// ── Internal State ───────────────────────────────────────────────────
|
|
35
|
-
routes = [];
|
|
36
|
-
container = null;
|
|
37
|
-
adapterStatuses = {};
|
|
38
|
-
stopErrorWatch = null;
|
|
39
|
-
peerAdapters = [];
|
|
40
|
-
constructor(options = {}) {
|
|
41
|
-
this.basePath = options.basePath ?? "/_debug";
|
|
42
|
-
this.enabled = options.enabled ?? process.env.NODE_ENV !== "production";
|
|
43
|
-
this.exposeConfig = options.exposeConfig ?? false;
|
|
44
|
-
this.configPrefixes = options.configPrefixes ?? [
|
|
45
|
-
"APP_",
|
|
46
|
-
"NODE_ENV"
|
|
47
|
-
];
|
|
48
|
-
this.errorRateThreshold = options.errorRateThreshold ?? 0.5;
|
|
49
|
-
this.peerAdapters = options.adapters ?? [];
|
|
50
|
-
this.requestCount = ref(0);
|
|
51
|
-
this.errorCount = ref(0);
|
|
52
|
-
this.clientErrorCount = ref(0);
|
|
53
|
-
this.startedAt = ref(Date.now());
|
|
54
|
-
this.routeLatency = reactive({});
|
|
55
|
-
this.errorRate = computed(() => this.requestCount.value > 0 ? this.errorCount.value / this.requestCount.value : 0);
|
|
56
|
-
this.uptimeSeconds = computed(() => Math.floor((Date.now() - this.startedAt.value) / 1e3));
|
|
57
|
-
if (options.onErrorRateExceeded) {
|
|
58
|
-
const callback = options.onErrorRateExceeded;
|
|
59
|
-
const threshold = this.errorRateThreshold;
|
|
60
|
-
this.stopErrorWatch = watch(this.errorRate, (rate) => {
|
|
61
|
-
if (rate > threshold) {
|
|
62
|
-
callback(rate);
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
} else {
|
|
66
|
-
this.stopErrorWatch = watch(this.errorRate, (rate) => {
|
|
67
|
-
if (rate > this.errorRateThreshold) {
|
|
68
|
-
log.warn(`Error rate elevated: ${(rate * 100).toFixed(1)}%`);
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
// ── Adapter Lifecycle ────────────────────────────────────────────────
|
|
74
|
-
beforeMount(app, container) {
|
|
75
|
-
if (!this.enabled) return;
|
|
76
|
-
this.container = container;
|
|
77
|
-
this.startedAt.value = Date.now();
|
|
78
|
-
this.routes = [];
|
|
79
|
-
this.adapterStatuses[this.name] = "running";
|
|
80
|
-
const router = Router();
|
|
81
|
-
router.get("/routes", (_req, res) => {
|
|
82
|
-
res.json({
|
|
83
|
-
routes: this.routes
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
router.get("/container", (_req, res) => {
|
|
87
|
-
const registrations = this.container?.getRegistrations() ?? [];
|
|
88
|
-
res.json({
|
|
89
|
-
registrations,
|
|
90
|
-
count: registrations.length
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
router.get("/metrics", (_req, res) => {
|
|
94
|
-
res.json({
|
|
95
|
-
requests: this.requestCount.value,
|
|
96
|
-
serverErrors: this.errorCount.value,
|
|
97
|
-
clientErrors: this.clientErrorCount.value,
|
|
98
|
-
errorRate: this.errorRate.value,
|
|
99
|
-
uptimeSeconds: this.uptimeSeconds.value,
|
|
100
|
-
startedAt: new Date(this.startedAt.value).toISOString(),
|
|
101
|
-
routeLatency: this.routeLatency
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
router.get("/health", (_req, res) => {
|
|
105
|
-
const healthy = this.errorRate.value < this.errorRateThreshold;
|
|
106
|
-
const status = healthy ? "healthy" : "degraded";
|
|
107
|
-
res.status(healthy ? 200 : 503).json({
|
|
108
|
-
status,
|
|
109
|
-
errorRate: this.errorRate.value,
|
|
110
|
-
uptime: this.uptimeSeconds.value,
|
|
111
|
-
adapters: this.adapterStatuses
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
router.get("/state", (_req, res) => {
|
|
115
|
-
const wsAdapter = this.peerAdapters.find((a) => a.name === "WsAdapter" && typeof a.getStats === "function");
|
|
116
|
-
res.json({
|
|
117
|
-
reactive: {
|
|
118
|
-
requestCount: this.requestCount.value,
|
|
119
|
-
errorCount: this.errorCount.value,
|
|
120
|
-
clientErrorCount: this.clientErrorCount.value,
|
|
121
|
-
errorRate: this.errorRate.value,
|
|
122
|
-
uptimeSeconds: this.uptimeSeconds.value,
|
|
123
|
-
startedAt: new Date(this.startedAt.value).toISOString()
|
|
124
|
-
},
|
|
125
|
-
routes: this.routes.length,
|
|
126
|
-
container: this.container?.getRegistrations().length ?? 0,
|
|
127
|
-
routeLatency: this.routeLatency,
|
|
128
|
-
...wsAdapter ? {
|
|
129
|
-
ws: wsAdapter.getStats()
|
|
130
|
-
} : {}
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
router.get("/ws", (_req, res) => {
|
|
134
|
-
const wsAdapter = this.peerAdapters.find((a) => a.name === "WsAdapter" && typeof a.getStats === "function");
|
|
135
|
-
if (!wsAdapter) {
|
|
136
|
-
res.json({
|
|
137
|
-
enabled: false,
|
|
138
|
-
message: "WsAdapter not found"
|
|
139
|
-
});
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
res.json({
|
|
143
|
-
enabled: true,
|
|
144
|
-
...wsAdapter.getStats()
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
if (this.exposeConfig) {
|
|
148
|
-
router.get("/config", (_req, res) => {
|
|
149
|
-
const config = {};
|
|
150
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
151
|
-
if (value === void 0) continue;
|
|
152
|
-
const allowed = this.configPrefixes.some((prefix) => key.startsWith(prefix));
|
|
153
|
-
config[key] = allowed ? value : "[REDACTED]";
|
|
154
|
-
}
|
|
155
|
-
res.json({
|
|
156
|
-
config
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
app.use(this.basePath, router);
|
|
161
|
-
log.info(`DevTools mounted at ${this.basePath}`);
|
|
162
|
-
}
|
|
163
|
-
middleware() {
|
|
164
|
-
if (!this.enabled) return [];
|
|
165
|
-
return [
|
|
166
|
-
{
|
|
167
|
-
handler: /* @__PURE__ */ __name((req, res, next) => {
|
|
168
|
-
const start = Date.now();
|
|
169
|
-
this.requestCount.value++;
|
|
170
|
-
res.on("finish", () => {
|
|
171
|
-
if (res.statusCode >= 500) this.errorCount.value++;
|
|
172
|
-
else if (res.statusCode >= 400) this.clientErrorCount.value++;
|
|
173
|
-
const routeKey = `${req.method} ${req.route?.path ?? req.path}`;
|
|
174
|
-
const elapsed = Date.now() - start;
|
|
175
|
-
if (!this.routeLatency[routeKey]) {
|
|
176
|
-
this.routeLatency[routeKey] = {
|
|
177
|
-
count: 0,
|
|
178
|
-
totalMs: 0,
|
|
179
|
-
minMs: Infinity,
|
|
180
|
-
maxMs: 0
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
const stats = this.routeLatency[routeKey];
|
|
184
|
-
stats.count++;
|
|
185
|
-
stats.totalMs += elapsed;
|
|
186
|
-
stats.minMs = Math.min(stats.minMs, elapsed);
|
|
187
|
-
stats.maxMs = Math.max(stats.maxMs, elapsed);
|
|
188
|
-
});
|
|
189
|
-
next();
|
|
190
|
-
}, "handler"),
|
|
191
|
-
phase: "beforeGlobal"
|
|
192
|
-
}
|
|
193
|
-
];
|
|
194
|
-
}
|
|
195
|
-
onRouteMount(controllerClass, mountPath) {
|
|
196
|
-
if (!this.enabled) return;
|
|
197
|
-
const routes = Reflect.getMetadata(METADATA.ROUTES, controllerClass) ?? [];
|
|
198
|
-
const classMiddleware = Reflect.getMetadata(METADATA.CLASS_MIDDLEWARES, controllerClass) ?? [];
|
|
199
|
-
for (const route of routes) {
|
|
200
|
-
const methodMiddleware = Reflect.getMetadata(METADATA.METHOD_MIDDLEWARES, controllerClass.prototype, route.handlerName) ?? [];
|
|
201
|
-
this.routes.push({
|
|
202
|
-
method: route.method.toUpperCase(),
|
|
203
|
-
path: `${mountPath}${route.path === "/" ? "" : route.path}`,
|
|
204
|
-
controller: controllerClass.name,
|
|
205
|
-
handler: route.handlerName,
|
|
206
|
-
middleware: [
|
|
207
|
-
...classMiddleware.map((m) => m.name || "anonymous"),
|
|
208
|
-
...methodMiddleware.map((m) => m.name || "anonymous")
|
|
209
|
-
]
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
afterStart(_server, _container) {
|
|
214
|
-
if (!this.enabled) return;
|
|
215
|
-
log.info(`DevTools ready \u2014 ${this.routes.length} routes tracked, ${this.container?.getRegistrations().length ?? 0} DI bindings`);
|
|
216
|
-
}
|
|
217
|
-
shutdown() {
|
|
218
|
-
this.stopErrorWatch?.();
|
|
219
|
-
this.adapterStatuses[this.name] = "stopped";
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
export {
|
|
224
|
-
DevToolsAdapter
|
|
225
|
-
};
|
|
226
|
-
//# sourceMappingURL=chunk-YYT24FTH.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/devtools.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport { Router } from 'express'\nimport {\n type AppAdapter,\n type AdapterMiddleware,\n type Container,\n METADATA,\n ref,\n computed,\n reactive,\n watch,\n createLogger,\n type Ref,\n type ComputedRef,\n} from '@forinda/kickjs-core'\n\nconst log = createLogger('DevTools')\n\n/** Route metadata collected during mount */\ninterface RouteInfo {\n method: string\n path: string\n controller: string\n handler: string\n middleware: string[]\n}\n\n/** Per-route latency stats */\ninterface RouteStats {\n count: number\n totalMs: number\n minMs: number\n maxMs: number\n}\n\nexport interface DevToolsOptions {\n /** Base path for debug endpoints (default: '/_debug') */\n basePath?: string\n /** Only enable when this is true (default: process.env.NODE_ENV !== 'production') */\n enabled?: boolean\n /** Include environment variables (sanitized) at /_debug/config (default: false) */\n exposeConfig?: boolean\n /** Env var prefixes to expose (default: ['APP_', 'NODE_ENV']). Others are redacted. */\n configPrefixes?: string[]\n /** Callback when error rate exceeds threshold */\n onErrorRateExceeded?: (rate: number) => void\n /** Error rate threshold (default: 0.5 = 50%) */\n errorRateThreshold?: number\n /** Other adapters to discover stats from (e.g., WsAdapter) */\n adapters?: any[]\n}\n\n/**\n * DevToolsAdapter — Vue-style reactive introspection for KickJS applications.\n *\n * Exposes debug endpoints powered by reactive state (ref, computed, watch):\n * - `GET /_debug/routes` — all registered routes with middleware\n * - `GET /_debug/container` — DI registry with scopes and instantiation status\n * - `GET /_debug/metrics` — live request/error counts, error rate, uptime\n * - `GET /_debug/health` — deep health check with adapter status\n * - `GET /_debug/config` — sanitized environment variables (opt-in)\n * - `GET /_debug/state` — full reactive state snapshot\n *\n * @example\n * ```ts\n * import { DevToolsAdapter } from '@forinda/kickjs-http/devtools'\n *\n * bootstrap({\n * modules: [UserModule],\n * adapters: [\n * new DevToolsAdapter({\n * enabled: process.env.NODE_ENV !== 'production',\n * exposeConfig: true,\n * configPrefixes: ['APP_', 'DATABASE_'],\n * }),\n * ],\n * })\n * ```\n */\nexport class DevToolsAdapter implements AppAdapter {\n readonly name = 'DevToolsAdapter'\n\n private basePath: string\n private enabled: boolean\n private exposeConfig: boolean\n private configPrefixes: string[]\n private errorRateThreshold: number\n\n // ── Reactive State ───────────────────────────────────────────────────\n /** Total requests received */\n readonly requestCount: Ref<number>\n /** Total responses with status >= 500 */\n readonly errorCount: Ref<number>\n /** Total responses with status >= 400 and < 500 */\n readonly clientErrorCount: Ref<number>\n /** Server start time */\n readonly startedAt: Ref<number>\n /** Computed error rate (server errors / total requests) */\n readonly errorRate: ComputedRef<number>\n /** Computed uptime in seconds */\n readonly uptimeSeconds: ComputedRef<number>\n /** Per-route latency tracking */\n readonly routeLatency: Record<string, RouteStats>\n\n // ── Internal State ───────────────────────────────────────────────────\n private routes: RouteInfo[] = []\n private container: Container | null = null\n private adapterStatuses: Record<string, string> = {}\n private stopErrorWatch: (() => void) | null = null\n private peerAdapters: any[] = []\n\n constructor(options: DevToolsOptions = {}) {\n this.basePath = options.basePath ?? '/_debug'\n this.enabled = options.enabled ?? process.env.NODE_ENV !== 'production'\n this.exposeConfig = options.exposeConfig ?? false\n this.configPrefixes = options.configPrefixes ?? ['APP_', 'NODE_ENV']\n this.errorRateThreshold = options.errorRateThreshold ?? 0.5\n this.peerAdapters = options.adapters ?? []\n\n // Initialize reactive state\n this.requestCount = ref(0)\n this.errorCount = ref(0)\n this.clientErrorCount = ref(0)\n this.startedAt = ref(Date.now())\n this.routeLatency = reactive({})\n\n this.errorRate = computed(() =>\n this.requestCount.value > 0 ? this.errorCount.value / this.requestCount.value : 0,\n )\n\n this.uptimeSeconds = computed(() => Math.floor((Date.now() - this.startedAt.value) / 1000))\n\n // Watch error rate — log warnings when elevated\n if (options.onErrorRateExceeded) {\n const callback = options.onErrorRateExceeded\n const threshold = this.errorRateThreshold\n this.stopErrorWatch = watch(this.errorRate, (rate) => {\n if (rate > threshold) {\n callback(rate)\n }\n })\n } else {\n this.stopErrorWatch = watch(this.errorRate, (rate) => {\n if (rate > this.errorRateThreshold) {\n log.warn(`Error rate elevated: ${(rate * 100).toFixed(1)}%`)\n }\n })\n }\n }\n\n // ── Adapter Lifecycle ────────────────────────────────────────────────\n\n beforeMount(app: any, container: Container): void {\n if (!this.enabled) return\n\n this.container = container\n this.startedAt.value = Date.now()\n // Clear routes on rebuild/restart to prevent HMR duplication\n this.routes = []\n this.adapterStatuses[this.name] = 'running'\n\n const router = Router()\n\n router.get('/routes', (_req: Request, res: Response) => {\n res.json({ routes: this.routes })\n })\n\n router.get('/container', (_req: Request, res: Response) => {\n const registrations = this.container?.getRegistrations() ?? []\n res.json({ registrations, count: registrations.length })\n })\n\n router.get('/metrics', (_req: Request, res: Response) => {\n res.json({\n requests: this.requestCount.value,\n serverErrors: this.errorCount.value,\n clientErrors: this.clientErrorCount.value,\n errorRate: this.errorRate.value,\n uptimeSeconds: this.uptimeSeconds.value,\n startedAt: new Date(this.startedAt.value).toISOString(),\n routeLatency: this.routeLatency,\n })\n })\n\n router.get('/health', (_req: Request, res: Response) => {\n const healthy = this.errorRate.value < this.errorRateThreshold\n const status = healthy ? 'healthy' : 'degraded'\n\n res.status(healthy ? 200 : 503).json({\n status,\n errorRate: this.errorRate.value,\n uptime: this.uptimeSeconds.value,\n adapters: this.adapterStatuses,\n })\n })\n\n router.get('/state', (_req: Request, res: Response) => {\n const wsAdapter = this.peerAdapters.find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n res.json({\n reactive: {\n requestCount: this.requestCount.value,\n errorCount: this.errorCount.value,\n clientErrorCount: this.clientErrorCount.value,\n errorRate: this.errorRate.value,\n uptimeSeconds: this.uptimeSeconds.value,\n startedAt: new Date(this.startedAt.value).toISOString(),\n },\n routes: this.routes.length,\n container: this.container?.getRegistrations().length ?? 0,\n routeLatency: this.routeLatency,\n ...(wsAdapter ? { ws: wsAdapter.getStats() } : {}),\n })\n })\n\n router.get('/ws', (_req: Request, res: Response) => {\n const wsAdapter = this.peerAdapters.find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n if (!wsAdapter) {\n res.json({ enabled: false, message: 'WsAdapter not found' })\n return\n }\n res.json({ enabled: true, ...wsAdapter.getStats() })\n })\n\n if (this.exposeConfig) {\n router.get('/config', (_req: Request, res: Response) => {\n const config: Record<string, string> = {}\n for (const [key, value] of Object.entries(process.env)) {\n if (value === undefined) continue\n const allowed = this.configPrefixes.some((prefix) => key.startsWith(prefix))\n config[key] = allowed ? value : '[REDACTED]'\n }\n res.json({ config })\n })\n }\n\n app.use(this.basePath, router)\n log.info(`DevTools mounted at ${this.basePath}`)\n }\n\n middleware(): AdapterMiddleware[] {\n if (!this.enabled) return []\n\n return [\n {\n handler: (req: Request, res: Response, next: NextFunction) => {\n const start = Date.now()\n this.requestCount.value++\n\n res.on('finish', () => {\n if (res.statusCode >= 500) this.errorCount.value++\n else if (res.statusCode >= 400) this.clientErrorCount.value++\n\n // Track per-route latency\n const routeKey = `${req.method} ${req.route?.path ?? req.path}`\n const elapsed = Date.now() - start\n\n if (!this.routeLatency[routeKey]) {\n this.routeLatency[routeKey] = {\n count: 0,\n totalMs: 0,\n minMs: Infinity,\n maxMs: 0,\n }\n }\n const stats = this.routeLatency[routeKey]\n stats.count++\n stats.totalMs += elapsed\n stats.minMs = Math.min(stats.minMs, elapsed)\n stats.maxMs = Math.max(stats.maxMs, elapsed)\n })\n\n next()\n },\n phase: 'beforeGlobal',\n },\n ]\n }\n\n onRouteMount(controllerClass: any, mountPath: string): void {\n if (!this.enabled) return\n\n const routes: Array<{ method: string; path: string; handlerName: string }> =\n Reflect.getMetadata(METADATA.ROUTES, controllerClass) ?? []\n\n const classMiddleware: any[] =\n Reflect.getMetadata(METADATA.CLASS_MIDDLEWARES, controllerClass) ?? []\n\n for (const route of routes) {\n const methodMiddleware: any[] =\n Reflect.getMetadata(\n METADATA.METHOD_MIDDLEWARES,\n controllerClass.prototype,\n route.handlerName,\n ) ?? []\n\n this.routes.push({\n method: route.method.toUpperCase(),\n path: `${mountPath}${route.path === '/' ? '' : route.path}`,\n controller: controllerClass.name,\n handler: route.handlerName,\n middleware: [\n ...classMiddleware.map((m: any) => m.name || 'anonymous'),\n ...methodMiddleware.map((m: any) => m.name || 'anonymous'),\n ],\n })\n }\n }\n\n afterStart(_server: any, _container: Container): void {\n if (!this.enabled) return\n log.info(\n `DevTools ready — ${this.routes.length} routes tracked, ` +\n `${this.container?.getRegistrations().length ?? 0} DI bindings`,\n )\n }\n\n shutdown(): void {\n this.stopErrorWatch?.()\n this.adapterStatuses[this.name] = 'stopped'\n }\n}\n"],"mappings":"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","names":["Router","METADATA","ref","computed","reactive","watch","createLogger","log","createLogger","DevToolsAdapter","name","basePath","enabled","exposeConfig","configPrefixes","errorRateThreshold","requestCount","errorCount","clientErrorCount","startedAt","errorRate","uptimeSeconds","routeLatency","routes","container","adapterStatuses","stopErrorWatch","peerAdapters","options","process","env","NODE_ENV","adapters","ref","Date","now","reactive","computed","value","Math","floor","onErrorRateExceeded","callback","threshold","watch","rate","warn","toFixed","beforeMount","app","router","Router","get","_req","res","json","registrations","getRegistrations","count","length","requests","serverErrors","clientErrors","toISOString","healthy","status","uptime","wsAdapter","find","a","getStats","ws","message","config","key","Object","entries","undefined","allowed","some","prefix","startsWith","use","info","middleware","handler","req","next","start","on","statusCode","routeKey","method","route","path","elapsed","totalMs","minMs","Infinity","maxMs","stats","min","max","phase","onRouteMount","controllerClass","mountPath","Reflect","getMetadata","METADATA","ROUTES","classMiddleware","CLASS_MIDDLEWARES","methodMiddleware","METHOD_MIDDLEWARES","prototype","handlerName","push","toUpperCase","controller","map","m","afterStart","_server","_container","shutdown"]}
|