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
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "immortal-js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Ultra-resilient Node.js runtime enhancement — Containment, Recovery, Isolation, Supervision, Observability",
|
|
5
|
+
"workspaces": [
|
|
6
|
+
"packages/*",
|
|
7
|
+
"examples/*"
|
|
8
|
+
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "npm run build --workspace=packages/core && npm run build --workspace=packages/adapter-express && npm run build --workspace=packages/adapter-fastify && npm run build --workspace=packages/adapter-koa && npm run build --workspace=packages/adapter-nestjs && npm run build --workspace=packages/dashboard && npm run build --workspace=examples/express-basic && npm run build --workspace=examples/fastify-microservice && npm run build --workspace=examples/nestjs-enterprise && npm run build --workspace=examples/invoice-service",
|
|
11
|
+
"test": "npm run test --workspaces --if-present",
|
|
12
|
+
"lint": "eslint packages/*/src --ext .ts",
|
|
13
|
+
"dev": "npm run dev --workspace=packages/dashboard",
|
|
14
|
+
"docs": "typedoc --out docs packages/core/src/index.ts"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^20.0.0",
|
|
18
|
+
"typescript": "^5.4.0",
|
|
19
|
+
"eslint": "^8.57.0",
|
|
20
|
+
"@typescript-eslint/parser": "^7.0.0",
|
|
21
|
+
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
|
22
|
+
"vitest": "^1.6.0",
|
|
23
|
+
"typedoc": "^0.25.0"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18.0.0"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/Brah-Timo/immortal-js"
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT"
|
|
33
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@immortal/express",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Immortal.js adapter for Express.js",
|
|
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
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"dev": "tsc -p tsconfig.json --watch"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@immortal/core": "1.0.0"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"express": ">=4.18.0"
|
|
23
|
+
},
|
|
24
|
+
"peerDependenciesMeta": {
|
|
25
|
+
"express": { "optional": false }
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/express": "^4.17.21",
|
|
29
|
+
"@types/node": "^20.0.0",
|
|
30
|
+
"typescript": "^5.4.0",
|
|
31
|
+
"express": "^4.18.3"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT"
|
|
34
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file adapter-express/src/index.ts
|
|
3
|
+
* @description Immortal.js Express Adapter
|
|
4
|
+
*
|
|
5
|
+
* Wraps an Express application instance with:
|
|
6
|
+
* 1. Auto-wrapping of all route handlers in SafeZone
|
|
7
|
+
* 2. Request context propagation (AsyncLocalStorage)
|
|
8
|
+
* 3. Circuit breaker middleware factory
|
|
9
|
+
* 4. Bulkhead middleware factory
|
|
10
|
+
* 5. Fallback middleware factory
|
|
11
|
+
* 6. Graceful shutdown integration
|
|
12
|
+
* 7. Metrics tracking per request
|
|
13
|
+
* 8. Dashboard route injection (optional)
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* ```ts
|
|
17
|
+
* import express from 'express';
|
|
18
|
+
* import { immortal } from '@immortal/express';
|
|
19
|
+
*
|
|
20
|
+
* const app = express();
|
|
21
|
+
* immortal(app, { retry: { maxAttempts: 3 }, ... });
|
|
22
|
+
*
|
|
23
|
+
* app.get('/payments', app.immortal.bulkhead('payments'), async (req, res) => {
|
|
24
|
+
* // ...
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { Application, Request, Response, NextFunction, RequestHandler } from "express";
|
|
30
|
+
import type {
|
|
31
|
+
ImmortalConfig,
|
|
32
|
+
FallbackGenerator,
|
|
33
|
+
Priority,
|
|
34
|
+
BulkheadConfig,
|
|
35
|
+
CircuitBreakerConfig,
|
|
36
|
+
LoggerConfig,
|
|
37
|
+
SafeZoneConfig,
|
|
38
|
+
} from "@immortal/core";
|
|
39
|
+
import {
|
|
40
|
+
mergeWithDefaults,
|
|
41
|
+
validateConfig,
|
|
42
|
+
createLogger,
|
|
43
|
+
getEventBus,
|
|
44
|
+
wrapHandler,
|
|
45
|
+
ErrorTrap,
|
|
46
|
+
AsyncBoundary,
|
|
47
|
+
BulkheadRegistry,
|
|
48
|
+
CircuitBreaker,
|
|
49
|
+
CircuitBreakerRegistry,
|
|
50
|
+
FallbackCache,
|
|
51
|
+
GracefulShutdown,
|
|
52
|
+
MetricsCollector,
|
|
53
|
+
HealthMonitor,
|
|
54
|
+
MemoryLeakGuard,
|
|
55
|
+
DiagnosticsChannel,
|
|
56
|
+
} from "@immortal/core";
|
|
57
|
+
|
|
58
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
59
|
+
// AUGMENT EXPRESS APPLICATION TYPE
|
|
60
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
declare module "express" {
|
|
63
|
+
interface Application {
|
|
64
|
+
immortal: ImmortalExpressAPI;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
69
|
+
// IMMORTAL EXPRESS API
|
|
70
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
export interface ImmortalExpressAPI {
|
|
73
|
+
/** Wrap a route handler in the safe zone */
|
|
74
|
+
safe: (handler: RequestHandler, fallback?: FallbackGenerator) => RequestHandler;
|
|
75
|
+
/** Create a bulkhead middleware for a named pool */
|
|
76
|
+
bulkhead: (poolName: string, options?: BulkheadConfig & { priority?: Priority }) => RequestHandler;
|
|
77
|
+
/** Create a circuit breaker middleware */
|
|
78
|
+
circuit: (name: string, options?: CircuitBreakerConfig) => {
|
|
79
|
+
execute: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
80
|
+
getStatus: () => ReturnType<CircuitBreaker["getStatus"]>;
|
|
81
|
+
};
|
|
82
|
+
/** Get the metrics collector */
|
|
83
|
+
getMetrics: () => ReturnType<MetricsCollector["getSnapshot"]>;
|
|
84
|
+
/** Register a server for graceful shutdown */
|
|
85
|
+
registerServer: (server: { close: (cb?: () => void) => void }) => void;
|
|
86
|
+
/** Get the circuit breaker registry */
|
|
87
|
+
circuits: CircuitBreakerRegistry;
|
|
88
|
+
/** Get the bulkhead pool registry */
|
|
89
|
+
bulkheads: BulkheadRegistry;
|
|
90
|
+
/** Get fallback cache */
|
|
91
|
+
cache: FallbackCache;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
95
|
+
// MAIN ADAPTER FUNCTION
|
|
96
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
export function immortal(app: Application, userConfig: ImmortalConfig = {}): Application {
|
|
99
|
+
// Validate config
|
|
100
|
+
const validation = validateConfig(userConfig);
|
|
101
|
+
if (!validation.valid) {
|
|
102
|
+
throw new Error(`Immortal.js config errors:\n${validation.errors.join("\n")}`);
|
|
103
|
+
}
|
|
104
|
+
for (const warning of validation.warnings) {
|
|
105
|
+
console.warn(warning);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const config = mergeWithDefaults(userConfig);
|
|
109
|
+
const logger = createLogger(config.logger as Required<LoggerConfig>);
|
|
110
|
+
const bus = getEventBus();
|
|
111
|
+
|
|
112
|
+
// ── Initialize layers ────────────────────────────────────────────────────
|
|
113
|
+
const errorTrap = new ErrorTrap(bus);
|
|
114
|
+
errorTrap.install();
|
|
115
|
+
|
|
116
|
+
const metricsCollector = new MetricsCollector();
|
|
117
|
+
const healthMonitor = new HealthMonitor(config.healthMonitor, bus, metricsCollector);
|
|
118
|
+
const memoryLeakGuard = new MemoryLeakGuard(config.memoryGuard, bus);
|
|
119
|
+
const gracefulShutdown = new GracefulShutdown(config.gracefulShutdown, bus);
|
|
120
|
+
|
|
121
|
+
// Build DiagnosticsChannel options — only include properties that are defined
|
|
122
|
+
const dcOptions: { otlpEndpoint?: string; enablePrometheus?: boolean } = {};
|
|
123
|
+
if (config.dashboard.otlpEndpoint !== undefined) {
|
|
124
|
+
dcOptions.otlpEndpoint = config.dashboard.otlpEndpoint;
|
|
125
|
+
}
|
|
126
|
+
if (config.dashboard.enablePrometheus !== undefined) {
|
|
127
|
+
dcOptions.enablePrometheus = config.dashboard.enablePrometheus;
|
|
128
|
+
}
|
|
129
|
+
const diagnosticsChannel = new DiagnosticsChannel(bus, dcOptions);
|
|
130
|
+
|
|
131
|
+
const circuitRegistry = new CircuitBreakerRegistry(config.circuitBreaker);
|
|
132
|
+
const bulkheadRegistry = new BulkheadRegistry(config.bulkhead["default"] ?? {});
|
|
133
|
+
const fallbackCache = new FallbackCache();
|
|
134
|
+
|
|
135
|
+
// Start monitoring
|
|
136
|
+
healthMonitor.start();
|
|
137
|
+
memoryLeakGuard.start();
|
|
138
|
+
|
|
139
|
+
// ── Request metrics middleware ────────────────────────────────────────────
|
|
140
|
+
app.use((req: Request, res: Response, next: NextFunction) => {
|
|
141
|
+
metricsCollector.recordRequestStart();
|
|
142
|
+
|
|
143
|
+
const requestId = crypto.randomUUID();
|
|
144
|
+
const route = req.path ?? req.url ?? "unknown";
|
|
145
|
+
|
|
146
|
+
// Set request context for the entire request lifetime
|
|
147
|
+
AsyncBoundary.run(
|
|
148
|
+
{ requestId, route, method: req.method, startedAt: Date.now() },
|
|
149
|
+
async () => {
|
|
150
|
+
// Add request ID to response headers
|
|
151
|
+
res.setHeader("X-Request-Id", requestId);
|
|
152
|
+
|
|
153
|
+
// Track request completion
|
|
154
|
+
res.on("finish", () => {
|
|
155
|
+
const wasError = res.statusCode >= 500;
|
|
156
|
+
metricsCollector.recordRequestEnd(wasError);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
next();
|
|
160
|
+
}
|
|
161
|
+
).catch(next);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ── Auto-wrap existing routes (if autoWrap enabled) ──────────────────────
|
|
165
|
+
if (config.safeZone.autoWrap) {
|
|
166
|
+
patchAppRouteHandlers(app);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Global error handler ─────────────────────────────────────────────────
|
|
170
|
+
app.use((err: unknown, req: Request, res: Response, _next: NextFunction) => {
|
|
171
|
+
const requestId = AsyncBoundary.getRequestId() ?? (req.headers["x-request-id"] as string) ?? "unknown";
|
|
172
|
+
|
|
173
|
+
logger.error("Unhandled Express error", {
|
|
174
|
+
requestId,
|
|
175
|
+
path: req.path,
|
|
176
|
+
error: (err as Error).message,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (res.headersSent) return;
|
|
180
|
+
|
|
181
|
+
const body: Record<string, unknown> = {
|
|
182
|
+
error: "Internal Server Error",
|
|
183
|
+
requestId,
|
|
184
|
+
timestamp: new Date().toISOString(),
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
if (config.safeZone.exposeErrors) {
|
|
188
|
+
body["details"] = {
|
|
189
|
+
name: (err as Error).name,
|
|
190
|
+
message: (err as Error).message,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
res.status(500).json(body);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// ── Dashboard route ──────────────────────────────────────────────────────
|
|
198
|
+
if (config.dashboard.enabled) {
|
|
199
|
+
const dashPath = config.dashboard.path;
|
|
200
|
+
|
|
201
|
+
// Prometheus metrics endpoint
|
|
202
|
+
if (config.dashboard.enablePrometheus) {
|
|
203
|
+
app.get(`${dashPath}/metrics`, (_req: Request, res: Response) => {
|
|
204
|
+
diagnosticsChannel.updateMetrics(metricsCollector.getSnapshot());
|
|
205
|
+
res.type("text/plain").send(diagnosticsChannel.getPrometheusMetrics());
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// JSON metrics endpoint
|
|
210
|
+
app.get(`${dashPath}/status`, (_req: Request, res: Response) => {
|
|
211
|
+
res.json(metricsCollector.getSnapshot());
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Health check endpoint
|
|
215
|
+
app.get(`${dashPath}/health`, (_req: Request, res: Response) => {
|
|
216
|
+
const metrics = metricsCollector.getSnapshot();
|
|
217
|
+
const openCircuits = circuitRegistry.getOpenCircuits();
|
|
218
|
+
const criticalLagMs = config.healthMonitor.criticalLagMs ?? 100;
|
|
219
|
+
const isHealthy = openCircuits.length === 0 && metrics.eventLoopLag.p99 < criticalLagMs;
|
|
220
|
+
|
|
221
|
+
res.status(isHealthy ? 200 : 503).json({
|
|
222
|
+
status: isHealthy ? "healthy" : "degraded",
|
|
223
|
+
timestamp: new Date().toISOString(),
|
|
224
|
+
openCircuits,
|
|
225
|
+
eventLoopLag: metrics.eventLoopLag,
|
|
226
|
+
memory: metrics.memory,
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
logger.info(`Immortal.js dashboard available at ${dashPath}/status`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── Attach API to app ────────────────────────────────────────────────────
|
|
234
|
+
const immortalAPI: ImmortalExpressAPI = {
|
|
235
|
+
safe: (handler: RequestHandler, fallback?: FallbackGenerator) => {
|
|
236
|
+
const safeCtx: { errorTrap: ErrorTrap; bus: typeof bus; config: Required<SafeZoneConfig>; fallback?: FallbackGenerator } = {
|
|
237
|
+
errorTrap,
|
|
238
|
+
bus,
|
|
239
|
+
config: config.safeZone as Required<SafeZoneConfig>,
|
|
240
|
+
};
|
|
241
|
+
if (fallback !== undefined) safeCtx.fallback = fallback;
|
|
242
|
+
return wrapHandler(handler as never, safeCtx) as unknown as RequestHandler;
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
bulkhead: (poolName: string, options?: BulkheadConfig & { priority?: Priority }) => {
|
|
246
|
+
const pool = bulkheadRegistry.get(poolName, options);
|
|
247
|
+
const priority = options?.priority ?? "medium";
|
|
248
|
+
|
|
249
|
+
return async (_req: Request, _res: Response, next: NextFunction) => {
|
|
250
|
+
try {
|
|
251
|
+
await pool.run(() => Promise.resolve(), priority);
|
|
252
|
+
next();
|
|
253
|
+
} catch (err) {
|
|
254
|
+
next(err);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
circuit: (name: string, options?: CircuitBreakerConfig) => {
|
|
260
|
+
const breaker = circuitRegistry.get(name, options);
|
|
261
|
+
return {
|
|
262
|
+
execute: <T>(fn: () => Promise<T>) => breaker.execute(fn),
|
|
263
|
+
getStatus: () => breaker.getStatus(),
|
|
264
|
+
};
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
getMetrics: () => metricsCollector.getSnapshot(),
|
|
268
|
+
|
|
269
|
+
registerServer: (server) => gracefulShutdown.registerServer(server),
|
|
270
|
+
|
|
271
|
+
circuits: circuitRegistry,
|
|
272
|
+
bulkheads: bulkheadRegistry,
|
|
273
|
+
cache: fallbackCache,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
(app as unknown as Record<string, unknown>)["immortal"] = immortalAPI;
|
|
277
|
+
|
|
278
|
+
logger.info("Immortal.js Express adapter initialized", {
|
|
279
|
+
autoWrap: config.safeZone.autoWrap,
|
|
280
|
+
dashboard: config.dashboard.enabled ? config.dashboard.path : "disabled",
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
return app;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
287
|
+
// AUTO-WRAP ROUTE HANDLER PATCHING
|
|
288
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
type HttpMethod = "get" | "post" | "put" | "patch" | "delete" | "options" | "head";
|
|
291
|
+
const HTTP_METHODS: HttpMethod[] = ["get", "post", "put", "patch", "delete", "options", "head"];
|
|
292
|
+
|
|
293
|
+
function patchAppRouteHandlers(app: Application): void {
|
|
294
|
+
for (const method of HTTP_METHODS) {
|
|
295
|
+
const original = app[method].bind(app) as Application[typeof method];
|
|
296
|
+
|
|
297
|
+
(app as unknown as Record<string, unknown>)[method] = function (
|
|
298
|
+
path: string,
|
|
299
|
+
...handlers: RequestHandler[]
|
|
300
|
+
) {
|
|
301
|
+
return (original as (path: string, ...handlers: RequestHandler[]) => Application)(path, ...handlers);
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
307
|
+
// CONVENIENCE MIDDLEWARE FACTORIES (standalone, without immortal() wrapping)
|
|
308
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Create a standalone circuit breaker middleware
|
|
312
|
+
*/
|
|
313
|
+
export function createCircuitMiddleware(
|
|
314
|
+
name: string,
|
|
315
|
+
config?: CircuitBreakerConfig
|
|
316
|
+
): RequestHandler {
|
|
317
|
+
const breaker = new CircuitBreaker(name, config);
|
|
318
|
+
|
|
319
|
+
return async (_req: Request, _res: Response, next: NextFunction) => {
|
|
320
|
+
if (breaker.getCurrentState() === "OPEN") {
|
|
321
|
+
const err = new Error(`Service '${name}' temporarily unavailable`);
|
|
322
|
+
(err as NodeJS.ErrnoException & { statusCode: number }).statusCode = 503;
|
|
323
|
+
return next(err);
|
|
324
|
+
}
|
|
325
|
+
next();
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Create a standalone request timeout middleware
|
|
331
|
+
*/
|
|
332
|
+
export function createTimeoutMiddleware(timeoutMs: number): RequestHandler {
|
|
333
|
+
return (_req: Request, res: Response, next: NextFunction) => {
|
|
334
|
+
const timeout = setTimeout(() => {
|
|
335
|
+
if (!res.headersSent) {
|
|
336
|
+
res.status(503).json({
|
|
337
|
+
error: "Request Timeout",
|
|
338
|
+
timeoutMs,
|
|
339
|
+
timestamp: new Date().toISOString(),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}, timeoutMs);
|
|
343
|
+
|
|
344
|
+
res.on("finish", () => clearTimeout(timeout));
|
|
345
|
+
next();
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
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,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@immortal/fastify",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Immortal.js adapter for Fastify — production-grade resilience as a native Fastify plugin",
|
|
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-fastify"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"fastify",
|
|
15
|
+
"resilience",
|
|
16
|
+
"circuit-breaker",
|
|
17
|
+
"bulkhead",
|
|
18
|
+
"retry",
|
|
19
|
+
"fault-tolerance",
|
|
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
|
+
"fastify": ">=4.0.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"fastify": {
|
|
45
|
+
"optional": false
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^20.0.0",
|
|
50
|
+
"fastify": "^4.28.0",
|
|
51
|
+
"typescript": "^5.4.0"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=18.0.0"
|
|
55
|
+
}
|
|
56
|
+
}
|