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,164 @@
1
+ /**
2
+ * @file index.ts
3
+ * @description Immortal.js Core — Public API Entry Point
4
+ *
5
+ * "We don't promise you won't fall — we guarantee you stand back up
6
+ * before anyone notices you fell."
7
+ *
8
+ * @module @immortal/core
9
+ * @version 1.0.0
10
+ */
11
+
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+ // TYPE EXPORTS
14
+ // ─────────────────────────────────────────────────────────────────────────────
15
+ export type {
16
+ ImmortalConfig,
17
+ SafeZoneConfig,
18
+ RetryConfig,
19
+ CircuitBreakerConfig,
20
+ BulkheadConfig,
21
+ SupervisorConfig,
22
+ HealthMonitorConfig,
23
+ MemoryGuardConfig,
24
+ GracefulShutdownConfig,
25
+ DashboardConfig,
26
+ ChaosConfig,
27
+ LoggerConfig,
28
+ ImmortalPlugin,
29
+ PluginContext,
30
+ FallbackGenerator,
31
+ RequestContext,
32
+ ShutdownHook,
33
+ LogTransport,
34
+ ImmortalEvent,
35
+ ImmortalEventType,
36
+ SystemMetrics,
37
+ CircuitBreakerStatus,
38
+ BulkheadStatus,
39
+ WorkerStatus,
40
+ SerializedError,
41
+ Priority,
42
+ PriorityTask,
43
+ RestartStrategy,
44
+ } from "./types.js";
45
+
46
+ // ─────────────────────────────────────────────────────────────────────────────
47
+ // ERROR CLASS EXPORTS
48
+ // ─────────────────────────────────────────────────────────────────────────────
49
+ export {
50
+ ImmortalError,
51
+ BulkheadRejectedError,
52
+ CircuitOpenError,
53
+ RetryExhaustedError,
54
+ WorkerEscalatedError,
55
+ TimeoutError,
56
+ } from "./types.js";
57
+
58
+ // ─────────────────────────────────────────────────────────────────────────────
59
+ // CONFIG EXPORTS
60
+ // ─────────────────────────────────────────────────────────────────────────────
61
+ export {
62
+ mergeWithDefaults,
63
+ IMMORTAL_DEFAULTS,
64
+ DEFAULT_RETRY,
65
+ DEFAULT_CIRCUIT_BREAKER,
66
+ DEFAULT_BULKHEAD,
67
+ DEFAULT_SUPERVISOR,
68
+ DEFAULT_HEALTH_MONITOR,
69
+ DEFAULT_MEMORY_GUARD,
70
+ DEFAULT_GRACEFUL_SHUTDOWN,
71
+ } from "./config/defaults.js";
72
+
73
+ export { validateConfig } from "./config/schema.js";
74
+ export type { ValidationResult } from "./config/schema.js";
75
+
76
+ // ─────────────────────────────────────────────────────────────────────────────
77
+ // INFRASTRUCTURE EXPORTS
78
+ // ─────────────────────────────────────────────────────────────────────────────
79
+ export { createLogger, getLogger } from "./logger.js";
80
+ export { getEventBus, resetEventBus, ImmortalEventBus } from "./event-bus.js";
81
+
82
+ // ─────────────────────────────────────────────────────────────────────────────
83
+ // SAFE ZONE EXPORTS
84
+ // ─────────────────────────────────────────────────────────────────────────────
85
+ export { wrapHandler, createSafeZone } from "./safe-zone/SafeWrapper.js";
86
+ export { ErrorTrap } from "./safe-zone/ErrorTrap.js";
87
+ export { AsyncBoundary, asyncBoundary } from "./safe-zone/AsyncBoundary.js";
88
+
89
+ // ─────────────────────────────────────────────────────────────────────────────
90
+ // RECOVERY EXPORTS
91
+ // ─────────────────────────────────────────────────────────────────────────────
92
+ export { withRetry, RetryEngine } from "./recovery/RetryEngine.js";
93
+ export { CircuitBreaker } from "./recovery/CircuitBreaker.js";
94
+ export { FallbackCache } from "./recovery/FallbackCache.js";
95
+ export { withTimeout } from "./recovery/Timeout.js";
96
+
97
+ // ─────────────────────────────────────────────────────────────────────────────
98
+ // ISOLATION EXPORTS
99
+ // ─────────────────────────────────────────────────────────────────────────────
100
+ export { BulkheadPool } from "./isolation/BulkheadPool.js";
101
+ export { WorkerSandbox } from "./isolation/WorkerSandbox.js";
102
+
103
+ // ─────────────────────────────────────────────────────────────────────────────
104
+ // SUPERVISION EXPORTS
105
+ // ─────────────────────────────────────────────────────────────────────────────
106
+ export { Supervisor } from "./supervision/Supervisor.js";
107
+ export { ClusterManager } from "./supervision/ClusterManager.js";
108
+
109
+ // ─────────────────────────────────────────────────────────────────────────────
110
+ // MONITORING EXPORTS
111
+ // ─────────────────────────────────────────────────────────────────────────────
112
+ export { HealthMonitor } from "./monitoring/HealthMonitor.js";
113
+ export { MemoryLeakGuard } from "./monitoring/MemoryLeakGuard.js";
114
+ export { MetricsCollector } from "./monitoring/MetricsCollector.js";
115
+ export { DiagnosticsChannel } from "./monitoring/DiagnosticsChannel.js";
116
+
117
+ // ─────────────────────────────────────────────────────────────────────────────
118
+ // LIFECYCLE EXPORTS
119
+ // ─────────────────────────────────────────────────────────────────────────────
120
+ export { GracefulShutdown } from "./lifecycle/GracefulShutdown.js";
121
+
122
+ // ─────────────────────────────────────────────────────────────────────────────
123
+ // CHAOS ENGINE EXPORTS
124
+ // ─────────────────────────────────────────────────────────────────────────────
125
+ export { ChaosEngine } from "./chaos/ChaosEngine.js";
126
+
127
+ // ─────────────────────────────────────────────────────────────────────────────
128
+ // PLUGIN EXPORTS
129
+ // ─────────────────────────────────────────────────────────────────────────────
130
+ export {
131
+ ConsoleLogPlugin,
132
+ RequestTracingPlugin,
133
+ createAnomalyDetectionPlugin,
134
+ createSlackAlertPlugin,
135
+ BUILTIN_PLUGINS,
136
+ } from "./plugins/BuiltinPlugins.js";
137
+
138
+ // ─────────────────────────────────────────────────────────────────────────────
139
+ // ADDITIONAL RECOVERY EXPORTS
140
+ // ─────────────────────────────────────────────────────────────────────────────
141
+ export { CircuitBreakerRegistry } from "./recovery/CircuitBreaker.js";
142
+ export { RouteFallbackCache } from "./recovery/FallbackCache.js";
143
+ export { calculateBackoff } from "./recovery/RetryEngine.js";
144
+ export { withSimpleTimeout } from "./recovery/Timeout.js";
145
+
146
+ // ─────────────────────────────────────────────────────────────────────────────
147
+ // ADDITIONAL ISOLATION EXPORTS
148
+ // ─────────────────────────────────────────────────────────────────────────────
149
+ export { BulkheadRegistry } from "./isolation/BulkheadPool.js";
150
+
151
+ // ─────────────────────────────────────────────────────────────────────────────
152
+ // ADDITIONAL SUPERVISION EXPORTS
153
+ // ─────────────────────────────────────────────────────────────────────────────
154
+ export { computeRestartDecision } from "./supervision/RestartStrategy.js";
155
+
156
+ // ─────────────────────────────────────────────────────────────────────────────
157
+ // UTILITY EXPORTS
158
+ // ─────────────────────────────────────────────────────────────────────────────
159
+ export { classifyError, serializeError } from "./safe-zone/ErrorTrap.js";
160
+
161
+ // ─────────────────────────────────────────────────────────────────────────────
162
+ // IMMORTAL RUNTIME — Main orchestrator class
163
+ // ─────────────────────────────────────────────────────────────────────────────
164
+ export { ImmortalRuntime, createImmortal, getImmortal } from "./runtime.js";
@@ -0,0 +1,279 @@
1
+ /**
2
+ * @file BulkheadPool.ts
3
+ * @description Bulkhead isolation — prevents cascade failure between services
4
+ *
5
+ * The bulkhead pattern (named after ship compartments) ensures that:
6
+ * - Each external service gets its own resource pool
7
+ * - Pool saturation for one service CANNOT affect other services
8
+ * - Priority queuing allows critical operations to preempt lower-priority ones
9
+ * - Fail-fast rejection prevents queue buildup from degrading system response time
10
+ *
11
+ * Without bulkheads: a slow payment API fills the shared thread pool → search, orders,
12
+ * notifications all slow down → entire system collapses
13
+ * With bulkheads: payment pool fills → payments queue/reject → everything else runs fine
14
+ */
15
+
16
+ import type { BulkheadConfig, BulkheadStatus, Priority, PriorityTask } from "../types.js";
17
+ import { BulkheadRejectedError } from "../types.js";
18
+ import { DEFAULT_BULKHEAD } from "../config/defaults.js";
19
+ import { getEventBus } from "../event-bus.js";
20
+ import { getLogger } from "../logger.js";
21
+
22
+ // ─────────────────────────────────────────────────────────────────────────────
23
+ // PRIORITY QUEUE
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+
26
+ const PRIORITY_ORDER: Priority[] = ["critical", "high", "medium", "low"];
27
+
28
+ interface QueuedItem<T> {
29
+ task: () => Promise<T>;
30
+ priority: Priority;
31
+ enqueueTime: number;
32
+ resolve: (value: T) => void;
33
+ reject: (reason: unknown) => void;
34
+ timeoutId?: ReturnType<typeof setTimeout>;
35
+ }
36
+
37
+ class PriorityQueue<T> {
38
+ private buckets: Map<Priority, QueuedItem<T>[]>;
39
+
40
+ constructor() {
41
+ this.buckets = new Map();
42
+ for (const p of PRIORITY_ORDER) {
43
+ this.buckets.set(p, []);
44
+ }
45
+ }
46
+
47
+ enqueue(item: QueuedItem<T>): void {
48
+ this.buckets.get(item.priority)!.push(item);
49
+ }
50
+
51
+ dequeue(): QueuedItem<T> | undefined {
52
+ for (const priority of PRIORITY_ORDER) {
53
+ const bucket = this.buckets.get(priority)!;
54
+ if (bucket.length > 0) {
55
+ return bucket.shift();
56
+ }
57
+ }
58
+ return undefined;
59
+ }
60
+
61
+ get size(): number {
62
+ let total = 0;
63
+ for (const bucket of this.buckets.values()) {
64
+ total += bucket.length;
65
+ }
66
+ return total;
67
+ }
68
+
69
+ clear(): void {
70
+ for (const p of PRIORITY_ORDER) {
71
+ this.buckets.set(p, []);
72
+ }
73
+ }
74
+ }
75
+
76
+ // ─────────────────────────────────────────────────────────────────────────────
77
+ // BULKHEAD POOL
78
+ // ─────────────────────────────────────────────────────────────────────────────
79
+
80
+ export class BulkheadPool {
81
+ private active = 0;
82
+ private readonly queue: PriorityQueue<unknown>;
83
+ private readonly config: Required<BulkheadConfig>;
84
+ private totalRejected = 0;
85
+ private totalExecuted = 0;
86
+
87
+ constructor(
88
+ public readonly name: string,
89
+ config: BulkheadConfig = {}
90
+ ) {
91
+ this.config = { ...DEFAULT_BULKHEAD, ...config };
92
+ this.queue = new PriorityQueue<unknown>();
93
+ }
94
+
95
+ /**
96
+ * Execute a task through the bulkhead pool.
97
+ *
98
+ * If the pool has capacity → execute immediately
99
+ * If pool is full but queue has space → queue with optional priority
100
+ * If queue is also full → fail fast with BulkheadRejectedError
101
+ *
102
+ * @param task - Async task to execute
103
+ * @param priority - Task priority (default: 'medium')
104
+ * @param timeoutMs - Optional per-task timeout override
105
+ */
106
+ async run<T>(
107
+ task: () => Promise<T>,
108
+ priority: Priority = "medium",
109
+ timeoutMs?: number
110
+ ): Promise<T> {
111
+ const bus = getEventBus();
112
+
113
+ // Pool has capacity → execute immediately
114
+ if (this.active < this.config.maxConcurrent) {
115
+ return this.executeTask(task);
116
+ }
117
+
118
+ // Pool full → try to queue
119
+ if (this.queue.size >= this.config.maxQueueSize) {
120
+ this.totalRejected++;
121
+
122
+ bus.emit("bulkhead:rejected", {
123
+ service: this.name,
124
+ data: {
125
+ active: this.active,
126
+ queued: this.queue.size,
127
+ maxConcurrent: this.config.maxConcurrent,
128
+ maxQueueSize: this.config.maxQueueSize,
129
+ priority,
130
+ },
131
+ });
132
+
133
+ throw new BulkheadRejectedError(this.name, "saturated");
134
+ }
135
+
136
+ // Queue the task
137
+ const effectiveTimeout = timeoutMs ?? this.config.queueTimeoutMs;
138
+
139
+ bus.emit("bulkhead:queued", {
140
+ service: this.name,
141
+ data: { queued: this.queue.size + 1, priority },
142
+ });
143
+
144
+ return new Promise<T>((resolve, reject) => {
145
+ const item: QueuedItem<T> = {
146
+ task,
147
+ priority,
148
+ enqueueTime: Date.now(),
149
+ resolve,
150
+ reject,
151
+ };
152
+
153
+ // Queue timeout
154
+ item.timeoutId = setTimeout(() => {
155
+ // Remove from queue by rejecting
156
+ item.reject(new BulkheadRejectedError(this.name, "timeout"));
157
+ bus.emit("bulkhead:timeout", {
158
+ service: this.name,
159
+ data: { queuedMs: effectiveTimeout, priority },
160
+ });
161
+ }, effectiveTimeout);
162
+
163
+ this.queue.enqueue(item as QueuedItem<unknown>);
164
+ });
165
+ }
166
+
167
+ /**
168
+ * Priority-aware run — convenience wrapper
169
+ */
170
+ async runWithPriority<T>(priorityTask: PriorityTask<T>): Promise<T> {
171
+ return this.run(
172
+ priorityTask.task,
173
+ priorityTask.priority,
174
+ priorityTask.timeoutMs
175
+ );
176
+ }
177
+
178
+ /**
179
+ * Get current pool status for monitoring
180
+ */
181
+ getStatus(): BulkheadStatus {
182
+ return {
183
+ name: this.name,
184
+ active: this.active,
185
+ queued: this.queue.size,
186
+ maxConcurrent: this.config.maxConcurrent,
187
+ maxQueueSize: this.config.maxQueueSize,
188
+ rejected: this.totalRejected,
189
+ totalExecuted: this.totalExecuted,
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Drain all queued tasks (for graceful shutdown)
195
+ */
196
+ drain(): void {
197
+ let item = this.queue.dequeue() as QueuedItem<unknown> | undefined;
198
+ while (item) {
199
+ if (item.timeoutId) clearTimeout(item.timeoutId);
200
+ item.reject(new BulkheadRejectedError(this.name, "saturated"));
201
+ item = this.queue.dequeue() as QueuedItem<unknown> | undefined;
202
+ }
203
+ }
204
+
205
+ private async executeTask<T>(task: () => Promise<T>): Promise<T> {
206
+ this.active++;
207
+ try {
208
+ const result = await task();
209
+ this.totalExecuted++;
210
+ return result;
211
+ } finally {
212
+ this.active--;
213
+ this.processQueue();
214
+ }
215
+ }
216
+
217
+ private processQueue(): void {
218
+ if (this.queue.size === 0) return;
219
+ if (this.active >= this.config.maxConcurrent) return;
220
+
221
+ const item = this.queue.dequeue() as QueuedItem<unknown> | undefined;
222
+ if (!item) return;
223
+
224
+ if (item.timeoutId) clearTimeout(item.timeoutId);
225
+
226
+ const waitMs = Date.now() - item.enqueueTime;
227
+ getLogger().debug(`Bulkhead '${this.name}': executing queued task (waited ${waitMs}ms)`);
228
+
229
+ this.executeTask(item.task)
230
+ .then((result) => (item.resolve as (v: unknown) => void)(result))
231
+ .catch((err) => item.reject(err));
232
+ }
233
+ }
234
+
235
+ // ─────────────────────────────────────────────────────────────────────────────
236
+ // BULKHEAD REGISTRY
237
+ // ─────────────────────────────────────────────────────────────────────────────
238
+
239
+ export class BulkheadRegistry {
240
+ private pools = new Map<string, BulkheadPool>();
241
+ private readonly defaultConfig: BulkheadConfig;
242
+
243
+ constructor(defaultConfig: BulkheadConfig = {}) {
244
+ this.defaultConfig = defaultConfig;
245
+ }
246
+
247
+ /**
248
+ * Get or create a named bulkhead pool
249
+ */
250
+ get(name: string, config?: BulkheadConfig): BulkheadPool {
251
+ if (!this.pools.has(name)) {
252
+ this.pools.set(
253
+ name,
254
+ new BulkheadPool(name, { ...this.defaultConfig, ...config })
255
+ );
256
+ }
257
+ return this.pools.get(name)!;
258
+ }
259
+
260
+ /**
261
+ * Get statuses of all registered pools
262
+ */
263
+ getAllStatuses(): Record<string, BulkheadStatus> {
264
+ const result: Record<string, BulkheadStatus> = {};
265
+ for (const [name, pool] of this.pools) {
266
+ result[name] = pool.getStatus();
267
+ }
268
+ return result;
269
+ }
270
+
271
+ /**
272
+ * Drain all pools (graceful shutdown)
273
+ */
274
+ drainAll(): void {
275
+ for (const pool of this.pools.values()) {
276
+ pool.drain();
277
+ }
278
+ }
279
+ }