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,179 @@
|
|
|
1
|
+
# API Reference — Chaos Engineering
|
|
2
|
+
|
|
3
|
+
Immortal.js ships a built-in chaos engine for fault injection in staging and pre-production environments. It is **permanently disabled in `NODE_ENV=production`**.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ChaosEngine
|
|
8
|
+
|
|
9
|
+
### `new ChaosEngine()`
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { ChaosEngine } from '@immortal/core';
|
|
13
|
+
|
|
14
|
+
const chaos = new ChaosEngine();
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
### `chaos.addFault(config)`
|
|
20
|
+
|
|
21
|
+
Register a fault definition.
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
chaos.addFault({
|
|
25
|
+
type: 'latency',
|
|
26
|
+
probability: 0.1,
|
|
27
|
+
minMs: 100,
|
|
28
|
+
maxMs: 2_000,
|
|
29
|
+
targetRoutes: ['/api/orders', '/api/payments'],
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Fault types:**
|
|
34
|
+
|
|
35
|
+
#### `type: 'latency'`
|
|
36
|
+
|
|
37
|
+
Inject random delay before calling the real handler.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
interface LatencyOptions {
|
|
41
|
+
type: 'latency';
|
|
42
|
+
probability: number; // 0.0–1.0
|
|
43
|
+
minMs: number;
|
|
44
|
+
maxMs: number;
|
|
45
|
+
targetRoutes?: string[]; // if omitted, applies to all routes
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
#### `type: 'error'`
|
|
50
|
+
|
|
51
|
+
Throw a simulated error instead of calling the handler.
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
interface ErrorOptions {
|
|
55
|
+
type: 'error';
|
|
56
|
+
probability: number;
|
|
57
|
+
errorMessage?: string; // default: 'Chaos: injected error'
|
|
58
|
+
statusCode?: number;
|
|
59
|
+
targetRoutes?: string[];
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### `type: 'worker-kill'`
|
|
64
|
+
|
|
65
|
+
Simulate a worker process crash by throwing an uncaught error.
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
interface WorkerKillOptions {
|
|
69
|
+
type: 'worker-kill';
|
|
70
|
+
probability: number;
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
### `chaos.removeFault(index)`
|
|
77
|
+
|
|
78
|
+
Remove a registered fault by index.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const idx = chaos.addFault(config);
|
|
82
|
+
chaos.removeFault(idx);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `chaos.clearFaults()`
|
|
86
|
+
|
|
87
|
+
Remove all registered faults.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
chaos.clearFaults();
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `chaos.enable()` / `chaos.disable()`
|
|
94
|
+
|
|
95
|
+
Toggle the engine. When disabled, `wrapHandler` passes through directly.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
chaos.enable();
|
|
99
|
+
chaos.disable();
|
|
100
|
+
|
|
101
|
+
const isEnabled = chaos.isEnabled();
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### `chaos.wrapHandler(fn, route?)`
|
|
107
|
+
|
|
108
|
+
Wrap a route handler function with chaos injection.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
const chaosHandler = chaos.wrapHandler(originalHandler, '/api/orders');
|
|
112
|
+
|
|
113
|
+
// Express / Fastify:
|
|
114
|
+
app.get('/api/orders', chaosHandler);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The wrapped handler probabilistically applies registered faults before calling through to `fn`.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### `chaos.injectLatency(route?)`
|
|
122
|
+
|
|
123
|
+
Directly inject latency (for use outside of handler wrapping).
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
await chaos.injectLatency('/api/orders');
|
|
127
|
+
// Resolves after delay (or immediately if no latency fault matches)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Events
|
|
133
|
+
|
|
134
|
+
| Event | Trigger |
|
|
135
|
+
|-------|---------|
|
|
136
|
+
| `chaos:latency-injected` | Latency fault was applied |
|
|
137
|
+
| `chaos:error-injected` | Error fault was applied |
|
|
138
|
+
| `chaos:worker-killed` | Worker-kill fault was applied |
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Safety rules
|
|
143
|
+
|
|
144
|
+
1. **Production guard**: Attempting to `enable()` or `addFault()` when `NODE_ENV === 'production'` logs an error and is a no-op.
|
|
145
|
+
2. **Rate limiting**: The chaos engine tracks injections per-route to prevent runaway fault amplification.
|
|
146
|
+
3. **Deterministic testing**: Seed the random number generator for reproducible test scenarios.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Integration example
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import { ChaosEngine } from '@immortal/core';
|
|
154
|
+
import Fastify from 'fastify';
|
|
155
|
+
|
|
156
|
+
const chaos = new ChaosEngine();
|
|
157
|
+
|
|
158
|
+
if (process.env.CHAOS_ENABLED === 'true') {
|
|
159
|
+
chaos.enable();
|
|
160
|
+
chaos.addFault({
|
|
161
|
+
type: 'latency',
|
|
162
|
+
probability: parseFloat(process.env.CHAOS_LATENCY_PROB ?? '0.05'),
|
|
163
|
+
minMs: 100,
|
|
164
|
+
maxMs: 3_000,
|
|
165
|
+
});
|
|
166
|
+
chaos.addFault({
|
|
167
|
+
type: 'error',
|
|
168
|
+
probability: parseFloat(process.env.CHAOS_ERROR_PROB ?? '0.02'),
|
|
169
|
+
statusCode: 503,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const app = Fastify();
|
|
174
|
+
|
|
175
|
+
app.get('/api/products', chaos.wrapHandler(async (req, reply) => {
|
|
176
|
+
const products = await db.products.findAll();
|
|
177
|
+
reply.send(products);
|
|
178
|
+
}));
|
|
179
|
+
```
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# API Reference — Dashboard (`@immortal/dashboard`)
|
|
2
|
+
|
|
3
|
+
The `@immortal/dashboard` package provides a real-time web dashboard for visualizing the health, metrics, and event stream of your Immortal.js runtime.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @immortal/dashboard
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Optional dependency for WebSocket push:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install ws
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { createDashboard } from '@immortal/dashboard';
|
|
25
|
+
import { getImmortal } from '@immortal/core';
|
|
26
|
+
|
|
27
|
+
const dashboard = createDashboard({
|
|
28
|
+
port: 9090,
|
|
29
|
+
host: '0.0.0.0',
|
|
30
|
+
refreshIntervalMs: 2_000,
|
|
31
|
+
auth: {
|
|
32
|
+
username: 'admin',
|
|
33
|
+
password: process.env.DASHBOARD_PASSWORD!,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await dashboard.start();
|
|
38
|
+
console.log('Dashboard running at http://localhost:9090');
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
interface DashboardConfig {
|
|
47
|
+
port?: number; // default: 9090
|
|
48
|
+
host?: string; // default: '127.0.0.1'
|
|
49
|
+
refreshIntervalMs?: number; // default: 5_000
|
|
50
|
+
auth?: {
|
|
51
|
+
username: string;
|
|
52
|
+
password: string;
|
|
53
|
+
};
|
|
54
|
+
cors?: {
|
|
55
|
+
origin: string | string[];
|
|
56
|
+
};
|
|
57
|
+
websocket?: boolean; // enable WS push (requires 'ws' package)
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Dashboard pages
|
|
64
|
+
|
|
65
|
+
| Route | Description |
|
|
66
|
+
|-------|-------------|
|
|
67
|
+
| `/` | Overview — health, key metrics, recent events |
|
|
68
|
+
| `/metrics` | Full system metrics with time-series charts |
|
|
69
|
+
| `/circuits` | Circuit breaker states and history |
|
|
70
|
+
| `/bulkheads` | Bulkhead pool saturation |
|
|
71
|
+
| `/workers` | Supervisor worker states and restart history |
|
|
72
|
+
| `/events` | Live event stream (WebSocket if enabled) |
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## REST API
|
|
77
|
+
|
|
78
|
+
The dashboard exposes a JSON API:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
GET /api/health → { status, timestamp, metrics }
|
|
82
|
+
GET /api/metrics → SystemMetrics
|
|
83
|
+
GET /api/circuits → CircuitBreakerStatus[]
|
|
84
|
+
GET /api/bulkheads → BulkheadStatus[]
|
|
85
|
+
GET /api/workers → WorkerStatus[]
|
|
86
|
+
GET /api/events → ImmortalEvent[] (last 1000)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## WebSocket stream
|
|
92
|
+
|
|
93
|
+
When `websocket: true`, events are pushed in real time:
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
const ws = new WebSocket('ws://localhost:9090/ws');
|
|
97
|
+
ws.onmessage = (msg) => {
|
|
98
|
+
const event = JSON.parse(msg.data);
|
|
99
|
+
console.log(event.type, event.data);
|
|
100
|
+
};
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Security
|
|
106
|
+
|
|
107
|
+
- The dashboard is **not recommended for public exposure**. Bind to `127.0.0.1` and use a reverse proxy with TLS.
|
|
108
|
+
- Basic auth credentials are verified on every request.
|
|
109
|
+
- In production, prefer exporting metrics to Prometheus/Datadog and using their dashboards instead.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# API Reference — Isolation (Layer 3)
|
|
2
|
+
|
|
3
|
+
The **Isolation** layer prevents a misbehaving dependency or compute-heavy operation from consuming all available resources. It uses two mechanisms: **bulkheads** (concurrency limits) and **worker sandboxes** (thread isolation).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## BulkheadPool
|
|
8
|
+
|
|
9
|
+
A bulkhead limits the number of concurrent operations to a named pool, with an optional overflow queue.
|
|
10
|
+
|
|
11
|
+
### `new BulkheadPool(name, config?)`
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { BulkheadPool } from '@immortal/core';
|
|
15
|
+
|
|
16
|
+
const dbPool = new BulkheadPool('database', {
|
|
17
|
+
maxConcurrent: 10,
|
|
18
|
+
maxQueueSize: 50,
|
|
19
|
+
defaultTimeoutMs: 5_000,
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Config:**
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
interface BulkheadConfig {
|
|
27
|
+
maxConcurrent?: number; // default: 10
|
|
28
|
+
maxQueueSize?: number; // default: 100
|
|
29
|
+
defaultTimeoutMs?: number; // default: 30_000
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### `pool.run(task, priority?, timeoutMs?)`
|
|
34
|
+
|
|
35
|
+
Execute a task within the bulkhead.
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
const result = await pool.run(
|
|
39
|
+
() => db.query('SELECT * FROM users'),
|
|
40
|
+
'high', // 'high' | 'medium' | 'low'
|
|
41
|
+
3_000 // per-call timeout override
|
|
42
|
+
);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- If `active >= maxConcurrent` and `queued < maxQueueSize` → task is queued
|
|
46
|
+
- If queue is also full → throws `BulkheadRejectedError` immediately
|
|
47
|
+
- Priority ordering: `high` > `medium` > `low`
|
|
48
|
+
|
|
49
|
+
**Throws:** `BulkheadRejectedError` when the pool is saturated.
|
|
50
|
+
|
|
51
|
+
### `pool.getStatus()`
|
|
52
|
+
|
|
53
|
+
Returns the current pool snapshot:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
interface BulkheadStatus {
|
|
57
|
+
name: string;
|
|
58
|
+
active: number;
|
|
59
|
+
queued: number;
|
|
60
|
+
maxConcurrent: number;
|
|
61
|
+
maxQueueSize: number;
|
|
62
|
+
rejected: number;
|
|
63
|
+
totalExecuted: number;
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### `pool.drainAll()`
|
|
68
|
+
|
|
69
|
+
Waits for all currently active and queued tasks to complete. Useful during graceful shutdown.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
await pool.drainAll();
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Events
|
|
76
|
+
|
|
77
|
+
| Event | Trigger |
|
|
78
|
+
|-------|---------|
|
|
79
|
+
| `bulkhead:queued` | Task enters the overflow queue |
|
|
80
|
+
| `bulkhead:rejected` | Task rejected (pool + queue full) |
|
|
81
|
+
| `bulkhead:timeout` | Task exceeds its timeout while queued |
|
|
82
|
+
|
|
83
|
+
### `BulkheadRejectedError`
|
|
84
|
+
|
|
85
|
+
Thrown when the pool is full. Extends `ImmortalError` with `kind: 'operational'`.
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { BulkheadRejectedError } from '@immortal/core';
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await pool.run(task);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (err instanceof BulkheadRejectedError) {
|
|
94
|
+
return res.status(503).json({ error: 'Service overloaded, try again later' });
|
|
95
|
+
}
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## BulkheadRegistry
|
|
103
|
+
|
|
104
|
+
Singleton registry for named bulkhead pools.
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { BulkheadRegistry } from '@immortal/core';
|
|
108
|
+
|
|
109
|
+
const registry = BulkheadRegistry.getInstance();
|
|
110
|
+
|
|
111
|
+
// Get or create a named pool
|
|
112
|
+
const pool = registry.get('database', { maxConcurrent: 5 });
|
|
113
|
+
|
|
114
|
+
// Iterate all pools
|
|
115
|
+
for (const [name, pool] of registry.entries()) {
|
|
116
|
+
console.log(name, pool.getStatus());
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## WorkerSandbox
|
|
123
|
+
|
|
124
|
+
Executes untrusted or CPU-intensive code in an isolated Node.js worker thread.
|
|
125
|
+
|
|
126
|
+
### `new WorkerSandbox(config?)`
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { WorkerSandbox } from '@immortal/core';
|
|
130
|
+
|
|
131
|
+
const sandbox = new WorkerSandbox({
|
|
132
|
+
timeoutMs: 5_000,
|
|
133
|
+
memoryLimitMb: 128,
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Config:**
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
interface WorkerSandboxConfig {
|
|
141
|
+
timeoutMs?: number; // default: 10_000
|
|
142
|
+
memoryLimitMb?: number; // default: 256
|
|
143
|
+
maxWorkers?: number; // default: os.cpus().length
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### `sandbox.execute(code, args?)`
|
|
148
|
+
|
|
149
|
+
Execute a string of JavaScript code in an isolated worker thread. The code must `return` a value.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
const result = await sandbox.execute(
|
|
153
|
+
`return input.a + input.b`,
|
|
154
|
+
{ input: { a: 10, b: 32 } }
|
|
155
|
+
);
|
|
156
|
+
// → 42
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
- `args` are serialized and passed to the worker via `structuredClone`
|
|
160
|
+
- The worker terminates after `timeoutMs` milliseconds
|
|
161
|
+
- Memory is garbage-collected after the task completes
|
|
162
|
+
|
|
163
|
+
**Throws:** `ImmortalError` with `code: 'WORKER_TIMEOUT'` or `code: 'WORKER_ERROR'`.
|
|
164
|
+
|
|
165
|
+
### `sandbox.terminate()`
|
|
166
|
+
|
|
167
|
+
Forcefully terminates all worker threads in the pool.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Design Notes
|
|
172
|
+
|
|
173
|
+
### Bulkhead sizing
|
|
174
|
+
|
|
175
|
+
A common mistake is setting `maxConcurrent` too high. The optimal value depends on your I/O concurrency model:
|
|
176
|
+
|
|
177
|
+
- **Database connections:** Match your connection pool size (e.g. 10–20)
|
|
178
|
+
- **HTTP calls to external services:** Match the service's rate limit / connection limit
|
|
179
|
+
- **CPU work (Worker threads):** Match `os.cpus().length`
|
|
180
|
+
|
|
181
|
+
### Queue sizing
|
|
182
|
+
|
|
183
|
+
`maxQueueSize` is a safety valve. A large queue adds latency — callers wait longer before failing. A size of 0 means "reject immediately when all slots are taken." Choose based on your SLA:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
queue_wait_time ≈ (maxQueueSize / maxConcurrent) × avg_task_duration
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Priority queuing
|
|
190
|
+
|
|
191
|
+
Priority queuing (`'high' | 'medium' | 'low'`) ensures critical paths are served first during overload. Use sparingly — abusing `'high'` starves lower priorities.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# API Reference — Lifecycle
|
|
2
|
+
|
|
3
|
+
Lifecycle management ensures your service starts cleanly, shuts down gracefully, and drains in-flight requests before exiting.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## GracefulShutdown
|
|
8
|
+
|
|
9
|
+
### `new GracefulShutdown(config, bus)`
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { GracefulShutdown } from '@immortal/core';
|
|
13
|
+
|
|
14
|
+
const shutdown = new GracefulShutdown(
|
|
15
|
+
{
|
|
16
|
+
enabled: true,
|
|
17
|
+
timeoutMs: 30_000,
|
|
18
|
+
signals: ['SIGTERM', 'SIGINT'],
|
|
19
|
+
},
|
|
20
|
+
getEventBus()
|
|
21
|
+
);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Config:**
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
interface GracefulShutdownConfig {
|
|
28
|
+
enabled?: boolean;
|
|
29
|
+
timeoutMs?: number; // default: 30_000
|
|
30
|
+
signals?: NodeJS.Signals[]; // default: ['SIGTERM', 'SIGINT']
|
|
31
|
+
forceExitAfterMs?: number; // default: timeoutMs + 5_000
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
### `shutdown.addHandler(name, fn, priority?)`
|
|
38
|
+
|
|
39
|
+
Register a shutdown hook. Handlers are called in **descending priority order** (highest first).
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
shutdown.addHandler('http-server', async () => {
|
|
43
|
+
await server.close();
|
|
44
|
+
}, 100);
|
|
45
|
+
|
|
46
|
+
shutdown.addHandler('database', async () => {
|
|
47
|
+
await db.end();
|
|
48
|
+
}, 50);
|
|
49
|
+
|
|
50
|
+
shutdown.addHandler('cache', async () => {
|
|
51
|
+
await redis.quit();
|
|
52
|
+
}, 10);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Parameters:**
|
|
56
|
+
- `name: string` — identifier for logging
|
|
57
|
+
- `fn: () => Promise<void>` — async cleanup function
|
|
58
|
+
- `priority?: number` — default `0`, higher runs first
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
### `shutdown.execute(signal?)`
|
|
63
|
+
|
|
64
|
+
Trigger the shutdown sequence programmatically.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
await shutdown.execute('SIGTERM');
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
1. Emits `shutdown:initiated` on the event bus
|
|
71
|
+
2. Calls all registered handlers in priority order
|
|
72
|
+
3. Waits up to `timeoutMs` for each handler
|
|
73
|
+
4. Emits `shutdown:complete` (or `shutdown:forced` on timeout)
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### Automatic signal handling
|
|
78
|
+
|
|
79
|
+
When `enabled: true` and `signals` are configured, the shutdown sequence runs automatically on signal receipt:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// Registered automatically — no extra code needed
|
|
83
|
+
process.on('SIGTERM', () => shutdown.execute('SIGTERM'));
|
|
84
|
+
process.on('SIGINT', () => shutdown.execute('SIGINT'));
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## createImmortal
|
|
90
|
+
|
|
91
|
+
High-level factory that wires all five layers together.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { createImmortal } from '@immortal/core';
|
|
95
|
+
|
|
96
|
+
const immortal = await createImmortal({
|
|
97
|
+
logger: { level: 'info', transport: 'json' },
|
|
98
|
+
healthMonitor: { enabled: true, checkIntervalMs: 30_000 },
|
|
99
|
+
memoryGuard: { enabled: true, warningThresholdMb: 400 },
|
|
100
|
+
gracefulShutdown: { enabled: true, timeoutMs: 30_000 },
|
|
101
|
+
plugins: [ConsoleLogPlugin, RequestTracingPlugin],
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Returns an `ImmortalRuntime` instance with all layers initialized.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## ImmortalRuntime
|
|
110
|
+
|
|
111
|
+
### `runtime.config`
|
|
112
|
+
|
|
113
|
+
The fully-resolved (merged with defaults) configuration object.
|
|
114
|
+
|
|
115
|
+
### `runtime.getMetrics()`
|
|
116
|
+
|
|
117
|
+
Returns the current `SystemMetrics` snapshot.
|
|
118
|
+
|
|
119
|
+
### `runtime.isHealthy()`
|
|
120
|
+
|
|
121
|
+
Returns `true` if no critical health events have occurred.
|
|
122
|
+
|
|
123
|
+
### `runtime.addPlugin(plugin)`
|
|
124
|
+
|
|
125
|
+
Attach a plugin after initialization. `onInit` is called immediately.
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
runtime.addPlugin(createSlackAlertPlugin({ webhookUrl: '...' }));
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### `runtime.shutdown(signal?)`
|
|
132
|
+
|
|
133
|
+
Trigger graceful shutdown.
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
await runtime.shutdown('SIGTERM');
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## getImmortal
|
|
142
|
+
|
|
143
|
+
Returns the singleton `ImmortalRuntime`. Throws if `createImmortal()` has not been called.
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { getImmortal } from '@immortal/core';
|
|
147
|
+
|
|
148
|
+
const runtime = getImmortal();
|
|
149
|
+
const metrics = runtime.getMetrics();
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Typical startup pattern
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { createImmortal, ConsoleLogPlugin } from '@immortal/core';
|
|
158
|
+
import { createServer } from './server.js';
|
|
159
|
+
|
|
160
|
+
async function main() {
|
|
161
|
+
const immortal = await createImmortal({
|
|
162
|
+
logger: { level: process.env.LOG_LEVEL ?? 'info' },
|
|
163
|
+
gracefulShutdown: {
|
|
164
|
+
enabled: true,
|
|
165
|
+
timeoutMs: 30_000,
|
|
166
|
+
signals: ['SIGTERM', 'SIGINT'],
|
|
167
|
+
},
|
|
168
|
+
plugins: [ConsoleLogPlugin],
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const server = await createServer();
|
|
172
|
+
|
|
173
|
+
// Register shutdown hooks
|
|
174
|
+
immortal.config.gracefulShutdown; // already wired via createImmortal
|
|
175
|
+
// Add app-specific hooks via the GracefulShutdown instance:
|
|
176
|
+
const { GracefulShutdown } = await import('@immortal/core');
|
|
177
|
+
// (or construct separately and pass via plugin)
|
|
178
|
+
|
|
179
|
+
await server.listen({ port: 3000 });
|
|
180
|
+
console.log('Server running on port 3000');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
main().catch((err) => {
|
|
184
|
+
console.error('Fatal startup error:', err);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
});
|
|
187
|
+
```
|