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,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file adapter-nestjs/src/immortal.module.ts
|
|
3
|
+
* @description Immortal.js NestJS Dynamic Module
|
|
4
|
+
*
|
|
5
|
+
* Provides:
|
|
6
|
+
* - ImmortalModule.forRoot() for app-wide configuration
|
|
7
|
+
* - ImmortalInterceptor: wraps all controller methods in safe zone
|
|
8
|
+
* - ImmortalExceptionFilter: handles all unhandled exceptions
|
|
9
|
+
* - Injectable services: ImmortalService (metrics, circuit, bulkhead access)
|
|
10
|
+
* - Decorators: @Bulkhead(), @Circuit(), @Retry(), @Fallback()
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Note: NestJS types are referenced here without direct import
|
|
14
|
+
// to avoid requiring NestJS as a hard dependency of the adapter package.
|
|
15
|
+
// The actual NestJS decorators (@Module, @Injectable, etc.) would be imported
|
|
16
|
+
// from @nestjs/* in a real NestJS project.
|
|
17
|
+
|
|
18
|
+
import type { ImmortalConfig, SystemMetrics, Priority, LoggerConfig, BulkheadConfig } from "@immortal/core";
|
|
19
|
+
import {
|
|
20
|
+
mergeWithDefaults,
|
|
21
|
+
validateConfig,
|
|
22
|
+
createLogger,
|
|
23
|
+
getEventBus,
|
|
24
|
+
ErrorTrap,
|
|
25
|
+
MetricsCollector,
|
|
26
|
+
HealthMonitor,
|
|
27
|
+
MemoryLeakGuard,
|
|
28
|
+
BulkheadRegistry,
|
|
29
|
+
CircuitBreakerRegistry,
|
|
30
|
+
FallbackCache,
|
|
31
|
+
GracefulShutdown,
|
|
32
|
+
withRetry,
|
|
33
|
+
DiagnosticsChannel,
|
|
34
|
+
} from "@immortal/core";
|
|
35
|
+
|
|
36
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
// IMMORTAL SERVICE (Injectable)
|
|
38
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
export class ImmortalService {
|
|
41
|
+
private readonly metricsCollector: MetricsCollector;
|
|
42
|
+
private readonly circuitRegistry: CircuitBreakerRegistry;
|
|
43
|
+
private readonly bulkheadRegistry: BulkheadRegistry;
|
|
44
|
+
private readonly fallbackCache: FallbackCache;
|
|
45
|
+
private readonly config: Required<ImmortalConfig>;
|
|
46
|
+
|
|
47
|
+
constructor(
|
|
48
|
+
metricsCollector: MetricsCollector,
|
|
49
|
+
circuitRegistry: CircuitBreakerRegistry,
|
|
50
|
+
bulkheadRegistry: BulkheadRegistry,
|
|
51
|
+
fallbackCache: FallbackCache,
|
|
52
|
+
config: Required<ImmortalConfig>
|
|
53
|
+
) {
|
|
54
|
+
this.metricsCollector = metricsCollector;
|
|
55
|
+
this.circuitRegistry = circuitRegistry;
|
|
56
|
+
this.bulkheadRegistry = bulkheadRegistry;
|
|
57
|
+
this.fallbackCache = fallbackCache;
|
|
58
|
+
this.config = config;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Get current system metrics snapshot */
|
|
62
|
+
getMetrics(): SystemMetrics {
|
|
63
|
+
return this.metricsCollector.getSnapshot();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Execute an operation through a named circuit breaker */
|
|
67
|
+
async circuit<T>(name: string, fn: () => Promise<T>, fallback?: () => T): Promise<T> {
|
|
68
|
+
const breaker = this.circuitRegistry.get(name);
|
|
69
|
+
return breaker.execute(fn, fallback);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Execute an operation through a named bulkhead pool */
|
|
73
|
+
async bulkhead<T>(
|
|
74
|
+
poolName: string,
|
|
75
|
+
fn: () => Promise<T>,
|
|
76
|
+
priority: Priority = "medium"
|
|
77
|
+
): Promise<T> {
|
|
78
|
+
const pool = this.bulkheadRegistry.get(poolName);
|
|
79
|
+
return pool.run(fn, priority);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Execute with retry */
|
|
83
|
+
async retry<T>(fn: () => Promise<T>, operationName?: string): Promise<T> {
|
|
84
|
+
return withRetry(fn, this.config.retry, operationName);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Get circuit breaker status */
|
|
88
|
+
getCircuitStatus(name: string) {
|
|
89
|
+
return this.circuitRegistry.get(name).getStatus();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Get all circuit statuses */
|
|
93
|
+
getAllCircuitStatuses() {
|
|
94
|
+
return this.circuitRegistry.getAllStatuses();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Get bulkhead pool status */
|
|
98
|
+
getBulkheadStatus(name: string) {
|
|
99
|
+
return this.bulkheadRegistry.get(name).getStatus();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Get all bulkhead statuses */
|
|
103
|
+
getAllBulkheadStatuses() {
|
|
104
|
+
return this.bulkheadRegistry.getAllStatuses();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
109
|
+
// IMMORTAL MODULE FACTORY
|
|
110
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
export interface ImmortalModuleOptions extends ImmortalConfig {
|
|
113
|
+
isGlobal?: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Factory to create the NestJS module configuration object.
|
|
118
|
+
* In a real NestJS project, use @Module() decorator on this class.
|
|
119
|
+
*
|
|
120
|
+
* Usage:
|
|
121
|
+
* ```typescript
|
|
122
|
+
* @Module({
|
|
123
|
+
* imports: [ImmortalModule.forRoot({ retry: { maxAttempts: 3 } })],
|
|
124
|
+
* })
|
|
125
|
+
* export class AppModule {}
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export class ImmortalModule {
|
|
129
|
+
static forRoot(options: ImmortalModuleOptions = {}) {
|
|
130
|
+
const { isGlobal: _isGlobal, ...userConfig } = options;
|
|
131
|
+
|
|
132
|
+
const validation = validateConfig(userConfig);
|
|
133
|
+
if (!validation.valid) {
|
|
134
|
+
throw new Error(`Immortal.js NestJS module config errors:\n${validation.errors.join("\n")}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const config = mergeWithDefaults(userConfig);
|
|
138
|
+
const logger = createLogger(config.logger as Required<LoggerConfig>);
|
|
139
|
+
const bus = getEventBus();
|
|
140
|
+
|
|
141
|
+
// Initialize all layers
|
|
142
|
+
const errorTrap = new ErrorTrap(bus);
|
|
143
|
+
errorTrap.install();
|
|
144
|
+
|
|
145
|
+
const metricsCollector = new MetricsCollector();
|
|
146
|
+
const healthMonitor = new HealthMonitor(config.healthMonitor, bus, metricsCollector);
|
|
147
|
+
const memoryLeakGuard = new MemoryLeakGuard(config.memoryGuard, bus);
|
|
148
|
+
const gracefulShutdown = new GracefulShutdown(config.gracefulShutdown, bus);
|
|
149
|
+
const dcOptions: { otlpEndpoint?: string; enablePrometheus?: boolean } = {};
|
|
150
|
+
if (config.dashboard.otlpEndpoint !== undefined) {
|
|
151
|
+
dcOptions.otlpEndpoint = config.dashboard.otlpEndpoint;
|
|
152
|
+
}
|
|
153
|
+
if (config.dashboard.enablePrometheus !== undefined) {
|
|
154
|
+
dcOptions.enablePrometheus = config.dashboard.enablePrometheus;
|
|
155
|
+
}
|
|
156
|
+
const diagnosticsChannel = new DiagnosticsChannel(bus, dcOptions);
|
|
157
|
+
|
|
158
|
+
const circuitRegistry = new CircuitBreakerRegistry(config.circuitBreaker);
|
|
159
|
+
const bulkheadRegistry = new BulkheadRegistry(config.bulkhead["default"] ?? {});
|
|
160
|
+
const fallbackCache = new FallbackCache();
|
|
161
|
+
|
|
162
|
+
healthMonitor.start();
|
|
163
|
+
memoryLeakGuard.start();
|
|
164
|
+
|
|
165
|
+
const service = new ImmortalService(
|
|
166
|
+
metricsCollector,
|
|
167
|
+
circuitRegistry,
|
|
168
|
+
bulkheadRegistry,
|
|
169
|
+
fallbackCache,
|
|
170
|
+
config
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
logger.info("Immortal.js NestJS module initialized");
|
|
174
|
+
|
|
175
|
+
// Return the module definition that NestJS can use
|
|
176
|
+
return {
|
|
177
|
+
module: ImmortalModule,
|
|
178
|
+
global: _isGlobal ?? false,
|
|
179
|
+
providers: [
|
|
180
|
+
{ provide: "IMMORTAL_CONFIG", useValue: config },
|
|
181
|
+
{ provide: MetricsCollector, useValue: metricsCollector },
|
|
182
|
+
{ provide: CircuitBreakerRegistry, useValue: circuitRegistry },
|
|
183
|
+
{ provide: BulkheadRegistry, useValue: bulkheadRegistry },
|
|
184
|
+
{ provide: FallbackCache, useValue: fallbackCache },
|
|
185
|
+
{ provide: ImmortalService, useValue: service },
|
|
186
|
+
// Shutdown hook for NestJS lifecycle
|
|
187
|
+
{
|
|
188
|
+
provide: "IMMORTAL_SHUTDOWN",
|
|
189
|
+
useFactory: () => ({
|
|
190
|
+
onModuleDestroy: async () => {
|
|
191
|
+
healthMonitor.stop();
|
|
192
|
+
memoryLeakGuard.stop();
|
|
193
|
+
diagnosticsChannel.stop();
|
|
194
|
+
logger.info("Immortal.js NestJS module destroyed");
|
|
195
|
+
},
|
|
196
|
+
}),
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
exports: [ImmortalService, MetricsCollector, CircuitBreakerRegistry, BulkheadRegistry],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
static forRootAsync(options: {
|
|
204
|
+
useFactory: (...args: unknown[]) => Promise<ImmortalModuleOptions> | ImmortalModuleOptions;
|
|
205
|
+
inject?: unknown[];
|
|
206
|
+
}) {
|
|
207
|
+
return {
|
|
208
|
+
module: ImmortalModule,
|
|
209
|
+
providers: [
|
|
210
|
+
{
|
|
211
|
+
provide: "IMMORTAL_OPTIONS",
|
|
212
|
+
useFactory: options.useFactory,
|
|
213
|
+
inject: options.inject ?? [],
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
provide: ImmortalService,
|
|
217
|
+
useFactory: async (opts: ImmortalModuleOptions) => {
|
|
218
|
+
const module = ImmortalModule.forRoot(opts);
|
|
219
|
+
return module.providers.find((p) => "provide" in p && p.provide === ImmortalService)?.useValue;
|
|
220
|
+
},
|
|
221
|
+
inject: ["IMMORTAL_OPTIONS"],
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
exports: [ImmortalService],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
230
|
+
// METHOD DECORATORS
|
|
231
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @Bulkhead('payments') — Wrap method in bulkhead isolation
|
|
235
|
+
*/
|
|
236
|
+
export function Bulkhead(
|
|
237
|
+
poolName: string,
|
|
238
|
+
options: { priority?: Priority; maxConcurrent?: number } = {}
|
|
239
|
+
): MethodDecorator {
|
|
240
|
+
return function (
|
|
241
|
+
_target: object,
|
|
242
|
+
_propertyKey: string | symbol,
|
|
243
|
+
descriptor: PropertyDescriptor
|
|
244
|
+
): PropertyDescriptor {
|
|
245
|
+
const originalMethod = descriptor.value as (...args: unknown[]) => Promise<unknown>;
|
|
246
|
+
|
|
247
|
+
descriptor.value = async function (this: unknown, ...args: unknown[]): Promise<unknown> {
|
|
248
|
+
const registryConfig: BulkheadConfig = {};
|
|
249
|
+
if (options.maxConcurrent !== undefined) {
|
|
250
|
+
registryConfig.maxConcurrent = options.maxConcurrent;
|
|
251
|
+
}
|
|
252
|
+
const registry = new BulkheadRegistry(registryConfig);
|
|
253
|
+
const pool = registry.get(poolName);
|
|
254
|
+
return pool.run(
|
|
255
|
+
() => originalMethod.apply(this, args),
|
|
256
|
+
options.priority ?? "medium"
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
return descriptor;
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* @Circuit('payment-service') — Wrap method in circuit breaker
|
|
266
|
+
*/
|
|
267
|
+
export function Circuit(
|
|
268
|
+
name: string,
|
|
269
|
+
options: { fallback?: () => unknown } = {}
|
|
270
|
+
): MethodDecorator {
|
|
271
|
+
return function (
|
|
272
|
+
_target: object,
|
|
273
|
+
_propertyKey: string | symbol,
|
|
274
|
+
descriptor: PropertyDescriptor
|
|
275
|
+
): PropertyDescriptor {
|
|
276
|
+
const originalMethod = descriptor.value as (...args: unknown[]) => Promise<unknown>;
|
|
277
|
+
const breaker = new CircuitBreakerRegistry().get(name);
|
|
278
|
+
|
|
279
|
+
descriptor.value = async function (this: unknown, ...args: unknown[]): Promise<unknown> {
|
|
280
|
+
return breaker.execute(
|
|
281
|
+
() => originalMethod.apply(this, args),
|
|
282
|
+
options.fallback
|
|
283
|
+
);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
return descriptor;
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* @Retry({ maxAttempts: 3 }) — Wrap method with retry logic
|
|
292
|
+
*/
|
|
293
|
+
export function Retry(config?: Parameters<typeof withRetry>[1]): MethodDecorator {
|
|
294
|
+
return function (
|
|
295
|
+
_target: object,
|
|
296
|
+
propertyKey: string | symbol,
|
|
297
|
+
descriptor: PropertyDescriptor
|
|
298
|
+
): PropertyDescriptor {
|
|
299
|
+
const originalMethod = descriptor.value as (...args: unknown[]) => Promise<unknown>;
|
|
300
|
+
|
|
301
|
+
descriptor.value = async function (this: unknown, ...args: unknown[]): Promise<unknown> {
|
|
302
|
+
return withRetry(
|
|
303
|
+
() => originalMethod.apply(this, args),
|
|
304
|
+
config,
|
|
305
|
+
String(propertyKey)
|
|
306
|
+
);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
return descriptor;
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export { ImmortalConfig };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file index.ts
|
|
3
|
+
* @description Immortal.js NestJS Adapter — Public API Entry Point
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
ImmortalService,
|
|
8
|
+
ImmortalModule,
|
|
9
|
+
Bulkhead,
|
|
10
|
+
Circuit,
|
|
11
|
+
Retry,
|
|
12
|
+
} from "./immortal.module.js";
|
|
13
|
+
|
|
14
|
+
export type { ImmortalModuleOptions } from "./immortal.module.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
"experimentalDecorators": true,
|
|
11
|
+
"emitDecoratorMetadata": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules", "dist"],
|
|
15
|
+
"references": [{ "path": "../core" }]
|
|
16
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@immortal/core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Immortal.js core — framework-agnostic resilience runtime",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./safe-zone": {
|
|
14
|
+
"import": "./dist/safe-zone/index.js",
|
|
15
|
+
"types": "./dist/safe-zone/index.d.ts"
|
|
16
|
+
},
|
|
17
|
+
"./recovery": {
|
|
18
|
+
"import": "./dist/recovery/index.js",
|
|
19
|
+
"types": "./dist/recovery/index.d.ts"
|
|
20
|
+
},
|
|
21
|
+
"./isolation": {
|
|
22
|
+
"import": "./dist/isolation/index.js",
|
|
23
|
+
"types": "./dist/isolation/index.d.ts"
|
|
24
|
+
},
|
|
25
|
+
"./supervision": {
|
|
26
|
+
"import": "./dist/supervision/index.js",
|
|
27
|
+
"types": "./dist/supervision/index.d.ts"
|
|
28
|
+
},
|
|
29
|
+
"./monitoring": {
|
|
30
|
+
"import": "./dist/monitoring/index.js",
|
|
31
|
+
"types": "./dist/monitoring/index.d.ts"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.json",
|
|
36
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest",
|
|
39
|
+
"test:coverage": "vitest run --coverage",
|
|
40
|
+
"lint": "eslint src --ext .ts"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"zod": "^3.22.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^20.0.0",
|
|
47
|
+
"typescript": "^5.4.0",
|
|
48
|
+
"vitest": "^1.6.0",
|
|
49
|
+
"@vitest/coverage-v8": "^1.6.0"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18.0.0"
|
|
53
|
+
},
|
|
54
|
+
"sideEffects": false,
|
|
55
|
+
"license": "MIT"
|
|
56
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file ChaosEngine.ts
|
|
3
|
+
* @description Chaos engineering toolkit for testing resilience
|
|
4
|
+
*
|
|
5
|
+
* CRITICAL SAFETY RULE: ChaosEngine is DISABLED in production.
|
|
6
|
+
* Any attempt to enable it in production is blocked with an error.
|
|
7
|
+
*
|
|
8
|
+
* Provides:
|
|
9
|
+
* - Latency injection (configurable range, probability, target routes)
|
|
10
|
+
* - Error injection (configurable probability, error types)
|
|
11
|
+
* - Worker kill simulation
|
|
12
|
+
* - Memory pressure simulation
|
|
13
|
+
* - Network partition simulation (rejecting all requests to a service)
|
|
14
|
+
*
|
|
15
|
+
* Usage (development/staging only):
|
|
16
|
+
* ```ts
|
|
17
|
+
* const chaos = new ChaosEngine({ enabled: true });
|
|
18
|
+
* chaos.injectLatency({ probability: 0.1, range: [100, 500] });
|
|
19
|
+
* chaos.injectError({ probability: 0.05, statusCode: 503 });
|
|
20
|
+
*
|
|
21
|
+
* // As Express middleware
|
|
22
|
+
* app.use(chaos.middleware());
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { ChaosConfig } from "../types.js";
|
|
27
|
+
import { getEventBus } from "../event-bus.js";
|
|
28
|
+
import { getLogger } from "../logger.js";
|
|
29
|
+
|
|
30
|
+
export interface LatencyOptions {
|
|
31
|
+
probability?: number;
|
|
32
|
+
range?: [number, number];
|
|
33
|
+
targetRoutes?: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ErrorInjectionOptions {
|
|
37
|
+
probability?: number;
|
|
38
|
+
statusCode?: number;
|
|
39
|
+
message?: string;
|
|
40
|
+
targetRoutes?: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface MemoryPressureOptions {
|
|
44
|
+
/** MB of memory to allocate and hold */
|
|
45
|
+
sizeMb?: number;
|
|
46
|
+
/** Duration to hold the allocation in ms (default: 10s) */
|
|
47
|
+
durationMs?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
51
|
+
// CHAOS ENGINE CLASS
|
|
52
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
export class ChaosEngine {
|
|
55
|
+
private enabled: boolean;
|
|
56
|
+
private latencyConfig?: LatencyOptions;
|
|
57
|
+
private errorConfig?: ErrorInjectionOptions;
|
|
58
|
+
private memoryPressureBuffers: Buffer[] = [];
|
|
59
|
+
private memoryPressureTimer?: ReturnType<typeof setTimeout>;
|
|
60
|
+
|
|
61
|
+
constructor(config: ChaosConfig = {}) {
|
|
62
|
+
// Hard safety check
|
|
63
|
+
if (config.enabled && process.env["NODE_ENV"] === "production") {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"CHAOS ENGINE REFUSED: Cannot enable chaos mode in production (NODE_ENV=production). " +
|
|
66
|
+
"This is a hard safety block — not configurable."
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.enabled = config.enabled ?? false;
|
|
71
|
+
|
|
72
|
+
if (this.enabled) {
|
|
73
|
+
if (config.latencyProbability) {
|
|
74
|
+
const latencyOpts: LatencyOptions = {
|
|
75
|
+
probability: config.latencyProbability,
|
|
76
|
+
range: config.latencyRange ?? [10, 500],
|
|
77
|
+
};
|
|
78
|
+
if (config.targetRoutes !== undefined) latencyOpts.targetRoutes = config.targetRoutes;
|
|
79
|
+
this.latencyConfig = latencyOpts;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (config.errorProbability) {
|
|
83
|
+
const errorOpts: ErrorInjectionOptions = {
|
|
84
|
+
probability: config.errorProbability,
|
|
85
|
+
statusCode: 503,
|
|
86
|
+
};
|
|
87
|
+
if (config.targetRoutes !== undefined) errorOpts.targetRoutes = config.targetRoutes;
|
|
88
|
+
this.errorConfig = errorOpts;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getLogger().warn("⚠️ CHAOS MODE ENABLED — intentional failures will be injected", {
|
|
92
|
+
latency: !!this.latencyConfig,
|
|
93
|
+
errors: !!this.errorConfig,
|
|
94
|
+
memoryPressure: config.memoryPressureMb,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Inject latency into a specific operation
|
|
101
|
+
*/
|
|
102
|
+
async injectLatency(route?: string, options?: LatencyOptions): Promise<void> {
|
|
103
|
+
if (!this.enabled) return;
|
|
104
|
+
|
|
105
|
+
const cfg = { ...this.latencyConfig, ...options };
|
|
106
|
+
const probability = cfg.probability ?? 0.1;
|
|
107
|
+
const [minMs, maxMs] = cfg.range ?? [10, 500];
|
|
108
|
+
const targets = cfg.targetRoutes;
|
|
109
|
+
|
|
110
|
+
// Check if this route should be targeted
|
|
111
|
+
if (targets && route && !targets.some((r) => route.includes(r))) return;
|
|
112
|
+
|
|
113
|
+
if (Math.random() < probability) {
|
|
114
|
+
const delay = Math.floor(Math.random() * (maxMs - minMs) + minMs);
|
|
115
|
+
|
|
116
|
+
getEventBus().emit("chaos:latency-injected", {
|
|
117
|
+
route,
|
|
118
|
+
data: { delayMs: delay, probability },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
getLogger().debug(`[CHAOS] Injecting ${delay}ms latency on ${route}`);
|
|
122
|
+
await sleep(delay);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Possibly throw a chaos error
|
|
128
|
+
*/
|
|
129
|
+
injectError(route?: string, options?: ErrorInjectionOptions): void {
|
|
130
|
+
if (!this.enabled) return;
|
|
131
|
+
|
|
132
|
+
const cfg = { ...this.errorConfig, ...options };
|
|
133
|
+
const probability = cfg.probability ?? 0.05;
|
|
134
|
+
const targets = cfg.targetRoutes;
|
|
135
|
+
|
|
136
|
+
if (targets && route && !targets.some((r) => route.includes(r))) return;
|
|
137
|
+
|
|
138
|
+
if (Math.random() < probability) {
|
|
139
|
+
getEventBus().emit("chaos:error-injected", {
|
|
140
|
+
route,
|
|
141
|
+
data: { probability, statusCode: cfg.statusCode ?? 503 },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
getLogger().debug(`[CHAOS] Injecting error on ${route}`);
|
|
145
|
+
|
|
146
|
+
const err = new Error(cfg.message ?? "[CHAOS] Injected error for resilience testing");
|
|
147
|
+
(err as Error & { statusCode: number }).statusCode = cfg.statusCode ?? 503;
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Simulate memory pressure by allocating and holding large buffers
|
|
154
|
+
*/
|
|
155
|
+
simulateMemoryPressure(options: MemoryPressureOptions = {}): void {
|
|
156
|
+
if (!this.enabled) return;
|
|
157
|
+
|
|
158
|
+
const sizeMb = options.sizeMb ?? 100;
|
|
159
|
+
const durationMs = options.durationMs ?? 10_000;
|
|
160
|
+
|
|
161
|
+
getLogger().warn(`[CHAOS] Simulating ${sizeMb}MB memory pressure for ${durationMs}ms`);
|
|
162
|
+
|
|
163
|
+
// Allocate memory
|
|
164
|
+
const chunkSize = Math.min(sizeMb, 50); // Max 50MB chunks
|
|
165
|
+
const numChunks = Math.ceil(sizeMb / chunkSize);
|
|
166
|
+
|
|
167
|
+
for (let i = 0; i < numChunks; i++) {
|
|
168
|
+
const buf = Buffer.alloc(chunkSize * 1024 * 1024);
|
|
169
|
+
// Write to buffer to prevent optimization
|
|
170
|
+
buf.fill(i % 256);
|
|
171
|
+
this.memoryPressureBuffers.push(buf);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Release after duration
|
|
175
|
+
this.memoryPressureTimer = setTimeout(() => {
|
|
176
|
+
this.memoryPressureBuffers = [];
|
|
177
|
+
getLogger().info("[CHAOS] Memory pressure simulation complete — buffers released");
|
|
178
|
+
}, durationMs).unref();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Kill a simulated worker (sends SIGTERM to self in testing — careful!)
|
|
183
|
+
*/
|
|
184
|
+
simulateWorkerKill(delay = 1000): void {
|
|
185
|
+
if (!this.enabled) return;
|
|
186
|
+
|
|
187
|
+
getLogger().warn(`[CHAOS] Simulating worker kill in ${delay}ms`);
|
|
188
|
+
getEventBus().emit("chaos:worker-killed", { data: { delay } });
|
|
189
|
+
|
|
190
|
+
setTimeout(() => {
|
|
191
|
+
process.kill(process.pid, "SIGTERM");
|
|
192
|
+
}, delay).unref();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Create an Express/Koa middleware that applies chaos to every request
|
|
197
|
+
*/
|
|
198
|
+
middleware(): (req: { path?: string; url?: string }, _res: unknown, next: (err?: unknown) => void) => Promise<void> {
|
|
199
|
+
return async (req, _res, next) => {
|
|
200
|
+
if (!this.enabled) {
|
|
201
|
+
next();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const route = (req.path ?? req.url ?? "unknown").toString();
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await this.injectLatency(route);
|
|
209
|
+
this.injectError(route);
|
|
210
|
+
next();
|
|
211
|
+
} catch (err) {
|
|
212
|
+
next(err);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Check if chaos mode is active
|
|
219
|
+
*/
|
|
220
|
+
isEnabled(): boolean {
|
|
221
|
+
return this.enabled;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Disable chaos mode (can always be disabled, even dynamically)
|
|
226
|
+
*/
|
|
227
|
+
disable(): void {
|
|
228
|
+
this.enabled = false;
|
|
229
|
+
this.memoryPressureBuffers = [];
|
|
230
|
+
if (this.memoryPressureTimer) clearTimeout(this.memoryPressureTimer);
|
|
231
|
+
getLogger().info("[CHAOS] Chaos mode disabled");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get chaos configuration summary
|
|
236
|
+
*/
|
|
237
|
+
getSummary(): Record<string, unknown> {
|
|
238
|
+
return {
|
|
239
|
+
enabled: this.enabled,
|
|
240
|
+
latency: this.latencyConfig ?? null,
|
|
241
|
+
errors: this.errorConfig ?? null,
|
|
242
|
+
isProduction: process.env["NODE_ENV"] === "production",
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function sleep(ms: number): Promise<void> {
|
|
248
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
249
|
+
}
|