@zdavison/matador 2.0.1
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/cli.ts +453 -0
- package/dist/checkpoint/context.d.ts +59 -0
- package/dist/checkpoint/context.d.ts.map +1 -0
- package/dist/checkpoint/context.js +140 -0
- package/dist/checkpoint/context.test.d.ts +2 -0
- package/dist/checkpoint/context.test.d.ts.map +1 -0
- package/dist/checkpoint/context.test.js +424 -0
- package/dist/checkpoint/index.d.ts +7 -0
- package/dist/checkpoint/index.d.ts.map +1 -0
- package/dist/checkpoint/index.js +6 -0
- package/dist/checkpoint/stores/memory.d.ts +29 -0
- package/dist/checkpoint/stores/memory.d.ts.map +1 -0
- package/dist/checkpoint/stores/memory.js +39 -0
- package/dist/checkpoint/stores/noop.d.ts +14 -0
- package/dist/checkpoint/stores/noop.d.ts.map +1 -0
- package/dist/checkpoint/stores/noop.js +18 -0
- package/dist/checkpoint/stores/stores.test.d.ts +2 -0
- package/dist/checkpoint/stores/stores.test.d.ts.map +1 -0
- package/dist/checkpoint/stores/stores.test.js +146 -0
- package/dist/checkpoint/types.d.ts +119 -0
- package/dist/checkpoint/types.d.ts.map +1 -0
- package/dist/checkpoint/types.js +1 -0
- package/dist/codec/codec.d.ts +29 -0
- package/dist/codec/codec.d.ts.map +1 -0
- package/dist/codec/codec.js +15 -0
- package/dist/codec/header-aware-codec.d.ts +36 -0
- package/dist/codec/header-aware-codec.d.ts.map +1 -0
- package/dist/codec/header-aware-codec.js +1 -0
- package/dist/codec/index.d.ts +6 -0
- package/dist/codec/index.d.ts.map +1 -0
- package/dist/codec/index.js +3 -0
- package/dist/codec/json-codec.d.ts +13 -0
- package/dist/codec/json-codec.d.ts.map +1 -0
- package/dist/codec/json-codec.js +64 -0
- package/dist/codec/rabbitmq-codec.d.ts +28 -0
- package/dist/codec/rabbitmq-codec.d.ts.map +1 -0
- package/dist/codec/rabbitmq-codec.js +242 -0
- package/dist/codec/rabbitmq-codec.test.d.ts +2 -0
- package/dist/codec/rabbitmq-codec.test.d.ts.map +1 -0
- package/dist/codec/rabbitmq-codec.test.js +433 -0
- package/dist/core/fanout.d.ts +59 -0
- package/dist/core/fanout.d.ts.map +1 -0
- package/dist/core/fanout.js +121 -0
- package/dist/core/fanout.test.d.ts +2 -0
- package/dist/core/fanout.test.d.ts.map +1 -0
- package/dist/core/fanout.test.js +1055 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +3 -0
- package/dist/core/matador.d.ts +133 -0
- package/dist/core/matador.d.ts.map +1 -0
- package/dist/core/matador.js +222 -0
- package/dist/core/matador.test.d.ts +2 -0
- package/dist/core/matador.test.d.ts.map +1 -0
- package/dist/core/matador.test.js +446 -0
- package/dist/core/shutdown.d.ts +78 -0
- package/dist/core/shutdown.d.ts.map +1 -0
- package/dist/core/shutdown.js +111 -0
- package/dist/core/shutdown.test.d.ts +2 -0
- package/dist/core/shutdown.test.d.ts.map +1 -0
- package/dist/core/shutdown.test.js +524 -0
- package/dist/errors/checkpoint-errors.d.ts +30 -0
- package/dist/errors/checkpoint-errors.d.ts.map +1 -0
- package/dist/errors/checkpoint-errors.js +49 -0
- package/dist/errors/has-description.d.ts +18 -0
- package/dist/errors/has-description.d.ts.map +1 -0
- package/dist/errors/has-description.js +9 -0
- package/dist/errors/index.d.ts +6 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +22 -0
- package/dist/errors/matador-errors.d.ts +183 -0
- package/dist/errors/matador-errors.d.ts.map +1 -0
- package/dist/errors/matador-errors.js +376 -0
- package/dist/errors/retry-errors.d.ts +67 -0
- package/dist/errors/retry-errors.d.ts.map +1 -0
- package/dist/errors/retry-errors.js +108 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/safe-hooks.d.ts +34 -0
- package/dist/hooks/safe-hooks.d.ts.map +1 -0
- package/dist/hooks/safe-hooks.js +135 -0
- package/dist/hooks/types.d.ts +189 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +9 -0
- package/dist/index.cjs +3156 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline/index.d.ts +3 -0
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/pipeline.d.ts +60 -0
- package/dist/pipeline/pipeline.d.ts.map +1 -0
- package/dist/pipeline/pipeline.js +261 -0
- package/dist/pipeline/pipeline.test.d.ts +2 -0
- package/dist/pipeline/pipeline.test.d.ts.map +1 -0
- package/dist/pipeline/pipeline.test.js +1065 -0
- package/dist/retry/index.d.ts +4 -0
- package/dist/retry/index.d.ts.map +1 -0
- package/dist/retry/index.js +1 -0
- package/dist/retry/policy.d.ts +43 -0
- package/dist/retry/policy.d.ts.map +1 -0
- package/dist/retry/policy.js +1 -0
- package/dist/retry/standard-policy.d.ts +44 -0
- package/dist/retry/standard-policy.d.ts.map +1 -0
- package/dist/retry/standard-policy.js +102 -0
- package/dist/retry/standard-policy.test.d.ts +2 -0
- package/dist/retry/standard-policy.test.d.ts.map +1 -0
- package/dist/retry/standard-policy.test.js +190 -0
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +2 -0
- package/dist/schema/registry.d.ts +63 -0
- package/dist/schema/registry.d.ts.map +1 -0
- package/dist/schema/registry.js +171 -0
- package/dist/schema/registry.test.d.ts +2 -0
- package/dist/schema/registry.test.d.ts.map +1 -0
- package/dist/schema/registry.test.js +278 -0
- package/dist/schema/types.d.ts +158 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +74 -0
- package/dist/schema/types.test.d.ts +2 -0
- package/dist/schema/types.test.d.ts.map +1 -0
- package/dist/schema/types.test.js +243 -0
- package/dist/topology/builder.d.ts +66 -0
- package/dist/topology/builder.d.ts.map +1 -0
- package/dist/topology/builder.js +156 -0
- package/dist/topology/builder.test.d.ts +2 -0
- package/dist/topology/builder.test.d.ts.map +1 -0
- package/dist/topology/builder.test.js +222 -0
- package/dist/topology/index.d.ts +5 -0
- package/dist/topology/index.d.ts.map +1 -0
- package/dist/topology/index.js +2 -0
- package/dist/topology/types.d.ts +76 -0
- package/dist/topology/types.d.ts.map +1 -0
- package/dist/topology/types.js +18 -0
- package/dist/transport/capabilities.d.ts +66 -0
- package/dist/transport/capabilities.d.ts.map +1 -0
- package/dist/transport/capabilities.js +18 -0
- package/dist/transport/connection-manager.d.ts +95 -0
- package/dist/transport/connection-manager.d.ts.map +1 -0
- package/dist/transport/connection-manager.js +144 -0
- package/dist/transport/index.d.ts +11 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +5 -0
- package/dist/transport/local/local-transport.d.ts +62 -0
- package/dist/transport/local/local-transport.d.ts.map +1 -0
- package/dist/transport/local/local-transport.js +241 -0
- package/dist/transport/local/local-transport.test.d.ts +2 -0
- package/dist/transport/local/local-transport.test.d.ts.map +1 -0
- package/dist/transport/local/local-transport.test.js +192 -0
- package/dist/transport/multi/multi-transport.d.ts +94 -0
- package/dist/transport/multi/multi-transport.d.ts.map +1 -0
- package/dist/transport/multi/multi-transport.js +184 -0
- package/dist/transport/multi/multi-transport.test.d.ts +2 -0
- package/dist/transport/multi/multi-transport.test.d.ts.map +1 -0
- package/dist/transport/multi/multi-transport.test.js +236 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts +73 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.js +518 -0
- package/dist/transport/transport.d.ts +152 -0
- package/dist/transport/transport.d.ts.map +1 -0
- package/dist/transport/transport.js +1 -0
- package/dist/types/common.d.ts +41 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +12 -0
- package/dist/types/envelope.d.ts +82 -0
- package/dist/types/envelope.d.ts.map +1 -0
- package/dist/types/envelope.js +36 -0
- package/dist/types/event.d.ts +96 -0
- package/dist/types/event.d.ts.map +1 -0
- package/dist/types/event.js +26 -0
- package/dist/types/event.test.d.ts +2 -0
- package/dist/types/event.test.d.ts.map +1 -0
- package/dist/types/event.test.js +130 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +4 -0
- package/dist/types/subscriber.d.ts +207 -0
- package/dist/types/subscriber.d.ts.map +1 -0
- package/dist/types/subscriber.js +99 -0
- package/examples/config.ts +126 -0
- package/examples/event.ts +26 -0
- package/examples/order-event.json +19 -0
- package/package.json +66 -0
- package/src/checkpoint/context.test.ts +510 -0
- package/src/checkpoint/context.ts +213 -0
- package/src/checkpoint/index.ts +30 -0
- package/src/checkpoint/stores/memory.ts +47 -0
- package/src/checkpoint/stores/noop.ts +22 -0
- package/src/checkpoint/stores/stores.test.ts +177 -0
- package/src/checkpoint/types.ts +147 -0
- package/src/codec/codec.ts +42 -0
- package/src/codec/header-aware-codec.ts +41 -0
- package/src/codec/index.ts +11 -0
- package/src/codec/json-codec.ts +69 -0
- package/src/codec/rabbitmq-codec.test.ts +516 -0
- package/src/codec/rabbitmq-codec.ts +336 -0
- package/src/core/fanout.test.ts +1351 -0
- package/src/core/fanout.ts +184 -0
- package/src/core/index.ts +12 -0
- package/src/core/matador.test.ts +575 -0
- package/src/core/matador.ts +344 -0
- package/src/core/shutdown.test.ts +853 -0
- package/src/core/shutdown.ts +165 -0
- package/src/errors/checkpoint-errors.ts +62 -0
- package/src/errors/has-description.ts +25 -0
- package/src/errors/index.ts +57 -0
- package/src/errors/matador-errors.ts +477 -0
- package/src/errors/retry-errors.ts +134 -0
- package/src/hooks/index.ts +15 -0
- package/src/hooks/safe-hooks.ts +223 -0
- package/src/hooks/types.ts +248 -0
- package/src/index.ts +231 -0
- package/src/pipeline/index.ts +2 -0
- package/src/pipeline/pipeline.test.ts +1317 -0
- package/src/pipeline/pipeline.ts +371 -0
- package/src/retry/index.ts +4 -0
- package/src/retry/policy.ts +46 -0
- package/src/retry/standard-policy.test.ts +282 -0
- package/src/retry/standard-policy.ts +156 -0
- package/src/schema/index.ts +16 -0
- package/src/schema/registry.test.ts +339 -0
- package/src/schema/registry.ts +226 -0
- package/src/schema/types.test.ts +281 -0
- package/src/schema/types.ts +217 -0
- package/src/topology/builder.test.ts +275 -0
- package/src/topology/builder.ts +199 -0
- package/src/topology/index.ts +15 -0
- package/src/topology/types.ts +109 -0
- package/src/transport/capabilities.ts +88 -0
- package/src/transport/connection-manager.ts +218 -0
- package/src/transport/index.ts +42 -0
- package/src/transport/local/local-transport.test.ts +262 -0
- package/src/transport/local/local-transport.ts +327 -0
- package/src/transport/multi/multi-transport.test.ts +320 -0
- package/src/transport/multi/multi-transport.ts +294 -0
- package/src/transport/rabbitmq/rabbitmq-transport.ts +753 -0
- package/src/transport/transport.ts +200 -0
- package/src/types/common.ts +53 -0
- package/src/types/envelope.ts +152 -0
- package/src/types/event.test.ts +157 -0
- package/src/types/event.ts +118 -0
- package/src/types/index.ts +52 -0
- package/src/types/subscriber.ts +310 -0
- package/test/e2e/multi-transport.e2e.test.ts +236 -0
- package/test/e2e/rabbitmq-transport.e2e.test.ts +327 -0
- package/test/e2e/transport-compliance.e2e.test.ts +505 -0
- package/test/integration/matador.integration.test.ts +634 -0
- package/tsconfig.json +29 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsup.config.ts +13 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
import type { Channel, ChannelModel, ConsumeMessage, Options } from 'amqplib';
|
|
2
|
+
import amqplib from 'amqplib';
|
|
3
|
+
import { RabbitMQCodec } from '../../codec/rabbitmq-codec.js';
|
|
4
|
+
import {
|
|
5
|
+
DelayedMessagesNotSupportedError,
|
|
6
|
+
TransportNotConnectedError,
|
|
7
|
+
} from '../../errors/index.js';
|
|
8
|
+
import { type Logger, consoleLogger } from '../../hooks/index.js';
|
|
9
|
+
import type { QueueDefinition, Topology } from '../../topology/types.js';
|
|
10
|
+
import type { Envelope } from '../../types/index.js';
|
|
11
|
+
import type { TransportCapabilities } from '../capabilities.js';
|
|
12
|
+
import {
|
|
13
|
+
ConnectionManager,
|
|
14
|
+
type ConnectionManagerConfig,
|
|
15
|
+
} from '../connection-manager.js';
|
|
16
|
+
import type {
|
|
17
|
+
MessageHandler,
|
|
18
|
+
MessageReceipt,
|
|
19
|
+
SendOptions,
|
|
20
|
+
SubscribeOptions,
|
|
21
|
+
Subscription,
|
|
22
|
+
Transport,
|
|
23
|
+
} from '../transport.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Configuration options for the RabbitMQ transport.
|
|
27
|
+
*/
|
|
28
|
+
export interface RabbitMQTransportConfig {
|
|
29
|
+
/** RabbitMQ connection URL */
|
|
30
|
+
readonly url: string;
|
|
31
|
+
|
|
32
|
+
/** Connection manager configuration */
|
|
33
|
+
readonly connection?: Partial<ConnectionManagerConfig> | undefined;
|
|
34
|
+
|
|
35
|
+
/** Use quorum queues for durability (default: true) */
|
|
36
|
+
readonly quorumQueues?: boolean | undefined;
|
|
37
|
+
|
|
38
|
+
/** Default prefetch count per consumer (default: 10) */
|
|
39
|
+
readonly defaultPrefetch?: number | undefined;
|
|
40
|
+
|
|
41
|
+
/** Enable the delayed message exchange plugin if available (default: true) */
|
|
42
|
+
readonly enableDelayedMessages?: boolean | undefined;
|
|
43
|
+
|
|
44
|
+
/** Logger for transport events (defaults to console) */
|
|
45
|
+
readonly logger?: Logger | undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Internal structure for tracking a queue's dedicated channel.
|
|
50
|
+
*/
|
|
51
|
+
interface QueueChannel {
|
|
52
|
+
readonly channel: Channel;
|
|
53
|
+
readonly consumers: ActiveConsumer[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Internal structure for tracking active consumers.
|
|
58
|
+
*/
|
|
59
|
+
interface ActiveConsumer {
|
|
60
|
+
readonly consumerTag: string;
|
|
61
|
+
readonly queue: string;
|
|
62
|
+
active: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* RabbitMQ transport implementation using amqplib.
|
|
67
|
+
*/
|
|
68
|
+
export class RabbitMQTransport implements Transport {
|
|
69
|
+
readonly name = 'rabbitmq';
|
|
70
|
+
|
|
71
|
+
private _capabilities: TransportCapabilities = {
|
|
72
|
+
deliveryModes: ['at-least-once'],
|
|
73
|
+
delayedMessages: false,
|
|
74
|
+
deadLetterRouting: 'native',
|
|
75
|
+
attemptTracking: true,
|
|
76
|
+
concurrencyModel: 'prefetch',
|
|
77
|
+
ordering: 'none',
|
|
78
|
+
priorities: true,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
get capabilities(): TransportCapabilities {
|
|
82
|
+
return this._capabilities;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private connection: ChannelModel | null = null;
|
|
86
|
+
private publishChannel: Channel | null = null;
|
|
87
|
+
private readonly connectionManager: ConnectionManager;
|
|
88
|
+
private readonly queueChannels = new Map<string, QueueChannel>();
|
|
89
|
+
private topology: Topology | null = null;
|
|
90
|
+
private readonly codec = new RabbitMQCodec();
|
|
91
|
+
|
|
92
|
+
private readonly config: Required<
|
|
93
|
+
Omit<RabbitMQTransportConfig, 'connection' | 'logger'>
|
|
94
|
+
> & {
|
|
95
|
+
connection: Partial<ConnectionManagerConfig>;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
private readonly logger: Logger;
|
|
99
|
+
private delayedExchangeAvailable = false;
|
|
100
|
+
|
|
101
|
+
constructor(config: RabbitMQTransportConfig) {
|
|
102
|
+
this.logger = config.logger ?? consoleLogger;
|
|
103
|
+
this.config = {
|
|
104
|
+
url: config.url,
|
|
105
|
+
connection: config.connection ?? {},
|
|
106
|
+
quorumQueues: config.quorumQueues ?? true,
|
|
107
|
+
defaultPrefetch: config.defaultPrefetch ?? 10,
|
|
108
|
+
enableDelayedMessages: config.enableDelayedMessages ?? true,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
this.connectionManager = new ConnectionManager(
|
|
112
|
+
() => this.doConnect(),
|
|
113
|
+
() => this.doDisconnect(),
|
|
114
|
+
this.config.connection,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async connect(): Promise<void> {
|
|
119
|
+
await this.connectionManager.connect();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async disconnect(): Promise<void> {
|
|
123
|
+
await this.connectionManager.disconnect();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
isConnected(): boolean {
|
|
127
|
+
return this.connectionManager.isConnected();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async applyTopology(topology: Topology): Promise<void> {
|
|
131
|
+
this.topology = topology;
|
|
132
|
+
|
|
133
|
+
if (!this.publishChannel) {
|
|
134
|
+
throw new TransportNotConnectedError(this.name, 'applyTopology');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const channel = this.publishChannel;
|
|
138
|
+
|
|
139
|
+
// Create the main exchange for routing messages to queues
|
|
140
|
+
const mainExchange = this.getMainExchangeName(topology.namespace);
|
|
141
|
+
await channel.assertExchange(mainExchange, 'direct', { durable: true });
|
|
142
|
+
|
|
143
|
+
// Create dead-letter exchange if DLQ is enabled
|
|
144
|
+
const dlxExchange = this.getDLXExchangeName(topology.namespace);
|
|
145
|
+
if (
|
|
146
|
+
topology.deadLetter.unhandled.enabled ||
|
|
147
|
+
topology.deadLetter.undeliverable.enabled
|
|
148
|
+
) {
|
|
149
|
+
await channel.assertExchange(dlxExchange, 'direct', { durable: true });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check for delayed message exchange plugin
|
|
153
|
+
if (this.config.enableDelayedMessages) {
|
|
154
|
+
await this.setupDelayedExchange(topology.namespace);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Create work queues
|
|
158
|
+
for (const queueDef of topology.queues) {
|
|
159
|
+
await this.assertWorkQueue(channel, topology, queueDef);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Create DLQs
|
|
163
|
+
if (topology.deadLetter.unhandled.enabled) {
|
|
164
|
+
await this.assertDeadLetterQueues(channel, topology, 'unhandled');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (topology.deadLetter.undeliverable.enabled) {
|
|
168
|
+
await this.assertDeadLetterQueues(channel, topology, 'undeliverable');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async send(
|
|
173
|
+
queue: string,
|
|
174
|
+
envelope: Envelope,
|
|
175
|
+
options?: SendOptions,
|
|
176
|
+
): Promise<Transport['name']> {
|
|
177
|
+
if (!this.publishChannel || !this.topology) {
|
|
178
|
+
throw new TransportNotConnectedError(this.name, 'send');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const encoded = this.codec.encode(envelope);
|
|
182
|
+
const buffer = Buffer.from(encoded.body);
|
|
183
|
+
|
|
184
|
+
const publishOptions: Options.Publish = {
|
|
185
|
+
persistent: true,
|
|
186
|
+
contentType: encoded.contentType,
|
|
187
|
+
messageId: envelope.id,
|
|
188
|
+
timestamp: Date.now(),
|
|
189
|
+
headers: encoded.headers,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
if (options?.priority !== undefined) {
|
|
193
|
+
publishOptions.priority = options.priority;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Handle delayed messages
|
|
197
|
+
if (options?.delay !== undefined && options.delay > 0) {
|
|
198
|
+
if (!this.delayedExchangeAvailable) {
|
|
199
|
+
throw new DelayedMessagesNotSupportedError(this.name);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const delayedExchange = this.getDelayedExchangeName(
|
|
203
|
+
this.topology.namespace,
|
|
204
|
+
);
|
|
205
|
+
publishOptions.headers = {
|
|
206
|
+
...publishOptions.headers,
|
|
207
|
+
'x-delay': options.delay,
|
|
208
|
+
};
|
|
209
|
+
this.publishChannel.publish(
|
|
210
|
+
delayedExchange,
|
|
211
|
+
queue,
|
|
212
|
+
buffer,
|
|
213
|
+
publishOptions,
|
|
214
|
+
);
|
|
215
|
+
return this.name;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Transport-specific options
|
|
219
|
+
if (options?.transport?.rabbitmq?.expiration !== undefined) {
|
|
220
|
+
publishOptions.expiration = String(options.transport.rabbitmq.expiration);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (options?.transport?.rabbitmq?.persistent !== undefined) {
|
|
224
|
+
publishOptions.persistent = options.transport.rabbitmq.persistent;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const routingKey = options?.transport?.rabbitmq?.routingKey ?? queue;
|
|
228
|
+
const exchange = this.getMainExchangeName(this.topology.namespace);
|
|
229
|
+
|
|
230
|
+
this.publishChannel.publish(exchange, routingKey, buffer, publishOptions);
|
|
231
|
+
return this.name;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async subscribe(
|
|
235
|
+
queue: string,
|
|
236
|
+
handler: MessageHandler,
|
|
237
|
+
options: SubscribeOptions = {},
|
|
238
|
+
): Promise<Subscription> {
|
|
239
|
+
if (!this.connection || !this.topology) {
|
|
240
|
+
throw new TransportNotConnectedError(this.name, 'subscribe');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Get or create a dedicated channel for this queue
|
|
244
|
+
const queueChannel = await this.getOrCreateQueueChannel(queue, options);
|
|
245
|
+
const { channel } = queueChannel;
|
|
246
|
+
|
|
247
|
+
const consumer: ActiveConsumer = {
|
|
248
|
+
consumerTag: '',
|
|
249
|
+
queue,
|
|
250
|
+
active: true,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const { consumerTag } = await channel.consume(
|
|
254
|
+
queue,
|
|
255
|
+
async (msg: ConsumeMessage | null) => {
|
|
256
|
+
if (!msg || !consumer.active) return;
|
|
257
|
+
|
|
258
|
+
const attemptNumber = this.getAttemptNumber(msg);
|
|
259
|
+
const receipt: MessageReceipt = {
|
|
260
|
+
handle: { channel, msg },
|
|
261
|
+
redelivered: msg.fields.redelivered,
|
|
262
|
+
attemptNumber,
|
|
263
|
+
deliveryCount: this.getDeliveryCount(msg, attemptNumber),
|
|
264
|
+
sourceQueue: queue,
|
|
265
|
+
sourceTransport: this.name,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const headers = (msg.properties.headers ?? {}) as Record<
|
|
270
|
+
string,
|
|
271
|
+
unknown
|
|
272
|
+
>;
|
|
273
|
+
const envelope = this.codec.decode(
|
|
274
|
+
new Uint8Array(msg.content),
|
|
275
|
+
headers,
|
|
276
|
+
);
|
|
277
|
+
await handler(envelope, receipt);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
// Handler errors should be caught in the pipeline
|
|
280
|
+
this.logger.error(
|
|
281
|
+
'[Matador] 🔴 Handler error in message processing',
|
|
282
|
+
error,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
{ noAck: false }, // Always manually ack
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
// Update the consumer tag
|
|
290
|
+
(consumer as { consumerTag: string }).consumerTag = consumerTag;
|
|
291
|
+
|
|
292
|
+
// Track the consumer
|
|
293
|
+
queueChannel.consumers.push(consumer);
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
unsubscribe: async () => {
|
|
297
|
+
consumer.active = false;
|
|
298
|
+
try {
|
|
299
|
+
await channel.cancel(consumerTag);
|
|
300
|
+
} catch {
|
|
301
|
+
// Channel may already be closed
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Remove consumer from tracking
|
|
305
|
+
const idx = queueChannel.consumers.indexOf(consumer);
|
|
306
|
+
if (idx !== -1) {
|
|
307
|
+
queueChannel.consumers.splice(idx, 1);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Close channel if no more consumers on this queue
|
|
311
|
+
if (queueChannel.consumers.length === 0) {
|
|
312
|
+
try {
|
|
313
|
+
await channel.close();
|
|
314
|
+
} catch {
|
|
315
|
+
// Ignore
|
|
316
|
+
}
|
|
317
|
+
this.queueChannels.delete(queue);
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
get isActive() {
|
|
321
|
+
return consumer.active;
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async complete(receipt: MessageReceipt): Promise<void> {
|
|
327
|
+
const { channel, msg } = receipt.handle as {
|
|
328
|
+
channel: Channel;
|
|
329
|
+
msg: ConsumeMessage;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
channel.ack(msg);
|
|
334
|
+
} catch {
|
|
335
|
+
// Channel may be closed, ignore
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async sendToDeadLetter(
|
|
340
|
+
receipt: MessageReceipt,
|
|
341
|
+
dlqName: string,
|
|
342
|
+
envelope: Envelope,
|
|
343
|
+
reason: string,
|
|
344
|
+
): Promise<void> {
|
|
345
|
+
if (!this.publishChannel || !this.topology) {
|
|
346
|
+
throw new TransportNotConnectedError(this.name, 'sendToDeadLetter');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Add error info to envelope
|
|
350
|
+
const dlqEnvelope: Envelope = {
|
|
351
|
+
...envelope,
|
|
352
|
+
docket: {
|
|
353
|
+
...envelope.docket,
|
|
354
|
+
lastError: reason,
|
|
355
|
+
firstError: envelope.docket.firstError ?? reason,
|
|
356
|
+
originalQueue: receipt.sourceQueue,
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const encoded = this.codec.encode(dlqEnvelope);
|
|
361
|
+
const buffer = Buffer.from(encoded.body);
|
|
362
|
+
const dlxExchange = this.getDLXExchangeName(this.topology.namespace);
|
|
363
|
+
const dlqQueueName = `${receipt.sourceQueue}.${dlqName}`;
|
|
364
|
+
|
|
365
|
+
const publishOptions: Options.Publish = {
|
|
366
|
+
persistent: true,
|
|
367
|
+
contentType: encoded.contentType,
|
|
368
|
+
messageId: envelope.id,
|
|
369
|
+
timestamp: Date.now(),
|
|
370
|
+
headers: {
|
|
371
|
+
...encoded.headers,
|
|
372
|
+
'x-matador-dead-letter-reason': reason,
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
this.publishChannel.publish(
|
|
377
|
+
dlxExchange,
|
|
378
|
+
dlqQueueName,
|
|
379
|
+
buffer,
|
|
380
|
+
publishOptions,
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// Complete the original message
|
|
384
|
+
await this.complete(receipt);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Private methods
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Gets or creates a dedicated channel for a queue subscription.
|
|
391
|
+
*
|
|
392
|
+
* We create separate channels per subscribed queue to enable independent
|
|
393
|
+
* prefetch/concurrency control.
|
|
394
|
+
*/
|
|
395
|
+
private async getOrCreateQueueChannel(
|
|
396
|
+
queue: string,
|
|
397
|
+
options: SubscribeOptions,
|
|
398
|
+
): Promise<QueueChannel> {
|
|
399
|
+
const existing = this.queueChannels.get(queue);
|
|
400
|
+
if (existing) {
|
|
401
|
+
return existing;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (!this.connection) {
|
|
405
|
+
throw new TransportNotConnectedError(
|
|
406
|
+
this.name,
|
|
407
|
+
'getOrCreateQueueChannel',
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Create a dedicated channel for this queue to control prefetch independently
|
|
412
|
+
const channel = await this.connection.createChannel();
|
|
413
|
+
|
|
414
|
+
const prefetch =
|
|
415
|
+
options.transport?.rabbitmq?.prefetch ??
|
|
416
|
+
options.concurrency ??
|
|
417
|
+
this.config.defaultPrefetch ??
|
|
418
|
+
10;
|
|
419
|
+
|
|
420
|
+
await channel.prefetch(prefetch);
|
|
421
|
+
|
|
422
|
+
const queueChannel: QueueChannel = {
|
|
423
|
+
channel,
|
|
424
|
+
consumers: [],
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
this.queueChannels.set(queue, queueChannel);
|
|
428
|
+
|
|
429
|
+
return queueChannel;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private async doConnect(): Promise<void> {
|
|
433
|
+
const connection = await amqplib.connect(this.config.url);
|
|
434
|
+
this.connection = connection;
|
|
435
|
+
|
|
436
|
+
// Handle connection errors - let ConnectionManager handle reconnection
|
|
437
|
+
connection.on('error', (err: Error) => {
|
|
438
|
+
this.logger.error('[Matador] 🔴 RabbitMQ connection error', err);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
connection.on('close', () => {
|
|
442
|
+
if (this.connectionManager.isConnected()) {
|
|
443
|
+
// Unexpected close, trigger reconnection
|
|
444
|
+
this.connectionManager.handleConnectionLost(
|
|
445
|
+
new Error('Connection closed unexpectedly'),
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Create the publish channel
|
|
451
|
+
this.publishChannel = await connection.createChannel();
|
|
452
|
+
|
|
453
|
+
// Handle publish channel errors to prevent unhandled error events
|
|
454
|
+
this.publishChannel.on('error', (err: Error) => {
|
|
455
|
+
this.logger.error('[Matador] 🔴 RabbitMQ publish channel error', err);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// Re-apply topology if we have one (reconnection scenario)
|
|
459
|
+
if (this.topology) {
|
|
460
|
+
await this.applyTopology(this.topology);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private async doDisconnect(): Promise<void> {
|
|
465
|
+
// Cancel all consumers and close queue channels
|
|
466
|
+
for (const queueChannel of this.queueChannels.values()) {
|
|
467
|
+
for (const consumer of queueChannel.consumers) {
|
|
468
|
+
consumer.active = false;
|
|
469
|
+
try {
|
|
470
|
+
await queueChannel.channel.cancel(consumer.consumerTag);
|
|
471
|
+
} catch {
|
|
472
|
+
// Ignore errors during cleanup
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
await queueChannel.channel.close();
|
|
477
|
+
} catch {
|
|
478
|
+
// Ignore
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
this.queueChannels.clear();
|
|
482
|
+
|
|
483
|
+
// Close publish channel
|
|
484
|
+
if (this.publishChannel) {
|
|
485
|
+
try {
|
|
486
|
+
await this.publishChannel.close();
|
|
487
|
+
} catch {
|
|
488
|
+
// Ignore
|
|
489
|
+
}
|
|
490
|
+
this.publishChannel = null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Close connection
|
|
494
|
+
if (this.connection) {
|
|
495
|
+
try {
|
|
496
|
+
await this.connection.close();
|
|
497
|
+
} catch {
|
|
498
|
+
// Ignore
|
|
499
|
+
}
|
|
500
|
+
this.connection = null;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Reset capabilities
|
|
504
|
+
this.delayedExchangeAvailable = false;
|
|
505
|
+
this._capabilities = {
|
|
506
|
+
...this._capabilities,
|
|
507
|
+
delayedMessages: false,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private async setupDelayedExchange(namespace: string): Promise<void> {
|
|
512
|
+
if (!this.connection) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Default to disabled
|
|
517
|
+
this.delayedExchangeAvailable = false;
|
|
518
|
+
|
|
519
|
+
const delayedExchange = this.getDelayedExchangeName(namespace);
|
|
520
|
+
const connection = this.connection;
|
|
521
|
+
|
|
522
|
+
// Use a promise-based approach to ensure all error paths resolve cleanly
|
|
523
|
+
// This prevents any error from propagating and affecting other channels
|
|
524
|
+
return new Promise<void>((resolve) => {
|
|
525
|
+
let resolved = false;
|
|
526
|
+
const safeResolve = () => {
|
|
527
|
+
if (!resolved) {
|
|
528
|
+
resolved = true;
|
|
529
|
+
resolve();
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
connection
|
|
534
|
+
.createChannel()
|
|
535
|
+
.then((probeChannel) => {
|
|
536
|
+
// Handle channel errors - this fires when RabbitMQ closes the channel
|
|
537
|
+
probeChannel.on('error', () => {
|
|
538
|
+
safeResolve();
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// Handle channel close
|
|
542
|
+
probeChannel.on('close', () => {
|
|
543
|
+
safeResolve();
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// Try to declare a delayed exchange
|
|
547
|
+
// This will fail if the plugin is not installed
|
|
548
|
+
probeChannel
|
|
549
|
+
.assertExchange(delayedExchange, 'x-delayed-message', {
|
|
550
|
+
durable: true,
|
|
551
|
+
arguments: { 'x-delayed-type': 'direct' },
|
|
552
|
+
})
|
|
553
|
+
.then(() => {
|
|
554
|
+
this.delayedExchangeAvailable = true;
|
|
555
|
+
this._capabilities = {
|
|
556
|
+
...this._capabilities,
|
|
557
|
+
delayedMessages: true,
|
|
558
|
+
};
|
|
559
|
+
this.logger.debug(
|
|
560
|
+
'[Matador] 🔌 Delayed message exchange plugin detected',
|
|
561
|
+
);
|
|
562
|
+
// Close the probe channel gracefully
|
|
563
|
+
probeChannel.close().catch(() => {});
|
|
564
|
+
safeResolve();
|
|
565
|
+
})
|
|
566
|
+
.catch(() => {
|
|
567
|
+
// assertExchange failed - plugin not available
|
|
568
|
+
// Channel is already closed by RabbitMQ, no need to close
|
|
569
|
+
this.logger.warn(
|
|
570
|
+
'[Matador] 🟡 RabbitMQ delayed message exchange plugin not available. ' +
|
|
571
|
+
'Delayed messages will not be supported.',
|
|
572
|
+
);
|
|
573
|
+
safeResolve();
|
|
574
|
+
});
|
|
575
|
+
})
|
|
576
|
+
.catch(() => {
|
|
577
|
+
// Failed to create channel - shouldn't happen but handle it
|
|
578
|
+
safeResolve();
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
private async assertWorkQueue(
|
|
584
|
+
channel: Channel,
|
|
585
|
+
topology: Topology,
|
|
586
|
+
queueDef: QueueDefinition,
|
|
587
|
+
): Promise<void> {
|
|
588
|
+
const queueName = queueDef.exact
|
|
589
|
+
? queueDef.name
|
|
590
|
+
: `${topology.namespace}.${queueDef.name}`;
|
|
591
|
+
|
|
592
|
+
const queueOptions: Options.AssertQueue = {
|
|
593
|
+
durable: true,
|
|
594
|
+
arguments: {} as Record<string, unknown>,
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
// Use quorum queues for durability
|
|
598
|
+
if (this.config.quorumQueues && !queueDef.exact) {
|
|
599
|
+
queueOptions.arguments['x-queue-type'] = 'quorum';
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Set up dead-letter exchange routing
|
|
603
|
+
const dlxExchange = this.getDLXExchangeName(topology.namespace);
|
|
604
|
+
if (
|
|
605
|
+
topology.deadLetter.unhandled.enabled ||
|
|
606
|
+
topology.deadLetter.undeliverable.enabled
|
|
607
|
+
) {
|
|
608
|
+
queueOptions.arguments['x-dead-letter-exchange'] = dlxExchange;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Enable priority if requested
|
|
612
|
+
if (queueDef.priorities) {
|
|
613
|
+
queueOptions.arguments['x-max-priority'] = 10;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Set consumer timeout if specified
|
|
617
|
+
if (queueDef.consumerTimeout) {
|
|
618
|
+
queueOptions.arguments['x-consumer-timeout'] = queueDef.consumerTimeout;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
await channel.assertQueue(queueName, queueOptions);
|
|
622
|
+
|
|
623
|
+
// Bind queue to main exchange
|
|
624
|
+
const mainExchange = this.getMainExchangeName(topology.namespace);
|
|
625
|
+
await channel.bindQueue(queueName, mainExchange, queueName);
|
|
626
|
+
|
|
627
|
+
// Bind to delayed exchange if available
|
|
628
|
+
if (this.delayedExchangeAvailable) {
|
|
629
|
+
const delayedExchange = this.getDelayedExchangeName(topology.namespace);
|
|
630
|
+
await channel.bindQueue(queueName, delayedExchange, queueName);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Create retry queue if retry is enabled
|
|
634
|
+
if (topology.retry.enabled) {
|
|
635
|
+
await this.assertRetryQueue(channel, topology, queueName);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private async assertRetryQueue(
|
|
640
|
+
channel: Channel,
|
|
641
|
+
topology: Topology,
|
|
642
|
+
workQueueName: string,
|
|
643
|
+
): Promise<void> {
|
|
644
|
+
const retryQueueName = `${workQueueName}.retry`;
|
|
645
|
+
const mainExchange = this.getMainExchangeName(topology.namespace);
|
|
646
|
+
|
|
647
|
+
const retryQueueOptions: Options.AssertQueue = {
|
|
648
|
+
durable: true,
|
|
649
|
+
arguments: {
|
|
650
|
+
'x-dead-letter-exchange': mainExchange,
|
|
651
|
+
'x-dead-letter-routing-key': workQueueName,
|
|
652
|
+
'x-message-ttl': topology.retry.defaultDelayMs,
|
|
653
|
+
} as Record<string, unknown>,
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
if (this.config.quorumQueues) {
|
|
657
|
+
retryQueueOptions.arguments['x-queue-type'] = 'quorum';
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
await channel.assertQueue(retryQueueName, retryQueueOptions);
|
|
661
|
+
await channel.bindQueue(retryQueueName, mainExchange, retryQueueName);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
private async assertDeadLetterQueues(
|
|
665
|
+
channel: Channel,
|
|
666
|
+
topology: Topology,
|
|
667
|
+
dlqType: 'unhandled' | 'undeliverable',
|
|
668
|
+
): Promise<void> {
|
|
669
|
+
const dlxExchange = this.getDLXExchangeName(topology.namespace);
|
|
670
|
+
const dlConfig = topology.deadLetter[dlqType];
|
|
671
|
+
|
|
672
|
+
for (const queueDef of topology.queues) {
|
|
673
|
+
if (queueDef.exact) continue;
|
|
674
|
+
|
|
675
|
+
const workQueueName = `${topology.namespace}.${queueDef.name}`;
|
|
676
|
+
const dlqName = `${workQueueName}.${dlqType}`;
|
|
677
|
+
|
|
678
|
+
const dlqOptions: Options.AssertQueue = {
|
|
679
|
+
durable: true,
|
|
680
|
+
arguments: {} as Record<string, unknown>,
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
if (dlConfig.maxLength) {
|
|
684
|
+
dlqOptions.arguments['x-max-length'] = dlConfig.maxLength;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// DLQs use classic queues (not quorum) for simplicity
|
|
688
|
+
await channel.assertQueue(dlqName, dlqOptions);
|
|
689
|
+
await channel.bindQueue(dlqName, dlxExchange, dlqName);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
private getMainExchangeName(namespace: string): string {
|
|
694
|
+
return `${namespace}.exchange`;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
private getDLXExchangeName(namespace: string): string {
|
|
698
|
+
return `${namespace}.dlx`;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
private getDelayedExchangeName(namespace: string): string {
|
|
702
|
+
return `${namespace}.delayed`;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
private getAttemptNumber(msg: ConsumeMessage): number {
|
|
706
|
+
const headerValue = msg.properties.headers?.['x-matador-attempts'];
|
|
707
|
+
if (typeof headerValue === 'number') {
|
|
708
|
+
return headerValue;
|
|
709
|
+
}
|
|
710
|
+
// Check for x-death header (native DLX redelivery count)
|
|
711
|
+
const xDeath = msg.properties.headers?.['x-death'];
|
|
712
|
+
if (Array.isArray(xDeath) && xDeath.length > 0) {
|
|
713
|
+
const deathCount = xDeath.reduce(
|
|
714
|
+
(sum: number, death: { count?: number }) => sum + (death.count ?? 0),
|
|
715
|
+
0,
|
|
716
|
+
);
|
|
717
|
+
return deathCount + 1;
|
|
718
|
+
}
|
|
719
|
+
return 1;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Gets the native delivery count for poison message detection.
|
|
724
|
+
* This tracks how many times the message was delivered without acknowledgment,
|
|
725
|
+
* which helps detect crash loops.
|
|
726
|
+
*/
|
|
727
|
+
private getDeliveryCount(msg: ConsumeMessage, attemptNumber: number): number {
|
|
728
|
+
// Check for explicit delivery count header (some RabbitMQ setups track this)
|
|
729
|
+
const deliveryCount = msg.properties.headers?.['x-delivery-count'];
|
|
730
|
+
if (typeof deliveryCount === 'number') {
|
|
731
|
+
return deliveryCount;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Check x-death header for dead-letter redelivery count
|
|
735
|
+
const xDeath = msg.properties.headers?.['x-death'];
|
|
736
|
+
if (Array.isArray(xDeath) && xDeath.length > 0) {
|
|
737
|
+
const deathCount = xDeath.reduce(
|
|
738
|
+
(sum: number, death: { count?: number }) => sum + (death.count ?? 0),
|
|
739
|
+
0,
|
|
740
|
+
);
|
|
741
|
+
// Add 1 because we're currently being delivered again
|
|
742
|
+
return deathCount + 1;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// If redelivered flag is set but no other tracking, count as 2 (first + this delivery)
|
|
746
|
+
if (msg.fields.redelivered) {
|
|
747
|
+
return Math.max(2, attemptNumber);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Default to attempt number
|
|
751
|
+
return attemptNumber;
|
|
752
|
+
}
|
|
753
|
+
}
|