@zdavison/matador 4.0.1 → 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
@@ -808,12 +808,22 @@ var FanoutEngine = class {
808
808
  topology;
809
809
  defaultQueue;
810
810
  enqueuingCount = 0;
811
+ retryBuffer = [];
812
+ maxRetryBufferSize;
813
+ disposeOnConnected;
811
814
  constructor(config) {
812
815
  this.transport = config.transport;
813
816
  this.schema = config.schema;
814
817
  this.hooks = config.hooks;
815
818
  this.topology = config.topology;
816
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?.();
817
827
  }
818
828
  /**
819
829
  * Current count of events being enqueued.
@@ -851,12 +861,13 @@ var FanoutEngine = class {
851
861
  universalMetadata,
852
862
  delayMs: options.delayMs
853
863
  });
864
+ const sendOptions = options.delayMs !== void 0 ? { delay: options.delayMs } : void 0;
854
865
  this.enqueuingCount++;
855
866
  try {
856
867
  const usedTransport = await this.transport.send(
857
868
  qualifiedQueue,
858
869
  envelope,
859
- options.delayMs !== void 0 ? { delay: options.delayMs } : void 0
870
+ sendOptions
860
871
  );
861
872
  sent++;
862
873
  await this.hooks.onEnqueueSuccess({
@@ -866,17 +877,48 @@ var FanoutEngine = class {
866
877
  });
867
878
  } catch (error) {
868
879
  const cause = error instanceof Error ? error : new Error(String(error));
869
- const err = new TransportSendError(qualifiedQueue, cause);
870
- errors.push({
871
- subscriberName: subscriber.name,
872
- queue: qualifiedQueue,
873
- error: err
874
- });
875
- await this.hooks.onEnqueueError({
876
- envelope,
877
- error: err,
878
- transport: this.transport.name
879
- });
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
+ }
880
922
  } finally {
881
923
  this.enqueuingCount--;
882
924
  }
@@ -888,6 +930,57 @@ var FanoutEngine = class {
888
930
  errors
889
931
  };
890
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
+ }
891
984
  async isSubscriberEnabled(subscriber) {
892
985
  if (!subscriber.enabled) {
893
986
  return true;
@@ -2354,6 +2447,7 @@ var Matador = class {
2354
2447
  if (!this.started) {
2355
2448
  return;
2356
2449
  }
2450
+ this.fanout.dispose();
2357
2451
  await this.shutdownManager.shutdown();
2358
2452
  this.started = false;
2359
2453
  }
@@ -2792,6 +2886,19 @@ var MultiTransport = class {
2792
2886
  isConnected() {
2793
2887
  return this.connected && this.primary.isConnected();
2794
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
+ }
2795
2902
  async applyTopology(topology) {
2796
2903
  await Promise.all(this.transports.map((t) => t.applyTopology(topology)));
2797
2904
  }
@@ -2941,6 +3048,18 @@ var RabbitMQTransport = class {
2941
3048
  isConnected() {
2942
3049
  return this.connectionManager.isConnected();
2943
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
+ }
2944
3063
  async applyTopology(topology) {
2945
3064
  this.topology = topology;
2946
3065
  if (!this.publishChannel) {
@@ -3219,6 +3338,14 @@ var RabbitMQTransport = class {
3219
3338
  return queueChannel;
3220
3339
  }
3221
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
+ }
3222
3349
  this.queueChannels.clear();
3223
3350
  this.logger.info(
3224
3351
  `[Matador] \u23F3 Connecting to RabbitMQ at '${redactAmqpUrl(this.config.url)}'.`
@@ -3236,21 +3363,33 @@ var RabbitMQTransport = class {
3236
3363
  consumer.active = false;
3237
3364
  }
3238
3365
  }
3366
+ this.connection = null;
3367
+ this.publishChannel = null;
3239
3368
  if (this.connectionManager.isConnected()) {
3240
3369
  this.connectionManager.handleConnectionLost(
3241
3370
  new Error("Connection closed unexpectedly")
3242
3371
  );
3243
3372
  }
3244
3373
  });
3245
- this.publishChannel = await connection.createConfirmChannel();
3246
- this.publishChannel.on("error", (err) => {
3247
- this.logger.error("[Matador] \u{1F534} RabbitMQ publish channel error", err);
3248
- });
3249
- if (this.topology) {
3250
- await this.applyTopology(this.topology);
3251
- for (const intent of this.subscriptionIntents) {
3252
- 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
+ }
3253
3384
  }
3385
+ } catch (err) {
3386
+ try {
3387
+ await connection.close();
3388
+ } catch {
3389
+ }
3390
+ this.connection = null;
3391
+ this.publishChannel = null;
3392
+ throw err;
3254
3393
  }
3255
3394
  this.logger.info("[Matador] \u{1F50C} Connected to RabbitMQ");
3256
3395
  }