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.
- package/LICENSE +21 -0
- package/README.md +577 -0
- package/docs/CHANGELOG.md +21 -0
- package/docs/CONTRIBUTING.md +41 -0
- package/docs/api/chaos.md +179 -0
- package/docs/api/dashboard.md +109 -0
- package/docs/api/isolation.md +191 -0
- package/docs/api/lifecycle.md +187 -0
- package/docs/api/monitoring.md +313 -0
- package/docs/api/plugins.md +217 -0
- package/docs/api/recovery.md +267 -0
- package/docs/api/safe-zone.md +236 -0
- package/docs/api/supervision.md +285 -0
- package/docs/guides/configuration.md +168 -0
- package/docs/guides/express.md +171 -0
- package/docs/guides/fastify.md +188 -0
- package/docs/guides/koa.md +102 -0
- package/docs/guides/nestjs.md +182 -0
- package/docs/guides/testing.md +91 -0
- package/examples/express-basic/index.ts +462 -0
- package/examples/express-basic/package.json +21 -0
- package/examples/express-basic/tsconfig.json +24 -0
- package/examples/fastify-microservice/index.ts +342 -0
- package/examples/fastify-microservice/package.json +22 -0
- package/examples/fastify-microservice/tsconfig.json +24 -0
- package/examples/invoice-service/data/invoices.db +0 -0
- package/examples/invoice-service/data/invoices.db-shm +0 -0
- package/examples/invoice-service/data/invoices.db-wal +0 -0
- package/examples/invoice-service/package.json +25 -0
- package/examples/invoice-service/public/index.html +5025 -0
- package/examples/invoice-service/src/db.ts +608 -0
- package/examples/invoice-service/src/pdf.ts +358 -0
- package/examples/invoice-service/src/server.ts +527 -0
- package/examples/invoice-service/src/store.ts +159 -0
- package/examples/invoice-service/src/types.ts +193 -0
- package/examples/invoice-service/tsconfig.json +23 -0
- package/examples/nestjs-enterprise/app.module.ts +561 -0
- package/examples/nestjs-enterprise/main.ts +67 -0
- package/examples/nestjs-enterprise/package.json +26 -0
- package/examples/nestjs-enterprise/tsconfig.json +27 -0
- package/immortal-js-1.0.0.tgz +0 -0
- package/package.json +33 -0
- package/packages/adapter-express/package.json +34 -0
- package/packages/adapter-express/src/index.ts +349 -0
- package/packages/adapter-express/tsconfig.json +14 -0
- package/packages/adapter-fastify/package.json +56 -0
- package/packages/adapter-fastify/src/plugin.ts +226 -0
- package/packages/adapter-fastify/tsconfig.json +14 -0
- package/packages/adapter-koa/package.json +55 -0
- package/packages/adapter-koa/src/index.ts +207 -0
- package/packages/adapter-koa/tsconfig.json +14 -0
- package/packages/adapter-nestjs/package.json +61 -0
- package/packages/adapter-nestjs/src/immortal.module.ts +313 -0
- package/packages/adapter-nestjs/src/index.ts +14 -0
- package/packages/adapter-nestjs/tsconfig.json +16 -0
- package/packages/core/package.json +56 -0
- package/packages/core/src/chaos/ChaosEngine.ts +249 -0
- package/packages/core/src/config/defaults.ts +200 -0
- package/packages/core/src/config/schema.ts +199 -0
- package/packages/core/src/event-bus.ts +168 -0
- package/packages/core/src/index.ts +164 -0
- package/packages/core/src/isolation/BulkheadPool.ts +279 -0
- package/packages/core/src/isolation/WorkerSandbox.ts +306 -0
- package/packages/core/src/isolation/index.ts +8 -0
- package/packages/core/src/lifecycle/GracefulShutdown.ts +161 -0
- package/packages/core/src/logger.ts +104 -0
- package/packages/core/src/monitoring/DiagnosticsChannel.ts +248 -0
- package/packages/core/src/monitoring/HealthMonitor.ts +191 -0
- package/packages/core/src/monitoring/MemoryLeakGuard.ts +340 -0
- package/packages/core/src/monitoring/MetricsCollector.ts +219 -0
- package/packages/core/src/monitoring/index.ts +10 -0
- package/packages/core/src/plugins/BuiltinPlugins.ts +269 -0
- package/packages/core/src/recovery/CircuitBreaker.ts +334 -0
- package/packages/core/src/recovery/FallbackCache.ts +328 -0
- package/packages/core/src/recovery/RetryEngine.ts +225 -0
- package/packages/core/src/recovery/Timeout.ts +97 -0
- package/packages/core/src/recovery/index.ts +11 -0
- package/packages/core/src/runtime.ts +242 -0
- package/packages/core/src/safe-zone/AsyncBoundary.ts +114 -0
- package/packages/core/src/safe-zone/ErrorTrap.ts +347 -0
- package/packages/core/src/safe-zone/SafeWrapper.ts +317 -0
- package/packages/core/src/safe-zone/index.ts +23 -0
- package/packages/core/src/supervision/ClusterManager.ts +243 -0
- package/packages/core/src/supervision/RestartStrategy.ts +68 -0
- package/packages/core/src/supervision/Supervisor.ts +311 -0
- package/packages/core/src/supervision/index.ts +11 -0
- package/packages/core/src/types.ts +470 -0
- package/packages/core/test/bulkhead.test.ts +310 -0
- package/packages/core/test/circuit-breaker.test.ts +153 -0
- package/packages/core/test/memory-guard.test.ts +213 -0
- package/packages/core/test/retry.test.ts +110 -0
- package/packages/core/test/safe-zone.test.ts +271 -0
- package/packages/core/test/supervisor.test.ts +310 -0
- package/packages/core/tsconfig.json +13 -0
- package/packages/dashboard/package.json +56 -0
- package/packages/dashboard/server/DashboardServer.ts +454 -0
- package/packages/dashboard/tsconfig.json +14 -0
- 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
|
+
}
|