@forinda/kickjs-http 0.4.3 → 0.5.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.
@@ -73,7 +73,7 @@ declare class Application {
73
73
  * 10. Adapter beforeStart hooks
74
74
  */
75
75
  setup(): void;
76
- /** Start the HTTP server, retrying up to 3 times on port conflict */
76
+ /** Start the HTTP server fails fast if port is in use */
77
77
  start(): void;
78
78
  /** HMR rebuild: swap Express handler without restarting the server */
79
79
  rebuild(): void;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Application
3
- } from "./chunk-4G2S7T4R.js";
3
+ } from "./chunk-OKB76LY2.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-OWLI3SBW.js";
4
- import "./chunk-4G2S7T4R.js";
3
+ } from "./chunk-D76WCWAW.js";
4
+ import "./chunk-OKB76LY2.js";
5
5
  import "./chunk-35NUARK7.js";
6
6
  import "./chunk-3NEDJA3J.js";
7
7
  import "./chunk-WCQVDF3K.js";
@@ -36,6 +36,7 @@ var DevToolsAdapter = class {
36
36
  container = null;
37
37
  adapterStatuses = {};
38
38
  stopErrorWatch = null;
39
+ peerAdapters = [];
39
40
  constructor(options = {}) {
40
41
  this.basePath = options.basePath ?? "/_debug";
41
42
  this.enabled = options.enabled ?? process.env.NODE_ENV !== "production";
@@ -45,6 +46,7 @@ var DevToolsAdapter = class {
45
46
  "NODE_ENV"
46
47
  ];
47
48
  this.errorRateThreshold = options.errorRateThreshold ?? 0.5;
49
+ this.peerAdapters = options.adapters ?? [];
48
50
  this.requestCount = ref(0);
49
51
  this.errorCount = ref(0);
50
52
  this.clientErrorCount = ref(0);
@@ -108,6 +110,7 @@ var DevToolsAdapter = class {
108
110
  });
109
111
  });
110
112
  router.get("/state", (_req, res) => {
113
+ const wsAdapter = this.peerAdapters.find((a) => a.name === "WsAdapter" && typeof a.getStats === "function");
111
114
  res.json({
112
115
  reactive: {
113
116
  requestCount: this.requestCount.value,
@@ -119,7 +122,24 @@ var DevToolsAdapter = class {
119
122
  },
120
123
  routes: this.routes.length,
121
124
  container: this.container?.getRegistrations().length ?? 0,
122
- routeLatency: this.routeLatency
125
+ routeLatency: this.routeLatency,
126
+ ...wsAdapter ? {
127
+ ws: wsAdapter.getStats()
128
+ } : {}
129
+ });
130
+ });
131
+ router.get("/ws", (_req, res) => {
132
+ const wsAdapter = this.peerAdapters.find((a) => a.name === "WsAdapter" && typeof a.getStats === "function");
133
+ if (!wsAdapter) {
134
+ res.json({
135
+ enabled: false,
136
+ message: "WsAdapter not found"
137
+ });
138
+ return;
139
+ }
140
+ res.json({
141
+ enabled: true,
142
+ ...wsAdapter.getStats()
123
143
  });
124
144
  });
125
145
  if (this.exposeConfig) {
@@ -170,6 +190,11 @@ var DevToolsAdapter = class {
170
190
  }
171
191
  ];
172
192
  }
193
+ beforeStart(_app, _container) {
194
+ if (!this.enabled) return;
195
+ this.routes = [];
196
+ this.adapterStatuses[this.name] = "running";
197
+ }
173
198
  onRouteMount(controllerClass, mountPath) {
174
199
  if (!this.enabled) return;
175
200
  const routes = Reflect.getMetadata(METADATA.ROUTES, controllerClass) ?? [];
@@ -188,10 +213,6 @@ var DevToolsAdapter = class {
188
213
  });
189
214
  }
190
215
  }
191
- beforeStart(_app, _container) {
192
- if (!this.enabled) return;
193
- this.adapterStatuses[this.name] = "running";
194
- }
195
216
  afterStart(_server, _container) {
196
217
  if (!this.enabled) return;
197
218
  log.info(`DevTools ready \u2014 ${this.routes.length} routes tracked, ${this.container?.getRegistrations().length ?? 0} DI bindings`);
@@ -205,4 +226,4 @@ var DevToolsAdapter = class {
205
226
  export {
206
227
  DevToolsAdapter
207
228
  };
208
- //# sourceMappingURL=chunk-DUQ7SN7N.js.map
229
+ //# sourceMappingURL=chunk-CNN7JFUB.js.map
@@ -0,0 +1 @@
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\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 beforeStart(_app: any, _container: Container): void {\n if (!this.enabled) return\n // Clear routes on rebuild/restart to prevent duplication\n this.routes = []\n this.adapterStatuses[this.name] = 'running'\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","beforeStart","_app","_container","onRouteMount","controllerClass","mountPath","Reflect","getMetadata","METADATA","ROUTES","classMiddleware","CLASS_MIDDLEWARES","methodMiddleware","METHOD_MIDDLEWARES","prototype","handlerName","push","toUpperCase","controller","map","m","afterStart","_server","shutdown"]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Application
3
- } from "./chunk-4G2S7T4R.js";
3
+ } from "./chunk-OKB76LY2.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-OWLI3SBW.js.map
60
+ //# sourceMappingURL=chunk-D76WCWAW.js.map
@@ -94,33 +94,27 @@ var Application = class {
94
94
  adapter.beforeStart?.(this.app, this.container);
95
95
  }
96
96
  }
97
- /** Start the HTTP server, retrying up to 3 times on port conflict */
97
+ /** Start the HTTP server fails fast if port is in use */
98
98
  start() {
99
99
  this.setup();
100
- const basePort = this.options.port ?? parseInt(process.env.PORT || "3000", 10);
101
- const maxRetries = 3;
102
- const tryListen = /* @__PURE__ */ __name((port, attempt) => {
103
- this.httpServer = http.createServer(this.app);
104
- this.httpServer.on("error", (err) => {
105
- if (err.code === "EADDRINUSE" && attempt < maxRetries) {
106
- const nextPort = port + 1;
107
- log.warn(`Port ${port} in use, trying ${nextPort}... (${attempt + 1}/${maxRetries})`);
108
- tryListen(nextPort, attempt + 1);
109
- } else {
110
- throw err;
111
- }
112
- });
113
- this.httpServer.listen(port, () => {
114
- if (port !== basePort) {
115
- log.warn(`Port ${basePort} was in use, using ${port} instead`);
116
- }
117
- log.info(`Server running on http://localhost:${port}`);
118
- for (const adapter of this.adapters) {
119
- adapter.afterStart?.(this.httpServer, this.container);
120
- }
121
- });
122
- }, "tryListen");
123
- tryListen(basePort, 0);
100
+ const port = this.options.port ?? parseInt(process.env.PORT || "3000", 10);
101
+ this.httpServer = http.createServer(this.app);
102
+ this.httpServer.on("error", (err) => {
103
+ if (err.code === "EADDRINUSE") {
104
+ log.error(`Port ${port} is already in use. Kill the existing process or use a different port:
105
+ PORT=${port + 1} kick dev
106
+ lsof -i :${port} # find what's using it
107
+ kill <PID> # stop it`);
108
+ process.exit(1);
109
+ }
110
+ throw err;
111
+ });
112
+ this.httpServer.listen(port, () => {
113
+ log.info(`Server running on http://localhost:${port}`);
114
+ for (const adapter of this.adapters) {
115
+ adapter.afterStart?.(this.httpServer, this.container);
116
+ }
117
+ });
124
118
  }
125
119
  /** HMR rebuild: swap Express handler without restarting the server */
126
120
  rebuild() {
@@ -193,4 +187,4 @@ var Application = class {
193
187
  export {
194
188
  Application
195
189
  };
196
- //# sourceMappingURL=chunk-4G2S7T4R.js.map
190
+ //# sourceMappingURL=chunk-OKB76LY2.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} 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"]}
@@ -20,6 +20,8 @@ interface DevToolsOptions {
20
20
  onErrorRateExceeded?: (rate: number) => void;
21
21
  /** Error rate threshold (default: 0.5 = 50%) */
22
22
  errorRateThreshold?: number;
23
+ /** Other adapters to discover stats from (e.g., WsAdapter) */
24
+ adapters?: any[];
23
25
  }
24
26
  /**
25
27
  * DevToolsAdapter — Vue-style reactive introspection for KickJS applications.
@@ -73,11 +75,12 @@ declare class DevToolsAdapter implements AppAdapter {
73
75
  private container;
74
76
  private adapterStatuses;
75
77
  private stopErrorWatch;
78
+ private peerAdapters;
76
79
  constructor(options?: DevToolsOptions);
77
80
  beforeMount(app: any, container: Container): void;
78
81
  middleware(): AdapterMiddleware[];
79
- onRouteMount(controllerClass: any, mountPath: string): void;
80
82
  beforeStart(_app: any, _container: Container): void;
83
+ onRouteMount(controllerClass: any, mountPath: string): void;
81
84
  afterStart(_server: any, _container: Container): void;
82
85
  shutdown(): void;
83
86
  }
package/dist/devtools.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  DevToolsAdapter
3
- } from "./chunk-DUQ7SN7N.js";
3
+ } from "./chunk-CNN7JFUB.js";
4
4
  import "./chunk-WCQVDF3K.js";
5
5
  export {
6
6
  DevToolsAdapter
package/dist/index.js CHANGED
@@ -6,17 +6,17 @@ import {
6
6
  } from "./chunk-NQJNMKW5.js";
7
7
  import {
8
8
  bootstrap
9
- } from "./chunk-OWLI3SBW.js";
9
+ } from "./chunk-D76WCWAW.js";
10
10
  import {
11
11
  Application
12
- } from "./chunk-4G2S7T4R.js";
12
+ } from "./chunk-OKB76LY2.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-DUQ7SN7N.js";
19
+ } from "./chunk-CNN7JFUB.js";
20
20
  import {
21
21
  buildRoutes,
22
22
  getControllerPath
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forinda/kickjs-http",
3
- "version": "0.4.3",
3
+ "version": "0.5.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.4.3"
101
+ "@forinda/kickjs-core": "0.5.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, retrying up to 3 times on port conflict */\n start(): void {\n this.setup()\n\n const basePort = this.options.port ?? parseInt(process.env.PORT || '3000', 10)\n const maxRetries = 3\n\n const tryListen = (port: number, attempt: number) => {\n this.httpServer = http.createServer(this.app)\n\n this.httpServer.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE' && attempt < maxRetries) {\n const nextPort = port + 1\n log.warn(`Port ${port} in use, trying ${nextPort}... (${attempt + 1}/${maxRetries})`)\n tryListen(nextPort, attempt + 1)\n } else {\n throw err\n }\n })\n\n this.httpServer.listen(port, () => {\n if (port !== basePort) {\n log.warn(`Port ${basePort} was in use, using ${port} instead`)\n }\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 tryListen(basePort, 0)\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","basePort","port","parseInt","process","env","PORT","maxRetries","tryListen","attempt","http","createServer","on","err","code","nextPort","warn","listen","afterStart","rebuild","reset","removeAllListeners","shutdown","results","Promise","allSettled","resolve","status","error","reason","close","getExpressApp","getHttpServer","entries","phase","push","handler"]}
@@ -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}\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\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\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\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 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 })\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 beforeStart(_app: any, _container: Container): void {\n if (!this.enabled) return\n this.adapterStatuses[this.name] = 'running'\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","options","process","env","NODE_ENV","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","adapters","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","beforeStart","_app","_container","afterStart","_server","shutdown"]}