@forinda/kickjs-http 0.5.2 → 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.
@@ -1,6 +1,6 @@
1
1
  import http from 'node:http';
2
2
  import { RequestHandler, Express } from 'express';
3
- import { AppModuleClass, AppAdapter } from '@forinda/kickjs-core';
3
+ import { AppModuleClass, AppAdapter, KickPlugin } from '@forinda/kickjs-core';
4
4
 
5
5
  /**
6
6
  * A middleware entry in the declarative pipeline.
@@ -43,6 +43,8 @@ interface ApplicationOptions {
43
43
  * requestId(), express.json({ limit: '100kb' })
44
44
  */
45
45
  middleware?: MiddlewareEntry[];
46
+ /** Plugins that bundle modules, adapters, middleware, and DI bindings */
47
+ plugins?: KickPlugin[];
46
48
  /** Express `trust proxy` setting */
47
49
  trustProxy?: boolean | number | string | ((ip: string, hopIndex: number) => boolean);
48
50
  /** Maximum JSON body size (only used when middleware is not provided) */
@@ -58,6 +60,7 @@ declare class Application {
58
60
  private container;
59
61
  private httpServer;
60
62
  private adapters;
63
+ private plugins;
61
64
  constructor(options: ApplicationOptions);
62
65
  /**
63
66
  * Full setup pipeline:
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Application
3
- } from "./chunk-OKB76LY2.js";
3
+ } from "./chunk-YRCR6Z5C.js";
4
4
  import "./chunk-35NUARK7.js";
5
5
  import "./chunk-3NEDJA3J.js";
6
6
  import "./chunk-WCQVDF3K.js";
package/dist/bootstrap.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  bootstrap
3
- } from "./chunk-D76WCWAW.js";
4
- import "./chunk-OKB76LY2.js";
3
+ } from "./chunk-W35YEQOE.js";
4
+ import "./chunk-YRCR6Z5C.js";
5
5
  import "./chunk-35NUARK7.js";
6
6
  import "./chunk-3NEDJA3J.js";
7
7
  import "./chunk-WCQVDF3K.js";
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-RPN7UFUO.js";
7
7
  import {
8
8
  RequestContext
9
- } from "./chunk-GDMORQ2P.js";
9
+ } from "./chunk-Y5ZSC2FB.js";
10
10
  import {
11
11
  __name
12
12
  } from "./chunk-WCQVDF3K.js";
@@ -66,4 +66,4 @@ export {
66
66
  getControllerPath,
67
67
  buildRoutes
68
68
  };
69
- //# sourceMappingURL=chunk-S7DJ5HF3.js.map
69
+ //# sourceMappingURL=chunk-E552HYEB.js.map
@@ -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"]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Application
3
- } from "./chunk-OKB76LY2.js";
3
+ } from "./chunk-YRCR6Z5C.js";
4
4
  import {
5
5
  __name,
6
6
  __require
@@ -57,4 +57,4 @@ __name(bootstrap, "bootstrap");
57
57
  export {
58
58
  bootstrap
59
59
  };
60
- //# sourceMappingURL=chunk-D76WCWAW.js.map
60
+ //# sourceMappingURL=chunk-W35YEQOE.js.map
@@ -148,9 +148,67 @@ var RequestContext = class {
148
148
  };
149
149
  return this.json(response);
150
150
  }
151
+ // ── Server-Sent Events ──────────────────────────────────────────────
152
+ /**
153
+ * Start an SSE (Server-Sent Events) stream.
154
+ * Sets the correct headers and returns helpers to send events.
155
+ *
156
+ * @example
157
+ * ```ts
158
+ * @Get('/events')
159
+ * async stream(ctx: RequestContext) {
160
+ * const sse = ctx.sse()
161
+ *
162
+ * const interval = setInterval(() => {
163
+ * sse.send({ time: new Date().toISOString() }, 'tick')
164
+ * }, 1000)
165
+ *
166
+ * sse.onClose(() => clearInterval(interval))
167
+ * }
168
+ * ```
169
+ */
170
+ sse() {
171
+ this.res.writeHead(200, {
172
+ "Content-Type": "text/event-stream",
173
+ "Cache-Control": "no-cache",
174
+ Connection: "keep-alive",
175
+ "X-Accel-Buffering": "no"
176
+ });
177
+ this.res.flushHeaders();
178
+ const closeCallbacks = [];
179
+ this.req.on("close", () => {
180
+ for (const cb of closeCallbacks) cb();
181
+ });
182
+ return {
183
+ /** Send an SSE event with optional event name and id */
184
+ send: /* @__PURE__ */ __name((data, event, id) => {
185
+ if (id) this.res.write(`id: ${id}
186
+ `);
187
+ if (event) this.res.write(`event: ${event}
188
+ `);
189
+ this.res.write(`data: ${JSON.stringify(data)}
190
+
191
+ `);
192
+ }, "send"),
193
+ /** Send a comment (keeps connection alive) */
194
+ comment: /* @__PURE__ */ __name((text) => {
195
+ this.res.write(`: ${text}
196
+
197
+ `);
198
+ }, "comment"),
199
+ /** Register a callback when the client disconnects */
200
+ onClose: /* @__PURE__ */ __name((fn) => {
201
+ closeCallbacks.push(fn);
202
+ }, "onClose"),
203
+ /** End the SSE stream */
204
+ close: /* @__PURE__ */ __name(() => {
205
+ this.res.end();
206
+ }, "close")
207
+ };
208
+ }
151
209
  };
152
210
 
153
211
  export {
154
212
  RequestContext
155
213
  };
156
- //# sourceMappingURL=chunk-GDMORQ2P.js.map
214
+ //# sourceMappingURL=chunk-Y5ZSC2FB.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/context.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport {\n parseQuery,\n type ParsedQuery,\n type QueryFieldConfig,\n type PaginatedResponse,\n} from './query'\n\n/**\n * Unified request/response abstraction passed to every controller method.\n * Shields handlers from raw Express objects and provides convenience methods.\n */\nexport class RequestContext<TBody = any, TParams = any, TQuery = any> {\n private metadata = new Map<string, any>()\n\n constructor(\n public readonly req: Request,\n public readonly res: Response,\n public readonly next: NextFunction,\n ) {}\n\n // ── Request Data ────────────────────────────────────────────────────\n\n get body(): TBody {\n return this.req.body as TBody\n }\n\n get params(): TParams {\n return this.req.params as TParams\n }\n\n get query(): TQuery {\n return this.req.query as TQuery\n }\n\n get headers() {\n return this.req.headers\n }\n\n get requestId(): string | undefined {\n return (this.req as any).requestId ?? (this.req.headers['x-request-id'] as string | undefined)\n }\n\n /** Session data (requires session middleware) */\n get session(): any {\n return (this.req as any).session\n }\n\n // ── Query String Parsing ───────────────────────────────────────────\n\n /**\n * Parse the request query string into structured filters, sort, pagination, and search.\n * Pass the result to an ORM query builder adapter (Drizzle, Prisma, Sequelize, etc.).\n *\n * @param fieldConfig - Optional whitelist for filterable, sortable, and searchable fields\n *\n * @example\n * ```ts\n * @Get('/')\n * async list(ctx: RequestContext) {\n * const parsed = ctx.qs({\n * filterable: ['status', 'priority'],\n * sortable: ['createdAt', 'title'],\n * })\n * const q = drizzleAdapter.build(parsed, { columns })\n * // ... use q.where, q.orderBy, q.limit, q.offset\n * }\n * ```\n */\n qs(fieldConfig?: QueryFieldConfig): ParsedQuery {\n return parseQuery(this.req.query as Record<string, any>, fieldConfig)\n }\n\n // ── File Uploads ────────────────────────────────────────────────────\n\n /** Single uploaded file (requires @FileUpload({ mode: 'single' })) */\n get file(): any {\n return (this.req as any).file\n }\n\n /** Array of uploaded files (requires @FileUpload({ mode: 'array' })) */\n get files(): any[] | undefined {\n return (this.req as any).files\n }\n\n // ── Metadata Store ──────────────────────────────────────────────────\n\n get<T = any>(key: string): T | undefined {\n return this.metadata.get(key) as T | undefined\n }\n\n set(key: string, value: any): void {\n this.metadata.set(key, value)\n }\n\n // ── Response Helpers ────────────────────────────────────────────────\n\n json(data: any, status = 200) {\n return this.res.status(status).json(data)\n }\n\n created(data: any) {\n return this.res.status(201).json(data)\n }\n\n noContent() {\n return this.res.status(204).end()\n }\n\n notFound(message = 'Not Found') {\n return this.res.status(404).json({ message })\n }\n\n badRequest(message: string) {\n return this.res.status(400).json({ message })\n }\n\n html(content: string, status = 200) {\n return this.res.status(status).type('html').send(content)\n }\n\n download(buffer: Buffer, filename: string, contentType = 'application/octet-stream') {\n this.res.setHeader('Content-Disposition', `attachment; filename=\"${filename}\"`)\n this.res.setHeader('Content-Type', contentType)\n return this.res.send(buffer)\n }\n\n /**\n * Parse query params and return a standardized paginated response.\n * Calls `ctx.qs()` internally, then wraps your data with pagination meta.\n *\n * @param fetcher - Async function that receives ParsedQuery and returns `{ data, total }`\n * @param fieldConfig - Optional whitelist for filterable, sortable, searchable fields\n *\n * @example\n * ```ts\n * @Get('/')\n * async list(ctx: RequestContext) {\n * return ctx.paginate(\n * async (parsed) => {\n * const data = await db.select().from(users)\n * .where(query.where).limit(parsed.pagination.limit)\n * .offset(parsed.pagination.offset).all()\n * const total = await db.select({ count: count() }).from(users).get()\n * return { data, total: total?.count ?? 0 }\n * },\n * { filterable: ['name', 'role'], sortable: ['createdAt'] },\n * )\n * }\n * ```\n */\n async paginate<T>(\n fetcher: (parsed: ParsedQuery) => Promise<{ data: T[]; total: number }>,\n fieldConfig?: QueryFieldConfig,\n ) {\n const parsed = this.qs(fieldConfig)\n const { data, total } = await fetcher(parsed)\n const { page, limit } = parsed.pagination\n const totalPages = Math.ceil(total / limit) || 1\n\n const response: PaginatedResponse<T> = {\n data,\n meta: {\n page,\n limit,\n total,\n totalPages,\n hasNext: page < totalPages,\n hasPrev: page > 1,\n },\n }\n\n return this.json(response)\n }\n}\n"],"mappings":";;;;;;;;AAYO,IAAMA,iBAAN,MAAMA;EAXb,OAWaA;;;;;;EACHC,WAAW,oBAAIC,IAAAA;EAEvB,YACkBC,KACAC,KACAC,MAChB;SAHgBF,MAAAA;SACAC,MAAAA;SACAC,OAAAA;EACf;;EAIH,IAAIC,OAAc;AAChB,WAAO,KAAKH,IAAIG;EAClB;EAEA,IAAIC,SAAkB;AACpB,WAAO,KAAKJ,IAAII;EAClB;EAEA,IAAIC,QAAgB;AAClB,WAAO,KAAKL,IAAIK;EAClB;EAEA,IAAIC,UAAU;AACZ,WAAO,KAAKN,IAAIM;EAClB;EAEA,IAAIC,YAAgC;AAClC,WAAQ,KAAKP,IAAYO,aAAc,KAAKP,IAAIM,QAAQ,cAAA;EAC1D;;EAGA,IAAIE,UAAe;AACjB,WAAQ,KAAKR,IAAYQ;EAC3B;;;;;;;;;;;;;;;;;;;;;EAuBAC,GAAGC,aAA6C;AAC9C,WAAOC,WAAW,KAAKX,IAAIK,OAA8BK,WAAAA;EAC3D;;;EAKA,IAAIE,OAAY;AACd,WAAQ,KAAKZ,IAAYY;EAC3B;;EAGA,IAAIC,QAA2B;AAC7B,WAAQ,KAAKb,IAAYa;EAC3B;;EAIAC,IAAaC,KAA4B;AACvC,WAAO,KAAKjB,SAASgB,IAAIC,GAAAA;EAC3B;EAEAC,IAAID,KAAaE,OAAkB;AACjC,SAAKnB,SAASkB,IAAID,KAAKE,KAAAA;EACzB;;EAIAC,KAAKC,MAAWC,SAAS,KAAK;AAC5B,WAAO,KAAKnB,IAAImB,OAAOA,MAAAA,EAAQF,KAAKC,IAAAA;EACtC;EAEAE,QAAQF,MAAW;AACjB,WAAO,KAAKlB,IAAImB,OAAO,GAAA,EAAKF,KAAKC,IAAAA;EACnC;EAEAG,YAAY;AACV,WAAO,KAAKrB,IAAImB,OAAO,GAAA,EAAKG,IAAG;EACjC;EAEAC,SAASC,UAAU,aAAa;AAC9B,WAAO,KAAKxB,IAAImB,OAAO,GAAA,EAAKF,KAAK;MAAEO;IAAQ,CAAA;EAC7C;EAEAC,WAAWD,SAAiB;AAC1B,WAAO,KAAKxB,IAAImB,OAAO,GAAA,EAAKF,KAAK;MAAEO;IAAQ,CAAA;EAC7C;EAEAE,KAAKC,SAAiBR,SAAS,KAAK;AAClC,WAAO,KAAKnB,IAAImB,OAAOA,MAAAA,EAAQS,KAAK,MAAA,EAAQC,KAAKF,OAAAA;EACnD;EAEAG,SAASC,QAAgBC,UAAkBC,cAAc,4BAA4B;AACnF,SAAKjC,IAAIkC,UAAU,uBAAuB,yBAAyBF,QAAAA,GAAW;AAC9E,SAAKhC,IAAIkC,UAAU,gBAAgBD,WAAAA;AACnC,WAAO,KAAKjC,IAAI6B,KAAKE,MAAAA;EACvB;;;;;;;;;;;;;;;;;;;;;;;;;EA0BA,MAAMI,SACJC,SACA3B,aACA;AACA,UAAM4B,SAAS,KAAK7B,GAAGC,WAAAA;AACvB,UAAM,EAAES,MAAMoB,MAAK,IAAK,MAAMF,QAAQC,MAAAA;AACtC,UAAM,EAAEE,MAAMC,MAAK,IAAKH,OAAOI;AAC/B,UAAMC,aAAaC,KAAKC,KAAKN,QAAQE,KAAAA,KAAU;AAE/C,UAAMK,WAAiC;MACrC3B;MACA4B,MAAM;QACJP;QACAC;QACAF;QACAI;QACAK,SAASR,OAAOG;QAChBM,SAAST,OAAO;MAClB;IACF;AAEA,WAAO,KAAKtB,KAAK4B,QAAAA;EACnB;AACF;","names":["RequestContext","metadata","Map","req","res","next","body","params","query","headers","requestId","session","qs","fieldConfig","parseQuery","file","files","get","key","set","value","json","data","status","created","noContent","end","notFound","message","badRequest","html","content","type","send","download","buffer","filename","contentType","setHeader","paginate","fetcher","parsed","total","page","limit","pagination","totalPages","Math","ceil","response","meta","hasNext","hasPrev"]}
1
+ {"version":3,"sources":["../src/context.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport {\n parseQuery,\n type ParsedQuery,\n type QueryFieldConfig,\n type PaginatedResponse,\n} from './query'\n\n/**\n * Unified request/response abstraction passed to every controller method.\n * Shields handlers from raw Express objects and provides convenience methods.\n */\nexport class RequestContext<TBody = any, TParams = any, TQuery = any> {\n private metadata = new Map<string, any>()\n\n constructor(\n public readonly req: Request,\n public readonly res: Response,\n public readonly next: NextFunction,\n ) {}\n\n // ── Request Data ────────────────────────────────────────────────────\n\n get body(): TBody {\n return this.req.body as TBody\n }\n\n get params(): TParams {\n return this.req.params as TParams\n }\n\n get query(): TQuery {\n return this.req.query as TQuery\n }\n\n get headers() {\n return this.req.headers\n }\n\n get requestId(): string | undefined {\n return (this.req as any).requestId ?? (this.req.headers['x-request-id'] as string | undefined)\n }\n\n /** Session data (requires session middleware) */\n get session(): any {\n return (this.req as any).session\n }\n\n // ── Query String Parsing ───────────────────────────────────────────\n\n /**\n * Parse the request query string into structured filters, sort, pagination, and search.\n * Pass the result to an ORM query builder adapter (Drizzle, Prisma, Sequelize, etc.).\n *\n * @param fieldConfig - Optional whitelist for filterable, sortable, and searchable fields\n *\n * @example\n * ```ts\n * @Get('/')\n * async list(ctx: RequestContext) {\n * const parsed = ctx.qs({\n * filterable: ['status', 'priority'],\n * sortable: ['createdAt', 'title'],\n * })\n * const q = drizzleAdapter.build(parsed, { columns })\n * // ... use q.where, q.orderBy, q.limit, q.offset\n * }\n * ```\n */\n qs(fieldConfig?: QueryFieldConfig): ParsedQuery {\n return parseQuery(this.req.query as Record<string, any>, fieldConfig)\n }\n\n // ── File Uploads ────────────────────────────────────────────────────\n\n /** Single uploaded file (requires @FileUpload({ mode: 'single' })) */\n get file(): any {\n return (this.req as any).file\n }\n\n /** Array of uploaded files (requires @FileUpload({ mode: 'array' })) */\n get files(): any[] | undefined {\n return (this.req as any).files\n }\n\n // ── Metadata Store ──────────────────────────────────────────────────\n\n get<T = any>(key: string): T | undefined {\n return this.metadata.get(key) as T | undefined\n }\n\n set(key: string, value: any): void {\n this.metadata.set(key, value)\n }\n\n // ── Response Helpers ────────────────────────────────────────────────\n\n json(data: any, status = 200) {\n return this.res.status(status).json(data)\n }\n\n created(data: any) {\n return this.res.status(201).json(data)\n }\n\n noContent() {\n return this.res.status(204).end()\n }\n\n notFound(message = 'Not Found') {\n return this.res.status(404).json({ message })\n }\n\n badRequest(message: string) {\n return this.res.status(400).json({ message })\n }\n\n html(content: string, status = 200) {\n return this.res.status(status).type('html').send(content)\n }\n\n download(buffer: Buffer, filename: string, contentType = 'application/octet-stream') {\n this.res.setHeader('Content-Disposition', `attachment; filename=\"${filename}\"`)\n this.res.setHeader('Content-Type', contentType)\n return this.res.send(buffer)\n }\n\n /**\n * Parse query params and return a standardized paginated response.\n * Calls `ctx.qs()` internally, then wraps your data with pagination meta.\n *\n * @param fetcher - Async function that receives ParsedQuery and returns `{ data, total }`\n * @param fieldConfig - Optional whitelist for filterable, sortable, searchable fields\n *\n * @example\n * ```ts\n * @Get('/')\n * async list(ctx: RequestContext) {\n * return ctx.paginate(\n * async (parsed) => {\n * const data = await db.select().from(users)\n * .where(query.where).limit(parsed.pagination.limit)\n * .offset(parsed.pagination.offset).all()\n * const total = await db.select({ count: count() }).from(users).get()\n * return { data, total: total?.count ?? 0 }\n * },\n * { filterable: ['name', 'role'], sortable: ['createdAt'] },\n * )\n * }\n * ```\n */\n async paginate<T>(\n fetcher: (parsed: ParsedQuery) => Promise<{ data: T[]; total: number }>,\n fieldConfig?: QueryFieldConfig,\n ) {\n const parsed = this.qs(fieldConfig)\n const { data, total } = await fetcher(parsed)\n const { page, limit } = parsed.pagination\n const totalPages = Math.ceil(total / limit) || 1\n\n const response: PaginatedResponse<T> = {\n data,\n meta: {\n page,\n limit,\n total,\n totalPages,\n hasNext: page < totalPages,\n hasPrev: page > 1,\n },\n }\n\n return this.json(response)\n }\n\n // ── Server-Sent Events ──────────────────────────────────────────────\n\n /**\n * Start an SSE (Server-Sent Events) stream.\n * Sets the correct headers and returns helpers to send events.\n *\n * @example\n * ```ts\n * @Get('/events')\n * async stream(ctx: RequestContext) {\n * const sse = ctx.sse()\n *\n * const interval = setInterval(() => {\n * sse.send({ time: new Date().toISOString() }, 'tick')\n * }, 1000)\n *\n * sse.onClose(() => clearInterval(interval))\n * }\n * ```\n */\n sse() {\n this.res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n })\n this.res.flushHeaders()\n\n const closeCallbacks: Array<() => void> = []\n\n this.req.on('close', () => {\n for (const cb of closeCallbacks) cb()\n })\n\n return {\n /** Send an SSE event with optional event name and id */\n send: (data: any, event?: string, id?: string) => {\n if (id) this.res.write(`id: ${id}\\n`)\n if (event) this.res.write(`event: ${event}\\n`)\n this.res.write(`data: ${JSON.stringify(data)}\\n\\n`)\n },\n /** Send a comment (keeps connection alive) */\n comment: (text: string) => {\n this.res.write(`: ${text}\\n\\n`)\n },\n /** Register a callback when the client disconnects */\n onClose: (fn: () => void) => {\n closeCallbacks.push(fn)\n },\n /** End the SSE stream */\n close: () => {\n this.res.end()\n },\n }\n }\n}\n"],"mappings":"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","names":["RequestContext","metadata","Map","req","res","next","body","params","query","headers","requestId","session","qs","fieldConfig","parseQuery","file","files","get","key","set","value","json","data","status","created","noContent","end","notFound","message","badRequest","html","content","type","send","download","buffer","filename","contentType","setHeader","paginate","fetcher","parsed","total","page","limit","pagination","totalPages","Math","ceil","response","meta","hasNext","hasPrev","sse","writeHead","Connection","flushHeaders","closeCallbacks","on","cb","event","id","write","JSON","stringify","comment","text","onClose","fn","push","close"]}
@@ -23,11 +23,17 @@ var Application = class {
23
23
  container;
24
24
  httpServer = null;
25
25
  adapters;
26
+ plugins;
26
27
  constructor(options) {
27
28
  this.options = options;
28
29
  this.app = express();
29
30
  this.container = Container.getInstance();
30
- this.adapters = options.adapters ?? [];
31
+ this.plugins = options.plugins ?? [];
32
+ this.adapters = [
33
+ // Plugin adapters first
34
+ ...this.plugins.flatMap((p) => p.adapters?.() ?? []),
35
+ ...options.adapters ?? []
36
+ ];
31
37
  }
32
38
  /**
33
39
  * Full setup pipeline:
@@ -51,6 +57,15 @@ var Application = class {
51
57
  this.app.disable("x-powered-by");
52
58
  this.app.set("trust proxy", this.options.trustProxy ?? false);
53
59
  this.mountMiddlewareList(adapterMw.beforeGlobal);
60
+ for (const plugin of this.plugins) {
61
+ plugin.register?.(this.container);
62
+ }
63
+ for (const plugin of this.plugins) {
64
+ const mw = plugin.middleware?.() ?? [];
65
+ for (const handler of mw) {
66
+ this.app.use(handler);
67
+ }
68
+ }
54
69
  if (this.options.middleware) {
55
70
  for (const entry of this.options.middleware) {
56
71
  this.mountMiddlewareEntry(entry);
@@ -62,7 +77,11 @@ var Application = class {
62
77
  }));
63
78
  }
64
79
  this.mountMiddlewareList(adapterMw.afterGlobal);
65
- const modules = this.options.modules.map((ModuleClass) => {
80
+ const allModuleClasses = [
81
+ ...this.plugins.flatMap((p) => p.modules?.() ?? []),
82
+ ...this.options.modules
83
+ ];
84
+ const modules = allModuleClasses.map((ModuleClass) => {
66
85
  const mod = new ModuleClass();
67
86
  mod.register(this.container);
68
87
  return mod;
@@ -109,11 +128,14 @@ var Application = class {
109
128
  }
110
129
  throw err;
111
130
  });
112
- this.httpServer.listen(port, () => {
131
+ this.httpServer.listen(port, async () => {
113
132
  log.info(`Server running on http://localhost:${port}`);
114
133
  for (const adapter of this.adapters) {
115
134
  adapter.afterStart?.(this.httpServer, this.container);
116
135
  }
136
+ for (const plugin of this.plugins) {
137
+ await plugin.onReady?.(this.container);
138
+ }
117
139
  });
118
140
  }
119
141
  /** HMR rebuild: swap Express handler without restarting the server */
@@ -131,7 +153,10 @@ var Application = class {
131
153
  /** Graceful shutdown — runs all adapter shutdowns in parallel, resilient to failures */
132
154
  async shutdown() {
133
155
  log.info("Shutting down...");
134
- const results = await Promise.allSettled(this.adapters.map((adapter) => Promise.resolve(adapter.shutdown?.())));
156
+ const results = await Promise.allSettled([
157
+ ...this.plugins.map((plugin) => Promise.resolve(plugin.shutdown?.())),
158
+ ...this.adapters.map((adapter) => Promise.resolve(adapter.shutdown?.()))
159
+ ]);
135
160
  for (const result of results) {
136
161
  if (result.status === "rejected") {
137
162
  log.error({
@@ -187,4 +212,4 @@ var Application = class {
187
212
  export {
188
213
  Application
189
214
  };
190
- //# sourceMappingURL=chunk-OKB76LY2.js.map
215
+ //# sourceMappingURL=chunk-YRCR6Z5C.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/application.ts"],"sourcesContent":["import http from 'node:http'\nimport express, { type Express, type RequestHandler } from 'express'\nimport {\n Container,\n createLogger,\n type AppModuleClass,\n type AppAdapter,\n type AdapterMiddleware,\n type KickPlugin,\n} from '@forinda/kickjs-core'\nimport { buildRoutes } from './router-builder'\nimport { requestId } from './middleware/request-id'\nimport { notFoundHandler, errorHandler } from './middleware/error-handler'\n\nconst log = createLogger('Application')\n\n/**\n * A middleware entry in the declarative pipeline.\n * Can be a bare handler or an object with path scoping.\n */\nexport type MiddlewareEntry = RequestHandler | { path: string; handler: RequestHandler }\n\nexport interface ApplicationOptions {\n /** Feature modules to load */\n modules: AppModuleClass[]\n /** Adapters that hook into the lifecycle (DB, Redis, Swagger, etc.) */\n adapters?: AppAdapter[]\n /** Server port (falls back to PORT env var, then 3000) */\n port?: number\n /** Global API prefix (default: '/api') */\n apiPrefix?: string\n /** Default API version (default: 1) — routes become /{prefix}/v{version}/{path} */\n defaultVersion?: number\n\n /**\n * Global middleware pipeline. Declared in order.\n * Replaces the hardcoded middleware stack — you control exactly what runs.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules,\n * middleware: [\n * helmet(),\n * cors(),\n * compression(),\n * morgan('dev'),\n * express.json({ limit: '1mb' }),\n * ],\n * })\n * ```\n *\n * If omitted, a sensible default is applied:\n * requestId(), express.json({ limit: '100kb' })\n */\n middleware?: MiddlewareEntry[]\n\n /** Plugins that bundle modules, adapters, middleware, and DI bindings */\n plugins?: KickPlugin[]\n\n /** Express `trust proxy` setting */\n trustProxy?: boolean | number | string | ((ip: string, hopIndex: number) => boolean)\n /** Maximum JSON body size (only used when middleware is not provided) */\n jsonLimit?: string | number\n}\n\n/**\n * The main application class. Wires together Express, the DI container,\n * feature modules, adapters, and the middleware pipeline.\n */\nexport class Application {\n private app: Express\n private container: Container\n private httpServer: http.Server | null = null\n private adapters: AppAdapter[]\n\n private plugins: KickPlugin[]\n\n constructor(private readonly options: ApplicationOptions) {\n this.app = express()\n this.container = Container.getInstance()\n this.plugins = options.plugins ?? []\n this.adapters = [\n // Plugin adapters first\n ...this.plugins.flatMap((p) => p.adapters?.() ?? []),\n ...(options.adapters ?? []),\n ]\n }\n\n /**\n * Full setup pipeline:\n * 1. Adapter beforeMount hooks (early routes — docs, health)\n * 2. Adapter middleware (phase: beforeGlobal)\n * 3. Global middleware (user-declared or defaults)\n * 4. Adapter middleware (phase: afterGlobal)\n * 5. Module registration + DI bootstrap\n * 6. Adapter middleware (phase: beforeRoutes)\n * 7. Module route mounting\n * 8. Adapter middleware (phase: afterRoutes)\n * 9. Error handlers (notFound + global)\n * 10. Adapter beforeStart hooks\n */\n setup(): void {\n log.info('Bootstrapping application...')\n\n // Collect adapter middleware by phase\n const adapterMw = this.collectAdapterMiddleware()\n\n // ── 1. Adapter beforeMount hooks ──────────────────────────────────\n for (const adapter of this.adapters) {\n adapter.beforeMount?.(this.app, this.container)\n }\n\n // ── 2. Hardened defaults ──────────────────────────────────────────\n this.app.disable('x-powered-by')\n this.app.set('trust proxy', this.options.trustProxy ?? false)\n\n // ── 3. Adapter middleware: beforeGlobal ───────────────────────────\n this.mountMiddlewareList(adapterMw.beforeGlobal)\n\n // ── 3b. Plugin registration ──────────────────────────────────────\n for (const plugin of this.plugins) {\n plugin.register?.(this.container)\n }\n\n // ── 3c. Plugin middleware ─────────────────────────────────────────\n for (const plugin of this.plugins) {\n const mw = plugin.middleware?.() ?? []\n for (const handler of mw) {\n this.app.use(handler)\n }\n }\n\n // ── 4. Global middleware ─────────────────────────────────────────\n if (this.options.middleware) {\n // User-declared pipeline — full control\n for (const entry of this.options.middleware) {\n this.mountMiddlewareEntry(entry)\n }\n } else {\n // Sensible defaults when no middleware declared\n this.app.use(requestId())\n this.app.use(express.json({ limit: this.options.jsonLimit ?? '100kb' }))\n }\n\n // ── 5. Adapter middleware: afterGlobal ────────────────────────────\n this.mountMiddlewareList(adapterMw.afterGlobal)\n\n // ── 6. Module registration + DI bootstrap ────────────────────────\n // Plugin modules first, then user modules\n const allModuleClasses = [\n ...this.plugins.flatMap((p) => p.modules?.() ?? []),\n ...this.options.modules,\n ]\n const modules = allModuleClasses.map((ModuleClass) => {\n const mod = new ModuleClass()\n mod.register(this.container)\n return mod\n })\n this.container.bootstrap()\n\n // ── 7. Adapter middleware: beforeRoutes ───────────────────────────\n this.mountMiddlewareList(adapterMw.beforeRoutes)\n\n // ── 8. Mount module routes with versioning ───────────────────────\n const apiPrefix = this.options.apiPrefix ?? '/api'\n const defaultVersion = this.options.defaultVersion ?? 1\n\n for (const mod of modules) {\n const result = mod.routes()\n const routeSets = Array.isArray(result) ? result : [result]\n\n for (const route of routeSets) {\n const version = route.version ?? defaultVersion\n const mountPath = `${apiPrefix}/v${version}${route.path}`\n this.app.use(mountPath, route.router)\n\n // Notify adapters (e.g. SwaggerAdapter for OpenAPI spec generation)\n if (route.controller) {\n for (const adapter of this.adapters) {\n adapter.onRouteMount?.(route.controller, mountPath)\n }\n }\n }\n }\n\n // ── 9. Adapter middleware: afterRoutes ────────────────────────────\n this.mountMiddlewareList(adapterMw.afterRoutes)\n\n // ── 10. Error handlers ───────────────────────────────────────────\n this.app.use(notFoundHandler())\n this.app.use(errorHandler())\n\n // ── 11. Adapter beforeStart hooks ────────────────────────────────\n for (const adapter of this.adapters) {\n adapter.beforeStart?.(this.app, this.container)\n }\n }\n\n /** Start the HTTP server — fails fast if port is in use */\n start(): void {\n this.setup()\n\n const port = this.options.port ?? parseInt(process.env.PORT || '3000', 10)\n this.httpServer = http.createServer(this.app)\n\n this.httpServer.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n log.error(\n `Port ${port} is already in use. Kill the existing process or use a different port:\\n` +\n ` PORT=${port + 1} kick dev\\n` +\n ` lsof -i :${port} # find what's using it\\n` +\n ` kill <PID> # stop it`,\n )\n process.exit(1)\n }\n throw err\n })\n\n this.httpServer.listen(port, async () => {\n log.info(`Server running on http://localhost:${port}`)\n\n for (const adapter of this.adapters) {\n adapter.afterStart?.(this.httpServer!, this.container)\n }\n\n // Plugin onReady hooks\n for (const plugin of this.plugins) {\n await plugin.onReady?.(this.container)\n }\n })\n }\n\n /** HMR rebuild: swap Express handler without restarting the server */\n rebuild(): void {\n // Reset the DI container so singletons are re-created with fresh code\n Container.reset()\n this.container = Container.getInstance()\n\n this.app = express()\n this.setup()\n\n if (this.httpServer) {\n this.httpServer.removeAllListeners('request')\n this.httpServer.on('request', this.app)\n log.info('HMR: Express app rebuilt and swapped')\n }\n }\n\n /** Graceful shutdown — runs all adapter shutdowns in parallel, resilient to failures */\n async shutdown(): Promise<void> {\n log.info('Shutting down...')\n\n // Run all plugin + adapter shutdowns concurrently\n const results = await Promise.allSettled([\n ...this.plugins.map((plugin) => Promise.resolve(plugin.shutdown?.())),\n ...this.adapters.map((adapter) => Promise.resolve(adapter.shutdown?.())),\n ])\n for (const result of results) {\n if (result.status === 'rejected') {\n log.error({ err: result.reason }, 'Adapter shutdown failed')\n }\n }\n\n if (this.httpServer) {\n await new Promise<void>((resolve) => this.httpServer!.close(() => resolve()))\n }\n }\n\n getExpressApp(): Express {\n return this.app\n }\n\n getHttpServer(): http.Server | null {\n return this.httpServer\n }\n\n // ── Internal helpers ────────────────────────────────────────────────\n\n private collectAdapterMiddleware() {\n const result = {\n beforeGlobal: [] as AdapterMiddleware[],\n afterGlobal: [] as AdapterMiddleware[],\n beforeRoutes: [] as AdapterMiddleware[],\n afterRoutes: [] as AdapterMiddleware[],\n }\n\n for (const adapter of this.adapters) {\n const entries = adapter.middleware?.() ?? []\n for (const entry of entries) {\n const phase = entry.phase ?? 'afterGlobal'\n result[phase].push(entry)\n }\n }\n\n return result\n }\n\n private mountMiddlewareList(entries: AdapterMiddleware[]): void {\n for (const entry of entries) {\n if (entry.path) {\n this.app.use(entry.path, entry.handler)\n } else {\n this.app.use(entry.handler)\n }\n }\n }\n\n private mountMiddlewareEntry(entry: MiddlewareEntry): void {\n if (typeof entry === 'function') {\n this.app.use(entry)\n } else {\n this.app.use(entry.path, entry.handler)\n }\n }\n}\n"],"mappings":"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","names":["http","express","Container","createLogger","log","createLogger","Application","app","container","httpServer","adapters","plugins","options","express","Container","getInstance","flatMap","p","setup","info","adapterMw","collectAdapterMiddleware","adapter","beforeMount","disable","set","trustProxy","mountMiddlewareList","beforeGlobal","plugin","register","mw","middleware","handler","use","entry","mountMiddlewareEntry","requestId","json","limit","jsonLimit","afterGlobal","allModuleClasses","modules","map","ModuleClass","mod","bootstrap","beforeRoutes","apiPrefix","defaultVersion","result","routes","routeSets","Array","isArray","route","version","mountPath","path","router","controller","onRouteMount","afterRoutes","notFoundHandler","errorHandler","beforeStart","start","port","parseInt","process","env","PORT","http","createServer","on","err","code","error","exit","listen","afterStart","onReady","rebuild","reset","removeAllListeners","shutdown","results","Promise","allSettled","resolve","status","reason","close","getExpressApp","getHttpServer","entries","phase","push"]}
package/dist/context.d.ts CHANGED
@@ -80,6 +80,34 @@ declare class RequestContext<TBody = any, TParams = any, TQuery = any> {
80
80
  data: T[];
81
81
  total: number;
82
82
  }>, fieldConfig?: QueryFieldConfig): Promise<Response<any, Record<string, any>>>;
83
+ /**
84
+ * Start an SSE (Server-Sent Events) stream.
85
+ * Sets the correct headers and returns helpers to send events.
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * @Get('/events')
90
+ * async stream(ctx: RequestContext) {
91
+ * const sse = ctx.sse()
92
+ *
93
+ * const interval = setInterval(() => {
94
+ * sse.send({ time: new Date().toISOString() }, 'tick')
95
+ * }, 1000)
96
+ *
97
+ * sse.onClose(() => clearInterval(interval))
98
+ * }
99
+ * ```
100
+ */
101
+ sse(): {
102
+ /** Send an SSE event with optional event name and id */
103
+ send: (data: any, event?: string, id?: string) => void;
104
+ /** Send a comment (keeps connection alive) */
105
+ comment: (text: string) => void;
106
+ /** Register a callback when the client disconnects */
107
+ onClose: (fn: () => void) => void;
108
+ /** End the SSE stream */
109
+ close: () => void;
110
+ };
83
111
  }
84
112
 
85
113
  export { RequestContext };
package/dist/context.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  RequestContext
3
- } from "./chunk-GDMORQ2P.js";
3
+ } from "./chunk-Y5ZSC2FB.js";
4
4
  import "./chunk-WYY34UWG.js";
5
5
  import "./chunk-WCQVDF3K.js";
6
6
  export {
package/dist/devtools.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  DevToolsAdapter
3
- } from "./chunk-YYT24FTH.js";
3
+ } from "./chunk-PLKCXCBN.js";
4
4
  import "./chunk-WCQVDF3K.js";
5
5
  export {
6
6
  DevToolsAdapter
package/dist/index.js CHANGED
@@ -6,21 +6,21 @@ import {
6
6
  } from "./chunk-NQJNMKW5.js";
7
7
  import {
8
8
  bootstrap
9
- } from "./chunk-D76WCWAW.js";
9
+ } from "./chunk-W35YEQOE.js";
10
10
  import {
11
11
  Application
12
- } from "./chunk-OKB76LY2.js";
12
+ } from "./chunk-YRCR6Z5C.js";
13
13
  import {
14
14
  REQUEST_ID_HEADER,
15
15
  requestId
16
16
  } from "./chunk-35NUARK7.js";
17
17
  import {
18
18
  DevToolsAdapter
19
- } from "./chunk-YYT24FTH.js";
19
+ } from "./chunk-PLKCXCBN.js";
20
20
  import {
21
21
  buildRoutes,
22
22
  getControllerPath
23
- } from "./chunk-S7DJ5HF3.js";
23
+ } from "./chunk-E552HYEB.js";
24
24
  import {
25
25
  buildUploadMiddleware,
26
26
  cleanupFiles,
@@ -32,7 +32,7 @@ import {
32
32
  } from "./chunk-RPN7UFUO.js";
33
33
  import {
34
34
  RequestContext
35
- } from "./chunk-GDMORQ2P.js";
35
+ } from "./chunk-Y5ZSC2FB.js";
36
36
  import {
37
37
  FILTER_OPERATORS,
38
38
  buildQueryParams,
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  buildRoutes,
3
3
  getControllerPath
4
- } from "./chunk-S7DJ5HF3.js";
4
+ } from "./chunk-E552HYEB.js";
5
5
  import "./chunk-LEILPDMW.js";
6
6
  import "./chunk-RPN7UFUO.js";
7
- import "./chunk-GDMORQ2P.js";
7
+ import "./chunk-Y5ZSC2FB.js";
8
8
  import "./chunk-WYY34UWG.js";
9
9
  import "./chunk-WCQVDF3K.js";
10
10
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forinda/kickjs-http",
3
- "version": "0.5.2",
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.5.2"
101
+ "@forinda/kickjs-core": "0.7.0"
102
102
  },
103
103
  "peerDependencies": {
104
104
  "express": "^5.1.0"
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/application.ts"],"sourcesContent":["import http from 'node:http'\nimport express, { type Express, type RequestHandler } from 'express'\nimport {\n Container,\n createLogger,\n type AppModuleClass,\n type AppAdapter,\n type AdapterMiddleware,\n} from '@forinda/kickjs-core'\nimport { buildRoutes } from './router-builder'\nimport { requestId } from './middleware/request-id'\nimport { notFoundHandler, errorHandler } from './middleware/error-handler'\n\nconst log = createLogger('Application')\n\n/**\n * A middleware entry in the declarative pipeline.\n * Can be a bare handler or an object with path scoping.\n */\nexport type MiddlewareEntry = RequestHandler | { path: string; handler: RequestHandler }\n\nexport interface ApplicationOptions {\n /** Feature modules to load */\n modules: AppModuleClass[]\n /** Adapters that hook into the lifecycle (DB, Redis, Swagger, etc.) */\n adapters?: AppAdapter[]\n /** Server port (falls back to PORT env var, then 3000) */\n port?: number\n /** Global API prefix (default: '/api') */\n apiPrefix?: string\n /** Default API version (default: 1) — routes become /{prefix}/v{version}/{path} */\n defaultVersion?: number\n\n /**\n * Global middleware pipeline. Declared in order.\n * Replaces the hardcoded middleware stack — you control exactly what runs.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules,\n * middleware: [\n * helmet(),\n * cors(),\n * compression(),\n * morgan('dev'),\n * express.json({ limit: '1mb' }),\n * ],\n * })\n * ```\n *\n * If omitted, a sensible default is applied:\n * requestId(), express.json({ limit: '100kb' })\n */\n middleware?: MiddlewareEntry[]\n\n /** Express `trust proxy` setting */\n trustProxy?: boolean | number | string | ((ip: string, hopIndex: number) => boolean)\n /** Maximum JSON body size (only used when middleware is not provided) */\n jsonLimit?: string | number\n}\n\n/**\n * The main application class. Wires together Express, the DI container,\n * feature modules, adapters, and the middleware pipeline.\n */\nexport class Application {\n private app: Express\n private container: Container\n private httpServer: http.Server | null = null\n private adapters: AppAdapter[]\n\n constructor(private readonly options: ApplicationOptions) {\n this.app = express()\n this.container = Container.getInstance()\n this.adapters = options.adapters ?? []\n }\n\n /**\n * Full setup pipeline:\n * 1. Adapter beforeMount hooks (early routes — docs, health)\n * 2. Adapter middleware (phase: beforeGlobal)\n * 3. Global middleware (user-declared or defaults)\n * 4. Adapter middleware (phase: afterGlobal)\n * 5. Module registration + DI bootstrap\n * 6. Adapter middleware (phase: beforeRoutes)\n * 7. Module route mounting\n * 8. Adapter middleware (phase: afterRoutes)\n * 9. Error handlers (notFound + global)\n * 10. Adapter beforeStart hooks\n */\n setup(): void {\n log.info('Bootstrapping application...')\n\n // Collect adapter middleware by phase\n const adapterMw = this.collectAdapterMiddleware()\n\n // ── 1. Adapter beforeMount hooks ──────────────────────────────────\n for (const adapter of this.adapters) {\n adapter.beforeMount?.(this.app, this.container)\n }\n\n // ── 2. Hardened defaults ──────────────────────────────────────────\n this.app.disable('x-powered-by')\n this.app.set('trust proxy', this.options.trustProxy ?? false)\n\n // ── 3. Adapter middleware: beforeGlobal ───────────────────────────\n this.mountMiddlewareList(adapterMw.beforeGlobal)\n\n // ── 4. Global middleware ─────────────────────────────────────────\n if (this.options.middleware) {\n // User-declared pipeline — full control\n for (const entry of this.options.middleware) {\n this.mountMiddlewareEntry(entry)\n }\n } else {\n // Sensible defaults when no middleware declared\n this.app.use(requestId())\n this.app.use(express.json({ limit: this.options.jsonLimit ?? '100kb' }))\n }\n\n // ── 5. Adapter middleware: afterGlobal ────────────────────────────\n this.mountMiddlewareList(adapterMw.afterGlobal)\n\n // ── 6. Module registration + DI bootstrap ────────────────────────\n const modules = this.options.modules.map((ModuleClass) => {\n const mod = new ModuleClass()\n mod.register(this.container)\n return mod\n })\n this.container.bootstrap()\n\n // ── 7. Adapter middleware: beforeRoutes ───────────────────────────\n this.mountMiddlewareList(adapterMw.beforeRoutes)\n\n // ── 8. Mount module routes with versioning ───────────────────────\n const apiPrefix = this.options.apiPrefix ?? '/api'\n const defaultVersion = this.options.defaultVersion ?? 1\n\n for (const mod of modules) {\n const result = mod.routes()\n const routeSets = Array.isArray(result) ? result : [result]\n\n for (const route of routeSets) {\n const version = route.version ?? defaultVersion\n const mountPath = `${apiPrefix}/v${version}${route.path}`\n this.app.use(mountPath, route.router)\n\n // Notify adapters (e.g. SwaggerAdapter for OpenAPI spec generation)\n if (route.controller) {\n for (const adapter of this.adapters) {\n adapter.onRouteMount?.(route.controller, mountPath)\n }\n }\n }\n }\n\n // ── 9. Adapter middleware: afterRoutes ────────────────────────────\n this.mountMiddlewareList(adapterMw.afterRoutes)\n\n // ── 10. Error handlers ───────────────────────────────────────────\n this.app.use(notFoundHandler())\n this.app.use(errorHandler())\n\n // ── 11. Adapter beforeStart hooks ────────────────────────────────\n for (const adapter of this.adapters) {\n adapter.beforeStart?.(this.app, this.container)\n }\n }\n\n /** Start the HTTP server — fails fast if port is in use */\n start(): void {\n this.setup()\n\n const port = this.options.port ?? parseInt(process.env.PORT || '3000', 10)\n this.httpServer = http.createServer(this.app)\n\n this.httpServer.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n log.error(\n `Port ${port} is already in use. Kill the existing process or use a different port:\\n` +\n ` PORT=${port + 1} kick dev\\n` +\n ` lsof -i :${port} # find what's using it\\n` +\n ` kill <PID> # stop it`,\n )\n process.exit(1)\n }\n throw err\n })\n\n this.httpServer.listen(port, () => {\n log.info(`Server running on http://localhost:${port}`)\n\n for (const adapter of this.adapters) {\n adapter.afterStart?.(this.httpServer!, this.container)\n }\n })\n }\n\n /** HMR rebuild: swap Express handler without restarting the server */\n rebuild(): void {\n // Reset the DI container so singletons are re-created with fresh code\n Container.reset()\n this.container = Container.getInstance()\n\n this.app = express()\n this.setup()\n\n if (this.httpServer) {\n this.httpServer.removeAllListeners('request')\n this.httpServer.on('request', this.app)\n log.info('HMR: Express app rebuilt and swapped')\n }\n }\n\n /** Graceful shutdown — runs all adapter shutdowns in parallel, resilient to failures */\n async shutdown(): Promise<void> {\n log.info('Shutting down...')\n\n // Run all adapter shutdowns concurrently — don't let one failure block the rest\n const results = await Promise.allSettled(\n this.adapters.map((adapter) => Promise.resolve(adapter.shutdown?.())),\n )\n for (const result of results) {\n if (result.status === 'rejected') {\n log.error({ err: result.reason }, 'Adapter shutdown failed')\n }\n }\n\n if (this.httpServer) {\n await new Promise<void>((resolve) => this.httpServer!.close(() => resolve()))\n }\n }\n\n getExpressApp(): Express {\n return this.app\n }\n\n getHttpServer(): http.Server | null {\n return this.httpServer\n }\n\n // ── Internal helpers ────────────────────────────────────────────────\n\n private collectAdapterMiddleware() {\n const result = {\n beforeGlobal: [] as AdapterMiddleware[],\n afterGlobal: [] as AdapterMiddleware[],\n beforeRoutes: [] as AdapterMiddleware[],\n afterRoutes: [] as AdapterMiddleware[],\n }\n\n for (const adapter of this.adapters) {\n const entries = adapter.middleware?.() ?? []\n for (const entry of entries) {\n const phase = entry.phase ?? 'afterGlobal'\n result[phase].push(entry)\n }\n }\n\n return result\n }\n\n private mountMiddlewareList(entries: AdapterMiddleware[]): void {\n for (const entry of entries) {\n if (entry.path) {\n this.app.use(entry.path, entry.handler)\n } else {\n this.app.use(entry.handler)\n }\n }\n }\n\n private mountMiddlewareEntry(entry: MiddlewareEntry): void {\n if (typeof entry === 'function') {\n this.app.use(entry)\n } else {\n this.app.use(entry.path, entry.handler)\n }\n }\n}\n"],"mappings":"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","names":["http","express","Container","createLogger","log","createLogger","Application","app","container","httpServer","adapters","options","express","Container","getInstance","setup","info","adapterMw","collectAdapterMiddleware","adapter","beforeMount","disable","set","trustProxy","mountMiddlewareList","beforeGlobal","middleware","entry","mountMiddlewareEntry","use","requestId","json","limit","jsonLimit","afterGlobal","modules","map","ModuleClass","mod","register","bootstrap","beforeRoutes","apiPrefix","defaultVersion","result","routes","routeSets","Array","isArray","route","version","mountPath","path","router","controller","onRouteMount","afterRoutes","notFoundHandler","errorHandler","beforeStart","start","port","parseInt","process","env","PORT","http","createServer","on","err","code","error","exit","listen","afterStart","rebuild","reset","removeAllListeners","shutdown","results","Promise","allSettled","resolve","status","reason","close","getExpressApp","getHttpServer","entries","phase","push","handler"]}
@@ -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"]}