mailery 0.1.2 → 0.2.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.
- package/README.md +4 -4
- package/dist/index.cjs +341 -131
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +341 -131
- package/dist/index.js.map +1 -1
- package/dist/{null-OzIqP7A8.d.cts → null-CWw3Gpbl.d.cts} +65 -43
- package/dist/{null-OzIqP7A8.d.ts → null-CWw3Gpbl.d.ts} +65 -43
- package/dist/testing.cjs +346 -136
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +3 -3
- package/dist/testing.d.ts +3 -3
- package/dist/testing.js +346 -136
- package/dist/testing.js.map +1 -1
- package/package.json +29 -4
package/dist/testing.cjs
CHANGED
|
@@ -4,7 +4,6 @@ var mongodb = require('mongodb');
|
|
|
4
4
|
var crypto2 = require('crypto');
|
|
5
5
|
var sgMail = require('@sendgrid/mail');
|
|
6
6
|
var zod = require('zod');
|
|
7
|
-
var bullmq = require('bullmq');
|
|
8
7
|
var IORedis = require('ioredis');
|
|
9
8
|
var Handlebars = require('handlebars');
|
|
10
9
|
var htmlToText = require('html-to-text');
|
|
@@ -9500,10 +9499,10 @@ var require_dist2 = __commonJS({
|
|
|
9500
9499
|
const proxyResponsePromise = (0, parse_proxy_response_1.parseProxyResponse)(socket);
|
|
9501
9500
|
socket.write(`${payload}\r
|
|
9502
9501
|
`);
|
|
9503
|
-
const { connect, buffered } = await proxyResponsePromise;
|
|
9504
|
-
req.emit("proxyConnect",
|
|
9505
|
-
this.emit("proxyConnect",
|
|
9506
|
-
if (
|
|
9502
|
+
const { connect: connect2, buffered } = await proxyResponsePromise;
|
|
9503
|
+
req.emit("proxyConnect", connect2);
|
|
9504
|
+
this.emit("proxyConnect", connect2, req);
|
|
9505
|
+
if (connect2.statusCode === 200) {
|
|
9507
9506
|
req.once("socket", resume);
|
|
9508
9507
|
if (opts.secureEndpoint) {
|
|
9509
9508
|
debug("Upgrading socket connection to TLS");
|
|
@@ -14384,7 +14383,8 @@ async function ensureIndexes(db, prefix = "mailer_") {
|
|
|
14384
14383
|
{ key: { flowRunId: 1 }, sparse: true },
|
|
14385
14384
|
{ key: { broadcastId: 1 }, sparse: true },
|
|
14386
14385
|
{ key: { providerMessageId: 1 }, sparse: true },
|
|
14387
|
-
{ key: { status: 1, queuedAt: 1 } }
|
|
14386
|
+
{ key: { status: 1, queuedAt: 1 } },
|
|
14387
|
+
{ key: { status: 1, updatedAt: 1 } }
|
|
14388
14388
|
]),
|
|
14389
14389
|
c.suppressions.createIndexes([
|
|
14390
14390
|
{ key: { email: 1, scope: 1 }, unique: true, partialFilterExpression: { email: { $type: "string" } } },
|
|
@@ -14476,6 +14476,104 @@ function signDoiToken(payload, secret) {
|
|
|
14476
14476
|
const hmac = crypto2__default.default.createHmac("sha256", secret).update(bodyB64).digest();
|
|
14477
14477
|
return `${bodyB64}.${b64url(hmac)}`;
|
|
14478
14478
|
}
|
|
14479
|
+
var QUEUE_NAMES = {
|
|
14480
|
+
tick: "mailer-tick",
|
|
14481
|
+
advance: "mailer-advance",
|
|
14482
|
+
send: "mailer-send",
|
|
14483
|
+
webhook: "mailer-webhook"
|
|
14484
|
+
};
|
|
14485
|
+
var BullDriver = class _BullDriver {
|
|
14486
|
+
queues;
|
|
14487
|
+
redis;
|
|
14488
|
+
bullQueues;
|
|
14489
|
+
workers = null;
|
|
14490
|
+
bull;
|
|
14491
|
+
static async create(redisConfig) {
|
|
14492
|
+
let bull;
|
|
14493
|
+
try {
|
|
14494
|
+
bull = await import('bullmq');
|
|
14495
|
+
} catch {
|
|
14496
|
+
throw new Error(
|
|
14497
|
+
"mailery: queue driver 'bull' requires the 'bullmq' peer dependency. Run `npm install bullmq ioredis`."
|
|
14498
|
+
);
|
|
14499
|
+
}
|
|
14500
|
+
const redis = isRedisLike(redisConfig) ? redisConfig : connect(redisConfig);
|
|
14501
|
+
return new _BullDriver(bull, redis);
|
|
14502
|
+
}
|
|
14503
|
+
constructor(bull, redis) {
|
|
14504
|
+
this.bull = bull;
|
|
14505
|
+
this.redis = redis;
|
|
14506
|
+
const opts = { connection: redis };
|
|
14507
|
+
this.bullQueues = {
|
|
14508
|
+
tick: new bull.Queue(QUEUE_NAMES.tick, opts),
|
|
14509
|
+
advance: new bull.Queue(QUEUE_NAMES.advance, opts),
|
|
14510
|
+
send: new bull.Queue(QUEUE_NAMES.send, opts),
|
|
14511
|
+
webhook: new bull.Queue(QUEUE_NAMES.webhook, opts)
|
|
14512
|
+
};
|
|
14513
|
+
this.queues = {
|
|
14514
|
+
tick: adaptBullQueue(this.bullQueues.tick),
|
|
14515
|
+
advance: adaptBullQueue(this.bullQueues.advance),
|
|
14516
|
+
send: adaptBullQueue(this.bullQueues.send),
|
|
14517
|
+
webhook: adaptBullQueue(this.bullQueues.webhook)
|
|
14518
|
+
};
|
|
14519
|
+
}
|
|
14520
|
+
async scheduleRepeatingTick(intervalSeconds) {
|
|
14521
|
+
await this.bullQueues.tick.upsertJobScheduler(
|
|
14522
|
+
"mailer-tick-repeat",
|
|
14523
|
+
{ every: intervalSeconds * 1e3 },
|
|
14524
|
+
{ name: "tick", data: {} }
|
|
14525
|
+
);
|
|
14526
|
+
}
|
|
14527
|
+
async startWorkers(opts) {
|
|
14528
|
+
if (this.workers) return;
|
|
14529
|
+
const base = { connection: this.redis };
|
|
14530
|
+
const { Worker } = this.bull;
|
|
14531
|
+
const tick = new Worker(
|
|
14532
|
+
QUEUE_NAMES.tick,
|
|
14533
|
+
async (job) => opts.handlers.tick(job.data),
|
|
14534
|
+
{ ...base, concurrency: 1 }
|
|
14535
|
+
);
|
|
14536
|
+
const advance = new Worker(
|
|
14537
|
+
QUEUE_NAMES.advance,
|
|
14538
|
+
async (job) => opts.handlers.advance(job.data),
|
|
14539
|
+
{ ...base, concurrency: 10 }
|
|
14540
|
+
);
|
|
14541
|
+
const send = new Worker(
|
|
14542
|
+
QUEUE_NAMES.send,
|
|
14543
|
+
async (job) => opts.handlers.send(job.data),
|
|
14544
|
+
{
|
|
14545
|
+
...base,
|
|
14546
|
+
concurrency: opts.concurrency.send,
|
|
14547
|
+
limiter: opts.sendRateLimit ? { max: opts.sendRateLimit.max, duration: opts.sendRateLimit.durationMs } : void 0
|
|
14548
|
+
}
|
|
14549
|
+
);
|
|
14550
|
+
const webhook = new Worker(
|
|
14551
|
+
QUEUE_NAMES.webhook,
|
|
14552
|
+
async (job) => opts.handlers.webhook(job.data),
|
|
14553
|
+
{ ...base, concurrency: 4 }
|
|
14554
|
+
);
|
|
14555
|
+
this.workers = { tick, advance, send, webhook };
|
|
14556
|
+
}
|
|
14557
|
+
async stopWorkers() {
|
|
14558
|
+
if (!this.workers) return;
|
|
14559
|
+
await Promise.all([
|
|
14560
|
+
this.workers.tick.close(),
|
|
14561
|
+
this.workers.advance.close(),
|
|
14562
|
+
this.workers.send.close(),
|
|
14563
|
+
this.workers.webhook.close()
|
|
14564
|
+
]);
|
|
14565
|
+
this.workers = null;
|
|
14566
|
+
}
|
|
14567
|
+
async close() {
|
|
14568
|
+
await this.stopWorkers();
|
|
14569
|
+
await Promise.all([
|
|
14570
|
+
this.bullQueues.tick.close(),
|
|
14571
|
+
this.bullQueues.advance.close(),
|
|
14572
|
+
this.bullQueues.send.close(),
|
|
14573
|
+
this.bullQueues.webhook.close()
|
|
14574
|
+
]);
|
|
14575
|
+
}
|
|
14576
|
+
};
|
|
14479
14577
|
function adaptBullQueue(q) {
|
|
14480
14578
|
return {
|
|
14481
14579
|
add: (name, data, opts) => q.add(name, data, opts),
|
|
@@ -14483,39 +14581,16 @@ function adaptBullQueue(q) {
|
|
|
14483
14581
|
close: () => q.close()
|
|
14484
14582
|
};
|
|
14485
14583
|
}
|
|
14486
|
-
function
|
|
14487
|
-
return
|
|
14488
|
-
add: async () => void 0,
|
|
14489
|
-
getWaitingCount: async () => 0,
|
|
14490
|
-
close: async () => void 0
|
|
14491
|
-
};
|
|
14492
|
-
}
|
|
14493
|
-
function noopQueues() {
|
|
14494
|
-
return {
|
|
14495
|
-
tick: noopQueueAPI(),
|
|
14496
|
-
advance: noopQueueAPI(),
|
|
14497
|
-
send: noopQueueAPI(),
|
|
14498
|
-
webhook: noopQueueAPI()
|
|
14499
|
-
};
|
|
14500
|
-
}
|
|
14501
|
-
function namespacedQueueNames(prefix) {
|
|
14502
|
-
return {
|
|
14503
|
-
tick: "mailer-tick",
|
|
14504
|
-
advance: "mailer-advance",
|
|
14505
|
-
send: "mailer-send",
|
|
14506
|
-
webhook: "mailer-webhook"
|
|
14507
|
-
};
|
|
14584
|
+
function isRedisLike(x) {
|
|
14585
|
+
return !!x && typeof x === "object" && typeof x.get === "function" && typeof x.set === "function";
|
|
14508
14586
|
}
|
|
14509
|
-
function
|
|
14510
|
-
if (isRedisLike(opts)) return opts;
|
|
14587
|
+
function connect(opts) {
|
|
14511
14588
|
const config = {
|
|
14512
14589
|
maxRetriesPerRequest: null,
|
|
14513
14590
|
// BullMQ requirement
|
|
14514
14591
|
enableReadyCheck: false
|
|
14515
14592
|
};
|
|
14516
|
-
if (opts.url)
|
|
14517
|
-
return new IORedis__default.default(opts.url, config);
|
|
14518
|
-
}
|
|
14593
|
+
if (opts.url) return new IORedis__default.default(opts.url, config);
|
|
14519
14594
|
return new IORedis__default.default({
|
|
14520
14595
|
...config,
|
|
14521
14596
|
host: opts.host ?? "127.0.0.1",
|
|
@@ -14526,69 +14601,198 @@ function makeRedis(opts) {
|
|
|
14526
14601
|
tls: opts.tls ? {} : void 0
|
|
14527
14602
|
});
|
|
14528
14603
|
}
|
|
14529
|
-
|
|
14530
|
-
|
|
14531
|
-
|
|
14532
|
-
|
|
14533
|
-
|
|
14534
|
-
|
|
14535
|
-
|
|
14536
|
-
|
|
14537
|
-
|
|
14538
|
-
|
|
14539
|
-
|
|
14540
|
-
|
|
14604
|
+
|
|
14605
|
+
// src/server/queues/agenda.ts
|
|
14606
|
+
var QUEUE_NAMES2 = {
|
|
14607
|
+
tick: "mailer-tick",
|
|
14608
|
+
advance: "mailer-advance",
|
|
14609
|
+
send: "mailer-send",
|
|
14610
|
+
webhook: "mailer-webhook"
|
|
14611
|
+
};
|
|
14612
|
+
var AgendaDriver = class _AgendaDriver {
|
|
14613
|
+
queues;
|
|
14614
|
+
agenda;
|
|
14615
|
+
agendaMod;
|
|
14616
|
+
sendLimiter = null;
|
|
14617
|
+
started = false;
|
|
14618
|
+
static async create(opts) {
|
|
14619
|
+
let agendaMod;
|
|
14620
|
+
let backendMod;
|
|
14621
|
+
try {
|
|
14622
|
+
agendaMod = await import('agenda');
|
|
14623
|
+
backendMod = await import('@agendajs/mongo-backend');
|
|
14624
|
+
} catch {
|
|
14625
|
+
throw new Error(
|
|
14626
|
+
"mailery: queue driver 'agenda' requires the 'agenda' and '@agendajs/mongo-backend' peer dependencies. Run `npm install agenda @agendajs/mongo-backend bottleneck`."
|
|
14627
|
+
);
|
|
14628
|
+
}
|
|
14629
|
+
const backend = new backendMod.MongoBackend({
|
|
14630
|
+
mongo: opts.db,
|
|
14631
|
+
collection: opts.collectionName ?? "_mailerJobs"
|
|
14632
|
+
});
|
|
14633
|
+
const agenda = new agendaMod.Agenda({
|
|
14634
|
+
backend,
|
|
14635
|
+
processEvery: `${opts.processEverySeconds ?? 5} seconds`,
|
|
14636
|
+
defaultLockLifetime: (opts.lockLifetimeSeconds ?? 10 * 60) * 1e3,
|
|
14637
|
+
maxConcurrency: 50,
|
|
14638
|
+
defaultConcurrency: 5
|
|
14639
|
+
});
|
|
14640
|
+
return new _AgendaDriver(agenda, agendaMod, opts.db);
|
|
14641
|
+
}
|
|
14642
|
+
db;
|
|
14643
|
+
constructor(agenda, agendaMod, db) {
|
|
14644
|
+
this.agenda = agenda;
|
|
14645
|
+
this.agendaMod = agendaMod;
|
|
14646
|
+
this.db = db;
|
|
14647
|
+
this.queues = {
|
|
14648
|
+
tick: this.makeQueueAPI(QUEUE_NAMES2.tick),
|
|
14649
|
+
advance: this.makeQueueAPI(QUEUE_NAMES2.advance),
|
|
14650
|
+
send: this.makeQueueAPI(QUEUE_NAMES2.send),
|
|
14651
|
+
webhook: this.makeQueueAPI(QUEUE_NAMES2.webhook)
|
|
14652
|
+
};
|
|
14653
|
+
}
|
|
14654
|
+
makeQueueAPI(name) {
|
|
14655
|
+
return {
|
|
14656
|
+
add: async (_jobName, data, opts) => {
|
|
14657
|
+
const payload = { ...data };
|
|
14658
|
+
if (opts?.jobId) {
|
|
14659
|
+
payload.__jobId = opts.jobId;
|
|
14660
|
+
if (await this.findPending(name, opts.jobId)) return;
|
|
14661
|
+
}
|
|
14662
|
+
const job = this.agenda.create(name, payload);
|
|
14663
|
+
if (opts?.delay) job.schedule(new Date(Date.now() + opts.delay));
|
|
14664
|
+
await job.save();
|
|
14665
|
+
},
|
|
14666
|
+
getWaitingCount: async () => {
|
|
14667
|
+
return this.jobsCollection().countDocuments({
|
|
14668
|
+
name,
|
|
14669
|
+
$or: [{ lockedAt: null }, { lockedAt: { $exists: false } }],
|
|
14670
|
+
nextRunAt: { $lte: /* @__PURE__ */ new Date() }
|
|
14671
|
+
});
|
|
14672
|
+
},
|
|
14673
|
+
close: async () => {
|
|
14674
|
+
}
|
|
14675
|
+
};
|
|
14676
|
+
}
|
|
14677
|
+
/** Direct access to the Mongo collection Agenda persists jobs into. */
|
|
14678
|
+
jobsCollection() {
|
|
14679
|
+
return this.db.collection(this.collectionName());
|
|
14680
|
+
}
|
|
14681
|
+
collectionName() {
|
|
14682
|
+
return "_mailerJobs";
|
|
14683
|
+
}
|
|
14684
|
+
async findPending(name, jobId) {
|
|
14685
|
+
return this.jobsCollection().findOne({
|
|
14686
|
+
name,
|
|
14687
|
+
"data.__jobId": jobId,
|
|
14688
|
+
$or: [{ lastFinishedAt: null }, { lastFinishedAt: { $exists: false } }]
|
|
14689
|
+
});
|
|
14690
|
+
}
|
|
14691
|
+
async scheduleRepeatingTick(intervalSeconds) {
|
|
14692
|
+
if (!this.started) {
|
|
14693
|
+
this.agenda.define(QUEUE_NAMES2.tick, async () => {
|
|
14694
|
+
}, { concurrency: 1 });
|
|
14695
|
+
await this.agenda.start();
|
|
14696
|
+
this.started = true;
|
|
14697
|
+
}
|
|
14698
|
+
await this.agenda.every(`${intervalSeconds} seconds`, QUEUE_NAMES2.tick);
|
|
14699
|
+
}
|
|
14700
|
+
async startWorkers(opts) {
|
|
14701
|
+
const exp = this.agendaMod.backoffStrategies.exponential;
|
|
14702
|
+
if (opts.sendRateLimit) {
|
|
14703
|
+
try {
|
|
14704
|
+
const Bottleneck = (await import('bottleneck')).default;
|
|
14705
|
+
this.sendLimiter = new Bottleneck({
|
|
14706
|
+
minTime: Math.ceil(opts.sendRateLimit.durationMs / opts.sendRateLimit.max),
|
|
14707
|
+
maxConcurrent: opts.concurrency.send
|
|
14708
|
+
});
|
|
14709
|
+
} catch {
|
|
14710
|
+
throw new Error(
|
|
14711
|
+
"mailery: queue driver 'agenda' with sendRateLimit requires the 'bottleneck' peer dependency. Run `npm install bottleneck`."
|
|
14712
|
+
);
|
|
14713
|
+
}
|
|
14714
|
+
}
|
|
14715
|
+
const retryBackoff = exp({ delay: 6e4, maxRetries: Math.max(0, opts.retryAttempts - 1), factor: 2 });
|
|
14716
|
+
this.agenda.define(QUEUE_NAMES2.tick, async (job) => {
|
|
14717
|
+
await opts.handlers.tick(job.attrs.data);
|
|
14718
|
+
}, { concurrency: 1 });
|
|
14719
|
+
this.agenda.define(QUEUE_NAMES2.advance, async (job) => {
|
|
14720
|
+
await opts.handlers.advance(job.attrs.data);
|
|
14721
|
+
}, { concurrency: 10, backoff: retryBackoff });
|
|
14722
|
+
this.agenda.define(QUEUE_NAMES2.send, async (job) => {
|
|
14723
|
+
const data = job.attrs.data;
|
|
14724
|
+
if (this.sendLimiter) {
|
|
14725
|
+
await this.sendLimiter.schedule(() => opts.handlers.send(data));
|
|
14726
|
+
} else {
|
|
14727
|
+
await opts.handlers.send(data);
|
|
14728
|
+
}
|
|
14729
|
+
}, { concurrency: opts.concurrency.send, backoff: retryBackoff });
|
|
14730
|
+
this.agenda.define(QUEUE_NAMES2.webhook, async (job) => {
|
|
14731
|
+
await opts.handlers.webhook(job.attrs.data);
|
|
14732
|
+
}, { concurrency: 4, backoff: retryBackoff });
|
|
14733
|
+
if (!this.started) {
|
|
14734
|
+
await this.agenda.start();
|
|
14735
|
+
this.started = true;
|
|
14736
|
+
}
|
|
14737
|
+
}
|
|
14738
|
+
async stopWorkers() {
|
|
14739
|
+
if (!this.started) return;
|
|
14740
|
+
await this.agenda.stop();
|
|
14741
|
+
this.started = false;
|
|
14742
|
+
if (this.sendLimiter) {
|
|
14743
|
+
await this.sendLimiter.stop({ dropWaitingJobs: true }).catch(() => {
|
|
14744
|
+
});
|
|
14745
|
+
this.sendLimiter = null;
|
|
14746
|
+
}
|
|
14747
|
+
}
|
|
14748
|
+
async close() {
|
|
14749
|
+
await this.stopWorkers();
|
|
14750
|
+
}
|
|
14751
|
+
};
|
|
14752
|
+
|
|
14753
|
+
// src/server/queues/noop.ts
|
|
14754
|
+
function noopQueueAPI() {
|
|
14541
14755
|
return {
|
|
14542
|
-
|
|
14543
|
-
|
|
14544
|
-
|
|
14545
|
-
send: adaptBullQueue(bullQueues.send),
|
|
14546
|
-
webhook: adaptBullQueue(bullQueues.webhook)
|
|
14547
|
-
},
|
|
14548
|
-
bullQueues
|
|
14756
|
+
add: async () => void 0,
|
|
14757
|
+
getWaitingCount: async () => 0,
|
|
14758
|
+
close: async () => void 0
|
|
14549
14759
|
};
|
|
14550
14760
|
}
|
|
14551
|
-
|
|
14552
|
-
|
|
14553
|
-
|
|
14554
|
-
|
|
14555
|
-
|
|
14556
|
-
|
|
14557
|
-
}
|
|
14558
|
-
function createWorkers(input) {
|
|
14559
|
-
const names = namespacedQueueNames();
|
|
14560
|
-
const base = { connection: input.redis };
|
|
14561
|
-
const tick = new bullmq.Worker(names.tick, async (job) => input.handlers.tick(job.data), {
|
|
14562
|
-
...base,
|
|
14563
|
-
concurrency: 1
|
|
14564
|
-
// single tick driver per worker process
|
|
14565
|
-
});
|
|
14566
|
-
const advance = new bullmq.Worker(
|
|
14567
|
-
names.advance,
|
|
14568
|
-
async (job) => input.handlers.advance(job.data),
|
|
14569
|
-
{ ...base, concurrency: 10 }
|
|
14570
|
-
);
|
|
14571
|
-
const sendOpts = {
|
|
14572
|
-
...base,
|
|
14573
|
-
concurrency: input.concurrency.send,
|
|
14574
|
-
limiter: input.sendRateLimit ? { max: input.sendRateLimit.max, duration: input.sendRateLimit.durationMs } : void 0
|
|
14761
|
+
var NoopDriver = class {
|
|
14762
|
+
queues = {
|
|
14763
|
+
tick: noopQueueAPI(),
|
|
14764
|
+
advance: noopQueueAPI(),
|
|
14765
|
+
send: noopQueueAPI(),
|
|
14766
|
+
webhook: noopQueueAPI()
|
|
14575
14767
|
};
|
|
14576
|
-
|
|
14577
|
-
|
|
14578
|
-
|
|
14579
|
-
|
|
14580
|
-
|
|
14581
|
-
|
|
14582
|
-
|
|
14583
|
-
}
|
|
14584
|
-
|
|
14585
|
-
|
|
14586
|
-
|
|
14587
|
-
async function
|
|
14588
|
-
|
|
14589
|
-
|
|
14590
|
-
|
|
14591
|
-
|
|
14768
|
+
async scheduleRepeatingTick(_intervalSeconds) {
|
|
14769
|
+
}
|
|
14770
|
+
async startWorkers(_opts) {
|
|
14771
|
+
}
|
|
14772
|
+
async stopWorkers() {
|
|
14773
|
+
}
|
|
14774
|
+
async close() {
|
|
14775
|
+
}
|
|
14776
|
+
};
|
|
14777
|
+
|
|
14778
|
+
// src/server/queues/index.ts
|
|
14779
|
+
async function createQueueDriver(config, fallbackDb) {
|
|
14780
|
+
switch (config.driver) {
|
|
14781
|
+
case "bull":
|
|
14782
|
+
return BullDriver.create(config.redis);
|
|
14783
|
+
case "agenda":
|
|
14784
|
+
return AgendaDriver.create({
|
|
14785
|
+
db: config.db ?? fallbackDb,
|
|
14786
|
+
processEverySeconds: config.processEverySeconds,
|
|
14787
|
+
lockLifetimeSeconds: config.lockLifetimeSeconds,
|
|
14788
|
+
collectionName: config.collectionName
|
|
14789
|
+
});
|
|
14790
|
+
case "noop":
|
|
14791
|
+
return new NoopDriver();
|
|
14792
|
+
default: {
|
|
14793
|
+
throw new Error(`mailery: unknown queue driver`);
|
|
14794
|
+
}
|
|
14795
|
+
}
|
|
14592
14796
|
}
|
|
14593
14797
|
|
|
14594
14798
|
// src/server/runner/triggers.ts
|
|
@@ -15075,7 +15279,8 @@ async function dispatchSend(sendId, ctx) {
|
|
|
15075
15279
|
status: "sending",
|
|
15076
15280
|
fromName: rendered.fromName,
|
|
15077
15281
|
fromEmail: rendered.fromEmail,
|
|
15078
|
-
subject: rendered.subject
|
|
15282
|
+
subject: rendered.subject,
|
|
15283
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
15079
15284
|
}
|
|
15080
15285
|
}
|
|
15081
15286
|
);
|
|
@@ -15149,6 +15354,7 @@ function buildRenderContext(contact, run, vars, ctx) {
|
|
|
15149
15354
|
};
|
|
15150
15355
|
}
|
|
15151
15356
|
function newSendDoc(input) {
|
|
15357
|
+
const now = /* @__PURE__ */ new Date();
|
|
15152
15358
|
return {
|
|
15153
15359
|
_id: input._id,
|
|
15154
15360
|
dedupeKey: input.dedupeKey,
|
|
@@ -15179,7 +15385,8 @@ function newSendDoc(input) {
|
|
|
15179
15385
|
clickedLinks: [],
|
|
15180
15386
|
unsubscribedAt: null,
|
|
15181
15387
|
complainedAt: null,
|
|
15182
|
-
queuedAt:
|
|
15388
|
+
queuedAt: now,
|
|
15389
|
+
updatedAt: now,
|
|
15183
15390
|
sentAt: null,
|
|
15184
15391
|
deliveredAt: null
|
|
15185
15392
|
};
|
|
@@ -15661,6 +15868,7 @@ async function buildSendDoc(broadcast, template, contact, ctx, scheduledMs, resp
|
|
|
15661
15868
|
unsubscribedAt: null,
|
|
15662
15869
|
complainedAt: null,
|
|
15663
15870
|
queuedAt: /* @__PURE__ */ new Date(),
|
|
15871
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
15664
15872
|
sentAt: null,
|
|
15665
15873
|
deliveredAt: null
|
|
15666
15874
|
};
|
|
@@ -15733,6 +15941,7 @@ async function promoteSoftBounces(ctx) {
|
|
|
15733
15941
|
}
|
|
15734
15942
|
|
|
15735
15943
|
// src/server/runner/tick.ts
|
|
15944
|
+
var STRANDED_SEND_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
15736
15945
|
async function runTick(ctx) {
|
|
15737
15946
|
await processNewlyFiredEventTriggers(ctx).catch((err) => {
|
|
15738
15947
|
console.error("mailery: triggers scan failed", err);
|
|
@@ -15740,6 +15949,9 @@ async function runTick(ctx) {
|
|
|
15740
15949
|
await sweepStrandedFlowRuns(ctx).catch((err) => {
|
|
15741
15950
|
console.error("mailery: sweep failed", err);
|
|
15742
15951
|
});
|
|
15952
|
+
await sweepStrandedSends(ctx).catch((err) => {
|
|
15953
|
+
console.error("mailery: stranded-send sweep failed", err);
|
|
15954
|
+
});
|
|
15743
15955
|
await drainOutbox(ctx).catch((err) => {
|
|
15744
15956
|
console.error("mailery: outbox drain failed", err);
|
|
15745
15957
|
});
|
|
@@ -15753,6 +15965,24 @@ async function runTick(ctx) {
|
|
|
15753
15965
|
console.error("mailery: soft-bounce promotion failed", err);
|
|
15754
15966
|
});
|
|
15755
15967
|
}
|
|
15968
|
+
async function sweepStrandedSends(ctx) {
|
|
15969
|
+
const cutoff = new Date(Date.now() - STRANDED_SEND_THRESHOLD_MS);
|
|
15970
|
+
const cursor = ctx.collections.sends.find(
|
|
15971
|
+
{ status: "sending", updatedAt: { $lt: cutoff } },
|
|
15972
|
+
{ projection: { _id: 1 } }
|
|
15973
|
+
).limit(500);
|
|
15974
|
+
for await (const row of cursor) {
|
|
15975
|
+
const reset = await ctx.collections.sends.updateOne(
|
|
15976
|
+
{ _id: row._id, status: "sending", updatedAt: { $lt: cutoff } },
|
|
15977
|
+
{ $set: { status: "queued", updatedAt: /* @__PURE__ */ new Date() } }
|
|
15978
|
+
);
|
|
15979
|
+
if (reset.modifiedCount === 0) continue;
|
|
15980
|
+
await ctx.queues.send.add("send", { sendId: String(row._id) }, {
|
|
15981
|
+
attempts: ctx.config.sendRetryAttempts,
|
|
15982
|
+
backoff: { type: "exponential", delay: 6e4 }
|
|
15983
|
+
});
|
|
15984
|
+
}
|
|
15985
|
+
}
|
|
15756
15986
|
async function drainOutbox(ctx) {
|
|
15757
15987
|
const batch = await ctx.collections.outbox.find({ status: "pending" }).sort({ enqueuedAt: 1 }).limit(200).toArray();
|
|
15758
15988
|
for (const row of batch) {
|
|
@@ -15923,12 +16153,11 @@ var Mailer = class _Mailer {
|
|
|
15923
16153
|
collections;
|
|
15924
16154
|
adapter;
|
|
15925
16155
|
providers;
|
|
15926
|
-
redis;
|
|
15927
16156
|
queues;
|
|
15928
16157
|
config;
|
|
15929
16158
|
events;
|
|
15930
|
-
|
|
15931
|
-
|
|
16159
|
+
queueDriver;
|
|
16160
|
+
workersStarted = false;
|
|
15932
16161
|
runnerContext;
|
|
15933
16162
|
constructor(args) {
|
|
15934
16163
|
this.config = args.config;
|
|
@@ -15936,9 +16165,8 @@ var Mailer = class _Mailer {
|
|
|
15936
16165
|
this.collections = args.collections;
|
|
15937
16166
|
this.adapter = args.adapter;
|
|
15938
16167
|
this.providers = args.providers;
|
|
15939
|
-
this.
|
|
15940
|
-
this.queues = args.queues;
|
|
15941
|
-
this.bullQueues = args.bullQueues;
|
|
16168
|
+
this.queueDriver = args.queueDriver;
|
|
16169
|
+
this.queues = args.queueDriver.queues;
|
|
15942
16170
|
this.events = args.events;
|
|
15943
16171
|
this.runnerContext = {
|
|
15944
16172
|
db: this.db,
|
|
@@ -16003,10 +16231,12 @@ var Mailer = class _Mailer {
|
|
|
16003
16231
|
throw new Error("Mailer.fromEnv: no provider configured (set MAILER_SENDGRID_API_KEY, ...)");
|
|
16004
16232
|
}
|
|
16005
16233
|
const defaultProvider = env.MAILER_DEFAULT_PROVIDER ?? Object.keys(providers)[0];
|
|
16234
|
+
const driverEnv = env.MAILER_QUEUE_DRIVER ?? "bull";
|
|
16235
|
+
const queue = driverEnv === "agenda" ? { driver: "agenda" } : driverEnv === "noop" ? { driver: "noop" } : { driver: "bull", redis: { url: required("MAILER_REDIS_URL") } };
|
|
16006
16236
|
return _Mailer.init({
|
|
16007
16237
|
db,
|
|
16008
16238
|
adapter,
|
|
16009
|
-
|
|
16239
|
+
queue,
|
|
16010
16240
|
providers,
|
|
16011
16241
|
defaultProvider,
|
|
16012
16242
|
publicUrl: required("MAILER_PUBLIC_URL"),
|
|
@@ -16022,19 +16252,9 @@ var Mailer = class _Mailer {
|
|
|
16022
16252
|
}
|
|
16023
16253
|
const collections = getCollections(config.db, config.collectionPrefix);
|
|
16024
16254
|
await ensureIndexes(config.db, config.collectionPrefix);
|
|
16025
|
-
|
|
16026
|
-
|
|
16027
|
-
|
|
16028
|
-
if (config.redis === null) {
|
|
16029
|
-
queues = noopQueues();
|
|
16030
|
-
} else {
|
|
16031
|
-
redis = makeRedis(config.redis);
|
|
16032
|
-
const created = createQueues(redis);
|
|
16033
|
-
queues = created.queues;
|
|
16034
|
-
bullQueues = created.bullQueues;
|
|
16035
|
-
if (!config.workerless) {
|
|
16036
|
-
await scheduleTick(bullQueues, config.tickIntervalSeconds);
|
|
16037
|
-
}
|
|
16255
|
+
const queueDriver = await createQueueDriver(config.queue, config.db);
|
|
16256
|
+
if (!config.workerless && config.queue.driver !== "noop") {
|
|
16257
|
+
await queueDriver.scheduleRepeatingTick(config.tickIntervalSeconds);
|
|
16038
16258
|
}
|
|
16039
16259
|
return new _Mailer({
|
|
16040
16260
|
config,
|
|
@@ -16042,9 +16262,7 @@ var Mailer = class _Mailer {
|
|
|
16042
16262
|
collections,
|
|
16043
16263
|
adapter: config.adapter,
|
|
16044
16264
|
providers: config.providers,
|
|
16045
|
-
|
|
16046
|
-
queues,
|
|
16047
|
-
bullQueues,
|
|
16265
|
+
queueDriver,
|
|
16048
16266
|
events: new EventRegistry()
|
|
16049
16267
|
});
|
|
16050
16268
|
}
|
|
@@ -16352,6 +16570,7 @@ var Mailer = class _Mailer {
|
|
|
16352
16570
|
unsubscribedAt: null,
|
|
16353
16571
|
complainedAt: null,
|
|
16354
16572
|
queuedAt: /* @__PURE__ */ new Date(),
|
|
16573
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
16355
16574
|
sentAt: null,
|
|
16356
16575
|
deliveredAt: null
|
|
16357
16576
|
});
|
|
@@ -16383,14 +16602,16 @@ var Mailer = class _Mailer {
|
|
|
16383
16602
|
// Workers
|
|
16384
16603
|
// -------------------------------------------------------------------------
|
|
16385
16604
|
async startWorkers() {
|
|
16386
|
-
if (this.
|
|
16387
|
-
if (
|
|
16605
|
+
if (this.workersStarted) return;
|
|
16606
|
+
if (this.config.queue.driver === "noop") {
|
|
16607
|
+
throw new Error("startWorkers requires a non-noop queue driver");
|
|
16608
|
+
}
|
|
16388
16609
|
const provider = this.providers[this.config.defaultProvider];
|
|
16389
16610
|
const sendRate = provider?.sendRatePerSecond ?? this.config.sendRatePerSecond;
|
|
16390
|
-
this.
|
|
16391
|
-
redis: this.redis,
|
|
16611
|
+
await this.queueDriver.startWorkers({
|
|
16392
16612
|
concurrency: { send: this.config.sendConcurrency },
|
|
16393
16613
|
sendRateLimit: { max: sendRate, durationMs: 1e3 },
|
|
16614
|
+
retryAttempts: this.config.sendRetryAttempts,
|
|
16394
16615
|
handlers: {
|
|
16395
16616
|
tick: async () => {
|
|
16396
16617
|
await runTick(this.runnerContext);
|
|
@@ -16408,6 +16629,7 @@ var Mailer = class _Mailer {
|
|
|
16408
16629
|
}
|
|
16409
16630
|
}
|
|
16410
16631
|
});
|
|
16632
|
+
this.workersStarted = true;
|
|
16411
16633
|
}
|
|
16412
16634
|
/** Process unprocessed webhook events in mailer_webhook_events. */
|
|
16413
16635
|
async processWebhookBacklog() {
|
|
@@ -16434,20 +16656,8 @@ var Mailer = class _Mailer {
|
|
|
16434
16656
|
}
|
|
16435
16657
|
}
|
|
16436
16658
|
async stop() {
|
|
16437
|
-
|
|
16438
|
-
|
|
16439
|
-
this.workers = null;
|
|
16440
|
-
}
|
|
16441
|
-
if (this.bullQueues) {
|
|
16442
|
-
await closeBullQueues(this.bullQueues);
|
|
16443
|
-
this.bullQueues = null;
|
|
16444
|
-
} else {
|
|
16445
|
-
await closeQueues(this.queues);
|
|
16446
|
-
}
|
|
16447
|
-
if (this.redis) {
|
|
16448
|
-
await this.redis.quit().catch(() => {
|
|
16449
|
-
});
|
|
16450
|
-
}
|
|
16659
|
+
await this.queueDriver.close();
|
|
16660
|
+
this.workersStarted = false;
|
|
16451
16661
|
}
|
|
16452
16662
|
/** Used internally by the admin router and tests; not part of the public API. */
|
|
16453
16663
|
getRunnerContext() {
|
|
@@ -16543,7 +16753,7 @@ async function createTestMailer(opts = {}) {
|
|
|
16543
16753
|
const mailer = await Mailer.init({
|
|
16544
16754
|
db,
|
|
16545
16755
|
adapter,
|
|
16546
|
-
|
|
16756
|
+
queue: { driver: "noop" },
|
|
16547
16757
|
providers: { null: provider },
|
|
16548
16758
|
defaultProvider: "null",
|
|
16549
16759
|
publicUrl: "http://localhost:3000",
|