@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
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3156 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var amqplib = require('amqplib');
|
|
4
|
+
|
|
5
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
6
|
+
|
|
7
|
+
var amqplib__default = /*#__PURE__*/_interopDefault(amqplib);
|
|
8
|
+
|
|
9
|
+
// src/errors/has-description.ts
|
|
10
|
+
function hasDescription(error) {
|
|
11
|
+
return typeof error === "object" && error !== null && "description" in error && typeof error.description === "string";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/errors/retry-errors.ts
|
|
15
|
+
var RetryControlError = class extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = this.constructor.name;
|
|
19
|
+
Object.defineProperty(this, "name", {
|
|
20
|
+
value: this.constructor.name,
|
|
21
|
+
enumerable: true,
|
|
22
|
+
configurable: false,
|
|
23
|
+
writable: false
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Returns a serializable representation for logging/monitoring.
|
|
28
|
+
*/
|
|
29
|
+
toJSON() {
|
|
30
|
+
return {
|
|
31
|
+
name: this.name,
|
|
32
|
+
message: this.message,
|
|
33
|
+
description: this.description,
|
|
34
|
+
stack: this.stack
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var DoRetry = class extends RetryControlError {
|
|
39
|
+
description = "A subscriber explicitly requested retry by throwing DoRetry. ACTION: Check the subscriber code to understand why retry was forced. This overrides the default retry behavior based on idempotency settings.";
|
|
40
|
+
constructor(message = "Forced retry requested") {
|
|
41
|
+
super(message);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var DontRetry = class extends RetryControlError {
|
|
45
|
+
description = "A subscriber explicitly prevented retry by throwing DontRetry. ACTION: Check the subscriber code to understand why retry was disabled. Typically used for permanent failures like invalid data or business rule violations. The message will be sent to the dead-letter queue for manual review.";
|
|
46
|
+
constructor(message = "Retry explicitly disabled") {
|
|
47
|
+
super(message);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var EventAssertionError = class extends Error {
|
|
51
|
+
description = "An event assertion failed, indicating a programming error or invariant violation. ACTION: Review the assertion failure message to identify the bug in the event payload or subscriber logic. These errors are never retried and go directly to the dead-letter queue.";
|
|
52
|
+
constructor(message) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "EventAssertionError";
|
|
55
|
+
Object.defineProperty(this, "name", {
|
|
56
|
+
value: "EventAssertionError",
|
|
57
|
+
enumerable: true,
|
|
58
|
+
configurable: false,
|
|
59
|
+
writable: false
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
toJSON() {
|
|
63
|
+
return {
|
|
64
|
+
name: this.name,
|
|
65
|
+
message: this.message,
|
|
66
|
+
description: this.description,
|
|
67
|
+
stack: this.stack
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
function isDoRetry(error) {
|
|
72
|
+
return error instanceof DoRetry;
|
|
73
|
+
}
|
|
74
|
+
function isDontRetry(error) {
|
|
75
|
+
return error instanceof DontRetry;
|
|
76
|
+
}
|
|
77
|
+
function isAssertionError(error) {
|
|
78
|
+
return error instanceof EventAssertionError;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/errors/matador-errors.ts
|
|
82
|
+
var MatadorError = class extends Error {
|
|
83
|
+
constructor(message) {
|
|
84
|
+
super(message);
|
|
85
|
+
this.name = this.constructor.name;
|
|
86
|
+
Object.defineProperty(this, "name", {
|
|
87
|
+
value: this.constructor.name,
|
|
88
|
+
enumerable: true,
|
|
89
|
+
configurable: false,
|
|
90
|
+
writable: false
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Returns a serializable representation for logging/monitoring.
|
|
95
|
+
*/
|
|
96
|
+
toJSON() {
|
|
97
|
+
return {
|
|
98
|
+
name: this.name,
|
|
99
|
+
message: this.message,
|
|
100
|
+
description: this.description,
|
|
101
|
+
stack: this.stack
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
var NotStartedError = class extends MatadorError {
|
|
106
|
+
description = "Matador has not been started. ACTION: Call matador.start() before dispatching events or performing other operations. Ensure start() completes successfully before using other methods.";
|
|
107
|
+
constructor(operation = "operation") {
|
|
108
|
+
super(
|
|
109
|
+
`Cannot perform ${operation}: Matador has not been started. Call start() first.`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var ShutdownInProgressError = class extends MatadorError {
|
|
114
|
+
description = "Matador is shutting down and will not accept new events. ACTION: Do not dispatch events after calling shutdown(). If you need to send events, do so before initiating shutdown. Consider implementing a pre-shutdown event flush if needed.";
|
|
115
|
+
constructor() {
|
|
116
|
+
super("Cannot dispatch events: Matador is shutting down.");
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var TransportNotConnectedError = class extends MatadorError {
|
|
120
|
+
constructor(transportName, operation = "operation") {
|
|
121
|
+
super(
|
|
122
|
+
`Cannot perform ${operation}: Transport "${transportName}" is not connected. Ensure connect() was called and the broker is accessible.`
|
|
123
|
+
);
|
|
124
|
+
this.transportName = transportName;
|
|
125
|
+
}
|
|
126
|
+
description = "The transport is not connected to the message broker. ACTION: Ensure the transport is connected by calling transport.connect() or matador.start(). Check that the broker (e.g., RabbitMQ) is running and accessible. Verify connection settings (URL, credentials, network access).";
|
|
127
|
+
};
|
|
128
|
+
var TransportClosedError = class extends MatadorError {
|
|
129
|
+
constructor(transportName) {
|
|
130
|
+
super(
|
|
131
|
+
`Transport "${transportName}" has been closed and will not accept new operations.`
|
|
132
|
+
);
|
|
133
|
+
this.transportName = transportName;
|
|
134
|
+
}
|
|
135
|
+
description = "The transport has been closed and will not accept new operations. ACTION: This typically occurs during application shutdown. If unexpected, check for early shutdown triggers. Events sent after transport closure will be lost.";
|
|
136
|
+
};
|
|
137
|
+
var AllTransportsFailedError = class extends MatadorError {
|
|
138
|
+
constructor(queue, errors) {
|
|
139
|
+
super(
|
|
140
|
+
`All transports failed to send message to queue "${queue}". Errors: ${errors.map((e) => e.message).join("; ")}`
|
|
141
|
+
);
|
|
142
|
+
this.queue = queue;
|
|
143
|
+
this.errors = errors;
|
|
144
|
+
}
|
|
145
|
+
description = "All transports failed to send the message. ACTION: Check the health of all configured transports (primary and fallbacks). Review the errors array for specific failure reasons. Ensure at least one transport is properly configured and reachable. Consider adding a LocalTransport as a last-resort fallback.";
|
|
146
|
+
};
|
|
147
|
+
var TransportSendError = class extends MatadorError {
|
|
148
|
+
constructor(queue, cause) {
|
|
149
|
+
super(`Failed to send message to queue "${queue}": ${cause.message}`);
|
|
150
|
+
this.queue = queue;
|
|
151
|
+
this.cause = cause;
|
|
152
|
+
}
|
|
153
|
+
description = "Failed to send a message through the transport. ACTION: Check the underlying error for details. Common causes: (1) Transport disconnected during send, (2) Network issues between application and broker, (3) Broker rejected the message (size, permissions, queue limits). The message was NOT delivered and should be retried or logged.";
|
|
154
|
+
toJSON() {
|
|
155
|
+
return {
|
|
156
|
+
...super.toJSON(),
|
|
157
|
+
queue: this.queue,
|
|
158
|
+
cause: {
|
|
159
|
+
name: this.cause.name,
|
|
160
|
+
message: this.cause.message
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
var DelayedMessagesNotSupportedError = class extends MatadorError {
|
|
166
|
+
constructor(transportName) {
|
|
167
|
+
super(
|
|
168
|
+
`Delayed messages require the RabbitMQ delayed message exchange plugin. Install rabbitmq_delayed_message_exchange or remove delayMs from event options.`
|
|
169
|
+
);
|
|
170
|
+
this.transportName = transportName;
|
|
171
|
+
}
|
|
172
|
+
description = "Delayed messages were requested but the transport does not support them. ACTION: For RabbitMQ, install the rabbitmq_delayed_message_exchange plugin. Run: rabbitmq-plugins enable rabbitmq_delayed_message_exchange Then restart RabbitMQ and reconnect. Alternatively, remove delayMs from your event options if delays are not required.";
|
|
173
|
+
};
|
|
174
|
+
var EventNotRegisteredError = class extends MatadorError {
|
|
175
|
+
constructor(eventKey) {
|
|
176
|
+
super(
|
|
177
|
+
`Event "${eventKey}" is not registered in schema. Register it using matador.register(EventClass, subscribers).`
|
|
178
|
+
);
|
|
179
|
+
this.eventKey = eventKey;
|
|
180
|
+
}
|
|
181
|
+
description = "The event type is not registered in the schema. ACTION: Register the event using matador.register(EventClass, subscribers) before dispatching. If this occurs during message consumption, it may indicate schema drift between services. Ensure all services have matching schema registrations for shared events.";
|
|
182
|
+
};
|
|
183
|
+
var SubscriberNotRegisteredError = class extends MatadorError {
|
|
184
|
+
constructor(subscriberName, eventKey) {
|
|
185
|
+
super(
|
|
186
|
+
`Subscriber "${subscriberName}" is not registered` + (eventKey ? ` for event "${eventKey}"` : "") + ". Check schema registration."
|
|
187
|
+
);
|
|
188
|
+
this.subscriberName = subscriberName;
|
|
189
|
+
this.eventKey = eventKey;
|
|
190
|
+
}
|
|
191
|
+
description = "The subscriber is not registered for this event in the schema. ACTION: Ensure the subscriber is included in the registration for this event. This may occur if: (1) The subscriber was removed from the schema but messages still exist, (2) Schema drift between producer and consumer services, (3) A deployment is in progress with different schema versions. Check the dead-letter queue for affected messages.";
|
|
192
|
+
};
|
|
193
|
+
var NoSubscribersExistError = class extends MatadorError {
|
|
194
|
+
constructor(eventKey) {
|
|
195
|
+
super(
|
|
196
|
+
`No subscribers registered for event "${eventKey}". Add subscribers using matador.register(EventClass, [subscriber1, subscriber2]).`
|
|
197
|
+
);
|
|
198
|
+
this.eventKey = eventKey;
|
|
199
|
+
}
|
|
200
|
+
description = "The event has no subscribers registered. ACTION: Register at least one subscriber for this event type. If subscribers were intentionally removed, consider also removing the event dispatch. Events without subscribers are not useful and may indicate configuration issues.";
|
|
201
|
+
};
|
|
202
|
+
var InvalidSchemaError = class extends MatadorError {
|
|
203
|
+
constructor(message, cause) {
|
|
204
|
+
super(`Invalid schema: ${message}` + (cause ? `. Cause: ${cause}` : ""));
|
|
205
|
+
this.cause = cause;
|
|
206
|
+
}
|
|
207
|
+
description = "The schema configuration is invalid. ACTION: Review the schema registration for issues. Common problems: (1) Duplicate subscriber names for the same event, (2) Missing required fields on event class (key, description), (3) Invalid alias configuration. Check the cause property for specific details.";
|
|
208
|
+
};
|
|
209
|
+
var SubscriberIsStubError = class extends MatadorError {
|
|
210
|
+
constructor(subscriberName) {
|
|
211
|
+
super(
|
|
212
|
+
`Subscriber "${subscriberName}" is a stub and cannot be processed locally. Replace with a full Subscriber implementation in the consumer schema.`
|
|
213
|
+
);
|
|
214
|
+
this.subscriberName = subscriberName;
|
|
215
|
+
}
|
|
216
|
+
description = "A SubscriberStub was registered in a consuming schema. ACTION: SubscriberStubs should only be used in producer schemas to declare that a subscriber exists in another service. In the consumer service, provide a full Subscriber with a callback function. Remove the stub from the consumer schema and add the actual implementation.";
|
|
217
|
+
};
|
|
218
|
+
var LocalTransportCannotProcessStubError = class extends MatadorError {
|
|
219
|
+
constructor(subscriberName) {
|
|
220
|
+
super(
|
|
221
|
+
`LocalTransport cannot process stub subscriber "${subscriberName}". Stub subscribers require a distributed transport like RabbitMQ.`
|
|
222
|
+
);
|
|
223
|
+
this.subscriberName = subscriberName;
|
|
224
|
+
}
|
|
225
|
+
description = "The LocalTransport cannot process events for SubscriberStubs. ACTION: SubscriberStubs represent remote implementations that only RabbitMQ can route. If using LocalTransport for testing, provide mock implementations instead of stubs. For production fallback scenarios, be aware that stub-targeted events will be dropped.";
|
|
226
|
+
};
|
|
227
|
+
var QueueNotFoundError = class extends MatadorError {
|
|
228
|
+
constructor(queueName) {
|
|
229
|
+
super(
|
|
230
|
+
`Queue "${queueName}" not found. Ensure it is defined in topology and applyTopology() was called.`
|
|
231
|
+
);
|
|
232
|
+
this.queueName = queueName;
|
|
233
|
+
}
|
|
234
|
+
description = "The specified queue does not exist or has not been created. ACTION: Ensure the queue is defined in the topology configuration. Call transport.applyTopology() or matador.start() to create queues. Check that the queue name matches the topology definition.";
|
|
235
|
+
};
|
|
236
|
+
var InvalidEventError = class extends MatadorError {
|
|
237
|
+
constructor(message, cause) {
|
|
238
|
+
super(`Invalid event: ${message}` + (cause ? `. Cause: ${cause}` : ""));
|
|
239
|
+
this.cause = cause;
|
|
240
|
+
}
|
|
241
|
+
description = "The event is invalid or missing required fields. ACTION: Ensure the event has all required properties. Common issues: missing targetSubscriber during processing, null/undefined data when the event type requires data, malformed event structure from codec decode failure.";
|
|
242
|
+
};
|
|
243
|
+
var MessageMaybePoisonedError = class extends MatadorError {
|
|
244
|
+
constructor(eventId, deliveryCount, maxDeliveries) {
|
|
245
|
+
super(
|
|
246
|
+
`Message "${eventId}" delivered ${deliveryCount} times (max: ${maxDeliveries}). Possible poison message - will not be retried.`
|
|
247
|
+
);
|
|
248
|
+
this.eventId = eventId;
|
|
249
|
+
this.deliveryCount = deliveryCount;
|
|
250
|
+
this.maxDeliveries = maxDeliveries;
|
|
251
|
+
}
|
|
252
|
+
description = "A message was redelivered multiple times without successful processing. This usually indicates the message causes a crash or timeout during processing. ACTION: (1) Check application logs for errors/crashes during message processing, (2) Inspect the message in the dead-letter queue for malformed data, (3) Review the subscriber code for bugs that cause crashes, (4) Consider increasing processing timeout if the operation is legitimately slow. This message will NOT be retried to prevent crash loops.";
|
|
253
|
+
};
|
|
254
|
+
var IdempotentMessageCannotRetryError = class extends MatadorError {
|
|
255
|
+
constructor(eventId, subscriberName) {
|
|
256
|
+
super(
|
|
257
|
+
`Non-idempotent subscriber "${subscriberName}" cannot retry redelivered message "${eventId}". Mark subscriber as idempotent='yes' or implement idempotency handling.`
|
|
258
|
+
);
|
|
259
|
+
this.eventId = eventId;
|
|
260
|
+
this.subscriberName = subscriberName;
|
|
261
|
+
}
|
|
262
|
+
description = "A non-idempotent subscriber received a redelivered message. Retrying would risk duplicate side effects (e.g., double payments, duplicate emails). ACTION: (1) Mark the subscriber as idempotent if it safely handles duplicates, (2) Implement idempotency keys in the subscriber logic, (3) Manually inspect and replay the message from the dead-letter queue after verification. The message will be sent to the dead-letter queue for manual review.";
|
|
263
|
+
};
|
|
264
|
+
var TimeoutError = class extends MatadorError {
|
|
265
|
+
constructor(operation, timeoutMs) {
|
|
266
|
+
super(`Operation "${operation}" timed out after ${timeoutMs}ms.`);
|
|
267
|
+
this.operation = operation;
|
|
268
|
+
this.timeoutMs = timeoutMs;
|
|
269
|
+
}
|
|
270
|
+
description = "An operation timed out before completing. ACTION: (1) Increase the timeout if the operation legitimately needs more time, (2) Optimize the operation to complete faster, (3) Check for deadlocks or blocking operations, (4) Verify external service dependencies are responsive.";
|
|
271
|
+
};
|
|
272
|
+
function isMatadorError(error) {
|
|
273
|
+
return error instanceof MatadorError;
|
|
274
|
+
}
|
|
275
|
+
function isNotStartedError(error) {
|
|
276
|
+
return error instanceof NotStartedError;
|
|
277
|
+
}
|
|
278
|
+
function isTransportNotConnectedError(error) {
|
|
279
|
+
return error instanceof TransportNotConnectedError;
|
|
280
|
+
}
|
|
281
|
+
function isEventNotRegisteredError(error) {
|
|
282
|
+
return error instanceof EventNotRegisteredError;
|
|
283
|
+
}
|
|
284
|
+
function isSubscriberNotRegisteredError(error) {
|
|
285
|
+
return error instanceof SubscriberNotRegisteredError;
|
|
286
|
+
}
|
|
287
|
+
function isMessageMaybePoisonedError(error) {
|
|
288
|
+
return error instanceof MessageMaybePoisonedError;
|
|
289
|
+
}
|
|
290
|
+
function isIdempotentMessageCannotRetryError(error) {
|
|
291
|
+
return error instanceof IdempotentMessageCannotRetryError;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/errors/checkpoint-errors.ts
|
|
295
|
+
var DuplicateIoKeyError = class extends MatadorError {
|
|
296
|
+
constructor(key, subscriberName) {
|
|
297
|
+
super(
|
|
298
|
+
`Duplicate io key "${key}" in subscriber "${subscriberName}". Each io() call must have a unique key.`
|
|
299
|
+
);
|
|
300
|
+
this.key = key;
|
|
301
|
+
this.subscriberName = subscriberName;
|
|
302
|
+
}
|
|
303
|
+
description = "The same io() key was used multiple times in a single subscriber execution. Each io() call must have a unique key to ensure correct checkpoint behavior. ACTION: Ensure all io() keys are unique within the subscriber. For dynamic operations (e.g., loops), include a unique identifier in the key (e.g., `process-item-${item.id}`).";
|
|
304
|
+
};
|
|
305
|
+
var CheckpointStoreError = class extends MatadorError {
|
|
306
|
+
constructor(operation, envelopeId, cause) {
|
|
307
|
+
super(
|
|
308
|
+
`Checkpoint store ${operation} failed for envelope "${envelopeId}": ${cause.message}`
|
|
309
|
+
);
|
|
310
|
+
this.operation = operation;
|
|
311
|
+
this.envelopeId = envelopeId;
|
|
312
|
+
this.cause = cause;
|
|
313
|
+
}
|
|
314
|
+
description = "A checkpoint store operation failed. This may cause issues with resumable subscribers. ACTION: Check the underlying storage system (Redis, etc.) for connectivity issues. The subscriber may re-execute operations that were already completed.";
|
|
315
|
+
};
|
|
316
|
+
function isDuplicateIoKeyError(error) {
|
|
317
|
+
return error instanceof DuplicateIoKeyError;
|
|
318
|
+
}
|
|
319
|
+
function isCheckpointStoreError(error) {
|
|
320
|
+
return error instanceof CheckpointStoreError;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/topology/types.ts
|
|
324
|
+
function getQualifiedQueueName(namespace, queueName) {
|
|
325
|
+
return `${namespace}.${queueName}`;
|
|
326
|
+
}
|
|
327
|
+
function getDeadLetterQueueName(namespace, queueName, dlqType) {
|
|
328
|
+
return `${namespace}.${queueName}.${dlqType}`;
|
|
329
|
+
}
|
|
330
|
+
function getRetryQueueName(namespace, queueName) {
|
|
331
|
+
return `${namespace}.${queueName}.retry`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/topology/builder.ts
|
|
335
|
+
var TopologyValidationError = class extends Error {
|
|
336
|
+
constructor(message, issues) {
|
|
337
|
+
super(message);
|
|
338
|
+
this.issues = issues;
|
|
339
|
+
this.name = "TopologyValidationError";
|
|
340
|
+
}
|
|
341
|
+
description = "The topology configuration is invalid. Check the issues array for specific validation failures such as missing namespace, invalid queue names, or conflicting settings. This error occurs during Matador initialization and must be fixed in the configuration.";
|
|
342
|
+
};
|
|
343
|
+
var TopologyBuilder = class _TopologyBuilder {
|
|
344
|
+
/**
|
|
345
|
+
* Creates a new TopologyBuilder instance.
|
|
346
|
+
*/
|
|
347
|
+
static create() {
|
|
348
|
+
return new _TopologyBuilder();
|
|
349
|
+
}
|
|
350
|
+
namespace = "";
|
|
351
|
+
queues = [];
|
|
352
|
+
deadLetter = {
|
|
353
|
+
unhandled: { enabled: true },
|
|
354
|
+
undeliverable: { enabled: true }
|
|
355
|
+
};
|
|
356
|
+
retry = {
|
|
357
|
+
enabled: true,
|
|
358
|
+
defaultDelayMs: 1e3,
|
|
359
|
+
maxDelayMs: 3e5
|
|
360
|
+
// 5 minutes
|
|
361
|
+
};
|
|
362
|
+
/**
|
|
363
|
+
* Sets the namespace prefix for all queues.
|
|
364
|
+
*/
|
|
365
|
+
withNamespace(namespace) {
|
|
366
|
+
this.namespace = namespace;
|
|
367
|
+
return this;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Adds a queue to the topology.
|
|
371
|
+
*/
|
|
372
|
+
addQueue(name, options = {}) {
|
|
373
|
+
this.queues.push({ name, ...options });
|
|
374
|
+
return this;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Alias for addQueue().
|
|
378
|
+
*/
|
|
379
|
+
queue(name, options = {}) {
|
|
380
|
+
return this.addQueue(name, options);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Configures dead-letter queue settings.
|
|
384
|
+
*/
|
|
385
|
+
withDeadLetter(config) {
|
|
386
|
+
this.deadLetter = {
|
|
387
|
+
unhandled: config.unhandled ?? this.deadLetter.unhandled,
|
|
388
|
+
undeliverable: config.undeliverable ?? this.deadLetter.undeliverable
|
|
389
|
+
};
|
|
390
|
+
return this;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Configures retry settings.
|
|
394
|
+
*/
|
|
395
|
+
withRetry(config) {
|
|
396
|
+
this.retry = {
|
|
397
|
+
enabled: config.enabled ?? this.retry.enabled,
|
|
398
|
+
defaultDelayMs: config.defaultDelayMs ?? this.retry.defaultDelayMs,
|
|
399
|
+
maxDelayMs: config.maxDelayMs ?? this.retry.maxDelayMs
|
|
400
|
+
};
|
|
401
|
+
return this;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Disables retry functionality.
|
|
405
|
+
*/
|
|
406
|
+
withoutRetry() {
|
|
407
|
+
this.retry = { ...this.retry, enabled: false };
|
|
408
|
+
return this;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Disables dead-letter queues.
|
|
412
|
+
*/
|
|
413
|
+
withoutDeadLetter() {
|
|
414
|
+
this.deadLetter = {
|
|
415
|
+
unhandled: { enabled: false },
|
|
416
|
+
undeliverable: { enabled: false }
|
|
417
|
+
};
|
|
418
|
+
return this;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Validates the topology configuration.
|
|
422
|
+
*/
|
|
423
|
+
validate() {
|
|
424
|
+
const issues = [];
|
|
425
|
+
if (!this.namespace || this.namespace.trim() === "") {
|
|
426
|
+
issues.push("Namespace is required");
|
|
427
|
+
} else if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(this.namespace)) {
|
|
428
|
+
issues.push(
|
|
429
|
+
"Namespace must start with a letter and contain only alphanumeric characters, underscores, and hyphens"
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
if (this.queues.length === 0) {
|
|
433
|
+
issues.push("At least one queue is required");
|
|
434
|
+
}
|
|
435
|
+
const queueNames = /* @__PURE__ */ new Set();
|
|
436
|
+
for (const queue of this.queues) {
|
|
437
|
+
if (!queue.name || queue.name.trim() === "") {
|
|
438
|
+
issues.push("Queue name cannot be empty");
|
|
439
|
+
} else if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(queue.name)) {
|
|
440
|
+
issues.push(
|
|
441
|
+
`Queue name "${queue.name}" must start with a letter and contain only alphanumeric characters, underscores, and hyphens`
|
|
442
|
+
);
|
|
443
|
+
} else if (queueNames.has(queue.name)) {
|
|
444
|
+
issues.push(`Duplicate queue name: "${queue.name}"`);
|
|
445
|
+
} else {
|
|
446
|
+
queueNames.add(queue.name);
|
|
447
|
+
}
|
|
448
|
+
if (queue.concurrency !== void 0 && queue.concurrency < 1) {
|
|
449
|
+
issues.push(`Queue "${queue.name}" concurrency must be at least 1`);
|
|
450
|
+
}
|
|
451
|
+
if (queue.consumerTimeout !== void 0 && queue.consumerTimeout < 0) {
|
|
452
|
+
issues.push(
|
|
453
|
+
`Queue "${queue.name}" consumer timeout must be non-negative`
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (this.retry.enabled) {
|
|
458
|
+
if (this.retry.defaultDelayMs < 0) {
|
|
459
|
+
issues.push("Default retry delay must be non-negative");
|
|
460
|
+
}
|
|
461
|
+
if (this.retry.maxDelayMs < this.retry.defaultDelayMs) {
|
|
462
|
+
issues.push(
|
|
463
|
+
"Max retry delay must be greater than or equal to default delay"
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return issues;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Builds the topology configuration.
|
|
471
|
+
* @throws TopologyValidationError if validation fails
|
|
472
|
+
*/
|
|
473
|
+
build() {
|
|
474
|
+
const issues = this.validate();
|
|
475
|
+
if (issues.length > 0) {
|
|
476
|
+
throw new TopologyValidationError(
|
|
477
|
+
`Invalid topology: ${issues.join("; ")}`,
|
|
478
|
+
issues
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
namespace: this.namespace,
|
|
483
|
+
queues: [...this.queues],
|
|
484
|
+
deadLetter: this.deadLetter,
|
|
485
|
+
retry: this.retry
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
// src/types/common.ts
|
|
491
|
+
function validResult() {
|
|
492
|
+
return { valid: true, errors: [] };
|
|
493
|
+
}
|
|
494
|
+
function invalidResult(errors) {
|
|
495
|
+
return { valid: false, errors };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/types/envelope.ts
|
|
499
|
+
function createEnvelope(options) {
|
|
500
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
501
|
+
const mergedMetadata = options.universalMetadata || options.metadata ? { ...options.universalMetadata, ...options.metadata } : void 0;
|
|
502
|
+
return {
|
|
503
|
+
id: options.id ?? crypto.randomUUID(),
|
|
504
|
+
data: options.data,
|
|
505
|
+
docket: {
|
|
506
|
+
// Routing
|
|
507
|
+
eventKey: options.eventKey,
|
|
508
|
+
...options.eventDescription !== void 0 && {
|
|
509
|
+
eventDescription: options.eventDescription
|
|
510
|
+
},
|
|
511
|
+
targetSubscriber: options.targetSubscriber,
|
|
512
|
+
...options.delayMs !== void 0 && options.delayMs > 0 && {
|
|
513
|
+
scheduledFor: new Date(Date.now() + options.delayMs).toISOString()
|
|
514
|
+
},
|
|
515
|
+
// Processing state
|
|
516
|
+
attempts: 1,
|
|
517
|
+
createdAt: now,
|
|
518
|
+
// Observability
|
|
519
|
+
importance: options.importance,
|
|
520
|
+
...options.correlationId !== void 0 && {
|
|
521
|
+
correlationId: options.correlationId
|
|
522
|
+
},
|
|
523
|
+
...mergedMetadata !== void 0 && { metadata: mergedMetadata }
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/types/event.ts
|
|
529
|
+
var MatadorEvent = class {
|
|
530
|
+
static key;
|
|
531
|
+
static description;
|
|
532
|
+
static aliases;
|
|
533
|
+
/** Event-specific metadata */
|
|
534
|
+
metadata;
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
// src/types/subscriber.ts
|
|
538
|
+
function isSubscriberStub(subscriber) {
|
|
539
|
+
return "isStub" in subscriber && subscriber.isStub === true;
|
|
540
|
+
}
|
|
541
|
+
function isSubscriber(subscriber) {
|
|
542
|
+
return "callback" in subscriber && typeof subscriber.callback === "function";
|
|
543
|
+
}
|
|
544
|
+
function isResumableSubscriber(subscriber) {
|
|
545
|
+
return isSubscriber(subscriber) && subscriber.idempotent === "resumable";
|
|
546
|
+
}
|
|
547
|
+
function createSubscriber(input) {
|
|
548
|
+
const base = {
|
|
549
|
+
name: input.name,
|
|
550
|
+
description: input.description,
|
|
551
|
+
callback: input.callback,
|
|
552
|
+
importance: input.importance ?? "should-investigate",
|
|
553
|
+
...input.targetQueue !== void 0 && {
|
|
554
|
+
targetQueue: input.targetQueue
|
|
555
|
+
},
|
|
556
|
+
...input.enabled !== void 0 && { enabled: input.enabled }
|
|
557
|
+
};
|
|
558
|
+
if (input.idempotent === "resumable") {
|
|
559
|
+
return {
|
|
560
|
+
...base,
|
|
561
|
+
idempotent: "resumable",
|
|
562
|
+
callback: input.callback
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
return {
|
|
566
|
+
...base,
|
|
567
|
+
idempotent: input.idempotent ?? "unknown",
|
|
568
|
+
callback: input.callback
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
function createSubscriberStub(input) {
|
|
572
|
+
return {
|
|
573
|
+
name: input.name,
|
|
574
|
+
description: input.description,
|
|
575
|
+
isStub: true,
|
|
576
|
+
idempotent: input.idempotent ?? "unknown",
|
|
577
|
+
importance: input.importance ?? "should-investigate",
|
|
578
|
+
...input.targetQueue !== void 0 && {
|
|
579
|
+
targetQueue: input.targetQueue
|
|
580
|
+
},
|
|
581
|
+
...input.enabled !== void 0 && { enabled: input.enabled }
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/core/fanout.ts
|
|
586
|
+
var FanoutEngine = class {
|
|
587
|
+
transport;
|
|
588
|
+
schema;
|
|
589
|
+
hooks;
|
|
590
|
+
namespace;
|
|
591
|
+
defaultQueue;
|
|
592
|
+
enqueuingCount = 0;
|
|
593
|
+
constructor(config) {
|
|
594
|
+
this.transport = config.transport;
|
|
595
|
+
this.schema = config.schema;
|
|
596
|
+
this.hooks = config.hooks;
|
|
597
|
+
this.namespace = config.namespace;
|
|
598
|
+
this.defaultQueue = config.defaultQueue;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Current count of events being enqueued.
|
|
602
|
+
*/
|
|
603
|
+
get eventsBeingEnqueuedCount() {
|
|
604
|
+
return this.enqueuingCount;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Sends an event to all registered subscribers.
|
|
608
|
+
*/
|
|
609
|
+
async send(eventClass, event, options = {}) {
|
|
610
|
+
const eventKey = eventClass.key;
|
|
611
|
+
const subscribers = this.schema.getSubscribers(eventKey);
|
|
612
|
+
const errors = [];
|
|
613
|
+
let sent = 0;
|
|
614
|
+
let skipped = 0;
|
|
615
|
+
const universalMetadata = await this.hooks.loadUniversalMetadata();
|
|
616
|
+
const mergedMetadata = event.metadata || options.metadata ? { ...event.metadata, ...options.metadata } : void 0;
|
|
617
|
+
for (const subscriber of subscribers) {
|
|
618
|
+
const enabled = await this.isSubscriberEnabled(subscriber);
|
|
619
|
+
if (!enabled) {
|
|
620
|
+
skipped++;
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
const targetQueue = subscriber.targetQueue ?? this.defaultQueue;
|
|
624
|
+
const qualifiedQueue = getQualifiedQueueName(this.namespace, targetQueue);
|
|
625
|
+
const envelope = createEnvelope({
|
|
626
|
+
eventKey,
|
|
627
|
+
eventDescription: eventClass.description,
|
|
628
|
+
targetSubscriber: subscriber.name,
|
|
629
|
+
data: event.data,
|
|
630
|
+
importance: subscriber.importance ?? "should-investigate",
|
|
631
|
+
correlationId: options.correlationId,
|
|
632
|
+
metadata: mergedMetadata,
|
|
633
|
+
universalMetadata,
|
|
634
|
+
delayMs: options.delayMs
|
|
635
|
+
});
|
|
636
|
+
this.enqueuingCount++;
|
|
637
|
+
try {
|
|
638
|
+
const usedTransport = await this.transport.send(
|
|
639
|
+
qualifiedQueue,
|
|
640
|
+
envelope,
|
|
641
|
+
options.delayMs !== void 0 ? { delay: options.delayMs } : void 0
|
|
642
|
+
);
|
|
643
|
+
sent++;
|
|
644
|
+
await this.hooks.onEnqueueSuccess({
|
|
645
|
+
envelope,
|
|
646
|
+
queue: qualifiedQueue,
|
|
647
|
+
transport: usedTransport
|
|
648
|
+
});
|
|
649
|
+
} catch (error) {
|
|
650
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
651
|
+
const err = new TransportSendError(qualifiedQueue, cause);
|
|
652
|
+
errors.push({
|
|
653
|
+
subscriberName: subscriber.name,
|
|
654
|
+
queue: qualifiedQueue,
|
|
655
|
+
error: err
|
|
656
|
+
});
|
|
657
|
+
await this.hooks.onEnqueueError({
|
|
658
|
+
envelope,
|
|
659
|
+
error: err,
|
|
660
|
+
transport: this.transport.name
|
|
661
|
+
});
|
|
662
|
+
} finally {
|
|
663
|
+
this.enqueuingCount--;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
eventKey,
|
|
668
|
+
subscribersSent: sent,
|
|
669
|
+
subscribersSkipped: skipped,
|
|
670
|
+
errors
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
async isSubscriberEnabled(subscriber) {
|
|
674
|
+
if (!subscriber.enabled) {
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
try {
|
|
678
|
+
const result = await subscriber.enabled();
|
|
679
|
+
return result;
|
|
680
|
+
} catch {
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
// src/hooks/types.ts
|
|
687
|
+
var consoleLogger = {
|
|
688
|
+
debug: (message, ...args) => console.debug(message, ...args),
|
|
689
|
+
info: (message, ...args) => console.info(message, ...args),
|
|
690
|
+
warn: (message, ...args) => console.warn(message, ...args),
|
|
691
|
+
error: (message, ...args) => console.error(message, ...args)
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
// src/hooks/safe-hooks.ts
|
|
695
|
+
var SafeHooks = class {
|
|
696
|
+
hooks;
|
|
697
|
+
/** The logger instance used by Matador. */
|
|
698
|
+
logger;
|
|
699
|
+
constructor(hooks = {}) {
|
|
700
|
+
this.hooks = hooks;
|
|
701
|
+
this.logger = hooks.logger ?? consoleLogger;
|
|
702
|
+
}
|
|
703
|
+
async onEnqueueSuccess(context) {
|
|
704
|
+
await this.safeCall(
|
|
705
|
+
"onEnqueueSuccess",
|
|
706
|
+
() => this.hooks.onEnqueueSuccess?.(context)
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
async onEnqueueWarning(context) {
|
|
710
|
+
await this.safeCall(
|
|
711
|
+
"onEnqueueWarning",
|
|
712
|
+
() => this.hooks.onEnqueueWarning?.(context)
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
async onEnqueueError(context) {
|
|
716
|
+
await this.safeCall(
|
|
717
|
+
"onEnqueueError",
|
|
718
|
+
() => this.hooks.onEnqueueError?.(context)
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
async onWorkerWrap(envelope, subscriber, execute) {
|
|
722
|
+
if (this.hooks.onWorkerWrap) {
|
|
723
|
+
try {
|
|
724
|
+
await this.hooks.onWorkerWrap(envelope, subscriber, execute);
|
|
725
|
+
} catch (error) {
|
|
726
|
+
this.logger.warn(
|
|
727
|
+
"[Matador] \u{1F7E1} Hook onWorkerWrap threw an error",
|
|
728
|
+
error
|
|
729
|
+
);
|
|
730
|
+
await execute();
|
|
731
|
+
}
|
|
732
|
+
} else {
|
|
733
|
+
await execute();
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
async onWorkerBeforeProcess(envelope, subscriber) {
|
|
737
|
+
await this.safeCall(
|
|
738
|
+
"onWorkerBeforeProcess",
|
|
739
|
+
() => this.hooks.onWorkerBeforeProcess?.(envelope, subscriber)
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
async onWorkerSuccess(context) {
|
|
743
|
+
await this.safeCall(
|
|
744
|
+
"onWorkerSuccess",
|
|
745
|
+
() => this.hooks.onWorkerSuccess?.(context)
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
async onWorkerError(context) {
|
|
749
|
+
await this.safeCall(
|
|
750
|
+
"onWorkerError",
|
|
751
|
+
() => this.hooks.onWorkerError?.(context)
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
async onDecodeError(context) {
|
|
755
|
+
await this.safeCall(
|
|
756
|
+
"onDecodeError",
|
|
757
|
+
() => this.hooks.onDecodeError?.(context)
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
async onConnectionStateChange(state) {
|
|
761
|
+
await this.safeCall(
|
|
762
|
+
"onConnectionStateChange",
|
|
763
|
+
() => this.hooks.onConnectionStateChange?.(state)
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
async loadUniversalMetadata() {
|
|
767
|
+
if (!this.hooks.loadUniversalMetadata) {
|
|
768
|
+
return {};
|
|
769
|
+
}
|
|
770
|
+
try {
|
|
771
|
+
const result = await this.hooks.loadUniversalMetadata();
|
|
772
|
+
return result ?? {};
|
|
773
|
+
} catch (error) {
|
|
774
|
+
this.logger.warn(
|
|
775
|
+
"[Matador] \u{1F7E1} Hook loadUniversalMetadata threw an error",
|
|
776
|
+
error
|
|
777
|
+
);
|
|
778
|
+
return {};
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
async getQueueConcurrency(queueName) {
|
|
782
|
+
if (!this.hooks.getQueueConcurrency) {
|
|
783
|
+
return void 0;
|
|
784
|
+
}
|
|
785
|
+
try {
|
|
786
|
+
return await this.hooks.getQueueConcurrency(queueName);
|
|
787
|
+
} catch (error) {
|
|
788
|
+
this.logger.warn(
|
|
789
|
+
"[Matador] \u{1F7E1} Hook getQueueConcurrency threw an error",
|
|
790
|
+
error
|
|
791
|
+
);
|
|
792
|
+
return void 0;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
async getRetryDelay(envelope, attemptNumber) {
|
|
796
|
+
if (!this.hooks.getRetryDelay) {
|
|
797
|
+
return void 0;
|
|
798
|
+
}
|
|
799
|
+
try {
|
|
800
|
+
return await this.hooks.getRetryDelay(envelope, attemptNumber);
|
|
801
|
+
} catch (error) {
|
|
802
|
+
this.logger.warn("[Matador] \u{1F7E1} Hook getRetryDelay threw an error", error);
|
|
803
|
+
return void 0;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
async getAttempts(envelope) {
|
|
807
|
+
if (!this.hooks.getAttempts) {
|
|
808
|
+
return void 0;
|
|
809
|
+
}
|
|
810
|
+
try {
|
|
811
|
+
return await this.hooks.getAttempts(envelope);
|
|
812
|
+
} catch (error) {
|
|
813
|
+
this.logger.warn("[Matador] \u{1F7E1} Hook getAttempts threw an error", error);
|
|
814
|
+
return void 0;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
async getMaxDeliveries(envelope) {
|
|
818
|
+
if (!this.hooks.getMaxDeliveries) {
|
|
819
|
+
return void 0;
|
|
820
|
+
}
|
|
821
|
+
try {
|
|
822
|
+
return await this.hooks.getMaxDeliveries(envelope);
|
|
823
|
+
} catch (error) {
|
|
824
|
+
this.logger.warn(
|
|
825
|
+
"[Matador] \u{1F7E1} Hook getMaxDeliveries threw an error",
|
|
826
|
+
error
|
|
827
|
+
);
|
|
828
|
+
return void 0;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
// === Checkpoint Hooks ===
|
|
832
|
+
async onCheckpointLoaded(context) {
|
|
833
|
+
await this.safeCall(
|
|
834
|
+
"onCheckpointLoaded",
|
|
835
|
+
() => this.hooks.onCheckpointLoaded?.(context)
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
async onCheckpointHit(context) {
|
|
839
|
+
await this.safeCall(
|
|
840
|
+
"onCheckpointHit",
|
|
841
|
+
() => this.hooks.onCheckpointHit?.(context)
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
async onCheckpointMiss(context) {
|
|
845
|
+
await this.safeCall(
|
|
846
|
+
"onCheckpointMiss",
|
|
847
|
+
() => this.hooks.onCheckpointMiss?.(context)
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
async onCheckpointCleared(context) {
|
|
851
|
+
await this.safeCall(
|
|
852
|
+
"onCheckpointCleared",
|
|
853
|
+
() => this.hooks.onCheckpointCleared?.(context)
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
async safeCall(hookName, fn) {
|
|
857
|
+
try {
|
|
858
|
+
await fn();
|
|
859
|
+
} catch (error) {
|
|
860
|
+
this.logger.warn(`[Matador] \u{1F7E1} Hook ${hookName} threw an error`, error);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
// src/core/shutdown.ts
|
|
866
|
+
var defaultShutdownConfig = {
|
|
867
|
+
gracefulShutdownTimeout: 3e4,
|
|
868
|
+
idlePollingInterval: 1e3,
|
|
869
|
+
logger: consoleLogger
|
|
870
|
+
};
|
|
871
|
+
var ShutdownManager = class {
|
|
872
|
+
constructor(getEnqueueCount, stopReceiving, disconnectTransport, config = {}) {
|
|
873
|
+
this.getEnqueueCount = getEnqueueCount;
|
|
874
|
+
this.stopReceiving = stopReceiving;
|
|
875
|
+
this.disconnectTransport = disconnectTransport;
|
|
876
|
+
this.config = { ...defaultShutdownConfig, ...config };
|
|
877
|
+
}
|
|
878
|
+
_state = "running";
|
|
879
|
+
config;
|
|
880
|
+
eventsBeingProcessed = 0;
|
|
881
|
+
acceptingEnqueue = true;
|
|
882
|
+
/**
|
|
883
|
+
* Current shutdown state.
|
|
884
|
+
*/
|
|
885
|
+
get state() {
|
|
886
|
+
return this._state;
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Whether enqueue is currently allowed.
|
|
890
|
+
*/
|
|
891
|
+
get isEnqueueAllowed() {
|
|
892
|
+
return this.acceptingEnqueue;
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Gets current handler state.
|
|
896
|
+
*/
|
|
897
|
+
getHandlersState() {
|
|
898
|
+
const eventsBeingEnqueued = this.getEnqueueCount();
|
|
899
|
+
return {
|
|
900
|
+
eventsBeingProcessed: this.eventsBeingProcessed,
|
|
901
|
+
eventsBeingEnqueued,
|
|
902
|
+
isIdle: this.eventsBeingProcessed === 0 && eventsBeingEnqueued === 0
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Increment processing counter.
|
|
907
|
+
*/
|
|
908
|
+
incrementProcessing() {
|
|
909
|
+
this.eventsBeingProcessed++;
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Decrement processing counter.
|
|
913
|
+
*/
|
|
914
|
+
decrementProcessing() {
|
|
915
|
+
this.eventsBeingProcessed--;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Performs graceful shutdown.
|
|
919
|
+
*/
|
|
920
|
+
async shutdown() {
|
|
921
|
+
if (this._state !== "running") {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
this._state = "stopping-receive";
|
|
925
|
+
await this.stopReceiving();
|
|
926
|
+
this._state = "waiting-handlers";
|
|
927
|
+
await this.waitForIdle();
|
|
928
|
+
this._state = "stopping-enqueue";
|
|
929
|
+
this.acceptingEnqueue = false;
|
|
930
|
+
this._state = "disconnecting";
|
|
931
|
+
await this.disconnectTransport();
|
|
932
|
+
this._state = "stopped";
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Forcefully stops without waiting.
|
|
936
|
+
*/
|
|
937
|
+
async forceStop() {
|
|
938
|
+
this.acceptingEnqueue = false;
|
|
939
|
+
await this.stopReceiving();
|
|
940
|
+
await this.disconnectTransport();
|
|
941
|
+
this._state = "stopped";
|
|
942
|
+
}
|
|
943
|
+
async waitForIdle() {
|
|
944
|
+
const deadline = Date.now() + this.config.gracefulShutdownTimeout;
|
|
945
|
+
while (!this.getHandlersState().isIdle) {
|
|
946
|
+
if (Date.now() > deadline) {
|
|
947
|
+
this.config.logger.warn(
|
|
948
|
+
`[Matador] \u26A0\uFE0F Shutdown timeout reached with ${this.eventsBeingProcessed} events still processing`
|
|
949
|
+
);
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
await this.sleep(this.config.idlePollingInterval);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
sleep(ms) {
|
|
956
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
// src/codec/codec.ts
|
|
961
|
+
var CodecDecodeError = class extends Error {
|
|
962
|
+
constructor(message, cause) {
|
|
963
|
+
super(message);
|
|
964
|
+
this.cause = cause;
|
|
965
|
+
this.name = "CodecDecodeError";
|
|
966
|
+
}
|
|
967
|
+
description = "Failed to decode a message from the transport. This typically indicates corrupted data, incompatible codec versions, or messages from a different system. Check the cause property for the underlying parsing error. The message will be sent to the dead-letter queue for investigation.";
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
// src/codec/json-codec.ts
|
|
971
|
+
var JsonCodec = class {
|
|
972
|
+
contentType = "application/json";
|
|
973
|
+
encode(envelope) {
|
|
974
|
+
const json = JSON.stringify(envelope);
|
|
975
|
+
return new TextEncoder().encode(json);
|
|
976
|
+
}
|
|
977
|
+
decode(buffer) {
|
|
978
|
+
if (buffer.length === 0) {
|
|
979
|
+
throw new CodecDecodeError("Cannot decode empty buffer");
|
|
980
|
+
}
|
|
981
|
+
let json;
|
|
982
|
+
try {
|
|
983
|
+
json = new TextDecoder("utf-8").decode(buffer);
|
|
984
|
+
} catch (error) {
|
|
985
|
+
throw new CodecDecodeError("Invalid UTF-8 encoding", error);
|
|
986
|
+
}
|
|
987
|
+
let parsed;
|
|
988
|
+
try {
|
|
989
|
+
parsed = JSON.parse(json);
|
|
990
|
+
} catch (error) {
|
|
991
|
+
throw new CodecDecodeError("Invalid JSON", error);
|
|
992
|
+
}
|
|
993
|
+
if (!this.isValidEnvelope(parsed)) {
|
|
994
|
+
throw new CodecDecodeError("Invalid envelope structure");
|
|
995
|
+
}
|
|
996
|
+
return parsed;
|
|
997
|
+
}
|
|
998
|
+
isValidEnvelope(value) {
|
|
999
|
+
if (typeof value !== "object" || value === null) {
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
const envelope = value;
|
|
1003
|
+
if (typeof envelope["id"] !== "string") return false;
|
|
1004
|
+
if (!("data" in envelope)) return false;
|
|
1005
|
+
if (typeof envelope["docket"] !== "object" || envelope["docket"] === null)
|
|
1006
|
+
return false;
|
|
1007
|
+
const docket = envelope["docket"];
|
|
1008
|
+
if (typeof docket["eventKey"] !== "string") return false;
|
|
1009
|
+
if (typeof docket["targetSubscriber"] !== "string") return false;
|
|
1010
|
+
if (typeof docket["attempts"] !== "number") return false;
|
|
1011
|
+
if (typeof docket["createdAt"] !== "string") return false;
|
|
1012
|
+
if (typeof docket["importance"] !== "string") return false;
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
// src/codec/rabbitmq-codec.ts
|
|
1018
|
+
var HEADERS = {
|
|
1019
|
+
// Routing
|
|
1020
|
+
EVENT_KEY: "x-matador-event-key",
|
|
1021
|
+
TARGET_SUBSCRIBER: "x-matador-subscriber",
|
|
1022
|
+
SCHEDULED_FOR: "x-matador-scheduled-for",
|
|
1023
|
+
ORIGINAL_QUEUE: "x-matador-original-queue",
|
|
1024
|
+
// Processing state
|
|
1025
|
+
ATTEMPTS: "x-matador-attempts",
|
|
1026
|
+
CREATED_AT: "x-matador-created-at",
|
|
1027
|
+
FIRST_ERROR: "x-matador-first-error",
|
|
1028
|
+
LAST_ERROR: "x-matador-last-error",
|
|
1029
|
+
// Observability
|
|
1030
|
+
IMPORTANCE: "x-matador-importance",
|
|
1031
|
+
CORRELATION_ID: "x-matador-correlation-id",
|
|
1032
|
+
// v1 compatibility headers
|
|
1033
|
+
V1_EVENT_ID: "x-event-id",
|
|
1034
|
+
V1_CORRELATION_ID: "x-correlation-id"};
|
|
1035
|
+
var RabbitMQCodec = class {
|
|
1036
|
+
contentType = "application/json";
|
|
1037
|
+
encode(envelope) {
|
|
1038
|
+
const { docket } = envelope;
|
|
1039
|
+
const body = {
|
|
1040
|
+
id: envelope.id,
|
|
1041
|
+
data: envelope.data,
|
|
1042
|
+
...docket.metadata !== void 0 && { metadata: docket.metadata }
|
|
1043
|
+
};
|
|
1044
|
+
const headers = {
|
|
1045
|
+
// Routing
|
|
1046
|
+
[HEADERS.EVENT_KEY]: docket.eventKey,
|
|
1047
|
+
[HEADERS.TARGET_SUBSCRIBER]: docket.targetSubscriber,
|
|
1048
|
+
// Processing state
|
|
1049
|
+
[HEADERS.ATTEMPTS]: docket.attempts,
|
|
1050
|
+
[HEADERS.CREATED_AT]: docket.createdAt,
|
|
1051
|
+
// Observability
|
|
1052
|
+
[HEADERS.IMPORTANCE]: docket.importance
|
|
1053
|
+
};
|
|
1054
|
+
if (docket.scheduledFor !== void 0) {
|
|
1055
|
+
headers[HEADERS.SCHEDULED_FOR] = docket.scheduledFor;
|
|
1056
|
+
}
|
|
1057
|
+
if (docket.originalQueue !== void 0) {
|
|
1058
|
+
headers[HEADERS.ORIGINAL_QUEUE] = docket.originalQueue;
|
|
1059
|
+
}
|
|
1060
|
+
if (docket.firstError !== void 0) {
|
|
1061
|
+
headers[HEADERS.FIRST_ERROR] = docket.firstError;
|
|
1062
|
+
}
|
|
1063
|
+
if (docket.lastError !== void 0) {
|
|
1064
|
+
headers[HEADERS.LAST_ERROR] = docket.lastError;
|
|
1065
|
+
}
|
|
1066
|
+
if (docket.correlationId !== void 0) {
|
|
1067
|
+
headers[HEADERS.CORRELATION_ID] = docket.correlationId;
|
|
1068
|
+
}
|
|
1069
|
+
return {
|
|
1070
|
+
body: new TextEncoder().encode(JSON.stringify(body)),
|
|
1071
|
+
headers,
|
|
1072
|
+
contentType: this.contentType
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
decode(body, headers) {
|
|
1076
|
+
if (body.length === 0) {
|
|
1077
|
+
throw new CodecDecodeError("Cannot decode empty buffer");
|
|
1078
|
+
}
|
|
1079
|
+
let json;
|
|
1080
|
+
try {
|
|
1081
|
+
json = new TextDecoder("utf-8").decode(body);
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
throw new CodecDecodeError("Invalid UTF-8 encoding", error);
|
|
1084
|
+
}
|
|
1085
|
+
let parsed;
|
|
1086
|
+
try {
|
|
1087
|
+
parsed = JSON.parse(json);
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
throw new CodecDecodeError("Invalid JSON", error);
|
|
1090
|
+
}
|
|
1091
|
+
if (this.isV1Body(parsed)) {
|
|
1092
|
+
return this.decodeV1(parsed, headers);
|
|
1093
|
+
}
|
|
1094
|
+
if (!this.isV2Body(parsed)) {
|
|
1095
|
+
throw new CodecDecodeError("Invalid message body structure");
|
|
1096
|
+
}
|
|
1097
|
+
return this.decodeV2(parsed, headers);
|
|
1098
|
+
}
|
|
1099
|
+
isV1Body(value) {
|
|
1100
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1101
|
+
const obj = value;
|
|
1102
|
+
return typeof obj["key"] === "string" && typeof obj["targetSubscriber"] === "string" && "data" in obj && !("payload" in obj);
|
|
1103
|
+
}
|
|
1104
|
+
isV2Body(value) {
|
|
1105
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1106
|
+
const obj = value;
|
|
1107
|
+
return typeof obj["id"] === "string" && "data" in obj && !("key" in obj);
|
|
1108
|
+
}
|
|
1109
|
+
decodeV2(body, headers) {
|
|
1110
|
+
const eventKey = this.requireStringHeader(headers, HEADERS.EVENT_KEY);
|
|
1111
|
+
const targetSubscriber = this.requireStringHeader(
|
|
1112
|
+
headers,
|
|
1113
|
+
HEADERS.TARGET_SUBSCRIBER
|
|
1114
|
+
);
|
|
1115
|
+
const attempts = this.requireNumberHeader(headers, HEADERS.ATTEMPTS);
|
|
1116
|
+
const createdAt = this.requireStringHeader(headers, HEADERS.CREATED_AT);
|
|
1117
|
+
const importance = this.requireStringHeader(headers, HEADERS.IMPORTANCE);
|
|
1118
|
+
const scheduledFor = this.optionalStringHeader(
|
|
1119
|
+
headers,
|
|
1120
|
+
HEADERS.SCHEDULED_FOR
|
|
1121
|
+
);
|
|
1122
|
+
const originalQueue = this.optionalStringHeader(
|
|
1123
|
+
headers,
|
|
1124
|
+
HEADERS.ORIGINAL_QUEUE
|
|
1125
|
+
);
|
|
1126
|
+
const firstError = this.optionalStringHeader(headers, HEADERS.FIRST_ERROR);
|
|
1127
|
+
const lastError = this.optionalStringHeader(headers, HEADERS.LAST_ERROR);
|
|
1128
|
+
const correlationId = this.optionalStringHeader(
|
|
1129
|
+
headers,
|
|
1130
|
+
HEADERS.CORRELATION_ID
|
|
1131
|
+
);
|
|
1132
|
+
const docket = {
|
|
1133
|
+
// Routing
|
|
1134
|
+
eventKey,
|
|
1135
|
+
targetSubscriber,
|
|
1136
|
+
...scheduledFor !== void 0 && { scheduledFor },
|
|
1137
|
+
...originalQueue !== void 0 && { originalQueue },
|
|
1138
|
+
// Processing state
|
|
1139
|
+
attempts,
|
|
1140
|
+
createdAt,
|
|
1141
|
+
...firstError !== void 0 && { firstError },
|
|
1142
|
+
...lastError !== void 0 && { lastError },
|
|
1143
|
+
// Observability
|
|
1144
|
+
importance,
|
|
1145
|
+
...correlationId !== void 0 && { correlationId },
|
|
1146
|
+
...body.metadata !== void 0 && { metadata: body.metadata }
|
|
1147
|
+
};
|
|
1148
|
+
return {
|
|
1149
|
+
id: body.id,
|
|
1150
|
+
data: body.data,
|
|
1151
|
+
docket
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Decodes a v1 format message for backwards compatibility.
|
|
1156
|
+
* v1 format: { key, data, metadata, universal, targetSubscriber, ... }
|
|
1157
|
+
*/
|
|
1158
|
+
decodeV1(body, headers) {
|
|
1159
|
+
const { event_id, user_id, correlation_id, ...otherUniversal } = body.universal ?? {};
|
|
1160
|
+
const mergedMetadata = {};
|
|
1161
|
+
if (body.metadata && typeof body.metadata === "object") {
|
|
1162
|
+
Object.assign(mergedMetadata, body.metadata);
|
|
1163
|
+
}
|
|
1164
|
+
if (user_id !== void 0 && user_id !== null) {
|
|
1165
|
+
mergedMetadata["user_id"] = user_id;
|
|
1166
|
+
}
|
|
1167
|
+
Object.assign(mergedMetadata, otherUniversal);
|
|
1168
|
+
const attempts = typeof headers[HEADERS.ATTEMPTS] === "number" ? headers[HEADERS.ATTEMPTS] : 1;
|
|
1169
|
+
const importance = typeof headers[HEADERS.IMPORTANCE] === "string" ? headers[HEADERS.IMPORTANCE] : "should-investigate";
|
|
1170
|
+
const correlationId = headers[HEADERS.CORRELATION_ID] ?? headers[HEADERS.V1_CORRELATION_ID] ?? correlation_id ?? void 0;
|
|
1171
|
+
const eventId = headers[HEADERS.V1_EVENT_ID] ?? event_id ?? crypto.randomUUID();
|
|
1172
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1173
|
+
let scheduledFor;
|
|
1174
|
+
if (body.options?.delayMs) {
|
|
1175
|
+
scheduledFor = new Date(Date.now() + body.options.delayMs).toISOString();
|
|
1176
|
+
}
|
|
1177
|
+
const docket = {
|
|
1178
|
+
// Routing
|
|
1179
|
+
eventKey: body.key,
|
|
1180
|
+
targetSubscriber: body.targetSubscriber,
|
|
1181
|
+
...scheduledFor !== void 0 && { scheduledFor },
|
|
1182
|
+
// Processing state
|
|
1183
|
+
attempts,
|
|
1184
|
+
createdAt: now,
|
|
1185
|
+
// Observability
|
|
1186
|
+
importance,
|
|
1187
|
+
...correlationId !== void 0 && { correlationId },
|
|
1188
|
+
...Object.keys(mergedMetadata).length > 0 && {
|
|
1189
|
+
metadata: mergedMetadata
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
return {
|
|
1193
|
+
id: eventId,
|
|
1194
|
+
data: body.data,
|
|
1195
|
+
docket
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
requireStringHeader(headers, name) {
|
|
1199
|
+
const value = headers[name];
|
|
1200
|
+
if (value === void 0) {
|
|
1201
|
+
throw new CodecDecodeError(`Missing required header: ${name}`);
|
|
1202
|
+
}
|
|
1203
|
+
if (typeof value !== "string") {
|
|
1204
|
+
throw new CodecDecodeError(
|
|
1205
|
+
`Header ${name} must be string, got ${typeof value}`
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
return value;
|
|
1209
|
+
}
|
|
1210
|
+
requireNumberHeader(headers, name) {
|
|
1211
|
+
const value = headers[name];
|
|
1212
|
+
if (value === void 0) {
|
|
1213
|
+
throw new CodecDecodeError(`Missing required header: ${name}`);
|
|
1214
|
+
}
|
|
1215
|
+
if (typeof value !== "number") {
|
|
1216
|
+
throw new CodecDecodeError(
|
|
1217
|
+
`Header ${name} must be number, got ${typeof value}`
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
return value;
|
|
1221
|
+
}
|
|
1222
|
+
optionalStringHeader(headers, name) {
|
|
1223
|
+
const value = headers[name];
|
|
1224
|
+
if (value === void 0) return void 0;
|
|
1225
|
+
if (typeof value !== "string") return void 0;
|
|
1226
|
+
return value;
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
|
|
1230
|
+
// src/checkpoint/context.ts
|
|
1231
|
+
var ResumableContext = class {
|
|
1232
|
+
checkpoint;
|
|
1233
|
+
usedKeys = /* @__PURE__ */ new Set();
|
|
1234
|
+
store;
|
|
1235
|
+
envelope;
|
|
1236
|
+
subscriber;
|
|
1237
|
+
hooks;
|
|
1238
|
+
constructor(config) {
|
|
1239
|
+
this.store = config.store;
|
|
1240
|
+
this.envelope = config.envelope;
|
|
1241
|
+
this.subscriber = config.subscriber;
|
|
1242
|
+
this.hooks = config.hooks;
|
|
1243
|
+
this.checkpoint = config.existingCheckpoint ?? {
|
|
1244
|
+
envelopeId: config.envelope.id,
|
|
1245
|
+
subscriberName: config.subscriber.name,
|
|
1246
|
+
completedSteps: {}
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
get attempt() {
|
|
1250
|
+
return this.envelope.docket.attempts;
|
|
1251
|
+
}
|
|
1252
|
+
get isRetry() {
|
|
1253
|
+
return this.attempt > 1;
|
|
1254
|
+
}
|
|
1255
|
+
async io(key, fn) {
|
|
1256
|
+
if (this.usedKeys.has(key)) {
|
|
1257
|
+
throw new DuplicateIoKeyError(key, this.subscriber.name);
|
|
1258
|
+
}
|
|
1259
|
+
this.usedKeys.add(key);
|
|
1260
|
+
if (key in this.checkpoint.completedSteps) {
|
|
1261
|
+
await this.hooks?.onCheckpointHit?.({
|
|
1262
|
+
envelope: this.envelope,
|
|
1263
|
+
subscriber: this.subscriber,
|
|
1264
|
+
stepKey: key
|
|
1265
|
+
});
|
|
1266
|
+
return this.checkpoint.completedSteps[key];
|
|
1267
|
+
}
|
|
1268
|
+
await this.hooks?.onCheckpointMiss?.({
|
|
1269
|
+
envelope: this.envelope,
|
|
1270
|
+
subscriber: this.subscriber,
|
|
1271
|
+
stepKey: key
|
|
1272
|
+
});
|
|
1273
|
+
const result = await fn();
|
|
1274
|
+
this.checkpoint = {
|
|
1275
|
+
...this.checkpoint,
|
|
1276
|
+
completedSteps: {
|
|
1277
|
+
...this.checkpoint.completedSteps,
|
|
1278
|
+
[key]: result
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
await this.store.set(this.envelope.id, this.checkpoint);
|
|
1282
|
+
return result;
|
|
1283
|
+
}
|
|
1284
|
+
async all(ops) {
|
|
1285
|
+
const keysInThisCall = /* @__PURE__ */ new Set();
|
|
1286
|
+
for (const [key] of ops) {
|
|
1287
|
+
if (keysInThisCall.has(key)) {
|
|
1288
|
+
throw new DuplicateIoKeyError(key, this.subscriber.name);
|
|
1289
|
+
}
|
|
1290
|
+
keysInThisCall.add(key);
|
|
1291
|
+
}
|
|
1292
|
+
for (const [key] of ops) {
|
|
1293
|
+
if (this.usedKeys.has(key)) {
|
|
1294
|
+
throw new DuplicateIoKeyError(key, this.subscriber.name);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
for (const [key] of ops) {
|
|
1298
|
+
this.usedKeys.add(key);
|
|
1299
|
+
}
|
|
1300
|
+
const results = await Promise.all(
|
|
1301
|
+
ops.map(async ([key, fn]) => {
|
|
1302
|
+
if (key in this.checkpoint.completedSteps) {
|
|
1303
|
+
await this.hooks?.onCheckpointHit?.({
|
|
1304
|
+
envelope: this.envelope,
|
|
1305
|
+
subscriber: this.subscriber,
|
|
1306
|
+
stepKey: key
|
|
1307
|
+
});
|
|
1308
|
+
return this.checkpoint.completedSteps[key];
|
|
1309
|
+
}
|
|
1310
|
+
await this.hooks?.onCheckpointMiss?.({
|
|
1311
|
+
envelope: this.envelope,
|
|
1312
|
+
subscriber: this.subscriber,
|
|
1313
|
+
stepKey: key
|
|
1314
|
+
});
|
|
1315
|
+
const result = await fn();
|
|
1316
|
+
this.checkpoint = {
|
|
1317
|
+
...this.checkpoint,
|
|
1318
|
+
completedSteps: {
|
|
1319
|
+
...this.checkpoint.completedSteps,
|
|
1320
|
+
[key]: result
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
return result;
|
|
1324
|
+
})
|
|
1325
|
+
);
|
|
1326
|
+
await this.store.set(this.envelope.id, this.checkpoint);
|
|
1327
|
+
return results;
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Clears the checkpoint from storage.
|
|
1331
|
+
* Called after successful completion or dead-letter.
|
|
1332
|
+
*/
|
|
1333
|
+
async clear() {
|
|
1334
|
+
await this.store.delete(this.envelope.id);
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Gets the current checkpoint state.
|
|
1338
|
+
* Useful for debugging and testing.
|
|
1339
|
+
*/
|
|
1340
|
+
getCheckpoint() {
|
|
1341
|
+
return this.checkpoint;
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Gets the number of cached steps.
|
|
1345
|
+
*/
|
|
1346
|
+
get cachedStepCount() {
|
|
1347
|
+
return Object.keys(this.checkpoint.completedSteps).length;
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
|
|
1351
|
+
// src/checkpoint/stores/memory.ts
|
|
1352
|
+
var MemoryCheckpointStore = class {
|
|
1353
|
+
checkpoints = /* @__PURE__ */ new Map();
|
|
1354
|
+
async get(envelopeId) {
|
|
1355
|
+
return this.checkpoints.get(envelopeId);
|
|
1356
|
+
}
|
|
1357
|
+
async set(envelopeId, checkpoint) {
|
|
1358
|
+
this.checkpoints.set(envelopeId, checkpoint);
|
|
1359
|
+
}
|
|
1360
|
+
async delete(envelopeId) {
|
|
1361
|
+
this.checkpoints.delete(envelopeId);
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Gets the number of stored checkpoints.
|
|
1365
|
+
* Useful for testing.
|
|
1366
|
+
*/
|
|
1367
|
+
get size() {
|
|
1368
|
+
return this.checkpoints.size;
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Clears all stored checkpoints.
|
|
1372
|
+
* Useful for testing.
|
|
1373
|
+
*/
|
|
1374
|
+
clear() {
|
|
1375
|
+
this.checkpoints.clear();
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Gets all stored checkpoint IDs.
|
|
1379
|
+
* Useful for testing.
|
|
1380
|
+
*/
|
|
1381
|
+
keys() {
|
|
1382
|
+
return [...this.checkpoints.keys()];
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
// src/checkpoint/stores/noop.ts
|
|
1387
|
+
var NoOpCheckpointStore = class {
|
|
1388
|
+
async get(_envelopeId) {
|
|
1389
|
+
return void 0;
|
|
1390
|
+
}
|
|
1391
|
+
async set(_envelopeId, _checkpoint) {
|
|
1392
|
+
}
|
|
1393
|
+
async delete(_envelopeId) {
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
|
|
1397
|
+
// src/pipeline/pipeline.ts
|
|
1398
|
+
var ProcessingPipeline = class {
|
|
1399
|
+
transport;
|
|
1400
|
+
schema;
|
|
1401
|
+
codec;
|
|
1402
|
+
retryPolicy;
|
|
1403
|
+
hooks;
|
|
1404
|
+
checkpointStore;
|
|
1405
|
+
constructor(config) {
|
|
1406
|
+
this.transport = config.transport;
|
|
1407
|
+
this.schema = config.schema;
|
|
1408
|
+
this.codec = config.codec;
|
|
1409
|
+
this.retryPolicy = config.retryPolicy;
|
|
1410
|
+
this.hooks = config.hooks;
|
|
1411
|
+
this.checkpointStore = config.checkpointStore ?? new NoOpCheckpointStore();
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Processes a raw message from the transport.
|
|
1415
|
+
*/
|
|
1416
|
+
async process(rawMessage, receipt) {
|
|
1417
|
+
const startTime = performance.now();
|
|
1418
|
+
let envelope;
|
|
1419
|
+
try {
|
|
1420
|
+
envelope = this.codec.decode(rawMessage);
|
|
1421
|
+
} catch (error2) {
|
|
1422
|
+
const decodeError = error2 instanceof CodecDecodeError ? error2 : new CodecDecodeError("Unknown decode error", error2);
|
|
1423
|
+
await this.transport.complete(receipt);
|
|
1424
|
+
await this.hooks.onDecodeError({
|
|
1425
|
+
error: decodeError,
|
|
1426
|
+
rawMessage,
|
|
1427
|
+
sourceQueue: receipt.sourceQueue,
|
|
1428
|
+
transport: receipt.sourceTransport
|
|
1429
|
+
});
|
|
1430
|
+
return {
|
|
1431
|
+
success: false,
|
|
1432
|
+
error: decodeError,
|
|
1433
|
+
durationMs: performance.now() - startTime
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
const subscriberDef = this.schema.getSubscriberDefinition(
|
|
1437
|
+
envelope.docket.eventKey,
|
|
1438
|
+
envelope.docket.targetSubscriber
|
|
1439
|
+
);
|
|
1440
|
+
if (!subscriberDef) {
|
|
1441
|
+
const error2 = new SubscriberNotRegisteredError(
|
|
1442
|
+
envelope.docket.targetSubscriber,
|
|
1443
|
+
envelope.docket.eventKey
|
|
1444
|
+
);
|
|
1445
|
+
const decision2 = this.getUnhandledRetryDecision(envelope, receipt, error2);
|
|
1446
|
+
await this.handleRetryDecision(receipt, envelope, decision2);
|
|
1447
|
+
return {
|
|
1448
|
+
success: false,
|
|
1449
|
+
envelope,
|
|
1450
|
+
error: error2,
|
|
1451
|
+
decision: decision2,
|
|
1452
|
+
durationMs: performance.now() - startTime
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
const subscriber = this.schema.getExecutableSubscriber(
|
|
1456
|
+
envelope.docket.eventKey,
|
|
1457
|
+
envelope.docket.targetSubscriber
|
|
1458
|
+
);
|
|
1459
|
+
if (!subscriber) {
|
|
1460
|
+
const error2 = new SubscriberIsStubError(envelope.docket.targetSubscriber);
|
|
1461
|
+
const decision2 = this.getUnhandledRetryDecision(envelope, receipt, error2);
|
|
1462
|
+
await this.handleRetryDecision(receipt, envelope, decision2);
|
|
1463
|
+
return {
|
|
1464
|
+
success: false,
|
|
1465
|
+
envelope,
|
|
1466
|
+
subscriber: subscriberDef,
|
|
1467
|
+
error: error2,
|
|
1468
|
+
decision: decision2,
|
|
1469
|
+
durationMs: performance.now() - startTime
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
let result;
|
|
1473
|
+
let error;
|
|
1474
|
+
let context;
|
|
1475
|
+
const isResumable = isResumableSubscriber(subscriber);
|
|
1476
|
+
let existingCheckpoint;
|
|
1477
|
+
if (isResumable) {
|
|
1478
|
+
existingCheckpoint = await this.checkpointStore.get(envelope.id);
|
|
1479
|
+
if (existingCheckpoint) {
|
|
1480
|
+
await this.hooks.onCheckpointLoaded?.({
|
|
1481
|
+
envelope,
|
|
1482
|
+
subscriber: subscriberDef,
|
|
1483
|
+
checkpoint: existingCheckpoint,
|
|
1484
|
+
cachedSteps: Object.keys(existingCheckpoint.completedSteps).length
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
context = new ResumableContext({
|
|
1488
|
+
store: this.checkpointStore,
|
|
1489
|
+
envelope,
|
|
1490
|
+
subscriber: subscriberDef,
|
|
1491
|
+
existingCheckpoint,
|
|
1492
|
+
hooks: {
|
|
1493
|
+
onCheckpointHit: (ctx) => this.hooks.onCheckpointHit?.(ctx),
|
|
1494
|
+
onCheckpointMiss: (ctx) => this.hooks.onCheckpointMiss?.(ctx)
|
|
1495
|
+
}
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
await this.hooks.onWorkerWrap(envelope, subscriberDef, async () => {
|
|
1499
|
+
await this.hooks.onWorkerBeforeProcess(envelope, subscriberDef);
|
|
1500
|
+
try {
|
|
1501
|
+
if (isResumable && context) {
|
|
1502
|
+
const resumableCallback = subscriber.callback;
|
|
1503
|
+
result = await resumableCallback(envelope, context);
|
|
1504
|
+
} else {
|
|
1505
|
+
const standardCallback = subscriber.callback;
|
|
1506
|
+
result = await standardCallback(envelope);
|
|
1507
|
+
}
|
|
1508
|
+
} catch (e) {
|
|
1509
|
+
error = e instanceof Error ? e : new Error(String(e));
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
const durationMs = performance.now() - startTime;
|
|
1513
|
+
if (!error) {
|
|
1514
|
+
if (context) {
|
|
1515
|
+
await context.clear();
|
|
1516
|
+
await this.hooks.onCheckpointCleared?.({
|
|
1517
|
+
envelope,
|
|
1518
|
+
subscriber: subscriberDef,
|
|
1519
|
+
reason: "success"
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
await this.transport.complete(receipt);
|
|
1523
|
+
await this.hooks.onWorkerSuccess({
|
|
1524
|
+
envelope,
|
|
1525
|
+
subscriber: subscriberDef,
|
|
1526
|
+
result,
|
|
1527
|
+
durationMs,
|
|
1528
|
+
transport: receipt.sourceTransport
|
|
1529
|
+
});
|
|
1530
|
+
return {
|
|
1531
|
+
success: true,
|
|
1532
|
+
envelope,
|
|
1533
|
+
subscriber: subscriberDef,
|
|
1534
|
+
durationMs
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
const decision = this.retryPolicy.shouldRetry({
|
|
1538
|
+
envelope,
|
|
1539
|
+
error,
|
|
1540
|
+
subscriber: subscriberDef,
|
|
1541
|
+
receipt
|
|
1542
|
+
});
|
|
1543
|
+
envelope.docket.lastError = error.message;
|
|
1544
|
+
envelope.docket.firstError ??= error.message;
|
|
1545
|
+
if (decision.action === "dead-letter" && context) {
|
|
1546
|
+
await context.clear();
|
|
1547
|
+
await this.hooks.onCheckpointCleared?.({
|
|
1548
|
+
envelope,
|
|
1549
|
+
subscriber: subscriberDef,
|
|
1550
|
+
reason: "dead-letter"
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
await this.handleRetryDecision(receipt, envelope, decision);
|
|
1554
|
+
await this.hooks.onWorkerError({
|
|
1555
|
+
envelope,
|
|
1556
|
+
subscriber: subscriberDef,
|
|
1557
|
+
error,
|
|
1558
|
+
durationMs,
|
|
1559
|
+
decision,
|
|
1560
|
+
transport: receipt.sourceTransport
|
|
1561
|
+
});
|
|
1562
|
+
return {
|
|
1563
|
+
success: false,
|
|
1564
|
+
envelope,
|
|
1565
|
+
subscriber: subscriberDef,
|
|
1566
|
+
error,
|
|
1567
|
+
decision,
|
|
1568
|
+
durationMs
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
async handleRetryDecision(receipt, envelope, decision) {
|
|
1572
|
+
switch (decision.action) {
|
|
1573
|
+
case "retry": {
|
|
1574
|
+
envelope.docket.attempts++;
|
|
1575
|
+
envelope.docket.scheduledFor = new Date(
|
|
1576
|
+
Date.now() + decision.delay
|
|
1577
|
+
).toISOString();
|
|
1578
|
+
await this.transport.send(receipt.sourceQueue, envelope, {
|
|
1579
|
+
delay: decision.delay
|
|
1580
|
+
});
|
|
1581
|
+
await this.transport.complete(receipt);
|
|
1582
|
+
break;
|
|
1583
|
+
}
|
|
1584
|
+
case "dead-letter": {
|
|
1585
|
+
await this.sendToDeadLetter(
|
|
1586
|
+
receipt,
|
|
1587
|
+
envelope,
|
|
1588
|
+
decision.queue,
|
|
1589
|
+
decision.reason
|
|
1590
|
+
);
|
|
1591
|
+
break;
|
|
1592
|
+
}
|
|
1593
|
+
case "discard": {
|
|
1594
|
+
await this.transport.complete(receipt);
|
|
1595
|
+
break;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
async sendToDeadLetter(receipt, envelope, dlqName, reason) {
|
|
1600
|
+
envelope.docket.originalQueue ??= receipt.sourceQueue;
|
|
1601
|
+
if (this.transport.sendToDeadLetter) {
|
|
1602
|
+
await this.transport.sendToDeadLetter(receipt, dlqName, envelope, reason);
|
|
1603
|
+
} else {
|
|
1604
|
+
const fullDlqName = `${receipt.sourceQueue}.${dlqName}`;
|
|
1605
|
+
await this.transport.send(fullDlqName, envelope);
|
|
1606
|
+
await this.transport.complete(receipt);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Gets retry decision for unhandled messages (subscriber not found).
|
|
1611
|
+
* Uses same retry policy logic but sends to 'unhandled' DLQ after max attempts.
|
|
1612
|
+
*/
|
|
1613
|
+
getUnhandledRetryDecision(envelope, receipt, error) {
|
|
1614
|
+
const syntheticSubscriber = {
|
|
1615
|
+
name: envelope.docket.targetSubscriber,
|
|
1616
|
+
description: "Synthetic subscriber for unhandled message retry",
|
|
1617
|
+
idempotent: "yes",
|
|
1618
|
+
// Safe to retry unhandled messages
|
|
1619
|
+
importance: envelope.docket.importance ?? "should-investigate"
|
|
1620
|
+
};
|
|
1621
|
+
const decision = this.retryPolicy.shouldRetry({
|
|
1622
|
+
envelope,
|
|
1623
|
+
error,
|
|
1624
|
+
subscriber: syntheticSubscriber,
|
|
1625
|
+
receipt
|
|
1626
|
+
});
|
|
1627
|
+
if (decision.action === "dead-letter") {
|
|
1628
|
+
return {
|
|
1629
|
+
action: "dead-letter",
|
|
1630
|
+
queue: "unhandled",
|
|
1631
|
+
reason: decision.reason
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
return decision;
|
|
1635
|
+
}
|
|
1636
|
+
};
|
|
1637
|
+
|
|
1638
|
+
// src/retry/standard-policy.ts
|
|
1639
|
+
var defaultRetryConfig = {
|
|
1640
|
+
maxAttempts: 3,
|
|
1641
|
+
baseDelay: 1e3,
|
|
1642
|
+
maxDelay: 3e5,
|
|
1643
|
+
// 5 minutes
|
|
1644
|
+
backoffMultiplier: 2,
|
|
1645
|
+
maxDeliveries: 5
|
|
1646
|
+
};
|
|
1647
|
+
var StandardRetryPolicy = class {
|
|
1648
|
+
config;
|
|
1649
|
+
constructor(config = {}) {
|
|
1650
|
+
this.config = { ...defaultRetryConfig, ...config };
|
|
1651
|
+
}
|
|
1652
|
+
shouldRetry(context) {
|
|
1653
|
+
const { envelope, error, subscriber, receipt } = context;
|
|
1654
|
+
const errorMessage = error.message;
|
|
1655
|
+
if (receipt.deliveryCount >= this.config.maxDeliveries) {
|
|
1656
|
+
const poisonError = new MessageMaybePoisonedError(
|
|
1657
|
+
envelope.id,
|
|
1658
|
+
receipt.deliveryCount,
|
|
1659
|
+
this.config.maxDeliveries
|
|
1660
|
+
);
|
|
1661
|
+
return {
|
|
1662
|
+
action: "dead-letter",
|
|
1663
|
+
queue: "undeliverable",
|
|
1664
|
+
reason: poisonError.message
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
if (isAssertionError(error)) {
|
|
1668
|
+
return {
|
|
1669
|
+
action: "dead-letter",
|
|
1670
|
+
queue: "undeliverable",
|
|
1671
|
+
reason: `assertion error: ${errorMessage}`
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
if (isDontRetry(error)) {
|
|
1675
|
+
return {
|
|
1676
|
+
action: "dead-letter",
|
|
1677
|
+
queue: "undeliverable",
|
|
1678
|
+
reason: errorMessage
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
if (isDoRetry(error)) {
|
|
1682
|
+
if (receipt.attemptNumber >= this.config.maxAttempts) {
|
|
1683
|
+
return {
|
|
1684
|
+
action: "dead-letter",
|
|
1685
|
+
queue: "undeliverable",
|
|
1686
|
+
reason: `max attempts exceeded (${this.config.maxAttempts}) with forced retry`
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
return {
|
|
1690
|
+
action: "retry",
|
|
1691
|
+
delay: this.getDelay(context)
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
if (receipt.attemptNumber >= this.config.maxAttempts) {
|
|
1695
|
+
return {
|
|
1696
|
+
action: "dead-letter",
|
|
1697
|
+
queue: "undeliverable",
|
|
1698
|
+
reason: `max attempts exceeded (${this.config.maxAttempts})`
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
if (receipt.redelivered && (subscriber.idempotent === "no" || subscriber.idempotent === "unknown")) {
|
|
1702
|
+
const idempotentError = new IdempotentMessageCannotRetryError(
|
|
1703
|
+
envelope.id,
|
|
1704
|
+
subscriber.name
|
|
1705
|
+
);
|
|
1706
|
+
return {
|
|
1707
|
+
action: "dead-letter",
|
|
1708
|
+
queue: "undeliverable",
|
|
1709
|
+
reason: idempotentError.message
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
return {
|
|
1713
|
+
action: "retry",
|
|
1714
|
+
delay: this.getDelay(context)
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
getDelay(context) {
|
|
1718
|
+
const attempt = context.receipt.attemptNumber;
|
|
1719
|
+
const delay = this.config.baseDelay * Math.pow(this.config.backoffMultiplier, attempt - 1);
|
|
1720
|
+
return Math.min(delay, this.config.maxDelay);
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
|
|
1724
|
+
// src/schema/types.ts
|
|
1725
|
+
function bind(eventClass, subscribers) {
|
|
1726
|
+
return [eventClass, subscribers];
|
|
1727
|
+
}
|
|
1728
|
+
function isSchemaEntryTuple(entry) {
|
|
1729
|
+
return Array.isArray(entry);
|
|
1730
|
+
}
|
|
1731
|
+
function installPlugins(schema, plugins) {
|
|
1732
|
+
const result = {};
|
|
1733
|
+
for (const [eventKey, entry] of Object.entries(schema)) {
|
|
1734
|
+
const [eventClass, existingSubscribers] = isSchemaEntryTuple(entry) ? [entry[0], entry[1]] : [entry.eventClass, entry.subscribers];
|
|
1735
|
+
const pluginSubscribers = [];
|
|
1736
|
+
for (const plugin of plugins) {
|
|
1737
|
+
if (!plugin.exclusions?.includes(eventKey)) {
|
|
1738
|
+
pluginSubscribers.push(plugin.subscriber);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
result[eventKey] = [
|
|
1742
|
+
eventClass,
|
|
1743
|
+
[...existingSubscribers, ...pluginSubscribers]
|
|
1744
|
+
];
|
|
1745
|
+
}
|
|
1746
|
+
return result;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
// src/schema/registry.ts
|
|
1750
|
+
var SchemaError = class extends Error {
|
|
1751
|
+
description = "A schema operation failed, such as registering a duplicate event or conflicting alias. This is a configuration error that should be fixed during development. Review the event and subscriber registrations to resolve the conflict.";
|
|
1752
|
+
constructor(message) {
|
|
1753
|
+
super(message);
|
|
1754
|
+
this.name = "SchemaError";
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
var SchemaRegistry = class {
|
|
1758
|
+
entries = /* @__PURE__ */ new Map();
|
|
1759
|
+
aliases = /* @__PURE__ */ new Map();
|
|
1760
|
+
/**
|
|
1761
|
+
* Registers an event class with its subscribers.
|
|
1762
|
+
*/
|
|
1763
|
+
register(eventClass, subscribers, options = {}) {
|
|
1764
|
+
const key = eventClass.key;
|
|
1765
|
+
if (this.entries.has(key) && !options.override) {
|
|
1766
|
+
throw new SchemaError(`Event "${key}" is already registered`);
|
|
1767
|
+
}
|
|
1768
|
+
this.entries.set(key, {
|
|
1769
|
+
eventClass,
|
|
1770
|
+
subscribers
|
|
1771
|
+
});
|
|
1772
|
+
if (eventClass.aliases) {
|
|
1773
|
+
for (const alias of eventClass.aliases) {
|
|
1774
|
+
if (this.aliases.has(alias) && this.aliases.get(alias) !== key && !options.override) {
|
|
1775
|
+
throw new SchemaError(
|
|
1776
|
+
`Alias "${alias}" is already registered for event "${this.aliases.get(alias)}"`
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
this.aliases.set(alias, key);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Gets the event class for a given key.
|
|
1785
|
+
*/
|
|
1786
|
+
getEventClass(key) {
|
|
1787
|
+
const entry = this.entries.get(key);
|
|
1788
|
+
if (entry) return entry.eventClass;
|
|
1789
|
+
const aliasKey = this.aliases.get(key);
|
|
1790
|
+
if (aliasKey) {
|
|
1791
|
+
return this.entries.get(aliasKey)?.eventClass;
|
|
1792
|
+
}
|
|
1793
|
+
return void 0;
|
|
1794
|
+
}
|
|
1795
|
+
/**
|
|
1796
|
+
* Gets all subscribers for an event key.
|
|
1797
|
+
*/
|
|
1798
|
+
getSubscribers(eventKey) {
|
|
1799
|
+
const entry = this.getEntry(eventKey);
|
|
1800
|
+
return entry?.subscribers ?? [];
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Gets a specific subscriber by event key and subscriber name.
|
|
1804
|
+
*/
|
|
1805
|
+
getSubscriber(eventKey, subscriberName) {
|
|
1806
|
+
const subscribers = this.getSubscribers(eventKey);
|
|
1807
|
+
return subscribers.find((s) => s.name === subscriberName);
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* Gets a subscriber definition for the pipeline.
|
|
1811
|
+
*/
|
|
1812
|
+
getSubscriberDefinition(eventKey, subscriberName) {
|
|
1813
|
+
const subscriber = this.getSubscriber(eventKey, subscriberName);
|
|
1814
|
+
if (!subscriber) return void 0;
|
|
1815
|
+
const def = {
|
|
1816
|
+
name: subscriber.name,
|
|
1817
|
+
description: subscriber.description,
|
|
1818
|
+
idempotent: subscriber.idempotent ?? "unknown",
|
|
1819
|
+
importance: subscriber.importance ?? "should-investigate"
|
|
1820
|
+
};
|
|
1821
|
+
if (subscriber.targetQueue !== void 0) {
|
|
1822
|
+
def.targetQueue = subscriber.targetQueue;
|
|
1823
|
+
}
|
|
1824
|
+
return def;
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Gets a subscriber with callback for execution.
|
|
1828
|
+
*/
|
|
1829
|
+
getExecutableSubscriber(eventKey, subscriberName) {
|
|
1830
|
+
const subscriber = this.getSubscriber(eventKey, subscriberName);
|
|
1831
|
+
if (!subscriber || !isSubscriber(subscriber)) return void 0;
|
|
1832
|
+
return subscriber;
|
|
1833
|
+
}
|
|
1834
|
+
/**
|
|
1835
|
+
* Gets the event class by alias.
|
|
1836
|
+
*/
|
|
1837
|
+
getEventByAlias(alias) {
|
|
1838
|
+
const key = this.aliases.get(alias);
|
|
1839
|
+
if (!key) return void 0;
|
|
1840
|
+
return this.entries.get(key)?.eventClass;
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Checks if an event key is registered.
|
|
1844
|
+
*/
|
|
1845
|
+
hasEvent(key) {
|
|
1846
|
+
return this.entries.has(key) || this.aliases.has(key);
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* Gets all registered event keys.
|
|
1850
|
+
*/
|
|
1851
|
+
getEventKeys() {
|
|
1852
|
+
return [...this.entries.keys()];
|
|
1853
|
+
}
|
|
1854
|
+
/**
|
|
1855
|
+
* Validates the schema for issues.
|
|
1856
|
+
*/
|
|
1857
|
+
validate() {
|
|
1858
|
+
const issues = [];
|
|
1859
|
+
for (const [key, entry] of this.entries) {
|
|
1860
|
+
const names = /* @__PURE__ */ new Set();
|
|
1861
|
+
for (const subscriber of entry.subscribers) {
|
|
1862
|
+
if (names.has(subscriber.name)) {
|
|
1863
|
+
issues.push({
|
|
1864
|
+
severity: "error",
|
|
1865
|
+
eventKey: key,
|
|
1866
|
+
message: `Duplicate subscriber name: "${subscriber.name}"`
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
names.add(subscriber.name);
|
|
1870
|
+
}
|
|
1871
|
+
const stubs = entry.subscribers.filter((s) => !isSubscriber(s));
|
|
1872
|
+
if (stubs.length === entry.subscribers.length && stubs.length > 0) {
|
|
1873
|
+
issues.push({
|
|
1874
|
+
severity: "warning",
|
|
1875
|
+
eventKey: key,
|
|
1876
|
+
message: "All subscribers are stubs, no local handlers"
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
return {
|
|
1881
|
+
valid: issues.filter((i) => i.severity === "error").length === 0,
|
|
1882
|
+
issues
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Clears all registered entries.
|
|
1887
|
+
*/
|
|
1888
|
+
clear() {
|
|
1889
|
+
this.entries.clear();
|
|
1890
|
+
this.aliases.clear();
|
|
1891
|
+
}
|
|
1892
|
+
getEntry(eventKey) {
|
|
1893
|
+
const entry = this.entries.get(eventKey);
|
|
1894
|
+
if (entry) return entry;
|
|
1895
|
+
const aliasKey = this.aliases.get(eventKey);
|
|
1896
|
+
if (aliasKey) {
|
|
1897
|
+
return this.entries.get(aliasKey);
|
|
1898
|
+
}
|
|
1899
|
+
return void 0;
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
|
|
1903
|
+
// src/core/matador.ts
|
|
1904
|
+
var Matador = class {
|
|
1905
|
+
transport;
|
|
1906
|
+
topology;
|
|
1907
|
+
schema;
|
|
1908
|
+
codec;
|
|
1909
|
+
retryPolicy;
|
|
1910
|
+
hooks;
|
|
1911
|
+
pipeline;
|
|
1912
|
+
fanout;
|
|
1913
|
+
shutdownManager;
|
|
1914
|
+
consumeFrom;
|
|
1915
|
+
subscriptions = [];
|
|
1916
|
+
started = false;
|
|
1917
|
+
/**
|
|
1918
|
+
* Creates a new Matador instance.
|
|
1919
|
+
*
|
|
1920
|
+
* @param config - Static configuration (transport, topology, schema, etc.)
|
|
1921
|
+
* @param hooks - Lifecycle hooks for logging, monitoring, and dynamic configuration.
|
|
1922
|
+
* Passed separately to support NestJS dependency injection.
|
|
1923
|
+
*/
|
|
1924
|
+
constructor(config, hooks) {
|
|
1925
|
+
this.transport = config.transport;
|
|
1926
|
+
this.topology = config.topology;
|
|
1927
|
+
this.consumeFrom = config.consumeFrom ?? [];
|
|
1928
|
+
this.schema = new SchemaRegistry();
|
|
1929
|
+
this.codec = config.codec ?? new JsonCodec();
|
|
1930
|
+
this.retryPolicy = config.retryPolicy ?? new StandardRetryPolicy();
|
|
1931
|
+
this.hooks = new SafeHooks(hooks);
|
|
1932
|
+
this.registerSchema(config.schema);
|
|
1933
|
+
this.pipeline = new ProcessingPipeline({
|
|
1934
|
+
transport: this.transport,
|
|
1935
|
+
schema: this.schema,
|
|
1936
|
+
codec: this.codec,
|
|
1937
|
+
retryPolicy: this.retryPolicy,
|
|
1938
|
+
hooks: this.hooks,
|
|
1939
|
+
checkpointStore: config.checkpointStore
|
|
1940
|
+
});
|
|
1941
|
+
const defaultQueue = this.topology.queues[0]?.name ?? "default";
|
|
1942
|
+
this.fanout = new FanoutEngine({
|
|
1943
|
+
transport: this.transport,
|
|
1944
|
+
schema: this.schema,
|
|
1945
|
+
hooks: this.hooks,
|
|
1946
|
+
namespace: this.topology.namespace,
|
|
1947
|
+
defaultQueue
|
|
1948
|
+
});
|
|
1949
|
+
this.shutdownManager = new ShutdownManager(
|
|
1950
|
+
() => this.fanout.eventsBeingEnqueuedCount,
|
|
1951
|
+
() => this.stopReceiving(),
|
|
1952
|
+
() => this.transport.disconnect(),
|
|
1953
|
+
config.shutdownConfig
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
/**
|
|
1957
|
+
* Registers an event class with its subscribers.
|
|
1958
|
+
*/
|
|
1959
|
+
register(eventClass, subscribers) {
|
|
1960
|
+
this.schema.register(eventClass, subscribers);
|
|
1961
|
+
return this;
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* Registers events from a schema object.
|
|
1965
|
+
* Supports both object format and tuple format entries.
|
|
1966
|
+
*
|
|
1967
|
+
* @example
|
|
1968
|
+
* ```typescript
|
|
1969
|
+
* // Tuple format
|
|
1970
|
+
* matador.registerSchema({
|
|
1971
|
+
* [UserCreatedEvent.key]: [UserCreatedEvent, [emailSubscriber]],
|
|
1972
|
+
* [OrderPlacedEvent.key]: [OrderPlacedEvent, [invoiceSubscriber]],
|
|
1973
|
+
* });
|
|
1974
|
+
*
|
|
1975
|
+
* // Object format
|
|
1976
|
+
* matador.registerSchema({
|
|
1977
|
+
* [UserCreatedEvent.key]: { eventClass: UserCreatedEvent, subscribers: [emailSubscriber] },
|
|
1978
|
+
* });
|
|
1979
|
+
* ```
|
|
1980
|
+
*/
|
|
1981
|
+
registerSchema(schema) {
|
|
1982
|
+
for (const entry of Object.values(schema)) {
|
|
1983
|
+
if (isSchemaEntryTuple(entry)) {
|
|
1984
|
+
const [eventClass, subscribers] = entry;
|
|
1985
|
+
this.schema.register(eventClass, subscribers);
|
|
1986
|
+
} else {
|
|
1987
|
+
this.schema.register(entry.eventClass, entry.subscribers);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
return this;
|
|
1991
|
+
}
|
|
1992
|
+
/**
|
|
1993
|
+
* Starts Matador - connects transport and begins consuming.
|
|
1994
|
+
* This method is idempotent - calling it multiple times is safe.
|
|
1995
|
+
*/
|
|
1996
|
+
async start() {
|
|
1997
|
+
if (this.started) {
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
const validation = this.schema.validate();
|
|
2001
|
+
if (!validation.valid) {
|
|
2002
|
+
const errors = validation.issues.filter((i) => i.severity === "error");
|
|
2003
|
+
throw new InvalidSchemaError(
|
|
2004
|
+
"Schema validation failed",
|
|
2005
|
+
errors.map((e) => e.message).join(", ")
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
await this.transport.connect();
|
|
2009
|
+
await this.transport.applyTopology(this.topology);
|
|
2010
|
+
for (const queueName of this.consumeFrom) {
|
|
2011
|
+
const qualifiedName = getQualifiedQueueName(
|
|
2012
|
+
this.topology.namespace,
|
|
2013
|
+
queueName
|
|
2014
|
+
);
|
|
2015
|
+
const queueDef = this.topology.queues.find((q) => q.name === queueName);
|
|
2016
|
+
const subscription = await this.transport.subscribe(
|
|
2017
|
+
qualifiedName,
|
|
2018
|
+
async (envelope, receipt) => {
|
|
2019
|
+
this.shutdownManager.incrementProcessing();
|
|
2020
|
+
try {
|
|
2021
|
+
const rawMessage = this.codec.encode(envelope);
|
|
2022
|
+
await this.pipeline.process(rawMessage, receipt);
|
|
2023
|
+
} finally {
|
|
2024
|
+
this.shutdownManager.decrementProcessing();
|
|
2025
|
+
}
|
|
2026
|
+
},
|
|
2027
|
+
queueDef?.concurrency !== void 0 ? { concurrency: queueDef.concurrency } : void 0
|
|
2028
|
+
);
|
|
2029
|
+
this.subscriptions.push(subscription);
|
|
2030
|
+
}
|
|
2031
|
+
this.started = true;
|
|
2032
|
+
}
|
|
2033
|
+
async send(eventOrClass, dataOrOptions, maybeOptions) {
|
|
2034
|
+
if (!this.started) {
|
|
2035
|
+
throw new NotStartedError("send");
|
|
2036
|
+
}
|
|
2037
|
+
if (!this.shutdownManager.isEnqueueAllowed) {
|
|
2038
|
+
throw new ShutdownInProgressError();
|
|
2039
|
+
}
|
|
2040
|
+
const isEventClass = typeof eventOrClass === "function" && "key" in eventOrClass;
|
|
2041
|
+
if (isEventClass) {
|
|
2042
|
+
const eventClass = eventOrClass;
|
|
2043
|
+
const data = dataOrOptions;
|
|
2044
|
+
const options = maybeOptions;
|
|
2045
|
+
const event = new eventClass(data);
|
|
2046
|
+
return this.fanout.send(eventClass, event, options);
|
|
2047
|
+
} else {
|
|
2048
|
+
const event = eventOrClass;
|
|
2049
|
+
const options = dataOrOptions;
|
|
2050
|
+
const eventClass = event.constructor;
|
|
2051
|
+
return this.fanout.send(eventClass, event, options);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Gets current handler state.
|
|
2056
|
+
*/
|
|
2057
|
+
getHandlersState() {
|
|
2058
|
+
return this.shutdownManager.getHandlersState();
|
|
2059
|
+
}
|
|
2060
|
+
/**
|
|
2061
|
+
* Checks if Matador is idle (no processing or enqueuing).
|
|
2062
|
+
*/
|
|
2063
|
+
isIdle() {
|
|
2064
|
+
return this.shutdownManager.getHandlersState().isIdle;
|
|
2065
|
+
}
|
|
2066
|
+
/**
|
|
2067
|
+
* Waits for all handlers to become idle.
|
|
2068
|
+
*/
|
|
2069
|
+
async waitForIdle(timeoutMs = 3e4) {
|
|
2070
|
+
const deadline = Date.now() + timeoutMs;
|
|
2071
|
+
while (!this.isIdle()) {
|
|
2072
|
+
if (Date.now() > deadline) {
|
|
2073
|
+
return false;
|
|
2074
|
+
}
|
|
2075
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2076
|
+
}
|
|
2077
|
+
return true;
|
|
2078
|
+
}
|
|
2079
|
+
/**
|
|
2080
|
+
* Gracefully shuts down Matador.
|
|
2081
|
+
*/
|
|
2082
|
+
async shutdown() {
|
|
2083
|
+
if (!this.started) {
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
await this.shutdownManager.shutdown();
|
|
2087
|
+
this.started = false;
|
|
2088
|
+
}
|
|
2089
|
+
/**
|
|
2090
|
+
* Checks if transport is connected.
|
|
2091
|
+
*/
|
|
2092
|
+
isConnected() {
|
|
2093
|
+
return this.transport.isConnected();
|
|
2094
|
+
}
|
|
2095
|
+
async stopReceiving() {
|
|
2096
|
+
for (const subscription of this.subscriptions) {
|
|
2097
|
+
await subscription.unsubscribe();
|
|
2098
|
+
}
|
|
2099
|
+
this.subscriptions.length = 0;
|
|
2100
|
+
}
|
|
2101
|
+
};
|
|
2102
|
+
|
|
2103
|
+
// src/transport/capabilities.ts
|
|
2104
|
+
function supportsDeliveryMode(capabilities, mode) {
|
|
2105
|
+
return capabilities.deliveryModes.includes(mode);
|
|
2106
|
+
}
|
|
2107
|
+
function supportsDelayedMessages(capabilities) {
|
|
2108
|
+
return capabilities.delayedMessages;
|
|
2109
|
+
}
|
|
2110
|
+
function hasNativeDeadLetter(capabilities) {
|
|
2111
|
+
return capabilities.deadLetterRouting === "native";
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// src/transport/connection-manager.ts
|
|
2115
|
+
var defaultConnectionConfig = {
|
|
2116
|
+
maxReconnectAttempts: 0,
|
|
2117
|
+
// Infinite
|
|
2118
|
+
initialReconnectDelay: 1e3,
|
|
2119
|
+
maxReconnectDelay: 3e4,
|
|
2120
|
+
backoffMultiplier: 2
|
|
2121
|
+
};
|
|
2122
|
+
var ConnectionManager = class {
|
|
2123
|
+
constructor(connectFn, disconnectFn, config = {}) {
|
|
2124
|
+
this.connectFn = connectFn;
|
|
2125
|
+
this.disconnectFn = disconnectFn;
|
|
2126
|
+
this.config = { ...defaultConnectionConfig, ...config };
|
|
2127
|
+
}
|
|
2128
|
+
_state = { status: "disconnected" };
|
|
2129
|
+
listeners = /* @__PURE__ */ new Set();
|
|
2130
|
+
reconnectTimer = null;
|
|
2131
|
+
config;
|
|
2132
|
+
/**
|
|
2133
|
+
* Current connection state.
|
|
2134
|
+
*/
|
|
2135
|
+
get state() {
|
|
2136
|
+
return this._state;
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Whether currently connected.
|
|
2140
|
+
*/
|
|
2141
|
+
isConnected() {
|
|
2142
|
+
return this._state.status === "connected";
|
|
2143
|
+
}
|
|
2144
|
+
/**
|
|
2145
|
+
* Register a callback for connection state changes.
|
|
2146
|
+
*/
|
|
2147
|
+
onStateChange(callback) {
|
|
2148
|
+
this.listeners.add(callback);
|
|
2149
|
+
return () => this.listeners.delete(callback);
|
|
2150
|
+
}
|
|
2151
|
+
/**
|
|
2152
|
+
* Attempt to establish connection.
|
|
2153
|
+
*/
|
|
2154
|
+
async connect() {
|
|
2155
|
+
if (this._state.status === "connected") {
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
this.setState({ status: "connecting", attempt: 1 });
|
|
2159
|
+
try {
|
|
2160
|
+
await this.connectFn();
|
|
2161
|
+
this.setState({ status: "connected" });
|
|
2162
|
+
} catch (error) {
|
|
2163
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2164
|
+
await this.handleConnectionFailure(err, 1);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* Gracefully disconnect.
|
|
2169
|
+
*/
|
|
2170
|
+
async disconnect() {
|
|
2171
|
+
this.clearReconnectTimer();
|
|
2172
|
+
if (this._state.status === "disconnected") {
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
try {
|
|
2176
|
+
await this.disconnectFn();
|
|
2177
|
+
} finally {
|
|
2178
|
+
this.setState({ status: "disconnected" });
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
/**
|
|
2182
|
+
* Called by transport when connection is lost unexpectedly.
|
|
2183
|
+
*/
|
|
2184
|
+
async handleConnectionLost(error) {
|
|
2185
|
+
if (this._state.status === "disconnected") {
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
this.setState({ status: "reconnecting", attempt: 1, lastError: error });
|
|
2189
|
+
await this.attemptReconnect(1);
|
|
2190
|
+
}
|
|
2191
|
+
/**
|
|
2192
|
+
* Calculate delay for a given attempt using exponential backoff.
|
|
2193
|
+
*/
|
|
2194
|
+
calculateDelay(attempt) {
|
|
2195
|
+
const delay = this.config.initialReconnectDelay * Math.pow(this.config.backoffMultiplier, attempt - 1);
|
|
2196
|
+
return Math.min(delay, this.config.maxReconnectDelay);
|
|
2197
|
+
}
|
|
2198
|
+
setState(state) {
|
|
2199
|
+
this._state = state;
|
|
2200
|
+
for (const listener of this.listeners) {
|
|
2201
|
+
try {
|
|
2202
|
+
listener(state);
|
|
2203
|
+
} catch {
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
async handleConnectionFailure(error, attempt) {
|
|
2208
|
+
if (this.shouldGiveUp(attempt)) {
|
|
2209
|
+
this.setState({ status: "failed", error });
|
|
2210
|
+
throw error;
|
|
2211
|
+
}
|
|
2212
|
+
this.setState({
|
|
2213
|
+
status: "reconnecting",
|
|
2214
|
+
attempt: attempt + 1,
|
|
2215
|
+
lastError: error
|
|
2216
|
+
});
|
|
2217
|
+
await this.attemptReconnect(attempt + 1);
|
|
2218
|
+
}
|
|
2219
|
+
shouldGiveUp(attempt) {
|
|
2220
|
+
return this.config.maxReconnectAttempts > 0 && attempt >= this.config.maxReconnectAttempts;
|
|
2221
|
+
}
|
|
2222
|
+
async attemptReconnect(attempt) {
|
|
2223
|
+
const delay = this.calculateDelay(attempt);
|
|
2224
|
+
await new Promise((resolve) => {
|
|
2225
|
+
this.reconnectTimer = setTimeout(resolve, delay);
|
|
2226
|
+
});
|
|
2227
|
+
if (this._state.status === "disconnected") {
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
try {
|
|
2231
|
+
await this.connectFn();
|
|
2232
|
+
this.setState({ status: "connected" });
|
|
2233
|
+
} catch (error) {
|
|
2234
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2235
|
+
await this.handleConnectionFailure(err, attempt);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
clearReconnectTimer() {
|
|
2239
|
+
if (this.reconnectTimer !== null) {
|
|
2240
|
+
clearTimeout(this.reconnectTimer);
|
|
2241
|
+
this.reconnectTimer = null;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
};
|
|
2245
|
+
|
|
2246
|
+
// src/transport/local/local-transport.ts
|
|
2247
|
+
var localCapabilities = {
|
|
2248
|
+
deliveryModes: ["at-least-once", "at-most-once"],
|
|
2249
|
+
delayedMessages: true,
|
|
2250
|
+
// Implemented with setTimeout
|
|
2251
|
+
deadLetterRouting: "manual",
|
|
2252
|
+
attemptTracking: false,
|
|
2253
|
+
concurrencyModel: "none",
|
|
2254
|
+
ordering: "queue",
|
|
2255
|
+
priorities: false
|
|
2256
|
+
};
|
|
2257
|
+
var LocalTransport = class {
|
|
2258
|
+
name = "local";
|
|
2259
|
+
capabilities = localCapabilities;
|
|
2260
|
+
connected = false;
|
|
2261
|
+
queues = /* @__PURE__ */ new Map();
|
|
2262
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
2263
|
+
completedMessages = [];
|
|
2264
|
+
delayedTimers = /* @__PURE__ */ new Set();
|
|
2265
|
+
messageIdCounter = 0;
|
|
2266
|
+
logger;
|
|
2267
|
+
constructor(logger) {
|
|
2268
|
+
this.logger = logger ?? consoleLogger;
|
|
2269
|
+
}
|
|
2270
|
+
async connect() {
|
|
2271
|
+
this.connected = true;
|
|
2272
|
+
}
|
|
2273
|
+
async disconnect() {
|
|
2274
|
+
for (const timer of this.delayedTimers) {
|
|
2275
|
+
clearTimeout(timer);
|
|
2276
|
+
}
|
|
2277
|
+
this.delayedTimers.clear();
|
|
2278
|
+
for (const subs of this.subscriptions.values()) {
|
|
2279
|
+
for (const sub of subs) {
|
|
2280
|
+
sub.active = false;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
this.connected = false;
|
|
2284
|
+
}
|
|
2285
|
+
isConnected() {
|
|
2286
|
+
return this.connected;
|
|
2287
|
+
}
|
|
2288
|
+
async applyTopology(topology) {
|
|
2289
|
+
if (!this.connected) {
|
|
2290
|
+
throw new TransportNotConnectedError(this.name, "applyTopology");
|
|
2291
|
+
}
|
|
2292
|
+
for (const queueDef of topology.queues) {
|
|
2293
|
+
const queueName = `${topology.namespace}.${queueDef.name}`;
|
|
2294
|
+
if (!this.queues.has(queueName)) {
|
|
2295
|
+
this.queues.set(queueName, []);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
async send(queue, envelope, options) {
|
|
2300
|
+
if (!this.connected) {
|
|
2301
|
+
throw new TransportNotConnectedError(this.name, "send");
|
|
2302
|
+
}
|
|
2303
|
+
if (options?.delay !== void 0 && options.delay > 0) {
|
|
2304
|
+
this.scheduleDelayedMessage(queue, envelope, options.delay);
|
|
2305
|
+
return this.name;
|
|
2306
|
+
}
|
|
2307
|
+
await this.enqueue(queue, envelope);
|
|
2308
|
+
return this.name;
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* Schedules a message for delayed delivery.
|
|
2312
|
+
* Returns immediately (non-blocking) to match real transport behavior.
|
|
2313
|
+
*/
|
|
2314
|
+
scheduleDelayedMessage(queue, envelope, delayMs) {
|
|
2315
|
+
const timer = setTimeout(() => {
|
|
2316
|
+
this.delayedTimers.delete(timer);
|
|
2317
|
+
this.enqueue(queue, envelope).catch((error) => {
|
|
2318
|
+
this.logger.error(
|
|
2319
|
+
"[Matador] \u{1F534} Failed to enqueue delayed message",
|
|
2320
|
+
error
|
|
2321
|
+
);
|
|
2322
|
+
});
|
|
2323
|
+
}, delayMs);
|
|
2324
|
+
this.delayedTimers.add(timer);
|
|
2325
|
+
}
|
|
2326
|
+
async enqueue(queue, envelope) {
|
|
2327
|
+
const messages = this.getOrCreateQueue(queue);
|
|
2328
|
+
const messageId = `${++this.messageIdCounter}`;
|
|
2329
|
+
const queuedMessage = {
|
|
2330
|
+
envelope,
|
|
2331
|
+
id: messageId,
|
|
2332
|
+
completed: false
|
|
2333
|
+
};
|
|
2334
|
+
messages.push(queuedMessage);
|
|
2335
|
+
await this.deliverToSubscribers(queue, queuedMessage);
|
|
2336
|
+
}
|
|
2337
|
+
async deliverToSubscribers(queue, message) {
|
|
2338
|
+
const subs = this.subscriptions.get(queue);
|
|
2339
|
+
if (!subs) return;
|
|
2340
|
+
for (const sub of subs) {
|
|
2341
|
+
if (!sub.active || message.completed) continue;
|
|
2342
|
+
const receipt = {
|
|
2343
|
+
handle: message,
|
|
2344
|
+
redelivered: false,
|
|
2345
|
+
attemptNumber: message.envelope.docket.attempts,
|
|
2346
|
+
deliveryCount: message.envelope.docket.attempts,
|
|
2347
|
+
sourceQueue: queue,
|
|
2348
|
+
sourceTransport: this.name
|
|
2349
|
+
};
|
|
2350
|
+
try {
|
|
2351
|
+
await sub.handler(message.envelope, receipt);
|
|
2352
|
+
} catch (error) {
|
|
2353
|
+
this.logger.error("\u{1F534} Handler error in message processing", error);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
async subscribe(queue, handler, options = {}) {
|
|
2358
|
+
if (!this.connected) {
|
|
2359
|
+
throw new TransportNotConnectedError(this.name, "subscribe");
|
|
2360
|
+
}
|
|
2361
|
+
const subscription = {
|
|
2362
|
+
handler,
|
|
2363
|
+
options,
|
|
2364
|
+
active: true
|
|
2365
|
+
};
|
|
2366
|
+
const subs = this.subscriptions.get(queue) ?? [];
|
|
2367
|
+
subs.push(subscription);
|
|
2368
|
+
this.subscriptions.set(queue, subs);
|
|
2369
|
+
const messages = this.queues.get(queue) ?? [];
|
|
2370
|
+
for (const message of messages) {
|
|
2371
|
+
if (message.completed) continue;
|
|
2372
|
+
await this.deliverToSubscribers(queue, message);
|
|
2373
|
+
}
|
|
2374
|
+
return {
|
|
2375
|
+
unsubscribe: async () => {
|
|
2376
|
+
subscription.active = false;
|
|
2377
|
+
const remaining = subs.filter((s) => s !== subscription);
|
|
2378
|
+
if (remaining.length > 0) {
|
|
2379
|
+
this.subscriptions.set(queue, remaining);
|
|
2380
|
+
} else {
|
|
2381
|
+
this.subscriptions.delete(queue);
|
|
2382
|
+
}
|
|
2383
|
+
},
|
|
2384
|
+
get isActive() {
|
|
2385
|
+
return subscription.active;
|
|
2386
|
+
}
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
async complete(receipt) {
|
|
2390
|
+
const message = receipt.handle;
|
|
2391
|
+
message.completed = true;
|
|
2392
|
+
this.completedMessages.push(receipt);
|
|
2393
|
+
}
|
|
2394
|
+
async sendToDeadLetter(receipt, dlqName, envelope, _reason) {
|
|
2395
|
+
const dlqQueueName = `${receipt.sourceQueue}.${dlqName}`;
|
|
2396
|
+
await this.enqueue(dlqQueueName, envelope);
|
|
2397
|
+
await this.complete(receipt);
|
|
2398
|
+
}
|
|
2399
|
+
// Test helpers
|
|
2400
|
+
/**
|
|
2401
|
+
* Gets the current size of a queue.
|
|
2402
|
+
*/
|
|
2403
|
+
getQueueSize(queue) {
|
|
2404
|
+
const messages = this.queues.get(queue);
|
|
2405
|
+
if (!messages) return 0;
|
|
2406
|
+
return messages.filter((m) => !m.completed).length;
|
|
2407
|
+
}
|
|
2408
|
+
/**
|
|
2409
|
+
* Gets all completed message receipts.
|
|
2410
|
+
*/
|
|
2411
|
+
getCompleted() {
|
|
2412
|
+
return this.completedMessages;
|
|
2413
|
+
}
|
|
2414
|
+
/**
|
|
2415
|
+
* Gets pending (uncompleted) messages from a queue.
|
|
2416
|
+
*/
|
|
2417
|
+
getPendingMessages(queue) {
|
|
2418
|
+
const messages = this.queues.get(queue);
|
|
2419
|
+
if (!messages) return [];
|
|
2420
|
+
return messages.filter((m) => !m.completed).map((m) => m.envelope);
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Clears all state (for test isolation).
|
|
2424
|
+
*/
|
|
2425
|
+
clear() {
|
|
2426
|
+
this.queues.clear();
|
|
2427
|
+
this.subscriptions.clear();
|
|
2428
|
+
this.completedMessages.length = 0;
|
|
2429
|
+
this.messageIdCounter = 0;
|
|
2430
|
+
for (const timer of this.delayedTimers) {
|
|
2431
|
+
clearTimeout(timer);
|
|
2432
|
+
}
|
|
2433
|
+
this.delayedTimers.clear();
|
|
2434
|
+
}
|
|
2435
|
+
/**
|
|
2436
|
+
* Receives one message from the queue without a subscription.
|
|
2437
|
+
* Useful for testing.
|
|
2438
|
+
*/
|
|
2439
|
+
async receiveOne(queue) {
|
|
2440
|
+
const messages = this.queues.get(queue);
|
|
2441
|
+
if (!messages) return null;
|
|
2442
|
+
const pending = messages.find((m) => !m.completed);
|
|
2443
|
+
if (!pending) return null;
|
|
2444
|
+
const receipt = {
|
|
2445
|
+
handle: pending,
|
|
2446
|
+
redelivered: false,
|
|
2447
|
+
attemptNumber: pending.envelope.docket.attempts,
|
|
2448
|
+
deliveryCount: pending.envelope.docket.attempts,
|
|
2449
|
+
sourceQueue: queue,
|
|
2450
|
+
sourceTransport: this.name
|
|
2451
|
+
};
|
|
2452
|
+
return { envelope: pending.envelope, receipt };
|
|
2453
|
+
}
|
|
2454
|
+
getOrCreateQueue(queue) {
|
|
2455
|
+
let messages = this.queues.get(queue);
|
|
2456
|
+
if (!messages) {
|
|
2457
|
+
messages = [];
|
|
2458
|
+
this.queues.set(queue, messages);
|
|
2459
|
+
}
|
|
2460
|
+
return messages;
|
|
2461
|
+
}
|
|
2462
|
+
};
|
|
2463
|
+
|
|
2464
|
+
// src/transport/multi/multi-transport.ts
|
|
2465
|
+
var SOURCE_TRANSPORT = /* @__PURE__ */ Symbol("sourceTransport");
|
|
2466
|
+
function getSourceTransport(receipt) {
|
|
2467
|
+
return receipt[SOURCE_TRANSPORT];
|
|
2468
|
+
}
|
|
2469
|
+
function mergeCapabilities(transports) {
|
|
2470
|
+
const primary = transports[0];
|
|
2471
|
+
if (!primary) {
|
|
2472
|
+
throw new Error("At least one transport is required");
|
|
2473
|
+
}
|
|
2474
|
+
return primary.capabilities;
|
|
2475
|
+
}
|
|
2476
|
+
var MultiTransport = class {
|
|
2477
|
+
name;
|
|
2478
|
+
capabilities;
|
|
2479
|
+
primary;
|
|
2480
|
+
/** All available transports, in order of preference (primary first). */
|
|
2481
|
+
transports;
|
|
2482
|
+
/** Whether fallback to secondary transports is enabled. */
|
|
2483
|
+
fallbackEnabled;
|
|
2484
|
+
hooks;
|
|
2485
|
+
connected = false;
|
|
2486
|
+
constructor(config, hooks = {}) {
|
|
2487
|
+
const primary = config.transports[0];
|
|
2488
|
+
if (!primary) {
|
|
2489
|
+
throw new Error("At least one transport is required");
|
|
2490
|
+
}
|
|
2491
|
+
this.primary = primary;
|
|
2492
|
+
this.transports = config.transports;
|
|
2493
|
+
this.fallbackEnabled = config.fallbackEnabled ?? true;
|
|
2494
|
+
this.hooks = hooks;
|
|
2495
|
+
this.name = `multi(${this.transports.map((t) => t.name).join(",")})`;
|
|
2496
|
+
this.capabilities = mergeCapabilities(this.transports);
|
|
2497
|
+
}
|
|
2498
|
+
async connect() {
|
|
2499
|
+
await Promise.all(this.transports.map((t) => t.connect()));
|
|
2500
|
+
this.connected = true;
|
|
2501
|
+
}
|
|
2502
|
+
async disconnect() {
|
|
2503
|
+
await Promise.all(this.transports.map((t) => t.disconnect()));
|
|
2504
|
+
this.connected = false;
|
|
2505
|
+
}
|
|
2506
|
+
isConnected() {
|
|
2507
|
+
return this.connected && this.primary.isConnected();
|
|
2508
|
+
}
|
|
2509
|
+
async applyTopology(topology) {
|
|
2510
|
+
await Promise.all(this.transports.map((t) => t.applyTopology(topology)));
|
|
2511
|
+
}
|
|
2512
|
+
async send(queue, envelope, options) {
|
|
2513
|
+
const selectedTransport = await this.selectTransport();
|
|
2514
|
+
if (!this.fallbackEnabled) {
|
|
2515
|
+
return selectedTransport.send(queue, envelope, options);
|
|
2516
|
+
}
|
|
2517
|
+
const transportOrder = this.getTransportOrder(selectedTransport);
|
|
2518
|
+
const errors = [];
|
|
2519
|
+
for (let i = 0; i < transportOrder.length; i++) {
|
|
2520
|
+
const transport = transportOrder[i];
|
|
2521
|
+
try {
|
|
2522
|
+
return await transport.send(queue, envelope, options);
|
|
2523
|
+
} catch (error) {
|
|
2524
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2525
|
+
errors.push(err);
|
|
2526
|
+
const nextTransport = transportOrder[i + 1];
|
|
2527
|
+
if (nextTransport && this.hooks.onEnqueueFallback) {
|
|
2528
|
+
this.hooks.onEnqueueFallback({
|
|
2529
|
+
envelope,
|
|
2530
|
+
queue,
|
|
2531
|
+
failedTransport: transport.name,
|
|
2532
|
+
nextTransport: nextTransport.name,
|
|
2533
|
+
error: err
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
throw new AllTransportsFailedError(queue, errors);
|
|
2539
|
+
}
|
|
2540
|
+
/**
|
|
2541
|
+
* Selects the transport to use based on the getDesiredBackend hook.
|
|
2542
|
+
*/
|
|
2543
|
+
async selectTransport() {
|
|
2544
|
+
if (!this.hooks.getDesiredBackend) {
|
|
2545
|
+
return this.primary;
|
|
2546
|
+
}
|
|
2547
|
+
try {
|
|
2548
|
+
const desiredBackend = await this.hooks.getDesiredBackend();
|
|
2549
|
+
if (!desiredBackend) {
|
|
2550
|
+
return this.primary;
|
|
2551
|
+
}
|
|
2552
|
+
const selected = this.transports.find((t) => t.name === desiredBackend);
|
|
2553
|
+
if (selected) {
|
|
2554
|
+
return selected;
|
|
2555
|
+
}
|
|
2556
|
+
return this.primary;
|
|
2557
|
+
} catch {
|
|
2558
|
+
return this.primary;
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
/**
|
|
2562
|
+
* Returns transports in order, with the selected transport first.
|
|
2563
|
+
*/
|
|
2564
|
+
getTransportOrder(selected) {
|
|
2565
|
+
if (selected === this.primary) {
|
|
2566
|
+
return this.transports;
|
|
2567
|
+
}
|
|
2568
|
+
return [selected, ...this.transports.filter((t) => t !== selected)];
|
|
2569
|
+
}
|
|
2570
|
+
async subscribe(queue, handler, options) {
|
|
2571
|
+
const subscriptions = await Promise.all(
|
|
2572
|
+
this.transports.map((transport) => {
|
|
2573
|
+
const wrappedHandler = (envelope, receipt) => {
|
|
2574
|
+
const taggedReceipt = Object.assign(receipt, {
|
|
2575
|
+
[SOURCE_TRANSPORT]: transport
|
|
2576
|
+
});
|
|
2577
|
+
return handler(envelope, taggedReceipt);
|
|
2578
|
+
};
|
|
2579
|
+
return transport.subscribe(queue, wrappedHandler, options);
|
|
2580
|
+
})
|
|
2581
|
+
);
|
|
2582
|
+
return {
|
|
2583
|
+
unsubscribe: async () => {
|
|
2584
|
+
await Promise.all(subscriptions.map((s) => s.unsubscribe()));
|
|
2585
|
+
},
|
|
2586
|
+
get isActive() {
|
|
2587
|
+
return subscriptions.some((s) => s.isActive);
|
|
2588
|
+
}
|
|
2589
|
+
};
|
|
2590
|
+
}
|
|
2591
|
+
async complete(receipt) {
|
|
2592
|
+
const transport = getSourceTransport(receipt) ?? this.primary;
|
|
2593
|
+
return transport.complete(receipt);
|
|
2594
|
+
}
|
|
2595
|
+
async sendToDeadLetter(receipt, dlqName, envelope, reason) {
|
|
2596
|
+
const transport = getSourceTransport(receipt) ?? this.primary;
|
|
2597
|
+
if (transport.sendToDeadLetter) {
|
|
2598
|
+
return transport.sendToDeadLetter(receipt, dlqName, envelope, reason);
|
|
2599
|
+
}
|
|
2600
|
+
throw new Error("Transport does not support dead letter routing");
|
|
2601
|
+
}
|
|
2602
|
+
};
|
|
2603
|
+
var RabbitMQTransport = class {
|
|
2604
|
+
name = "rabbitmq";
|
|
2605
|
+
_capabilities = {
|
|
2606
|
+
deliveryModes: ["at-least-once"],
|
|
2607
|
+
delayedMessages: false,
|
|
2608
|
+
deadLetterRouting: "native",
|
|
2609
|
+
attemptTracking: true,
|
|
2610
|
+
concurrencyModel: "prefetch",
|
|
2611
|
+
ordering: "none",
|
|
2612
|
+
priorities: true
|
|
2613
|
+
};
|
|
2614
|
+
get capabilities() {
|
|
2615
|
+
return this._capabilities;
|
|
2616
|
+
}
|
|
2617
|
+
connection = null;
|
|
2618
|
+
publishChannel = null;
|
|
2619
|
+
connectionManager;
|
|
2620
|
+
queueChannels = /* @__PURE__ */ new Map();
|
|
2621
|
+
topology = null;
|
|
2622
|
+
codec = new RabbitMQCodec();
|
|
2623
|
+
config;
|
|
2624
|
+
logger;
|
|
2625
|
+
delayedExchangeAvailable = false;
|
|
2626
|
+
constructor(config) {
|
|
2627
|
+
this.logger = config.logger ?? consoleLogger;
|
|
2628
|
+
this.config = {
|
|
2629
|
+
url: config.url,
|
|
2630
|
+
connection: config.connection ?? {},
|
|
2631
|
+
quorumQueues: config.quorumQueues ?? true,
|
|
2632
|
+
defaultPrefetch: config.defaultPrefetch ?? 10,
|
|
2633
|
+
enableDelayedMessages: config.enableDelayedMessages ?? true
|
|
2634
|
+
};
|
|
2635
|
+
this.connectionManager = new ConnectionManager(
|
|
2636
|
+
() => this.doConnect(),
|
|
2637
|
+
() => this.doDisconnect(),
|
|
2638
|
+
this.config.connection
|
|
2639
|
+
);
|
|
2640
|
+
}
|
|
2641
|
+
async connect() {
|
|
2642
|
+
await this.connectionManager.connect();
|
|
2643
|
+
}
|
|
2644
|
+
async disconnect() {
|
|
2645
|
+
await this.connectionManager.disconnect();
|
|
2646
|
+
}
|
|
2647
|
+
isConnected() {
|
|
2648
|
+
return this.connectionManager.isConnected();
|
|
2649
|
+
}
|
|
2650
|
+
async applyTopology(topology) {
|
|
2651
|
+
this.topology = topology;
|
|
2652
|
+
if (!this.publishChannel) {
|
|
2653
|
+
throw new TransportNotConnectedError(this.name, "applyTopology");
|
|
2654
|
+
}
|
|
2655
|
+
const channel = this.publishChannel;
|
|
2656
|
+
const mainExchange = this.getMainExchangeName(topology.namespace);
|
|
2657
|
+
await channel.assertExchange(mainExchange, "direct", { durable: true });
|
|
2658
|
+
const dlxExchange = this.getDLXExchangeName(topology.namespace);
|
|
2659
|
+
if (topology.deadLetter.unhandled.enabled || topology.deadLetter.undeliverable.enabled) {
|
|
2660
|
+
await channel.assertExchange(dlxExchange, "direct", { durable: true });
|
|
2661
|
+
}
|
|
2662
|
+
if (this.config.enableDelayedMessages) {
|
|
2663
|
+
await this.setupDelayedExchange(topology.namespace);
|
|
2664
|
+
}
|
|
2665
|
+
for (const queueDef of topology.queues) {
|
|
2666
|
+
await this.assertWorkQueue(channel, topology, queueDef);
|
|
2667
|
+
}
|
|
2668
|
+
if (topology.deadLetter.unhandled.enabled) {
|
|
2669
|
+
await this.assertDeadLetterQueues(channel, topology, "unhandled");
|
|
2670
|
+
}
|
|
2671
|
+
if (topology.deadLetter.undeliverable.enabled) {
|
|
2672
|
+
await this.assertDeadLetterQueues(channel, topology, "undeliverable");
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
async send(queue, envelope, options) {
|
|
2676
|
+
if (!this.publishChannel || !this.topology) {
|
|
2677
|
+
throw new TransportNotConnectedError(this.name, "send");
|
|
2678
|
+
}
|
|
2679
|
+
const encoded = this.codec.encode(envelope);
|
|
2680
|
+
const buffer = Buffer.from(encoded.body);
|
|
2681
|
+
const publishOptions = {
|
|
2682
|
+
persistent: true,
|
|
2683
|
+
contentType: encoded.contentType,
|
|
2684
|
+
messageId: envelope.id,
|
|
2685
|
+
timestamp: Date.now(),
|
|
2686
|
+
headers: encoded.headers
|
|
2687
|
+
};
|
|
2688
|
+
if (options?.priority !== void 0) {
|
|
2689
|
+
publishOptions.priority = options.priority;
|
|
2690
|
+
}
|
|
2691
|
+
if (options?.delay !== void 0 && options.delay > 0) {
|
|
2692
|
+
if (!this.delayedExchangeAvailable) {
|
|
2693
|
+
throw new DelayedMessagesNotSupportedError(this.name);
|
|
2694
|
+
}
|
|
2695
|
+
const delayedExchange = this.getDelayedExchangeName(
|
|
2696
|
+
this.topology.namespace
|
|
2697
|
+
);
|
|
2698
|
+
publishOptions.headers = {
|
|
2699
|
+
...publishOptions.headers,
|
|
2700
|
+
"x-delay": options.delay
|
|
2701
|
+
};
|
|
2702
|
+
this.publishChannel.publish(
|
|
2703
|
+
delayedExchange,
|
|
2704
|
+
queue,
|
|
2705
|
+
buffer,
|
|
2706
|
+
publishOptions
|
|
2707
|
+
);
|
|
2708
|
+
return this.name;
|
|
2709
|
+
}
|
|
2710
|
+
if (options?.transport?.rabbitmq?.expiration !== void 0) {
|
|
2711
|
+
publishOptions.expiration = String(options.transport.rabbitmq.expiration);
|
|
2712
|
+
}
|
|
2713
|
+
if (options?.transport?.rabbitmq?.persistent !== void 0) {
|
|
2714
|
+
publishOptions.persistent = options.transport.rabbitmq.persistent;
|
|
2715
|
+
}
|
|
2716
|
+
const routingKey = options?.transport?.rabbitmq?.routingKey ?? queue;
|
|
2717
|
+
const exchange = this.getMainExchangeName(this.topology.namespace);
|
|
2718
|
+
this.publishChannel.publish(exchange, routingKey, buffer, publishOptions);
|
|
2719
|
+
return this.name;
|
|
2720
|
+
}
|
|
2721
|
+
async subscribe(queue, handler, options = {}) {
|
|
2722
|
+
if (!this.connection || !this.topology) {
|
|
2723
|
+
throw new TransportNotConnectedError(this.name, "subscribe");
|
|
2724
|
+
}
|
|
2725
|
+
const queueChannel = await this.getOrCreateQueueChannel(queue, options);
|
|
2726
|
+
const { channel } = queueChannel;
|
|
2727
|
+
const consumer = {
|
|
2728
|
+
consumerTag: "",
|
|
2729
|
+
queue,
|
|
2730
|
+
active: true
|
|
2731
|
+
};
|
|
2732
|
+
const { consumerTag } = await channel.consume(
|
|
2733
|
+
queue,
|
|
2734
|
+
async (msg) => {
|
|
2735
|
+
if (!msg || !consumer.active) return;
|
|
2736
|
+
const attemptNumber = this.getAttemptNumber(msg);
|
|
2737
|
+
const receipt = {
|
|
2738
|
+
handle: { channel, msg },
|
|
2739
|
+
redelivered: msg.fields.redelivered,
|
|
2740
|
+
attemptNumber,
|
|
2741
|
+
deliveryCount: this.getDeliveryCount(msg, attemptNumber),
|
|
2742
|
+
sourceQueue: queue,
|
|
2743
|
+
sourceTransport: this.name
|
|
2744
|
+
};
|
|
2745
|
+
try {
|
|
2746
|
+
const headers = msg.properties.headers ?? {};
|
|
2747
|
+
const envelope = this.codec.decode(
|
|
2748
|
+
new Uint8Array(msg.content),
|
|
2749
|
+
headers
|
|
2750
|
+
);
|
|
2751
|
+
await handler(envelope, receipt);
|
|
2752
|
+
} catch (error) {
|
|
2753
|
+
this.logger.error(
|
|
2754
|
+
"[Matador] \u{1F534} Handler error in message processing",
|
|
2755
|
+
error
|
|
2756
|
+
);
|
|
2757
|
+
}
|
|
2758
|
+
},
|
|
2759
|
+
{ noAck: false }
|
|
2760
|
+
// Always manually ack
|
|
2761
|
+
);
|
|
2762
|
+
consumer.consumerTag = consumerTag;
|
|
2763
|
+
queueChannel.consumers.push(consumer);
|
|
2764
|
+
return {
|
|
2765
|
+
unsubscribe: async () => {
|
|
2766
|
+
consumer.active = false;
|
|
2767
|
+
try {
|
|
2768
|
+
await channel.cancel(consumerTag);
|
|
2769
|
+
} catch {
|
|
2770
|
+
}
|
|
2771
|
+
const idx = queueChannel.consumers.indexOf(consumer);
|
|
2772
|
+
if (idx !== -1) {
|
|
2773
|
+
queueChannel.consumers.splice(idx, 1);
|
|
2774
|
+
}
|
|
2775
|
+
if (queueChannel.consumers.length === 0) {
|
|
2776
|
+
try {
|
|
2777
|
+
await channel.close();
|
|
2778
|
+
} catch {
|
|
2779
|
+
}
|
|
2780
|
+
this.queueChannels.delete(queue);
|
|
2781
|
+
}
|
|
2782
|
+
},
|
|
2783
|
+
get isActive() {
|
|
2784
|
+
return consumer.active;
|
|
2785
|
+
}
|
|
2786
|
+
};
|
|
2787
|
+
}
|
|
2788
|
+
async complete(receipt) {
|
|
2789
|
+
const { channel, msg } = receipt.handle;
|
|
2790
|
+
try {
|
|
2791
|
+
channel.ack(msg);
|
|
2792
|
+
} catch {
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
async sendToDeadLetter(receipt, dlqName, envelope, reason) {
|
|
2796
|
+
if (!this.publishChannel || !this.topology) {
|
|
2797
|
+
throw new TransportNotConnectedError(this.name, "sendToDeadLetter");
|
|
2798
|
+
}
|
|
2799
|
+
const dlqEnvelope = {
|
|
2800
|
+
...envelope,
|
|
2801
|
+
docket: {
|
|
2802
|
+
...envelope.docket,
|
|
2803
|
+
lastError: reason,
|
|
2804
|
+
firstError: envelope.docket.firstError ?? reason,
|
|
2805
|
+
originalQueue: receipt.sourceQueue
|
|
2806
|
+
}
|
|
2807
|
+
};
|
|
2808
|
+
const encoded = this.codec.encode(dlqEnvelope);
|
|
2809
|
+
const buffer = Buffer.from(encoded.body);
|
|
2810
|
+
const dlxExchange = this.getDLXExchangeName(this.topology.namespace);
|
|
2811
|
+
const dlqQueueName = `${receipt.sourceQueue}.${dlqName}`;
|
|
2812
|
+
const publishOptions = {
|
|
2813
|
+
persistent: true,
|
|
2814
|
+
contentType: encoded.contentType,
|
|
2815
|
+
messageId: envelope.id,
|
|
2816
|
+
timestamp: Date.now(),
|
|
2817
|
+
headers: {
|
|
2818
|
+
...encoded.headers,
|
|
2819
|
+
"x-matador-dead-letter-reason": reason
|
|
2820
|
+
}
|
|
2821
|
+
};
|
|
2822
|
+
this.publishChannel.publish(
|
|
2823
|
+
dlxExchange,
|
|
2824
|
+
dlqQueueName,
|
|
2825
|
+
buffer,
|
|
2826
|
+
publishOptions
|
|
2827
|
+
);
|
|
2828
|
+
await this.complete(receipt);
|
|
2829
|
+
}
|
|
2830
|
+
// Private methods
|
|
2831
|
+
/**
|
|
2832
|
+
* Gets or creates a dedicated channel for a queue subscription.
|
|
2833
|
+
*
|
|
2834
|
+
* We create separate channels per subscribed queue to enable independent
|
|
2835
|
+
* prefetch/concurrency control.
|
|
2836
|
+
*/
|
|
2837
|
+
async getOrCreateQueueChannel(queue, options) {
|
|
2838
|
+
const existing = this.queueChannels.get(queue);
|
|
2839
|
+
if (existing) {
|
|
2840
|
+
return existing;
|
|
2841
|
+
}
|
|
2842
|
+
if (!this.connection) {
|
|
2843
|
+
throw new TransportNotConnectedError(
|
|
2844
|
+
this.name,
|
|
2845
|
+
"getOrCreateQueueChannel"
|
|
2846
|
+
);
|
|
2847
|
+
}
|
|
2848
|
+
const channel = await this.connection.createChannel();
|
|
2849
|
+
const prefetch = options.transport?.rabbitmq?.prefetch ?? options.concurrency ?? this.config.defaultPrefetch ?? 10;
|
|
2850
|
+
await channel.prefetch(prefetch);
|
|
2851
|
+
const queueChannel = {
|
|
2852
|
+
channel,
|
|
2853
|
+
consumers: []
|
|
2854
|
+
};
|
|
2855
|
+
this.queueChannels.set(queue, queueChannel);
|
|
2856
|
+
return queueChannel;
|
|
2857
|
+
}
|
|
2858
|
+
async doConnect() {
|
|
2859
|
+
const connection = await amqplib__default.default.connect(this.config.url);
|
|
2860
|
+
this.connection = connection;
|
|
2861
|
+
connection.on("error", (err) => {
|
|
2862
|
+
this.logger.error("[Matador] \u{1F534} RabbitMQ connection error", err);
|
|
2863
|
+
});
|
|
2864
|
+
connection.on("close", () => {
|
|
2865
|
+
if (this.connectionManager.isConnected()) {
|
|
2866
|
+
this.connectionManager.handleConnectionLost(
|
|
2867
|
+
new Error("Connection closed unexpectedly")
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
2870
|
+
});
|
|
2871
|
+
this.publishChannel = await connection.createChannel();
|
|
2872
|
+
this.publishChannel.on("error", (err) => {
|
|
2873
|
+
this.logger.error("[Matador] \u{1F534} RabbitMQ publish channel error", err);
|
|
2874
|
+
});
|
|
2875
|
+
if (this.topology) {
|
|
2876
|
+
await this.applyTopology(this.topology);
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
async doDisconnect() {
|
|
2880
|
+
for (const queueChannel of this.queueChannels.values()) {
|
|
2881
|
+
for (const consumer of queueChannel.consumers) {
|
|
2882
|
+
consumer.active = false;
|
|
2883
|
+
try {
|
|
2884
|
+
await queueChannel.channel.cancel(consumer.consumerTag);
|
|
2885
|
+
} catch {
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
try {
|
|
2889
|
+
await queueChannel.channel.close();
|
|
2890
|
+
} catch {
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
this.queueChannels.clear();
|
|
2894
|
+
if (this.publishChannel) {
|
|
2895
|
+
try {
|
|
2896
|
+
await this.publishChannel.close();
|
|
2897
|
+
} catch {
|
|
2898
|
+
}
|
|
2899
|
+
this.publishChannel = null;
|
|
2900
|
+
}
|
|
2901
|
+
if (this.connection) {
|
|
2902
|
+
try {
|
|
2903
|
+
await this.connection.close();
|
|
2904
|
+
} catch {
|
|
2905
|
+
}
|
|
2906
|
+
this.connection = null;
|
|
2907
|
+
}
|
|
2908
|
+
this.delayedExchangeAvailable = false;
|
|
2909
|
+
this._capabilities = {
|
|
2910
|
+
...this._capabilities,
|
|
2911
|
+
delayedMessages: false
|
|
2912
|
+
};
|
|
2913
|
+
}
|
|
2914
|
+
async setupDelayedExchange(namespace) {
|
|
2915
|
+
if (!this.connection) {
|
|
2916
|
+
return;
|
|
2917
|
+
}
|
|
2918
|
+
this.delayedExchangeAvailable = false;
|
|
2919
|
+
const delayedExchange = this.getDelayedExchangeName(namespace);
|
|
2920
|
+
const connection = this.connection;
|
|
2921
|
+
return new Promise((resolve) => {
|
|
2922
|
+
let resolved = false;
|
|
2923
|
+
const safeResolve = () => {
|
|
2924
|
+
if (!resolved) {
|
|
2925
|
+
resolved = true;
|
|
2926
|
+
resolve();
|
|
2927
|
+
}
|
|
2928
|
+
};
|
|
2929
|
+
connection.createChannel().then((probeChannel) => {
|
|
2930
|
+
probeChannel.on("error", () => {
|
|
2931
|
+
safeResolve();
|
|
2932
|
+
});
|
|
2933
|
+
probeChannel.on("close", () => {
|
|
2934
|
+
safeResolve();
|
|
2935
|
+
});
|
|
2936
|
+
probeChannel.assertExchange(delayedExchange, "x-delayed-message", {
|
|
2937
|
+
durable: true,
|
|
2938
|
+
arguments: { "x-delayed-type": "direct" }
|
|
2939
|
+
}).then(() => {
|
|
2940
|
+
this.delayedExchangeAvailable = true;
|
|
2941
|
+
this._capabilities = {
|
|
2942
|
+
...this._capabilities,
|
|
2943
|
+
delayedMessages: true
|
|
2944
|
+
};
|
|
2945
|
+
this.logger.debug(
|
|
2946
|
+
"[Matador] \u{1F50C} Delayed message exchange plugin detected"
|
|
2947
|
+
);
|
|
2948
|
+
probeChannel.close().catch(() => {
|
|
2949
|
+
});
|
|
2950
|
+
safeResolve();
|
|
2951
|
+
}).catch(() => {
|
|
2952
|
+
this.logger.warn(
|
|
2953
|
+
"[Matador] \u{1F7E1} RabbitMQ delayed message exchange plugin not available. Delayed messages will not be supported."
|
|
2954
|
+
);
|
|
2955
|
+
safeResolve();
|
|
2956
|
+
});
|
|
2957
|
+
}).catch(() => {
|
|
2958
|
+
safeResolve();
|
|
2959
|
+
});
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2962
|
+
async assertWorkQueue(channel, topology, queueDef) {
|
|
2963
|
+
const queueName = queueDef.exact ? queueDef.name : `${topology.namespace}.${queueDef.name}`;
|
|
2964
|
+
const queueOptions = {
|
|
2965
|
+
durable: true,
|
|
2966
|
+
arguments: {}
|
|
2967
|
+
};
|
|
2968
|
+
if (this.config.quorumQueues && !queueDef.exact) {
|
|
2969
|
+
queueOptions.arguments["x-queue-type"] = "quorum";
|
|
2970
|
+
}
|
|
2971
|
+
const dlxExchange = this.getDLXExchangeName(topology.namespace);
|
|
2972
|
+
if (topology.deadLetter.unhandled.enabled || topology.deadLetter.undeliverable.enabled) {
|
|
2973
|
+
queueOptions.arguments["x-dead-letter-exchange"] = dlxExchange;
|
|
2974
|
+
}
|
|
2975
|
+
if (queueDef.priorities) {
|
|
2976
|
+
queueOptions.arguments["x-max-priority"] = 10;
|
|
2977
|
+
}
|
|
2978
|
+
if (queueDef.consumerTimeout) {
|
|
2979
|
+
queueOptions.arguments["x-consumer-timeout"] = queueDef.consumerTimeout;
|
|
2980
|
+
}
|
|
2981
|
+
await channel.assertQueue(queueName, queueOptions);
|
|
2982
|
+
const mainExchange = this.getMainExchangeName(topology.namespace);
|
|
2983
|
+
await channel.bindQueue(queueName, mainExchange, queueName);
|
|
2984
|
+
if (this.delayedExchangeAvailable) {
|
|
2985
|
+
const delayedExchange = this.getDelayedExchangeName(topology.namespace);
|
|
2986
|
+
await channel.bindQueue(queueName, delayedExchange, queueName);
|
|
2987
|
+
}
|
|
2988
|
+
if (topology.retry.enabled) {
|
|
2989
|
+
await this.assertRetryQueue(channel, topology, queueName);
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
async assertRetryQueue(channel, topology, workQueueName) {
|
|
2993
|
+
const retryQueueName = `${workQueueName}.retry`;
|
|
2994
|
+
const mainExchange = this.getMainExchangeName(topology.namespace);
|
|
2995
|
+
const retryQueueOptions = {
|
|
2996
|
+
durable: true,
|
|
2997
|
+
arguments: {
|
|
2998
|
+
"x-dead-letter-exchange": mainExchange,
|
|
2999
|
+
"x-dead-letter-routing-key": workQueueName,
|
|
3000
|
+
"x-message-ttl": topology.retry.defaultDelayMs
|
|
3001
|
+
}
|
|
3002
|
+
};
|
|
3003
|
+
if (this.config.quorumQueues) {
|
|
3004
|
+
retryQueueOptions.arguments["x-queue-type"] = "quorum";
|
|
3005
|
+
}
|
|
3006
|
+
await channel.assertQueue(retryQueueName, retryQueueOptions);
|
|
3007
|
+
await channel.bindQueue(retryQueueName, mainExchange, retryQueueName);
|
|
3008
|
+
}
|
|
3009
|
+
async assertDeadLetterQueues(channel, topology, dlqType) {
|
|
3010
|
+
const dlxExchange = this.getDLXExchangeName(topology.namespace);
|
|
3011
|
+
const dlConfig = topology.deadLetter[dlqType];
|
|
3012
|
+
for (const queueDef of topology.queues) {
|
|
3013
|
+
if (queueDef.exact) continue;
|
|
3014
|
+
const workQueueName = `${topology.namespace}.${queueDef.name}`;
|
|
3015
|
+
const dlqName = `${workQueueName}.${dlqType}`;
|
|
3016
|
+
const dlqOptions = {
|
|
3017
|
+
durable: true,
|
|
3018
|
+
arguments: {}
|
|
3019
|
+
};
|
|
3020
|
+
if (dlConfig.maxLength) {
|
|
3021
|
+
dlqOptions.arguments["x-max-length"] = dlConfig.maxLength;
|
|
3022
|
+
}
|
|
3023
|
+
await channel.assertQueue(dlqName, dlqOptions);
|
|
3024
|
+
await channel.bindQueue(dlqName, dlxExchange, dlqName);
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
getMainExchangeName(namespace) {
|
|
3028
|
+
return `${namespace}.exchange`;
|
|
3029
|
+
}
|
|
3030
|
+
getDLXExchangeName(namespace) {
|
|
3031
|
+
return `${namespace}.dlx`;
|
|
3032
|
+
}
|
|
3033
|
+
getDelayedExchangeName(namespace) {
|
|
3034
|
+
return `${namespace}.delayed`;
|
|
3035
|
+
}
|
|
3036
|
+
getAttemptNumber(msg) {
|
|
3037
|
+
const headerValue = msg.properties.headers?.["x-matador-attempts"];
|
|
3038
|
+
if (typeof headerValue === "number") {
|
|
3039
|
+
return headerValue;
|
|
3040
|
+
}
|
|
3041
|
+
const xDeath = msg.properties.headers?.["x-death"];
|
|
3042
|
+
if (Array.isArray(xDeath) && xDeath.length > 0) {
|
|
3043
|
+
const deathCount = xDeath.reduce(
|
|
3044
|
+
(sum, death) => sum + (death.count ?? 0),
|
|
3045
|
+
0
|
|
3046
|
+
);
|
|
3047
|
+
return deathCount + 1;
|
|
3048
|
+
}
|
|
3049
|
+
return 1;
|
|
3050
|
+
}
|
|
3051
|
+
/**
|
|
3052
|
+
* Gets the native delivery count for poison message detection.
|
|
3053
|
+
* This tracks how many times the message was delivered without acknowledgment,
|
|
3054
|
+
* which helps detect crash loops.
|
|
3055
|
+
*/
|
|
3056
|
+
getDeliveryCount(msg, attemptNumber) {
|
|
3057
|
+
const deliveryCount = msg.properties.headers?.["x-delivery-count"];
|
|
3058
|
+
if (typeof deliveryCount === "number") {
|
|
3059
|
+
return deliveryCount;
|
|
3060
|
+
}
|
|
3061
|
+
const xDeath = msg.properties.headers?.["x-death"];
|
|
3062
|
+
if (Array.isArray(xDeath) && xDeath.length > 0) {
|
|
3063
|
+
const deathCount = xDeath.reduce(
|
|
3064
|
+
(sum, death) => sum + (death.count ?? 0),
|
|
3065
|
+
0
|
|
3066
|
+
);
|
|
3067
|
+
return deathCount + 1;
|
|
3068
|
+
}
|
|
3069
|
+
if (msg.fields.redelivered) {
|
|
3070
|
+
return Math.max(2, attemptNumber);
|
|
3071
|
+
}
|
|
3072
|
+
return attemptNumber;
|
|
3073
|
+
}
|
|
3074
|
+
};
|
|
3075
|
+
|
|
3076
|
+
exports.AllTransportsFailedError = AllTransportsFailedError;
|
|
3077
|
+
exports.CheckpointStoreError = CheckpointStoreError;
|
|
3078
|
+
exports.CodecDecodeError = CodecDecodeError;
|
|
3079
|
+
exports.ConnectionManager = ConnectionManager;
|
|
3080
|
+
exports.DelayedMessagesNotSupportedError = DelayedMessagesNotSupportedError;
|
|
3081
|
+
exports.DoRetry = DoRetry;
|
|
3082
|
+
exports.DontRetry = DontRetry;
|
|
3083
|
+
exports.DuplicateIoKeyError = DuplicateIoKeyError;
|
|
3084
|
+
exports.EventAssertionError = EventAssertionError;
|
|
3085
|
+
exports.EventNotRegisteredError = EventNotRegisteredError;
|
|
3086
|
+
exports.FanoutEngine = FanoutEngine;
|
|
3087
|
+
exports.IdempotentMessageCannotRetryError = IdempotentMessageCannotRetryError;
|
|
3088
|
+
exports.InvalidEventError = InvalidEventError;
|
|
3089
|
+
exports.InvalidSchemaError = InvalidSchemaError;
|
|
3090
|
+
exports.JsonCodec = JsonCodec;
|
|
3091
|
+
exports.LocalTransport = LocalTransport;
|
|
3092
|
+
exports.LocalTransportCannotProcessStubError = LocalTransportCannotProcessStubError;
|
|
3093
|
+
exports.Matador = Matador;
|
|
3094
|
+
exports.MatadorError = MatadorError;
|
|
3095
|
+
exports.MatadorEvent = MatadorEvent;
|
|
3096
|
+
exports.MemoryCheckpointStore = MemoryCheckpointStore;
|
|
3097
|
+
exports.MessageMaybePoisonedError = MessageMaybePoisonedError;
|
|
3098
|
+
exports.MultiTransport = MultiTransport;
|
|
3099
|
+
exports.NoOpCheckpointStore = NoOpCheckpointStore;
|
|
3100
|
+
exports.NoSubscribersExistError = NoSubscribersExistError;
|
|
3101
|
+
exports.NotStartedError = NotStartedError;
|
|
3102
|
+
exports.ProcessingPipeline = ProcessingPipeline;
|
|
3103
|
+
exports.QueueNotFoundError = QueueNotFoundError;
|
|
3104
|
+
exports.RabbitMQCodec = RabbitMQCodec;
|
|
3105
|
+
exports.RabbitMQTransport = RabbitMQTransport;
|
|
3106
|
+
exports.ResumableContext = ResumableContext;
|
|
3107
|
+
exports.RetryControlError = RetryControlError;
|
|
3108
|
+
exports.SafeHooks = SafeHooks;
|
|
3109
|
+
exports.SchemaError = SchemaError;
|
|
3110
|
+
exports.SchemaRegistry = SchemaRegistry;
|
|
3111
|
+
exports.ShutdownInProgressError = ShutdownInProgressError;
|
|
3112
|
+
exports.ShutdownManager = ShutdownManager;
|
|
3113
|
+
exports.StandardRetryPolicy = StandardRetryPolicy;
|
|
3114
|
+
exports.SubscriberIsStubError = SubscriberIsStubError;
|
|
3115
|
+
exports.SubscriberNotRegisteredError = SubscriberNotRegisteredError;
|
|
3116
|
+
exports.TimeoutError = TimeoutError;
|
|
3117
|
+
exports.TopologyBuilder = TopologyBuilder;
|
|
3118
|
+
exports.TopologyValidationError = TopologyValidationError;
|
|
3119
|
+
exports.TransportClosedError = TransportClosedError;
|
|
3120
|
+
exports.TransportNotConnectedError = TransportNotConnectedError;
|
|
3121
|
+
exports.TransportSendError = TransportSendError;
|
|
3122
|
+
exports.bind = bind;
|
|
3123
|
+
exports.consoleLogger = consoleLogger;
|
|
3124
|
+
exports.createEnvelope = createEnvelope;
|
|
3125
|
+
exports.createSubscriber = createSubscriber;
|
|
3126
|
+
exports.createSubscriberStub = createSubscriberStub;
|
|
3127
|
+
exports.defaultConnectionConfig = defaultConnectionConfig;
|
|
3128
|
+
exports.defaultRetryConfig = defaultRetryConfig;
|
|
3129
|
+
exports.defaultShutdownConfig = defaultShutdownConfig;
|
|
3130
|
+
exports.getDeadLetterQueueName = getDeadLetterQueueName;
|
|
3131
|
+
exports.getQualifiedQueueName = getQualifiedQueueName;
|
|
3132
|
+
exports.getRetryQueueName = getRetryQueueName;
|
|
3133
|
+
exports.hasDescription = hasDescription;
|
|
3134
|
+
exports.hasNativeDeadLetter = hasNativeDeadLetter;
|
|
3135
|
+
exports.installPlugins = installPlugins;
|
|
3136
|
+
exports.invalidResult = invalidResult;
|
|
3137
|
+
exports.isAssertionError = isAssertionError;
|
|
3138
|
+
exports.isCheckpointStoreError = isCheckpointStoreError;
|
|
3139
|
+
exports.isDoRetry = isDoRetry;
|
|
3140
|
+
exports.isDontRetry = isDontRetry;
|
|
3141
|
+
exports.isDuplicateIoKeyError = isDuplicateIoKeyError;
|
|
3142
|
+
exports.isEventNotRegisteredError = isEventNotRegisteredError;
|
|
3143
|
+
exports.isIdempotentMessageCannotRetryError = isIdempotentMessageCannotRetryError;
|
|
3144
|
+
exports.isMatadorError = isMatadorError;
|
|
3145
|
+
exports.isMessageMaybePoisonedError = isMessageMaybePoisonedError;
|
|
3146
|
+
exports.isNotStartedError = isNotStartedError;
|
|
3147
|
+
exports.isSchemaEntryTuple = isSchemaEntryTuple;
|
|
3148
|
+
exports.isSubscriber = isSubscriber;
|
|
3149
|
+
exports.isSubscriberNotRegisteredError = isSubscriberNotRegisteredError;
|
|
3150
|
+
exports.isSubscriberStub = isSubscriberStub;
|
|
3151
|
+
exports.isTransportNotConnectedError = isTransportNotConnectedError;
|
|
3152
|
+
exports.supportsDelayedMessages = supportsDelayedMessages;
|
|
3153
|
+
exports.supportsDeliveryMode = supportsDeliveryMode;
|
|
3154
|
+
exports.validResult = validResult;
|
|
3155
|
+
//# sourceMappingURL=index.cjs.map
|
|
3156
|
+
//# sourceMappingURL=index.cjs.map
|