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,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 })` |