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,561 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file examples/nestjs-enterprise/app.module.ts
|
|
3
|
+
* @description Immortal.js — NestJS Enterprise Root Module (Ultra Pro)
|
|
4
|
+
*
|
|
5
|
+
* ─── DI Strategy ────────────────────────────────────────────────────────────
|
|
6
|
+
* tsx/esm does NOT emit decorator metadata (emitDecoratorMetadata is ignored
|
|
7
|
+
* at runtime), so NestJS cannot resolve constructor parameter types from
|
|
8
|
+
* TypeScript types alone.
|
|
9
|
+
*
|
|
10
|
+
* Fix: Use explicit string injection tokens + @Inject('TOKEN') on every
|
|
11
|
+
* constructor parameter so NestJS never needs reflect-metadata at all.
|
|
12
|
+
*
|
|
13
|
+
* ─── Architecture ───────────────────────────────────────────────────────────
|
|
14
|
+
* IMMORTAL_RUNTIME → singleton ImmortalRuntimeService (all primitives)
|
|
15
|
+
* OrdersService → injected via @Inject('IMMORTAL_RUNTIME')
|
|
16
|
+
* PaymentsService → injected via @Inject('IMMORTAL_RUNTIME')
|
|
17
|
+
* All Controllers → injected via @Inject('ORDERS_SERVICE') etc.
|
|
18
|
+
*
|
|
19
|
+
* Routes:
|
|
20
|
+
* GET / — welcome + route map
|
|
21
|
+
* GET /health — full system health
|
|
22
|
+
* GET /orders — list orders (Bulkhead + Circuit)
|
|
23
|
+
* POST /orders — create order (Retry + Bulkhead)
|
|
24
|
+
* GET /payments/:id — get payment (Circuit + Retry)
|
|
25
|
+
* GET /status/circuits — circuit breaker statuses
|
|
26
|
+
* GET /status/bulkheads — bulkhead pool statuses
|
|
27
|
+
* GET /status/cache — cache stats
|
|
28
|
+
* GET /chaos/trip/:name — force-trip circuit (dev only)
|
|
29
|
+
* GET /chaos/reset — reset all circuits (dev only)
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import "reflect-metadata";
|
|
33
|
+
import {
|
|
34
|
+
Module,
|
|
35
|
+
Controller,
|
|
36
|
+
Get,
|
|
37
|
+
Post,
|
|
38
|
+
Body,
|
|
39
|
+
Param,
|
|
40
|
+
Inject,
|
|
41
|
+
Injectable,
|
|
42
|
+
HttpException,
|
|
43
|
+
HttpStatus,
|
|
44
|
+
type DynamicModule,
|
|
45
|
+
} from "@nestjs/common";
|
|
46
|
+
|
|
47
|
+
import {
|
|
48
|
+
CircuitBreaker,
|
|
49
|
+
BulkheadPool,
|
|
50
|
+
FallbackCache,
|
|
51
|
+
GracefulShutdown,
|
|
52
|
+
MetricsCollector,
|
|
53
|
+
HealthMonitor,
|
|
54
|
+
MemoryLeakGuard,
|
|
55
|
+
ErrorTrap,
|
|
56
|
+
withRetry,
|
|
57
|
+
withTimeout,
|
|
58
|
+
getEventBus,
|
|
59
|
+
mergeWithDefaults,
|
|
60
|
+
} from "@immortal/core";
|
|
61
|
+
|
|
62
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
63
|
+
// INJECTION TOKENS (string-based — works without reflect-metadata)
|
|
64
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export const IMMORTAL_RUNTIME = "IMMORTAL_RUNTIME";
|
|
67
|
+
export const ORDERS_SVC = "ORDERS_SERVICE";
|
|
68
|
+
export const PAYMENTS_SVC = "PAYMENTS_SERVICE";
|
|
69
|
+
|
|
70
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
71
|
+
// DOMAIN TYPES
|
|
72
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
export interface Order {
|
|
75
|
+
id: string;
|
|
76
|
+
productId: string;
|
|
77
|
+
quantity: number;
|
|
78
|
+
amount: number;
|
|
79
|
+
status: "pending" | "confirmed" | "failed";
|
|
80
|
+
createdAt: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface CreateOrderDto {
|
|
84
|
+
productId: string;
|
|
85
|
+
quantity: number;
|
|
86
|
+
amount: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface PaymentResult {
|
|
90
|
+
transactionId: string;
|
|
91
|
+
status: "settled" | "pending" | "failed";
|
|
92
|
+
amount?: number;
|
|
93
|
+
processedAt?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
97
|
+
// IMMORTAL RUNTIME SERVICE
|
|
98
|
+
// All Immortal.js primitives live here.
|
|
99
|
+
// Registered with token IMMORTAL_RUNTIME → no metadata needed.
|
|
100
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
@Injectable()
|
|
103
|
+
export class ImmortalRuntimeService {
|
|
104
|
+
readonly circuitOrders: CircuitBreaker;
|
|
105
|
+
readonly circuitPayment: CircuitBreaker;
|
|
106
|
+
readonly bulkheadOrders: BulkheadPool;
|
|
107
|
+
readonly bulkheadPayment: BulkheadPool;
|
|
108
|
+
readonly cache: FallbackCache<unknown>;
|
|
109
|
+
readonly metrics: MetricsCollector;
|
|
110
|
+
private readonly healthMonitor: HealthMonitor;
|
|
111
|
+
private readonly memoryGuard: MemoryLeakGuard;
|
|
112
|
+
private readonly gracefulShutdown: GracefulShutdown;
|
|
113
|
+
|
|
114
|
+
constructor() {
|
|
115
|
+
const config = mergeWithDefaults({});
|
|
116
|
+
const bus = getEventBus();
|
|
117
|
+
|
|
118
|
+
new ErrorTrap(bus).install();
|
|
119
|
+
|
|
120
|
+
this.circuitOrders = new CircuitBreaker("order-db", {
|
|
121
|
+
threshold: 5, cooldownMs: 15_000, slidingWindowSize: 20,
|
|
122
|
+
});
|
|
123
|
+
this.circuitPayment = new CircuitBreaker("payment-gateway", {
|
|
124
|
+
threshold: 3, cooldownMs: 30_000, slidingWindowSize: 10,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
this.bulkheadOrders = new BulkheadPool("orders", { maxConcurrent: 20, maxQueueSize: 100, queueTimeoutMs: 10_000 });
|
|
128
|
+
this.bulkheadPayment = new BulkheadPool("payments", { maxConcurrent: 10, maxQueueSize: 30, queueTimeoutMs: 15_000 });
|
|
129
|
+
|
|
130
|
+
this.cache = new FallbackCache<unknown>({ maxSize: 500, defaultTtlMs: 30_000 });
|
|
131
|
+
this.metrics = new MetricsCollector();
|
|
132
|
+
|
|
133
|
+
this.healthMonitor = new HealthMonitor(config.healthMonitor, bus, this.metrics);
|
|
134
|
+
this.memoryGuard = new MemoryLeakGuard(
|
|
135
|
+
{ checkIntervalMs: 15_000, restartThresholdMb: 1_500 },
|
|
136
|
+
bus
|
|
137
|
+
);
|
|
138
|
+
this.gracefulShutdown = new GracefulShutdown(
|
|
139
|
+
{ timeoutMs: 25_000, signals: ["SIGTERM", "SIGINT"] },
|
|
140
|
+
bus
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
this.healthMonitor.start();
|
|
144
|
+
this.memoryGuard.start();
|
|
145
|
+
void this.gracefulShutdown;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
getSnapshot() { return this.metrics.getSnapshot(); }
|
|
149
|
+
getAllCircuits() {
|
|
150
|
+
return {
|
|
151
|
+
"order-db": this.circuitOrders.getStatus(),
|
|
152
|
+
"payment-gateway": this.circuitPayment.getStatus(),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
getAllBulkheads() {
|
|
156
|
+
return {
|
|
157
|
+
orders: this.bulkheadOrders.getStatus(),
|
|
158
|
+
payments: this.bulkheadPayment.getStatus(),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
164
|
+
// ORDERS SERVICE
|
|
165
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
@Injectable()
|
|
168
|
+
export class OrdersService {
|
|
169
|
+
constructor(
|
|
170
|
+
@Inject(IMMORTAL_RUNTIME)
|
|
171
|
+
private readonly runtime: ImmortalRuntimeService
|
|
172
|
+
) {}
|
|
173
|
+
|
|
174
|
+
async findAll(): Promise<{ orders: Order[]; count: number }> {
|
|
175
|
+
return this.runtime.bulkheadOrders.run(
|
|
176
|
+
() =>
|
|
177
|
+
this.runtime.circuitOrders.execute(
|
|
178
|
+
async () => {
|
|
179
|
+
await sleep(20 + rnd(80));
|
|
180
|
+
if (Math.random() < 0.04) throw new Error("DB read timeout");
|
|
181
|
+
return {
|
|
182
|
+
orders: [
|
|
183
|
+
makeOrder("ord-0001", "SKU-ALPHA", 2, 49.99),
|
|
184
|
+
makeOrder("ord-0002", "SKU-BETA", 1, 99.99),
|
|
185
|
+
makeOrder("ord-0003", "SKU-GAMMA", 5, 9.99),
|
|
186
|
+
makeOrder("ord-0004", "SKU-DELTA", 3, 199.99),
|
|
187
|
+
],
|
|
188
|
+
count: 4,
|
|
189
|
+
};
|
|
190
|
+
},
|
|
191
|
+
() => ({ orders: [] as Order[], count: 0 })
|
|
192
|
+
),
|
|
193
|
+
"medium"
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async create(dto: CreateOrderDto): Promise<Order> {
|
|
198
|
+
return withRetry(
|
|
199
|
+
() =>
|
|
200
|
+
this.runtime.bulkheadOrders.run(async () => {
|
|
201
|
+
await sleep(50 + rnd(100));
|
|
202
|
+
if (Math.random() < 0.06) throw new Error("Transient write error");
|
|
203
|
+
const order = makeOrder(
|
|
204
|
+
`ord-${Date.now()}`,
|
|
205
|
+
dto.productId,
|
|
206
|
+
dto.quantity,
|
|
207
|
+
dto.amount
|
|
208
|
+
);
|
|
209
|
+
this.runtime.cache.set(`order:${order.id}`, order);
|
|
210
|
+
return order;
|
|
211
|
+
}, "high"),
|
|
212
|
+
{ maxAttempts: 3, baseDelayMs: 200, maxDelayMs: 2_000 }
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async checkInventory(productId: string): Promise<{ stock: number }> {
|
|
217
|
+
return withTimeout(
|
|
218
|
+
() =>
|
|
219
|
+
this.runtime.circuitOrders.execute(
|
|
220
|
+
async () => {
|
|
221
|
+
await sleep(20 + rnd(60));
|
|
222
|
+
if (Math.random() < 0.08) throw new Error("Inventory service unavailable");
|
|
223
|
+
return { stock: Math.floor(Math.random() * 200) + 1 };
|
|
224
|
+
},
|
|
225
|
+
() => ({ stock: 0 })
|
|
226
|
+
),
|
|
227
|
+
5_000,
|
|
228
|
+
`inventory-check-${productId}`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
234
|
+
// PAYMENTS SERVICE
|
|
235
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
@Injectable()
|
|
238
|
+
export class PaymentsService {
|
|
239
|
+
constructor(
|
|
240
|
+
@Inject(IMMORTAL_RUNTIME)
|
|
241
|
+
private readonly runtime: ImmortalRuntimeService
|
|
242
|
+
) {}
|
|
243
|
+
|
|
244
|
+
async getPayment(id: string): Promise<PaymentResult> {
|
|
245
|
+
const fallback = (): PaymentResult => ({
|
|
246
|
+
transactionId: `queued-${Date.now()}`,
|
|
247
|
+
status: "pending" as const,
|
|
248
|
+
});
|
|
249
|
+
return this.runtime.circuitPayment.execute(
|
|
250
|
+
() =>
|
|
251
|
+
withRetry(
|
|
252
|
+
async (): Promise<PaymentResult> => {
|
|
253
|
+
await sleep(100 + rnd(200));
|
|
254
|
+
if (Math.random() < 0.08) throw new Error("Payment gateway timeout");
|
|
255
|
+
return {
|
|
256
|
+
transactionId: id,
|
|
257
|
+
status: "settled" as const,
|
|
258
|
+
amount: Math.round(Math.random() * 10_000) / 100,
|
|
259
|
+
processedAt: new Date().toISOString(),
|
|
260
|
+
};
|
|
261
|
+
},
|
|
262
|
+
{ maxAttempts: 2, baseDelayMs: 500, maxDelayMs: 3_000 }
|
|
263
|
+
),
|
|
264
|
+
fallback
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async charge(orderId: string, amount: number): Promise<PaymentResult> {
|
|
269
|
+
const fallback = (): PaymentResult => ({
|
|
270
|
+
transactionId: `queued-${orderId}`,
|
|
271
|
+
status: "pending" as const,
|
|
272
|
+
});
|
|
273
|
+
return this.runtime.bulkheadPayment.run(
|
|
274
|
+
() =>
|
|
275
|
+
this.runtime.circuitPayment.execute(
|
|
276
|
+
() =>
|
|
277
|
+
withRetry(
|
|
278
|
+
() =>
|
|
279
|
+
withTimeout(
|
|
280
|
+
async (): Promise<PaymentResult> => {
|
|
281
|
+
await sleep(100 + rnd(200));
|
|
282
|
+
if (Math.random() < 0.05) throw new Error("Charge failed");
|
|
283
|
+
return {
|
|
284
|
+
transactionId: `txn-${orderId}-${Date.now()}`,
|
|
285
|
+
status: "settled" as const,
|
|
286
|
+
amount,
|
|
287
|
+
processedAt: new Date().toISOString(),
|
|
288
|
+
};
|
|
289
|
+
},
|
|
290
|
+
10_000,
|
|
291
|
+
"payment-charge"
|
|
292
|
+
),
|
|
293
|
+
{ maxAttempts: 2, baseDelayMs: 300 }
|
|
294
|
+
),
|
|
295
|
+
fallback
|
|
296
|
+
),
|
|
297
|
+
"high"
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
303
|
+
// CONTROLLERS
|
|
304
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
/** Root controller — GET / returns a route map */
|
|
307
|
+
@Controller()
|
|
308
|
+
export class AppController {
|
|
309
|
+
@Get()
|
|
310
|
+
index() {
|
|
311
|
+
return {
|
|
312
|
+
name: "Immortal.js NestJS Enterprise API",
|
|
313
|
+
version: "1.0.0",
|
|
314
|
+
status: "running",
|
|
315
|
+
timestamp: new Date().toISOString(),
|
|
316
|
+
routes: {
|
|
317
|
+
"GET /": "This route map",
|
|
318
|
+
"GET /health": "Full system health snapshot",
|
|
319
|
+
"GET /orders": "List orders [Bulkhead + Circuit]",
|
|
320
|
+
"POST /orders": "Create order [Retry + Bulkhead]",
|
|
321
|
+
"GET /payments/:id": "Get payment [Circuit + Retry]",
|
|
322
|
+
"GET /status/circuits": "All circuit breaker statuses",
|
|
323
|
+
"GET /status/bulkheads": "All bulkhead pool statuses",
|
|
324
|
+
"GET /status/cache": "Cache stats",
|
|
325
|
+
"GET /chaos/trip/:name": "Force-trip a circuit (dev only)",
|
|
326
|
+
"GET /chaos/reset": "Reset all circuits (dev only)",
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
@Controller("orders")
|
|
333
|
+
export class OrdersController {
|
|
334
|
+
constructor(
|
|
335
|
+
@Inject(ORDERS_SVC)
|
|
336
|
+
private readonly ordersService: OrdersService
|
|
337
|
+
) {}
|
|
338
|
+
|
|
339
|
+
/** GET /orders */
|
|
340
|
+
@Get()
|
|
341
|
+
findAll() {
|
|
342
|
+
return this.ordersService.findAll();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** POST /orders Body: { productId, quantity, amount } */
|
|
346
|
+
@Post()
|
|
347
|
+
async create(@Body() dto: CreateOrderDto) {
|
|
348
|
+
if (!dto.productId?.trim())
|
|
349
|
+
throw new HttpException("productId is required", HttpStatus.BAD_REQUEST);
|
|
350
|
+
if (!dto.quantity || dto.quantity < 1)
|
|
351
|
+
throw new HttpException("quantity must be ≥ 1", HttpStatus.BAD_REQUEST);
|
|
352
|
+
if (!dto.amount || dto.amount <= 0)
|
|
353
|
+
throw new HttpException("amount must be > 0", HttpStatus.BAD_REQUEST);
|
|
354
|
+
return this.ordersService.create(dto);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
@Controller("payments")
|
|
359
|
+
export class PaymentsController {
|
|
360
|
+
constructor(
|
|
361
|
+
@Inject(PAYMENTS_SVC)
|
|
362
|
+
private readonly paymentsService: PaymentsService
|
|
363
|
+
) {}
|
|
364
|
+
|
|
365
|
+
/** GET /payments/:id */
|
|
366
|
+
@Get(":id")
|
|
367
|
+
getPayment(@Param("id") id: string) {
|
|
368
|
+
return this.paymentsService.getPayment(id);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
@Controller("health")
|
|
373
|
+
export class HealthController {
|
|
374
|
+
constructor(
|
|
375
|
+
@Inject(IMMORTAL_RUNTIME)
|
|
376
|
+
private readonly runtime: ImmortalRuntimeService
|
|
377
|
+
) {}
|
|
378
|
+
|
|
379
|
+
@Get()
|
|
380
|
+
getHealth() {
|
|
381
|
+
const snap = this.runtime.getSnapshot();
|
|
382
|
+
const bk = this.runtime.getAllBulkheads();
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
status: "healthy",
|
|
386
|
+
uptime: Math.round(process.uptime()),
|
|
387
|
+
memoryMb: {
|
|
388
|
+
heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
|
389
|
+
rss: Math.round(process.memoryUsage().rss / 1024 / 1024),
|
|
390
|
+
},
|
|
391
|
+
eventLoop: snap.eventLoopLag,
|
|
392
|
+
circuits: this.runtime.getAllCircuits(),
|
|
393
|
+
bulkheads: {
|
|
394
|
+
orders: { active: bk.orders.active, queued: bk.orders.queued },
|
|
395
|
+
payments: { active: bk.payments.active, queued: bk.payments.queued },
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
@Controller("status")
|
|
402
|
+
export class StatusController {
|
|
403
|
+
constructor(
|
|
404
|
+
@Inject(IMMORTAL_RUNTIME)
|
|
405
|
+
private readonly runtime: ImmortalRuntimeService
|
|
406
|
+
) {}
|
|
407
|
+
|
|
408
|
+
@Get("circuits")
|
|
409
|
+
circuits() { return this.runtime.getAllCircuits(); }
|
|
410
|
+
|
|
411
|
+
@Get("bulkheads")
|
|
412
|
+
bulkheads() { return this.runtime.getAllBulkheads(); }
|
|
413
|
+
|
|
414
|
+
@Get("cache")
|
|
415
|
+
cacheStats() {
|
|
416
|
+
const snap = this.runtime.getSnapshot();
|
|
417
|
+
return {
|
|
418
|
+
eventLoopLag: snap.eventLoopLag,
|
|
419
|
+
memory: process.memoryUsage(),
|
|
420
|
+
uptime: process.uptime(),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
@Controller("chaos")
|
|
426
|
+
export class ChaosController {
|
|
427
|
+
constructor(
|
|
428
|
+
@Inject(IMMORTAL_RUNTIME)
|
|
429
|
+
private readonly runtime: ImmortalRuntimeService
|
|
430
|
+
) {}
|
|
431
|
+
|
|
432
|
+
@Get("trip/:name")
|
|
433
|
+
trip(@Param("name") name: string) {
|
|
434
|
+
if (process.env["NODE_ENV"] === "production")
|
|
435
|
+
throw new HttpException("Chaos disabled in production", HttpStatus.FORBIDDEN);
|
|
436
|
+
|
|
437
|
+
const circuits: Record<string, CircuitBreaker> = {
|
|
438
|
+
"order-db": this.runtime.circuitOrders,
|
|
439
|
+
"payment-gateway": this.runtime.circuitPayment,
|
|
440
|
+
};
|
|
441
|
+
const cb = circuits[name];
|
|
442
|
+
if (!cb)
|
|
443
|
+
throw new HttpException(
|
|
444
|
+
`Unknown circuit '${name}'. Known: ${Object.keys(circuits).join(", ")}`,
|
|
445
|
+
HttpStatus.NOT_FOUND
|
|
446
|
+
);
|
|
447
|
+
cb.forceState("OPEN");
|
|
448
|
+
return { message: `Circuit '${name}' forced OPEN`, state: cb.getStatus().state };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
@Get("reset")
|
|
452
|
+
reset() {
|
|
453
|
+
if (process.env["NODE_ENV"] === "production")
|
|
454
|
+
throw new HttpException("Chaos disabled in production", HttpStatus.FORBIDDEN);
|
|
455
|
+
this.runtime.circuitOrders.forceState("CLOSED");
|
|
456
|
+
this.runtime.circuitPayment.forceState("CLOSED");
|
|
457
|
+
return {
|
|
458
|
+
message: "All circuits reset to CLOSED",
|
|
459
|
+
circuits: this.runtime.getAllCircuits(),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
465
|
+
// IMMORTAL CORE MODULE
|
|
466
|
+
// Provides ImmortalRuntimeService, OrdersService, PaymentsService.
|
|
467
|
+
// All providers use explicit string tokens + useFactory so NestJS never
|
|
468
|
+
// needs to resolve types from reflect-metadata.
|
|
469
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
470
|
+
|
|
471
|
+
@Module({
|
|
472
|
+
providers: [
|
|
473
|
+
// ── Singleton runtime (all Immortal.js primitives) ─────────────────────
|
|
474
|
+
{
|
|
475
|
+
provide: IMMORTAL_RUNTIME,
|
|
476
|
+
useFactory: () => new ImmortalRuntimeService(),
|
|
477
|
+
},
|
|
478
|
+
// ── Domain services (explicitly injected via factory) ──────────────────
|
|
479
|
+
{
|
|
480
|
+
provide: ORDERS_SVC,
|
|
481
|
+
useFactory: (runtime: ImmortalRuntimeService) => new OrdersService(runtime),
|
|
482
|
+
inject: [IMMORTAL_RUNTIME],
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
provide: PAYMENTS_SVC,
|
|
486
|
+
useFactory: (runtime: ImmortalRuntimeService) => new PaymentsService(runtime),
|
|
487
|
+
inject: [IMMORTAL_RUNTIME],
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
exports: [IMMORTAL_RUNTIME, ORDERS_SVC, PAYMENTS_SVC],
|
|
491
|
+
})
|
|
492
|
+
export class ImmortalCoreModule {
|
|
493
|
+
static forRoot(): DynamicModule {
|
|
494
|
+
return {
|
|
495
|
+
module: ImmortalCoreModule,
|
|
496
|
+
global: true,
|
|
497
|
+
providers: [
|
|
498
|
+
{
|
|
499
|
+
provide: IMMORTAL_RUNTIME,
|
|
500
|
+
useFactory: () => new ImmortalRuntimeService(),
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
provide: ORDERS_SVC,
|
|
504
|
+
useFactory: (runtime: ImmortalRuntimeService) => new OrdersService(runtime),
|
|
505
|
+
inject: [IMMORTAL_RUNTIME],
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
provide: PAYMENTS_SVC,
|
|
509
|
+
useFactory: (runtime: ImmortalRuntimeService) => new PaymentsService(runtime),
|
|
510
|
+
inject: [IMMORTAL_RUNTIME],
|
|
511
|
+
},
|
|
512
|
+
],
|
|
513
|
+
exports: [IMMORTAL_RUNTIME, ORDERS_SVC, PAYMENTS_SVC],
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
519
|
+
// ROOT MODULE
|
|
520
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
521
|
+
|
|
522
|
+
@Module({
|
|
523
|
+
imports: [ImmortalCoreModule.forRoot()],
|
|
524
|
+
controllers: [
|
|
525
|
+
AppController,
|
|
526
|
+
OrdersController,
|
|
527
|
+
PaymentsController,
|
|
528
|
+
HealthController,
|
|
529
|
+
StatusController,
|
|
530
|
+
ChaosController,
|
|
531
|
+
],
|
|
532
|
+
})
|
|
533
|
+
export class AppModule {}
|
|
534
|
+
|
|
535
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
536
|
+
// UTILITIES
|
|
537
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
538
|
+
|
|
539
|
+
function sleep(ms: number): Promise<void> {
|
|
540
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function rnd(max: number): number {
|
|
544
|
+
return Math.random() * max;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function makeOrder(
|
|
548
|
+
id: string,
|
|
549
|
+
productId: string,
|
|
550
|
+
quantity: number,
|
|
551
|
+
amount: number
|
|
552
|
+
): Order {
|
|
553
|
+
return {
|
|
554
|
+
id,
|
|
555
|
+
productId,
|
|
556
|
+
quantity,
|
|
557
|
+
amount,
|
|
558
|
+
status: "confirmed",
|
|
559
|
+
createdAt: new Date().toISOString(),
|
|
560
|
+
};
|
|
561
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file examples/nestjs-enterprise/main.ts
|
|
3
|
+
* @description Immortal.js — NestJS Enterprise Bootstrap
|
|
4
|
+
*
|
|
5
|
+
* Run: npm run dev (tsx ESM — Windows/Linux/macOS)
|
|
6
|
+
* Build: npm run build
|
|
7
|
+
* Start: PORT=3000 npm start
|
|
8
|
+
*
|
|
9
|
+
* DI Note:
|
|
10
|
+
* tsx ESM does NOT emit decorator metadata even with emitDecoratorMetadata:true.
|
|
11
|
+
* All injection uses string tokens + @Inject('TOKEN') — no metadata needed.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import "reflect-metadata";
|
|
15
|
+
import { NestFactory } from "@nestjs/core";
|
|
16
|
+
import { AppModule } from "./app.module.js";
|
|
17
|
+
|
|
18
|
+
async function bootstrap(): Promise<void> {
|
|
19
|
+
const app = await NestFactory.create(AppModule, {
|
|
20
|
+
logger: ["log", "error", "warn"],
|
|
21
|
+
bodyParser: true,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
app.enableCors({
|
|
25
|
+
origin: "*",
|
|
26
|
+
methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
|
|
27
|
+
allowedHeaders: ["Content-Type", "Authorization"],
|
|
28
|
+
});
|
|
29
|
+
app.enableShutdownHooks();
|
|
30
|
+
|
|
31
|
+
const PORT = parseInt(process.env["PORT"] ?? "3000", 10);
|
|
32
|
+
await app.listen(PORT, "0.0.0.0");
|
|
33
|
+
|
|
34
|
+
console.log(`
|
|
35
|
+
╔══════════════════════════════════════════════════════════════════╗
|
|
36
|
+
║ Immortal.js — NestJS Enterprise Example 🚀 Ultra Pro ║
|
|
37
|
+
╠══════════════════════════════════════════════════════════════════╣
|
|
38
|
+
║ Server → http://localhost:${PORT}/ ║
|
|
39
|
+
╠══════════════════════════════════════════════════════════════════╣
|
|
40
|
+
║ Routes ║
|
|
41
|
+
║ GET / Route map ║
|
|
42
|
+
║ GET /health System health snapshot ║
|
|
43
|
+
║ GET /orders List orders [Bulkhead + Circuit] ║
|
|
44
|
+
║ POST /orders Create order [Retry + Bulkhead] ║
|
|
45
|
+
║ GET /payments/:id Get payment [Circuit + Retry] ║
|
|
46
|
+
║ GET /status/circuits Circuit breaker statuses ║
|
|
47
|
+
║ GET /status/bulkheads Bulkhead pool statuses ║
|
|
48
|
+
║ GET /chaos/trip/:name Force-trip circuit (dev only) ║
|
|
49
|
+
║ GET /chaos/reset Reset all circuits (dev only) ║
|
|
50
|
+
╠══════════════════════════════════════════════════════════════════╣
|
|
51
|
+
║ Immortal.js Resilience Patterns ║
|
|
52
|
+
║ ● CircuitBreaker order-db, payment-gateway ║
|
|
53
|
+
║ ● BulkheadPool orders (max 20), payments (max 10) ║
|
|
54
|
+
║ ● withRetry exponential back-off on transient errors ║
|
|
55
|
+
║ ● withTimeout hard deadline on external ops (5s / 10s) ║
|
|
56
|
+
║ ● GracefulShutdown SIGTERM/SIGINT → drain → close ║
|
|
57
|
+
║ ● MemoryLeakGuard restart at 1500 MB heap ║
|
|
58
|
+
╠══════════════════════════════════════════════════════════════════╣
|
|
59
|
+
║ DI Strategy: string tokens + @Inject() — no reflect-metadata ║
|
|
60
|
+
╚══════════════════════════════════════════════════════════════════╝
|
|
61
|
+
`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
bootstrap().catch((err: unknown) => {
|
|
65
|
+
console.error("Fatal startup error:", err);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "immortal-nestjs-enterprise-example",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Immortal.js — NestJS enterprise example with DI decorators, interceptors, and resilience",
|
|
5
|
+
"private": true,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "node --import tsx/esm --no-warnings main.ts",
|
|
9
|
+
"start": "node dist/main.js",
|
|
10
|
+
"build": "tsc"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@immortal/core": "*",
|
|
14
|
+
"@immortal/nestjs": "*",
|
|
15
|
+
"@nestjs/common": "^10.3.0",
|
|
16
|
+
"@nestjs/core": "^10.3.0",
|
|
17
|
+
"@nestjs/platform-express": "^10.3.0",
|
|
18
|
+
"reflect-metadata": "^0.2.1",
|
|
19
|
+
"rxjs": "^7.8.1"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^20.0.0",
|
|
23
|
+
"tsx": "^4.7.0",
|
|
24
|
+
"typescript": "^5.4.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
"experimentalDecorators": true,
|
|
14
|
+
"emitDecoratorMetadata": true,
|
|
15
|
+
"declaration": true,
|
|
16
|
+
"declarationMap": true,
|
|
17
|
+
"sourceMap": true,
|
|
18
|
+
"esModuleInterop": true,
|
|
19
|
+
"allowSyntheticDefaultImports": true,
|
|
20
|
+
"forceConsistentCasingInFileNames": true,
|
|
21
|
+
"skipLibCheck": true,
|
|
22
|
+
"outDir": "dist",
|
|
23
|
+
"rootDir": "./"
|
|
24
|
+
},
|
|
25
|
+
"include": ["main.ts", "app.module.ts"],
|
|
26
|
+
"exclude": ["node_modules", "dist"]
|
|
27
|
+
}
|
|
Binary file
|