@zdavison/matador 4.0.2 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/fanout.d.ts +51 -0
- package/dist/core/fanout.d.ts.map +1 -1
- package/dist/core/fanout.js +141 -28
- package/dist/core/fanout.test.js +533 -7
- package/dist/core/matador.d.ts +33 -0
- package/dist/core/matador.d.ts.map +1 -1
- package/dist/core/matador.js +9 -2
- package/dist/core/matador.test.js +336 -7
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +2 -2
- package/dist/errors/matador-errors.d.ts +10 -8
- package/dist/errors/matador-errors.d.ts.map +1 -1
- package/dist/errors/matador-errors.js +18 -14
- package/dist/index.cjs +326 -92
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/pipeline/pipeline.d.ts +30 -2
- package/dist/pipeline/pipeline.d.ts.map +1 -1
- package/dist/pipeline/pipeline.js +61 -9
- package/dist/pipeline/pipeline.test.js +123 -0
- package/dist/retry/index.d.ts +1 -1
- package/dist/retry/index.d.ts.map +1 -1
- package/dist/retry/policy.d.ts +25 -0
- package/dist/retry/policy.d.ts.map +1 -1
- package/dist/retry/standard-policy.d.ts +19 -1
- package/dist/retry/standard-policy.d.ts.map +1 -1
- package/dist/retry/standard-policy.js +39 -2
- package/dist/retry/standard-policy.test.js +65 -0
- package/dist/topology/builder.test.js +7 -0
- package/dist/topology/types.d.ts +14 -0
- package/dist/topology/types.d.ts.map +1 -1
- package/dist/transport/local/local-transport.d.ts +6 -0
- package/dist/transport/local/local-transport.d.ts.map +1 -1
- package/dist/transport/local/local-transport.js +35 -18
- package/dist/transport/local/local-transport.test.js +40 -4
- package/dist/transport/multi/multi-transport.d.ts.map +1 -1
- package/dist/transport/multi/multi-transport.js +3 -0
- package/dist/transport/multi/multi-transport.test.js +82 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -1
- package/dist/transport/rabbitmq/rabbitmq-transport.js +34 -27
- package/dist/transport/rabbitmq/rabbitmq-transport.test.js +28 -0
- package/dist/transport/transport.d.ts +6 -0
- package/dist/transport/transport.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -202,6 +202,16 @@ var DelayedMessagesNotSupportedError = class extends MatadorError {
|
|
|
202
202
|
transportName;
|
|
203
203
|
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.";
|
|
204
204
|
};
|
|
205
|
+
var LocalTransportNoActiveSubscriberError = class extends MatadorError {
|
|
206
|
+
constructor(queue) {
|
|
207
|
+
super(
|
|
208
|
+
`LocalTransport has no active subscriber for queue "${queue}"; message cannot be delivered.`
|
|
209
|
+
);
|
|
210
|
+
this.queue = queue;
|
|
211
|
+
}
|
|
212
|
+
queue;
|
|
213
|
+
description = "The LocalTransport has no active subscriber for this queue in the current process, so it cannot guarantee delivery. ACTION: If the subscriber is a SubscriberStub (implemented in another service), the local transport can never deliver it locally \u2014 a distributed transport like RabbitMQ is required. If the subscriber should run in this process, ensure matador.start() has subscribed to this queue before sending.";
|
|
214
|
+
};
|
|
205
215
|
var EventNotRegisteredError = class extends MatadorError {
|
|
206
216
|
constructor(eventKey) {
|
|
207
217
|
super(
|
|
@@ -252,16 +262,6 @@ var SubscriberIsStubError = class extends MatadorError {
|
|
|
252
262
|
subscriberName;
|
|
253
263
|
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.";
|
|
254
264
|
};
|
|
255
|
-
var LocalTransportCannotProcessStubError = class extends MatadorError {
|
|
256
|
-
constructor(subscriberName) {
|
|
257
|
-
super(
|
|
258
|
-
`LocalTransport cannot process stub subscriber "${subscriberName}". Stub subscribers require a distributed transport like RabbitMQ.`
|
|
259
|
-
);
|
|
260
|
-
this.subscriberName = subscriberName;
|
|
261
|
-
}
|
|
262
|
-
subscriberName;
|
|
263
|
-
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.";
|
|
264
|
-
};
|
|
265
265
|
var UnknownQueueReferenceError = class extends MatadorError {
|
|
266
266
|
constructor(queueName) {
|
|
267
267
|
super(
|
|
@@ -801,6 +801,7 @@ function createSubscriberStub(input) {
|
|
|
801
801
|
}
|
|
802
802
|
|
|
803
803
|
// src/core/fanout.ts
|
|
804
|
+
var DEFAULT_MAX_CONSECUTIVE_FLUSH_FAILURES = 10;
|
|
804
805
|
var FanoutEngine = class {
|
|
805
806
|
transport;
|
|
806
807
|
schema;
|
|
@@ -808,9 +809,13 @@ var FanoutEngine = class {
|
|
|
808
809
|
topology;
|
|
809
810
|
defaultQueue;
|
|
810
811
|
enqueuingCount = 0;
|
|
812
|
+
flushInFlightCount = 0;
|
|
811
813
|
retryBuffer = [];
|
|
812
814
|
maxRetryBufferSize;
|
|
815
|
+
maxRetryAttempts;
|
|
816
|
+
maxConsecutiveFlushFailures;
|
|
813
817
|
disposeOnConnected;
|
|
818
|
+
retryTimer;
|
|
814
819
|
constructor(config) {
|
|
815
820
|
this.transport = config.transport;
|
|
816
821
|
this.schema = config.schema;
|
|
@@ -818,18 +823,29 @@ var FanoutEngine = class {
|
|
|
818
823
|
this.topology = config.topology;
|
|
819
824
|
this.defaultQueue = config.defaultQueue;
|
|
820
825
|
this.maxRetryBufferSize = config.maxRetryBufferSize ?? 5e3;
|
|
826
|
+
this.maxRetryAttempts = config.maxRetryAttempts;
|
|
827
|
+
const maxConsecutiveFlushFailures = config.maxConsecutiveFlushFailures ?? DEFAULT_MAX_CONSECUTIVE_FLUSH_FAILURES;
|
|
828
|
+
this.maxConsecutiveFlushFailures = maxConsecutiveFlushFailures > 0 ? maxConsecutiveFlushFailures : Number.POSITIVE_INFINITY;
|
|
821
829
|
this.disposeOnConnected = this.transport.onConnected?.(() => {
|
|
822
830
|
void this.flushRetryBuffer();
|
|
823
831
|
});
|
|
832
|
+
const retryIntervalMs = config.retryIntervalMs ?? 3e4;
|
|
833
|
+
if (retryIntervalMs > 0) {
|
|
834
|
+
this.retryTimer = setInterval(() => {
|
|
835
|
+
void this.flushRetryBuffer();
|
|
836
|
+
}, retryIntervalMs);
|
|
837
|
+
this.retryTimer.unref?.();
|
|
838
|
+
}
|
|
824
839
|
}
|
|
825
840
|
dispose() {
|
|
826
841
|
this.disposeOnConnected?.();
|
|
842
|
+
clearInterval(this.retryTimer);
|
|
827
843
|
}
|
|
828
844
|
/**
|
|
829
845
|
* Current count of events being enqueued.
|
|
830
846
|
*/
|
|
831
847
|
get eventsBeingEnqueuedCount() {
|
|
832
|
-
return this.enqueuingCount;
|
|
848
|
+
return this.enqueuingCount + this.flushInFlightCount;
|
|
833
849
|
}
|
|
834
850
|
/**
|
|
835
851
|
* Sends an event to all registered subscribers.
|
|
@@ -883,10 +899,17 @@ var FanoutEngine = class {
|
|
|
883
899
|
queue: qualifiedQueue,
|
|
884
900
|
envelope,
|
|
885
901
|
sendOptions,
|
|
886
|
-
subscriberName: subscriber.name
|
|
902
|
+
subscriberName: subscriber.name,
|
|
903
|
+
attempts: 0
|
|
887
904
|
});
|
|
888
905
|
this.hooks.logger.warn(
|
|
889
|
-
`[Matador] \u{1F7E1} Message for '${subscriber.name}' buffered for retry on reconnect
|
|
906
|
+
`[Matador] \u{1F7E1} Message for '${subscriber.name}' buffered for retry on reconnect.`,
|
|
907
|
+
{
|
|
908
|
+
queue: qualifiedQueue,
|
|
909
|
+
subscriberName: subscriber.name,
|
|
910
|
+
bufferSize: this.retryBuffer.length,
|
|
911
|
+
maxBufferSize: this.maxRetryBufferSize
|
|
912
|
+
}
|
|
890
913
|
);
|
|
891
914
|
if (options.throwOnBufferedFailure) {
|
|
892
915
|
const err = new TransportSendError(qualifiedQueue, cause);
|
|
@@ -930,6 +953,31 @@ var FanoutEngine = class {
|
|
|
930
953
|
errors
|
|
931
954
|
};
|
|
932
955
|
}
|
|
956
|
+
/**
|
|
957
|
+
* Attempts a single buffered item during a flush pass
|
|
958
|
+
* @returns true if the attempt succeeded; false on failure
|
|
959
|
+
*/
|
|
960
|
+
async attemptFlushItem(item) {
|
|
961
|
+
this.enqueuingCount++;
|
|
962
|
+
try {
|
|
963
|
+
const usedTransport = await this.transport.send(
|
|
964
|
+
item.queue,
|
|
965
|
+
item.envelope,
|
|
966
|
+
item.sendOptions
|
|
967
|
+
);
|
|
968
|
+
await this.hooks.onEnqueueSuccess({
|
|
969
|
+
envelope: item.envelope,
|
|
970
|
+
queue: item.queue,
|
|
971
|
+
transport: usedTransport
|
|
972
|
+
});
|
|
973
|
+
return true;
|
|
974
|
+
} catch (error) {
|
|
975
|
+
await this.handleFlushFailure(item, error);
|
|
976
|
+
return false;
|
|
977
|
+
} finally {
|
|
978
|
+
this.enqueuingCount--;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
933
981
|
/**
|
|
934
982
|
* Flushes the retry buffer
|
|
935
983
|
*
|
|
@@ -941,39 +989,36 @@ var FanoutEngine = class {
|
|
|
941
989
|
`[Matador] \u23F3 Flushing ${this.retryBuffer.length} buffered message(s)...`
|
|
942
990
|
);
|
|
943
991
|
const toFlush = this.retryBuffer.splice(0);
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
if (
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
error: err,
|
|
966
|
-
transport: this.transport.name
|
|
967
|
-
});
|
|
992
|
+
this.flushInFlightCount += toFlush.length;
|
|
993
|
+
let consecutiveFailures = 0;
|
|
994
|
+
try {
|
|
995
|
+
for (let i = 0; i < toFlush.length; i++) {
|
|
996
|
+
const item = toFlush[i];
|
|
997
|
+
if (!item) {
|
|
998
|
+
this.flushInFlightCount--;
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
const succeeded = await this.attemptFlushItem(item);
|
|
1002
|
+
this.flushInFlightCount--;
|
|
1003
|
+
const randomMs = Math.random() * 10;
|
|
1004
|
+
await new Promise((resolve) => setTimeout(resolve, randomMs));
|
|
1005
|
+
consecutiveFailures = succeeded ? 0 : consecutiveFailures + 1;
|
|
1006
|
+
if (consecutiveFailures >= this.maxConsecutiveFlushFailures) {
|
|
1007
|
+
const untried = toFlush.slice(i + 1);
|
|
1008
|
+
this.flushInFlightCount -= untried.length;
|
|
1009
|
+
if (untried.length > 0) {
|
|
1010
|
+
await this.rebufferUntried(untried, consecutiveFailures);
|
|
1011
|
+
}
|
|
1012
|
+
break;
|
|
968
1013
|
}
|
|
969
|
-
} finally {
|
|
970
|
-
this.enqueuingCount--;
|
|
971
1014
|
}
|
|
1015
|
+
} finally {
|
|
1016
|
+
this.flushInFlightCount = 0;
|
|
972
1017
|
}
|
|
973
1018
|
const remaining = this.retryBuffer.length;
|
|
974
1019
|
if (remaining > 0) {
|
|
975
1020
|
this.hooks.logger.warn(
|
|
976
|
-
`[Matador] \u{1F7E1} ${remaining} buffered message(s) could not be flushed; will retry
|
|
1021
|
+
`[Matador] \u{1F7E1} ${remaining} buffered message(s) could not be flushed; will retry later.`
|
|
977
1022
|
);
|
|
978
1023
|
} else {
|
|
979
1024
|
this.hooks.logger.info(
|
|
@@ -981,6 +1026,67 @@ var FanoutEngine = class {
|
|
|
981
1026
|
);
|
|
982
1027
|
}
|
|
983
1028
|
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Re-buffers messages that were never attempted because a flush pass
|
|
1031
|
+
* stopped out early. Respects maxRetryBufferSize: concurrent sends can have
|
|
1032
|
+
* refilled the buffer while this flush was running, so anything beyond
|
|
1033
|
+
* remaining capacity is dropped and reported instead of silently growing
|
|
1034
|
+
* the buffer past its cap.
|
|
1035
|
+
*/
|
|
1036
|
+
async rebufferUntried(untried, consecutiveFailures) {
|
|
1037
|
+
const capacity = Math.max(
|
|
1038
|
+
0,
|
|
1039
|
+
this.maxRetryBufferSize - this.retryBuffer.length
|
|
1040
|
+
);
|
|
1041
|
+
const toRebuffer = untried.slice(0, capacity);
|
|
1042
|
+
const dropped = untried.slice(capacity);
|
|
1043
|
+
if (toRebuffer.length > 0) {
|
|
1044
|
+
this.retryBuffer.unshift(...toRebuffer);
|
|
1045
|
+
}
|
|
1046
|
+
const droppedSuffix = dropped.length > 0 ? `, ${dropped.length} dropped (buffer full).` : ".";
|
|
1047
|
+
this.hooks.logger.warn(
|
|
1048
|
+
`[Matador] \u{1F7E1} Stopping this flush pass after ${consecutiveFailures} consecutive failures; ${toRebuffer.length} untried message(s) re-buffered for the next attempt${droppedSuffix}`
|
|
1049
|
+
);
|
|
1050
|
+
for (const item of dropped) {
|
|
1051
|
+
this.hooks.logger.error(
|
|
1052
|
+
`[Matador] \u{1F534} Retry buffer full (${this.maxRetryBufferSize}). Message for '${item.subscriberName}' dropped and will not be retried.`
|
|
1053
|
+
);
|
|
1054
|
+
const err = new TransportSendError(
|
|
1055
|
+
item.queue,
|
|
1056
|
+
new Error("Retry buffer full")
|
|
1057
|
+
);
|
|
1058
|
+
await this.hooks.onEnqueueError({
|
|
1059
|
+
envelope: item.envelope,
|
|
1060
|
+
error: err,
|
|
1061
|
+
transport: this.transport.name
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Handles a failed flush attempt for a single buffered item: drops it once
|
|
1067
|
+
* maxRetryAttempts is exceeded, otherwise re-buffers it (unless the buffer
|
|
1068
|
+
* is full, in which case it's dropped too).
|
|
1069
|
+
*/
|
|
1070
|
+
async handleFlushFailure(item, error) {
|
|
1071
|
+
item.attempts++;
|
|
1072
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
1073
|
+
const exceededAttempts = this.maxRetryAttempts !== void 0 && item.attempts >= this.maxRetryAttempts;
|
|
1074
|
+
if (!exceededAttempts && this.retryBuffer.length < this.maxRetryBufferSize) {
|
|
1075
|
+
this.retryBuffer.push(item);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
if (exceededAttempts) {
|
|
1079
|
+
this.hooks.logger.error(
|
|
1080
|
+
`[Matador] \u{1F534} Message for '${item.subscriberName}' exceeded max retry attempts (${this.maxRetryAttempts}) and will not be retried further.`
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
const err = new TransportSendError(item.queue, cause);
|
|
1084
|
+
await this.hooks.onEnqueueError({
|
|
1085
|
+
envelope: item.envelope,
|
|
1086
|
+
error: err,
|
|
1087
|
+
transport: this.transport.name
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
984
1090
|
async isSubscriberEnabled(subscriber) {
|
|
985
1091
|
if (!subscriber.enabled) {
|
|
986
1092
|
return true;
|
|
@@ -1787,10 +1893,20 @@ var ProcessingPipeline = class {
|
|
|
1787
1893
|
durationMs: performance.now() - startTime
|
|
1788
1894
|
};
|
|
1789
1895
|
}
|
|
1896
|
+
const isResumable = isResumableSubscriber(subscriber);
|
|
1897
|
+
const shouldProcessResult = await this.handleShouldProcess(
|
|
1898
|
+
envelope,
|
|
1899
|
+
subscriberDef,
|
|
1900
|
+
isResumable,
|
|
1901
|
+
receipt,
|
|
1902
|
+
startTime
|
|
1903
|
+
);
|
|
1904
|
+
if (!shouldProcessResult.success) {
|
|
1905
|
+
return shouldProcessResult;
|
|
1906
|
+
}
|
|
1790
1907
|
let result;
|
|
1791
1908
|
let error;
|
|
1792
1909
|
let context;
|
|
1793
|
-
const isResumable = isResumableSubscriber(subscriber);
|
|
1794
1910
|
let existingCheckpoint;
|
|
1795
1911
|
if (isResumable) {
|
|
1796
1912
|
existingCheckpoint = await this.checkpointStore.get(envelope.id);
|
|
@@ -1866,10 +1982,65 @@ var ProcessingPipeline = class {
|
|
|
1866
1982
|
subscriber: subscriberDef,
|
|
1867
1983
|
receipt
|
|
1868
1984
|
});
|
|
1985
|
+
return await this.handleFailure(
|
|
1986
|
+
error,
|
|
1987
|
+
decision,
|
|
1988
|
+
envelope,
|
|
1989
|
+
subscriberDef,
|
|
1990
|
+
isResumable,
|
|
1991
|
+
receipt,
|
|
1992
|
+
durationMs
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
/**
|
|
1996
|
+
* Runs the retry policy's pre-processing check.
|
|
1997
|
+
*
|
|
1998
|
+
* This returns a terminal `ProcessResult` if the message should be skipped (dead-lettered/discarded) without ever invoking the callback,
|
|
1999
|
+
* or a success result if shouldProcess passes and we should continue processing the message.
|
|
2000
|
+
* @param envelope - The message envelope
|
|
2001
|
+
* @param subscriberDef - The subscriber definition
|
|
2002
|
+
* @param isResumable - Whether the subscriber is resumable
|
|
2003
|
+
* @param receipt - The message receipt
|
|
2004
|
+
* @param startTime - The start time of the processing
|
|
2005
|
+
* @returns A `ProcessResult` indicating if we should continue with processing the message, or fail it preemptively
|
|
2006
|
+
*/
|
|
2007
|
+
async handleShouldProcess(envelope, subscriberDef, isResumable, receipt, startTime) {
|
|
2008
|
+
const decision = this.retryPolicy.shouldProcess({ envelope, receipt });
|
|
2009
|
+
if (decision.action === "process") {
|
|
2010
|
+
return {
|
|
2011
|
+
success: true,
|
|
2012
|
+
durationMs: 0
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
const durationMs = performance.now() - startTime;
|
|
2016
|
+
const error = new Error(decision.reason);
|
|
2017
|
+
return await this.handleFailure(
|
|
2018
|
+
error,
|
|
2019
|
+
decision,
|
|
2020
|
+
envelope,
|
|
2021
|
+
subscriberDef,
|
|
2022
|
+
isResumable,
|
|
2023
|
+
receipt,
|
|
2024
|
+
durationMs
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Performs failure handling and cleanup based on retry decision (e.g. dead-letter).
|
|
2029
|
+
*
|
|
2030
|
+
* @param error - The error from the processing attempt
|
|
2031
|
+
* @param decision - The decision from executing the appropriate retry policy check
|
|
2032
|
+
* @param envelope - The message envelope
|
|
2033
|
+
* @param subscriberDef - The subscriber definition
|
|
2034
|
+
* @param isResumable - Whether the subscriber is resumable
|
|
2035
|
+
* @param receipt - The message receipt
|
|
2036
|
+
* @param durationMs - The duration of processing
|
|
2037
|
+
* @returns A `ProcessResult` indicating failure
|
|
2038
|
+
*/
|
|
2039
|
+
async handleFailure(error, decision, envelope, subscriberDef, isResumable, receipt, durationMs) {
|
|
1869
2040
|
envelope.docket.lastError = error.message;
|
|
1870
2041
|
envelope.docket.firstError ??= error.message;
|
|
1871
|
-
if (decision.action === "dead-letter" &&
|
|
1872
|
-
await
|
|
2042
|
+
if (decision.action === "dead-letter" && isResumable) {
|
|
2043
|
+
await this.checkpointStore.delete(envelope.id);
|
|
1873
2044
|
await this.hooks.onCheckpointCleared?.({
|
|
1874
2045
|
envelope,
|
|
1875
2046
|
subscriber: subscriberDef,
|
|
@@ -1975,15 +2146,31 @@ var StandardRetryPolicy = class {
|
|
|
1975
2146
|
constructor(config = {}) {
|
|
1976
2147
|
this.config = { ...defaultRetryConfig, ...config };
|
|
1977
2148
|
}
|
|
2149
|
+
/**
|
|
2150
|
+
* Check if the message should be dead-lettered before the subscriber callback is invoked
|
|
2151
|
+
*
|
|
2152
|
+
* Uses the same delivery-count threshold as `shouldRetry` poison check, so an
|
|
2153
|
+
* already-poisoned message is dead-lettered without ever running the callback again.
|
|
2154
|
+
*
|
|
2155
|
+
* @param context - The shouldProcess context.
|
|
2156
|
+
* @returns A 'process' decision if the message is not poisoned; a dead-letter decision otherwise.
|
|
2157
|
+
*/
|
|
2158
|
+
shouldProcess(context) {
|
|
2159
|
+
const poisonError = this.checkPoisoned(context.envelope, context.receipt);
|
|
2160
|
+
if (poisonError) {
|
|
2161
|
+
return {
|
|
2162
|
+
action: "dead-letter",
|
|
2163
|
+
queue: "undeliverable",
|
|
2164
|
+
reason: poisonError.message
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
return { action: "process" };
|
|
2168
|
+
}
|
|
1978
2169
|
shouldRetry(context) {
|
|
1979
2170
|
const { envelope, error, subscriber, receipt } = context;
|
|
1980
2171
|
const errorMessage = error.message;
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
envelope.id,
|
|
1984
|
-
receipt.deliveryCount,
|
|
1985
|
-
this.config.maxDeliveries
|
|
1986
|
-
);
|
|
2172
|
+
const poisonError = this.checkPoisoned(envelope, receipt);
|
|
2173
|
+
if (poisonError) {
|
|
1987
2174
|
return {
|
|
1988
2175
|
action: "dead-letter",
|
|
1989
2176
|
queue: "undeliverable",
|
|
@@ -2045,6 +2232,23 @@ var StandardRetryPolicy = class {
|
|
|
2045
2232
|
const delay = this.config.baseDelay * this.config.backoffMultiplier ** (attempt - 1);
|
|
2046
2233
|
return Math.min(delay, this.config.maxDelay);
|
|
2047
2234
|
}
|
|
2235
|
+
/**
|
|
2236
|
+
* Checks the native delivery count against the poison threshold
|
|
2237
|
+
*
|
|
2238
|
+
* @param envelope - The message envelope.
|
|
2239
|
+
* @param receipt - The message receipt.
|
|
2240
|
+
* @returns A MessageMaybePoisonedError if the message is poisoned; null otherwise
|
|
2241
|
+
*/
|
|
2242
|
+
checkPoisoned(envelope, receipt) {
|
|
2243
|
+
if (receipt.deliveryCount >= this.config.maxDeliveries) {
|
|
2244
|
+
return new MessageMaybePoisonedError(
|
|
2245
|
+
envelope.id,
|
|
2246
|
+
receipt.deliveryCount,
|
|
2247
|
+
this.config.maxDeliveries
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2250
|
+
return null;
|
|
2251
|
+
}
|
|
2048
2252
|
};
|
|
2049
2253
|
|
|
2050
2254
|
// src/schema/types.ts
|
|
@@ -2273,7 +2477,11 @@ var Matador = class {
|
|
|
2273
2477
|
schema: this.schema,
|
|
2274
2478
|
hooks: this.hooks,
|
|
2275
2479
|
topology: this.topology,
|
|
2276
|
-
defaultQueue
|
|
2480
|
+
defaultQueue,
|
|
2481
|
+
maxRetryBufferSize: config.maxRetryBufferSize,
|
|
2482
|
+
maxRetryAttempts: config.maxRetryAttempts,
|
|
2483
|
+
retryIntervalMs: config.retryIntervalMs,
|
|
2484
|
+
maxConsecutiveFlushFailures: config.maxConsecutiveFlushFailures
|
|
2277
2485
|
});
|
|
2278
2486
|
this.shutdownManager = new ShutdownManager(
|
|
2279
2487
|
() => this.fanout.eventsBeingEnqueuedCount,
|
|
@@ -2351,6 +2559,7 @@ var Matador = class {
|
|
|
2351
2559
|
"[Matador] \u{1F7E1} Worker not subscribing to any queues (consumeFrom is empty)."
|
|
2352
2560
|
);
|
|
2353
2561
|
}
|
|
2562
|
+
this.started = true;
|
|
2354
2563
|
for (const queueName of this.consumeFrom) {
|
|
2355
2564
|
const qualifiedName = resolveTargetQueueName(this.topology, queueName);
|
|
2356
2565
|
const queueDef = findQueueDefinition(this.topology, queueName);
|
|
@@ -2369,7 +2578,6 @@ var Matador = class {
|
|
|
2369
2578
|
);
|
|
2370
2579
|
this.subscriptions.push(subscription);
|
|
2371
2580
|
}
|
|
2372
|
-
this.started = true;
|
|
2373
2581
|
}
|
|
2374
2582
|
async send(eventOrClass, dataOrOptions, maybeOptions) {
|
|
2375
2583
|
if (!this.started) {
|
|
@@ -2459,7 +2667,7 @@ var Matador = class {
|
|
|
2459
2667
|
}
|
|
2460
2668
|
async unsubscribeAll() {
|
|
2461
2669
|
for (const subscription of this.subscriptions) {
|
|
2462
|
-
await subscription.unsubscribe();
|
|
2670
|
+
await (subscription.pauseForShutdown ?? subscription.unsubscribe)();
|
|
2463
2671
|
}
|
|
2464
2672
|
this.subscriptions.length = 0;
|
|
2465
2673
|
}
|
|
@@ -2621,6 +2829,7 @@ var localCapabilities = {
|
|
|
2621
2829
|
ordering: "queue",
|
|
2622
2830
|
priorities: false
|
|
2623
2831
|
};
|
|
2832
|
+
var MAX_COMPLETED_MESSAGES = 1e3;
|
|
2624
2833
|
var LocalTransport = class {
|
|
2625
2834
|
name = "local";
|
|
2626
2835
|
capabilities = localCapabilities;
|
|
@@ -2664,7 +2873,7 @@ var LocalTransport = class {
|
|
|
2664
2873
|
topology.prefix
|
|
2665
2874
|
);
|
|
2666
2875
|
if (!this.queues.has(queueName)) {
|
|
2667
|
-
this.queues.set(queueName,
|
|
2876
|
+
this.queues.set(queueName, /* @__PURE__ */ new Map());
|
|
2668
2877
|
}
|
|
2669
2878
|
}
|
|
2670
2879
|
}
|
|
@@ -2696,6 +2905,18 @@ var LocalTransport = class {
|
|
|
2696
2905
|
this.delayedTimers.add(timer);
|
|
2697
2906
|
}
|
|
2698
2907
|
async enqueue(queue, envelope) {
|
|
2908
|
+
const subs = this.subscriptions.get(queue);
|
|
2909
|
+
if (!subs || subs.length === 0) {
|
|
2910
|
+
throw new LocalTransportNoActiveSubscriberError(queue);
|
|
2911
|
+
}
|
|
2912
|
+
await this.storeAndDeliver(queue, envelope);
|
|
2913
|
+
}
|
|
2914
|
+
/**
|
|
2915
|
+
* Stores a message and delivers it to any active subscribers, without
|
|
2916
|
+
* requiring one to be present. Dead-letter queues are meant to hold
|
|
2917
|
+
* messages for later manual inspection, so they don't need a live consumer.
|
|
2918
|
+
*/
|
|
2919
|
+
async storeAndDeliver(queue, envelope) {
|
|
2699
2920
|
const messages = this.getOrCreateQueue(queue);
|
|
2700
2921
|
const messageId = `${++this.messageIdCounter}`;
|
|
2701
2922
|
const queuedMessage = {
|
|
@@ -2703,17 +2924,11 @@ var LocalTransport = class {
|
|
|
2703
2924
|
id: messageId,
|
|
2704
2925
|
completed: false
|
|
2705
2926
|
};
|
|
2706
|
-
messages.
|
|
2927
|
+
messages.set(messageId, queuedMessage);
|
|
2707
2928
|
await this.deliverToSubscribers(queue, queuedMessage);
|
|
2708
2929
|
}
|
|
2709
2930
|
async deliverToSubscribers(queue, message) {
|
|
2710
|
-
const subs = this.subscriptions.get(queue);
|
|
2711
|
-
if (!subs) {
|
|
2712
|
-
this.logger.warn(
|
|
2713
|
-
`[Matador][LocalTransport] \u{1F7E1} No subscriptions found for queue '${queue}', message will be lost.`
|
|
2714
|
-
);
|
|
2715
|
-
return;
|
|
2716
|
-
}
|
|
2931
|
+
const subs = this.subscriptions.get(queue) ?? [];
|
|
2717
2932
|
for (const sub of subs) {
|
|
2718
2933
|
if (!sub.active || message.completed) continue;
|
|
2719
2934
|
const receipt = {
|
|
@@ -2746,7 +2961,7 @@ var LocalTransport = class {
|
|
|
2746
2961
|
const subs = this.subscriptions.get(queue) ?? [];
|
|
2747
2962
|
subs.push(subscription);
|
|
2748
2963
|
this.subscriptions.set(queue, subs);
|
|
2749
|
-
const messages = this.queues.get(queue) ?? [];
|
|
2964
|
+
const messages = this.queues.get(queue)?.values() ?? [];
|
|
2750
2965
|
for (const message of messages) {
|
|
2751
2966
|
if (message.completed) continue;
|
|
2752
2967
|
await this.deliverToSubscribers(queue, message);
|
|
@@ -2761,6 +2976,11 @@ var LocalTransport = class {
|
|
|
2761
2976
|
this.subscriptions.delete(queue);
|
|
2762
2977
|
}
|
|
2763
2978
|
},
|
|
2979
|
+
// No-op: LocalTransport is only ever fed from within the process
|
|
2980
|
+
// So, when shutting down, it needs to continue and handle new messages until the end
|
|
2981
|
+
// As, an ongoing message (before shutdown) could create new (local) messages
|
|
2982
|
+
pauseForShutdown: async () => {
|
|
2983
|
+
},
|
|
2764
2984
|
get isActive() {
|
|
2765
2985
|
return subscription.active;
|
|
2766
2986
|
}
|
|
@@ -2770,10 +2990,14 @@ var LocalTransport = class {
|
|
|
2770
2990
|
const message = receipt.handle;
|
|
2771
2991
|
message.completed = true;
|
|
2772
2992
|
this.completedMessages.push(receipt);
|
|
2993
|
+
if (this.completedMessages.length > MAX_COMPLETED_MESSAGES) {
|
|
2994
|
+
this.completedMessages.shift();
|
|
2995
|
+
}
|
|
2996
|
+
this.queues.get(receipt.sourceQueue)?.delete(message.id);
|
|
2773
2997
|
}
|
|
2774
2998
|
async sendToDeadLetter(receipt, dlqName, envelope, _reason) {
|
|
2775
2999
|
const dlqQueueName = `${receipt.sourceQueue}.${dlqName}`;
|
|
2776
|
-
await this.
|
|
3000
|
+
await this.storeAndDeliver(dlqQueueName, envelope);
|
|
2777
3001
|
await this.complete(receipt);
|
|
2778
3002
|
}
|
|
2779
3003
|
// Test helpers
|
|
@@ -2781,9 +3005,7 @@ var LocalTransport = class {
|
|
|
2781
3005
|
* Gets the current size of a queue.
|
|
2782
3006
|
*/
|
|
2783
3007
|
getQueueSize(queue) {
|
|
2784
|
-
|
|
2785
|
-
if (!messages) return 0;
|
|
2786
|
-
return messages.filter((m) => !m.completed).length;
|
|
3008
|
+
return this.queues.get(queue)?.size ?? 0;
|
|
2787
3009
|
}
|
|
2788
3010
|
/**
|
|
2789
3011
|
* Gets all completed message receipts.
|
|
@@ -2797,7 +3019,7 @@ var LocalTransport = class {
|
|
|
2797
3019
|
getPendingMessages(queue) {
|
|
2798
3020
|
const messages = this.queues.get(queue);
|
|
2799
3021
|
if (!messages) return [];
|
|
2800
|
-
return messages.
|
|
3022
|
+
return Array.from(messages.values()).map((m) => m.envelope);
|
|
2801
3023
|
}
|
|
2802
3024
|
/**
|
|
2803
3025
|
* Clears all state (for test isolation).
|
|
@@ -2819,7 +3041,7 @@ var LocalTransport = class {
|
|
|
2819
3041
|
async receiveOne(queue) {
|
|
2820
3042
|
const messages = this.queues.get(queue);
|
|
2821
3043
|
if (!messages) return null;
|
|
2822
|
-
const pending = messages.
|
|
3044
|
+
const pending = messages.values().next().value;
|
|
2823
3045
|
if (!pending) return null;
|
|
2824
3046
|
const receipt = {
|
|
2825
3047
|
handle: pending,
|
|
@@ -2834,7 +3056,7 @@ var LocalTransport = class {
|
|
|
2834
3056
|
getOrCreateQueue(queue) {
|
|
2835
3057
|
let messages = this.queues.get(queue);
|
|
2836
3058
|
if (!messages) {
|
|
2837
|
-
messages =
|
|
3059
|
+
messages = /* @__PURE__ */ new Map();
|
|
2838
3060
|
this.queues.set(queue, messages);
|
|
2839
3061
|
}
|
|
2840
3062
|
return messages;
|
|
@@ -2977,6 +3199,11 @@ var MultiTransport = class {
|
|
|
2977
3199
|
unsubscribe: async () => {
|
|
2978
3200
|
await Promise.all(subscriptions.map((s) => s.unsubscribe()));
|
|
2979
3201
|
},
|
|
3202
|
+
pauseForShutdown: async () => {
|
|
3203
|
+
await Promise.all(
|
|
3204
|
+
subscriptions.map((s) => (s.pauseForShutdown ?? s.unsubscribe)())
|
|
3205
|
+
);
|
|
3206
|
+
},
|
|
2980
3207
|
get isActive() {
|
|
2981
3208
|
return subscriptions.some((s) => s.isActive);
|
|
2982
3209
|
}
|
|
@@ -3153,33 +3380,37 @@ var RabbitMQTransport = class {
|
|
|
3153
3380
|
};
|
|
3154
3381
|
this.subscriptionIntents.push(intent);
|
|
3155
3382
|
await this.activateIntent(intent);
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3383
|
+
const unsubscribe = async () => {
|
|
3384
|
+
intent.active = false;
|
|
3385
|
+
const idx = this.subscriptionIntents.indexOf(intent);
|
|
3386
|
+
if (idx !== -1) this.subscriptionIntents.splice(idx, 1);
|
|
3387
|
+
const consumer = intent.currentConsumer;
|
|
3388
|
+
if (consumer) {
|
|
3389
|
+
consumer.active = false;
|
|
3390
|
+
const queueChannel = this.queueChannels.get(queue);
|
|
3391
|
+
if (queueChannel) {
|
|
3392
|
+
try {
|
|
3393
|
+
await queueChannel.channel.cancel(consumer.consumerTag);
|
|
3394
|
+
} catch {
|
|
3395
|
+
}
|
|
3396
|
+
const cIdx = queueChannel.consumers.indexOf(consumer);
|
|
3397
|
+
if (cIdx !== -1) queueChannel.consumers.splice(cIdx, 1);
|
|
3398
|
+
if (queueChannel.consumers.length === 0) {
|
|
3166
3399
|
try {
|
|
3167
|
-
await queueChannel.channel.
|
|
3400
|
+
await queueChannel.channel.close();
|
|
3168
3401
|
} catch {
|
|
3169
3402
|
}
|
|
3170
|
-
|
|
3171
|
-
if (cIdx !== -1) queueChannel.consumers.splice(cIdx, 1);
|
|
3172
|
-
if (queueChannel.consumers.length === 0) {
|
|
3173
|
-
try {
|
|
3174
|
-
await queueChannel.channel.close();
|
|
3175
|
-
} catch {
|
|
3176
|
-
}
|
|
3177
|
-
this.queueChannels.delete(queue);
|
|
3178
|
-
}
|
|
3403
|
+
this.queueChannels.delete(queue);
|
|
3179
3404
|
}
|
|
3180
|
-
intent.currentConsumer = null;
|
|
3181
3405
|
}
|
|
3182
|
-
|
|
3406
|
+
intent.currentConsumer = null;
|
|
3407
|
+
}
|
|
3408
|
+
};
|
|
3409
|
+
return {
|
|
3410
|
+
unsubscribe,
|
|
3411
|
+
// Messages on this transport comes from outside (RabbitMQ), so, when shutting down, we need to unsubscribe directly
|
|
3412
|
+
// Otherwise, we could receive traffic forever, and an idle state would never be reached.
|
|
3413
|
+
pauseForShutdown: unsubscribe,
|
|
3183
3414
|
get isActive() {
|
|
3184
3415
|
return intent.active;
|
|
3185
3416
|
}
|
|
@@ -3493,6 +3724,9 @@ var RabbitMQTransport = class {
|
|
|
3493
3724
|
if (queueDef.consumerTimeout) {
|
|
3494
3725
|
queueOptions.arguments["x-consumer-timeout"] = queueDef.consumerTimeout;
|
|
3495
3726
|
}
|
|
3727
|
+
if (queueDef.singleActiveConsumer) {
|
|
3728
|
+
queueOptions.arguments["x-single-active-consumer"] = true;
|
|
3729
|
+
}
|
|
3496
3730
|
return queueOptions;
|
|
3497
3731
|
}
|
|
3498
3732
|
async assertWorkQueue(channel, topology, queueDef) {
|
|
@@ -3634,7 +3868,7 @@ exports.InvalidEventError = InvalidEventError;
|
|
|
3634
3868
|
exports.InvalidSchemaError = InvalidSchemaError;
|
|
3635
3869
|
exports.JsonCodec = JsonCodec;
|
|
3636
3870
|
exports.LocalTransport = LocalTransport;
|
|
3637
|
-
exports.
|
|
3871
|
+
exports.LocalTransportNoActiveSubscriberError = LocalTransportNoActiveSubscriberError;
|
|
3638
3872
|
exports.Matador = Matador;
|
|
3639
3873
|
exports.MatadorError = MatadorError;
|
|
3640
3874
|
exports.MatadorEvent = MatadorEvent;
|