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,267 @@
|
|
|
1
|
+
# API Reference — Recovery (Layer 2)
|
|
2
|
+
|
|
3
|
+
The **Recovery** layer provides automatic healing after failures: retrying transient errors, circuit breaking broken dependencies, and serving cached fallbacks when live data is unavailable.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Retry Engine
|
|
8
|
+
|
|
9
|
+
### `withRetry(fn, options?)`
|
|
10
|
+
|
|
11
|
+
Wraps an async function with automatic retry and exponential backoff.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { withRetry } from '@immortal/core';
|
|
15
|
+
|
|
16
|
+
const result = await withRetry(
|
|
17
|
+
() => fetch('https://api.example.com/data').then(r => r.json()),
|
|
18
|
+
{
|
|
19
|
+
maxAttempts: 5,
|
|
20
|
+
baseDelayMs: 200,
|
|
21
|
+
backoffFactor: 2,
|
|
22
|
+
jitter: true,
|
|
23
|
+
retryOn: (err) => err.code === 'ECONNRESET',
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Options:**
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
interface RetryOptions {
|
|
32
|
+
maxAttempts?: number; // default: 3
|
|
33
|
+
baseDelayMs?: number; // default: 100
|
|
34
|
+
backoffFactor?: number; // default: 2
|
|
35
|
+
maxDelayMs?: number; // default: 30_000
|
|
36
|
+
jitter?: boolean; // default: true
|
|
37
|
+
retryOn?: (err: unknown, attempt: number) => boolean;
|
|
38
|
+
onRetry?: (err: unknown, attempt: number, delayMs: number) => void;
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Retry logic:**
|
|
43
|
+
- Attempt 1: immediate
|
|
44
|
+
- Attempt 2: `baseDelayMs * backoffFactor^1` ± jitter
|
|
45
|
+
- Attempt 3: `baseDelayMs * backoffFactor^2` ± jitter
|
|
46
|
+
- Capped at `maxDelayMs`
|
|
47
|
+
|
|
48
|
+
The `retryOn` predicate receives the raw error and attempt number. Return `false` to stop retrying immediately (for `programming` errors, for example).
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
### `withTimeout(fn, timeoutMs, label?)`
|
|
53
|
+
|
|
54
|
+
Races an async operation against a deadline.
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { withTimeout } from '@immortal/core';
|
|
58
|
+
|
|
59
|
+
const result = await withTimeout(
|
|
60
|
+
() => paymentService.charge(amount),
|
|
61
|
+
5_000,
|
|
62
|
+
'payment-charge'
|
|
63
|
+
);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Throws `ImmortalError` with `kind: 'operational'` and `code: 'TIMEOUT'` on expiry.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
### `withSimpleTimeout(fn, timeoutMs)`
|
|
71
|
+
|
|
72
|
+
Alias for `withTimeout` without the label parameter.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
### `calculateBackoff(attempt, baseMs, maxMs)`
|
|
77
|
+
|
|
78
|
+
Pure utility — computes the delay for retry attempt `n` with full jitter.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { calculateBackoff } from '@immortal/core';
|
|
82
|
+
|
|
83
|
+
const delay = calculateBackoff(3, 100, 30_000);
|
|
84
|
+
// e.g. → 347 (ms)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Circuit Breaker
|
|
90
|
+
|
|
91
|
+
### `new CircuitBreaker(name, config?)`
|
|
92
|
+
|
|
93
|
+
Creates a named circuit breaker. Circuit breakers prevent cascading failures by stopping calls to a broken dependency.
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { CircuitBreaker } from '@immortal/core';
|
|
97
|
+
|
|
98
|
+
const cb = new CircuitBreaker('payment-service', {
|
|
99
|
+
threshold: 5, // failures to trip open
|
|
100
|
+
timeout: 60_000, // ms to stay open before half-open probe
|
|
101
|
+
halfOpenRequests: 2, // successful probes needed to close
|
|
102
|
+
volumeThreshold: 10, // min requests before tripping
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Config:**
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
interface CircuitBreakerConfig {
|
|
110
|
+
threshold?: number; // default: 5
|
|
111
|
+
timeout?: number; // default: 60_000
|
|
112
|
+
halfOpenRequests?: number; // default: 1
|
|
113
|
+
volumeThreshold?: number; // default: 0
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### `cb.run(fn)`
|
|
118
|
+
|
|
119
|
+
Execute a function protected by the circuit breaker. Throws `CircuitOpenError` when the circuit is open.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
const result = await cb.run(() => db.query(sql));
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### `cb.getCurrentState()`
|
|
126
|
+
|
|
127
|
+
Returns `'closed' | 'open' | 'half-open'`.
|
|
128
|
+
|
|
129
|
+
### `cb.getStatus()`
|
|
130
|
+
|
|
131
|
+
Returns detailed status object:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
interface CircuitBreakerStatus {
|
|
135
|
+
name: string;
|
|
136
|
+
state: 'closed' | 'open' | 'half-open';
|
|
137
|
+
failures: number;
|
|
138
|
+
successes: number;
|
|
139
|
+
lastFailureTime?: number;
|
|
140
|
+
nextAttemptTime?: number;
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### `cb.forceState(state)`
|
|
145
|
+
|
|
146
|
+
Manually set circuit state — useful in tests and admin APIs.
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
cb.forceState('open');
|
|
150
|
+
cb.forceState('closed');
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Events
|
|
154
|
+
|
|
155
|
+
Circuit breakers emit events on the Immortal event bus:
|
|
156
|
+
|
|
157
|
+
| Event | Trigger |
|
|
158
|
+
|-------|---------|
|
|
159
|
+
| `circuit:opened` | Circuit trips open |
|
|
160
|
+
| `circuit:closed` | Circuit resets to closed |
|
|
161
|
+
| `circuit:half-open` | Probe attempt starts |
|
|
162
|
+
| `circuit:rejected` | Call rejected while open |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
### `CircuitBreakerRegistry`
|
|
167
|
+
|
|
168
|
+
Singleton registry for named circuit breakers.
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { CircuitBreakerRegistry } from '@immortal/core';
|
|
172
|
+
|
|
173
|
+
const registry = CircuitBreakerRegistry.getInstance();
|
|
174
|
+
|
|
175
|
+
// Get or create
|
|
176
|
+
const cb = registry.get('payment-service', config);
|
|
177
|
+
|
|
178
|
+
// Get all names
|
|
179
|
+
const names = registry.getAll().map(cb => cb.name);
|
|
180
|
+
|
|
181
|
+
// Reset all (testing)
|
|
182
|
+
registry.resetAll();
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Fallback Cache
|
|
188
|
+
|
|
189
|
+
### `new RouteFallbackCache(config?)`
|
|
190
|
+
|
|
191
|
+
LRU cache that stores last-known-good responses per route key.
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import { RouteFallbackCache } from '@immortal/core';
|
|
195
|
+
|
|
196
|
+
const cache = new RouteFallbackCache({
|
|
197
|
+
maxEntries: 1000,
|
|
198
|
+
ttlMs: 5 * 60_000, // 5 minutes default TTL
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Config:**
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
interface FallbackCacheConfig {
|
|
206
|
+
maxEntries?: number; // default: 500
|
|
207
|
+
ttlMs?: number; // default: 300_000 (5 min)
|
|
208
|
+
purgIntervalMs?: number;
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### `cache.set(key, value, options?)`
|
|
213
|
+
|
|
214
|
+
Store a value. Per-entry TTL overrides the global default.
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
cache.set('GET /products/42', productData, { ttlMs: 600_000 });
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### `cache.get(key)`
|
|
221
|
+
|
|
222
|
+
Retrieve a value. Returns `undefined` on miss or expiry.
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
const cached = cache.get('GET /products/42');
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### `cache.has(key)`
|
|
229
|
+
|
|
230
|
+
Check existence without retrieving.
|
|
231
|
+
|
|
232
|
+
### `cache.delete(key)`
|
|
233
|
+
|
|
234
|
+
Remove a specific entry.
|
|
235
|
+
|
|
236
|
+
### `cache.clear()`
|
|
237
|
+
|
|
238
|
+
Flush all entries.
|
|
239
|
+
|
|
240
|
+
### `cache.getStats()`
|
|
241
|
+
|
|
242
|
+
Returns `{ size, hits, misses, hitRate }`.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Integration Pattern
|
|
247
|
+
|
|
248
|
+
Combining retry + circuit breaker + fallback cache:
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
const cache = new RouteFallbackCache();
|
|
252
|
+
const cb = new CircuitBreaker('inventory');
|
|
253
|
+
|
|
254
|
+
async function getInventory(productId: string) {
|
|
255
|
+
try {
|
|
256
|
+
const result = await cb.run(() =>
|
|
257
|
+
withRetry(() => inventoryApi.get(productId), { maxAttempts: 2 })
|
|
258
|
+
);
|
|
259
|
+
cache.set(`inventory:${productId}`, result);
|
|
260
|
+
return result;
|
|
261
|
+
} catch (err) {
|
|
262
|
+
const cached = cache.get(`inventory:${productId}`);
|
|
263
|
+
if (cached) return { ...cached, stale: true };
|
|
264
|
+
throw err;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# API Reference — Safe Zone (Layer 1: Containment)
|
|
2
|
+
|
|
3
|
+
The **Safe Zone** is the innermost layer of Immortal.js. It captures every unhandled error at the process and request boundary, classifies it, and routes it to the appropriate recovery mechanism.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## `classifyError(error)`
|
|
8
|
+
|
|
9
|
+
Classifies any thrown value into one of five canonical categories.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { classifyError } from '@immortal/core';
|
|
13
|
+
|
|
14
|
+
const kind = classifyError(error);
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Returns:** `'operational' | 'programming' | 'transient' | 'external' | 'unknown'`
|
|
18
|
+
|
|
19
|
+
| Category | Examples | Behaviour |
|
|
20
|
+
|----------|----------|-----------|
|
|
21
|
+
| `operational` | `ECONNREFUSED`, HTTP 5xx, timeout | Retry is safe |
|
|
22
|
+
| `programming` | `TypeError`, `RangeError`, `ReferenceError` | Do NOT retry — fix the code |
|
|
23
|
+
| `transient` | Rate limit (HTTP 429), lock contention | Retry with backoff |
|
|
24
|
+
| `external` | Third-party API error, DNS failure | Circuit break |
|
|
25
|
+
| `unknown` | Generic `Error()`, unrecognised thrown value | Conservative — treat as operational |
|
|
26
|
+
|
|
27
|
+
**Example:**
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
try {
|
|
31
|
+
await db.query(sql);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
const kind = classifyError(err);
|
|
34
|
+
if (kind === 'programming') throw err; // surface immediately
|
|
35
|
+
if (kind === 'transient') await withRetry(...); // safe to retry
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## `serializeError(error)`
|
|
42
|
+
|
|
43
|
+
Converts any thrown value to a plain `SerializedError` object safe for logging / JSON transport.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { serializeError } from '@immortal/core';
|
|
47
|
+
|
|
48
|
+
const serialized = serializeError(error);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Returns:** `SerializedError`
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
interface SerializedError {
|
|
55
|
+
message: string;
|
|
56
|
+
name: string;
|
|
57
|
+
kind: ErrorKind;
|
|
58
|
+
stack?: string; // only in non-production
|
|
59
|
+
code?: string; // e.g. 'ECONNREFUSED'
|
|
60
|
+
statusCode?: number; // HTTP status if present
|
|
61
|
+
context?: Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## `AsyncBoundary`
|
|
68
|
+
|
|
69
|
+
Static class that manages async execution context (trace IDs, request IDs, parent spans).
|
|
70
|
+
|
|
71
|
+
All methods are **static** — never instantiate `AsyncBoundary`.
|
|
72
|
+
|
|
73
|
+
### `AsyncBoundary.run(fn, context?)`
|
|
74
|
+
|
|
75
|
+
Executes `fn` inside an isolated async context.
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
const result = await AsyncBoundary.run(async () => {
|
|
79
|
+
// All code here shares the same request context
|
|
80
|
+
return processOrder(order);
|
|
81
|
+
}, { requestId: 'req-abc-123', traceId: 'trace-xyz' });
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Parameters:**
|
|
85
|
+
- `fn: () => Promise<T>` — async function to execute
|
|
86
|
+
- `context?: Partial<BoundaryContext>` — initial context values
|
|
87
|
+
|
|
88
|
+
**Returns:** `Promise<T>`
|
|
89
|
+
|
|
90
|
+
### `AsyncBoundary.getContext()`
|
|
91
|
+
|
|
92
|
+
Returns the current boundary context, or `null` outside a boundary.
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
const ctx = AsyncBoundary.getContext();
|
|
96
|
+
if (ctx) {
|
|
97
|
+
console.log(ctx.requestId, ctx.traceId);
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### `AsyncBoundary.getRequestId()`
|
|
102
|
+
|
|
103
|
+
Shorthand for `AsyncBoundary.getContext()?.requestId`.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
const reqId = AsyncBoundary.getRequestId();
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### `AsyncBoundary.enrich(extra)`
|
|
110
|
+
|
|
111
|
+
Merges additional data into the current context. No-op if called outside a boundary.
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
AsyncBoundary.enrich({ userId: 'u-42', tenantId: 'acme' });
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## `asyncBoundary(fn, options?)`
|
|
120
|
+
|
|
121
|
+
Higher-order function that wraps a handler with an `AsyncBoundary.run` call automatically.
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
import { asyncBoundary } from '@immortal/core';
|
|
125
|
+
|
|
126
|
+
const safeHandler = asyncBoundary(originalHandler, {
|
|
127
|
+
fallback: (err) => ({ error: 'Service unavailable', kind: classifyError(err) }),
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Options:**
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
interface AsyncBoundaryOptions<T> {
|
|
135
|
+
fallback?: (error: unknown) => T | Promise<T>;
|
|
136
|
+
context?: Partial<BoundaryContext>;
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## `ErrorTrap`
|
|
143
|
+
|
|
144
|
+
Installs global handlers for `uncaughtException` and `unhandledRejection`, emitting structured events on the Immortal event bus.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { ErrorTrap } from '@immortal/core';
|
|
148
|
+
import { getEventBus } from '@immortal/core';
|
|
149
|
+
|
|
150
|
+
const trap = new ErrorTrap(getEventBus());
|
|
151
|
+
trap.install();
|
|
152
|
+
|
|
153
|
+
// Later, if needed:
|
|
154
|
+
trap.uninstall();
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Events emitted:**
|
|
158
|
+
|
|
159
|
+
| Event | Trigger |
|
|
160
|
+
|-------|---------|
|
|
161
|
+
| `error:captured` | Any uncaught exception or unhandled rejection |
|
|
162
|
+
| `error:escalated` | When `programming` errors are detected (non-retriable) |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## `ImmortalError`
|
|
167
|
+
|
|
168
|
+
Custom error class with structured metadata.
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { ImmortalError } from '@immortal/core';
|
|
172
|
+
|
|
173
|
+
throw new ImmortalError('Payment failed', {
|
|
174
|
+
kind: 'external',
|
|
175
|
+
code: 'PAYMENT_DECLINED',
|
|
176
|
+
statusCode: 402,
|
|
177
|
+
context: { orderId: '42', amount: 9999 },
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Constructor:**
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
new ImmortalError(message: string, options?: {
|
|
185
|
+
kind?: ErrorKind;
|
|
186
|
+
code?: string;
|
|
187
|
+
statusCode?: number;
|
|
188
|
+
context?: Record<string, unknown>;
|
|
189
|
+
cause?: unknown;
|
|
190
|
+
})
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## `SafeWrapper` / `createSafeZone`
|
|
196
|
+
|
|
197
|
+
Creates a safe zone configuration for wrapping multiple route handlers with shared options.
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import { createSafeZone } from '@immortal/core';
|
|
201
|
+
|
|
202
|
+
const safeZone = createSafeZone({
|
|
203
|
+
defaultFallback: () => ({ error: 'Unavailable' }),
|
|
204
|
+
onError: (err, ctx) => logger.error('Route error', { err, ...ctx }),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Wrap individual handlers
|
|
208
|
+
const wrappedHandler = safeZone.handler(myHandler);
|
|
209
|
+
const wrappedWithFallback = safeZone.handler(myHandler, customFallback);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Types
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
type ErrorKind = 'operational' | 'programming' | 'transient' | 'external' | 'unknown';
|
|
218
|
+
|
|
219
|
+
interface SerializedError {
|
|
220
|
+
message: string;
|
|
221
|
+
name: string;
|
|
222
|
+
kind: ErrorKind;
|
|
223
|
+
stack?: string;
|
|
224
|
+
code?: string;
|
|
225
|
+
statusCode?: number;
|
|
226
|
+
context?: Record<string, unknown>;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
interface BoundaryContext {
|
|
230
|
+
requestId: string;
|
|
231
|
+
traceId: string;
|
|
232
|
+
spanId: string;
|
|
233
|
+
parentSpanId?: string;
|
|
234
|
+
[key: string]: unknown;
|
|
235
|
+
}
|
|
236
|
+
```
|