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,200 @@
1
+ /**
2
+ * @file defaults.ts
3
+ * @description Sensible production-ready defaults for all Immortal.js config options
4
+ * All defaults are conservative — they protect without being overly aggressive
5
+ */
6
+
7
+ import type {
8
+ ImmortalConfig,
9
+ RetryConfig,
10
+ CircuitBreakerConfig,
11
+ BulkheadConfig,
12
+ SupervisorConfig,
13
+ HealthMonitorConfig,
14
+ MemoryGuardConfig,
15
+ GracefulShutdownConfig,
16
+ DashboardConfig,
17
+ SafeZoneConfig,
18
+ LoggerConfig,
19
+ } from "../types.js";
20
+
21
+ export const DEFAULT_SAFE_ZONE: Required<SafeZoneConfig> = {
22
+ autoWrap: true,
23
+ defaultFallback: undefined as never,
24
+ exposeErrors: process.env["NODE_ENV"] !== "production",
25
+ sanitizeFields: ["password", "token", "secret", "authorization", "cookie", "creditCard", "ssn"],
26
+ };
27
+
28
+ export const DEFAULT_RETRY: Required<RetryConfig> = {
29
+ maxAttempts: 3,
30
+ baseDelayMs: 200,
31
+ maxDelayMs: 10_000,
32
+ jitterFactor: 0.3,
33
+ isRetryable: defaultIsRetryable,
34
+ retryBudget: 50, // max 50 retries per second across all operations
35
+ };
36
+
37
+ export const DEFAULT_CIRCUIT_BREAKER: Required<CircuitBreakerConfig> = {
38
+ threshold: 5,
39
+ cooldownMs: 15_000,
40
+ slidingWindowSize: 100,
41
+ minimumRequests: 10,
42
+ failureRateThreshold: 50, // 50% failure rate opens circuit
43
+ };
44
+
45
+ export const DEFAULT_BULKHEAD: Required<BulkheadConfig> = {
46
+ maxConcurrent: 10,
47
+ maxQueueSize: 50,
48
+ queueTimeoutMs: 30_000,
49
+ enablePriority: false,
50
+ };
51
+
52
+ export const DEFAULT_SUPERVISOR: Required<SupervisorConfig> = {
53
+ strategy: "one_for_one",
54
+ maxRestartsInWindow: 5,
55
+ windowMs: 60_000,
56
+ baseRestartDelayMs: 100,
57
+ clusterMode: false,
58
+ clusterWorkers: 1,
59
+ };
60
+
61
+ export const DEFAULT_HEALTH_MONITOR: Required<HealthMonitorConfig> = {
62
+ intervalMs: 5_000,
63
+ eventLoopLagMs: 100,
64
+ criticalLagMs: 500,
65
+ cpuWarningPercent: 80,
66
+ cpuCriticalPercent: 95,
67
+ };
68
+
69
+ export const DEFAULT_MEMORY_GUARD: Required<MemoryGuardConfig> = {
70
+ checkIntervalMs: 10_000,
71
+ warningThresholdMb: 1024,
72
+ restartThresholdMb: 1500,
73
+ growthWindowCount: 3,
74
+ enableObjectTracking: false,
75
+ adaptiveThresholds: true,
76
+ };
77
+
78
+ export const DEFAULT_GRACEFUL_SHUTDOWN: Required<GracefulShutdownConfig> = {
79
+ timeoutMs: 20_000,
80
+ signals: ["SIGTERM", "SIGINT"],
81
+ onShutdown: [],
82
+ };
83
+
84
+ export const DEFAULT_DASHBOARD: Required<DashboardConfig> = {
85
+ enabled: true,
86
+ port: 9999,
87
+ path: "/__immortal",
88
+ enableWebSocket: true,
89
+ enableSSE: true,
90
+ enableOTLP: false,
91
+ otlpEndpoint: "",
92
+ enablePrometheus: true,
93
+ auth: undefined as never,
94
+ };
95
+
96
+ export const DEFAULT_LOGGER: Required<LoggerConfig> = {
97
+ level: process.env["NODE_ENV"] === "production" ? "warn" : "info",
98
+ transport: undefined as never,
99
+ redact: ["password", "token", "secret", "authorization"],
100
+ };
101
+
102
+ export const IMMORTAL_DEFAULTS: Required<ImmortalConfig> = {
103
+ safeZone: DEFAULT_SAFE_ZONE,
104
+ retry: DEFAULT_RETRY,
105
+ circuitBreaker: DEFAULT_CIRCUIT_BREAKER,
106
+ bulkhead: { default: DEFAULT_BULKHEAD },
107
+ supervisor: DEFAULT_SUPERVISOR,
108
+ healthMonitor: DEFAULT_HEALTH_MONITOR,
109
+ memoryGuard: DEFAULT_MEMORY_GUARD,
110
+ gracefulShutdown: DEFAULT_GRACEFUL_SHUTDOWN,
111
+ dashboard: DEFAULT_DASHBOARD,
112
+ plugins: [],
113
+ chaos: { enabled: false },
114
+ logger: DEFAULT_LOGGER,
115
+ };
116
+
117
+ /**
118
+ * Default retryable error classifier
119
+ * Only retries on transient infrastructure errors, never on business logic errors
120
+ */
121
+ function defaultIsRetryable(error: unknown): boolean {
122
+ if (!(error instanceof Error)) return false;
123
+
124
+ const msg = error.message.toLowerCase();
125
+ const code = (error as NodeJS.ErrnoException).code ?? "";
126
+
127
+ // Network/connection errors — always retryable
128
+ const networkCodes = [
129
+ "ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT",
130
+ "EPIPE", "EHOSTUNREACH", "ENETUNREACH", "EAI_AGAIN",
131
+ ];
132
+ if (networkCodes.includes(code)) return true;
133
+
134
+ // HTTP status codes via error properties
135
+ const statusCode = (error as { statusCode?: number; status?: number }).statusCode
136
+ ?? (error as { statusCode?: number; status?: number }).status;
137
+
138
+ if (statusCode) {
139
+ // 429 Too Many Requests — retryable with backoff
140
+ if (statusCode === 429) return true;
141
+ // 503 Service Unavailable — retryable
142
+ if (statusCode === 503) return true;
143
+ // 504 Gateway Timeout — retryable
144
+ if (statusCode === 504) return true;
145
+ // 408 Request Timeout — retryable
146
+ if (statusCode === 408) return true;
147
+ // 4xx (except above) — NOT retryable (client errors)
148
+ if (statusCode >= 400 && statusCode < 500) return false;
149
+ // 500, 502 — retryable (server errors)
150
+ if (statusCode === 500 || statusCode === 502) return true;
151
+ // Other 5xx — retryable
152
+ if (statusCode >= 500) return true;
153
+ // 2xx, 3xx — not an error, don't retry
154
+ return false;
155
+ }
156
+
157
+ // Timeout errors by message pattern
158
+ if (msg.includes("timeout") || msg.includes("timed out")) return true;
159
+ if (msg.includes("connection reset") || msg.includes("connection refused")) return true;
160
+ if (msg.includes("socket hang up")) return true;
161
+
162
+ // Default: don't retry unknown errors
163
+ return false;
164
+ }
165
+
166
+ /**
167
+ * Deep merge config with defaults
168
+ */
169
+ export function mergeWithDefaults(userConfig: ImmortalConfig): Required<ImmortalConfig> {
170
+ return {
171
+ safeZone: { ...DEFAULT_SAFE_ZONE, ...userConfig.safeZone },
172
+ retry: { ...DEFAULT_RETRY, ...userConfig.retry },
173
+ circuitBreaker: { ...DEFAULT_CIRCUIT_BREAKER, ...userConfig.circuitBreaker },
174
+ bulkhead: {
175
+ default: DEFAULT_BULKHEAD,
176
+ ...Object.fromEntries(
177
+ Object.entries(userConfig.bulkhead ?? {}).map(([k, v]) => [
178
+ k,
179
+ { ...DEFAULT_BULKHEAD, ...v },
180
+ ])
181
+ ),
182
+ },
183
+ supervisor: { ...DEFAULT_SUPERVISOR, ...userConfig.supervisor },
184
+ healthMonitor: { ...DEFAULT_HEALTH_MONITOR, ...userConfig.healthMonitor },
185
+ memoryGuard: { ...DEFAULT_MEMORY_GUARD, ...userConfig.memoryGuard },
186
+ gracefulShutdown: {
187
+ ...DEFAULT_GRACEFUL_SHUTDOWN,
188
+ ...userConfig.gracefulShutdown,
189
+ signals: userConfig.gracefulShutdown?.signals ?? DEFAULT_GRACEFUL_SHUTDOWN.signals,
190
+ onShutdown: [
191
+ ...(DEFAULT_GRACEFUL_SHUTDOWN.onShutdown ?? []),
192
+ ...(userConfig.gracefulShutdown?.onShutdown ?? []),
193
+ ],
194
+ },
195
+ dashboard: { ...DEFAULT_DASHBOARD, ...userConfig.dashboard },
196
+ plugins: userConfig.plugins ?? [],
197
+ chaos: { enabled: false, ...userConfig.chaos },
198
+ logger: { ...DEFAULT_LOGGER, ...userConfig.logger },
199
+ };
200
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * @file schema.ts
3
+ * @description Zod validation schemas for all Immortal.js configuration objects
4
+ * Provides runtime validation with descriptive error messages
5
+ */
6
+
7
+ import { z } from "zod";
8
+
9
+ // ─────────────────────────────────────────────────────────────────────────────
10
+ // PRIMITIVE VALIDATORS
11
+ // ─────────────────────────────────────────────────────────────────────────────
12
+
13
+ const positiveInt = (name: string) =>
14
+ z.number().int().positive({ message: `${name} must be a positive integer` });
15
+
16
+ const positiveMs = (name: string) =>
17
+ z.number().positive({ message: `${name} must be a positive number (milliseconds)` });
18
+
19
+ const probability = (name: string) =>
20
+ z.number().min(0).max(1, { message: `${name} must be between 0 and 1` });
21
+
22
+ // ─────────────────────────────────────────────────────────────────────────────
23
+ // SECTION SCHEMAS
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+
26
+ export const SafeZoneConfigSchema = z.object({
27
+ autoWrap: z.boolean().optional(),
28
+ defaultFallback: z.function().optional(),
29
+ exposeErrors: z.boolean().optional(),
30
+ sanitizeFields: z.array(z.string()).optional(),
31
+ }).strict();
32
+
33
+ export const RetryConfigSchema = z.object({
34
+ maxAttempts: positiveInt("maxAttempts").max(20).optional(),
35
+ baseDelayMs: positiveMs("baseDelayMs").optional(),
36
+ maxDelayMs: positiveMs("maxDelayMs").optional(),
37
+ jitterFactor: probability("jitterFactor").optional(),
38
+ isRetryable: z.function().optional(),
39
+ retryBudget: positiveInt("retryBudget").optional(),
40
+ }).strict().refine(
41
+ (d) => !d.baseDelayMs || !d.maxDelayMs || d.maxDelayMs >= d.baseDelayMs,
42
+ { message: "maxDelayMs must be >= baseDelayMs", path: ["maxDelayMs"] }
43
+ );
44
+
45
+ export const CircuitBreakerConfigSchema = z.object({
46
+ threshold: positiveInt("threshold").optional(),
47
+ cooldownMs: positiveMs("cooldownMs").optional(),
48
+ slidingWindowSize: positiveInt("slidingWindowSize").min(10).optional(),
49
+ minimumRequests: positiveInt("minimumRequests").optional(),
50
+ failureRateThreshold: z.number().min(1).max(100).optional(),
51
+ }).strict();
52
+
53
+ export const BulkheadConfigSchema = z.object({
54
+ maxConcurrent: positiveInt("maxConcurrent").optional(),
55
+ maxQueueSize: positiveInt("maxQueueSize").optional(),
56
+ queueTimeoutMs: positiveMs("queueTimeoutMs").optional(),
57
+ enablePriority: z.boolean().optional(),
58
+ }).strict();
59
+
60
+ export const SupervisorConfigSchema = z.object({
61
+ strategy: z.enum(["one_for_one", "one_for_all", "rest_for_one"]).optional(),
62
+ maxRestartsInWindow: positiveInt("maxRestartsInWindow").optional(),
63
+ windowMs: positiveMs("windowMs").optional(),
64
+ baseRestartDelayMs: positiveMs("baseRestartDelayMs").optional(),
65
+ clusterMode: z.boolean().optional(),
66
+ clusterWorkers: positiveInt("clusterWorkers").max(128).optional(),
67
+ }).strict();
68
+
69
+ export const HealthMonitorConfigSchema = z.object({
70
+ intervalMs: positiveMs("intervalMs").min(100).optional(),
71
+ eventLoopLagMs: positiveMs("eventLoopLagMs").optional(),
72
+ criticalLagMs: positiveMs("criticalLagMs").optional(),
73
+ cpuWarningPercent: z.number().min(1).max(100).optional(),
74
+ cpuCriticalPercent: z.number().min(1).max(100).optional(),
75
+ }).strict().refine(
76
+ (d) => !d.eventLoopLagMs || !d.criticalLagMs || d.criticalLagMs > d.eventLoopLagMs,
77
+ { message: "criticalLagMs must be > eventLoopLagMs", path: ["criticalLagMs"] }
78
+ );
79
+
80
+ export const MemoryGuardConfigSchema = z.object({
81
+ checkIntervalMs: positiveMs("checkIntervalMs").min(1000).optional(),
82
+ warningThresholdMb: positiveInt("warningThresholdMb").optional(),
83
+ restartThresholdMb: positiveInt("restartThresholdMb").optional(),
84
+ growthWindowCount: positiveInt("growthWindowCount").min(2).max(20).optional(),
85
+ enableObjectTracking: z.boolean().optional(),
86
+ adaptiveThresholds: z.boolean().optional(),
87
+ }).strict().refine(
88
+ (d) => !d.warningThresholdMb || !d.restartThresholdMb || d.restartThresholdMb > d.warningThresholdMb,
89
+ { message: "restartThresholdMb must be > warningThresholdMb", path: ["restartThresholdMb"] }
90
+ );
91
+
92
+ export const GracefulShutdownConfigSchema = z.object({
93
+ timeoutMs: positiveMs("timeoutMs").max(120_000).optional(),
94
+ signals: z.array(z.string()).optional(),
95
+ onShutdown: z.array(z.function()).optional(),
96
+ }).strict();
97
+
98
+ export const DashboardConfigSchema = z.object({
99
+ enabled: z.boolean().optional(),
100
+ port: z.number().int().min(1024).max(65535).optional(),
101
+ path: z.string().startsWith("/").optional(),
102
+ enableWebSocket: z.boolean().optional(),
103
+ enableSSE: z.boolean().optional(),
104
+ enableOTLP: z.boolean().optional(),
105
+ otlpEndpoint: z.string().url().optional().or(z.literal("")),
106
+ enablePrometheus: z.boolean().optional(),
107
+ auth: z.object({ username: z.string().min(1), password: z.string().min(8) }).optional(),
108
+ }).strict();
109
+
110
+ export const ChaosConfigSchema = z.object({
111
+ enabled: z.boolean().optional(),
112
+ latencyProbability: probability("latencyProbability").optional(),
113
+ latencyRange: z.tuple([z.number().min(0), z.number().max(60_000)]).optional(),
114
+ errorProbability: probability("errorProbability").optional(),
115
+ workerKillProbability: probability("workerKillProbability").optional(),
116
+ memoryPressureMb: positiveInt("memoryPressureMb").max(2048).optional(),
117
+ targetRoutes: z.array(z.string()).optional(),
118
+ }).strict();
119
+
120
+ export const LoggerConfigSchema = z.object({
121
+ level: z.enum(["debug", "info", "warn", "error", "silent"]).optional(),
122
+ transport: z.any().optional(),
123
+ redact: z.array(z.string()).optional(),
124
+ }).strict();
125
+
126
+ export const ImmortalConfigSchema = z.object({
127
+ safeZone: SafeZoneConfigSchema.optional(),
128
+ retry: RetryConfigSchema.optional(),
129
+ circuitBreaker: CircuitBreakerConfigSchema.optional(),
130
+ bulkhead: z.record(z.string(), BulkheadConfigSchema).optional(),
131
+ supervisor: SupervisorConfigSchema.optional(),
132
+ healthMonitor: HealthMonitorConfigSchema.optional(),
133
+ memoryGuard: MemoryGuardConfigSchema.optional(),
134
+ gracefulShutdown: GracefulShutdownConfigSchema.optional(),
135
+ dashboard: DashboardConfigSchema.optional(),
136
+ plugins: z.array(z.any()).optional(),
137
+ chaos: ChaosConfigSchema.optional(),
138
+ logger: LoggerConfigSchema.optional(),
139
+ }).strict();
140
+
141
+ // ─────────────────────────────────────────────────────────────────────────────
142
+ // VALIDATION FUNCTION
143
+ // ─────────────────────────────────────────────────────────────────────────────
144
+
145
+ export interface ValidationResult {
146
+ valid: boolean;
147
+ errors: string[];
148
+ warnings: string[];
149
+ }
150
+
151
+ export function validateConfig(config: unknown): ValidationResult {
152
+ const result = ImmortalConfigSchema.safeParse(config);
153
+ const warnings: string[] = [];
154
+
155
+ if (!result.success) {
156
+ return {
157
+ valid: false,
158
+ errors: result.error.issues.map(
159
+ (i) => `[${i.path.join(".")}] ${i.message}`
160
+ ),
161
+ warnings,
162
+ };
163
+ }
164
+
165
+ // Additional semantic warnings
166
+ const cfg = result.data;
167
+
168
+ if (cfg.chaos?.enabled && process.env["NODE_ENV"] === "production") {
169
+ warnings.push(
170
+ "⚠️ Chaos mode is enabled in production! This will cause intentional failures."
171
+ );
172
+ }
173
+
174
+ if (cfg.safeZone?.exposeErrors && process.env["NODE_ENV"] === "production") {
175
+ warnings.push(
176
+ "⚠️ exposeErrors is true in production — internal error details will be exposed to clients."
177
+ );
178
+ }
179
+
180
+ if (cfg.retry?.maxAttempts && cfg.retry.maxAttempts > 10) {
181
+ warnings.push(
182
+ `⚠️ retry.maxAttempts=${cfg.retry.maxAttempts} is very high — consider using retryBudget instead.`
183
+ );
184
+ }
185
+
186
+ if (cfg.supervisor?.clusterMode && cfg.supervisor?.clusterWorkers === 1) {
187
+ warnings.push(
188
+ "ℹ️ clusterMode=true with clusterWorkers=1 has no redundancy benefit."
189
+ );
190
+ }
191
+
192
+ if (cfg.dashboard?.enabled && !cfg.dashboard?.auth && process.env["NODE_ENV"] === "production") {
193
+ warnings.push(
194
+ "⚠️ Dashboard is enabled without authentication in production — expose with caution."
195
+ );
196
+ }
197
+
198
+ return { valid: true, errors: [], warnings };
199
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * @file event-bus.ts
3
+ * @description Immortal.js internal event bus
4
+ * Central nervous system connecting all layers (Monitoring → Supervisor, etc.)
5
+ * Uses Node.js EventEmitter extended with typed events
6
+ */
7
+
8
+ import { EventEmitter } from "node:events";
9
+ import type { ImmortalEvent, ImmortalEventType } from "./types.js";
10
+ import { getLogger } from "./logger.js";
11
+
12
+ type EventListener = (event: ImmortalEvent) => void;
13
+
14
+ export class ImmortalEventBus extends EventEmitter {
15
+ private history: ImmortalEvent[] = [];
16
+ private readonly maxHistory: number;
17
+
18
+ constructor(maxHistory = 1000) {
19
+ super();
20
+ this.maxHistory = maxHistory;
21
+ this.setMaxListeners(100); // Many layers listen to events
22
+ }
23
+
24
+ /**
25
+ * Emit a typed Immortal event — logs it, stores in history, and notifies listeners
26
+ */
27
+ override emit(eventType: ImmortalEventType, data?: Omit<ImmortalEvent, "type" | "timestamp">): boolean;
28
+ override emit(event: string | symbol, ...args: unknown[]): boolean;
29
+ override emit(eventOrType: string | symbol | ImmortalEventType, ...args: unknown[]): boolean {
30
+ // Handle typed Immortal events
31
+ if (typeof eventOrType === "string" && isImmortalEventType(eventOrType)) {
32
+ const [data] = args as [Omit<ImmortalEvent, "type" | "timestamp"> | undefined];
33
+ const event: ImmortalEvent = {
34
+ type: eventOrType,
35
+ timestamp: Date.now(),
36
+ ...data,
37
+ };
38
+
39
+ // Store in history (circular buffer)
40
+ this.history.push(event);
41
+ if (this.history.length > this.maxHistory) {
42
+ this.history.shift();
43
+ }
44
+
45
+ // Log significant events
46
+ const logger = getLogger();
47
+ if (event.type.includes("critical") || event.type.includes("escalated") || event.type.includes("crash")) {
48
+ logger.error(`Event: ${event.type}`, event.data as Record<string, unknown>);
49
+ } else if (event.type.includes("warning") || event.type.includes("opened") || event.type.includes("rejected")) {
50
+ logger.warn(`Event: ${event.type}`, event.data as Record<string, unknown>);
51
+ } else {
52
+ logger.debug(`Event: ${event.type}`, event.data as Record<string, unknown>);
53
+ }
54
+
55
+ // Emit to listeners on specific type channel
56
+ super.emit(eventOrType, event);
57
+ // Also emit to wildcard listeners
58
+ super.emit("*", event);
59
+ return true;
60
+ }
61
+
62
+ // Fall through to standard EventEmitter for internal events
63
+ return super.emit(eventOrType, ...args);
64
+ }
65
+
66
+ /**
67
+ * Subscribe to a specific event type
68
+ */
69
+ override on(eventType: ImmortalEventType, listener: (event: ImmortalEvent) => void): this;
70
+ // eslint-disable-next-line @typescript-eslint/unified-signatures
71
+ override on(event: string | symbol, listener: (...args: unknown[]) => void): this;
72
+ override on(eventOrType: string | symbol, listener: ((...args: unknown[]) => void) | ((event: ImmortalEvent) => void)): this {
73
+ return super.on(eventOrType, listener as (...args: unknown[]) => void);
74
+ }
75
+
76
+ /**
77
+ * Subscribe to ALL Immortal events (wildcard)
78
+ */
79
+ onAny(listener: EventListener): this {
80
+ return super.on("*", listener);
81
+ }
82
+
83
+ /**
84
+ * Get recent event history
85
+ */
86
+ getHistory(
87
+ filter?: { type?: ImmortalEventType; since?: number; limit?: number }
88
+ ): ImmortalEvent[] {
89
+ let result = [...this.history];
90
+
91
+ if (filter?.type) {
92
+ result = result.filter((e) => e.type === filter.type);
93
+ }
94
+
95
+ if (filter?.since !== undefined) {
96
+ const since = filter.since;
97
+ result = result.filter((e) => e.timestamp >= since);
98
+ }
99
+
100
+ if (filter?.limit !== undefined) {
101
+ result = result.slice(-filter.limit);
102
+ }
103
+
104
+ return result;
105
+ }
106
+
107
+ /**
108
+ * Get event counts by type for the last N milliseconds
109
+ */
110
+ getEventCounts(windowMs = 60_000): Record<string, number> {
111
+ const since = Date.now() - windowMs;
112
+ const counts: Record<string, number> = {};
113
+
114
+ for (const event of this.history) {
115
+ if (event.timestamp >= since) {
116
+ counts[event.type] = (counts[event.type] ?? 0) + 1;
117
+ }
118
+ }
119
+
120
+ return counts;
121
+ }
122
+
123
+ /**
124
+ * Clear event history (useful for testing)
125
+ */
126
+ clearHistory(): void {
127
+ this.history = [];
128
+ }
129
+ }
130
+
131
+ // ─────────────────────────────────────────────────────────────────────────────
132
+ // SINGLETON INSTANCE
133
+ // ─────────────────────────────────────────────────────────────────────────────
134
+
135
+ let _bus: ImmortalEventBus | null = null;
136
+
137
+ export function getEventBus(): ImmortalEventBus {
138
+ if (!_bus) {
139
+ _bus = new ImmortalEventBus();
140
+ }
141
+ return _bus;
142
+ }
143
+
144
+ export function resetEventBus(): void {
145
+ _bus = null;
146
+ }
147
+
148
+ // ─────────────────────────────────────────────────────────────────────────────
149
+ // TYPE GUARD
150
+ // ─────────────────────────────────────────────────────────────────────────────
151
+
152
+ const IMMORTAL_EVENT_TYPES = new Set<string>([
153
+ "error:captured", "error:escalated",
154
+ "retry:attempt", "retry:exhausted", "retry:success",
155
+ "circuit:opened", "circuit:closed", "circuit:half-open", "circuit:rejected",
156
+ "bulkhead:queued", "bulkhead:rejected", "bulkhead:timeout",
157
+ "worker:started", "worker:restarted", "worker:escalated", "worker:crashed",
158
+ "memory:warning", "memory:critical", "memory:leak-detected", "memory:proactive-restart",
159
+ "eventloop:lag-warning", "eventloop:lag-critical",
160
+ "cpu:warning", "cpu:critical",
161
+ "shutdown:initiated", "shutdown:complete", "shutdown:forced",
162
+ "fallback:cache-hit", "fallback:default-used",
163
+ "chaos:latency-injected", "chaos:error-injected", "chaos:worker-killed",
164
+ ]);
165
+
166
+ function isImmortalEventType(type: string): type is ImmortalEventType {
167
+ return IMMORTAL_EVENT_TYPES.has(type);
168
+ }