@goodie-ts/resilience 0.4.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/README.md +58 -0
- package/dist/circuit-breaker-interceptor.d.ts +24 -0
- package/dist/circuit-breaker-interceptor.d.ts.map +1 -0
- package/dist/circuit-breaker-interceptor.js +118 -0
- package/dist/circuit-breaker-interceptor.js.map +1 -0
- package/dist/decorators/circuit-breaker.d.ts +18 -0
- package/dist/decorators/circuit-breaker.d.ts.map +1 -0
- package/dist/decorators/circuit-breaker.js +12 -0
- package/dist/decorators/circuit-breaker.js.map +1 -0
- package/dist/decorators/retryable.d.ts +18 -0
- package/dist/decorators/retryable.d.ts.map +1 -0
- package/dist/decorators/retryable.js +12 -0
- package/dist/decorators/retryable.js.map +1 -0
- package/dist/decorators/timeout.d.ts +12 -0
- package/dist/decorators/timeout.d.ts.map +1 -0
- package/dist/decorators/timeout.js +14 -0
- package/dist/decorators/timeout.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/resilience-transformer-plugin.d.ts +9 -0
- package/dist/resilience-transformer-plugin.d.ts.map +1 -0
- package/dist/resilience-transformer-plugin.js +211 -0
- package/dist/resilience-transformer-plugin.js.map +1 -0
- package/dist/retry-interceptor.d.ts +27 -0
- package/dist/retry-interceptor.d.ts.map +1 -0
- package/dist/retry-interceptor.js +68 -0
- package/dist/retry-interceptor.js.map +1 -0
- package/dist/timeout-interceptor.d.ts +16 -0
- package/dist/timeout-interceptor.d.ts.map +1 -0
- package/dist/timeout-interceptor.js +33 -0
- package/dist/timeout-interceptor.js.map +1 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# @goodie-ts/resilience
|
|
2
|
+
|
|
3
|
+
Resilience patterns for [goodie-ts](https://github.com/GOOD-Code-ApS/goodie) — retry, circuit breaker, and timeout decorators. Built on `@goodie-ts/aop`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @goodie-ts/resilience
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Declarative resilience via decorators. Interceptors execute in order: **Timeout > Circuit Breaker > Retry > method** — so the timeout covers all retries, and the circuit breaker tracks the overall outcome.
|
|
14
|
+
|
|
15
|
+
## Decorators
|
|
16
|
+
|
|
17
|
+
| Decorator | Description | Defaults |
|
|
18
|
+
|-----------|-------------|----------|
|
|
19
|
+
| `@Retryable({ maxAttempts?, delay?, multiplier? })` | Retry with exponential backoff + jitter | 3 attempts, 1000ms, 2x |
|
|
20
|
+
| `@CircuitBreaker({ failureThreshold?, resetTimeout?, halfOpenAttempts? })` | Circuit breaker state machine | 5 failures, 30s reset, 3 probes |
|
|
21
|
+
| `@Timeout(durationMs)` | Reject if method exceeds duration | required |
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { Retryable, CircuitBreaker, Timeout } from '@goodie-ts/resilience';
|
|
27
|
+
import { Singleton } from '@goodie-ts/decorators';
|
|
28
|
+
|
|
29
|
+
@Singleton()
|
|
30
|
+
class PaymentService {
|
|
31
|
+
@Timeout(5000)
|
|
32
|
+
@CircuitBreaker({ failureThreshold: 3, resetTimeout: 10_000 })
|
|
33
|
+
@Retryable({ maxAttempts: 3, delay: 500 })
|
|
34
|
+
async charge(amount: number) {
|
|
35
|
+
return fetch('/api/charge', { body: JSON.stringify({ amount }) });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Error Types
|
|
41
|
+
|
|
42
|
+
- `TimeoutError` — thrown when `@Timeout` duration is exceeded
|
|
43
|
+
- `CircuitOpenError` — thrown when the circuit breaker is OPEN and rejecting calls
|
|
44
|
+
|
|
45
|
+
## Vite Plugin Setup
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { diPlugin } from '@goodie-ts/vite-plugin';
|
|
49
|
+
import { createResiliencePlugin } from '@goodie-ts/resilience';
|
|
50
|
+
|
|
51
|
+
export default defineConfig({
|
|
52
|
+
plugins: [diPlugin({ plugins: [createResiliencePlugin()] })],
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
[MIT](https://github.com/GOOD-Code-ApS/goodie/blob/main/LICENSE)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { InvocationContext, MethodInterceptor } from '@goodie-ts/aop';
|
|
2
|
+
type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
|
|
3
|
+
/** Error thrown when the circuit breaker is open and rejecting calls. */
|
|
4
|
+
export declare class CircuitOpenError extends Error {
|
|
5
|
+
constructor(methodKey: string);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* AOP interceptor implementing the circuit breaker pattern.
|
|
9
|
+
*
|
|
10
|
+
* State machine: CLOSED → OPEN → HALF_OPEN → CLOSED (on success) or OPEN (on failure).
|
|
11
|
+
*
|
|
12
|
+
* Each decorated method gets its own circuit, keyed by `className:methodName`.
|
|
13
|
+
*/
|
|
14
|
+
export declare class CircuitBreakerInterceptor implements MethodInterceptor {
|
|
15
|
+
private readonly circuits;
|
|
16
|
+
intercept(ctx: InvocationContext): unknown;
|
|
17
|
+
/** Visible for testing — get the current state of a circuit. */
|
|
18
|
+
getCircuitState(className: string, methodName: string): CircuitState;
|
|
19
|
+
private getOrCreateCircuit;
|
|
20
|
+
private onSuccess;
|
|
21
|
+
private onFailure;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=circuit-breaker-interceptor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-breaker-interceptor.d.ts","sourceRoot":"","sources":["../src/circuit-breaker-interceptor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAE3E,KAAK,YAAY,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;AAoBpD,yEAAyE;AACzE,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,SAAS,EAAE,MAAM;CAI9B;AAED;;;;;;GAMG;AACH,qBAAa,yBAA0B,YAAW,iBAAiB;IACjE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;IAE5D,SAAS,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO;IAyD1C,gEAAgE;IAChE,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,YAAY;IAKpE,OAAO,CAAC,kBAAkB;IAkB1B,OAAO,CAAC,SAAS;IAajB,OAAO,CAAC,SAAS;CAclB"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** Error thrown when the circuit breaker is open and rejecting calls. */
|
|
2
|
+
export class CircuitOpenError extends Error {
|
|
3
|
+
constructor(methodKey) {
|
|
4
|
+
super(`Circuit breaker is OPEN for ${methodKey}`);
|
|
5
|
+
this.name = 'CircuitOpenError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* AOP interceptor implementing the circuit breaker pattern.
|
|
10
|
+
*
|
|
11
|
+
* State machine: CLOSED → OPEN → HALF_OPEN → CLOSED (on success) or OPEN (on failure).
|
|
12
|
+
*
|
|
13
|
+
* Each decorated method gets its own circuit, keyed by `className:methodName`.
|
|
14
|
+
*/
|
|
15
|
+
export class CircuitBreakerInterceptor {
|
|
16
|
+
circuits = new Map();
|
|
17
|
+
intercept(ctx) {
|
|
18
|
+
const meta = ctx.metadata;
|
|
19
|
+
if (!meta)
|
|
20
|
+
return ctx.proceed();
|
|
21
|
+
const key = `${ctx.className}:${ctx.methodName}`;
|
|
22
|
+
const circuit = this.getOrCreateCircuit(key, meta);
|
|
23
|
+
if (circuit.state === 'OPEN') {
|
|
24
|
+
const elapsed = Date.now() - circuit.lastFailureTime;
|
|
25
|
+
if (elapsed >= circuit.resetTimeout) {
|
|
26
|
+
circuit.state = 'HALF_OPEN';
|
|
27
|
+
circuit.successCount = 0;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
throw new CircuitOpenError(key);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Only allow one probe at a time during HALF_OPEN. Concurrent calls are
|
|
34
|
+
// rejected immediately so a burst of requests doesn't overwhelm a
|
|
35
|
+
// recovering backend.
|
|
36
|
+
if (circuit.state === 'HALF_OPEN' && circuit.halfOpenProbeInFlight) {
|
|
37
|
+
throw new CircuitOpenError(key);
|
|
38
|
+
}
|
|
39
|
+
const isProbe = circuit.state === 'HALF_OPEN';
|
|
40
|
+
if (isProbe) {
|
|
41
|
+
circuit.halfOpenProbeInFlight = true;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const result = ctx.proceed();
|
|
45
|
+
if (result instanceof Promise) {
|
|
46
|
+
return result.then((value) => {
|
|
47
|
+
if (isProbe)
|
|
48
|
+
circuit.halfOpenProbeInFlight = false;
|
|
49
|
+
this.onSuccess(circuit);
|
|
50
|
+
return value;
|
|
51
|
+
}, (error) => {
|
|
52
|
+
if (isProbe)
|
|
53
|
+
circuit.halfOpenProbeInFlight = false;
|
|
54
|
+
this.onFailure(circuit);
|
|
55
|
+
throw error;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (isProbe)
|
|
59
|
+
circuit.halfOpenProbeInFlight = false;
|
|
60
|
+
this.onSuccess(circuit);
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (isProbe)
|
|
65
|
+
circuit.halfOpenProbeInFlight = false;
|
|
66
|
+
this.onFailure(circuit);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Visible for testing — get the current state of a circuit. */
|
|
71
|
+
getCircuitState(className, methodName) {
|
|
72
|
+
const key = `${className}:${methodName}`;
|
|
73
|
+
return this.circuits.get(key)?.state ?? 'CLOSED';
|
|
74
|
+
}
|
|
75
|
+
getOrCreateCircuit(key, meta) {
|
|
76
|
+
let circuit = this.circuits.get(key);
|
|
77
|
+
if (!circuit) {
|
|
78
|
+
circuit = {
|
|
79
|
+
state: 'CLOSED',
|
|
80
|
+
failureCount: 0,
|
|
81
|
+
successCount: 0,
|
|
82
|
+
lastFailureTime: 0,
|
|
83
|
+
failureThreshold: meta.failureThreshold,
|
|
84
|
+
resetTimeout: meta.resetTimeout,
|
|
85
|
+
halfOpenAttempts: meta.halfOpenAttempts,
|
|
86
|
+
halfOpenProbeInFlight: false,
|
|
87
|
+
};
|
|
88
|
+
this.circuits.set(key, circuit);
|
|
89
|
+
}
|
|
90
|
+
return circuit;
|
|
91
|
+
}
|
|
92
|
+
onSuccess(circuit) {
|
|
93
|
+
if (circuit.state === 'HALF_OPEN') {
|
|
94
|
+
circuit.successCount++;
|
|
95
|
+
if (circuit.successCount >= circuit.halfOpenAttempts) {
|
|
96
|
+
circuit.state = 'CLOSED';
|
|
97
|
+
circuit.failureCount = 0;
|
|
98
|
+
circuit.successCount = 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else if (circuit.state === 'CLOSED') {
|
|
102
|
+
circuit.failureCount = 0;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
onFailure(circuit) {
|
|
106
|
+
circuit.failureCount++;
|
|
107
|
+
circuit.lastFailureTime = Date.now();
|
|
108
|
+
if (circuit.state === 'HALF_OPEN') {
|
|
109
|
+
circuit.state = 'OPEN';
|
|
110
|
+
circuit.successCount = 0;
|
|
111
|
+
}
|
|
112
|
+
else if (circuit.state === 'CLOSED' &&
|
|
113
|
+
circuit.failureCount >= circuit.failureThreshold) {
|
|
114
|
+
circuit.state = 'OPEN';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=circuit-breaker-interceptor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-breaker-interceptor.js","sourceRoot":"","sources":["../src/circuit-breaker-interceptor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface CircuitBreakerOptions {
|
|
2
|
+
/** Number of failures before opening the circuit (default: 5). */
|
|
3
|
+
failureThreshold?: number;
|
|
4
|
+
/** Time in ms before moving from OPEN to HALF_OPEN (default: 30000). */
|
|
5
|
+
resetTimeout?: number;
|
|
6
|
+
/** Number of successes in HALF_OPEN needed to close the circuit (default: 1). */
|
|
7
|
+
halfOpenAttempts?: number;
|
|
8
|
+
}
|
|
9
|
+
type MethodDecorator_Stage3 = (target: (...args: never) => unknown, context: ClassMethodDecoratorContext) => void;
|
|
10
|
+
/**
|
|
11
|
+
* Mark a method for circuit breaker protection.
|
|
12
|
+
*
|
|
13
|
+
* At compile time, the resilience transformer plugin reads this decorator
|
|
14
|
+
* and wires the `CircuitBreakerInterceptor` via AOP.
|
|
15
|
+
*/
|
|
16
|
+
export declare function CircuitBreaker(_opts?: CircuitBreakerOptions): MethodDecorator_Stage3;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=circuit-breaker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-breaker.d.ts","sourceRoot":"","sources":["../../src/decorators/circuit-breaker.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACpC,kEAAkE;IAClE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,wEAAwE;IACxE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,KAAK,sBAAsB,GAAG,CAC5B,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,EACnC,OAAO,EAAE,2BAA2B,KACjC,IAAI,CAAC;AAEV;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,KAAK,CAAC,EAAE,qBAAqB,GAC5B,sBAAsB,CAIxB"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mark a method for circuit breaker protection.
|
|
3
|
+
*
|
|
4
|
+
* At compile time, the resilience transformer plugin reads this decorator
|
|
5
|
+
* and wires the `CircuitBreakerInterceptor` via AOP.
|
|
6
|
+
*/
|
|
7
|
+
export function CircuitBreaker(_opts) {
|
|
8
|
+
return (_target, _context) => {
|
|
9
|
+
// No-op: read at compile time by the resilience transformer plugin
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=circuit-breaker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-breaker.js","sourceRoot":"","sources":["../../src/decorators/circuit-breaker.ts"],"names":[],"mappings":"AAcA;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAC5B,KAA6B;IAE7B,OAAO,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE;QAC3B,mEAAmE;IACrE,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface RetryOptions {
|
|
2
|
+
/** Maximum number of retry attempts (default: 3). */
|
|
3
|
+
maxAttempts?: number;
|
|
4
|
+
/** Delay between retries in milliseconds (default: 1000). */
|
|
5
|
+
delay?: number;
|
|
6
|
+
/** Multiplier for exponential backoff (default: 1 — no backoff). */
|
|
7
|
+
multiplier?: number;
|
|
8
|
+
}
|
|
9
|
+
type MethodDecorator_Stage3 = (target: (...args: never) => unknown, context: ClassMethodDecoratorContext) => void;
|
|
10
|
+
/**
|
|
11
|
+
* Mark a method for automatic retry on failure.
|
|
12
|
+
*
|
|
13
|
+
* At compile time, the resilience transformer plugin reads this decorator
|
|
14
|
+
* and wires the `RetryInterceptor` via AOP. No runtime metadata is stored.
|
|
15
|
+
*/
|
|
16
|
+
export declare function Retryable(_opts?: RetryOptions): MethodDecorator_Stage3;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=retryable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retryable.d.ts","sourceRoot":"","sources":["../../src/decorators/retryable.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,KAAK,sBAAsB,GAAG,CAC5B,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,EACnC,OAAO,EAAE,2BAA2B,KACjC,IAAI,CAAC;AAEV;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,CAAC,EAAE,YAAY,GAAG,sBAAsB,CAItE"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mark a method for automatic retry on failure.
|
|
3
|
+
*
|
|
4
|
+
* At compile time, the resilience transformer plugin reads this decorator
|
|
5
|
+
* and wires the `RetryInterceptor` via AOP. No runtime metadata is stored.
|
|
6
|
+
*/
|
|
7
|
+
export function Retryable(_opts) {
|
|
8
|
+
return (_target, _context) => {
|
|
9
|
+
// No-op: read at compile time by the resilience transformer plugin
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=retryable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retryable.js","sourceRoot":"","sources":["../../src/decorators/retryable.ts"],"names":[],"mappings":"AAcA;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,KAAoB;IAC5C,OAAO,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE;QAC3B,mEAAmE;IACrE,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type MethodDecorator_Stage3 = (target: (...args: never) => unknown, context: ClassMethodDecoratorContext) => void;
|
|
2
|
+
/**
|
|
3
|
+
* Mark a method for automatic timeout.
|
|
4
|
+
*
|
|
5
|
+
* At compile time, the resilience transformer plugin reads this decorator
|
|
6
|
+
* and wires the `TimeoutInterceptor` via AOP.
|
|
7
|
+
*
|
|
8
|
+
* @param duration - Timeout duration in milliseconds.
|
|
9
|
+
*/
|
|
10
|
+
export declare function Timeout(_duration: number): MethodDecorator_Stage3;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=timeout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeout.d.ts","sourceRoot":"","sources":["../../src/decorators/timeout.ts"],"names":[],"mappings":"AAAA,KAAK,sBAAsB,GAAG,CAC5B,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,EACnC,OAAO,EAAE,2BAA2B,KACjC,IAAI,CAAC;AAEV;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,sBAAsB,CAIjE"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mark a method for automatic timeout.
|
|
3
|
+
*
|
|
4
|
+
* At compile time, the resilience transformer plugin reads this decorator
|
|
5
|
+
* and wires the `TimeoutInterceptor` via AOP.
|
|
6
|
+
*
|
|
7
|
+
* @param duration - Timeout duration in milliseconds.
|
|
8
|
+
*/
|
|
9
|
+
export function Timeout(_duration) {
|
|
10
|
+
return (_target, _context) => {
|
|
11
|
+
// No-op: read at compile time by the resilience transformer plugin
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=timeout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeout.js","sourceRoot":"","sources":["../../src/decorators/timeout.ts"],"names":[],"mappings":"AAKA;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CAAC,SAAiB;IACvC,OAAO,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE;QAC3B,mEAAmE;IACrE,CAAC,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { CircuitBreakerInterceptor, CircuitOpenError, } from './circuit-breaker-interceptor.js';
|
|
2
|
+
export { CircuitBreaker } from './decorators/circuit-breaker.js';
|
|
3
|
+
export { Retryable } from './decorators/retryable.js';
|
|
4
|
+
export { Timeout } from './decorators/timeout.js';
|
|
5
|
+
export { createResiliencePlugin } from './resilience-transformer-plugin.js';
|
|
6
|
+
export { RetryInterceptor } from './retry-interceptor.js';
|
|
7
|
+
export { TimeoutError, TimeoutInterceptor } from './timeout-interceptor.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,EACzB,gBAAgB,GACjB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACjE,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { CircuitBreakerInterceptor, CircuitOpenError, } from './circuit-breaker-interceptor.js';
|
|
2
|
+
export { CircuitBreaker } from './decorators/circuit-breaker.js';
|
|
3
|
+
export { Retryable } from './decorators/retryable.js';
|
|
4
|
+
export { Timeout } from './decorators/timeout.js';
|
|
5
|
+
export { createResiliencePlugin } from './resilience-transformer-plugin.js';
|
|
6
|
+
export { RetryInterceptor } from './retry-interceptor.js';
|
|
7
|
+
export { TimeoutError, TimeoutInterceptor } from './timeout-interceptor.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,EACzB,gBAAgB,GACjB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACjE,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TransformerPlugin } from '@goodie-ts/transformer';
|
|
2
|
+
/**
|
|
3
|
+
* Create the resilience transformer plugin.
|
|
4
|
+
*
|
|
5
|
+
* Scans @Retryable, @CircuitBreaker, and @Timeout decorators on methods
|
|
6
|
+
* and wires the appropriate interceptors as AOP dependencies at compile time.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createResiliencePlugin(): TransformerPlugin;
|
|
9
|
+
//# sourceMappingURL=resilience-transformer-plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resilience-transformer-plugin.d.ts","sourceRoot":"","sources":["../src/resilience-transformer-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,iBAAiB,EAClB,MAAM,wBAAwB,CAAC;AA4BhC;;;;;GAKG;AACH,wBAAgB,sBAAsB,IAAI,iBAAiB,CAiJ1D"}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
const DECORATOR_MAP = {
|
|
2
|
+
Retryable: 'retry',
|
|
3
|
+
CircuitBreaker: 'circuitBreaker',
|
|
4
|
+
Timeout: 'timeout',
|
|
5
|
+
};
|
|
6
|
+
/** Order values — outermost (lowest) runs first: Timeout → CircuitBreaker → Retry */
|
|
7
|
+
const ORDER_MAP = {
|
|
8
|
+
timeout: -30, // Outermost — enforces deadline
|
|
9
|
+
circuitBreaker: -20, // Middle — rejects if circuit open
|
|
10
|
+
retry: -10, // Innermost — retries close to the method
|
|
11
|
+
};
|
|
12
|
+
const INTERCEPTOR_CLASS_MAP = {
|
|
13
|
+
retry: 'RetryInterceptor',
|
|
14
|
+
circuitBreaker: 'CircuitBreakerInterceptor',
|
|
15
|
+
timeout: 'TimeoutInterceptor',
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Create the resilience transformer plugin.
|
|
19
|
+
*
|
|
20
|
+
* Scans @Retryable, @CircuitBreaker, and @Timeout decorators on methods
|
|
21
|
+
* and wires the appropriate interceptors as AOP dependencies at compile time.
|
|
22
|
+
*/
|
|
23
|
+
export function createResiliencePlugin() {
|
|
24
|
+
const classResilienceInfo = new Map();
|
|
25
|
+
return {
|
|
26
|
+
name: 'resilience',
|
|
27
|
+
beforeScan() {
|
|
28
|
+
classResilienceInfo.clear();
|
|
29
|
+
},
|
|
30
|
+
visitMethod(ctx) {
|
|
31
|
+
const decorators = ctx.methodDeclaration.getDecorators();
|
|
32
|
+
for (const decorator of decorators) {
|
|
33
|
+
const decoratorName = decorator.getName();
|
|
34
|
+
const kind = DECORATOR_MAP[decoratorName];
|
|
35
|
+
if (!kind)
|
|
36
|
+
continue;
|
|
37
|
+
const args = decorator.getArguments();
|
|
38
|
+
const metadata = parseMetadata(kind, args);
|
|
39
|
+
const key = `${ctx.filePath}:${ctx.className}`;
|
|
40
|
+
const existing = classResilienceInfo.get(key) ?? [];
|
|
41
|
+
existing.push({
|
|
42
|
+
methodName: ctx.methodName,
|
|
43
|
+
kind,
|
|
44
|
+
metadata,
|
|
45
|
+
});
|
|
46
|
+
classResilienceInfo.set(key, existing);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
afterResolve(beans) {
|
|
50
|
+
const usedInterceptors = new Set();
|
|
51
|
+
for (const bean of beans) {
|
|
52
|
+
const className = bean.tokenRef.kind === 'class' ? bean.tokenRef.className : undefined;
|
|
53
|
+
if (!className)
|
|
54
|
+
continue;
|
|
55
|
+
const key = `${bean.tokenRef.importPath}:${className}`;
|
|
56
|
+
const infos = classResilienceInfo.get(key);
|
|
57
|
+
if (!infos || infos.length === 0)
|
|
58
|
+
continue;
|
|
59
|
+
const existing = (bean.metadata.interceptedMethods ?? []);
|
|
60
|
+
for (const info of infos) {
|
|
61
|
+
const interceptorClassName = INTERCEPTOR_CLASS_MAP[info.kind];
|
|
62
|
+
usedInterceptors.add(interceptorClassName);
|
|
63
|
+
const methodEntry = existing.find((m) => m.methodName === info.methodName);
|
|
64
|
+
const interceptorRef = {
|
|
65
|
+
className: interceptorClassName,
|
|
66
|
+
importPath: '@goodie-ts/resilience',
|
|
67
|
+
adviceType: 'around',
|
|
68
|
+
order: ORDER_MAP[info.kind],
|
|
69
|
+
metadata: info.metadata,
|
|
70
|
+
};
|
|
71
|
+
if (methodEntry) {
|
|
72
|
+
methodEntry.interceptors.push(interceptorRef);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
existing.push({
|
|
76
|
+
methodName: info.methodName,
|
|
77
|
+
interceptors: [interceptorRef],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
bean.metadata.interceptedMethods = existing;
|
|
82
|
+
}
|
|
83
|
+
if (usedInterceptors.size === 0)
|
|
84
|
+
return beans;
|
|
85
|
+
// Add synthetic beans for each used interceptor
|
|
86
|
+
const syntheticBeans = [];
|
|
87
|
+
for (const interceptorClassName of usedInterceptors) {
|
|
88
|
+
syntheticBeans.push({
|
|
89
|
+
tokenRef: {
|
|
90
|
+
kind: 'class',
|
|
91
|
+
className: interceptorClassName,
|
|
92
|
+
importPath: '@goodie-ts/resilience',
|
|
93
|
+
},
|
|
94
|
+
scope: 'singleton',
|
|
95
|
+
eager: false,
|
|
96
|
+
name: undefined,
|
|
97
|
+
constructorDeps: [],
|
|
98
|
+
fieldDeps: [],
|
|
99
|
+
factoryKind: 'constructor',
|
|
100
|
+
providesSource: undefined,
|
|
101
|
+
metadata: {},
|
|
102
|
+
sourceLocation: {
|
|
103
|
+
filePath: '@goodie-ts/resilience',
|
|
104
|
+
line: 0,
|
|
105
|
+
column: 0,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return [...beans, ...syntheticBeans];
|
|
110
|
+
},
|
|
111
|
+
codegen(beans) {
|
|
112
|
+
const usedClasses = new Set();
|
|
113
|
+
for (const bean of beans) {
|
|
114
|
+
const methods = bean.metadata.interceptedMethods;
|
|
115
|
+
if (!methods)
|
|
116
|
+
continue;
|
|
117
|
+
for (const m of methods) {
|
|
118
|
+
for (const i of m.interceptors) {
|
|
119
|
+
if (i.importPath === '@goodie-ts/resilience') {
|
|
120
|
+
usedClasses.add(i.className);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (usedClasses.size === 0)
|
|
126
|
+
return {};
|
|
127
|
+
const classNames = [...usedClasses].sort().join(', ');
|
|
128
|
+
return {
|
|
129
|
+
imports: [
|
|
130
|
+
`import { ${classNames} } from '@goodie-ts/resilience'`,
|
|
131
|
+
"import { buildInterceptorChain } from '@goodie-ts/aop'",
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** Strip TypeScript numeric separators: 1_000 → 1000 */
|
|
138
|
+
function stripSeparators(s) {
|
|
139
|
+
return s.replace(/_/g, '');
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Parse decorator arguments into metadata.
|
|
143
|
+
*
|
|
144
|
+
* **Limitation:** Only literal numeric values are supported. Const references
|
|
145
|
+
* or computed expressions (e.g. `maxAttempts: MAX_RETRIES`) will fall back to
|
|
146
|
+
* defaults silently. This matches the compile-time AST text parsing approach
|
|
147
|
+
* used by all goodie-ts transformer plugins.
|
|
148
|
+
*/
|
|
149
|
+
function parseMetadata(kind, args) {
|
|
150
|
+
switch (kind) {
|
|
151
|
+
case 'retry': {
|
|
152
|
+
const defaults = { maxAttempts: 3, delay: 1000, multiplier: 1 };
|
|
153
|
+
if (args.length === 0)
|
|
154
|
+
return defaults;
|
|
155
|
+
const text = args[0].getText();
|
|
156
|
+
const maxMatch = text.match(/maxAttempts\s*:\s*(\d[\d_]*)/);
|
|
157
|
+
const delayMatch = text.match(/delay\s*:\s*(\d[\d_]*)/);
|
|
158
|
+
const multMatch = text.match(/multiplier\s*:\s*([\d_]+(?:\.[\d_]+)?)/);
|
|
159
|
+
return {
|
|
160
|
+
maxAttempts: maxMatch
|
|
161
|
+
? Number.parseInt(stripSeparators(maxMatch[1]), 10)
|
|
162
|
+
: defaults.maxAttempts,
|
|
163
|
+
delay: delayMatch
|
|
164
|
+
? Number.parseInt(stripSeparators(delayMatch[1]), 10)
|
|
165
|
+
: defaults.delay,
|
|
166
|
+
multiplier: multMatch
|
|
167
|
+
? Number.parseFloat(stripSeparators(multMatch[1]))
|
|
168
|
+
: defaults.multiplier,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
case 'circuitBreaker': {
|
|
172
|
+
const defaults = {
|
|
173
|
+
failureThreshold: 5,
|
|
174
|
+
resetTimeout: 30000,
|
|
175
|
+
halfOpenAttempts: 1,
|
|
176
|
+
};
|
|
177
|
+
if (args.length === 0)
|
|
178
|
+
return defaults;
|
|
179
|
+
const text = args[0].getText();
|
|
180
|
+
const threshMatch = text.match(/failureThreshold\s*:\s*(\d[\d_]*)/);
|
|
181
|
+
const resetMatch = text.match(/resetTimeout\s*:\s*(\d[\d_]*)/);
|
|
182
|
+
const halfMatch = text.match(/halfOpenAttempts\s*:\s*(\d[\d_]*)/);
|
|
183
|
+
return {
|
|
184
|
+
failureThreshold: threshMatch
|
|
185
|
+
? Number.parseInt(stripSeparators(threshMatch[1]), 10)
|
|
186
|
+
: defaults.failureThreshold,
|
|
187
|
+
resetTimeout: resetMatch
|
|
188
|
+
? Number.parseInt(stripSeparators(resetMatch[1]), 10)
|
|
189
|
+
: defaults.resetTimeout,
|
|
190
|
+
halfOpenAttempts: halfMatch
|
|
191
|
+
? Number.parseInt(stripSeparators(halfMatch[1]), 10)
|
|
192
|
+
: defaults.halfOpenAttempts,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
case 'timeout': {
|
|
196
|
+
if (args.length === 0)
|
|
197
|
+
return { duration: 5000 };
|
|
198
|
+
const text = stripSeparators(args[0].getText());
|
|
199
|
+
const num = Number.parseInt(text, 10);
|
|
200
|
+
if (Number.isNaN(num)) {
|
|
201
|
+
if (text.includes('{')) {
|
|
202
|
+
console.warn(`[resilience] @Timeout received an object literal argument (${text}). ` +
|
|
203
|
+
'Only numeric durations are supported — falling back to 5000ms.');
|
|
204
|
+
}
|
|
205
|
+
return { duration: 5000 };
|
|
206
|
+
}
|
|
207
|
+
return { duration: num };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=resilience-transformer-plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resilience-transformer-plugin.js","sourceRoot":"","sources":["../src/resilience-transformer-plugin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { InvocationContext, MethodInterceptor } from '@goodie-ts/aop';
|
|
2
|
+
/**
|
|
3
|
+
* AOP interceptor that retries failed method calls with configurable
|
|
4
|
+
* backoff strategy (exponential backoff with random jitter).
|
|
5
|
+
*
|
|
6
|
+
* Reads retry configuration from `ctx.metadata` (set by the resilience
|
|
7
|
+
* transformer plugin).
|
|
8
|
+
*
|
|
9
|
+
* **Design note — interceptor chain:** Retry sits innermost in the interceptor
|
|
10
|
+
* chain (order -10). On retry, `proceed()` calls the target method directly —
|
|
11
|
+
* outer interceptors (circuit breaker, timeout) are NOT re-entered. This is
|
|
12
|
+
* intentional: the timeout deadline applies to the total call including all
|
|
13
|
+
* retries, and the circuit breaker tracks the overall outcome, not individual
|
|
14
|
+
* retry attempts.
|
|
15
|
+
*
|
|
16
|
+
* **Design note — sync methods:** When a sync method fails and retries are
|
|
17
|
+
* needed, the retry delay uses `setTimeout`, which returns a `Promise`.
|
|
18
|
+
* As a result, decorated sync methods effectively become async on the first
|
|
19
|
+
* failure. Callers should always `await` the return value of `@Retryable`
|
|
20
|
+
* methods.
|
|
21
|
+
*/
|
|
22
|
+
export declare class RetryInterceptor implements MethodInterceptor {
|
|
23
|
+
intercept(ctx: InvocationContext): unknown;
|
|
24
|
+
private tryCall;
|
|
25
|
+
private handleError;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=retry-interceptor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry-interceptor.d.ts","sourceRoot":"","sources":["../src/retry-interceptor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAS3E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,gBAAiB,YAAW,iBAAiB;IACxD,SAAS,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO;IAQ1C,OAAO,CAAC,OAAO;IAoBf,OAAO,CAAC,WAAW;CA+BpB"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AOP interceptor that retries failed method calls with configurable
|
|
3
|
+
* backoff strategy (exponential backoff with random jitter).
|
|
4
|
+
*
|
|
5
|
+
* Reads retry configuration from `ctx.metadata` (set by the resilience
|
|
6
|
+
* transformer plugin).
|
|
7
|
+
*
|
|
8
|
+
* **Design note — interceptor chain:** Retry sits innermost in the interceptor
|
|
9
|
+
* chain (order -10). On retry, `proceed()` calls the target method directly —
|
|
10
|
+
* outer interceptors (circuit breaker, timeout) are NOT re-entered. This is
|
|
11
|
+
* intentional: the timeout deadline applies to the total call including all
|
|
12
|
+
* retries, and the circuit breaker tracks the overall outcome, not individual
|
|
13
|
+
* retry attempts.
|
|
14
|
+
*
|
|
15
|
+
* **Design note — sync methods:** When a sync method fails and retries are
|
|
16
|
+
* needed, the retry delay uses `setTimeout`, which returns a `Promise`.
|
|
17
|
+
* As a result, decorated sync methods effectively become async on the first
|
|
18
|
+
* failure. Callers should always `await` the return value of `@Retryable`
|
|
19
|
+
* methods.
|
|
20
|
+
*/
|
|
21
|
+
export class RetryInterceptor {
|
|
22
|
+
intercept(ctx) {
|
|
23
|
+
const meta = ctx.metadata;
|
|
24
|
+
if (!meta)
|
|
25
|
+
return ctx.proceed();
|
|
26
|
+
const result = this.tryCall(ctx, meta, 1);
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
tryCall(ctx, meta, attempt) {
|
|
30
|
+
try {
|
|
31
|
+
const result = ctx.proceed();
|
|
32
|
+
if (result instanceof Promise) {
|
|
33
|
+
return result.catch((error) => this.handleError(ctx, meta, attempt, error));
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
return this.handleError(ctx, meta, attempt, error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
handleError(ctx, meta, attempt, error) {
|
|
42
|
+
if (attempt >= meta.maxAttempts) {
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
// Exponential backoff with random jitter (50–100% of computed delay)
|
|
46
|
+
// to prevent thundering herd when many callers retry simultaneously.
|
|
47
|
+
const baseDelay = meta.delay * meta.multiplier ** (attempt - 1);
|
|
48
|
+
const delayMs = baseDelay * (0.5 + Math.random() * 0.5);
|
|
49
|
+
// For async methods, use setTimeout-based delay
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
try {
|
|
53
|
+
const result = this.tryCall(ctx, meta, attempt + 1);
|
|
54
|
+
if (result instanceof Promise) {
|
|
55
|
+
result.then(resolve, reject);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
resolve(result);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (retryError) {
|
|
62
|
+
reject(retryError);
|
|
63
|
+
}
|
|
64
|
+
}, delayMs);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=retry-interceptor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry-interceptor.js","sourceRoot":"","sources":["../src/retry-interceptor.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,OAAO,gBAAgB;IAC3B,SAAS,CAAC,GAAsB;QAC9B,MAAM,IAAI,GAAG,GAAG,CAAC,QAAqC,CAAC;QACvD,IAAI,CAAC,IAAI;YAAE,OAAO,GAAG,CAAC,OAAO,EAAE,CAAC;QAEhC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QAC1C,OAAO,MAAM,CAAC;IAChB,CAAC;IAEO,OAAO,CACb,GAAsB,EACtB,IAAmB,EACnB,OAAe;QAEf,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC;YAE7B,IAAI,MAAM,YAAY,OAAO,EAAE,CAAC;gBAC9B,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAC5B,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,CAC5C,CAAC;YACJ,CAAC;YAED,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;IAEO,WAAW,CACjB,GAAsB,EACtB,IAAmB,EACnB,OAAe,EACf,KAAc;QAEd,IAAI,OAAO,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YAChC,MAAM,KAAK,CAAC;QACd,CAAC;QAED,qEAAqE;QACrE,qEAAqE;QACrE,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,UAAU,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;QAChE,MAAM,OAAO,GAAG,SAAS,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC;QAExD,gDAAgD;QAChD,OAAO,IAAI,OAAO,CAAU,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAC9C,UAAU,CAAC,GAAG,EAAE;gBACd,IAAI,CAAC;oBACH,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC;oBACpD,IAAI,MAAM,YAAY,OAAO,EAAE,CAAC;wBAC9B,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;oBAC/B,CAAC;yBAAM,CAAC;wBACN,OAAO,CAAC,MAAM,CAAC,CAAC;oBAClB,CAAC;gBACH,CAAC;gBAAC,OAAO,UAAU,EAAE,CAAC;oBACpB,MAAM,CAAC,UAAU,CAAC,CAAC;gBACrB,CAAC;YACH,CAAC,EAAE,OAAO,CAAC,CAAC;QACd,CAAC,CAAC,CAAC;IACL,CAAC;CACF"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { InvocationContext, MethodInterceptor } from '@goodie-ts/aop';
|
|
2
|
+
/** Error thrown when a method call exceeds its timeout. */
|
|
3
|
+
export declare class TimeoutError extends Error {
|
|
4
|
+
constructor(methodKey: string, duration: number);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* AOP interceptor that enforces a timeout on method execution.
|
|
8
|
+
*
|
|
9
|
+
* For async methods, uses `Promise.race` with a timeout promise.
|
|
10
|
+
* For sync methods, the timeout cannot be enforced (sync code blocks the
|
|
11
|
+
* event loop), so the result is returned as-is.
|
|
12
|
+
*/
|
|
13
|
+
export declare class TimeoutInterceptor implements MethodInterceptor {
|
|
14
|
+
intercept(ctx: InvocationContext): unknown;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=timeout-interceptor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeout-interceptor.d.ts","sourceRoot":"","sources":["../src/timeout-interceptor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAO3E,2DAA2D;AAC3D,qBAAa,YAAa,SAAQ,KAAK;gBACzB,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;CAIhD;AAED;;;;;;GAMG;AACH,qBAAa,kBAAmB,YAAW,iBAAiB;IAC1D,SAAS,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO;CAuB3C"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Error thrown when a method call exceeds its timeout. */
|
|
2
|
+
export class TimeoutError extends Error {
|
|
3
|
+
constructor(methodKey, duration) {
|
|
4
|
+
super(`Method ${methodKey} timed out after ${duration}ms`);
|
|
5
|
+
this.name = 'TimeoutError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* AOP interceptor that enforces a timeout on method execution.
|
|
10
|
+
*
|
|
11
|
+
* For async methods, uses `Promise.race` with a timeout promise.
|
|
12
|
+
* For sync methods, the timeout cannot be enforced (sync code blocks the
|
|
13
|
+
* event loop), so the result is returned as-is.
|
|
14
|
+
*/
|
|
15
|
+
export class TimeoutInterceptor {
|
|
16
|
+
intercept(ctx) {
|
|
17
|
+
const meta = ctx.metadata;
|
|
18
|
+
if (!meta)
|
|
19
|
+
return ctx.proceed();
|
|
20
|
+
const key = `${ctx.className}:${ctx.methodName}`;
|
|
21
|
+
const result = ctx.proceed();
|
|
22
|
+
// Timeout only applies to async methods (sync methods can't be interrupted)
|
|
23
|
+
if (result instanceof Promise) {
|
|
24
|
+
let timeoutId;
|
|
25
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
26
|
+
timeoutId = setTimeout(() => reject(new TimeoutError(key, meta.duration)), meta.duration);
|
|
27
|
+
});
|
|
28
|
+
return Promise.race([result, timeoutPromise]).finally(() => clearTimeout(timeoutId));
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=timeout-interceptor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeout-interceptor.js","sourceRoot":"","sources":["../src/timeout-interceptor.ts"],"names":[],"mappings":"AAOA,2DAA2D;AAC3D,MAAM,OAAO,YAAa,SAAQ,KAAK;IACrC,YAAY,SAAiB,EAAE,QAAgB;QAC7C,KAAK,CAAC,UAAU,SAAS,oBAAoB,QAAQ,IAAI,CAAC,CAAC;QAC3D,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;IAC7B,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,kBAAkB;IAC7B,SAAS,CAAC,GAAsB;QAC9B,MAAM,IAAI,GAAG,GAAG,CAAC,QAAuC,CAAC;QACzD,IAAI,CAAC,IAAI;YAAE,OAAO,GAAG,CAAC,OAAO,EAAE,CAAC;QAEhC,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC,SAAS,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;QACjD,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC;QAE7B,4EAA4E;QAC5E,IAAI,MAAM,YAAY,OAAO,EAAE,CAAC;YAC9B,IAAI,SAAwC,CAAC;YAC7C,MAAM,cAAc,GAAG,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE;gBACtD,SAAS,GAAG,UAAU,CACpB,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,EAClD,IAAI,CAAC,QAAQ,CACd,CAAC;YACJ,CAAC,CAAC,CAAC;YACH,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CACzD,YAAY,CAAC,SAAS,CAAC,CACxB,CAAC;QACJ,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@goodie-ts/resilience",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Resilience patterns for goodie-ts — @Retryable, @CircuitBreaker, @Timeout decorators",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/GOOD-Code-ApS/goodie.git",
|
|
10
|
+
"directory": "packages/resilience"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"main": "dist/index.js",
|
|
16
|
+
"types": "dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@goodie-ts/aop": "workspace:*"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@goodie-ts/transformer": "workspace:*"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@goodie-ts/transformer": "workspace:*",
|
|
35
|
+
"@types/node": "^22.0.0",
|
|
36
|
+
"ts-morph": "^24.0.0"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist"
|
|
40
|
+
]
|
|
41
|
+
}
|