mailery 0.12.0 → 0.13.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.
@@ -407,6 +407,20 @@ type QueueDriverConfig = {
407
407
  processEverySeconds?: number;
408
408
  lockLifetimeSeconds?: number;
409
409
  collectionName?: string;
410
+ /**
411
+ * Namespaces the jobs collection (`_mailerJobs_<prefix>`) so multiple
412
+ * mailery instances can share one Mongo database — the counterpart to
413
+ * the Bull driver's Redis key prefix, and read from the same
414
+ * `MAILER_QUEUE_PREFIX` env var. Ignored when `collectionName` is set.
415
+ * Letters, digits, '_' and '-' only.
416
+ */
417
+ prefix?: string;
418
+ /**
419
+ * Days to retain failed job documents before the driver sweeps them
420
+ * (default 7, matching the Bull driver's `removeOnFail` age). 0 disables
421
+ * the sweep. Succeeded jobs are removed on completion regardless.
422
+ */
423
+ failedJobRetentionDays?: number;
410
424
  } | {
411
425
  driver: 'noop';
412
426
  };
@@ -1420,7 +1434,8 @@ declare class Mailer {
1420
1434
  * MAILER_MONGODB_URI — Mongo connection string (required)
1421
1435
  * MAILER_MONGODB_DB — database name (optional; defaults to the URI default)
1422
1436
  * MAILER_REDIS_URL — Redis connection URL (required)
1423
- * MAILER_QUEUE_PREFIX — Redis key prefix to namespace this instance (optional)
1437
+ * MAILER_QUEUE_PREFIX — namespaces this instance (optional). Bull: Redis key
1438
+ * prefix. Agenda: jobs collection suffix.
1424
1439
  * MAILER_PUBLIC_URL — base for tracking/unsub URLs (required)
1425
1440
  * MAILER_UNSUBSCRIBE_SECRET — HMAC key (required)
1426
1441
  * MAILER_SENDER_ADDRESS — postal address for CAN-SPAM (optional)
@@ -407,6 +407,20 @@ type QueueDriverConfig = {
407
407
  processEverySeconds?: number;
408
408
  lockLifetimeSeconds?: number;
409
409
  collectionName?: string;
410
+ /**
411
+ * Namespaces the jobs collection (`_mailerJobs_<prefix>`) so multiple
412
+ * mailery instances can share one Mongo database — the counterpart to
413
+ * the Bull driver's Redis key prefix, and read from the same
414
+ * `MAILER_QUEUE_PREFIX` env var. Ignored when `collectionName` is set.
415
+ * Letters, digits, '_' and '-' only.
416
+ */
417
+ prefix?: string;
418
+ /**
419
+ * Days to retain failed job documents before the driver sweeps them
420
+ * (default 7, matching the Bull driver's `removeOnFail` age). 0 disables
421
+ * the sweep. Succeeded jobs are removed on completion regardless.
422
+ */
423
+ failedJobRetentionDays?: number;
410
424
  } | {
411
425
  driver: 'noop';
412
426
  };
@@ -1420,7 +1434,8 @@ declare class Mailer {
1420
1434
  * MAILER_MONGODB_URI — Mongo connection string (required)
1421
1435
  * MAILER_MONGODB_DB — database name (optional; defaults to the URI default)
1422
1436
  * MAILER_REDIS_URL — Redis connection URL (required)
1423
- * MAILER_QUEUE_PREFIX — Redis key prefix to namespace this instance (optional)
1437
+ * MAILER_QUEUE_PREFIX — namespaces this instance (optional). Bull: Redis key
1438
+ * prefix. Agenda: jobs collection suffix.
1424
1439
  * MAILER_PUBLIC_URL — base for tracking/unsub URLs (required)
1425
1440
  * MAILER_UNSUBSCRIBE_SECRET — HMAC key (required)
1426
1441
  * MAILER_SENDER_ADDRESS — postal address for CAN-SPAM (optional)
package/dist/testing.cjs CHANGED
@@ -14808,6 +14808,9 @@ var QUEUE_NAMES2 = {
14808
14808
  send: "mailer-send",
14809
14809
  webhook: "mailer-webhook"
14810
14810
  };
14811
+ var DEFAULT_COLLECTION = "_mailerJobs";
14812
+ var DEFAULT_FAILED_RETENTION_DAYS = 7;
14813
+ var FAILED_SWEEP_INTERVAL_MS = 60 * 60 * 1e3;
14811
14814
  var AgendaDriver = class _AgendaDriver {
14812
14815
  queues;
14813
14816
  agenda;
@@ -14825,7 +14828,7 @@ var AgendaDriver = class _AgendaDriver {
14825
14828
  "mailery: queue driver 'agenda' requires the 'agenda' and '@agendajs/mongo-backend' peer dependencies. Run `npm install agenda @agendajs/mongo-backend bottleneck`."
14826
14829
  );
14827
14830
  }
14828
- const collectionName = opts.collectionName ?? "_mailerJobs";
14831
+ const collectionName = opts.collectionName ?? collectionFor(opts.prefix);
14829
14832
  const backend = new backendMod.MongoBackend({
14830
14833
  mongo: opts.db,
14831
14834
  collection: collectionName
@@ -14835,17 +14838,29 @@ var AgendaDriver = class _AgendaDriver {
14835
14838
  processEvery: `${opts.processEverySeconds ?? 5} seconds`,
14836
14839
  defaultLockLifetime: (opts.lockLifetimeSeconds ?? 10 * 60) * 1e3,
14837
14840
  maxConcurrency: 50,
14838
- defaultConcurrency: 5
14841
+ defaultConcurrency: 5,
14842
+ // Drop succeeded one-shot jobs instead of retaining them forever.
14843
+ // Agenda guards this on `!nextRunAt`, so repeating jobs survive.
14844
+ removeOnComplete: true
14839
14845
  });
14840
- return new _AgendaDriver(agenda, agendaMod, opts.db, collectionName);
14846
+ const retentionDays = opts.failedJobRetentionDays ?? DEFAULT_FAILED_RETENTION_DAYS;
14847
+ try {
14848
+ await opts.db.collection(collectionName).createIndex({ failedAt: 1 }, { sparse: true });
14849
+ } catch (err) {
14850
+ console.error("mailery: could not index the Agenda jobs collection on failedAt", err);
14851
+ }
14852
+ return new _AgendaDriver(agenda, agendaMod, opts.db, collectionName, retentionDays);
14841
14853
  }
14842
14854
  db;
14843
14855
  collName;
14844
- constructor(agenda, agendaMod, db, collectionName) {
14856
+ failedRetentionDays;
14857
+ sweepTimer = null;
14858
+ constructor(agenda, agendaMod, db, collectionName, failedRetentionDays) {
14845
14859
  this.agenda = agenda;
14846
14860
  this.agendaMod = agendaMod;
14847
14861
  this.db = db;
14848
14862
  this.collName = collectionName;
14863
+ this.failedRetentionDays = failedRetentionDays;
14849
14864
  this.queues = {
14850
14865
  tick: this.makeQueueAPI(QUEUE_NAMES2.tick),
14851
14866
  advance: this.makeQueueAPI(QUEUE_NAMES2.advance),
@@ -14926,9 +14941,45 @@ var AgendaDriver = class _AgendaDriver {
14926
14941
  if (!this.started) {
14927
14942
  await this.agenda.start();
14928
14943
  this.started = true;
14944
+ this.startFailedJobSweep();
14945
+ }
14946
+ }
14947
+ /**
14948
+ * Agenda's auto-remove only fires on success, so failed documents would
14949
+ * otherwise be retained forever. Runs once on worker start, then hourly.
14950
+ */
14951
+ startFailedJobSweep() {
14952
+ if (this.failedRetentionDays <= 0 || this.sweepTimer) return;
14953
+ void this.sweepFailedJobs();
14954
+ this.sweepTimer = setInterval(() => void this.sweepFailedJobs(), FAILED_SWEEP_INTERVAL_MS);
14955
+ this.sweepTimer.unref?.();
14956
+ }
14957
+ /** Delete failed, fully-retired job documents older than the retention window. */
14958
+ async sweepFailedJobs() {
14959
+ const cutoff = new Date(Date.now() - this.failedRetentionDays * 24 * 3600 * 1e3);
14960
+ try {
14961
+ const res = await this.jobsCollection().deleteMany({
14962
+ failedAt: { $lt: cutoff },
14963
+ // Never touch the repeating tick.
14964
+ repeatInterval: { $in: [null, void 0] },
14965
+ // Leave anything still scheduled for a retry, and anything a worker
14966
+ // currently holds a lock on.
14967
+ $and: [
14968
+ { $or: [{ nextRunAt: null }, { nextRunAt: { $exists: false } }, { nextRunAt: { $lt: cutoff } }] },
14969
+ { $or: [{ lockedAt: null }, { lockedAt: { $exists: false } }, { lockedAt: { $lt: cutoff } }] }
14970
+ ]
14971
+ });
14972
+ return res.deletedCount ?? 0;
14973
+ } catch (err) {
14974
+ console.error("mailery: failed-job sweep failed", err);
14975
+ return 0;
14929
14976
  }
14930
14977
  }
14931
14978
  async stopWorkers() {
14979
+ if (this.sweepTimer) {
14980
+ clearInterval(this.sweepTimer);
14981
+ this.sweepTimer = null;
14982
+ }
14932
14983
  if (!this.started) return;
14933
14984
  await this.agenda.stop();
14934
14985
  this.started = false;
@@ -14942,6 +14993,15 @@ var AgendaDriver = class _AgendaDriver {
14942
14993
  await this.stopWorkers();
14943
14994
  }
14944
14995
  };
14996
+ function collectionFor(prefix) {
14997
+ if (!prefix) return DEFAULT_COLLECTION;
14998
+ if (!/^[A-Za-z0-9_-]+$/.test(prefix)) {
14999
+ throw new Error(
15000
+ `mailery: queue prefix "${prefix}" must contain only letters, digits, '_' or '-' \u2014 it becomes part of a MongoDB collection name.`
15001
+ );
15002
+ }
15003
+ return `${DEFAULT_COLLECTION}_${prefix}`;
15004
+ }
14945
15005
 
14946
15006
  // src/server/queues/noop.ts
14947
15007
  function noopQueueAPI() {
@@ -14978,7 +15038,9 @@ async function createQueueDriver(config, fallbackDb) {
14978
15038
  db: config.db ?? fallbackDb,
14979
15039
  processEverySeconds: config.processEverySeconds,
14980
15040
  lockLifetimeSeconds: config.lockLifetimeSeconds,
14981
- collectionName: config.collectionName
15041
+ collectionName: config.collectionName,
15042
+ prefix: config.prefix,
15043
+ failedJobRetentionDays: config.failedJobRetentionDays
14982
15044
  });
14983
15045
  case "noop":
14984
15046
  return new NoopDriver();
@@ -17418,7 +17480,8 @@ var Mailer = class _Mailer {
17418
17480
  * MAILER_MONGODB_URI — Mongo connection string (required)
17419
17481
  * MAILER_MONGODB_DB — database name (optional; defaults to the URI default)
17420
17482
  * MAILER_REDIS_URL — Redis connection URL (required)
17421
- * MAILER_QUEUE_PREFIX — Redis key prefix to namespace this instance (optional)
17483
+ * MAILER_QUEUE_PREFIX — namespaces this instance (optional). Bull: Redis key
17484
+ * prefix. Agenda: jobs collection suffix.
17422
17485
  * MAILER_PUBLIC_URL — base for tracking/unsub URLs (required)
17423
17486
  * MAILER_UNSUBSCRIBE_SECRET — HMAC key (required)
17424
17487
  * MAILER_SENDER_ADDRESS — postal address for CAN-SPAM (optional)
@@ -17467,7 +17530,7 @@ var Mailer = class _Mailer {
17467
17530
  }
17468
17531
  const defaultProvider = env.MAILER_DEFAULT_PROVIDER ?? Object.keys(providers)[0];
17469
17532
  const driverEnv = env.MAILER_QUEUE_DRIVER ?? "bull";
17470
- const queue = driverEnv === "agenda" ? { driver: "agenda" } : driverEnv === "noop" ? { driver: "noop" } : {
17533
+ const queue = driverEnv === "agenda" ? { driver: "agenda", prefix: env.MAILER_QUEUE_PREFIX } : driverEnv === "noop" ? { driver: "noop" } : {
17471
17534
  driver: "bull",
17472
17535
  redis: { url: required("MAILER_REDIS_URL") },
17473
17536
  prefix: env.MAILER_QUEUE_PREFIX