@zdavison/matador 4.0.1 → 4.0.3
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 +62 -0
- package/dist/core/fanout.d.ts.map +1 -1
- package/dist/core/fanout.js +217 -15
- package/dist/core/fanout.test.js +904 -6
- package/dist/core/matador.d.ts +33 -0
- package/dist/core/matador.d.ts.map +1 -1
- package/dist/core/matador.js +6 -1
- package/dist/core/matador.test.js +243 -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 +345 -73
- 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/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 +6 -0
- package/dist/transport/multi/multi-transport.d.ts.map +1 -1
- package/dist/transport/multi/multi-transport.js +20 -0
- package/dist/transport/multi/multi-transport.test.js +82 -0
- package/dist/transport/rabbitmq/rabbitmq-transport-reconnection.test.js +45 -6
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts +6 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -1
- package/dist/transport/rabbitmq/rabbitmq-transport.js +83 -40
- package/dist/transport/transport.d.ts +12 -0
- package/dist/transport/transport.d.ts.map +1 -1
- package/dist/types/event.d.ts +17 -0
- package/dist/types/event.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,18 +809,43 @@ var FanoutEngine = class {
|
|
|
808
809
|
topology;
|
|
809
810
|
defaultQueue;
|
|
810
811
|
enqueuingCount = 0;
|
|
812
|
+
flushInFlightCount = 0;
|
|
813
|
+
retryBuffer = [];
|
|
814
|
+
maxRetryBufferSize;
|
|
815
|
+
maxRetryAttempts;
|
|
816
|
+
maxConsecutiveFlushFailures;
|
|
817
|
+
disposeOnConnected;
|
|
818
|
+
retryTimer;
|
|
811
819
|
constructor(config) {
|
|
812
820
|
this.transport = config.transport;
|
|
813
821
|
this.schema = config.schema;
|
|
814
822
|
this.hooks = config.hooks;
|
|
815
823
|
this.topology = config.topology;
|
|
816
824
|
this.defaultQueue = config.defaultQueue;
|
|
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;
|
|
829
|
+
this.disposeOnConnected = this.transport.onConnected?.(() => {
|
|
830
|
+
void this.flushRetryBuffer();
|
|
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
|
+
}
|
|
839
|
+
}
|
|
840
|
+
dispose() {
|
|
841
|
+
this.disposeOnConnected?.();
|
|
842
|
+
clearInterval(this.retryTimer);
|
|
817
843
|
}
|
|
818
844
|
/**
|
|
819
845
|
* Current count of events being enqueued.
|
|
820
846
|
*/
|
|
821
847
|
get eventsBeingEnqueuedCount() {
|
|
822
|
-
return this.enqueuingCount;
|
|
848
|
+
return this.enqueuingCount + this.flushInFlightCount;
|
|
823
849
|
}
|
|
824
850
|
/**
|
|
825
851
|
* Sends an event to all registered subscribers.
|
|
@@ -851,12 +877,13 @@ var FanoutEngine = class {
|
|
|
851
877
|
universalMetadata,
|
|
852
878
|
delayMs: options.delayMs
|
|
853
879
|
});
|
|
880
|
+
const sendOptions = options.delayMs !== void 0 ? { delay: options.delayMs } : void 0;
|
|
854
881
|
this.enqueuingCount++;
|
|
855
882
|
try {
|
|
856
883
|
const usedTransport = await this.transport.send(
|
|
857
884
|
qualifiedQueue,
|
|
858
885
|
envelope,
|
|
859
|
-
|
|
886
|
+
sendOptions
|
|
860
887
|
);
|
|
861
888
|
sent++;
|
|
862
889
|
await this.hooks.onEnqueueSuccess({
|
|
@@ -866,17 +893,55 @@ var FanoutEngine = class {
|
|
|
866
893
|
});
|
|
867
894
|
} catch (error) {
|
|
868
895
|
const cause = error instanceof Error ? error : new Error(String(error));
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
896
|
+
const shouldBuffer = options.buffer !== false;
|
|
897
|
+
if (shouldBuffer && this.retryBuffer.length < this.maxRetryBufferSize) {
|
|
898
|
+
this.retryBuffer.push({
|
|
899
|
+
queue: qualifiedQueue,
|
|
900
|
+
envelope,
|
|
901
|
+
sendOptions,
|
|
902
|
+
subscriberName: subscriber.name,
|
|
903
|
+
attempts: 0
|
|
904
|
+
});
|
|
905
|
+
this.hooks.logger.warn(
|
|
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
|
+
}
|
|
913
|
+
);
|
|
914
|
+
if (options.throwOnBufferedFailure) {
|
|
915
|
+
const err = new TransportSendError(qualifiedQueue, cause);
|
|
916
|
+
errors.push({
|
|
917
|
+
subscriberName: subscriber.name,
|
|
918
|
+
queue: qualifiedQueue,
|
|
919
|
+
error: err
|
|
920
|
+
});
|
|
921
|
+
await this.hooks.onEnqueueError({
|
|
922
|
+
envelope,
|
|
923
|
+
error: err,
|
|
924
|
+
transport: this.transport.name
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
} else {
|
|
928
|
+
if (shouldBuffer) {
|
|
929
|
+
this.hooks.logger.error(
|
|
930
|
+
`[Matador] \u{1F534} Retry buffer full (${this.maxRetryBufferSize}). Message for '${subscriber.name}' dropped and will not be retried.`
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
const err = new TransportSendError(qualifiedQueue, cause);
|
|
934
|
+
errors.push({
|
|
935
|
+
subscriberName: subscriber.name,
|
|
936
|
+
queue: qualifiedQueue,
|
|
937
|
+
error: err
|
|
938
|
+
});
|
|
939
|
+
await this.hooks.onEnqueueError({
|
|
940
|
+
envelope,
|
|
941
|
+
error: err,
|
|
942
|
+
transport: this.transport.name
|
|
943
|
+
});
|
|
944
|
+
}
|
|
880
945
|
} finally {
|
|
881
946
|
this.enqueuingCount--;
|
|
882
947
|
}
|
|
@@ -888,6 +953,140 @@ var FanoutEngine = class {
|
|
|
888
953
|
errors
|
|
889
954
|
};
|
|
890
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
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Flushes the retry buffer
|
|
983
|
+
*
|
|
984
|
+
* This is called when the transport reconnects, and is used to retry any messages that were buffered while the transport was disconnected
|
|
985
|
+
*/
|
|
986
|
+
async flushRetryBuffer() {
|
|
987
|
+
if (this.retryBuffer.length === 0) return;
|
|
988
|
+
this.hooks.logger.info(
|
|
989
|
+
`[Matador] \u23F3 Flushing ${this.retryBuffer.length} buffered message(s)...`
|
|
990
|
+
);
|
|
991
|
+
const toFlush = this.retryBuffer.splice(0);
|
|
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;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
} finally {
|
|
1016
|
+
this.flushInFlightCount = 0;
|
|
1017
|
+
}
|
|
1018
|
+
const remaining = this.retryBuffer.length;
|
|
1019
|
+
if (remaining > 0) {
|
|
1020
|
+
this.hooks.logger.warn(
|
|
1021
|
+
`[Matador] \u{1F7E1} ${remaining} buffered message(s) could not be flushed; will retry later.`
|
|
1022
|
+
);
|
|
1023
|
+
} else {
|
|
1024
|
+
this.hooks.logger.info(
|
|
1025
|
+
"[Matador] \u{1F7E2} All buffered messages flushed successfully."
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
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
|
+
}
|
|
891
1090
|
async isSubscriberEnabled(subscriber) {
|
|
892
1091
|
if (!subscriber.enabled) {
|
|
893
1092
|
return true;
|
|
@@ -2180,7 +2379,11 @@ var Matador = class {
|
|
|
2180
2379
|
schema: this.schema,
|
|
2181
2380
|
hooks: this.hooks,
|
|
2182
2381
|
topology: this.topology,
|
|
2183
|
-
defaultQueue
|
|
2382
|
+
defaultQueue,
|
|
2383
|
+
maxRetryBufferSize: config.maxRetryBufferSize,
|
|
2384
|
+
maxRetryAttempts: config.maxRetryAttempts,
|
|
2385
|
+
retryIntervalMs: config.retryIntervalMs,
|
|
2386
|
+
maxConsecutiveFlushFailures: config.maxConsecutiveFlushFailures
|
|
2184
2387
|
});
|
|
2185
2388
|
this.shutdownManager = new ShutdownManager(
|
|
2186
2389
|
() => this.fanout.eventsBeingEnqueuedCount,
|
|
@@ -2354,6 +2557,7 @@ var Matador = class {
|
|
|
2354
2557
|
if (!this.started) {
|
|
2355
2558
|
return;
|
|
2356
2559
|
}
|
|
2560
|
+
this.fanout.dispose();
|
|
2357
2561
|
await this.shutdownManager.shutdown();
|
|
2358
2562
|
this.started = false;
|
|
2359
2563
|
}
|
|
@@ -2365,7 +2569,7 @@ var Matador = class {
|
|
|
2365
2569
|
}
|
|
2366
2570
|
async unsubscribeAll() {
|
|
2367
2571
|
for (const subscription of this.subscriptions) {
|
|
2368
|
-
await subscription.unsubscribe();
|
|
2572
|
+
await (subscription.pauseForShutdown ?? subscription.unsubscribe)();
|
|
2369
2573
|
}
|
|
2370
2574
|
this.subscriptions.length = 0;
|
|
2371
2575
|
}
|
|
@@ -2527,6 +2731,7 @@ var localCapabilities = {
|
|
|
2527
2731
|
ordering: "queue",
|
|
2528
2732
|
priorities: false
|
|
2529
2733
|
};
|
|
2734
|
+
var MAX_COMPLETED_MESSAGES = 1e3;
|
|
2530
2735
|
var LocalTransport = class {
|
|
2531
2736
|
name = "local";
|
|
2532
2737
|
capabilities = localCapabilities;
|
|
@@ -2570,7 +2775,7 @@ var LocalTransport = class {
|
|
|
2570
2775
|
topology.prefix
|
|
2571
2776
|
);
|
|
2572
2777
|
if (!this.queues.has(queueName)) {
|
|
2573
|
-
this.queues.set(queueName,
|
|
2778
|
+
this.queues.set(queueName, /* @__PURE__ */ new Map());
|
|
2574
2779
|
}
|
|
2575
2780
|
}
|
|
2576
2781
|
}
|
|
@@ -2602,6 +2807,18 @@ var LocalTransport = class {
|
|
|
2602
2807
|
this.delayedTimers.add(timer);
|
|
2603
2808
|
}
|
|
2604
2809
|
async enqueue(queue, envelope) {
|
|
2810
|
+
const subs = this.subscriptions.get(queue);
|
|
2811
|
+
if (!subs || subs.length === 0) {
|
|
2812
|
+
throw new LocalTransportNoActiveSubscriberError(queue);
|
|
2813
|
+
}
|
|
2814
|
+
await this.storeAndDeliver(queue, envelope);
|
|
2815
|
+
}
|
|
2816
|
+
/**
|
|
2817
|
+
* Stores a message and delivers it to any active subscribers, without
|
|
2818
|
+
* requiring one to be present. Dead-letter queues are meant to hold
|
|
2819
|
+
* messages for later manual inspection, so they don't need a live consumer.
|
|
2820
|
+
*/
|
|
2821
|
+
async storeAndDeliver(queue, envelope) {
|
|
2605
2822
|
const messages = this.getOrCreateQueue(queue);
|
|
2606
2823
|
const messageId = `${++this.messageIdCounter}`;
|
|
2607
2824
|
const queuedMessage = {
|
|
@@ -2609,17 +2826,11 @@ var LocalTransport = class {
|
|
|
2609
2826
|
id: messageId,
|
|
2610
2827
|
completed: false
|
|
2611
2828
|
};
|
|
2612
|
-
messages.
|
|
2829
|
+
messages.set(messageId, queuedMessage);
|
|
2613
2830
|
await this.deliverToSubscribers(queue, queuedMessage);
|
|
2614
2831
|
}
|
|
2615
2832
|
async deliverToSubscribers(queue, message) {
|
|
2616
|
-
const subs = this.subscriptions.get(queue);
|
|
2617
|
-
if (!subs) {
|
|
2618
|
-
this.logger.warn(
|
|
2619
|
-
`[Matador][LocalTransport] \u{1F7E1} No subscriptions found for queue '${queue}', message will be lost.`
|
|
2620
|
-
);
|
|
2621
|
-
return;
|
|
2622
|
-
}
|
|
2833
|
+
const subs = this.subscriptions.get(queue) ?? [];
|
|
2623
2834
|
for (const sub of subs) {
|
|
2624
2835
|
if (!sub.active || message.completed) continue;
|
|
2625
2836
|
const receipt = {
|
|
@@ -2652,7 +2863,7 @@ var LocalTransport = class {
|
|
|
2652
2863
|
const subs = this.subscriptions.get(queue) ?? [];
|
|
2653
2864
|
subs.push(subscription);
|
|
2654
2865
|
this.subscriptions.set(queue, subs);
|
|
2655
|
-
const messages = this.queues.get(queue) ?? [];
|
|
2866
|
+
const messages = this.queues.get(queue)?.values() ?? [];
|
|
2656
2867
|
for (const message of messages) {
|
|
2657
2868
|
if (message.completed) continue;
|
|
2658
2869
|
await this.deliverToSubscribers(queue, message);
|
|
@@ -2667,6 +2878,11 @@ var LocalTransport = class {
|
|
|
2667
2878
|
this.subscriptions.delete(queue);
|
|
2668
2879
|
}
|
|
2669
2880
|
},
|
|
2881
|
+
// No-op: LocalTransport is only ever fed from within the process
|
|
2882
|
+
// So, when shutting down, it needs to continue and handle new messages until the end
|
|
2883
|
+
// As, an ongoing message (before shutdown) could create new (local) messages
|
|
2884
|
+
pauseForShutdown: async () => {
|
|
2885
|
+
},
|
|
2670
2886
|
get isActive() {
|
|
2671
2887
|
return subscription.active;
|
|
2672
2888
|
}
|
|
@@ -2676,10 +2892,14 @@ var LocalTransport = class {
|
|
|
2676
2892
|
const message = receipt.handle;
|
|
2677
2893
|
message.completed = true;
|
|
2678
2894
|
this.completedMessages.push(receipt);
|
|
2895
|
+
if (this.completedMessages.length > MAX_COMPLETED_MESSAGES) {
|
|
2896
|
+
this.completedMessages.shift();
|
|
2897
|
+
}
|
|
2898
|
+
this.queues.get(receipt.sourceQueue)?.delete(message.id);
|
|
2679
2899
|
}
|
|
2680
2900
|
async sendToDeadLetter(receipt, dlqName, envelope, _reason) {
|
|
2681
2901
|
const dlqQueueName = `${receipt.sourceQueue}.${dlqName}`;
|
|
2682
|
-
await this.
|
|
2902
|
+
await this.storeAndDeliver(dlqQueueName, envelope);
|
|
2683
2903
|
await this.complete(receipt);
|
|
2684
2904
|
}
|
|
2685
2905
|
// Test helpers
|
|
@@ -2687,9 +2907,7 @@ var LocalTransport = class {
|
|
|
2687
2907
|
* Gets the current size of a queue.
|
|
2688
2908
|
*/
|
|
2689
2909
|
getQueueSize(queue) {
|
|
2690
|
-
|
|
2691
|
-
if (!messages) return 0;
|
|
2692
|
-
return messages.filter((m) => !m.completed).length;
|
|
2910
|
+
return this.queues.get(queue)?.size ?? 0;
|
|
2693
2911
|
}
|
|
2694
2912
|
/**
|
|
2695
2913
|
* Gets all completed message receipts.
|
|
@@ -2703,7 +2921,7 @@ var LocalTransport = class {
|
|
|
2703
2921
|
getPendingMessages(queue) {
|
|
2704
2922
|
const messages = this.queues.get(queue);
|
|
2705
2923
|
if (!messages) return [];
|
|
2706
|
-
return messages.
|
|
2924
|
+
return Array.from(messages.values()).map((m) => m.envelope);
|
|
2707
2925
|
}
|
|
2708
2926
|
/**
|
|
2709
2927
|
* Clears all state (for test isolation).
|
|
@@ -2725,7 +2943,7 @@ var LocalTransport = class {
|
|
|
2725
2943
|
async receiveOne(queue) {
|
|
2726
2944
|
const messages = this.queues.get(queue);
|
|
2727
2945
|
if (!messages) return null;
|
|
2728
|
-
const pending = messages.
|
|
2946
|
+
const pending = messages.values().next().value;
|
|
2729
2947
|
if (!pending) return null;
|
|
2730
2948
|
const receipt = {
|
|
2731
2949
|
handle: pending,
|
|
@@ -2740,7 +2958,7 @@ var LocalTransport = class {
|
|
|
2740
2958
|
getOrCreateQueue(queue) {
|
|
2741
2959
|
let messages = this.queues.get(queue);
|
|
2742
2960
|
if (!messages) {
|
|
2743
|
-
messages =
|
|
2961
|
+
messages = /* @__PURE__ */ new Map();
|
|
2744
2962
|
this.queues.set(queue, messages);
|
|
2745
2963
|
}
|
|
2746
2964
|
return messages;
|
|
@@ -2792,6 +3010,19 @@ var MultiTransport = class {
|
|
|
2792
3010
|
isConnected() {
|
|
2793
3011
|
return this.connected && this.primary.isConnected();
|
|
2794
3012
|
}
|
|
3013
|
+
/**
|
|
3014
|
+
* Registers a callback to fire each time the transport successfully (re)connects
|
|
3015
|
+
* @param callback - The callback to fire when the transport successfully (re)connects
|
|
3016
|
+
* @returns A function to unsubscribe from the callback
|
|
3017
|
+
*/
|
|
3018
|
+
onConnected(callback) {
|
|
3019
|
+
const unsubFunctions = this.transports.map((t) => t.onConnected?.(callback)).filter(Boolean);
|
|
3020
|
+
return () => {
|
|
3021
|
+
for (const unsub of unsubFunctions) {
|
|
3022
|
+
unsub();
|
|
3023
|
+
}
|
|
3024
|
+
};
|
|
3025
|
+
}
|
|
2795
3026
|
async applyTopology(topology) {
|
|
2796
3027
|
await Promise.all(this.transports.map((t) => t.applyTopology(topology)));
|
|
2797
3028
|
}
|
|
@@ -2870,6 +3101,11 @@ var MultiTransport = class {
|
|
|
2870
3101
|
unsubscribe: async () => {
|
|
2871
3102
|
await Promise.all(subscriptions.map((s) => s.unsubscribe()));
|
|
2872
3103
|
},
|
|
3104
|
+
pauseForShutdown: async () => {
|
|
3105
|
+
await Promise.all(
|
|
3106
|
+
subscriptions.map((s) => (s.pauseForShutdown ?? s.unsubscribe)())
|
|
3107
|
+
);
|
|
3108
|
+
},
|
|
2873
3109
|
get isActive() {
|
|
2874
3110
|
return subscriptions.some((s) => s.isActive);
|
|
2875
3111
|
}
|
|
@@ -2941,6 +3177,18 @@ var RabbitMQTransport = class {
|
|
|
2941
3177
|
isConnected() {
|
|
2942
3178
|
return this.connectionManager.isConnected();
|
|
2943
3179
|
}
|
|
3180
|
+
/**
|
|
3181
|
+
* Registers a callback to fire each time the transport (here RabbitMQ) successfully (re)connects
|
|
3182
|
+
* @param callback - The callback to fire when the transport (here RabbitMQ) successfully (re)connects
|
|
3183
|
+
* @returns A function to unsubscribe from the callback
|
|
3184
|
+
*/
|
|
3185
|
+
onConnected(callback) {
|
|
3186
|
+
return this.connectionManager.onStateChange((state) => {
|
|
3187
|
+
if (state.status === "connected") {
|
|
3188
|
+
callback();
|
|
3189
|
+
}
|
|
3190
|
+
});
|
|
3191
|
+
}
|
|
2944
3192
|
async applyTopology(topology) {
|
|
2945
3193
|
this.topology = topology;
|
|
2946
3194
|
if (!this.publishChannel) {
|
|
@@ -3034,33 +3282,37 @@ var RabbitMQTransport = class {
|
|
|
3034
3282
|
};
|
|
3035
3283
|
this.subscriptionIntents.push(intent);
|
|
3036
3284
|
await this.activateIntent(intent);
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3285
|
+
const unsubscribe = async () => {
|
|
3286
|
+
intent.active = false;
|
|
3287
|
+
const idx = this.subscriptionIntents.indexOf(intent);
|
|
3288
|
+
if (idx !== -1) this.subscriptionIntents.splice(idx, 1);
|
|
3289
|
+
const consumer = intent.currentConsumer;
|
|
3290
|
+
if (consumer) {
|
|
3291
|
+
consumer.active = false;
|
|
3292
|
+
const queueChannel = this.queueChannels.get(queue);
|
|
3293
|
+
if (queueChannel) {
|
|
3294
|
+
try {
|
|
3295
|
+
await queueChannel.channel.cancel(consumer.consumerTag);
|
|
3296
|
+
} catch {
|
|
3297
|
+
}
|
|
3298
|
+
const cIdx = queueChannel.consumers.indexOf(consumer);
|
|
3299
|
+
if (cIdx !== -1) queueChannel.consumers.splice(cIdx, 1);
|
|
3300
|
+
if (queueChannel.consumers.length === 0) {
|
|
3047
3301
|
try {
|
|
3048
|
-
await queueChannel.channel.
|
|
3302
|
+
await queueChannel.channel.close();
|
|
3049
3303
|
} catch {
|
|
3050
3304
|
}
|
|
3051
|
-
|
|
3052
|
-
if (cIdx !== -1) queueChannel.consumers.splice(cIdx, 1);
|
|
3053
|
-
if (queueChannel.consumers.length === 0) {
|
|
3054
|
-
try {
|
|
3055
|
-
await queueChannel.channel.close();
|
|
3056
|
-
} catch {
|
|
3057
|
-
}
|
|
3058
|
-
this.queueChannels.delete(queue);
|
|
3059
|
-
}
|
|
3305
|
+
this.queueChannels.delete(queue);
|
|
3060
3306
|
}
|
|
3061
|
-
intent.currentConsumer = null;
|
|
3062
3307
|
}
|
|
3063
|
-
|
|
3308
|
+
intent.currentConsumer = null;
|
|
3309
|
+
}
|
|
3310
|
+
};
|
|
3311
|
+
return {
|
|
3312
|
+
unsubscribe,
|
|
3313
|
+
// Messages on this transport comes from outside (RabbitMQ), so, when shutting down, we need to unsubscribe directly
|
|
3314
|
+
// Otherwise, we could receive traffic forever, and an idle state would never be reached.
|
|
3315
|
+
pauseForShutdown: unsubscribe,
|
|
3064
3316
|
get isActive() {
|
|
3065
3317
|
return intent.active;
|
|
3066
3318
|
}
|
|
@@ -3219,6 +3471,14 @@ var RabbitMQTransport = class {
|
|
|
3219
3471
|
return queueChannel;
|
|
3220
3472
|
}
|
|
3221
3473
|
async doConnect() {
|
|
3474
|
+
if (this.connection) {
|
|
3475
|
+
try {
|
|
3476
|
+
await this.connection.close();
|
|
3477
|
+
} catch {
|
|
3478
|
+
}
|
|
3479
|
+
this.connection = null;
|
|
3480
|
+
this.publishChannel = null;
|
|
3481
|
+
}
|
|
3222
3482
|
this.queueChannels.clear();
|
|
3223
3483
|
this.logger.info(
|
|
3224
3484
|
`[Matador] \u23F3 Connecting to RabbitMQ at '${redactAmqpUrl(this.config.url)}'.`
|
|
@@ -3236,21 +3496,33 @@ var RabbitMQTransport = class {
|
|
|
3236
3496
|
consumer.active = false;
|
|
3237
3497
|
}
|
|
3238
3498
|
}
|
|
3499
|
+
this.connection = null;
|
|
3500
|
+
this.publishChannel = null;
|
|
3239
3501
|
if (this.connectionManager.isConnected()) {
|
|
3240
3502
|
this.connectionManager.handleConnectionLost(
|
|
3241
3503
|
new Error("Connection closed unexpectedly")
|
|
3242
3504
|
);
|
|
3243
3505
|
}
|
|
3244
3506
|
});
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
this.
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3507
|
+
try {
|
|
3508
|
+
this.publishChannel = await connection.createConfirmChannel();
|
|
3509
|
+
this.publishChannel.on("error", (err) => {
|
|
3510
|
+
this.logger.error("[Matador] \u{1F534} RabbitMQ publish channel error", err);
|
|
3511
|
+
});
|
|
3512
|
+
if (this.topology) {
|
|
3513
|
+
await this.applyTopology(this.topology);
|
|
3514
|
+
for (const intent of this.subscriptionIntents) {
|
|
3515
|
+
await this.activateIntent(intent);
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
} catch (err) {
|
|
3519
|
+
try {
|
|
3520
|
+
await connection.close();
|
|
3521
|
+
} catch {
|
|
3253
3522
|
}
|
|
3523
|
+
this.connection = null;
|
|
3524
|
+
this.publishChannel = null;
|
|
3525
|
+
throw err;
|
|
3254
3526
|
}
|
|
3255
3527
|
this.logger.info("[Matador] \u{1F50C} Connected to RabbitMQ");
|
|
3256
3528
|
}
|
|
@@ -3495,7 +3767,7 @@ exports.InvalidEventError = InvalidEventError;
|
|
|
3495
3767
|
exports.InvalidSchemaError = InvalidSchemaError;
|
|
3496
3768
|
exports.JsonCodec = JsonCodec;
|
|
3497
3769
|
exports.LocalTransport = LocalTransport;
|
|
3498
|
-
exports.
|
|
3770
|
+
exports.LocalTransportNoActiveSubscriberError = LocalTransportNoActiveSubscriberError;
|
|
3499
3771
|
exports.Matador = Matador;
|
|
3500
3772
|
exports.MatadorError = MatadorError;
|
|
3501
3773
|
exports.MatadorEvent = MatadorEvent;
|