@cqrs-ddd/pipeline-resilience 0.5.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 (40) hide show
  1. package/COMMERCIAL_LICENSE.txt +34 -0
  2. package/LICENSE +661 -0
  3. package/README.md +36 -0
  4. package/dist/errors/resilience-configuration.error.d.ts +13 -0
  5. package/dist/errors/resilience-configuration.error.d.ts.map +1 -0
  6. package/dist/errors/resilience-configuration.error.js +19 -0
  7. package/dist/errors/resilience-configuration.error.js.map +1 -0
  8. package/dist/errors/resilience-policy-configuration.error.d.ts +12 -0
  9. package/dist/errors/resilience-policy-configuration.error.d.ts.map +1 -0
  10. package/dist/errors/resilience-policy-configuration.error.js +17 -0
  11. package/dist/errors/resilience-policy-configuration.error.js.map +1 -0
  12. package/dist/helpers/policy-factory.d.ts +34 -0
  13. package/dist/helpers/policy-factory.d.ts.map +1 -0
  14. package/dist/helpers/policy-factory.js +189 -0
  15. package/dist/helpers/policy-factory.js.map +1 -0
  16. package/dist/helpers/resilience-context.d.ts +38 -0
  17. package/dist/helpers/resilience-context.d.ts.map +1 -0
  18. package/dist/helpers/resilience-context.js +45 -0
  19. package/dist/helpers/resilience-context.js.map +1 -0
  20. package/dist/helpers/resilience.intent.d.ts +29 -0
  21. package/dist/helpers/resilience.intent.d.ts.map +1 -0
  22. package/dist/helpers/resilience.intent.js +21 -0
  23. package/dist/helpers/resilience.intent.js.map +1 -0
  24. package/dist/index.d.ts +10 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +12 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/interfaces/resilience-options.interface.d.ts +326 -0
  29. package/dist/interfaces/resilience-options.interface.d.ts.map +1 -0
  30. package/dist/interfaces/resilience-options.interface.js +3 -0
  31. package/dist/interfaces/resilience-options.interface.js.map +1 -0
  32. package/dist/resilience-policies.d.ts +48 -0
  33. package/dist/resilience-policies.d.ts.map +1 -0
  34. package/dist/resilience-policies.js +75 -0
  35. package/dist/resilience-policies.js.map +1 -0
  36. package/dist/resilience.behavior.d.ts +64 -0
  37. package/dist/resilience.behavior.d.ts.map +1 -0
  38. package/dist/resilience.behavior.js +159 -0
  39. package/dist/resilience.behavior.js.map +1 -0
  40. package/package.json +68 -0
@@ -0,0 +1,326 @@
1
+ import type { IPolicy } from 'cockatiel';
2
+ /**
3
+ * The resilience layers that can be composed, listed from **outermost**
4
+ * (runs first / wraps everything) to **innermost** (closest to the handler).
5
+ *
6
+ * The default composition order is:
7
+ * `fallback → retry → circuitBreaker → bulkhead → timeout → handler`.
8
+ */
9
+ export type ResilienceLayer = 'fallback' | 'retry' | 'circuitBreaker' | 'bulkhead' | 'timeout';
10
+ /**
11
+ * The layers {@link ResilienceBehavior} applies around a whole handler.
12
+ * Circuit breaker and fallback belong to a dependency, so they exist only on
13
+ * named policies ({@link ResiliencePolicyOptions}).
14
+ */
15
+ export type HandlerResilienceLayer = Exclude<ResilienceLayer, 'circuitBreaker' | 'fallback'>;
16
+ /** Jitter strategy used by the exponential backoff generator. */
17
+ export type JitterStrategy = 'decorrelated' | 'none' | 'full' | 'half';
18
+ /** Backoff strategy controlling the delay between retry attempts. */
19
+ export type RetryBackoff =
20
+ /** Wait a fixed `delay` (ms) between every attempt. */
21
+ {
22
+ type: 'constant';
23
+ delay: number;
24
+ }
25
+ /**
26
+ * Decorrelated-jitter exponential backoff (recommended). All fields are
27
+ * optional and fall back to cockatiel's defaults
28
+ * (`initialDelay: 128`, `maxDelay: 30_000`, `exponent: 2`).
29
+ */
30
+ | {
31
+ type: 'exponential';
32
+ initialDelay?: number;
33
+ maxDelay?: number;
34
+ exponent?: number;
35
+ jitter?: JitterStrategy;
36
+ }
37
+ /** Walk through an explicit list of delays (ms); the last value repeats. */
38
+ | {
39
+ type: 'iterable';
40
+ delays: number[];
41
+ };
42
+ /** Retry configuration of a named policy: re-runs the call on a handled failure. */
43
+ export interface RetryPolicyOptions {
44
+ /**
45
+ * Maximum number of retry attempts after the initial call (e.g. `3` allows
46
+ * the original execution plus up to three retries), matching Cockatiel's
47
+ * `retry(..., { maxAttempts })` semantics.
48
+ */
49
+ maxAttempts: number;
50
+ /** Delay strategy between attempts. Defaults to no delay. */
51
+ backoff?: RetryBackoff;
52
+ }
53
+ /** Retry configuration of the behavior — re-runs all downstream pipeline work on a handled failure. */
54
+ export interface RetryOptions extends RetryPolicyOptions {
55
+ /**
56
+ * Explicit acknowledgement that replaying the handler/downstream behaviors is
57
+ * safe. Required for `command` and `event` retries because those request kinds
58
+ * commonly perform side effects.
59
+ *
60
+ * Queries do not require this acknowledgement.
61
+ *
62
+ * Set this only when the operation is genuinely replay-safe — for example the
63
+ * side effect is protected by a downstream idempotency key, transaction, or
64
+ * otherwise repeatable contract. `@cqrs-ddd/pipeline-idempotency` does not make
65
+ * arbitrary external effects exactly-once by itself.
66
+ *
67
+ * @example Replay-safe command retry
68
+ * ```ts
69
+ * @pipeline.wrap({ kind: 'command' }, [ResilienceBehavior, {
70
+ * retry: {
71
+ * maxAttempts: 2,
72
+ * replaySafe: true,
73
+ * backoff: { type: 'exponential', maxDelay: 1_000 },
74
+ * },
75
+ * handle: (error) => error instanceof TransientGatewayError,
76
+ * }])
77
+ * ```
78
+ */
79
+ replaySafe?: boolean;
80
+ }
81
+ /** Circuit breaker strategy controlling when the circuit opens. */
82
+ export type BreakerStrategy =
83
+ /** Open after `threshold` consecutive failures. */
84
+ {
85
+ type: 'consecutive';
86
+ threshold: number;
87
+ }
88
+ /**
89
+ * Open when the failure proportion exceeds `threshold` (0–1) within a
90
+ * rolling `duration` (ms) window. `minimumRps` avoids tripping under low load.
91
+ */
92
+ | {
93
+ type: 'sampling';
94
+ threshold: number;
95
+ duration: number;
96
+ minimumRps?: number;
97
+ }
98
+ /**
99
+ * Open when the failure proportion exceeds `threshold` (0–1) over the last
100
+ * `size` calls (count-based sliding window).
101
+ */
102
+ | {
103
+ type: 'count';
104
+ threshold: number;
105
+ size: number;
106
+ minimumNumberOfCalls?: number;
107
+ };
108
+ /** Circuit breaker configuration. */
109
+ export interface CircuitBreakerOptions {
110
+ /**
111
+ * Milliseconds the circuit stays open before allowing a trial (half-open)
112
+ * call through.
113
+ */
114
+ halfOpenAfter: number;
115
+ /** The breaker policy controlling when the circuit trips. */
116
+ breaker: BreakerStrategy;
117
+ }
118
+ /** Timeout configuration of a named policy. Underlying work stops only when it cooperates with the abort signal. */
119
+ export interface TimeoutPolicyOptions {
120
+ /** Duration in milliseconds after which the call times out. */
121
+ duration: number;
122
+ /**
123
+ * - `aggressive` (default): reject immediately with `TaskCancelledError`.
124
+ * - `cooperative`: signal cancellation and wait for the call to settle.
125
+ */
126
+ strategy?: 'aggressive' | 'cooperative';
127
+ }
128
+ /** Timeout configuration of the behavior. Underlying work stops only when it cooperates with the abort signal. */
129
+ export interface TimeoutOptions extends TimeoutPolicyOptions {
130
+ /**
131
+ * Explicit acknowledgement that an `aggressive` timeout is safe on a `command`
132
+ * or `event` handler. The caller is answered while the handler keeps running,
133
+ * so a retry, a released idempotency claim or a client retry can run the same
134
+ * side effect alongside it. Required for those request kinds unless the
135
+ * strategy is `cooperative`.
136
+ */
137
+ replaySafe?: boolean;
138
+ }
139
+ /** Bulkhead configuration — limits concurrent in-flight executions. */
140
+ export interface BulkheadOptions {
141
+ /** Maximum number of concurrent executions. */
142
+ limit: number;
143
+ /** Optional number of queued executions allowed beyond `limit`. */
144
+ queue?: number;
145
+ }
146
+ /** Fallback configuration — substitutes a value when execution fails. */
147
+ export type FallbackOptions =
148
+ /** Return a static `value` when a handled failure occurs. */
149
+ {
150
+ value: unknown;
151
+ }
152
+ /** Lazily produce a value when a handled failure occurs. */
153
+ | {
154
+ factory: () => unknown;
155
+ };
156
+ /** What every telemetry hook receives. */
157
+ export interface ResilienceTelemetryEvent {
158
+ /** The named policy that fired the event; absent for a handler's policy. */
159
+ policyName?: string;
160
+ }
161
+ /**
162
+ * Optional telemetry hooks fired by the underlying cockatiel policies.
163
+ *
164
+ * These are attached once per policy: per handler for the behavior, per name
165
+ * for a named policy. They are scoped to that policy rather than to a single
166
+ * request.
167
+ */
168
+ export interface ResilienceTelemetry {
169
+ /** Fired before each retry, with the upcoming attempt number and delay (ms). */
170
+ onRetry?(event: ResilienceTelemetryEvent & {
171
+ attempt: number;
172
+ delay: number;
173
+ /**
174
+ * The request that triggered this retry.
175
+ *
176
+ * A policy is cached per handler, so a handler registered for several event
177
+ * types shares one policy; without this the consumer could not tell which
178
+ * event is actually retrying.
179
+ */
180
+ requestName?: string;
181
+ }): void;
182
+ /** Fired when the circuit breaker opens (trips). */
183
+ onCircuitOpen?(event: ResilienceTelemetryEvent): void;
184
+ /** Fired when the circuit breaker closes (recovers). */
185
+ onCircuitClose?(event: ResilienceTelemetryEvent): void;
186
+ /** Fired when the circuit breaker enters the half-open trial state. */
187
+ onCircuitHalfOpen?(event: ResilienceTelemetryEvent): void;
188
+ /** Fired when a timeout is reached. */
189
+ onTimeout?(event: ResilienceTelemetryEvent): void;
190
+ /** Fired when the bulkhead rejects a call (capacity + queue exhausted). */
191
+ onBulkheadRejected?(event: ResilienceTelemetryEvent): void;
192
+ }
193
+ /**
194
+ * A named policy for an outbound dependency, declared once in
195
+ * `new ResiliencePolicies({ ... })` and shared by every caller through
196
+ * `ResiliencePolicies` or `@InjectResiliencePolicy(name)`.
197
+ *
198
+ * It wraps one outbound call, not a handler, so it offers every layer —
199
+ * a circuit breaker per dependency, a fallback value for one call — and needs
200
+ * no `replaySafe` acknowledgment: the adapter retries only its own call.
201
+ * Retry, circuit breaker and fallback still require `handle(error)` or
202
+ * `handleAllErrors: true`.
203
+ *
204
+ * @example A payment API shared by several handlers
205
+ * ```ts
206
+ * new ResiliencePolicies({
207
+ * paymentsApi: {
208
+ * handle: (error) => error instanceof PaymentGatewayUnavailableError,
209
+ * retry: { maxAttempts: 2, backoff: { type: 'exponential' } },
210
+ * circuitBreaker: { halfOpenAfter: 30_000, breaker: { type: 'consecutive', threshold: 5 } },
211
+ * timeout: { duration: 3_000, strategy: 'cooperative' },
212
+ * },
213
+ * });
214
+ * ```
215
+ */
216
+ export interface ResiliencePolicyOptions {
217
+ /** Retry the call. */
218
+ retry?: RetryPolicyOptions;
219
+ /** Circuit breaker, shared by every caller of the policy. */
220
+ circuitBreaker?: CircuitBreakerOptions;
221
+ /** Concurrency limit, shared by every caller of the policy. */
222
+ bulkhead?: BulkheadOptions;
223
+ /** Timeout of one call. */
224
+ timeout?: TimeoutPolicyOptions;
225
+ /** Value returned in place of a handled failure. */
226
+ fallback?: FallbackOptions;
227
+ /** Selects the errors retry, circuit breaker and fallback act on. */
228
+ handle?: (error: unknown) => boolean;
229
+ /** Acts on every error; set it only deliberately. */
230
+ handleAllErrors?: boolean;
231
+ /**
232
+ * Composition order, outermost first.
233
+ *
234
+ * @default ['fallback', 'retry', 'circuitBreaker', 'bulkhead', 'timeout']
235
+ */
236
+ order?: ResilienceLayer[];
237
+ /** Telemetry hooks; every event carries the policy name. */
238
+ telemetry?: ResilienceTelemetry;
239
+ }
240
+ /**
241
+ * Per-handler (or global default) resilience configuration.
242
+ *
243
+ * Compose any subset of the layers below; only the ones you set are applied.
244
+ * Supply via `pipeline.wrap(options, [ResilienceBehavior, { ... }])` for a handler, or via
245
+ * the `ResilienceBehavior` constructor as application-wide defaults.
246
+ *
247
+ * ### Safety model
248
+ *
249
+ * Handler-level resilience wraps `next()`. A retry therefore replays everything
250
+ * downstream of this behavior, including the handler itself. Because that can
251
+ * repeat side effects, the package makes the dangerous choices explicit:
252
+ *
253
+ * - timeout / bulkhead may be configured without an error classifier;
254
+ * - retry requires `handle(error)` unless `handleAllErrors: true` is
255
+ * explicitly selected;
256
+ * - retries for commands/events additionally require `retry.replaySafe: true`.
257
+ *
258
+ * A circuit breaker and a fallback belong to an outbound dependency, not to a
259
+ * handler: declare them on a named policy (`new ResiliencePolicies({ ... })`)
260
+ * and use it in the adapter, so only the remote call is repeated or short-circuited.
261
+ * The behavior contract rejects them here.
262
+ *
263
+ * @example Query retry with an explicit transient-error classifier
264
+ * ```ts
265
+ * class GetCatalogHandler {
266
+ * @pipeline.wrap({ kind: 'query' }, [ResilienceBehavior, {
267
+ * retry: { maxAttempts: 3, backoff: { type: 'exponential' } },
268
+ * timeout: { duration: 2_000 },
269
+ * handle: (error) => error instanceof CatalogUnavailableError,
270
+ * }])
271
+ * async handle(query: GetCatalogQuery) {}
272
+ * }
273
+ * ```
274
+ *
275
+ * @example Command timeout without retry
276
+ * ```ts
277
+ * class RebuildProjectionHandler {
278
+ * @pipeline.wrap({ kind: 'command' }, [ResilienceBehavior, {
279
+ * timeout: { duration: 30_000, strategy: 'cooperative' },
280
+ * }])
281
+ * async handle(command: RebuildProjectionCommand) {}
282
+ * }
283
+ * ```
284
+ */
285
+ export interface ResilienceBehaviorOptions {
286
+ /** Retry policy. */
287
+ retry?: RetryOptions;
288
+ /** Bulkhead (concurrency limiter) policy. Reused across invocations. */
289
+ bulkhead?: BulkheadOptions;
290
+ /** Timeout policy. */
291
+ timeout?: TimeoutOptions;
292
+ /**
293
+ * Predicate selecting which thrown errors are treated as *handled* failures
294
+ * (eligible for retry). Return `true` only for failures that really are safe
295
+ * for the configured policy.
296
+ *
297
+ * A declarative retry requires an explicit classifier.
298
+ * Use {@link handleAllErrors} only when handling every thrown error is
299
+ * intentionally part of the policy.
300
+ */
301
+ handle?: (error: unknown) => boolean;
302
+ /**
303
+ * Explicitly opt into Cockatiel's `handleAll` semantics when no classifier is
304
+ * supplied. Prefer {@link handle} in production so validation, authorization,
305
+ * domain and programmer errors are not retried.
306
+ */
307
+ handleAllErrors?: boolean;
308
+ /**
309
+ * Override the composition order of the configured layers. Only the listed,
310
+ * configured layers are wrapped; unlisted layers are skipped. The first entry
311
+ * is the outermost wrapper, the last is closest to the handler.
312
+ *
313
+ * @default ['retry', 'bulkhead', 'timeout']
314
+ */
315
+ order?: HandlerResilienceLayer[];
316
+ /** Optional telemetry hooks. */
317
+ telemetry?: ResilienceTelemetry;
318
+ /**
319
+ * Escape hatch: provide a fully pre-built cockatiel `IPolicy`. When set, all
320
+ * declarative options above are ignored and this policy is used verbatim.
321
+ * Declarative safety validation is skipped because the caller owns the policy
322
+ * semantics directly.
323
+ */
324
+ policy?: IPolicy;
325
+ }
326
+ //# sourceMappingURL=resilience-options.interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resilience-options.interface.d.ts","sourceRoot":"","sources":["../../src/interfaces/resilience-options.interface.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ export {};
3
+ //# sourceMappingURL=resilience-options.interface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resilience-options.interface.js","sourceRoot":"","sources":["../../src/interfaces/resilience-options.interface.ts"],"names":[],"mappings":"AAAA,sEAAsE"}
@@ -0,0 +1,48 @@
1
+ import type { PipelineLogger } from '@cqrs-ddd/pipeline';
2
+ import type { IDefaultPolicyContext } from 'cockatiel';
3
+ import { type AnyPolicy } from './helpers/policy-factory.js';
4
+ import type { ResiliencePolicyOptions } from './interfaces/resilience-options.interface.js';
5
+ /**
6
+ * The named policies of the application, for outbound adapters: a payment API
7
+ * client, an SMTP sender, a partner webhook.
8
+ *
9
+ * Every policy declared in the constructor's `options` is built once, when this registry is created at startup, and
10
+ * shared by every caller — so a circuit breaker or bulkhead tracks the
11
+ * dependency, whatever handler calls it. An invalid policy fails startup with
12
+ * {@link ResiliencePolicyConfigurationError}.
13
+ *
14
+ * @example An adapter running its call through a shared policy
15
+ * ```ts
16
+ * export class PaymentsClient {
17
+ * constructor(private readonly policies: ResiliencePolicies) {}
18
+ *
19
+ * charge(order: Order): Promise<Receipt> {
20
+ * return this.policies.execute('paymentsApi', ({ signal }) =>
21
+ * this.http.post('/charges', order, { signal }),
22
+ * );
23
+ * }
24
+ * }
25
+ * ```
26
+ */
27
+ export declare class ResiliencePolicies {
28
+ private readonly policies;
29
+ constructor(options?: Record<string, ResiliencePolicyOptions>, logger?: PipelineLogger);
30
+ /** The declared policy names, in declaration order. */
31
+ get names(): readonly string[];
32
+ /**
33
+ * Returns the shared cockatiel policy declared under `name`.
34
+ *
35
+ * @throws ResiliencePolicyConfigurationError when no policy has that name.
36
+ */
37
+ get(name: string): AnyPolicy;
38
+ /**
39
+ * Runs `fn` through the policy declared under `name`. `fn` receives the
40
+ * policy's `signal`, aborted on a cooperative timeout: pass it to the
41
+ * outbound call so the call really stops. With a fallback, the fallback
42
+ * value is returned in place of a handled failure.
43
+ *
44
+ * @throws ResiliencePolicyConfigurationError when no policy has that name.
45
+ */
46
+ execute<T>(name: string, fn: (context: IDefaultPolicyContext) => PromiseLike<T> | T): Promise<T>;
47
+ }
48
+ //# sourceMappingURL=resilience-policies.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resilience-policies.d.ts","sourceRoot":"","sources":["../src/resilience-policies.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAEvD,OAAO,EACL,KAAK,SAAS,EAEf,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,8CAA8C,CAAC;AAE5F;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgC;IAEzD,YACE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,EACjD,MAAM,CAAC,EAAE,cAAc,EAMxB;IAED,uDAAuD;IACvD,IAAI,KAAK,IAAI,SAAS,MAAM,EAAE,CAE7B;IAED;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAU3B;IAED;;;;;;;OAOG;IACH,OAAO,CAAC,CAAC,EACP,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,CAAC,OAAO,EAAE,qBAAqB,KAAK,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,GACzD,OAAO,CAAC,CAAC,CAAC,CAEZ;CACF"}
@@ -0,0 +1,75 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ import { ResiliencePolicyConfigurationError } from './errors/resilience-policy-configuration.error.js';
3
+ import { buildResiliencePolicy, } from './helpers/policy-factory.js';
4
+ /**
5
+ * The named policies of the application, for outbound adapters: a payment API
6
+ * client, an SMTP sender, a partner webhook.
7
+ *
8
+ * Every policy declared in the constructor's `options` is built once, when this registry is created at startup, and
9
+ * shared by every caller — so a circuit breaker or bulkhead tracks the
10
+ * dependency, whatever handler calls it. An invalid policy fails startup with
11
+ * {@link ResiliencePolicyConfigurationError}.
12
+ *
13
+ * @example An adapter running its call through a shared policy
14
+ * ```ts
15
+ * export class PaymentsClient {
16
+ * constructor(private readonly policies: ResiliencePolicies) {}
17
+ *
18
+ * charge(order: Order): Promise<Receipt> {
19
+ * return this.policies.execute('paymentsApi', ({ signal }) =>
20
+ * this.http.post('/charges', order, { signal }),
21
+ * );
22
+ * }
23
+ * }
24
+ * ```
25
+ */
26
+ export class ResiliencePolicies {
27
+ policies = new Map();
28
+ constructor(options, logger) {
29
+ const log = logger ?? console;
30
+ for (const [name, policyOptions] of Object.entries(options ?? {})) {
31
+ this.policies.set(name, buildNamedPolicy(name, policyOptions, log));
32
+ }
33
+ }
34
+ /** The declared policy names, in declaration order. */
35
+ get names() {
36
+ return [...this.policies.keys()];
37
+ }
38
+ /**
39
+ * Returns the shared cockatiel policy declared under `name`.
40
+ *
41
+ * @throws ResiliencePolicyConfigurationError when no policy has that name.
42
+ */
43
+ get(name) {
44
+ const policy = this.policies.get(name);
45
+ if (!policy) {
46
+ const declared = this.names.length > 0 ? this.names.join(', ') : '(none)';
47
+ throw new ResiliencePolicyConfigurationError(name, `no policy has this name; declared: ${declared}`);
48
+ }
49
+ return policy;
50
+ }
51
+ /**
52
+ * Runs `fn` through the policy declared under `name`. `fn` receives the
53
+ * policy's `signal`, aborted on a cooperative timeout: pass it to the
54
+ * outbound call so the call really stops. With a fallback, the fallback
55
+ * value is returned in place of a handled failure.
56
+ *
57
+ * @throws ResiliencePolicyConfigurationError when no policy has that name.
58
+ */
59
+ execute(name, fn) {
60
+ return this.get(name).execute(fn);
61
+ }
62
+ }
63
+ function buildNamedPolicy(name, options, logger) {
64
+ const classifiesErrors = typeof options.handle === 'function' || options.handleAllErrors === true;
65
+ if ((options.retry || options.circuitBreaker || options.fallback) &&
66
+ !classifiesErrors) {
67
+ throw new ResiliencePolicyConfigurationError(name, 'retry, circuitBreaker and fallback require handle(error) or handleAllErrors: true');
68
+ }
69
+ const policy = buildResiliencePolicy(options, { logger, policyName: name });
70
+ if (!policy) {
71
+ throw new ResiliencePolicyConfigurationError(name, 'it configures no layer (retry, circuitBreaker, bulkhead, timeout or fallback)');
72
+ }
73
+ return policy;
74
+ }
75
+ //# sourceMappingURL=resilience-policies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resilience-policies.js","sourceRoot":"","sources":["../src/resilience-policies.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAItE,OAAO,EAAE,kCAAkC,EAAE,MAAM,mDAAmD,CAAC;AACvG,OAAO,EAEL,qBAAqB,GACtB,MAAM,6BAA6B,CAAC;AAGrC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,OAAO,kBAAkB;IACZ,QAAQ,GAAG,IAAI,GAAG,EAAqB,CAAC;IAEzD,YACE,OAAiD,EACjD,MAAuB;QAEvB,MAAM,GAAG,GAAG,MAAM,IAAI,OAAO,CAAC;QAC9B,KAAK,MAAM,CAAC,IAAI,EAAE,aAAa,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE,CAAC;YAClE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,gBAAgB,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,CAAC,CAAC,CAAC;QACtE,CAAC;IACH,CAAC;IAED,uDAAuD;IACvD,IAAI,KAAK;QACP,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACnC,CAAC;IAED;;;;OAIG;IACH,GAAG,CAAC,IAAY;QACd,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACvC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;YAC1E,MAAM,IAAI,kCAAkC,CAC1C,IAAI,EACJ,sCAAsC,QAAQ,EAAE,CACjD,CAAC;QACJ,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;;;;;;OAOG;IACH,OAAO,CACL,IAAY,EACZ,EAA0D;QAE1D,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,EAAE,CAAe,CAAC;IAClD,CAAC;CACF;AAED,SAAS,gBAAgB,CACvB,IAAY,EACZ,OAAgC,EAChC,MAAsB;IAEtB,MAAM,gBAAgB,GACpB,OAAO,OAAO,CAAC,MAAM,KAAK,UAAU,IAAI,OAAO,CAAC,eAAe,KAAK,IAAI,CAAC;IAC3E,IACE,CAAC,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,cAAc,IAAI,OAAO,CAAC,QAAQ,CAAC;QAC7D,CAAC,gBAAgB,EACjB,CAAC;QACD,MAAM,IAAI,kCAAkC,CAC1C,IAAI,EACJ,mFAAmF,CACpF,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,qBAAqB,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5E,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,kCAAkC,CAC1C,IAAI,EACJ,+EAA+E,CAChF,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,64 @@
1
+ import type { PipelineLogger } from '@cqrs-ddd/pipeline';
2
+ import { type IPipelineBehavior, type IPipelineBehaviorContract, type IPipelineBehaviorOptionsResolver, type IPipelineContext, type NextDelegate, PIPELINE_BEHAVIOR_CONTRACT } from '@cqrs-ddd/pipeline';
3
+ import type { ResilienceBehaviorOptions } from './interfaces/resilience-options.interface.js';
4
+ /**
5
+ * Pipeline behavior that wraps each command / query / event handler in a
6
+ * cockatiel resilience policy — the layers that make sense around a whole
7
+ * handler: retry, timeout and bulkhead.
8
+ *
9
+ * A circuit breaker and a fallback belong to an outbound dependency, not to a
10
+ * handler: declare them on a named policy (`new ResiliencePolicies({ ... })`)
11
+ * and use it in the adapter through {@link ResiliencePolicies}. The behavior
12
+ * contract rejects them here.
13
+ *
14
+ * Resolution of the effective options for a handler where this behavior is
15
+ * attached:
16
+ * 1. Application-wide defaults, the first constructor argument.
17
+ * 2. Per-handler options from `pipeline.wrap(options, [ResilienceBehavior, { ... }])`,
18
+ * shallow-merged on top of the defaults (handler keys win).
19
+ *
20
+ * Policies are built **lazily on first invocation and cached per handler**, so
21
+ * a stateful layer (bulkhead) correctly shares state across
22
+ * every request to that handler. When no options resolve, the behavior caches
23
+ * that result and passes subsequent invocations directly to `next()` without
24
+ * constructing or executing a cockatiel policy.
25
+ *
26
+ * ### Replay / error-classification safety
27
+ *
28
+ * A handler-level retry calls `next()` again, which means the complete
29
+ * downstream pipeline and handler are replayed. To avoid accidental duplicate
30
+ * side effects, command/event retries must explicitly set
31
+ * `retry.replaySafe: true`. A retry must also define which errors are transient via `handle(error)`, unless the caller
32
+ * intentionally opts into `handleAllErrors: true`. An `aggressive` timeout
33
+ * (the default strategy) on a command/event answers the caller while the
34
+ * handler keeps running, so it requires `strategy: 'cooperative'` or
35
+ * `timeout.replaySafe: true`.
36
+ *
37
+ * Timeout and bulkhead-only policies do not require an error classifier because
38
+ * they do not decide which application errors are retryable.
39
+ * A custom pre-built Cockatiel `policy` also bypasses the declarative safety
40
+ * checks because the caller owns its semantics directly.
41
+ */
42
+ export declare class ResilienceBehavior implements IPipelineBehavior, IPipelineBehaviorOptionsResolver<ResilienceBehaviorOptions> {
43
+ private readonly defaultOptions?;
44
+ static readonly [PIPELINE_BEHAVIOR_CONTRACT]: IPipelineBehaviorContract;
45
+ private readonly logger;
46
+ /**
47
+ * Per-handler policy cache. `null` means "resolved, but nothing configured"
48
+ * (pass-through), distinct from `undefined` ("not yet resolved").
49
+ */
50
+ private readonly policyCache;
51
+ constructor(defaultOptions?: ResilienceBehaviorOptions | undefined, logger?: PipelineLogger);
52
+ handle(context: IPipelineContext, next: NextDelegate): Promise<unknown>;
53
+ /** Resolves, validates, and caches the composed policy for the handler in `context`. */
54
+ private resolvePolicy;
55
+ /**
56
+ * Rejects ambiguous whole-handler resilience configurations before Cockatiel
57
+ * policy creation. This turns replay/error-classification assumptions into an
58
+ * explicit application decision instead of a hidden default.
59
+ */
60
+ private assertSafeConfiguration;
61
+ /** Shallow-merges pipeline-level options over the constructor defaults. */
62
+ resolveEffectiveOptions(options?: ResilienceBehaviorOptions): ResilienceBehaviorOptions | undefined;
63
+ }
64
+ //# sourceMappingURL=resilience.behavior.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resilience.behavior.d.ts","sourceRoot":"","sources":["../src/resilience.behavior.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,gCAAgC,EACrC,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,0BAA0B,EAG3B,MAAM,oBAAoB,CAAC;AAU5B,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,8CAA8C,CAAC;AAgE9F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,qBAAa,kBACX,YACE,iBAAiB,EACjB,gCAAgC,CAAC,yBAAyB,CAAC;IA6B3D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;IA3BlC,MAAM,CAAC,QAAQ,CAAC,CAAC,0BAA0B,CAAC,EAAE,yBAAyB,CAiBrE;IAEF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA4C;IAExE,YACmB,cAAc,CAAC,EAAE,yBAAyB,YAAA,EAC3D,MAAM,CAAC,EAAE,cAAc,EAGxB;IAEK,MAAM,CACV,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,OAAO,CAAC,CAkBlB;IAED,wFAAwF;IACxF,OAAO,CAAC,aAAa;IAsBrB;;;;OAIG;IACH,OAAO,CAAC,uBAAuB;IAc/B,2EAA2E;IAC3E,uBAAuB,CACrB,OAAO,CAAC,EAAE,yBAAyB,GAClC,yBAAyB,GAAG,SAAS,CAGvC;CACF"}