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,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file adapter-fastify/src/plugin.ts
|
|
3
|
+
* @description Immortal.js Fastify Plugin
|
|
4
|
+
*
|
|
5
|
+
* Uses Fastify's native hook system for tighter integration:
|
|
6
|
+
* - onRequest: request ID injection + bulkhead check
|
|
7
|
+
* - onError: error capture + fallback dispatch
|
|
8
|
+
* - onResponse: metrics recording
|
|
9
|
+
* - onSend: cache response for fallback
|
|
10
|
+
* - onClose: graceful shutdown integration
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
FastifyPluginAsync,
|
|
15
|
+
FastifyRequest,
|
|
16
|
+
FastifyReply,
|
|
17
|
+
FastifyInstance,
|
|
18
|
+
HookHandlerDoneFunction,
|
|
19
|
+
RouteOptions,
|
|
20
|
+
} from "fastify";
|
|
21
|
+
import type { ImmortalConfig, FallbackGenerator, Priority, LoggerConfig } from "@immortal/core";
|
|
22
|
+
import {
|
|
23
|
+
mergeWithDefaults,
|
|
24
|
+
validateConfig,
|
|
25
|
+
createLogger,
|
|
26
|
+
getEventBus,
|
|
27
|
+
ErrorTrap,
|
|
28
|
+
AsyncBoundary,
|
|
29
|
+
BulkheadPool,
|
|
30
|
+
BulkheadRegistry,
|
|
31
|
+
CircuitBreakerRegistry,
|
|
32
|
+
FallbackCache,
|
|
33
|
+
GracefulShutdown,
|
|
34
|
+
MetricsCollector,
|
|
35
|
+
HealthMonitor,
|
|
36
|
+
MemoryLeakGuard,
|
|
37
|
+
DiagnosticsChannel,
|
|
38
|
+
} from "@immortal/core";
|
|
39
|
+
|
|
40
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
// FASTIFY DECLARATIONS
|
|
42
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
declare module "fastify" {
|
|
45
|
+
interface FastifyInstance {
|
|
46
|
+
immortal: {
|
|
47
|
+
getMetrics: () => ReturnType<MetricsCollector["getSnapshot"]>;
|
|
48
|
+
circuits: CircuitBreakerRegistry;
|
|
49
|
+
bulkheads: BulkheadRegistry;
|
|
50
|
+
cache: FallbackCache;
|
|
51
|
+
bulkhead: (name: string, priority?: Priority) => (
|
|
52
|
+
request: FastifyRequest,
|
|
53
|
+
reply: FastifyReply,
|
|
54
|
+
done: HookHandlerDoneFunction
|
|
55
|
+
) => void;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
interface FastifyRequest {
|
|
59
|
+
immortalRequestId: string;
|
|
60
|
+
immortalStartedAt: number;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
65
|
+
// PLUGIN OPTIONS
|
|
66
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
export interface ImmortalFastifyOptions extends ImmortalConfig {
|
|
69
|
+
/** Skip routes matching these patterns */
|
|
70
|
+
skipRoutes?: RegExp[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
74
|
+
// FASTIFY PLUGIN
|
|
75
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
export const immortalPlugin: FastifyPluginAsync<ImmortalFastifyOptions> = async (
|
|
78
|
+
fastify: FastifyInstance,
|
|
79
|
+
userOptions: ImmortalFastifyOptions
|
|
80
|
+
) => {
|
|
81
|
+
const { skipRoutes, ...userConfig } = userOptions;
|
|
82
|
+
|
|
83
|
+
// Validate config
|
|
84
|
+
const validation = validateConfig(userConfig);
|
|
85
|
+
if (!validation.valid) {
|
|
86
|
+
throw new Error(`Immortal.js config errors:\n${validation.errors.join("\n")}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const config = mergeWithDefaults(userConfig);
|
|
90
|
+
const logger = createLogger(config.logger as Required<LoggerConfig>);
|
|
91
|
+
const bus = getEventBus();
|
|
92
|
+
|
|
93
|
+
// Initialize layers
|
|
94
|
+
const errorTrap = new ErrorTrap(bus);
|
|
95
|
+
errorTrap.install();
|
|
96
|
+
|
|
97
|
+
const metricsCollector = new MetricsCollector();
|
|
98
|
+
const healthMonitor = new HealthMonitor(config.healthMonitor, bus, metricsCollector);
|
|
99
|
+
const memoryLeakGuard = new MemoryLeakGuard(config.memoryGuard, bus);
|
|
100
|
+
const gracefulShutdown = new GracefulShutdown(config.gracefulShutdown, bus);
|
|
101
|
+
const dcOptions: { otlpEndpoint?: string; enablePrometheus?: boolean } = {};
|
|
102
|
+
if (config.dashboard.otlpEndpoint !== undefined) {
|
|
103
|
+
dcOptions.otlpEndpoint = config.dashboard.otlpEndpoint;
|
|
104
|
+
}
|
|
105
|
+
if (config.dashboard.enablePrometheus !== undefined) {
|
|
106
|
+
dcOptions.enablePrometheus = config.dashboard.enablePrometheus;
|
|
107
|
+
}
|
|
108
|
+
const diagnosticsChannel = new DiagnosticsChannel(bus, dcOptions);
|
|
109
|
+
|
|
110
|
+
const circuitRegistry = new CircuitBreakerRegistry(config.circuitBreaker);
|
|
111
|
+
const bulkheadRegistry = new BulkheadRegistry(config.bulkhead["default"] ?? {});
|
|
112
|
+
const fallbackCache = new FallbackCache();
|
|
113
|
+
|
|
114
|
+
healthMonitor.start();
|
|
115
|
+
memoryLeakGuard.start();
|
|
116
|
+
|
|
117
|
+
// ── Fastify Hooks ────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
// onRequest: Context injection
|
|
120
|
+
fastify.addHook("onRequest", async (request: FastifyRequest, _reply: FastifyReply) => {
|
|
121
|
+
const shouldSkip = skipRoutes?.some((r) => r.test(request.routerPath ?? request.url));
|
|
122
|
+
if (shouldSkip) return;
|
|
123
|
+
|
|
124
|
+
const requestId = crypto.randomUUID();
|
|
125
|
+
request.immortalRequestId = requestId;
|
|
126
|
+
request.immortalStartedAt = Date.now();
|
|
127
|
+
|
|
128
|
+
// Run the rest of the request inside AsyncLocalStorage context
|
|
129
|
+
await AsyncBoundary.run(
|
|
130
|
+
{
|
|
131
|
+
requestId,
|
|
132
|
+
route: request.routerPath ?? request.url,
|
|
133
|
+
method: request.method,
|
|
134
|
+
startedAt: Date.now(),
|
|
135
|
+
},
|
|
136
|
+
async () => {
|
|
137
|
+
metricsCollector.recordRequestStart();
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// onError: Error capture
|
|
143
|
+
fastify.addHook("onError", async (request: FastifyRequest, _reply: FastifyReply, error: Error) => {
|
|
144
|
+
errorTrap.capture(error, {
|
|
145
|
+
requestId: request.immortalRequestId,
|
|
146
|
+
route: request.routerPath ?? request.url,
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// onResponse: Metrics tracking
|
|
151
|
+
fastify.addHook("onResponse", async (request: FastifyRequest, reply: FastifyReply) => {
|
|
152
|
+
const wasError = reply.statusCode >= 500;
|
|
153
|
+
metricsCollector.recordRequestEnd(wasError);
|
|
154
|
+
|
|
155
|
+
// Auto-cache successful responses for fallback
|
|
156
|
+
if (!wasError && reply.statusCode < 300) {
|
|
157
|
+
// Body caching would require serialization hook
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// onClose: Graceful shutdown
|
|
162
|
+
fastify.addHook("onClose", async (_instance: FastifyInstance) => {
|
|
163
|
+
logger.info("Fastify closing — initiating graceful shutdown");
|
|
164
|
+
healthMonitor.stop();
|
|
165
|
+
memoryLeakGuard.stop();
|
|
166
|
+
diagnosticsChannel.stop();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ── Dashboard Routes ─────────────────────────────────────────────────────
|
|
170
|
+
if (config.dashboard.enabled) {
|
|
171
|
+
const dashPath = config.dashboard.path;
|
|
172
|
+
|
|
173
|
+
fastify.get(`${dashPath}/status`, async (_request, _reply) => {
|
|
174
|
+
return metricsCollector.getSnapshot();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
fastify.get(`${dashPath}/health`, async (_request, reply) => {
|
|
178
|
+
const metrics = metricsCollector.getSnapshot();
|
|
179
|
+
const openCircuits = circuitRegistry.getOpenCircuits();
|
|
180
|
+
const isHealthy = openCircuits.length === 0;
|
|
181
|
+
|
|
182
|
+
reply.status(isHealthy ? 200 : 503);
|
|
183
|
+
return {
|
|
184
|
+
status: isHealthy ? "healthy" : "degraded",
|
|
185
|
+
timestamp: new Date().toISOString(),
|
|
186
|
+
openCircuits,
|
|
187
|
+
eventLoopLag: metrics.eventLoopLag,
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (config.dashboard.enablePrometheus) {
|
|
192
|
+
fastify.get(`${dashPath}/metrics`, async (_request, reply) => {
|
|
193
|
+
diagnosticsChannel.updateMetrics(metricsCollector.getSnapshot());
|
|
194
|
+
reply.type("text/plain");
|
|
195
|
+
return diagnosticsChannel.getPrometheusMetrics();
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Decorate fastify instance ────────────────────────────────────────────
|
|
201
|
+
fastify.decorate("immortal", {
|
|
202
|
+
getMetrics: () => metricsCollector.getSnapshot(),
|
|
203
|
+
circuits: circuitRegistry,
|
|
204
|
+
bulkheads: bulkheadRegistry,
|
|
205
|
+
cache: fallbackCache,
|
|
206
|
+
bulkhead: (name: string, priority: Priority = "medium") => {
|
|
207
|
+
const pool = bulkheadRegistry.get(name);
|
|
208
|
+
return async (
|
|
209
|
+
_request: FastifyRequest,
|
|
210
|
+
_reply: FastifyReply,
|
|
211
|
+
done: HookHandlerDoneFunction
|
|
212
|
+
) => {
|
|
213
|
+
try {
|
|
214
|
+
await pool.run(() => Promise.resolve(), priority);
|
|
215
|
+
done();
|
|
216
|
+
} catch (err) {
|
|
217
|
+
done(err as Error);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
logger.info("Immortal.js Fastify plugin initialized");
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export default immortalPlugin;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "./src",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"declarationDir": "./dist",
|
|
7
|
+
"composite": true,
|
|
8
|
+
"incremental": true,
|
|
9
|
+
"tsBuildInfoFile": "./dist/.tsbuildinfo"
|
|
10
|
+
},
|
|
11
|
+
"include": ["src/**/*"],
|
|
12
|
+
"exclude": ["node_modules", "dist"],
|
|
13
|
+
"references": [{ "path": "../core" }]
|
|
14
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@immortal/koa",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Immortal.js adapter for Koa — resilience via the onion-model middleware architecture",
|
|
5
|
+
"author": "Immortal.js Team",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/immortal-js/immortal#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/immortal-js/immortal.git",
|
|
11
|
+
"directory": "packages/adapter-koa"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"koa",
|
|
15
|
+
"resilience",
|
|
16
|
+
"circuit-breaker",
|
|
17
|
+
"bulkhead",
|
|
18
|
+
"retry",
|
|
19
|
+
"middleware",
|
|
20
|
+
"immortal"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"import": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc --project tsconfig.json",
|
|
33
|
+
"dev": "tsc --project tsconfig.json --watch",
|
|
34
|
+
"clean": "rm -rf dist",
|
|
35
|
+
"typecheck": "tsc --noEmit"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@immortal/core": "*"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"koa": ">=2.14.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"koa": { "optional": false }
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/koa": "^2.14.0",
|
|
48
|
+
"@types/node": "^20.0.0",
|
|
49
|
+
"koa": "^2.15.0",
|
|
50
|
+
"typescript": "^5.4.0"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=18.0.0"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file adapter-koa/src/index.ts
|
|
3
|
+
* @description Immortal.js Koa Adapter
|
|
4
|
+
*
|
|
5
|
+
* Koa's onion model (middleware compose) aligns naturally with Immortal's
|
|
6
|
+
* containment layer philosophy — each middleware wraps the next like nested shells.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```ts
|
|
10
|
+
* import Koa from 'koa';
|
|
11
|
+
* import { immortal } from '@immortal/koa';
|
|
12
|
+
*
|
|
13
|
+
* const app = new Koa();
|
|
14
|
+
* immortal(app, { retry: { maxAttempts: 3 } });
|
|
15
|
+
*
|
|
16
|
+
* app.use(async (ctx, next) => {
|
|
17
|
+
* await next();
|
|
18
|
+
* ctx.body = { status: 'ok' };
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { ImmortalConfig, FallbackGenerator, Priority, LoggerConfig } from "@immortal/core";
|
|
24
|
+
import {
|
|
25
|
+
mergeWithDefaults,
|
|
26
|
+
validateConfig,
|
|
27
|
+
createLogger,
|
|
28
|
+
getEventBus,
|
|
29
|
+
ErrorTrap,
|
|
30
|
+
AsyncBoundary,
|
|
31
|
+
BulkheadRegistry,
|
|
32
|
+
CircuitBreakerRegistry,
|
|
33
|
+
FallbackCache,
|
|
34
|
+
GracefulShutdown,
|
|
35
|
+
MetricsCollector,
|
|
36
|
+
HealthMonitor,
|
|
37
|
+
MemoryLeakGuard,
|
|
38
|
+
DiagnosticsChannel,
|
|
39
|
+
serializeError,
|
|
40
|
+
} from "@immortal/core";
|
|
41
|
+
|
|
42
|
+
// Minimal Koa-like types (avoids hard peer dependency in type imports)
|
|
43
|
+
interface KoaContext {
|
|
44
|
+
path: string;
|
|
45
|
+
method: string;
|
|
46
|
+
url: string;
|
|
47
|
+
status: number;
|
|
48
|
+
body: unknown;
|
|
49
|
+
set: (key: string, value: string) => void;
|
|
50
|
+
state: Record<string, unknown>;
|
|
51
|
+
request: { body?: unknown };
|
|
52
|
+
response: { status: number; body: unknown };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type KoaNext = () => Promise<void>;
|
|
56
|
+
type KoaMiddleware = (ctx: KoaContext, next: KoaNext) => Promise<void>;
|
|
57
|
+
|
|
58
|
+
interface KoaApplication {
|
|
59
|
+
use: (middleware: KoaMiddleware) => KoaApplication;
|
|
60
|
+
[key: string]: unknown;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
// MAIN ADAPTER FUNCTION
|
|
65
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
export function immortal(app: KoaApplication, userConfig: ImmortalConfig = {}): KoaApplication {
|
|
68
|
+
const validation = validateConfig(userConfig);
|
|
69
|
+
if (!validation.valid) {
|
|
70
|
+
throw new Error(`Immortal.js config errors:\n${validation.errors.join("\n")}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const config = mergeWithDefaults(userConfig);
|
|
74
|
+
const logger = createLogger(config.logger as Required<LoggerConfig>);
|
|
75
|
+
const bus = getEventBus();
|
|
76
|
+
|
|
77
|
+
const errorTrap = new ErrorTrap(bus);
|
|
78
|
+
errorTrap.install();
|
|
79
|
+
|
|
80
|
+
const metricsCollector = new MetricsCollector();
|
|
81
|
+
const healthMonitor = new HealthMonitor(config.healthMonitor, bus, metricsCollector);
|
|
82
|
+
const memoryLeakGuard = new MemoryLeakGuard(config.memoryGuard, bus);
|
|
83
|
+
const gracefulShutdown = new GracefulShutdown(config.gracefulShutdown, bus);
|
|
84
|
+
const dcOptions: { otlpEndpoint?: string; enablePrometheus?: boolean } = {};
|
|
85
|
+
if (config.dashboard.otlpEndpoint !== undefined) {
|
|
86
|
+
dcOptions.otlpEndpoint = config.dashboard.otlpEndpoint;
|
|
87
|
+
}
|
|
88
|
+
if (config.dashboard.enablePrometheus !== undefined) {
|
|
89
|
+
dcOptions.enablePrometheus = config.dashboard.enablePrometheus;
|
|
90
|
+
}
|
|
91
|
+
const diagnosticsChannel = new DiagnosticsChannel(bus, dcOptions);
|
|
92
|
+
|
|
93
|
+
const circuitRegistry = new CircuitBreakerRegistry(config.circuitBreaker);
|
|
94
|
+
const bulkheadRegistry = new BulkheadRegistry(config.bulkhead["default"] ?? {});
|
|
95
|
+
const fallbackCache = new FallbackCache();
|
|
96
|
+
|
|
97
|
+
healthMonitor.start();
|
|
98
|
+
memoryLeakGuard.start();
|
|
99
|
+
|
|
100
|
+
// ── Core containment middleware ──────────────────────────────────────────
|
|
101
|
+
// The outermost onion layer — catches everything that slips through inner layers
|
|
102
|
+
app.use(async (ctx: KoaContext, next: KoaNext) => {
|
|
103
|
+
const requestId = crypto.randomUUID();
|
|
104
|
+
const route = ctx.path ?? ctx.url;
|
|
105
|
+
const startedAt = Date.now();
|
|
106
|
+
|
|
107
|
+
ctx.set("X-Request-Id", requestId);
|
|
108
|
+
ctx.state["immortalRequestId"] = requestId;
|
|
109
|
+
ctx.state["immortalStartedAt"] = startedAt;
|
|
110
|
+
|
|
111
|
+
metricsCollector.recordRequestStart();
|
|
112
|
+
|
|
113
|
+
await AsyncBoundary.run(
|
|
114
|
+
{ requestId, route, method: ctx.method, startedAt },
|
|
115
|
+
async () => {
|
|
116
|
+
try {
|
|
117
|
+
await next();
|
|
118
|
+
metricsCollector.recordRequestEnd(ctx.status >= 500);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
metricsCollector.recordRequestEnd(true);
|
|
121
|
+
|
|
122
|
+
errorTrap.capture(err, { requestId, route });
|
|
123
|
+
|
|
124
|
+
bus.emit("error:captured", {
|
|
125
|
+
requestId,
|
|
126
|
+
route,
|
|
127
|
+
error: serializeError(err),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Check for cached fallback
|
|
131
|
+
const cached = fallbackCache.get(`${ctx.method}:${route}`);
|
|
132
|
+
if (cached) {
|
|
133
|
+
ctx.set("X-Immortal-Fallback", "true");
|
|
134
|
+
ctx.status = 200;
|
|
135
|
+
ctx.body = cached;
|
|
136
|
+
bus.emit("fallback:cache-hit", { route, data: { method: ctx.method } });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// No fallback: proper error response
|
|
141
|
+
const serialized = serializeError(err);
|
|
142
|
+
const statusCode = (err as { statusCode?: number; status?: number }).statusCode
|
|
143
|
+
?? (err as { statusCode?: number; status?: number }).status
|
|
144
|
+
?? 500;
|
|
145
|
+
|
|
146
|
+
ctx.status = statusCode;
|
|
147
|
+
ctx.body = {
|
|
148
|
+
error: statusCode >= 500 ? "Internal Server Error" : (err as Error).message,
|
|
149
|
+
requestId,
|
|
150
|
+
timestamp: new Date().toISOString(),
|
|
151
|
+
...(config.safeZone.exposeErrors ? { details: serialized } : {}),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ── Dashboard routes ─────────────────────────────────────────────────────
|
|
159
|
+
if (config.dashboard.enabled) {
|
|
160
|
+
const dashPath = config.dashboard.path;
|
|
161
|
+
|
|
162
|
+
app.use(async (ctx: KoaContext, next: KoaNext) => {
|
|
163
|
+
if (ctx.path === `${dashPath}/status`) {
|
|
164
|
+
ctx.body = metricsCollector.getSnapshot();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (ctx.path === `${dashPath}/health`) {
|
|
169
|
+
const openCircuits = circuitRegistry.getOpenCircuits();
|
|
170
|
+
const isHealthy = openCircuits.length === 0;
|
|
171
|
+
ctx.status = isHealthy ? 200 : 503;
|
|
172
|
+
ctx.body = { status: isHealthy ? "healthy" : "degraded", openCircuits };
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (config.dashboard.enablePrometheus && ctx.path === `${dashPath}/metrics`) {
|
|
177
|
+
diagnosticsChannel.updateMetrics(metricsCollector.getSnapshot());
|
|
178
|
+
ctx.set("Content-Type", "text/plain");
|
|
179
|
+
ctx.body = diagnosticsChannel.getPrometheusMetrics();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await next();
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Attach API to app ────────────────────────────────────────────────────
|
|
188
|
+
app["immortal"] = {
|
|
189
|
+
getMetrics: () => metricsCollector.getSnapshot(),
|
|
190
|
+
circuits: circuitRegistry,
|
|
191
|
+
bulkheads: bulkheadRegistry,
|
|
192
|
+
cache: fallbackCache,
|
|
193
|
+
registerServer: (server: { close: (cb?: () => void) => void }) =>
|
|
194
|
+
gracefulShutdown.registerServer(server),
|
|
195
|
+
bulkhead: (name: string, priority: Priority = "medium"): KoaMiddleware => {
|
|
196
|
+
const pool = bulkheadRegistry.get(name);
|
|
197
|
+
return async (_ctx: KoaContext, next: KoaNext) => {
|
|
198
|
+
await pool.run(next, priority);
|
|
199
|
+
};
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
logger.info("Immortal.js Koa adapter initialized");
|
|
204
|
+
return app;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export type { ImmortalConfig, FallbackGenerator };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "./src",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"declarationDir": "./dist",
|
|
7
|
+
"composite": true,
|
|
8
|
+
"incremental": true,
|
|
9
|
+
"tsBuildInfoFile": "./dist/.tsbuildinfo"
|
|
10
|
+
},
|
|
11
|
+
"include": ["src/**/*"],
|
|
12
|
+
"exclude": ["node_modules", "dist"],
|
|
13
|
+
"references": [{ "path": "../core" }]
|
|
14
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@immortal/nestjs",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Immortal.js Dynamic Module for NestJS — enterprise-grade resilience with DI-friendly decorators",
|
|
5
|
+
"author": "Immortal.js Team",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/immortal-js/immortal#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/immortal-js/immortal.git",
|
|
11
|
+
"directory": "packages/adapter-nestjs"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"nestjs",
|
|
15
|
+
"resilience",
|
|
16
|
+
"circuit-breaker",
|
|
17
|
+
"bulkhead",
|
|
18
|
+
"retry",
|
|
19
|
+
"dependency-injection",
|
|
20
|
+
"enterprise",
|
|
21
|
+
"immortal"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc --project tsconfig.json",
|
|
34
|
+
"dev": "tsc --project tsconfig.json --watch",
|
|
35
|
+
"clean": "rm -rf dist",
|
|
36
|
+
"typecheck": "tsc --noEmit"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@immortal/core": "*"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@nestjs/common": ">=10.0.0",
|
|
43
|
+
"@nestjs/core": ">=10.0.0",
|
|
44
|
+
"reflect-metadata": ">=0.1.13"
|
|
45
|
+
},
|
|
46
|
+
"peerDependenciesMeta": {
|
|
47
|
+
"@nestjs/common": { "optional": false },
|
|
48
|
+
"@nestjs/core": { "optional": false },
|
|
49
|
+
"reflect-metadata": { "optional": false }
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@nestjs/common": "^10.3.0",
|
|
53
|
+
"@nestjs/core": "^10.3.0",
|
|
54
|
+
"@types/node": "^20.0.0",
|
|
55
|
+
"reflect-metadata": "^0.2.1",
|
|
56
|
+
"typescript": "^5.4.0"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=18.0.0"
|
|
60
|
+
}
|
|
61
|
+
}
|