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,285 @@
|
|
|
1
|
+
# API Reference — Supervision (Layer 4)
|
|
2
|
+
|
|
3
|
+
The **Supervision** layer manages long-running processes — both in-process async workers and multi-core OS processes — with automatic restart policies and escalation.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Supervisor
|
|
8
|
+
|
|
9
|
+
Manages named async workers (in-process) with configurable restart limits.
|
|
10
|
+
|
|
11
|
+
### `new Supervisor(options?)`
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { Supervisor } from '@immortal/core';
|
|
15
|
+
|
|
16
|
+
const supervisor = new Supervisor({
|
|
17
|
+
config: {
|
|
18
|
+
maxRestartsInWindow: 5,
|
|
19
|
+
windowMs: 60_000,
|
|
20
|
+
baseRestartDelayMs: 1_000,
|
|
21
|
+
},
|
|
22
|
+
onEscalation: (workerId) => {
|
|
23
|
+
alertTeam(`Worker "${workerId}" escalated — manual intervention required`);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Options:**
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
interface SupervisorOptions {
|
|
32
|
+
config?: Partial<SupervisorConfig>;
|
|
33
|
+
onEscalation?: (workerId: string) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface SupervisorConfig {
|
|
37
|
+
maxRestartsInWindow: number; // default: 5
|
|
38
|
+
windowMs: number; // default: 60_000
|
|
39
|
+
baseRestartDelayMs: number; // default: 1_000
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
### `supervisor.register(id, entry)`
|
|
46
|
+
|
|
47
|
+
Register and immediately start a worker.
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
supervisor.register('http-server', {
|
|
51
|
+
spawnFn: () => startHttpServer(), // returns the worker instance
|
|
52
|
+
terminate: (instance) => instance.close(), // called before restart or on stop
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Entry:**
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
interface WorkerEntry {
|
|
60
|
+
spawnFn: () => unknown | Promise<unknown>;
|
|
61
|
+
terminate?: (instance: unknown) => void | Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`spawnFn` is called immediately. The returned instance (if any) is passed to `terminate` before each restart.
|
|
66
|
+
|
|
67
|
+
**Throws** if the same `id` is registered twice.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
### `supervisor.onWorkerExit(id, exitCode?)`
|
|
72
|
+
|
|
73
|
+
Signal that a worker has exited (used by your own process management code to trigger the restart logic).
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// Example: worker thread exited
|
|
77
|
+
worker.on('exit', (code) => {
|
|
78
|
+
supervisor.onWorkerExit('my-worker', code);
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The supervisor will schedule a restart after `baseRestartDelayMs * 2^(restartCount)` ms (exponential backoff), unless `maxRestartsInWindow` has been reached.
|
|
83
|
+
|
|
84
|
+
When the restart limit is reached, the worker state is set to `'escalated'`, no further restarts occur, and `onEscalation` is called.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### `supervisor.updateWorkerMetrics(id, metrics)`
|
|
89
|
+
|
|
90
|
+
Push current resource metrics for a worker (used by the Monitoring layer).
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
supervisor.updateWorkerMetrics('http-server', {
|
|
94
|
+
memoryMb: 256,
|
|
95
|
+
cpuPercent: 12.5,
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
### `supervisor.requestGracefulRestart(id, reason?)`
|
|
102
|
+
|
|
103
|
+
Trigger a graceful restart for a specific worker: calls `terminate`, then re-spawns.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
await supervisor.requestGracefulRestart('http-server', 'memory-threshold');
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### `supervisor.getWorkerStatuses()`
|
|
112
|
+
|
|
113
|
+
Returns the current state of all registered workers.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const statuses = supervisor.getWorkerStatuses();
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Returns:** `WorkerStatus[]`
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
interface WorkerStatus {
|
|
123
|
+
id: string;
|
|
124
|
+
state: 'running' | 'restarting' | 'escalated' | 'stopped';
|
|
125
|
+
restartCount: number;
|
|
126
|
+
uptime: number; // ms since last (re)start
|
|
127
|
+
pid?: number; // OS PID if available
|
|
128
|
+
lastRestartTime?: number; // epoch ms of last restart
|
|
129
|
+
memoryMb?: number;
|
|
130
|
+
cpuPercent?: number;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
### `supervisor.stop(id)`
|
|
137
|
+
|
|
138
|
+
Stop a specific worker (calls `terminate`, removes from registry).
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
await supervisor.stop('http-server');
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
### `supervisor.stopAll()`
|
|
147
|
+
|
|
148
|
+
Stop all registered workers.
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
await supervisor.stopAll();
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## computeRestartDecision
|
|
157
|
+
|
|
158
|
+
Low-level utility used internally by `Supervisor`. Determines which workers to restart based on a `RestartStrategy`.
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { computeRestartDecision } from '@immortal/core';
|
|
162
|
+
|
|
163
|
+
const decision = computeRestartDecision(
|
|
164
|
+
'worker-1', // failed worker ID
|
|
165
|
+
allWorkers, // WorkerEntry[]
|
|
166
|
+
'one-for-one' // strategy
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// decision.workersToRestart → string[] of worker IDs to restart
|
|
170
|
+
// decision.strategy → the strategy used
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Strategies:**
|
|
174
|
+
|
|
175
|
+
| Strategy | Description |
|
|
176
|
+
|----------|-------------|
|
|
177
|
+
| `'one-for-one'` | Restart only the failed worker (default) |
|
|
178
|
+
| `'one-for-all'` | Restart all workers when one fails |
|
|
179
|
+
| `'rest-for-one'` | Restart the failed worker and all workers registered after it |
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## ClusterManager
|
|
184
|
+
|
|
185
|
+
Manages OS-level worker processes using the Node.js `cluster` module.
|
|
186
|
+
|
|
187
|
+
### `ClusterManager.isPrimary()`
|
|
188
|
+
|
|
189
|
+
Static method — returns `true` if the current process is the cluster primary.
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import { ClusterManager } from '@immortal/core';
|
|
193
|
+
|
|
194
|
+
if (ClusterManager.isPrimary()) {
|
|
195
|
+
// primary: fork workers
|
|
196
|
+
} else {
|
|
197
|
+
// worker: start app
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### `new ClusterManager(options?)`
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
const mgr = new ClusterManager({
|
|
205
|
+
workerCount: 4, // default: os.cpus().length
|
|
206
|
+
supervisorConfig: {
|
|
207
|
+
maxRestartsInWindow: 3,
|
|
208
|
+
windowMs: 30_000,
|
|
209
|
+
baseRestartDelayMs: 500,
|
|
210
|
+
},
|
|
211
|
+
onWorkerExit: (id, code, signal) => {
|
|
212
|
+
console.log(`Worker ${id} exited: code=${code} signal=${signal}`);
|
|
213
|
+
},
|
|
214
|
+
onEscalation: (id) => {
|
|
215
|
+
alertTeam(`Cluster worker ${id} escalated`);
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### `mgr.start()`
|
|
221
|
+
|
|
222
|
+
Fork `workerCount` workers and attach exit/online handlers. Must be called in the primary process.
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
await mgr.start();
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### `mgr.rollingRestart()`
|
|
229
|
+
|
|
230
|
+
Zero-downtime rolling restart: fork new worker → wait for online → kill old worker → repeat.
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
await mgr.rollingRestart();
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### `mgr.stop()`
|
|
237
|
+
|
|
238
|
+
Send `SIGTERM` to all workers.
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
await mgr.stop();
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### `mgr.getWorkerStatuses()`
|
|
245
|
+
|
|
246
|
+
Returns `WorkerStatus[]` for all cluster workers.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Typical patterns
|
|
251
|
+
|
|
252
|
+
### Primary/Worker split
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
import { ClusterManager } from '@immortal/core';
|
|
256
|
+
|
|
257
|
+
if (ClusterManager.isPrimary()) {
|
|
258
|
+
const mgr = new ClusterManager({ workerCount: os.cpus().length });
|
|
259
|
+
await mgr.start();
|
|
260
|
+
|
|
261
|
+
// Schedule rolling restart every 24h
|
|
262
|
+
setInterval(() => mgr.rollingRestart(), 24 * 60 * 60_000).unref();
|
|
263
|
+
} else {
|
|
264
|
+
// Each worker starts its own HTTP server
|
|
265
|
+
const app = express();
|
|
266
|
+
app.listen(3000);
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### In-process supervisor
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
const supervisor = new Supervisor({ onEscalation: alertTeam });
|
|
274
|
+
|
|
275
|
+
supervisor.register('message-consumer', {
|
|
276
|
+
spawnFn: () => {
|
|
277
|
+
const consumer = new KafkaConsumer(config);
|
|
278
|
+
consumer.on('error', (err) => {
|
|
279
|
+
supervisor.onWorkerExit('message-consumer', 1);
|
|
280
|
+
});
|
|
281
|
+
return consumer;
|
|
282
|
+
},
|
|
283
|
+
terminate: (consumer) => consumer.disconnect(),
|
|
284
|
+
});
|
|
285
|
+
```
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Configuration Guide
|
|
2
|
+
|
|
3
|
+
Immortal.js is configured through a single `ImmortalConfig` object passed to `createImmortal()`. All fields are optional — sensible defaults are applied for everything.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Full configuration reference
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { createImmortal } from '@immortal/core';
|
|
11
|
+
|
|
12
|
+
const immortal = await createImmortal({
|
|
13
|
+
// ─────────────────────────────────────────────────────────
|
|
14
|
+
// Logger
|
|
15
|
+
// ─────────────────────────────────────────────────────────
|
|
16
|
+
logger: {
|
|
17
|
+
level: 'info', // 'debug' | 'info' | 'warn' | 'error' | 'silent'
|
|
18
|
+
transport: 'json', // 'console' | 'json' | 'pretty'
|
|
19
|
+
prefix: 'my-service', // prepended to all log lines
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
// ─────────────────────────────────────────────────────────
|
|
23
|
+
// Health Monitor (Layer 5)
|
|
24
|
+
// ─────────────────────────────────────────────────────────
|
|
25
|
+
healthMonitor: {
|
|
26
|
+
enabled: true,
|
|
27
|
+
checkIntervalMs: 30_000, // how often to check
|
|
28
|
+
eventLoopLagWarningMs: 50, // warn threshold
|
|
29
|
+
eventLoopLagCriticalMs: 100, // critical threshold
|
|
30
|
+
heapUsageWarningPercent: 80,
|
|
31
|
+
heapUsageCriticalPercent: 90,
|
|
32
|
+
cpuWarningPercent: 80,
|
|
33
|
+
cpuCriticalPercent: 95,
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
// ─────────────────────────────────────────────────────────
|
|
37
|
+
// Memory Leak Guard (Layer 5)
|
|
38
|
+
// ─────────────────────────────────────────────────────────
|
|
39
|
+
memoryGuard: {
|
|
40
|
+
enabled: true,
|
|
41
|
+
checkIntervalMs: 60_000,
|
|
42
|
+
warningThresholdMb: 400, // heap MB before warning event
|
|
43
|
+
restartThresholdMb: 600, // heap MB before restart event
|
|
44
|
+
growthWindowCount: 5, // samples to confirm growth trend
|
|
45
|
+
adaptive: true, // adjust thresholds based on baseline
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// ─────────────────────────────────────────────────────────
|
|
49
|
+
// Graceful Shutdown (Lifecycle)
|
|
50
|
+
// ─────────────────────────────────────────────────────────
|
|
51
|
+
gracefulShutdown: {
|
|
52
|
+
enabled: true,
|
|
53
|
+
timeoutMs: 30_000,
|
|
54
|
+
signals: ['SIGTERM', 'SIGINT'],
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// ─────────────────────────────────────────────────────────
|
|
58
|
+
// Supervisor (Layer 4)
|
|
59
|
+
// ─────────────────────────────────────────────────────────
|
|
60
|
+
supervisor: {
|
|
61
|
+
maxRestartsInWindow: 5,
|
|
62
|
+
windowMs: 60_000,
|
|
63
|
+
baseRestartDelayMs: 1_000,
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// ─────────────────────────────────────────────────────────
|
|
67
|
+
// Chaos Engineering
|
|
68
|
+
// ─────────────────────────────────────────────────────────
|
|
69
|
+
chaos: {
|
|
70
|
+
enabled: false, // NEVER enable in production — runtime enforces this
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
// ─────────────────────────────────────────────────────────
|
|
74
|
+
// Plugins
|
|
75
|
+
// ─────────────────────────────────────────────────────────
|
|
76
|
+
plugins: [],
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Environment-based configuration
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { createImmortal } from '@immortal/core';
|
|
86
|
+
|
|
87
|
+
const isProd = process.env.NODE_ENV === 'production';
|
|
88
|
+
|
|
89
|
+
const immortal = await createImmortal({
|
|
90
|
+
logger: {
|
|
91
|
+
level: isProd ? 'warn' : 'debug',
|
|
92
|
+
transport: isProd ? 'json' : 'pretty',
|
|
93
|
+
},
|
|
94
|
+
healthMonitor: {
|
|
95
|
+
enabled: true,
|
|
96
|
+
checkIntervalMs: isProd ? 30_000 : 60_000,
|
|
97
|
+
},
|
|
98
|
+
memoryGuard: {
|
|
99
|
+
enabled: isProd,
|
|
100
|
+
warningThresholdMb: parseInt(process.env.MEMORY_WARNING_MB ?? '400'),
|
|
101
|
+
restartThresholdMb: parseInt(process.env.MEMORY_RESTART_MB ?? '600'),
|
|
102
|
+
},
|
|
103
|
+
gracefulShutdown: {
|
|
104
|
+
enabled: true,
|
|
105
|
+
timeoutMs: parseInt(process.env.SHUTDOWN_TIMEOUT_MS ?? '30000'),
|
|
106
|
+
},
|
|
107
|
+
chaos: {
|
|
108
|
+
enabled: process.env.CHAOS_ENABLED === 'true',
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Default values
|
|
116
|
+
|
|
117
|
+
| Config key | Default |
|
|
118
|
+
|------------|---------|
|
|
119
|
+
| `logger.level` | `'info'` |
|
|
120
|
+
| `logger.transport` | `'console'` |
|
|
121
|
+
| `healthMonitor.enabled` | `true` |
|
|
122
|
+
| `healthMonitor.checkIntervalMs` | `30_000` |
|
|
123
|
+
| `healthMonitor.eventLoopLagWarningMs` | `50` |
|
|
124
|
+
| `healthMonitor.eventLoopLagCriticalMs` | `100` |
|
|
125
|
+
| `memoryGuard.enabled` | `true` |
|
|
126
|
+
| `memoryGuard.checkIntervalMs` | `60_000` |
|
|
127
|
+
| `memoryGuard.warningThresholdMb` | OS heap × 0.5 |
|
|
128
|
+
| `memoryGuard.restartThresholdMb` | OS heap × 0.75 |
|
|
129
|
+
| `memoryGuard.growthWindowCount` | `5` |
|
|
130
|
+
| `memoryGuard.adaptive` | `true` |
|
|
131
|
+
| `gracefulShutdown.enabled` | `true` |
|
|
132
|
+
| `gracefulShutdown.timeoutMs` | `30_000` |
|
|
133
|
+
| `gracefulShutdown.signals` | `['SIGTERM', 'SIGINT']` |
|
|
134
|
+
| `supervisor.maxRestartsInWindow` | `5` |
|
|
135
|
+
| `supervisor.windowMs` | `60_000` |
|
|
136
|
+
| `supervisor.baseRestartDelayMs` | `1_000` |
|
|
137
|
+
| `chaos.enabled` | `false` |
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Configuration validation
|
|
142
|
+
|
|
143
|
+
Immortal.js validates the configuration at construction time. Invalid configurations throw immediately with descriptive messages:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// Throws: "Immortal.js configuration errors:\n chaos.enabled must be false in production"
|
|
147
|
+
const immortal = await createImmortal({ chaos: { enabled: true } }); // in production
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Validation warnings (non-fatal) are printed to `console.warn`.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## TypeScript types
|
|
155
|
+
|
|
156
|
+
All configuration types are exported:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import type {
|
|
160
|
+
ImmortalConfig,
|
|
161
|
+
LoggerConfig,
|
|
162
|
+
HealthMonitorConfig,
|
|
163
|
+
MemoryGuardConfig,
|
|
164
|
+
GracefulShutdownConfig,
|
|
165
|
+
SupervisorConfig,
|
|
166
|
+
ChaosConfig,
|
|
167
|
+
} from '@immortal/core';
|
|
168
|
+
```
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Guide — Express Integration
|
|
2
|
+
|
|
3
|
+
Immortal.js works with Express through direct use of the `@immortal/core` primitives. There is no Express-specific adapter — the core API is framework-agnostic.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @immortal/core express
|
|
11
|
+
npm install -D @types/express
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Basic setup
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import express from 'express';
|
|
20
|
+
import {
|
|
21
|
+
createImmortal,
|
|
22
|
+
asyncBoundary,
|
|
23
|
+
CircuitBreaker,
|
|
24
|
+
BulkheadPool,
|
|
25
|
+
withRetry,
|
|
26
|
+
RouteFallbackCache,
|
|
27
|
+
classifyError,
|
|
28
|
+
BulkheadRejectedError,
|
|
29
|
+
} from '@immortal/core';
|
|
30
|
+
|
|
31
|
+
const app = express();
|
|
32
|
+
app.use(express.json());
|
|
33
|
+
|
|
34
|
+
// Initialize Immortal runtime
|
|
35
|
+
const immortal = await createImmortal({
|
|
36
|
+
logger: { level: 'info' },
|
|
37
|
+
healthMonitor: { enabled: true },
|
|
38
|
+
gracefulShutdown: { enabled: true, timeoutMs: 30_000 },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Resilience primitives
|
|
42
|
+
const dbBreaker = new CircuitBreaker('database', { threshold: 5 });
|
|
43
|
+
const dbPool = new BulkheadPool('database', { maxConcurrent: 10 });
|
|
44
|
+
const cache = new RouteFallbackCache();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Request tracing middleware
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { AsyncBoundary } from '@immortal/core';
|
|
53
|
+
import { randomUUID } from 'node:crypto';
|
|
54
|
+
|
|
55
|
+
app.use((req, res, next) => {
|
|
56
|
+
const requestId = (req.headers['x-request-id'] as string) ?? randomUUID();
|
|
57
|
+
res.setHeader('x-request-id', requestId);
|
|
58
|
+
|
|
59
|
+
AsyncBoundary.run(next, { requestId });
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Route with resilience
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
app.get('/api/products/:id', asyncBoundary(async (req, res) => {
|
|
69
|
+
const { id } = req.params;
|
|
70
|
+
const cacheKey = `products:${id}`;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const product = await dbPool.run(() =>
|
|
74
|
+
dbBreaker.run(() =>
|
|
75
|
+
withRetry(() => db.findProduct(id), { maxAttempts: 2 })
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
cache.set(cacheKey, product);
|
|
80
|
+
res.json(product);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
const stale = cache.get(cacheKey);
|
|
83
|
+
if (stale) return res.json({ ...stale, stale: true });
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
}));
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Error handling middleware
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import type { ErrorRequestHandler } from 'express';
|
|
95
|
+
import { ImmortalError, BulkheadRejectedError, classifyError } from '@immortal/core';
|
|
96
|
+
|
|
97
|
+
const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
|
|
98
|
+
if (err instanceof BulkheadRejectedError) {
|
|
99
|
+
return res.status(503).json({
|
|
100
|
+
error: 'Service overloaded — try again shortly',
|
|
101
|
+
retryAfter: 1,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (err instanceof ImmortalError) {
|
|
106
|
+
const status = err.statusCode ?? (err.kind === 'programming' ? 500 : 503);
|
|
107
|
+
return res.status(status).json({ error: err.message, code: err.code });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const kind = classifyError(err);
|
|
111
|
+
const status = kind === 'programming' ? 500 : 503;
|
|
112
|
+
res.status(status).json({ error: 'Internal server error', kind });
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
app.use(errorHandler);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Health endpoint
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
app.get('/health', (req, res) => {
|
|
124
|
+
res.json({
|
|
125
|
+
status: immortal.isHealthy() ? 'ok' : 'degraded',
|
|
126
|
+
timestamp: new Date().toISOString(),
|
|
127
|
+
uptime: process.uptime(),
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
app.get('/metrics', (req, res) => {
|
|
132
|
+
res.json(immortal.getMetrics());
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Graceful shutdown
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
const server = app.listen(3000, () => {
|
|
142
|
+
console.log('Server running on port 3000');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
process.on('SIGTERM', async () => {
|
|
146
|
+
// Stop accepting new connections
|
|
147
|
+
server.close(async () => {
|
|
148
|
+
await immortal.shutdown('SIGTERM');
|
|
149
|
+
process.exit(0);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Force exit after timeout
|
|
153
|
+
setTimeout(() => process.exit(1), 35_000).unref();
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Full patterns reference
|
|
160
|
+
|
|
161
|
+
| Pattern | Core API |
|
|
162
|
+
|---------|----------|
|
|
163
|
+
| Retry | `withRetry(fn, options)` |
|
|
164
|
+
| Timeout | `withTimeout(fn, ms, label)` |
|
|
165
|
+
| Circuit breaker | `new CircuitBreaker(name, config).run(fn)` |
|
|
166
|
+
| Bulkhead | `new BulkheadPool(name, config).run(fn)` |
|
|
167
|
+
| Stale cache | `new RouteFallbackCache().get(key)` |
|
|
168
|
+
| Error classification | `classifyError(err)` → operational/programming/transient/external |
|
|
169
|
+
| Request tracing | `AsyncBoundary.run(fn)` + `AsyncBoundary.getRequestId()` |
|
|
170
|
+
| Global error trap | `new ErrorTrap(bus).install()` |
|
|
171
|
+
| Safe handler wrap | `asyncBoundary(fn, { fallback })` |
|