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,342 @@
1
+ /**
2
+ * @file examples/fastify-microservice/index.ts
3
+ * @description Immortal.js — Fastify Microservice (Ultra Pro Edition)
4
+ *
5
+ * Demonstrates: Circuit Breaker, Bulkhead, Retry, Timeout, Fallback Cache,
6
+ * Graceful Shutdown, Health Monitor, Metrics endpoint.
7
+ *
8
+ * Run: npm run dev
9
+ * Build: npm run build
10
+ * Start: npm start
11
+ */
12
+
13
+ import Fastify from "fastify";
14
+ import {
15
+ CircuitBreaker,
16
+ BulkheadPool,
17
+ withRetry,
18
+ withTimeout,
19
+ FallbackCache,
20
+ GracefulShutdown,
21
+ getEventBus,
22
+ } from "@immortal/core";
23
+
24
+ // ─── App Setup ───────────────────────────────────────────────────────────────
25
+ // Use plain JSON logger — no pino-pretty dependency required
26
+ const fastify = Fastify({
27
+ logger: true,
28
+ });
29
+
30
+ // ─── Resilient Primitives ─────────────────────────────────────────────────────
31
+
32
+ // CircuitBreaker(name: string, config?) — positional constructor
33
+ const inventoryCircuit = new CircuitBreaker("inventory-service", {
34
+ threshold: 5,
35
+ cooldownMs: 12_000,
36
+ slidingWindowSize: 15,
37
+ });
38
+
39
+ const paymentCircuit = new CircuitBreaker("payment-gateway", {
40
+ threshold: 3,
41
+ cooldownMs: 30_000,
42
+ slidingWindowSize: 10,
43
+ });
44
+
45
+ // BulkheadPool(name: string, config?) — positional constructor
46
+ const ordersBulkhead = new BulkheadPool("orders", {
47
+ maxConcurrent: 20,
48
+ maxQueueSize: 100,
49
+ queueTimeoutMs: 5_000,
50
+ });
51
+
52
+ // FallbackCache({ maxSize?, defaultTtlMs? })
53
+ const productCache = new FallbackCache<unknown>({
54
+ maxSize: 500,
55
+ defaultTtlMs: 30_000,
56
+ });
57
+
58
+ // ─── Simulated External Services ─────────────────────────────────────────────
59
+
60
+ async function fetchInventory(productId: string): Promise<{ stock: number }> {
61
+ if (Math.random() < 0.15) throw new Error("Inventory service unavailable");
62
+ await sleep(20 + Math.random() * 80);
63
+ return { stock: Math.floor(Math.random() * 100), productId } as { stock: number };
64
+ }
65
+
66
+ async function processPayment(
67
+ orderId: string,
68
+ amount: number
69
+ ): Promise<{ transactionId: string }> {
70
+ if (Math.random() < 0.05) throw new Error("Payment gateway timeout");
71
+ await sleep(100 + Math.random() * 200);
72
+ return { transactionId: `txn-${orderId}-${Date.now()}`, amount } as { transactionId: string };
73
+ }
74
+
75
+ function sleep(ms: number): Promise<void> {
76
+ return new Promise((r) => setTimeout(r, ms));
77
+ }
78
+
79
+ // ─── Routes ───────────────────────────────────────────────────────────────────
80
+
81
+ /**
82
+ * GET /
83
+ * Welcome page with route map
84
+ */
85
+ fastify.get("/", async (_request, reply) => {
86
+ return reply.send({
87
+ name: "Immortal.js Fastify Microservice",
88
+ version: "1.0.0",
89
+ status: "running",
90
+ timestamp: new Date().toISOString(),
91
+ routes: {
92
+ "GET /": "This route map",
93
+ "GET /health": "Live health + circuit status",
94
+ "GET /metrics": "Prometheus-compatible metrics",
95
+ "GET /products/:id": "Inventory lookup [Circuit + Cache]",
96
+ "POST /orders": "Create order [Bulkhead + Retry + Circuit]",
97
+ "GET /chaos/trip-inventory": "Force inventory circuit OPEN (dev)",
98
+ "GET /chaos/reset-circuits": "Reset all circuits (dev)",
99
+ },
100
+ });
101
+ });
102
+
103
+ /**
104
+ * GET /products/:id
105
+ * Circuit Breaker wraps inventory fetch.
106
+ * On circuit open / timeout → serve from FallbackCache.
107
+ */
108
+ fastify.get<{ Params: { id: string } }>("/products/:id", async (request, reply) => {
109
+ const { id } = request.params;
110
+
111
+ try {
112
+ const data = await withTimeout(
113
+ () =>
114
+ inventoryCircuit.execute(
115
+ () => fetchInventory(id),
116
+ () => ({ stock: -1, fromFallback: true })
117
+ ),
118
+ 3_000,
119
+ "inventory-fetch"
120
+ );
121
+
122
+ // Cache successful (non-fallback) responses
123
+ if (!("fromFallback" in data)) {
124
+ productCache.set(`product:${id}`, data);
125
+ }
126
+
127
+ return reply
128
+ .header("X-Immortal-Circuit", inventoryCircuit.getStatus().state)
129
+ .send({ productId: id, ...data });
130
+ } catch {
131
+ // Serve stale cache on circuit open / timeout
132
+ const cached = productCache.get(`product:${id}`);
133
+ if (cached) {
134
+ return reply
135
+ .header("X-Immortal-Fallback", "true")
136
+ .header("X-Immortal-Fallback-Reason", "circuit-or-timeout")
137
+ .send({ productId: id, ...(cached as object), stale: true });
138
+ }
139
+ return reply.status(503).send({ error: "Product service unavailable" });
140
+ }
141
+ });
142
+
143
+ /**
144
+ * POST /orders
145
+ * Body: { productId: string, quantity: number, amount: number }
146
+ * Bulkhead → Retry inventory → CircuitBreaker payment
147
+ */
148
+ fastify.post<{ Body: { productId: string; quantity: number; amount: number } }>(
149
+ "/orders",
150
+ async (request, reply) => {
151
+ const { productId, quantity, amount } = request.body;
152
+
153
+ try {
154
+ const result = await ordersBulkhead.run(async () => {
155
+ // Check inventory with retry
156
+ const inventory = await withRetry(
157
+ () => fetchInventory(productId),
158
+ {
159
+ maxAttempts: 3,
160
+ baseDelayMs: 100,
161
+ maxDelayMs: 2_000,
162
+ isRetryable: (e) =>
163
+ !(e instanceof Error && e.message.includes("insufficient")),
164
+ }
165
+ );
166
+
167
+ if (inventory.stock < quantity) {
168
+ return reply
169
+ .status(409)
170
+ .send({ error: "Insufficient stock", available: inventory.stock });
171
+ }
172
+
173
+ const orderId = `ord-${Date.now()}`;
174
+
175
+ const payment = await paymentCircuit.execute(
176
+ () => withTimeout(() => processPayment(orderId, amount), 10_000, "payment"),
177
+ () => ({ transactionId: `queued-${orderId}` })
178
+ );
179
+
180
+ return {
181
+ orderId,
182
+ productId,
183
+ quantity,
184
+ payment,
185
+ timestamp: new Date().toISOString(),
186
+ };
187
+ }, "high");
188
+
189
+ return reply.status(201).send(result);
190
+ } catch (err: unknown) {
191
+ if (err instanceof Error && err.name === "BulkheadRejectedError") {
192
+ return reply.status(429).send({
193
+ error: "Order system overloaded — try again shortly",
194
+ retryAfter: 5,
195
+ bulkhead: ordersBulkhead.getStatus(),
196
+ });
197
+ }
198
+ throw err;
199
+ }
200
+ }
201
+ );
202
+
203
+ /**
204
+ * GET /health
205
+ * Real-time status of all resilience primitives
206
+ */
207
+ fastify.get("/health", async (_request, reply) => {
208
+ const bulkStatus = ordersBulkhead.getStatus();
209
+ return reply.send({
210
+ status: "healthy",
211
+ uptime: Math.round(process.uptime()),
212
+ memory: {
213
+ heapUsedMb: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
214
+ rssM: Math.round(process.memoryUsage().rss / 1024 / 1024),
215
+ },
216
+ circuits: {
217
+ inventory: inventoryCircuit.getStatus().state,
218
+ payment: paymentCircuit.getStatus().state,
219
+ },
220
+ bulkhead: {
221
+ active: bulkStatus.active,
222
+ queued: bulkStatus.queued,
223
+ },
224
+ });
225
+ });
226
+
227
+ /**
228
+ * GET /metrics
229
+ * Prometheus-compatible plain-text output
230
+ */
231
+ fastify.get("/metrics", async (_request, reply) => {
232
+ const invState = inventoryCircuit.getStatus().state;
233
+ const payState = paymentCircuit.getStatus().state;
234
+ const bk = ordersBulkhead.getStatus();
235
+
236
+ return reply.header("Content-Type", "text/plain; version=0.0.4").send(
237
+ [
238
+ `# HELP immortal_circuit_state Circuit breaker state (1=CLOSED 2=HALF_OPEN 3=OPEN)`,
239
+ `immortal_circuit_state{name="inventory"} ${stateToNum(invState)}`,
240
+ `immortal_circuit_state{name="payment"} ${stateToNum(payState)}`,
241
+ `# HELP immortal_bulkhead_active Active tasks in bulkhead pool`,
242
+ `immortal_bulkhead_active{pool="orders"} ${bk.active}`,
243
+ `# HELP immortal_bulkhead_queued Queued tasks in bulkhead pool`,
244
+ `immortal_bulkhead_queued{pool="orders"} ${bk.queued}`,
245
+ `# HELP process_uptime_seconds Node.js process uptime`,
246
+ `process_uptime_seconds ${Math.round(process.uptime())}`,
247
+ ].join("\n")
248
+ );
249
+ });
250
+
251
+ function stateToNum(state: string): number {
252
+ if (state === "CLOSED") return 1;
253
+ if (state === "HALF_OPEN") return 2;
254
+ return 3; // OPEN
255
+ }
256
+
257
+ /**
258
+ * GET /chaos/trip-inventory
259
+ * Force the inventory circuit breaker OPEN (dev/test only)
260
+ */
261
+ fastify.get("/chaos/trip-inventory", async (_request, reply) => {
262
+ if (process.env["NODE_ENV"] === "production") {
263
+ return reply.status(403).send({ error: "Chaos endpoints disabled in production" });
264
+ }
265
+ inventoryCircuit.forceState("OPEN");
266
+ return reply.send({
267
+ message: "Inventory circuit forced OPEN",
268
+ state: inventoryCircuit.getStatus().state,
269
+ });
270
+ });
271
+
272
+ /**
273
+ * GET /chaos/reset-circuits
274
+ * Reset all circuits to CLOSED
275
+ */
276
+ fastify.get("/chaos/reset-circuits", async (_request, reply) => {
277
+ if (process.env["NODE_ENV"] === "production") {
278
+ return reply.status(403).send({ error: "Chaos endpoints disabled in production" });
279
+ }
280
+ inventoryCircuit.forceState("CLOSED");
281
+ paymentCircuit.forceState("CLOSED");
282
+ return reply.send({
283
+ message: "All circuits reset to CLOSED",
284
+ inventory: inventoryCircuit.getStatus().state,
285
+ payment: paymentCircuit.getStatus().state,
286
+ });
287
+ });
288
+
289
+ // ─── Graceful Shutdown ────────────────────────────────────────────────────────
290
+ // GracefulShutdown(config, bus) — 2 args required
291
+
292
+ const bus = getEventBus();
293
+ const shutdown = new GracefulShutdown(
294
+ {
295
+ timeoutMs: 20_000,
296
+ signals: ["SIGTERM", "SIGINT"],
297
+ onShutdown: [
298
+ async (_signal: string) => {
299
+ fastify.log.info("Draining bulkhead queue…");
300
+ ordersBulkhead.drain();
301
+ },
302
+ async (_signal: string) => {
303
+ fastify.log.info("Closing Fastify server…");
304
+ await fastify.close();
305
+ },
306
+ ],
307
+ },
308
+ bus
309
+ );
310
+
311
+ // Keep reference alive (signal handlers installed in constructor)
312
+ void shutdown;
313
+
314
+ // ─── Start ────────────────────────────────────────────────────────────────────
315
+
316
+ const PORT = parseInt(process.env["PORT"] ?? "3000", 10);
317
+
318
+ try {
319
+ await fastify.listen({ port: PORT, host: "0.0.0.0" });
320
+
321
+ // Banner after successful listen
322
+ console.log(`
323
+ ╔══════════════════════════════════════════════════════════════╗
324
+ ║ Immortal.js — Fastify Microservice 🚀 ║
325
+ ╠══════════════════════════════════════════════════════════════╣
326
+ ║ Server → http://localhost:${PORT} ║
327
+ ║ Health → http://localhost:${PORT}/health ║
328
+ ║ Metrics → http://localhost:${PORT}/metrics ║
329
+ ╠══════════════════════════════════════════════════════════════╣
330
+ ║ Routes ║
331
+ ║ GET /products/:id Circuit Breaker + Cache ║
332
+ ║ POST /orders Bulkhead + Retry + Circuit ║
333
+ ║ GET /health Live health snapshot ║
334
+ ║ GET /metrics Prometheus-compatible metrics ║
335
+ ║ GET /chaos/trip-inventory Force circuit OPEN (dev) ║
336
+ ║ GET /chaos/reset-circuits Reset all circuits (dev) ║
337
+ ╚══════════════════════════════════════════════════════════════╝
338
+ `);
339
+ } catch (err) {
340
+ fastify.log.error(err);
341
+ process.exit(1);
342
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "immortal-fastify-microservice-example",
3
+ "version": "1.0.0",
4
+ "description": "Immortal.js — Fastify microservice example with full resilience patterns",
5
+ "private": true,
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "node --import tsx/esm --no-warnings index.ts",
9
+ "start": "node dist/index.js",
10
+ "build": "tsc"
11
+ },
12
+ "dependencies": {
13
+ "@immortal/core": "*",
14
+ "fastify": "^4.28.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^20.0.0",
18
+ "pino-pretty": "^11.0.0",
19
+ "tsx": "^4.7.0",
20
+ "typescript": "^5.4.0"
21
+ }
22
+ }
@@ -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
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "immortal-invoice-service",
3
+ "version": "3.0.0",
4
+ "description": "Immortal.js — Ultra Pro Invoice PDF Generator with SQLite, Light/Dark theme, Lucide icons",
5
+ "private": true,
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "node --import tsx/esm --no-warnings src/server.ts",
9
+ "build": "tsc",
10
+ "start": "node dist/server.js"
11
+ },
12
+ "dependencies": {
13
+ "@fastify/static": "^7.0.4",
14
+ "@immortal/core": "*",
15
+ "better-sqlite3": "^13.0.1",
16
+ "fastify": "^4.28.0",
17
+ "zod": "^3.22.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/better-sqlite3": "^7.6.8",
21
+ "@types/node": "^20.0.0",
22
+ "tsx": "^4.7.0",
23
+ "typescript": "^5.4.0"
24
+ }
25
+ }