mailery 0.12.0 → 0.12.1

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
@@ -1069,6 +1069,9 @@ var QUEUE_NAMES2 = {
1069
1069
  send: "mailer-send",
1070
1070
  webhook: "mailer-webhook"
1071
1071
  };
1072
+ var DEFAULT_COLLECTION = "_mailerJobs";
1073
+ var DEFAULT_FAILED_RETENTION_DAYS = 7;
1074
+ var FAILED_SWEEP_INTERVAL_MS = 60 * 60 * 1e3;
1072
1075
  var AgendaDriver = class _AgendaDriver {
1073
1076
  queues;
1074
1077
  agenda;
@@ -1086,7 +1089,7 @@ var AgendaDriver = class _AgendaDriver {
1086
1089
  "mailery: queue driver 'agenda' requires the 'agenda' and '@agendajs/mongo-backend' peer dependencies. Run `npm install agenda @agendajs/mongo-backend bottleneck`."
1087
1090
  );
1088
1091
  }
1089
- const collectionName = opts.collectionName ?? "_mailerJobs";
1092
+ const collectionName = opts.collectionName ?? collectionFor(opts.prefix);
1090
1093
  const backend = new backendMod.MongoBackend({
1091
1094
  mongo: opts.db,
1092
1095
  collection: collectionName
@@ -1096,17 +1099,29 @@ var AgendaDriver = class _AgendaDriver {
1096
1099
  processEvery: `${opts.processEverySeconds ?? 5} seconds`,
1097
1100
  defaultLockLifetime: (opts.lockLifetimeSeconds ?? 10 * 60) * 1e3,
1098
1101
  maxConcurrency: 50,
1099
- defaultConcurrency: 5
1102
+ defaultConcurrency: 5,
1103
+ // Drop succeeded one-shot jobs instead of retaining them forever.
1104
+ // Agenda guards this on `!nextRunAt`, so repeating jobs survive.
1105
+ removeOnComplete: true
1100
1106
  });
1101
- return new _AgendaDriver(agenda, agendaMod, opts.db, collectionName);
1107
+ const retentionDays = opts.failedJobRetentionDays ?? DEFAULT_FAILED_RETENTION_DAYS;
1108
+ try {
1109
+ await opts.db.collection(collectionName).createIndex({ failedAt: 1 }, { sparse: true });
1110
+ } catch (err) {
1111
+ console.error("mailery: could not index the Agenda jobs collection on failedAt", err);
1112
+ }
1113
+ return new _AgendaDriver(agenda, agendaMod, opts.db, collectionName, retentionDays);
1102
1114
  }
1103
1115
  db;
1104
1116
  collName;
1105
- constructor(agenda, agendaMod, db, collectionName) {
1117
+ failedRetentionDays;
1118
+ sweepTimer = null;
1119
+ constructor(agenda, agendaMod, db, collectionName, failedRetentionDays) {
1106
1120
  this.agenda = agenda;
1107
1121
  this.agendaMod = agendaMod;
1108
1122
  this.db = db;
1109
1123
  this.collName = collectionName;
1124
+ this.failedRetentionDays = failedRetentionDays;
1110
1125
  this.queues = {
1111
1126
  tick: this.makeQueueAPI(QUEUE_NAMES2.tick),
1112
1127
  advance: this.makeQueueAPI(QUEUE_NAMES2.advance),
@@ -1187,9 +1202,45 @@ var AgendaDriver = class _AgendaDriver {
1187
1202
  if (!this.started) {
1188
1203
  await this.agenda.start();
1189
1204
  this.started = true;
1205
+ this.startFailedJobSweep();
1206
+ }
1207
+ }
1208
+ /**
1209
+ * Agenda's auto-remove only fires on success, so failed documents would
1210
+ * otherwise be retained forever. Runs once on worker start, then hourly.
1211
+ */
1212
+ startFailedJobSweep() {
1213
+ if (this.failedRetentionDays <= 0 || this.sweepTimer) return;
1214
+ void this.sweepFailedJobs();
1215
+ this.sweepTimer = setInterval(() => void this.sweepFailedJobs(), FAILED_SWEEP_INTERVAL_MS);
1216
+ this.sweepTimer.unref?.();
1217
+ }
1218
+ /** Delete failed, fully-retired job documents older than the retention window. */
1219
+ async sweepFailedJobs() {
1220
+ const cutoff = new Date(Date.now() - this.failedRetentionDays * 24 * 3600 * 1e3);
1221
+ try {
1222
+ const res = await this.jobsCollection().deleteMany({
1223
+ failedAt: { $lt: cutoff },
1224
+ // Never touch the repeating tick.
1225
+ repeatInterval: { $in: [null, void 0] },
1226
+ // Leave anything still scheduled for a retry, and anything a worker
1227
+ // currently holds a lock on.
1228
+ $and: [
1229
+ { $or: [{ nextRunAt: null }, { nextRunAt: { $exists: false } }, { nextRunAt: { $lt: cutoff } }] },
1230
+ { $or: [{ lockedAt: null }, { lockedAt: { $exists: false } }, { lockedAt: { $lt: cutoff } }] }
1231
+ ]
1232
+ });
1233
+ return res.deletedCount ?? 0;
1234
+ } catch (err) {
1235
+ console.error("mailery: failed-job sweep failed", err);
1236
+ return 0;
1190
1237
  }
1191
1238
  }
1192
1239
  async stopWorkers() {
1240
+ if (this.sweepTimer) {
1241
+ clearInterval(this.sweepTimer);
1242
+ this.sweepTimer = null;
1243
+ }
1193
1244
  if (!this.started) return;
1194
1245
  await this.agenda.stop();
1195
1246
  this.started = false;
@@ -1203,6 +1254,15 @@ var AgendaDriver = class _AgendaDriver {
1203
1254
  await this.stopWorkers();
1204
1255
  }
1205
1256
  };
1257
+ function collectionFor(prefix) {
1258
+ if (!prefix) return DEFAULT_COLLECTION;
1259
+ if (!/^[A-Za-z0-9_-]+$/.test(prefix)) {
1260
+ throw new Error(
1261
+ `mailery: queue prefix "${prefix}" must contain only letters, digits, '_' or '-' \u2014 it becomes part of a MongoDB collection name.`
1262
+ );
1263
+ }
1264
+ return `${DEFAULT_COLLECTION}_${prefix}`;
1265
+ }
1206
1266
 
1207
1267
  // src/server/queues/noop.ts
1208
1268
  function noopQueueAPI() {
@@ -1239,7 +1299,9 @@ async function createQueueDriver(config, fallbackDb) {
1239
1299
  db: config.db ?? fallbackDb,
1240
1300
  processEverySeconds: config.processEverySeconds,
1241
1301
  lockLifetimeSeconds: config.lockLifetimeSeconds,
1242
- collectionName: config.collectionName
1302
+ collectionName: config.collectionName,
1303
+ prefix: config.prefix,
1304
+ failedJobRetentionDays: config.failedJobRetentionDays
1243
1305
  });
1244
1306
  case "noop":
1245
1307
  return new NoopDriver();
@@ -3995,7 +4057,8 @@ var Mailer = class _Mailer {
3995
4057
  * MAILER_MONGODB_URI — Mongo connection string (required)
3996
4058
  * MAILER_MONGODB_DB — database name (optional; defaults to the URI default)
3997
4059
  * MAILER_REDIS_URL — Redis connection URL (required)
3998
- * MAILER_QUEUE_PREFIX — Redis key prefix to namespace this instance (optional)
4060
+ * MAILER_QUEUE_PREFIX — namespaces this instance (optional). Bull: Redis key
4061
+ * prefix. Agenda: jobs collection suffix.
3999
4062
  * MAILER_PUBLIC_URL — base for tracking/unsub URLs (required)
4000
4063
  * MAILER_UNSUBSCRIBE_SECRET — HMAC key (required)
4001
4064
  * MAILER_SENDER_ADDRESS — postal address for CAN-SPAM (optional)
@@ -4044,7 +4107,7 @@ var Mailer = class _Mailer {
4044
4107
  }
4045
4108
  const defaultProvider = env.MAILER_DEFAULT_PROVIDER ?? Object.keys(providers)[0];
4046
4109
  const driverEnv = env.MAILER_QUEUE_DRIVER ?? "bull";
4047
- const queue = driverEnv === "agenda" ? { driver: "agenda" } : driverEnv === "noop" ? { driver: "noop" } : {
4110
+ const queue = driverEnv === "agenda" ? { driver: "agenda", prefix: env.MAILER_QUEUE_PREFIX } : driverEnv === "noop" ? { driver: "noop" } : {
4048
4111
  driver: "bull",
4049
4112
  redis: { url: required("MAILER_REDIS_URL") },
4050
4113
  prefix: env.MAILER_QUEUE_PREFIX
@@ -7717,7 +7780,7 @@ var DEDUPE_POLICIES = [
7717
7780
  ];
7718
7781
 
7719
7782
  // src/server/index.ts
7720
- var VERSION = "0.12.0" ;
7783
+ var VERSION = "0.12.1" ;
7721
7784
 
7722
7785
  exports.DEDUPE_POLICIES = DEDUPE_POLICIES;
7723
7786
  exports.FLOW_STEP_KINDS = FLOW_STEP_KINDS;