immortal-js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +577 -0
  3. package/docs/CHANGELOG.md +21 -0
  4. package/docs/CONTRIBUTING.md +41 -0
  5. package/docs/api/chaos.md +179 -0
  6. package/docs/api/dashboard.md +109 -0
  7. package/docs/api/isolation.md +191 -0
  8. package/docs/api/lifecycle.md +187 -0
  9. package/docs/api/monitoring.md +313 -0
  10. package/docs/api/plugins.md +217 -0
  11. package/docs/api/recovery.md +267 -0
  12. package/docs/api/safe-zone.md +236 -0
  13. package/docs/api/supervision.md +285 -0
  14. package/docs/guides/configuration.md +168 -0
  15. package/docs/guides/express.md +171 -0
  16. package/docs/guides/fastify.md +188 -0
  17. package/docs/guides/koa.md +102 -0
  18. package/docs/guides/nestjs.md +182 -0
  19. package/docs/guides/testing.md +91 -0
  20. package/examples/express-basic/index.ts +462 -0
  21. package/examples/express-basic/package.json +21 -0
  22. package/examples/express-basic/tsconfig.json +24 -0
  23. package/examples/fastify-microservice/index.ts +342 -0
  24. package/examples/fastify-microservice/package.json +22 -0
  25. package/examples/fastify-microservice/tsconfig.json +24 -0
  26. package/examples/invoice-service/data/invoices.db +0 -0
  27. package/examples/invoice-service/data/invoices.db-shm +0 -0
  28. package/examples/invoice-service/data/invoices.db-wal +0 -0
  29. package/examples/invoice-service/package.json +25 -0
  30. package/examples/invoice-service/public/index.html +5025 -0
  31. package/examples/invoice-service/src/db.ts +608 -0
  32. package/examples/invoice-service/src/pdf.ts +358 -0
  33. package/examples/invoice-service/src/server.ts +527 -0
  34. package/examples/invoice-service/src/store.ts +159 -0
  35. package/examples/invoice-service/src/types.ts +193 -0
  36. package/examples/invoice-service/tsconfig.json +23 -0
  37. package/examples/nestjs-enterprise/app.module.ts +561 -0
  38. package/examples/nestjs-enterprise/main.ts +67 -0
  39. package/examples/nestjs-enterprise/package.json +26 -0
  40. package/examples/nestjs-enterprise/tsconfig.json +27 -0
  41. package/immortal-js-1.0.0.tgz +0 -0
  42. package/package.json +33 -0
  43. package/packages/adapter-express/package.json +34 -0
  44. package/packages/adapter-express/src/index.ts +349 -0
  45. package/packages/adapter-express/tsconfig.json +14 -0
  46. package/packages/adapter-fastify/package.json +56 -0
  47. package/packages/adapter-fastify/src/plugin.ts +226 -0
  48. package/packages/adapter-fastify/tsconfig.json +14 -0
  49. package/packages/adapter-koa/package.json +55 -0
  50. package/packages/adapter-koa/src/index.ts +207 -0
  51. package/packages/adapter-koa/tsconfig.json +14 -0
  52. package/packages/adapter-nestjs/package.json +61 -0
  53. package/packages/adapter-nestjs/src/immortal.module.ts +313 -0
  54. package/packages/adapter-nestjs/src/index.ts +14 -0
  55. package/packages/adapter-nestjs/tsconfig.json +16 -0
  56. package/packages/core/package.json +56 -0
  57. package/packages/core/src/chaos/ChaosEngine.ts +249 -0
  58. package/packages/core/src/config/defaults.ts +200 -0
  59. package/packages/core/src/config/schema.ts +199 -0
  60. package/packages/core/src/event-bus.ts +168 -0
  61. package/packages/core/src/index.ts +164 -0
  62. package/packages/core/src/isolation/BulkheadPool.ts +279 -0
  63. package/packages/core/src/isolation/WorkerSandbox.ts +306 -0
  64. package/packages/core/src/isolation/index.ts +8 -0
  65. package/packages/core/src/lifecycle/GracefulShutdown.ts +161 -0
  66. package/packages/core/src/logger.ts +104 -0
  67. package/packages/core/src/monitoring/DiagnosticsChannel.ts +248 -0
  68. package/packages/core/src/monitoring/HealthMonitor.ts +191 -0
  69. package/packages/core/src/monitoring/MemoryLeakGuard.ts +340 -0
  70. package/packages/core/src/monitoring/MetricsCollector.ts +219 -0
  71. package/packages/core/src/monitoring/index.ts +10 -0
  72. package/packages/core/src/plugins/BuiltinPlugins.ts +269 -0
  73. package/packages/core/src/recovery/CircuitBreaker.ts +334 -0
  74. package/packages/core/src/recovery/FallbackCache.ts +328 -0
  75. package/packages/core/src/recovery/RetryEngine.ts +225 -0
  76. package/packages/core/src/recovery/Timeout.ts +97 -0
  77. package/packages/core/src/recovery/index.ts +11 -0
  78. package/packages/core/src/runtime.ts +242 -0
  79. package/packages/core/src/safe-zone/AsyncBoundary.ts +114 -0
  80. package/packages/core/src/safe-zone/ErrorTrap.ts +347 -0
  81. package/packages/core/src/safe-zone/SafeWrapper.ts +317 -0
  82. package/packages/core/src/safe-zone/index.ts +23 -0
  83. package/packages/core/src/supervision/ClusterManager.ts +243 -0
  84. package/packages/core/src/supervision/RestartStrategy.ts +68 -0
  85. package/packages/core/src/supervision/Supervisor.ts +311 -0
  86. package/packages/core/src/supervision/index.ts +11 -0
  87. package/packages/core/src/types.ts +470 -0
  88. package/packages/core/test/bulkhead.test.ts +310 -0
  89. package/packages/core/test/circuit-breaker.test.ts +153 -0
  90. package/packages/core/test/memory-guard.test.ts +213 -0
  91. package/packages/core/test/retry.test.ts +110 -0
  92. package/packages/core/test/safe-zone.test.ts +271 -0
  93. package/packages/core/test/supervisor.test.ts +310 -0
  94. package/packages/core/tsconfig.json +13 -0
  95. package/packages/dashboard/package.json +56 -0
  96. package/packages/dashboard/server/DashboardServer.ts +454 -0
  97. package/packages/dashboard/tsconfig.json +14 -0
  98. package/tsconfig.json +25 -0
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "./dist",
6
+ "declarationDir": "./dist",
7
+ "composite": true,
8
+ "incremental": true,
9
+ "tsBuildInfoFile": "./dist/.tsbuildinfo"
10
+ },
11
+ "include": ["src/**/*"],
12
+ "exclude": ["node_modules", "dist", "test"]
13
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@immortal/dashboard",
3
+ "version": "1.0.0",
4
+ "description": "Immortal.js Live Dashboard — real-time metrics, SSE stream, WebSocket, and Prometheus endpoint",
5
+ "author": "Immortal.js Team",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/immortal-js/immortal#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/immortal-js/immortal.git",
11
+ "directory": "packages/dashboard"
12
+ },
13
+ "keywords": [
14
+ "dashboard",
15
+ "metrics",
16
+ "prometheus",
17
+ "sse",
18
+ "websocket",
19
+ "observability",
20
+ "immortal"
21
+ ],
22
+ "type": "module",
23
+ "main": "./dist/server/DashboardServer.js",
24
+ "types": "./dist/server/DashboardServer.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "import": "./dist/server/DashboardServer.js",
28
+ "types": "./dist/server/DashboardServer.d.ts"
29
+ },
30
+ "./server": {
31
+ "import": "./dist/server/DashboardServer.js",
32
+ "types": "./dist/server/DashboardServer.d.ts"
33
+ }
34
+ },
35
+ "scripts": {
36
+ "build": "tsc --project tsconfig.json",
37
+ "dev": "tsc --project tsconfig.json --watch",
38
+ "clean": "rm -rf dist",
39
+ "typecheck": "tsc --noEmit"
40
+ },
41
+ "dependencies": {
42
+ "@immortal/core": "*"
43
+ },
44
+ "optionalDependencies": {
45
+ "ws": "^8.17.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^20.0.0",
49
+ "@types/ws": "^8.5.10",
50
+ "typescript": "^5.4.0",
51
+ "ws": "^8.17.0"
52
+ },
53
+ "engines": {
54
+ "node": ">=18.0.0"
55
+ }
56
+ }
@@ -0,0 +1,454 @@
1
+ /**
2
+ * @file DashboardServer.ts
3
+ * @description Immortal.js Live Dashboard Server
4
+ *
5
+ * Serves:
6
+ * 1. WebSocket feed — real-time metrics stream (1s intervals)
7
+ * 2. SSE feed — Server-Sent Events for browser clients
8
+ * 3. REST API — historical data queries
9
+ * 4. Static UI — React dashboard (served from dist/)
10
+ * 5. Prometheus metrics endpoint
11
+ *
12
+ * The dashboard is intentionally a separate process/port from the application
13
+ * so that dashboard availability doesn't affect application performance.
14
+ */
15
+
16
+ import http from "node:http";
17
+ import { EventEmitter } from "node:events";
18
+ import path from "node:path";
19
+ import type { SystemMetrics, ImmortalEvent, DashboardConfig } from "@immortal/core";
20
+ import type { ImmortalEventBus } from "@immortal/core";
21
+ import type { MetricsCollector } from "@immortal/core";
22
+ import type { DiagnosticsChannel } from "@immortal/core";
23
+ import { getLogger } from "@immortal/core";
24
+
25
+ // ─────────────────────────────────────────────────────────────────────────────
26
+ // DASHBOARD SERVER CLASS
27
+ // ─────────────────────────────────────────────────────────────────────────────
28
+
29
+ export class DashboardServer extends EventEmitter {
30
+ private server?: http.Server;
31
+ private readonly config: Required<DashboardConfig>;
32
+ private readonly bus: ImmortalEventBus;
33
+ private readonly metricsCollector: MetricsCollector;
34
+ private readonly diagnosticsChannel: DiagnosticsChannel;
35
+ private sseClients = new Set<http.ServerResponse>();
36
+ private wsClients = new Set<unknown>(); // ws.WebSocket
37
+ private metricsInterval?: ReturnType<typeof setInterval>;
38
+ private readonly eventHistory: ImmortalEvent[] = [];
39
+ private readonly maxHistorySize = 500;
40
+
41
+ constructor(
42
+ config: Required<DashboardConfig>,
43
+ bus: ImmortalEventBus,
44
+ metricsCollector: MetricsCollector,
45
+ diagnosticsChannel: DiagnosticsChannel
46
+ ) {
47
+ super();
48
+ this.config = config;
49
+ this.bus = bus;
50
+ this.metricsCollector = metricsCollector;
51
+ this.diagnosticsChannel = diagnosticsChannel;
52
+
53
+ // Collect event history for the "Timeline" view
54
+ this.bus.onAny((event: ImmortalEvent) => {
55
+ this.eventHistory.push(event);
56
+ if (this.eventHistory.length > this.maxHistorySize) {
57
+ this.eventHistory.shift();
58
+ }
59
+ this.broadcastEvent(event);
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Start the dashboard HTTP server
65
+ */
66
+ async start(): Promise<void> {
67
+ const logger = getLogger();
68
+
69
+ this.server = http.createServer((req, res) => {
70
+ this.handleRequest(req, res);
71
+ });
72
+
73
+ // Optional WebSocket upgrade (using ws library if available)
74
+ if (this.config.enableWebSocket) {
75
+ this.server.on("upgrade", async (request, socket, head) => {
76
+ try {
77
+ const { WebSocketServer } = await import("ws");
78
+ const wss = new WebSocketServer({ noServer: true });
79
+ wss.handleUpgrade(request, socket as never, head, (ws) => {
80
+ this.wsClients.add(ws);
81
+ ws.on("close", () => this.wsClients.delete(ws));
82
+ // Send initial state
83
+ const metrics = this.metricsCollector.getSnapshot();
84
+ ws.send(JSON.stringify({ type: "metrics", data: metrics }));
85
+ });
86
+ } catch {
87
+ socket.destroy();
88
+ }
89
+ });
90
+ }
91
+
92
+ return new Promise((resolve, reject) => {
93
+ this.server!.listen(this.config.port, () => {
94
+ logger.info(`Immortal.js Dashboard running on port ${this.config.port}`, {
95
+ status: `http://localhost:${this.config.port}${this.config.path}`,
96
+ prometheus: `http://localhost:${this.config.port}${this.config.path}/metrics`,
97
+ health: `http://localhost:${this.config.port}${this.config.path}/health`,
98
+ });
99
+
100
+ // Start metrics broadcast interval
101
+ this.metricsInterval = setInterval(() => {
102
+ this.broadcastMetrics();
103
+ }, 1000).unref();
104
+
105
+ resolve();
106
+ });
107
+
108
+ this.server!.on("error", reject);
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Stop the dashboard server
114
+ */
115
+ async stop(): Promise<void> {
116
+ if (this.metricsInterval) {
117
+ clearInterval(this.metricsInterval);
118
+ }
119
+
120
+ // Close all SSE connections
121
+ for (const client of this.sseClients) {
122
+ client.end();
123
+ }
124
+ this.sseClients.clear();
125
+
126
+ await new Promise<void>((resolve) => {
127
+ this.server?.close(() => resolve());
128
+ });
129
+
130
+ getLogger().info("Immortal.js Dashboard stopped");
131
+ }
132
+
133
+ // ─────────────────────────────────────────────────────────────────────────
134
+ // REQUEST HANDLING
135
+ // ─────────────────────────────────────────────────────────────────────────
136
+
137
+ private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
138
+ // CORS headers
139
+ res.setHeader("Access-Control-Allow-Origin", "*");
140
+ res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
141
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
142
+
143
+ if (req.method === "OPTIONS") {
144
+ res.writeHead(204);
145
+ res.end();
146
+ return;
147
+ }
148
+
149
+ // Basic auth check
150
+ if (this.config.auth) {
151
+ const authHeader = req.headers["authorization"];
152
+ if (!this.checkAuth(authHeader)) {
153
+ res.writeHead(401, { "WWW-Authenticate": 'Basic realm="Immortal Dashboard"' });
154
+ res.end("Unauthorized");
155
+ return;
156
+ }
157
+ }
158
+
159
+ const urlPath = req.url?.split("?")[0] ?? "/";
160
+ const dashPath = this.config.path;
161
+
162
+ // ── API Routes ─────────────────────────────────────────────────────────
163
+
164
+ if (urlPath === `${dashPath}/metrics`) {
165
+ this.diagnosticsChannel.updateMetrics(this.metricsCollector.getSnapshot());
166
+ res.writeHead(200, { "Content-Type": "text/plain; version=0.0.4" });
167
+ res.end(this.diagnosticsChannel.getPrometheusMetrics());
168
+ return;
169
+ }
170
+
171
+ if (urlPath === `${dashPath}/status`) {
172
+ this.sendJSON(res, this.metricsCollector.getSnapshot());
173
+ return;
174
+ }
175
+
176
+ if (urlPath === `${dashPath}/events`) {
177
+ this.sendJSON(res, {
178
+ events: this.eventHistory.slice(-100),
179
+ total: this.eventHistory.length,
180
+ });
181
+ return;
182
+ }
183
+
184
+ if (urlPath === `${dashPath}/health`) {
185
+ const metrics = this.metricsCollector.getSnapshot();
186
+ const isHealthy = metrics.eventLoopLag.p99 < 500 && metrics.memory.heapUsedMb < 1500;
187
+ res.writeHead(isHealthy ? 200 : 503, { "Content-Type": "application/json" });
188
+ res.end(JSON.stringify({
189
+ status: isHealthy ? "healthy" : "degraded",
190
+ timestamp: new Date().toISOString(),
191
+ metrics: {
192
+ eventLoopLag: metrics.eventLoopLag,
193
+ memory: { heapUsedMb: metrics.memory.heapUsedMb },
194
+ requests: metrics.requests,
195
+ },
196
+ }));
197
+ return;
198
+ }
199
+
200
+ // ── SSE Stream ─────────────────────────────────────────────────────────
201
+ if (urlPath === `${dashPath}/stream` && this.config.enableSSE) {
202
+ this.handleSSE(req, res);
203
+ return;
204
+ }
205
+
206
+ // ── Static UI ──────────────────────────────────────────────────────────
207
+ if (urlPath === dashPath || urlPath === `${dashPath}/`) {
208
+ this.serveUI(res);
209
+ return;
210
+ }
211
+
212
+ res.writeHead(404, { "Content-Type": "application/json" });
213
+ res.end(JSON.stringify({ error: "Not Found", path: urlPath }));
214
+ }
215
+
216
+ private handleSSE(req: http.IncomingMessage, res: http.ServerResponse): void {
217
+ res.writeHead(200, {
218
+ "Content-Type": "text/event-stream",
219
+ "Cache-Control": "no-cache",
220
+ "Connection": "keep-alive",
221
+ "X-Accel-Buffering": "no", // Nginx SSE fix
222
+ });
223
+
224
+ // Send initial state
225
+ const metrics = this.metricsCollector.getSnapshot();
226
+ res.write(`data: ${JSON.stringify({ type: "metrics", data: metrics })}\n\n`);
227
+
228
+ this.sseClients.add(res);
229
+
230
+ req.on("close", () => {
231
+ this.sseClients.delete(res);
232
+ });
233
+
234
+ // Keepalive ping every 30s
235
+ const keepalive = setInterval(() => {
236
+ res.write(": keepalive\n\n");
237
+ }, 30_000);
238
+
239
+ req.on("close", () => clearInterval(keepalive));
240
+ }
241
+
242
+ private serveUI(res: http.ServerResponse): void {
243
+ // Serve embedded minimal dashboard HTML if React build not available
244
+ const html = this.generateDashboardHTML();
245
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
246
+ res.end(html);
247
+ }
248
+
249
+ private broadcastMetrics(): void {
250
+ const metrics = this.metricsCollector.getSnapshot();
251
+ const message = JSON.stringify({ type: "metrics", data: metrics });
252
+
253
+ // SSE broadcast
254
+ for (const client of this.sseClients) {
255
+ try {
256
+ client.write(`data: ${message}\n\n`);
257
+ } catch {
258
+ this.sseClients.delete(client);
259
+ }
260
+ }
261
+
262
+ // WebSocket broadcast
263
+ for (const ws of this.wsClients) {
264
+ try {
265
+ (ws as { send: (data: string) => void }).send(message);
266
+ } catch {
267
+ this.wsClients.delete(ws);
268
+ }
269
+ }
270
+ }
271
+
272
+ private broadcastEvent(event: ImmortalEvent): void {
273
+ const message = JSON.stringify({ type: "event", data: event });
274
+
275
+ for (const client of this.sseClients) {
276
+ try {
277
+ client.write(`data: ${message}\n\n`);
278
+ } catch {
279
+ this.sseClients.delete(client);
280
+ }
281
+ }
282
+ }
283
+
284
+ private sendJSON(res: http.ServerResponse, data: unknown): void {
285
+ const body = JSON.stringify(data, null, 2);
286
+ res.writeHead(200, { "Content-Type": "application/json" });
287
+ res.end(body);
288
+ }
289
+
290
+ private checkAuth(authHeader: string | undefined): boolean {
291
+ if (!this.config.auth) return true;
292
+ if (!authHeader?.startsWith("Basic ")) return false;
293
+
294
+ const credentials = Buffer.from(authHeader.slice(6), "base64").toString();
295
+ const [username, password] = credentials.split(":");
296
+ return username === this.config.auth.username && password === this.config.auth.password;
297
+ }
298
+
299
+ private generateDashboardHTML(): string {
300
+ return `<!DOCTYPE html>
301
+ <html lang="en">
302
+ <head>
303
+ <meta charset="UTF-8">
304
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
305
+ <title>Immortal.js Dashboard</title>
306
+ <style>
307
+ * { box-sizing: border-box; margin: 0; padding: 0; }
308
+ body { font-family: 'SF Mono', 'Fira Code', monospace; background: #0d1117; color: #e6edf3; padding: 20px; }
309
+ h1 { font-size: 1.4rem; color: #58a6ff; margin-bottom: 20px; }
310
+ h1 span { color: #3fb950; }
311
+ .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; margin-bottom: 20px; }
312
+ .card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
313
+ .card h2 { font-size: 0.8rem; color: #8b949e; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 12px; }
314
+ .metric { display: flex; justify-content: space-between; align-items: center; margin: 6px 0; font-size: 0.85rem; }
315
+ .metric-name { color: #8b949e; }
316
+ .metric-value { color: #e6edf3; font-weight: bold; }
317
+ .ok { color: #3fb950; }
318
+ .warn { color: #d29922; }
319
+ .critical { color: #f85149; }
320
+ .circuit { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.75rem; }
321
+ .circuit.CLOSED { background: #1a3d1a; color: #3fb950; }
322
+ .circuit.OPEN { background: #3d1a1a; color: #f85149; }
323
+ .circuit.HALF_OPEN { background: #3d2d0a; color: #d29922; }
324
+ .status { font-size: 0.7rem; color: #8b949e; margin-top: 20px; }
325
+ .tagline { color: #8b949e; font-size: 0.85rem; font-style: italic; margin-bottom: 20px; }
326
+ #connection { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #f85149; margin-right: 6px; }
327
+ #connection.connected { background: #3fb950; }
328
+ </style>
329
+ </head>
330
+ <body>
331
+ <h1>☽ Immortal<span>.js</span> Dashboard</h1>
332
+ <p class="tagline">"We don't promise you won't fall — we guarantee you stand back up before anyone notices."</p>
333
+ <div id="connection-status">
334
+ <span id="connection"></span><span id="conn-text">Connecting...</span>
335
+ </div>
336
+ <div class="grid" id="metrics-grid">
337
+ <div class="card"><h2>Loading...</h2></div>
338
+ </div>
339
+ <div class="status">
340
+ Auto-refreshing via SSE · <a href="${this.config.path}/metrics" style="color:#58a6ff">Prometheus</a> ·
341
+ <a href="${this.config.path}/status" style="color:#58a6ff">JSON API</a> ·
342
+ <a href="${this.config.path}/events" style="color:#58a6ff">Events</a>
343
+ </div>
344
+
345
+ <script>
346
+ const dashPath = '${this.config.path}';
347
+
348
+ function classify(val, warn, critical) {
349
+ if (val >= critical) return 'critical';
350
+ if (val >= warn) return 'warn';
351
+ return 'ok';
352
+ }
353
+
354
+ function renderMetrics(metrics) {
355
+ const m = metrics;
356
+ const grid = document.getElementById('metrics-grid');
357
+
358
+ grid.innerHTML = \`
359
+ <div class="card">
360
+ <h2>Event Loop</h2>
361
+ <div class="metric"><span class="metric-name">p50</span><span class="metric-value \${classify(m.eventLoopLag.p50, 50, 200)}">\${m.eventLoopLag.p50.toFixed(1)}ms</span></div>
362
+ <div class="metric"><span class="metric-name">p95</span><span class="metric-value \${classify(m.eventLoopLag.p95, 100, 300)}">\${m.eventLoopLag.p95.toFixed(1)}ms</span></div>
363
+ <div class="metric"><span class="metric-name">p99</span><span class="metric-value \${classify(m.eventLoopLag.p99, 100, 500)}">\${m.eventLoopLag.p99.toFixed(1)}ms</span></div>
364
+ <div class="metric"><span class="metric-name">max</span><span class="metric-value">\${m.eventLoopLag.max.toFixed(1)}ms</span></div>
365
+ </div>
366
+
367
+ <div class="card">
368
+ <h2>Memory</h2>
369
+ <div class="metric"><span class="metric-name">Heap Used</span><span class="metric-value \${classify(m.memory.heapUsedMb, 512, 1024)}">\${m.memory.heapUsedMb.toFixed(1)} MB</span></div>
370
+ <div class="metric"><span class="metric-name">RSS</span><span class="metric-value">\${m.memory.rssMb.toFixed(1)} MB</span></div>
371
+ <div class="metric"><span class="metric-name">Growth Rate</span><span class="metric-value \${m.memory.heapGrowthRate > 20 ? 'warn' : 'ok'}">\${m.memory.heapGrowthRate.toFixed(2)} MB/min</span></div>
372
+ <div class="metric"><span class="metric-name">GC/min</span><span class="metric-value">\${m.memory.gcFrequency.toFixed(1)}</span></div>
373
+ </div>
374
+
375
+ <div class="card">
376
+ <h2>CPU</h2>
377
+ <div class="metric"><span class="metric-name">User</span><span class="metric-value \${classify(m.cpu.percentUser, 60, 90)}">\${m.cpu.percentUser.toFixed(1)}%</span></div>
378
+ <div class="metric"><span class="metric-name">System</span><span class="metric-value">\${m.cpu.percentSystem.toFixed(1)}%</span></div>
379
+ <div class="metric"><span class="metric-name">Active Handles</span><span class="metric-value">\${m.handles.active}</span></div>
380
+ </div>
381
+
382
+ <div class="card">
383
+ <h2>Requests</h2>
384
+ <div class="metric"><span class="metric-name">Total</span><span class="metric-value">\${m.requests.total.toLocaleString()}</span></div>
385
+ <div class="metric"><span class="metric-name">Active</span><span class="metric-value">\${m.requests.active}</span></div>
386
+ <div class="metric"><span class="metric-name">Rate</span><span class="metric-value">\${m.requests.perSecond.toFixed(1)} req/s</span></div>
387
+ <div class="metric"><span class="metric-name">Error Rate</span><span class="metric-value \${classify(m.requests.errorRate * 100, 5, 20)}">\${(m.requests.errorRate * 100).toFixed(2)}%</span></div>
388
+ </div>
389
+
390
+ \${Object.keys(m.circuits).length > 0 ? \`
391
+ <div class="card">
392
+ <h2>Circuit Breakers</h2>
393
+ \${Object.entries(m.circuits).map(([name, c]) => \`
394
+ <div class="metric">
395
+ <span class="metric-name">\${name}</span>
396
+ <span class="circuit \${c.state}">\${c.state}</span>
397
+ </div>
398
+ <div class="metric" style="font-size:0.75rem">
399
+ <span class="metric-name">Failure Rate</span>
400
+ <span class="metric-value">\${c.failureRate.toFixed(1)}%</span>
401
+ </div>
402
+ \`).join('')}
403
+ </div>\` : ''}
404
+
405
+ \${Object.keys(m.bulkheads).length > 0 ? \`
406
+ <div class="card">
407
+ <h2>Bulkhead Pools</h2>
408
+ \${Object.entries(m.bulkheads).map(([name, b]) => \`
409
+ <div class="metric">
410
+ <span class="metric-name">\${name}</span>
411
+ <span class="metric-value">\${b.active}/\${b.maxConcurrent} (\${b.queued} queued)</span>
412
+ </div>
413
+ \`).join('')}
414
+ </div>\` : ''}
415
+
416
+ \${m.workers.length > 0 ? \`
417
+ <div class="card">
418
+ <h2>Workers</h2>
419
+ \${m.workers.map(w => \`
420
+ <div class="metric">
421
+ <span class="metric-name">Worker \${w.id}</span>
422
+ <span class="metric-value \${w.state === 'running' ? 'ok' : w.state === 'escalated' ? 'critical' : 'warn'}">\${w.state}</span>
423
+ </div>
424
+ \`).join('')}
425
+ </div>\` : ''}
426
+ \`;
427
+ }
428
+
429
+ // SSE connection
430
+ const evtSource = new EventSource(dashPath + '/stream');
431
+ const connEl = document.getElementById('connection');
432
+ const connText = document.getElementById('conn-text');
433
+
434
+ evtSource.onopen = () => {
435
+ connEl.className = 'connected';
436
+ connText.textContent = 'Live';
437
+ };
438
+
439
+ evtSource.onmessage = (e) => {
440
+ const msg = JSON.parse(e.data);
441
+ if (msg.type === 'metrics') {
442
+ renderMetrics(msg.data);
443
+ }
444
+ };
445
+
446
+ evtSource.onerror = () => {
447
+ connEl.className = '';
448
+ connText.textContent = 'Reconnecting...';
449
+ };
450
+ </script>
451
+ </body>
452
+ </html>`;
453
+ }
454
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./",
5
+ "outDir": "./dist",
6
+ "declarationDir": "./dist",
7
+ "composite": true,
8
+ "incremental": true,
9
+ "tsBuildInfoFile": "./dist/.tsbuildinfo"
10
+ },
11
+ "include": ["server/**/*"],
12
+ "exclude": ["node_modules", "dist"],
13
+ "references": [{ "path": "../core" }]
14
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "lib": ["ES2022"],
7
+ "strict": true,
8
+ "strictNullChecks": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "exactOptionalPropertyTypes": true,
11
+ "noImplicitOverride": true,
12
+ "noPropertyAccessFromIndexSignature": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "esModuleInterop": true,
17
+ "allowSyntheticDefaultImports": true,
18
+ "forceConsistentCasingInFileNames": true,
19
+ "skipLibCheck": true,
20
+ "composite": true,
21
+ "incremental": true,
22
+ "outDir": "dist"
23
+ },
24
+ "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
25
+ }