@cqrs-ddd/pipeline-deadletter 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 (56) hide show
  1. package/COMMERCIAL_LICENSE.txt +34 -0
  2. package/LICENSE +661 -0
  3. package/README.md +31 -0
  4. package/dist/dead-letter.behavior.d.ts +58 -0
  5. package/dist/dead-letter.behavior.d.ts.map +1 -0
  6. package/dist/dead-letter.behavior.js +173 -0
  7. package/dist/dead-letter.behavior.js.map +1 -0
  8. package/dist/dead-letter.redriver.d.ts +71 -0
  9. package/dist/dead-letter.redriver.d.ts.map +1 -0
  10. package/dist/dead-letter.redriver.js +81 -0
  11. package/dist/dead-letter.redriver.js.map +1 -0
  12. package/dist/errors/dead-letter-redrive.error.d.ts +13 -0
  13. package/dist/errors/dead-letter-redrive.error.d.ts.map +1 -0
  14. package/dist/errors/dead-letter-redrive.error.js +18 -0
  15. package/dist/errors/dead-letter-redrive.error.js.map +1 -0
  16. package/dist/helpers/build-attributes.d.ts +36 -0
  17. package/dist/helpers/build-attributes.d.ts.map +1 -0
  18. package/dist/helpers/build-attributes.js +42 -0
  19. package/dist/helpers/build-attributes.js.map +1 -0
  20. package/dist/helpers/build-record.d.ts +15 -0
  21. package/dist/helpers/build-record.d.ts.map +1 -0
  22. package/dist/helpers/build-record.js +63 -0
  23. package/dist/helpers/build-record.js.map +1 -0
  24. package/dist/helpers/dead-letter.intent.d.ts +18 -0
  25. package/dist/helpers/dead-letter.intent.d.ts.map +1 -0
  26. package/dist/helpers/dead-letter.intent.js +18 -0
  27. package/dist/helpers/dead-letter.intent.js.map +1 -0
  28. package/dist/helpers/redrive-scope.d.ts +9 -0
  29. package/dist/helpers/redrive-scope.d.ts.map +1 -0
  30. package/dist/helpers/redrive-scope.js +16 -0
  31. package/dist/helpers/redrive-scope.js.map +1 -0
  32. package/dist/index.d.ts +13 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +12 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/interfaces/dead-letter-options.interface.d.ts +70 -0
  37. package/dist/interfaces/dead-letter-options.interface.d.ts.map +1 -0
  38. package/dist/interfaces/dead-letter-options.interface.js +3 -0
  39. package/dist/interfaces/dead-letter-options.interface.js.map +1 -0
  40. package/dist/interfaces/dead-letter-transport.interface.d.ts +103 -0
  41. package/dist/interfaces/dead-letter-transport.interface.d.ts.map +1 -0
  42. package/dist/interfaces/dead-letter-transport.interface.js +3 -0
  43. package/dist/interfaces/dead-letter-transport.interface.js.map +1 -0
  44. package/dist/transports/bullmq.transport.d.ts +43 -0
  45. package/dist/transports/bullmq.transport.d.ts.map +1 -0
  46. package/dist/transports/bullmq.transport.js +36 -0
  47. package/dist/transports/bullmq.transport.js.map +1 -0
  48. package/dist/transports/postgres.transport.d.ts +51 -0
  49. package/dist/transports/postgres.transport.d.ts.map +1 -0
  50. package/dist/transports/postgres.transport.js +125 -0
  51. package/dist/transports/postgres.transport.js.map +1 -0
  52. package/dist/transports/rabbitmq.transport.d.ts +56 -0
  53. package/dist/transports/rabbitmq.transport.d.ts.map +1 -0
  54. package/dist/transports/rabbitmq.transport.js +82 -0
  55. package/dist/transports/rabbitmq.transport.js.map +1 -0
  56. package/package.json +71 -0
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # @cqrs-ddd/pipeline-deadletter
2
+
3
+ Dead letters for `@cqrs-ddd/pipeline`: a failed operation (events by default) is
4
+ captured with its request, error, tenant and correlation id, through a BullMQ, RabbitMQ
5
+ or PostgreSQL transport, and `DeadLetterRedriver` runs it again later.
6
+
7
+ **Documentation:** [guide](https://aristoteliss.github.io/ddd-cqrs/packages/pipeline-deadletter/) · [API reference](https://aristoteliss.github.io/ddd-cqrs/api/cqrs-ddd/pipeline-deadletter/) · [all packages](https://aristoteliss.github.io/ddd-cqrs/)
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pnpm add @cqrs-ddd/pipeline-deadletter @cqrs-ddd/pipeline
13
+ ```
14
+
15
+ Requires Node.js 22.12 or later. Published as an ES module; a CommonJS application loads
16
+ it with `require()`.
17
+
18
+ ## Example
19
+
20
+ ```ts
21
+ const pipeline = createPipeline({ behaviors: [new DeadLetterBehavior(transport)] });
22
+
23
+ export const sendWelcome = pipeline.wrap({ name: 'sendWelcome', kind: 'event' }, deadLetter())(
24
+ async (event: UserCreated) => mailer.welcome(event.userId),
25
+ );
26
+ ```
27
+
28
+ ## License
29
+
30
+ Dual-licensed under **AGPLv3** and a **Commercial License**. See `LICENSE` and
31
+ `COMMERCIAL_LICENSE.txt`.
@@ -0,0 +1,58 @@
1
+ import { type IPipelineBehavior, type IPipelineBehaviorContract, type IPipelineContext, type NextDelegate, PIPELINE_BEHAVIOR_CONTRACT, type PipelineItemToken, type PipelineLogger } from '@cqrs-ddd/pipeline';
2
+ import type { DeadLetterBehaviorOptions } from './interfaces/dead-letter-options.interface.js';
3
+ import type { DeadLetterTransport } from './interfaces/dead-letter-transport.interface.js';
4
+ /**
5
+ * Unique symbol key set on `context.items` to `true` or `false` recording whether
6
+ * delivery to the dead-letter transport succeeded.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const delivered = context.items.get(DEAD_LETTER_ITEM) === true;
11
+ * ```
12
+ */
13
+ export declare const DEAD_LETTER_ITEM: unique symbol;
14
+ /**
15
+ * Typed token for {@link DEAD_LETTER_ITEM}: the dead-letter delivery outcome. Reads and writes the same
16
+ * `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
17
+ */
18
+ export declare const DEAD_LETTER_ITEM_TOKEN: PipelineItemToken<boolean>;
19
+ /**
20
+ * Pipeline behavior that forwards **failed** requests to a dead-letter sink.
21
+ *
22
+ * When the wrapped handler (and anything nested inside it, such as
23
+ * `ResilienceBehavior` retries) throws, this behavior captures a
24
+ * {@link DeadLetterRecord} and hands it to the configured
25
+ * {@link DeadLetterTransport}, then — by default — re-throws so the caller still
26
+ * sees the failure. Payload and metadata must be serializable by that transport.
27
+ *
28
+ * Transport-agnostic by design: it depends only on {@link DeadLetterTransport},
29
+ * so the backend (BullMQ, RabbitMQ, Postgres, …) is a one-line swap: the
30
+ * transport passed to the constructor.
31
+ *
32
+ * **Ordering:** place this **outside** retry behaviors so it only fires once
33
+ * retries are exhausted, but **inside** request validation behaviors so expected
34
+ * client validation errors are not dead-lettered.
35
+ *
36
+ * @example Fire-and-forget event — capture and swallow
37
+ * ```ts
38
+ * class SendWelcomeEmailOnUserCreated {
39
+ * @pipeline.wrap({ kind: 'event' }, [DeadLetterBehavior, { rethrow: false }])
40
+ * async handle(event: UserCreatedEvent) {}
41
+ * }
42
+ * ```
43
+ */
44
+ export declare class DeadLetterBehavior implements IPipelineBehavior {
45
+ private readonly transport;
46
+ static readonly [PIPELINE_BEHAVIOR_CONTRACT]: IPipelineBehaviorContract;
47
+ private readonly logger;
48
+ private readonly defaults;
49
+ constructor(transport: DeadLetterTransport, defaults?: DeadLetterBehaviorOptions, logger?: PipelineLogger);
50
+ handle(context: IPipelineContext, next: NextDelegate): Promise<unknown>;
51
+ /** Forwards the failed request; returns the record id once the transport has it. */
52
+ private capture;
53
+ /** Whether this request kind and error are configured to be captured. */
54
+ private shouldCapture;
55
+ /** Merges per-handler options over the constructor defaults. */
56
+ private resolveOptions;
57
+ }
58
+ //# sourceMappingURL=dead-letter.behavior.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dead-letter.behavior.d.ts","sourceRoot":"","sources":["../src/dead-letter.behavior.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,0BAA0B,EAG1B,KAAK,iBAAiB,EACtB,KAAK,cAAc,EAEpB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,+CAA+C,CAAC;AAC/F,OAAO,KAAK,EAEV,mBAAmB,EACpB,MAAM,iDAAiD,CAAC;AASzD;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,eAA6B,CAAC;AAE3D;;;GAGG;AACH,eAAO,MAAM,sBAAsB,EAAE,iBAAiB,CAAC,OAAO,CACK,CAAC;AAEpE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,kBAAmB,YAAW,iBAAiB;IA0BxD,OAAO,CAAC,QAAQ,CAAC,SAAS;IAzB5B,MAAM,CAAC,QAAQ,CAAC,CAAC,0BAA0B,CAAC,EAAE,yBAAyB,CAmBrE;IAEF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4B;IAErD,YACmB,SAAS,EAAE,mBAAmB,EAC/C,QAAQ,CAAC,EAAE,yBAAyB,EACpC,MAAM,CAAC,EAAE,cAAc,EAUxB;IAEK,MAAM,CACV,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,OAAO,CAAC,CAuClB;IAED,oFAAoF;YACtE,OAAO;IAyBrB,yEAAyE;IACzE,OAAO,CAAC,aAAa;IAiBrB,gEAAgE;IAChE,OAAO,CAAC,cAAc;CAoCvB"}
@@ -0,0 +1,173 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ import { createPipelineItem, PIPELINE_BEHAVIOR_CONTRACT, setPipelineItem, } from '@cqrs-ddd/pipeline';
3
+ import { buildDeadLetterRecord } from './helpers/build-record.js';
4
+ import { currentRedriveId } from './helpers/redrive-scope.js';
5
+ /**
6
+ * Kinds captured when `captureKinds` is omitted: events. A command's or a
7
+ * query's caller already receives the error; an event handler's failure has
8
+ * no caller to receive it.
9
+ */
10
+ const DEFAULT_CAPTURE_KINDS = ['event'];
11
+ /**
12
+ * Unique symbol key set on `context.items` to `true` or `false` recording whether
13
+ * delivery to the dead-letter transport succeeded.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const delivered = context.items.get(DEAD_LETTER_ITEM) === true;
18
+ * ```
19
+ */
20
+ export const DEAD_LETTER_ITEM = Symbol('DEAD_LETTER_ITEM');
21
+ /**
22
+ * Typed token for {@link DEAD_LETTER_ITEM}: the dead-letter delivery outcome. Reads and writes the same
23
+ * `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
24
+ */
25
+ export const DEAD_LETTER_ITEM_TOKEN = createPipelineItem('DEAD_LETTER_ITEM', DEAD_LETTER_ITEM);
26
+ /**
27
+ * Pipeline behavior that forwards **failed** requests to a dead-letter sink.
28
+ *
29
+ * When the wrapped handler (and anything nested inside it, such as
30
+ * `ResilienceBehavior` retries) throws, this behavior captures a
31
+ * {@link DeadLetterRecord} and hands it to the configured
32
+ * {@link DeadLetterTransport}, then — by default — re-throws so the caller still
33
+ * sees the failure. Payload and metadata must be serializable by that transport.
34
+ *
35
+ * Transport-agnostic by design: it depends only on {@link DeadLetterTransport},
36
+ * so the backend (BullMQ, RabbitMQ, Postgres, …) is a one-line swap: the
37
+ * transport passed to the constructor.
38
+ *
39
+ * **Ordering:** place this **outside** retry behaviors so it only fires once
40
+ * retries are exhausted, but **inside** request validation behaviors so expected
41
+ * client validation errors are not dead-lettered.
42
+ *
43
+ * @example Fire-and-forget event — capture and swallow
44
+ * ```ts
45
+ * class SendWelcomeEmailOnUserCreated {
46
+ * @pipeline.wrap({ kind: 'event' }, [DeadLetterBehavior, { rethrow: false }])
47
+ * async handle(event: UserCreatedEvent) {}
48
+ * }
49
+ * ```
50
+ */
51
+ export class DeadLetterBehavior {
52
+ transport;
53
+ static [PIPELINE_BEHAVIOR_CONTRACT] = {
54
+ validate: (context) => {
55
+ const options = context.effectiveOptions;
56
+ if (options?.rethrow !== false || context.requestKind === 'event') {
57
+ return undefined;
58
+ }
59
+ return [
60
+ {
61
+ handlerName: context.handlerName,
62
+ behaviorName: DeadLetterBehavior.name,
63
+ message: `rethrow: false on a ${context.requestKind} handler would answer its caller with undefined instead of the error`,
64
+ fix: 'Remove rethrow: false; swallowing a captured error is allowed on event handlers only.',
65
+ },
66
+ ];
67
+ },
68
+ };
69
+ logger;
70
+ defaults;
71
+ constructor(transport, defaults, logger) {
72
+ this.transport = transport;
73
+ if (typeof transport?.send !== 'function') {
74
+ throw new TypeError('DeadLetterBehavior requires a dead-letter transport with a send() method, such as a BullMQ, RabbitMQ or PostgreSQL transport.');
75
+ }
76
+ this.defaults = defaults ?? {};
77
+ this.logger = logger ?? console;
78
+ }
79
+ async handle(context, next) {
80
+ try {
81
+ return await next();
82
+ }
83
+ catch (error) {
84
+ // A redrive records its own attempt on the existing record, and must see
85
+ // the failure: never capture it again, never swallow it.
86
+ if (currentRedriveId() !== undefined)
87
+ throw error;
88
+ const options = this.resolveOptions(context);
89
+ const shouldCapture = this.shouldCapture(context, options, error);
90
+ let recordId;
91
+ if (shouldCapture) {
92
+ recordId = await this.capture(context, error, options);
93
+ setPipelineItem(context, DEAD_LETTER_ITEM_TOKEN, recordId !== undefined);
94
+ }
95
+ // Excluding a request kind or ignoring an error means this behavior is inactive
96
+ // for that failure; it must not silently swallow an error it did not capture.
97
+ // Only an event's error is ever swallowed: a command or query has a caller.
98
+ if ((options.rethrow ?? true) ||
99
+ context.requestKind !== 'event' ||
100
+ recordId === undefined) {
101
+ throw error;
102
+ }
103
+ this.logger.error?.(`Dead-lettered and swallowed ${context.requestName} ` +
104
+ `(correlationId: ${context.correlationId}, deadLetterId: ${recordId})`, DeadLetterBehavior.name);
105
+ return undefined;
106
+ }
107
+ }
108
+ /** Forwards the failed request; returns the record id once the transport has it. */
109
+ async capture(context, error, options) {
110
+ try {
111
+ const record = buildDeadLetterRecord(context, error, options);
112
+ await this.transport.send(record);
113
+ this.logger.warn?.(`Dead-lettered ${context.requestKind} ${context.requestName} ` +
114
+ `(correlationId: ${context.correlationId}, deadLetterId: ${record.id})`, DeadLetterBehavior.name);
115
+ return record.id;
116
+ }
117
+ catch (transportError) {
118
+ // The sink failing must never hide the real handler error: log and move on.
119
+ this.logger.error?.(`Failed to dead-letter ${context.requestName}: ` +
120
+ `${transportError instanceof Error ? transportError.message : transportError}`, DeadLetterBehavior.name);
121
+ return undefined;
122
+ }
123
+ }
124
+ /** Whether this request kind and error are configured to be captured. */
125
+ shouldCapture(context, options, error) {
126
+ const captureKinds = options.captureKinds ?? DEFAULT_CAPTURE_KINDS;
127
+ if (!captureKinds.includes(context.requestKind)) {
128
+ return false;
129
+ }
130
+ const { ignoreErrors } = options;
131
+ return !((typeof ignoreErrors === 'function' || Array.isArray(ignoreErrors)) &&
132
+ matchesIgnoredError(ignoreErrors, error, context));
133
+ }
134
+ /** Merges per-handler options over the constructor defaults. */
135
+ resolveOptions(context) {
136
+ const handlerOptions = context.getBehaviorOptions(DeadLetterBehavior);
137
+ if (!handlerOptions)
138
+ return this.defaults;
139
+ const merged = {
140
+ ...this.defaults,
141
+ ...handlerOptions,
142
+ };
143
+ if (this.defaults.ignoreErrors && handlerOptions.ignoreErrors) {
144
+ if (Array.isArray(this.defaults.ignoreErrors) &&
145
+ Array.isArray(handlerOptions.ignoreErrors)) {
146
+ merged.ignoreErrors = [
147
+ ...this.defaults.ignoreErrors,
148
+ ...handlerOptions.ignoreErrors,
149
+ ];
150
+ }
151
+ else {
152
+ const defaultFilter = this.defaults.ignoreErrors;
153
+ const handlerFilter = handlerOptions.ignoreErrors;
154
+ merged.ignoreErrors = (err, ctx) => matchesIgnoredError(defaultFilter, err, ctx) ||
155
+ matchesIgnoredError(handlerFilter, err, ctx);
156
+ }
157
+ }
158
+ if (this.defaults.redactKeys && handlerOptions.redactKeys) {
159
+ merged.redactKeys = Array.from(new Set([...this.defaults.redactKeys, ...handlerOptions.redactKeys]));
160
+ }
161
+ return merged;
162
+ }
163
+ }
164
+ function matchesIgnoredError(filter, error, context) {
165
+ if (typeof filter === 'function')
166
+ return filter(error, context);
167
+ for (const target of filter) {
168
+ if (typeof target === 'function' && error instanceof target)
169
+ return true;
170
+ }
171
+ return false;
172
+ }
173
+ //# sourceMappingURL=dead-letter.behavior.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dead-letter.behavior.js","sourceRoot":"","sources":["../src/dead-letter.behavior.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ import type { Constructor } from '@cqrs-ddd/pipeline';
2
+ import type { DeadLetterRecord, DeadLetterRequestKind, DeadLetterStore } from './interfaces/dead-letter-transport.interface.js';
3
+ /**
4
+ * Sends a rebuilt request back for handling. It must settle only when the
5
+ * handling has finished, and reject when it failed.
6
+ *
7
+ * For an event, run only the handler that failed (`record.handlerName`):
8
+ * publishing the event again would also rerun the handlers that succeeded.
9
+ */
10
+ export type DeadLetterDispatch = (request: unknown, record: DeadLetterRecord) => Promise<unknown> | unknown;
11
+ /** Options of {@link DeadLetterRedriver}. */
12
+ export interface DeadLetterRedriverOptions {
13
+ /**
14
+ * The request classes that may be redriven, matched by class name against
15
+ * `record.requestName`. A record of any other name is refused.
16
+ */
17
+ requestTypes: readonly Constructor[];
18
+ /** How to dispatch a rebuilt request, per request kind. */
19
+ dispatch: Partial<Record<DeadLetterRequestKind, DeadLetterDispatch>>;
20
+ /**
21
+ * Builds the request from a record. Default: an instance of the registered
22
+ * class with the stored payload's fields, without running its constructor.
23
+ * Required to redrive a record whose payload was redacted: it must restore
24
+ * the redacted values.
25
+ */
26
+ rebuild?: (record: DeadLetterRecord, requestType: Constructor) => unknown;
27
+ }
28
+ /** Outcome of a successful {@link DeadLetterRedriver.redrive}. */
29
+ export interface DeadLetterRedriveResult {
30
+ id: string;
31
+ /** What the dispatch returned. */
32
+ response: unknown;
33
+ }
34
+ /**
35
+ * Redrives dead letters kept by a {@link DeadLetterStore}: rebuilds the
36
+ * request, dispatches it, then marks the record resolved — or, when the
37
+ * handling fails again, counts the attempt, keeps its error, and rethrows it.
38
+ * During a redrive, `DeadLetterBehavior` neither captures the failure again
39
+ * nor swallows it.
40
+ *
41
+ * The application supplies the dispatch functions, one per request kind:
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * const redriver = new DeadLetterRedriver(store, {
46
+ * requestTypes: [SendWelcomeEmailCommand, UserCreatedEvent],
47
+ * dispatch: {
48
+ * command: (command) => commands.execute(command),
49
+ * // Only the failed handler, not every subscriber of the event.
50
+ * event: (event, record) => eventHandlers[record.handlerName](event),
51
+ * },
52
+ * });
53
+ * await redriver.redrive(recordId);
54
+ * ```
55
+ */
56
+ export declare class DeadLetterRedriver {
57
+ private readonly store;
58
+ private readonly options;
59
+ private readonly types;
60
+ constructor(store: DeadLetterStore, options: DeadLetterRedriverOptions);
61
+ /**
62
+ * Redrives the dead letter `id`.
63
+ *
64
+ * @throws DeadLetterRedriveError when it cannot be redriven; nothing is dispatched.
65
+ * @throws The handling error when the redrive fails; the attempt is counted.
66
+ */
67
+ redrive(id: string): Promise<DeadLetterRedriveResult>;
68
+ /** Closes the dead letter `id` without replaying it. */
69
+ resolve(id: string): Promise<void>;
70
+ }
71
+ //# sourceMappingURL=dead-letter.redriver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dead-letter.redriver.d.ts","sourceRoot":"","sources":["../src/dead-letter.redriver.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAGtD,OAAO,KAAK,EACV,gBAAgB,EAChB,qBAAqB,EACrB,eAAe,EAChB,MAAM,iDAAiD,CAAC;AAEzD;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAC/B,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,gBAAgB,KACrB,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;AAEhC,6CAA6C;AAC7C,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,YAAY,EAAE,SAAS,WAAW,EAAE,CAAC;IACrC,2DAA2D;IAC3D,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,qBAAqB,EAAE,kBAAkB,CAAC,CAAC,CAAC;IACrE;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,KAAK,OAAO,CAAC;CAC3E;AAED,kEAAkE;AAClE,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,kCAAkC;IAClC,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,kBAAkB;IAI3B,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJ1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmC;IAEzD,YACmB,KAAK,EAAE,eAAe,EACtB,OAAO,EAAE,yBAAyB,EAGpD;IAED;;;;;OAKG;IACG,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC,CA+C1D;IAED,wDAAwD;IACxD,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEjC;CACF"}
@@ -0,0 +1,81 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ import { DeadLetterRedriveError } from './errors/dead-letter-redrive.error.js';
3
+ import { runAsRedrive } from './helpers/redrive-scope.js';
4
+ /**
5
+ * Redrives dead letters kept by a {@link DeadLetterStore}: rebuilds the
6
+ * request, dispatches it, then marks the record resolved — or, when the
7
+ * handling fails again, counts the attempt, keeps its error, and rethrows it.
8
+ * During a redrive, `DeadLetterBehavior` neither captures the failure again
9
+ * nor swallows it.
10
+ *
11
+ * The application supplies the dispatch functions, one per request kind:
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const redriver = new DeadLetterRedriver(store, {
16
+ * requestTypes: [SendWelcomeEmailCommand, UserCreatedEvent],
17
+ * dispatch: {
18
+ * command: (command) => commands.execute(command),
19
+ * // Only the failed handler, not every subscriber of the event.
20
+ * event: (event, record) => eventHandlers[record.handlerName](event),
21
+ * },
22
+ * });
23
+ * await redriver.redrive(recordId);
24
+ * ```
25
+ */
26
+ export class DeadLetterRedriver {
27
+ store;
28
+ options;
29
+ types;
30
+ constructor(store, options) {
31
+ this.store = store;
32
+ this.options = options;
33
+ this.types = new Map(options.requestTypes.map((type) => [type.name, type]));
34
+ }
35
+ /**
36
+ * Redrives the dead letter `id`.
37
+ *
38
+ * @throws DeadLetterRedriveError when it cannot be redriven; nothing is dispatched.
39
+ * @throws The handling error when the redrive fails; the attempt is counted.
40
+ */
41
+ async redrive(id) {
42
+ const record = await this.store.get(id);
43
+ if (!record)
44
+ throw new DeadLetterRedriveError(id, 'no such dead letter');
45
+ if (record.status === 'resolved') {
46
+ throw new DeadLetterRedriveError(id, 'it is already resolved');
47
+ }
48
+ const requestType = this.types.get(record.requestName);
49
+ if (!requestType) {
50
+ throw new DeadLetterRedriveError(id, `request type ${record.requestName} is not registered in requestTypes`);
51
+ }
52
+ const dispatch = this.options.dispatch[record.requestKind];
53
+ if (!dispatch) {
54
+ throw new DeadLetterRedriveError(id, `no dispatch is configured for ${record.requestKind} requests`);
55
+ }
56
+ if (record.payloadRedacted && !this.options.rebuild) {
57
+ throw new DeadLetterRedriveError(id, 'its payload was redacted; configure rebuild to restore the redacted values');
58
+ }
59
+ const request = this.options.rebuild
60
+ ? this.options.rebuild(record, requestType)
61
+ : Object.assign(Object.create(requestType.prototype), record.payload);
62
+ let response;
63
+ try {
64
+ response = await runAsRedrive(id, () => dispatch(request, record));
65
+ }
66
+ catch (error) {
67
+ await this.store.recordAttempt(id, {
68
+ name: error instanceof Error ? error.name : 'unknown',
69
+ message: error instanceof Error ? error.message : String(error),
70
+ });
71
+ throw error;
72
+ }
73
+ await this.store.markResolved(id);
74
+ return { id, response };
75
+ }
76
+ /** Closes the dead letter `id` without replaying it. */
77
+ resolve(id) {
78
+ return this.store.markResolved(id);
79
+ }
80
+ }
81
+ //# sourceMappingURL=dead-letter.redriver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dead-letter.redriver.js","sourceRoot":"","sources":["../src/dead-letter.redriver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Raised by `DeadLetterRedriver` when a dead letter cannot be redriven: it does
3
+ * not exist, is already resolved, has no registered request type or
4
+ * dispatcher, or its payload was redacted and no `rebuild` is configured.
5
+ * Nothing is dispatched and the record is unchanged.
6
+ */
7
+ export declare class DeadLetterRedriveError extends Error {
8
+ readonly deadLetterId: string;
9
+ readonly reason: string;
10
+ readonly name = "DeadLetterRedriveError";
11
+ constructor(deadLetterId: string, reason: string);
12
+ }
13
+ //# sourceMappingURL=dead-letter-redrive.error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dead-letter-redrive.error.d.ts","sourceRoot":"","sources":["../../src/errors/dead-letter-redrive.error.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;aAI7B,YAAY,EAAE,MAAM;aACpB,MAAM,EAAE,MAAM;IAJhC,SAAkB,IAAI,4BAA4B;IAElD,YACkB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EAG/B;CACF"}
@@ -0,0 +1,18 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ /**
3
+ * Raised by `DeadLetterRedriver` when a dead letter cannot be redriven: it does
4
+ * not exist, is already resolved, has no registered request type or
5
+ * dispatcher, or its payload was redacted and no `rebuild` is configured.
6
+ * Nothing is dispatched and the record is unchanged.
7
+ */
8
+ export class DeadLetterRedriveError extends Error {
9
+ deadLetterId;
10
+ reason;
11
+ name = 'DeadLetterRedriveError';
12
+ constructor(deadLetterId, reason) {
13
+ super(`Cannot redrive dead letter ${deadLetterId}: ${reason}`);
14
+ this.deadLetterId = deadLetterId;
15
+ this.reason = reason;
16
+ }
17
+ }
18
+ //# sourceMappingURL=dead-letter-redrive.error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dead-letter-redrive.error.js","sourceRoot":"","sources":["../../src/errors/dead-letter-redrive.error.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE;;;;;GAKG;AACH,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IAI7B,YAAY;IACZ,MAAM;IAJN,IAAI,GAAG,wBAAwB,CAAC;IAElD,YACkB,YAAoB,EACpB,MAAc;QAE9B,KAAK,CAAC,8BAA8B,YAAY,KAAK,MAAM,EAAE,CAAC,CAAC;4BAH/C,YAAY;sBACZ,MAAM;IAGxB,CAAC;CACF"}
@@ -0,0 +1,36 @@
1
+ import { type IPipelineContext } from '@cqrs-ddd/pipeline';
2
+ /**
3
+ * The dead-letter outcome of one execution as flat attributes:
4
+ * `{ 'dead_letter.captured': true }` when `DeadLetterBehavior` delivered a
5
+ * record of the failure, `{}` otherwise — a success, an ignored error, a
6
+ * delivery that failed, or no dead-letter behavior at all.
7
+ *
8
+ * The result is plain data and needs no telemetry package: put it on a span, a
9
+ * log line, an audit record or a metric label (bounded values only).
10
+ *
11
+ * @param context - The pipeline execution, read after `DeadLetterBehavior` ran.
12
+ * @returns The attributes; empty unless a record was delivered.
13
+ *
14
+ * @example Span attributes
15
+ * With `AttributesBehavior` of `@cqrs-ddd/pipeline-opentelemetry`:
16
+ * ```ts
17
+ * createPipeline({
18
+ * globalBehaviors: {
19
+ * before: [TraceBehavior, [AttributesBehavior, { factories: [buildDeadLetterAttributes] }]],
20
+ * },
21
+ * });
22
+ * ```
23
+ *
24
+ * @example A log line from a custom behavior, after the chain ran
25
+ * ```ts
26
+ * async handle(context: IPipelineContext, next: NextDelegate) {
27
+ * try {
28
+ * return await next();
29
+ * } finally {
30
+ * this.logger.log({ request: context.requestName, ...buildDeadLetterAttributes(context) });
31
+ * }
32
+ * }
33
+ * ```
34
+ */
35
+ export declare function buildDeadLetterAttributes(context: IPipelineContext): Record<string, boolean>;
36
+ //# sourceMappingURL=build-attributes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-attributes.d.ts","sourceRoot":"","sources":["../../src/helpers/build-attributes.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,gBAAgB,GACxB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIzB"}
@@ -0,0 +1,42 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ import { getPipelineItem } from '@cqrs-ddd/pipeline';
3
+ import { DEAD_LETTER_ITEM_TOKEN } from '../dead-letter.behavior.js';
4
+ /**
5
+ * The dead-letter outcome of one execution as flat attributes:
6
+ * `{ 'dead_letter.captured': true }` when `DeadLetterBehavior` delivered a
7
+ * record of the failure, `{}` otherwise — a success, an ignored error, a
8
+ * delivery that failed, or no dead-letter behavior at all.
9
+ *
10
+ * The result is plain data and needs no telemetry package: put it on a span, a
11
+ * log line, an audit record or a metric label (bounded values only).
12
+ *
13
+ * @param context - The pipeline execution, read after `DeadLetterBehavior` ran.
14
+ * @returns The attributes; empty unless a record was delivered.
15
+ *
16
+ * @example Span attributes
17
+ * With `AttributesBehavior` of `@cqrs-ddd/pipeline-opentelemetry`:
18
+ * ```ts
19
+ * createPipeline({
20
+ * globalBehaviors: {
21
+ * before: [TraceBehavior, [AttributesBehavior, { factories: [buildDeadLetterAttributes] }]],
22
+ * },
23
+ * });
24
+ * ```
25
+ *
26
+ * @example A log line from a custom behavior, after the chain ran
27
+ * ```ts
28
+ * async handle(context: IPipelineContext, next: NextDelegate) {
29
+ * try {
30
+ * return await next();
31
+ * } finally {
32
+ * this.logger.log({ request: context.requestName, ...buildDeadLetterAttributes(context) });
33
+ * }
34
+ * }
35
+ * ```
36
+ */
37
+ export function buildDeadLetterAttributes(context) {
38
+ return getPipelineItem(context, DEAD_LETTER_ITEM_TOKEN) === true
39
+ ? { 'dead_letter.captured': true }
40
+ : {};
41
+ }
42
+ //# sourceMappingURL=build-attributes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-attributes.js","sourceRoot":"","sources":["../../src/helpers/build-attributes.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EAAE,eAAe,EAAyB,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAEpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,UAAU,yBAAyB,CACvC,OAAyB;IAEzB,OAAO,eAAe,CAAC,OAAO,EAAE,sBAAsB,CAAC,KAAK,IAAI;QAC9D,CAAC,CAAC,EAAE,sBAAsB,EAAE,IAAI,EAAE;QAClC,CAAC,CAAC,EAAE,CAAC;AACT,CAAC"}
@@ -0,0 +1,15 @@
1
+ import { type IPipelineContext } from '@cqrs-ddd/pipeline';
2
+ import type { DeadLetterBehaviorOptions } from '../interfaces/dead-letter-options.interface.js';
3
+ import type { DeadLetterRecord } from '../interfaces/dead-letter-transport.interface.js';
4
+ /**
5
+ * Build a transport-neutral {@link DeadLetterRecord} from a failed pipeline run.
6
+ *
7
+ * Non-`Error` throws are normalized to a record with `name: 'unknown'` so the
8
+ * transport always receives a well-formed shape.
9
+ *
10
+ * @param context - The pipeline context of the failed request.
11
+ * @param error - The thrown value (any type).
12
+ * @param options - Effective behavior options (stack inclusion, metadata, redaction).
13
+ */
14
+ export declare function buildDeadLetterRecord(context: IPipelineContext, error: unknown, options?: DeadLetterBehaviorOptions): DeadLetterRecord;
15
+ //# sourceMappingURL=build-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-record.d.ts","sourceRoot":"","sources":["../../src/helpers/build-record.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAO3D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,gDAAgD,CAAC;AAChG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kDAAkD,CAAC;AA6BzF;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,gBAAgB,EACzB,KAAK,EAAE,OAAO,EACd,OAAO,GAAE,yBAA8B,GACtC,gBAAgB,CA+BlB"}
@@ -0,0 +1,63 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ import { DEFAULT_REDACT_KEYS, REDACTED, redactValue, } from '@cqrs-ddd/safe-stringify';
3
+ import { uuidv7 } from '@cqrs-ddd/uuidv7';
4
+ /** Redact a payload using a custom redactor or key-based masking. */
5
+ function sanitizePayload(payload, options) {
6
+ if (options.redact)
7
+ return options.redact(payload);
8
+ const keys = options.redactKeys
9
+ ? [...DEFAULT_REDACT_KEYS, ...options.redactKeys]
10
+ : DEFAULT_REDACT_KEYS;
11
+ return redactValue(payload, keys);
12
+ }
13
+ /** Whether key-based redaction masked a value anywhere in `value`. */
14
+ function containsRedacted(value, seen = new WeakSet()) {
15
+ if (value === REDACTED)
16
+ return true;
17
+ if (typeof value !== 'object' || value === null || seen.has(value)) {
18
+ return false;
19
+ }
20
+ seen.add(value);
21
+ const entries = value instanceof Map ? [...value.values()] : Object.values(value);
22
+ return entries.some((entry) => containsRedacted(entry, seen));
23
+ }
24
+ /**
25
+ * Build a transport-neutral {@link DeadLetterRecord} from a failed pipeline run.
26
+ *
27
+ * Non-`Error` throws are normalized to a record with `name: 'unknown'` so the
28
+ * transport always receives a well-formed shape.
29
+ *
30
+ * @param context - The pipeline context of the failed request.
31
+ * @param error - The thrown value (any type).
32
+ * @param options - Effective behavior options (stack inclusion, metadata, redaction).
33
+ */
34
+ export function buildDeadLetterRecord(context, error, options = {}) {
35
+ const isError = error instanceof Error;
36
+ const userMetadata = options.metadata?.(context);
37
+ const payload = sanitizePayload(context.request, options);
38
+ return {
39
+ id: uuidv7(),
40
+ correlationId: context.correlationId,
41
+ tenantId: context.tenantId,
42
+ requestKind: context.requestKind,
43
+ requestName: context.requestName,
44
+ handlerName: context.handlerName,
45
+ payload,
46
+ error: {
47
+ name: isError ? error.name : 'unknown',
48
+ message: isError ? error.message : String(error),
49
+ stack: options.includeStack === false || !isError ? undefined : error.stack,
50
+ },
51
+ failedAt: new Date().toISOString(),
52
+ metadata: userMetadata || context.tenantId
53
+ ? {
54
+ ...(userMetadata ?? {}),
55
+ ...(context.tenantId ? { tenantId: context.tenantId } : {}),
56
+ }
57
+ : undefined,
58
+ attempts: 0,
59
+ status: 'open',
60
+ payloadRedacted: options.redact !== undefined || containsRedacted(payload),
61
+ };
62
+ }
63
+ //# sourceMappingURL=build-record.js.map