@zdavison/matador 4.0.0 → 4.0.2

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/index.cjs CHANGED
@@ -472,6 +472,8 @@ var TopologyBuilder = class _TopologyBuilder {
472
472
  * Only applies to default names — a {@link withNaming} override fully owns
473
473
  * its output and is never prefixed.
474
474
  *
475
+ * Pass `null` or `undefined` to disable prefixing.
476
+ *
475
477
  * @default 'matador'
476
478
  * @example
477
479
  * TopologyBuilder.create().withNamespace('myapp') // matador.myapp.events
@@ -806,12 +808,22 @@ var FanoutEngine = class {
806
808
  topology;
807
809
  defaultQueue;
808
810
  enqueuingCount = 0;
811
+ retryBuffer = [];
812
+ maxRetryBufferSize;
813
+ disposeOnConnected;
809
814
  constructor(config) {
810
815
  this.transport = config.transport;
811
816
  this.schema = config.schema;
812
817
  this.hooks = config.hooks;
813
818
  this.topology = config.topology;
814
819
  this.defaultQueue = config.defaultQueue;
820
+ this.maxRetryBufferSize = config.maxRetryBufferSize ?? 5e3;
821
+ this.disposeOnConnected = this.transport.onConnected?.(() => {
822
+ void this.flushRetryBuffer();
823
+ });
824
+ }
825
+ dispose() {
826
+ this.disposeOnConnected?.();
815
827
  }
816
828
  /**
817
829
  * Current count of events being enqueued.
@@ -849,12 +861,13 @@ var FanoutEngine = class {
849
861
  universalMetadata,
850
862
  delayMs: options.delayMs
851
863
  });
864
+ const sendOptions = options.delayMs !== void 0 ? { delay: options.delayMs } : void 0;
852
865
  this.enqueuingCount++;
853
866
  try {
854
867
  const usedTransport = await this.transport.send(
855
868
  qualifiedQueue,
856
869
  envelope,
857
- options.delayMs !== void 0 ? { delay: options.delayMs } : void 0
870
+ sendOptions
858
871
  );
859
872
  sent++;
860
873
  await this.hooks.onEnqueueSuccess({
@@ -864,17 +877,48 @@ var FanoutEngine = class {
864
877
  });
865
878
  } catch (error) {
866
879
  const cause = error instanceof Error ? error : new Error(String(error));
867
- const err = new TransportSendError(qualifiedQueue, cause);
868
- errors.push({
869
- subscriberName: subscriber.name,
870
- queue: qualifiedQueue,
871
- error: err
872
- });
873
- await this.hooks.onEnqueueError({
874
- envelope,
875
- error: err,
876
- transport: this.transport.name
877
- });
880
+ const shouldBuffer = options.buffer !== false;
881
+ if (shouldBuffer && this.retryBuffer.length < this.maxRetryBufferSize) {
882
+ this.retryBuffer.push({
883
+ queue: qualifiedQueue,
884
+ envelope,
885
+ sendOptions,
886
+ subscriberName: subscriber.name
887
+ });
888
+ this.hooks.logger.warn(
889
+ `[Matador] \u{1F7E1} Message for '${subscriber.name}' buffered for retry on reconnect (buffer: ${this.retryBuffer.length}/${this.maxRetryBufferSize}).`
890
+ );
891
+ if (options.throwOnBufferedFailure) {
892
+ const err = new TransportSendError(qualifiedQueue, cause);
893
+ errors.push({
894
+ subscriberName: subscriber.name,
895
+ queue: qualifiedQueue,
896
+ error: err
897
+ });
898
+ await this.hooks.onEnqueueError({
899
+ envelope,
900
+ error: err,
901
+ transport: this.transport.name
902
+ });
903
+ }
904
+ } else {
905
+ if (shouldBuffer) {
906
+ this.hooks.logger.error(
907
+ `[Matador] \u{1F534} Retry buffer full (${this.maxRetryBufferSize}). Message for '${subscriber.name}' dropped and will not be retried.`
908
+ );
909
+ }
910
+ const err = new TransportSendError(qualifiedQueue, cause);
911
+ errors.push({
912
+ subscriberName: subscriber.name,
913
+ queue: qualifiedQueue,
914
+ error: err
915
+ });
916
+ await this.hooks.onEnqueueError({
917
+ envelope,
918
+ error: err,
919
+ transport: this.transport.name
920
+ });
921
+ }
878
922
  } finally {
879
923
  this.enqueuingCount--;
880
924
  }
@@ -886,6 +930,57 @@ var FanoutEngine = class {
886
930
  errors
887
931
  };
888
932
  }
933
+ /**
934
+ * Flushes the retry buffer
935
+ *
936
+ * This is called when the transport reconnects, and is used to retry any messages that were buffered while the transport was disconnected
937
+ */
938
+ async flushRetryBuffer() {
939
+ if (this.retryBuffer.length === 0) return;
940
+ this.hooks.logger.info(
941
+ `[Matador] \u23F3 Flushing ${this.retryBuffer.length} buffered message(s)...`
942
+ );
943
+ const toFlush = this.retryBuffer.splice(0);
944
+ for (const item of toFlush) {
945
+ this.enqueuingCount++;
946
+ try {
947
+ const usedTransport = await this.transport.send(
948
+ item.queue,
949
+ item.envelope,
950
+ item.sendOptions
951
+ );
952
+ await this.hooks.onEnqueueSuccess({
953
+ envelope: item.envelope,
954
+ queue: item.queue,
955
+ transport: usedTransport
956
+ });
957
+ } catch (error) {
958
+ if (this.retryBuffer.length < this.maxRetryBufferSize) {
959
+ this.retryBuffer.push(item);
960
+ } else {
961
+ const cause = error instanceof Error ? error : new Error(String(error));
962
+ const err = new TransportSendError(item.queue, cause);
963
+ await this.hooks.onEnqueueError({
964
+ envelope: item.envelope,
965
+ error: err,
966
+ transport: this.transport.name
967
+ });
968
+ }
969
+ } finally {
970
+ this.enqueuingCount--;
971
+ }
972
+ }
973
+ const remaining = this.retryBuffer.length;
974
+ if (remaining > 0) {
975
+ this.hooks.logger.warn(
976
+ `[Matador] \u{1F7E1} ${remaining} buffered message(s) could not be flushed; will retry on next reconnect.`
977
+ );
978
+ } else {
979
+ this.hooks.logger.info(
980
+ "[Matador] \u{1F7E2} All buffered messages flushed successfully."
981
+ );
982
+ }
983
+ }
889
984
  async isSubscriberEnabled(subscriber) {
890
985
  if (!subscriber.enabled) {
891
986
  return true;
@@ -2352,6 +2447,7 @@ var Matador = class {
2352
2447
  if (!this.started) {
2353
2448
  return;
2354
2449
  }
2450
+ this.fanout.dispose();
2355
2451
  await this.shutdownManager.shutdown();
2356
2452
  this.started = false;
2357
2453
  }
@@ -2790,6 +2886,19 @@ var MultiTransport = class {
2790
2886
  isConnected() {
2791
2887
  return this.connected && this.primary.isConnected();
2792
2888
  }
2889
+ /**
2890
+ * Registers a callback to fire each time the transport successfully (re)connects
2891
+ * @param callback - The callback to fire when the transport successfully (re)connects
2892
+ * @returns A function to unsubscribe from the callback
2893
+ */
2894
+ onConnected(callback) {
2895
+ const unsubFunctions = this.transports.map((t) => t.onConnected?.(callback)).filter(Boolean);
2896
+ return () => {
2897
+ for (const unsub of unsubFunctions) {
2898
+ unsub();
2899
+ }
2900
+ };
2901
+ }
2793
2902
  async applyTopology(topology) {
2794
2903
  await Promise.all(this.transports.map((t) => t.applyTopology(topology)));
2795
2904
  }
@@ -2939,6 +3048,18 @@ var RabbitMQTransport = class {
2939
3048
  isConnected() {
2940
3049
  return this.connectionManager.isConnected();
2941
3050
  }
3051
+ /**
3052
+ * Registers a callback to fire each time the transport (here RabbitMQ) successfully (re)connects
3053
+ * @param callback - The callback to fire when the transport (here RabbitMQ) successfully (re)connects
3054
+ * @returns A function to unsubscribe from the callback
3055
+ */
3056
+ onConnected(callback) {
3057
+ return this.connectionManager.onStateChange((state) => {
3058
+ if (state.status === "connected") {
3059
+ callback();
3060
+ }
3061
+ });
3062
+ }
2942
3063
  async applyTopology(topology) {
2943
3064
  this.topology = topology;
2944
3065
  if (!this.publishChannel) {
@@ -3217,6 +3338,14 @@ var RabbitMQTransport = class {
3217
3338
  return queueChannel;
3218
3339
  }
3219
3340
  async doConnect() {
3341
+ if (this.connection) {
3342
+ try {
3343
+ await this.connection.close();
3344
+ } catch {
3345
+ }
3346
+ this.connection = null;
3347
+ this.publishChannel = null;
3348
+ }
3220
3349
  this.queueChannels.clear();
3221
3350
  this.logger.info(
3222
3351
  `[Matador] \u23F3 Connecting to RabbitMQ at '${redactAmqpUrl(this.config.url)}'.`
@@ -3234,21 +3363,33 @@ var RabbitMQTransport = class {
3234
3363
  consumer.active = false;
3235
3364
  }
3236
3365
  }
3366
+ this.connection = null;
3367
+ this.publishChannel = null;
3237
3368
  if (this.connectionManager.isConnected()) {
3238
3369
  this.connectionManager.handleConnectionLost(
3239
3370
  new Error("Connection closed unexpectedly")
3240
3371
  );
3241
3372
  }
3242
3373
  });
3243
- this.publishChannel = await connection.createConfirmChannel();
3244
- this.publishChannel.on("error", (err) => {
3245
- this.logger.error("[Matador] \u{1F534} RabbitMQ publish channel error", err);
3246
- });
3247
- if (this.topology) {
3248
- await this.applyTopology(this.topology);
3249
- for (const intent of this.subscriptionIntents) {
3250
- await this.activateIntent(intent);
3374
+ try {
3375
+ this.publishChannel = await connection.createConfirmChannel();
3376
+ this.publishChannel.on("error", (err) => {
3377
+ this.logger.error("[Matador] \u{1F534} RabbitMQ publish channel error", err);
3378
+ });
3379
+ if (this.topology) {
3380
+ await this.applyTopology(this.topology);
3381
+ for (const intent of this.subscriptionIntents) {
3382
+ await this.activateIntent(intent);
3383
+ }
3251
3384
  }
3385
+ } catch (err) {
3386
+ try {
3387
+ await connection.close();
3388
+ } catch {
3389
+ }
3390
+ this.connection = null;
3391
+ this.publishChannel = null;
3392
+ throw err;
3252
3393
  }
3253
3394
  this.logger.info("[Matador] \u{1F50C} Connected to RabbitMQ");
3254
3395
  }