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,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file examples/express-basic/index.ts
|
|
3
|
+
* @description Immortal.js — Express Resilience Example (Ultra Pro Edition)
|
|
4
|
+
*
|
|
5
|
+
* Demonstrates:
|
|
6
|
+
* - CircuitBreaker — wraps unreliable external APIs
|
|
7
|
+
* - BulkheadPool — per-service concurrency isolation
|
|
8
|
+
* - FallbackCache — serve stale data when backends fail
|
|
9
|
+
* - GracefulShutdown — drain connections before process exit
|
|
10
|
+
* - withRetry — exponential back-off for transient errors
|
|
11
|
+
* - withTimeout — hard deadline on any async operation
|
|
12
|
+
*
|
|
13
|
+
* Run: npm run dev
|
|
14
|
+
* Build: npm run build
|
|
15
|
+
* Start: npm start
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import express from "express";
|
|
19
|
+
import {
|
|
20
|
+
CircuitBreaker,
|
|
21
|
+
BulkheadPool,
|
|
22
|
+
FallbackCache,
|
|
23
|
+
GracefulShutdown,
|
|
24
|
+
withRetry,
|
|
25
|
+
withTimeout,
|
|
26
|
+
getEventBus,
|
|
27
|
+
} from "@immortal/core";
|
|
28
|
+
|
|
29
|
+
// ─── App ──────────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const app = express();
|
|
32
|
+
app.use(express.json());
|
|
33
|
+
|
|
34
|
+
// ─── Immortal.js Primitives ───────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Payment circuit breaker
|
|
38
|
+
* Opens after 5 failures in a 100-request sliding window.
|
|
39
|
+
* Attempts half-open probe after 15s cooldown.
|
|
40
|
+
*/
|
|
41
|
+
const paymentCircuit = new CircuitBreaker("payment-provider", {
|
|
42
|
+
threshold: 5,
|
|
43
|
+
cooldownMs: 15_000,
|
|
44
|
+
slidingWindowSize: 100,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Inventory circuit breaker
|
|
49
|
+
* More sensitive — opens after 3 failures in a 50-request window.
|
|
50
|
+
*/
|
|
51
|
+
const inventoryCircuit = new CircuitBreaker("inventory-service", {
|
|
52
|
+
threshold: 3,
|
|
53
|
+
cooldownMs: 10_000,
|
|
54
|
+
slidingWindowSize: 50,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Payments bulkhead — max 10 concurrent, 20 queued
|
|
59
|
+
* BulkheadRejectedError thrown if both pools are full.
|
|
60
|
+
*/
|
|
61
|
+
const paymentsBulkhead = new BulkheadPool("payments", {
|
|
62
|
+
maxConcurrent: 10,
|
|
63
|
+
maxQueueSize: 20,
|
|
64
|
+
queueTimeoutMs: 15_000,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Search bulkhead — high concurrency allowed (read-only)
|
|
69
|
+
*/
|
|
70
|
+
const searchBulkhead = new BulkheadPool("search", {
|
|
71
|
+
maxConcurrent: 50,
|
|
72
|
+
maxQueueSize: 100,
|
|
73
|
+
queueTimeoutMs: 5_000,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* FallbackCache — LRU with TTL
|
|
78
|
+
* Serves stale results when upstream is unavailable.
|
|
79
|
+
*/
|
|
80
|
+
const cache = new FallbackCache<unknown>({
|
|
81
|
+
maxSize: 1_000,
|
|
82
|
+
defaultTtlMs: 60_000,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ─── Graceful Shutdown ────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
const bus = getEventBus();
|
|
88
|
+
|
|
89
|
+
// ─── Routes ───────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* GET /
|
|
93
|
+
* Welcome + route map
|
|
94
|
+
*/
|
|
95
|
+
app.get("/", (_req, res) => {
|
|
96
|
+
res.json({
|
|
97
|
+
name: "Immortal.js Express Basic Example",
|
|
98
|
+
version: "1.0.0",
|
|
99
|
+
status: "running",
|
|
100
|
+
timestamp: new Date().toISOString(),
|
|
101
|
+
routes: {
|
|
102
|
+
"GET /": "This route map",
|
|
103
|
+
"GET /health": "Live health snapshot",
|
|
104
|
+
"GET /status/circuits": "All circuit statuses",
|
|
105
|
+
"POST /payments/charge": "Charge payment [Bulkhead + Circuit + Retry]",
|
|
106
|
+
"GET /inventory/:productId": "Check inventory [Circuit + Cache]",
|
|
107
|
+
"GET /search?q=...": "Search [Bulkhead + Cache]",
|
|
108
|
+
"GET /chaos/trip-payment": "Force payment circuit OPEN (dev)",
|
|
109
|
+
"GET /chaos/reset-circuits": "Reset all circuits (dev)",
|
|
110
|
+
"GET /simulate/crash": "Test global error handler",
|
|
111
|
+
"GET /simulate/leak": "Test MemoryLeakGuard",
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* GET /health
|
|
118
|
+
* Live snapshot of all Immortal.js primitive statuses.
|
|
119
|
+
*/
|
|
120
|
+
app.get("/health", (_req, res) => {
|
|
121
|
+
const payBk = paymentsBulkhead.getStatus();
|
|
122
|
+
const srchBk = searchBulkhead.getStatus();
|
|
123
|
+
|
|
124
|
+
res.json({
|
|
125
|
+
status: "ok",
|
|
126
|
+
timestamp: new Date().toISOString(),
|
|
127
|
+
uptime: Math.round(process.uptime()),
|
|
128
|
+
circuits: {
|
|
129
|
+
paymentProvider: paymentCircuit.getStatus(),
|
|
130
|
+
inventoryService: inventoryCircuit.getStatus(),
|
|
131
|
+
},
|
|
132
|
+
bulkheads: {
|
|
133
|
+
payments: { active: payBk.active, queued: payBk.queued },
|
|
134
|
+
search: { active: srchBk.active, queued: srchBk.queued },
|
|
135
|
+
},
|
|
136
|
+
cache: {
|
|
137
|
+
description: "FallbackCache (LRU + TTL) — inspect keys via /status/cache",
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* POST /payments/charge
|
|
144
|
+
* Body: { amount: number, cardToken: string }
|
|
145
|
+
*
|
|
146
|
+
* Layers:
|
|
147
|
+
* 1. BulkheadPool — cap concurrent payment requests
|
|
148
|
+
* 2. CircuitBreaker — open circuit if gateway repeatedly fails
|
|
149
|
+
* 3. withRetry — retry transient gateway errors
|
|
150
|
+
* 4. withTimeout — hard 10s deadline on the API call
|
|
151
|
+
*/
|
|
152
|
+
app.post("/payments/charge", async (req, res, next) => {
|
|
153
|
+
try {
|
|
154
|
+
const result = await paymentsBulkhead.run(
|
|
155
|
+
async () =>
|
|
156
|
+
paymentCircuit.execute(
|
|
157
|
+
async () =>
|
|
158
|
+
withRetry(
|
|
159
|
+
async () => {
|
|
160
|
+
const { amount, cardToken } = req.body as {
|
|
161
|
+
amount: number;
|
|
162
|
+
cardToken: string;
|
|
163
|
+
};
|
|
164
|
+
return withTimeout(
|
|
165
|
+
() => simulatePaymentAPI(amount, cardToken),
|
|
166
|
+
10_000,
|
|
167
|
+
"payment-api"
|
|
168
|
+
);
|
|
169
|
+
},
|
|
170
|
+
{ maxAttempts: 2, baseDelayMs: 300, maxDelayMs: 2_000 }
|
|
171
|
+
),
|
|
172
|
+
// Circuit-open fallback — inform caller to retry later
|
|
173
|
+
() => ({
|
|
174
|
+
success: false as boolean,
|
|
175
|
+
transactionId: `queued-${Date.now()}`,
|
|
176
|
+
amount: 0,
|
|
177
|
+
queued: true,
|
|
178
|
+
})
|
|
179
|
+
),
|
|
180
|
+
"high"
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
res.json(result);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
next(err);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* GET /inventory/:productId
|
|
191
|
+
* Circuit Breaker + FallbackCache
|
|
192
|
+
*/
|
|
193
|
+
app.get("/inventory/:productId", async (req, res, next) => {
|
|
194
|
+
const { productId } = req.params as { productId: string };
|
|
195
|
+
const cacheKey = `inventory:${productId}`;
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const data = await inventoryCircuit.execute(
|
|
199
|
+
async () => {
|
|
200
|
+
const result = await withTimeout(
|
|
201
|
+
() => simulateInventoryAPI(productId),
|
|
202
|
+
5_000,
|
|
203
|
+
"inventory-api"
|
|
204
|
+
);
|
|
205
|
+
// Cache successful response
|
|
206
|
+
cache.set(cacheKey, result, 30_000);
|
|
207
|
+
return result;
|
|
208
|
+
},
|
|
209
|
+
// Circuit-open fallback — serve stale cache or empty
|
|
210
|
+
() => {
|
|
211
|
+
const stale = cache.get(cacheKey);
|
|
212
|
+
if (stale) {
|
|
213
|
+
res.setHeader("X-Immortal-Fallback", "stale-cache");
|
|
214
|
+
return stale;
|
|
215
|
+
}
|
|
216
|
+
return { productId, stock: 0, stale: true, circuitOpen: true };
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
res.json(data);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
next(err);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* GET /search?q=...
|
|
228
|
+
* Bulkhead + FallbackCache (1-minute TTL)
|
|
229
|
+
*/
|
|
230
|
+
app.get("/search", async (req, res, next) => {
|
|
231
|
+
try {
|
|
232
|
+
const q = (req.query["q"] as string | undefined) ?? "";
|
|
233
|
+
const cacheKey = `search:${q}`;
|
|
234
|
+
|
|
235
|
+
const result = await searchBulkhead.run(async () => {
|
|
236
|
+
const cached = cache.get(cacheKey);
|
|
237
|
+
if (cached) {
|
|
238
|
+
res.setHeader("X-Cache", "HIT");
|
|
239
|
+
return cached;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const results = await simulateSearchAPI(q);
|
|
243
|
+
cache.set(cacheKey, results, 60_000);
|
|
244
|
+
return results;
|
|
245
|
+
}, "medium");
|
|
246
|
+
|
|
247
|
+
res.json(result);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
next(err);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* GET /status/circuits
|
|
255
|
+
* All circuit breaker statuses (open/closed/half-open)
|
|
256
|
+
*/
|
|
257
|
+
app.get("/status/circuits", (_req, res) => {
|
|
258
|
+
res.json({
|
|
259
|
+
paymentProvider: paymentCircuit.getStatus(),
|
|
260
|
+
inventoryService: inventoryCircuit.getStatus(),
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* GET /chaos/trip-payment
|
|
266
|
+
* Force payment circuit OPEN (dev / load-test use only)
|
|
267
|
+
*/
|
|
268
|
+
app.get("/chaos/trip-payment", (_req, res) => {
|
|
269
|
+
if (process.env["NODE_ENV"] === "production") {
|
|
270
|
+
res.status(403).json({ error: "Chaos endpoints disabled in production" });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
paymentCircuit.forceState("OPEN");
|
|
274
|
+
res.json({
|
|
275
|
+
message: "Payment circuit forced OPEN",
|
|
276
|
+
state: paymentCircuit.getStatus().state,
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* GET /chaos/reset-circuits
|
|
282
|
+
* Reset all circuits to CLOSED
|
|
283
|
+
*/
|
|
284
|
+
app.get("/chaos/reset-circuits", (_req, res) => {
|
|
285
|
+
if (process.env["NODE_ENV"] === "production") {
|
|
286
|
+
res.status(403).json({ error: "Chaos endpoints disabled in production" });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
paymentCircuit.forceState("CLOSED");
|
|
290
|
+
inventoryCircuit.forceState("CLOSED");
|
|
291
|
+
res.json({
|
|
292
|
+
message: "All circuits reset to CLOSED",
|
|
293
|
+
payment: paymentCircuit.getStatus().state,
|
|
294
|
+
inventory: inventoryCircuit.getStatus().state,
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* GET /simulate/crash
|
|
300
|
+
* Intentional error — exercises the global error handler.
|
|
301
|
+
*/
|
|
302
|
+
app.get("/simulate/crash", (_req, _res, next) => {
|
|
303
|
+
next(new Error("Simulated crash — global error handler should catch this!"));
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* GET /simulate/leak
|
|
308
|
+
* Intentional memory leak — exercises MemoryLeakGuard.
|
|
309
|
+
*/
|
|
310
|
+
const leakyArray: Buffer[] = [];
|
|
311
|
+
app.get("/simulate/leak", (_req, res) => {
|
|
312
|
+
leakyArray.push(Buffer.alloc(1024 * 1024)); // 1 MB per request
|
|
313
|
+
res.json({
|
|
314
|
+
message: "Memory leaked (1 MB)",
|
|
315
|
+
totalLeakedMb: leakyArray.length,
|
|
316
|
+
note: "MemoryLeakGuard detects heap growth trends and initiates proactive restart",
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// ─── Global Error Handler ─────────────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
app.use(
|
|
323
|
+
(
|
|
324
|
+
err: unknown,
|
|
325
|
+
_req: express.Request,
|
|
326
|
+
res: express.Response,
|
|
327
|
+
_next: express.NextFunction
|
|
328
|
+
) => {
|
|
329
|
+
if (res.headersSent) return;
|
|
330
|
+
|
|
331
|
+
const status =
|
|
332
|
+
(err as { statusCode?: number; status?: number }).statusCode ??
|
|
333
|
+
(err as { statusCode?: number; status?: number }).status ??
|
|
334
|
+
500;
|
|
335
|
+
|
|
336
|
+
const name = (err as { name?: string }).name ?? "Error";
|
|
337
|
+
|
|
338
|
+
// Translate Immortal.js-specific error names to HTTP status codes
|
|
339
|
+
const httpStatus =
|
|
340
|
+
name === "BulkheadRejectedError"
|
|
341
|
+
? 429
|
|
342
|
+
: name === "TimeoutError"
|
|
343
|
+
? 504
|
|
344
|
+
: status;
|
|
345
|
+
|
|
346
|
+
res.status(httpStatus).json({
|
|
347
|
+
error:
|
|
348
|
+
httpStatus >= 500
|
|
349
|
+
? "Internal Server Error"
|
|
350
|
+
: (err as Error).message ?? "Unknown error",
|
|
351
|
+
errorType: name,
|
|
352
|
+
timestamp: new Date().toISOString(),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
// ─── Start ────────────────────────────────────────────────────────────────────
|
|
358
|
+
|
|
359
|
+
const PORT = parseInt(process.env["PORT"] ?? "3000", 10);
|
|
360
|
+
|
|
361
|
+
const server = app.listen(PORT, () => {
|
|
362
|
+
console.log(`
|
|
363
|
+
╔══════════════════════════════════════════════════════════════╗
|
|
364
|
+
║ Immortal.js — Express Basic Example 🚀 ║
|
|
365
|
+
╠══════════════════════════════════════════════════════════════╣
|
|
366
|
+
║ Server → http://localhost:${PORT} ║
|
|
367
|
+
║ Health → http://localhost:${PORT}/health ║
|
|
368
|
+
║ Circuits → http://localhost:${PORT}/status/circuits ║
|
|
369
|
+
╠══════════════════════════════════════════════════════════════╣
|
|
370
|
+
║ Routes ║
|
|
371
|
+
║ POST /payments/charge Bulkhead + Circuit + Retry ║
|
|
372
|
+
║ GET /inventory/:productId Circuit + FallbackCache ║
|
|
373
|
+
║ GET /search?q=... Bulkhead + FallbackCache ║
|
|
374
|
+
║ GET /chaos/trip-payment Force payment circuit OPEN ║
|
|
375
|
+
║ GET /chaos/reset-circuits Reset all circuits ║
|
|
376
|
+
║ GET /simulate/crash Test global error handler ║
|
|
377
|
+
║ GET /simulate/leak Test MemoryLeakGuard ║
|
|
378
|
+
╚══════════════════════════════════════════════════════════════╝
|
|
379
|
+
`);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Register graceful shutdown — drains server before exit
|
|
383
|
+
const shutdown = new GracefulShutdown(
|
|
384
|
+
{
|
|
385
|
+
timeoutMs: 20_000,
|
|
386
|
+
signals: ["SIGTERM", "SIGINT"],
|
|
387
|
+
onShutdown: [
|
|
388
|
+
async (_signal: string) => {
|
|
389
|
+
// Drain the payments bulkhead queue
|
|
390
|
+
paymentsBulkhead.drain();
|
|
391
|
+
// Close HTTP server (stop accepting new requests)
|
|
392
|
+
await new Promise<void>((resolve, reject) => {
|
|
393
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
394
|
+
});
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
},
|
|
398
|
+
bus
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
// Keep reference alive (signal handlers are installed in constructor)
|
|
402
|
+
void shutdown;
|
|
403
|
+
|
|
404
|
+
// ─── Simulated External Services ─────────────────────────────────────────────
|
|
405
|
+
|
|
406
|
+
async function simulatePaymentAPI(
|
|
407
|
+
amount: number,
|
|
408
|
+
_cardToken: string
|
|
409
|
+
): Promise<{ success: boolean; transactionId: string; amount: number }> {
|
|
410
|
+
// 20% failure rate — exercises retry + circuit breaker
|
|
411
|
+
if (Math.random() < 0.2) {
|
|
412
|
+
const err = Object.assign(new Error("Payment gateway timeout"), {
|
|
413
|
+
statusCode: 503,
|
|
414
|
+
});
|
|
415
|
+
throw err;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
await sleep(100 + Math.random() * 200);
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
success: true,
|
|
422
|
+
transactionId: `txn_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
|
|
423
|
+
amount,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function simulateInventoryAPI(
|
|
428
|
+
productId: string
|
|
429
|
+
): Promise<{ productId: string; stock: number; warehouseId: string }> {
|
|
430
|
+
// 10% failure rate
|
|
431
|
+
if (Math.random() < 0.1) {
|
|
432
|
+
throw new Error("Inventory service unavailable");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
await sleep(30 + Math.random() * 70);
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
productId,
|
|
439
|
+
stock: Math.floor(Math.random() * 200),
|
|
440
|
+
warehouseId: `WH-${Math.floor(Math.random() * 5) + 1}`,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function simulateSearchAPI(
|
|
445
|
+
query: string
|
|
446
|
+
): Promise<{ results: Array<{ id: number; title: string; score: number }> }> {
|
|
447
|
+
await sleep(20 + Math.random() * 50);
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
results: Array.from({ length: 5 }, (_, i) => ({
|
|
451
|
+
id: i + 1,
|
|
452
|
+
title: `Result ${i + 1} for "${query}"`,
|
|
453
|
+
score: Math.round(Math.random() * 100) / 100,
|
|
454
|
+
})).sort((a, b) => b.score - a.score),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function sleep(ms: number): Promise<void> {
|
|
459
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export { app };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "immortal-express-basic-example",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "node --import tsx/esm --no-warnings index.ts",
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"start": "node dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@immortal/core": "*",
|
|
13
|
+
"express": "^4.18.3"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/express": "^4.17.21",
|
|
17
|
+
"@types/node": "^20.0.0",
|
|
18
|
+
"tsx": "^4.7.0",
|
|
19
|
+
"typescript": "^5.4.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"strictNullChecks": true,
|
|
9
|
+
"noUncheckedIndexedAccess": true,
|
|
10
|
+
"exactOptionalPropertyTypes": true,
|
|
11
|
+
"noImplicitOverride": true,
|
|
12
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
13
|
+
"declaration": false,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"esModuleInterop": true,
|
|
16
|
+
"allowSyntheticDefaultImports": true,
|
|
17
|
+
"forceConsistentCasingInFileNames": true,
|
|
18
|
+
"skipLibCheck": true,
|
|
19
|
+
"outDir": "dist",
|
|
20
|
+
"rootDir": "./"
|
|
21
|
+
},
|
|
22
|
+
"include": ["index.ts"],
|
|
23
|
+
"exclude": ["node_modules", "dist"]
|
|
24
|
+
}
|