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,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
+ ```