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.
@@ -1,6 +1,6 @@
1
1
  import { Db } from 'mongodb';
2
- import { C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, N as NormalizedEvent, F as FlowStep, a2 as FlowTrigger, m as FlowGoal, U as TemplateKind, T as TemplateDoc, l as FlowDoc, D as DeliveryWindow, R as RunnerContext, d as Mailer, a3 as QueueDriverConfig } from './null-CfzvqNEp.cjs';
3
- export { s as NullProvider } from './null-CfzvqNEp.cjs';
2
+ import { C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, N as NormalizedEvent, F as FlowStep, a2 as FlowTrigger, m as FlowGoal, U as TemplateKind, T as TemplateDoc, l as FlowDoc, D as DeliveryWindow, R as RunnerContext, d as Mailer, a3 as QueueDriverConfig } from './null-DDg3ojHe.cjs';
3
+ export { s as NullProvider } from './null-DDg3ojHe.cjs';
4
4
  import 'zod';
5
5
  import 'handlebars';
6
6
  import 'ioredis';
package/dist/testing.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Db } from 'mongodb';
2
- import { C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, N as NormalizedEvent, F as FlowStep, a2 as FlowTrigger, m as FlowGoal, U as TemplateKind, T as TemplateDoc, l as FlowDoc, D as DeliveryWindow, R as RunnerContext, d as Mailer, a3 as QueueDriverConfig } from './null-CfzvqNEp.js';
3
- export { s as NullProvider } from './null-CfzvqNEp.js';
2
+ import { C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, N as NormalizedEvent, F as FlowStep, a2 as FlowTrigger, m as FlowGoal, U as TemplateKind, T as TemplateDoc, l as FlowDoc, D as DeliveryWindow, R as RunnerContext, d as Mailer, a3 as QueueDriverConfig } from './null-DDg3ojHe.js';
3
+ export { s as NullProvider } from './null-DDg3ojHe.js';
4
4
  import 'zod';
5
5
  import 'handlebars';
6
6
  import 'ioredis';
package/dist/testing.js CHANGED
@@ -14795,6 +14795,9 @@ var QUEUE_NAMES2 = {
14795
14795
  send: "mailer-send",
14796
14796
  webhook: "mailer-webhook"
14797
14797
  };
14798
+ var DEFAULT_COLLECTION = "_mailerJobs";
14799
+ var DEFAULT_FAILED_RETENTION_DAYS = 7;
14800
+ var FAILED_SWEEP_INTERVAL_MS = 60 * 60 * 1e3;
14798
14801
  var AgendaDriver = class _AgendaDriver {
14799
14802
  queues;
14800
14803
  agenda;
@@ -14812,7 +14815,7 @@ var AgendaDriver = class _AgendaDriver {
14812
14815
  "mailery: queue driver 'agenda' requires the 'agenda' and '@agendajs/mongo-backend' peer dependencies. Run `npm install agenda @agendajs/mongo-backend bottleneck`."
14813
14816
  );
14814
14817
  }
14815
- const collectionName = opts.collectionName ?? "_mailerJobs";
14818
+ const collectionName = opts.collectionName ?? collectionFor(opts.prefix);
14816
14819
  const backend = new backendMod.MongoBackend({
14817
14820
  mongo: opts.db,
14818
14821
  collection: collectionName
@@ -14822,17 +14825,29 @@ var AgendaDriver = class _AgendaDriver {
14822
14825
  processEvery: `${opts.processEverySeconds ?? 5} seconds`,
14823
14826
  defaultLockLifetime: (opts.lockLifetimeSeconds ?? 10 * 60) * 1e3,
14824
14827
  maxConcurrency: 50,
14825
- defaultConcurrency: 5
14828
+ defaultConcurrency: 5,
14829
+ // Drop succeeded one-shot jobs instead of retaining them forever.
14830
+ // Agenda guards this on `!nextRunAt`, so repeating jobs survive.
14831
+ removeOnComplete: true
14826
14832
  });
14827
- return new _AgendaDriver(agenda, agendaMod, opts.db, collectionName);
14833
+ const retentionDays = opts.failedJobRetentionDays ?? DEFAULT_FAILED_RETENTION_DAYS;
14834
+ try {
14835
+ await opts.db.collection(collectionName).createIndex({ failedAt: 1 }, { sparse: true });
14836
+ } catch (err) {
14837
+ console.error("mailery: could not index the Agenda jobs collection on failedAt", err);
14838
+ }
14839
+ return new _AgendaDriver(agenda, agendaMod, opts.db, collectionName, retentionDays);
14828
14840
  }
14829
14841
  db;
14830
14842
  collName;
14831
- constructor(agenda, agendaMod, db, collectionName) {
14843
+ failedRetentionDays;
14844
+ sweepTimer = null;
14845
+ constructor(agenda, agendaMod, db, collectionName, failedRetentionDays) {
14832
14846
  this.agenda = agenda;
14833
14847
  this.agendaMod = agendaMod;
14834
14848
  this.db = db;
14835
14849
  this.collName = collectionName;
14850
+ this.failedRetentionDays = failedRetentionDays;
14836
14851
  this.queues = {
14837
14852
  tick: this.makeQueueAPI(QUEUE_NAMES2.tick),
14838
14853
  advance: this.makeQueueAPI(QUEUE_NAMES2.advance),
@@ -14913,9 +14928,45 @@ var AgendaDriver = class _AgendaDriver {
14913
14928
  if (!this.started) {
14914
14929
  await this.agenda.start();
14915
14930
  this.started = true;
14931
+ this.startFailedJobSweep();
14932
+ }
14933
+ }
14934
+ /**
14935
+ * Agenda's auto-remove only fires on success, so failed documents would
14936
+ * otherwise be retained forever. Runs once on worker start, then hourly.
14937
+ */
14938
+ startFailedJobSweep() {
14939
+ if (this.failedRetentionDays <= 0 || this.sweepTimer) return;
14940
+ void this.sweepFailedJobs();
14941
+ this.sweepTimer = setInterval(() => void this.sweepFailedJobs(), FAILED_SWEEP_INTERVAL_MS);
14942
+ this.sweepTimer.unref?.();
14943
+ }
14944
+ /** Delete failed, fully-retired job documents older than the retention window. */
14945
+ async sweepFailedJobs() {
14946
+ const cutoff = new Date(Date.now() - this.failedRetentionDays * 24 * 3600 * 1e3);
14947
+ try {
14948
+ const res = await this.jobsCollection().deleteMany({
14949
+ failedAt: { $lt: cutoff },
14950
+ // Never touch the repeating tick.
14951
+ repeatInterval: { $in: [null, void 0] },
14952
+ // Leave anything still scheduled for a retry, and anything a worker
14953
+ // currently holds a lock on.
14954
+ $and: [
14955
+ { $or: [{ nextRunAt: null }, { nextRunAt: { $exists: false } }, { nextRunAt: { $lt: cutoff } }] },
14956
+ { $or: [{ lockedAt: null }, { lockedAt: { $exists: false } }, { lockedAt: { $lt: cutoff } }] }
14957
+ ]
14958
+ });
14959
+ return res.deletedCount ?? 0;
14960
+ } catch (err) {
14961
+ console.error("mailery: failed-job sweep failed", err);
14962
+ return 0;
14916
14963
  }
14917
14964
  }
14918
14965
  async stopWorkers() {
14966
+ if (this.sweepTimer) {
14967
+ clearInterval(this.sweepTimer);
14968
+ this.sweepTimer = null;
14969
+ }
14919
14970
  if (!this.started) return;
14920
14971
  await this.agenda.stop();
14921
14972
  this.started = false;
@@ -14929,6 +14980,15 @@ var AgendaDriver = class _AgendaDriver {
14929
14980
  await this.stopWorkers();
14930
14981
  }
14931
14982
  };
14983
+ function collectionFor(prefix) {
14984
+ if (!prefix) return DEFAULT_COLLECTION;
14985
+ if (!/^[A-Za-z0-9_-]+$/.test(prefix)) {
14986
+ throw new Error(
14987
+ `mailery: queue prefix "${prefix}" must contain only letters, digits, '_' or '-' \u2014 it becomes part of a MongoDB collection name.`
14988
+ );
14989
+ }
14990
+ return `${DEFAULT_COLLECTION}_${prefix}`;
14991
+ }
14932
14992
 
14933
14993
  // src/server/queues/noop.ts
14934
14994
  function noopQueueAPI() {
@@ -14965,7 +15025,9 @@ async function createQueueDriver(config, fallbackDb) {
14965
15025
  db: config.db ?? fallbackDb,
14966
15026
  processEverySeconds: config.processEverySeconds,
14967
15027
  lockLifetimeSeconds: config.lockLifetimeSeconds,
14968
- collectionName: config.collectionName
15028
+ collectionName: config.collectionName,
15029
+ prefix: config.prefix,
15030
+ failedJobRetentionDays: config.failedJobRetentionDays
14969
15031
  });
14970
15032
  case "noop":
14971
15033
  return new NoopDriver();
@@ -17405,7 +17467,8 @@ var Mailer = class _Mailer {
17405
17467
  * MAILER_MONGODB_URI — Mongo connection string (required)
17406
17468
  * MAILER_MONGODB_DB — database name (optional; defaults to the URI default)
17407
17469
  * MAILER_REDIS_URL — Redis connection URL (required)
17408
- * MAILER_QUEUE_PREFIX — Redis key prefix to namespace this instance (optional)
17470
+ * MAILER_QUEUE_PREFIX — namespaces this instance (optional). Bull: Redis key
17471
+ * prefix. Agenda: jobs collection suffix.
17409
17472
  * MAILER_PUBLIC_URL — base for tracking/unsub URLs (required)
17410
17473
  * MAILER_UNSUBSCRIBE_SECRET — HMAC key (required)
17411
17474
  * MAILER_SENDER_ADDRESS — postal address for CAN-SPAM (optional)
@@ -17454,7 +17517,7 @@ var Mailer = class _Mailer {
17454
17517
  }
17455
17518
  const defaultProvider = env.MAILER_DEFAULT_PROVIDER ?? Object.keys(providers)[0];
17456
17519
  const driverEnv = env.MAILER_QUEUE_DRIVER ?? "bull";
17457
- const queue = driverEnv === "agenda" ? { driver: "agenda" } : driverEnv === "noop" ? { driver: "noop" } : {
17520
+ const queue = driverEnv === "agenda" ? { driver: "agenda", prefix: env.MAILER_QUEUE_PREFIX } : driverEnv === "noop" ? { driver: "noop" } : {
17458
17521
  driver: "bull",
17459
17522
  redis: { url: required("MAILER_REDIS_URL") },
17460
17523
  prefix: env.MAILER_QUEUE_PREFIX