@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
@@ -0,0 +1,56 @@
1
+ import type { DeadLetterRecord, DeadLetterTransport } from '../interfaces/dead-letter-transport.interface.js';
2
+ /**
3
+ * Minimal structural shape of an `amqplib` `ConfirmChannel`. Declared locally
4
+ * so this package does not hard-depend on `amqplib`. Add it in your app with
5
+ * `pnpm add amqplib` and create it via `connection.createConfirmChannel()`.
6
+ */
7
+ export interface RabbitMqConfirmChannelLike {
8
+ publish(exchange: string, routingKey: string, content: Buffer, options?: unknown): boolean;
9
+ /** Waits until the broker acknowledges or rejects all outstanding publishes. */
10
+ waitForConfirms(): Promise<void>;
11
+ /** EventEmitter lifecycle hook exposed by real amqplib channels. */
12
+ once?(event: 'drain' | 'close' | 'error', listener: (error?: Error) => void): unknown;
13
+ /** Removes lifecycle listeners after the backpressure wait settles. */
14
+ removeListener?(event: 'drain' | 'close' | 'error', listener: (error?: Error) => void): unknown;
15
+ }
16
+ /** Options for {@link RabbitMqDeadLetterTransport}. */
17
+ export interface RabbitMqDeadLetterTransportOptions {
18
+ /**
19
+ * Exchange to publish to. Default `''` (the AMQP default exchange), in which
20
+ * case `routingKey` is treated as the destination queue name.
21
+ */
22
+ exchange?: string;
23
+ /** Routing key (or queue name with the default exchange). Default `'dead-letter'`. */
24
+ routingKey?: string;
25
+ /**
26
+ * Extra `amqplib` publish options, merged over the defaults
27
+ * `{ persistent: true, contentType: 'application/json', correlationId }`.
28
+ */
29
+ publishOptions?: Record<string, unknown>;
30
+ }
31
+ /**
32
+ * {@link DeadLetterTransport} backed by **RabbitMQ** (`amqplib`).
33
+ *
34
+ * Publishes each dead letter as a persistent JSON message and waits for a
35
+ * broker publisher-confirm acknowledgement before reporting success. Make sure
36
+ * the target exchange/queue exists (`channel.assertQueue(...)`) before use.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * const conn = await amqp.connect(process.env.AMQP_URL);
41
+ * const channel = await conn.createConfirmChannel();
42
+ * await channel.assertQueue('dead-letters', { durable: true });
43
+ * const transport = new RabbitMqDeadLetterTransport(channel, { routingKey: 'dead-letters' });
44
+ * ```
45
+ */
46
+ export declare class RabbitMqDeadLetterTransport implements DeadLetterTransport {
47
+ private readonly channel;
48
+ private readonly exchange;
49
+ private readonly routingKey;
50
+ private readonly publishOptions;
51
+ constructor(channel: RabbitMqConfirmChannelLike, options?: RabbitMqDeadLetterTransportOptions);
52
+ send(record: DeadLetterRecord): Promise<void>;
53
+ /** Waits for flow control to recover, rejecting if the channel dies first. */
54
+ private waitForDrain;
55
+ }
56
+ //# sourceMappingURL=rabbitmq.transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rabbitmq.transport.d.ts","sourceRoot":"","sources":["../../src/transports/rabbitmq.transport.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,gBAAgB,EAChB,mBAAmB,EACpB,MAAM,kDAAkD,CAAC;AAE1D;;;;GAIG;AACH,MAAM,WAAW,0BAA0B;IACzC,OAAO,CACL,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,OAAO,GAChB,OAAO,CAAC;IACX,gFAAgF;IAChF,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,oEAAoE;IACpE,IAAI,CAAC,CACH,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,EAClC,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,GAChC,OAAO,CAAC;IACX,uEAAuE;IACvE,cAAc,CAAC,CACb,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,EAClC,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,GAChC,OAAO,CAAC;CACZ;AAED,uDAAuD;AACvD,MAAM,WAAW,kCAAkC;IACjD;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,2BAA4B,YAAW,mBAAmB;IAMnE,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA0B;IAEzD,YACmB,OAAO,EAAE,0BAA0B,EACpD,OAAO,GAAE,kCAAuC,EAUjD;IAEK,IAAI,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBlD;IAED,8EAA8E;YAChE,YAAY;CA2C3B"}
@@ -0,0 +1,82 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ /**
3
+ * {@link DeadLetterTransport} backed by **RabbitMQ** (`amqplib`).
4
+ *
5
+ * Publishes each dead letter as a persistent JSON message and waits for a
6
+ * broker publisher-confirm acknowledgement before reporting success. Make sure
7
+ * the target exchange/queue exists (`channel.assertQueue(...)`) before use.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const conn = await amqp.connect(process.env.AMQP_URL);
12
+ * const channel = await conn.createConfirmChannel();
13
+ * await channel.assertQueue('dead-letters', { durable: true });
14
+ * const transport = new RabbitMqDeadLetterTransport(channel, { routingKey: 'dead-letters' });
15
+ * ```
16
+ */
17
+ export class RabbitMqDeadLetterTransport {
18
+ channel;
19
+ exchange;
20
+ routingKey;
21
+ publishOptions;
22
+ constructor(channel, options = {}) {
23
+ this.channel = channel;
24
+ if (typeof channel.waitForConfirms !== 'function') {
25
+ throw new TypeError('RabbitMqDeadLetterTransport requires an amqplib ConfirmChannel.');
26
+ }
27
+ this.exchange = options.exchange ?? '';
28
+ this.routingKey = options.routingKey ?? 'dead-letter';
29
+ this.publishOptions = options.publishOptions ?? {};
30
+ }
31
+ async send(record) {
32
+ const content = Buffer.from(JSON.stringify(record));
33
+ const options = {
34
+ persistent: true,
35
+ contentType: 'application/json',
36
+ correlationId: record.correlationId,
37
+ ...this.publishOptions,
38
+ };
39
+ const writable = this.channel.publish(this.exchange, this.routingKey, content, options);
40
+ // amqplib models publish() after stream.Writable: false means the message
41
+ // was buffered successfully but callers should stop writing until `drain`.
42
+ // It is not a publish failure and must not be surfaced as one.
43
+ if (!writable) {
44
+ await Promise.all([this.waitForDrain(), this.channel.waitForConfirms()]);
45
+ return;
46
+ }
47
+ await this.channel.waitForConfirms();
48
+ }
49
+ /** Waits for flow control to recover, rejecting if the channel dies first. */
50
+ async waitForDrain() {
51
+ const { once, removeListener } = this.channel;
52
+ if (!once || !removeListener) {
53
+ throw new Error('RabbitMQ channel reported backpressure but does not expose EventEmitter lifecycle methods.');
54
+ }
55
+ await new Promise((resolve, reject) => {
56
+ let settled = false;
57
+ const cleanup = () => {
58
+ removeListener.call(this.channel, 'drain', onDrain);
59
+ removeListener.call(this.channel, 'close', onClose);
60
+ removeListener.call(this.channel, 'error', onError);
61
+ };
62
+ const finish = (error) => {
63
+ if (settled)
64
+ return;
65
+ settled = true;
66
+ cleanup();
67
+ if (error)
68
+ reject(error);
69
+ else
70
+ resolve();
71
+ };
72
+ const onDrain = () => finish();
73
+ const onClose = () => finish(new Error('RabbitMQ channel closed while waiting for publish backpressure to drain.'));
74
+ const onError = (error) => finish(error ??
75
+ new Error('RabbitMQ channel failed while waiting for publish backpressure to drain.'));
76
+ once.call(this.channel, 'drain', onDrain);
77
+ once.call(this.channel, 'close', onClose);
78
+ once.call(this.channel, 'error', onError);
79
+ });
80
+ }
81
+ }
82
+ //# sourceMappingURL=rabbitmq.transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rabbitmq.transport.js","sourceRoot":"","sources":["../../src/transports/rabbitmq.transport.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@cqrs-ddd/pipeline-deadletter",
3
+ "version": "0.5.0",
4
+ "description": "Dead letters for @cqrs-ddd/pipeline: captures failed commands and events with their context to a queue or table, and redrives them",
5
+ "author": "Aristotelis",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "type": "module",
8
+ "engines": {
9
+ "node": ">=22.12.0"
10
+ },
11
+ "keywords": [
12
+ "pipeline",
13
+ "cqrs",
14
+ "dead-letter",
15
+ "dlq",
16
+ "redrive",
17
+ "bullmq",
18
+ "rabbitmq",
19
+ "postgres",
20
+ "retry",
21
+ "resilience",
22
+ "behavior"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/aristoteliss/ddd-cqrs.git",
27
+ "directory": "packages/pipeline-deadletter"
28
+ },
29
+ "homepage": "https://aristoteliss.github.io/ddd-cqrs/packages/pipeline-deadletter/",
30
+ "bugs": {
31
+ "url": "https://github.com/aristoteliss/ddd-cqrs/issues"
32
+ },
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "./package.json": "./package.json"
39
+ },
40
+ "main": "dist/index.js",
41
+ "types": "dist/index.d.ts",
42
+ "files": [
43
+ "dist",
44
+ "LICENSE",
45
+ "COMMERCIAL_LICENSE.txt"
46
+ ],
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "peerDependencies": {
51
+ "@cqrs-ddd/pipeline": "^0.5.0"
52
+ },
53
+ "devDependencies": {
54
+ "@vitest/coverage-v8": "^5.0.3",
55
+ "vitest": "^5.0.3",
56
+ "@cqrs-ddd/pipeline": "0.5.0"
57
+ },
58
+ "dependencies": {
59
+ "@cqrs-ddd/uuidv7": "^0.5.0",
60
+ "@cqrs-ddd/safe-stringify": "^0.5.0"
61
+ },
62
+ "scripts": {
63
+ "build": "tsc -p tsconfig.build.json",
64
+ "build:watch": "tsc -p tsconfig.build.json --watch",
65
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
66
+ "rebuild": "pnpm run clean && pnpm run build",
67
+ "lint": "tsc --noEmit",
68
+ "test": "vitest run",
69
+ "test:watch": "vitest"
70
+ }
71
+ }