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,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
+ }