mailery 0.1.2 → 0.2.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/README.md +4 -4
- package/dist/admin/spa/index-csgOs39s.js +41 -0
- package/dist/admin/spa/index-csgOs39s.js.map +1 -0
- package/dist/admin/spa/index.html +1 -1
- package/dist/admin/spa/{template-editor-DUZfzI5F.js → template-editor-BuYSO1FP.js} +2 -2
- package/dist/admin/spa/{template-editor-DUZfzI5F.js.map → template-editor-BuYSO1FP.js.map} +1 -1
- package/dist/index.cjs +657 -133
- 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 +657 -134
- package/dist/index.js.map +1 -1
- package/dist/{null-OzIqP7A8.d.cts → null-DaisDvB_.d.cts} +123 -59
- package/dist/{null-OzIqP7A8.d.ts → null-DaisDvB_.d.ts} +123 -59
- package/dist/testing.cjs +365 -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 +365 -136
- package/dist/testing.js.map +1 -1
- package/package.json +29 -4
- package/dist/admin/spa/index-DlhleV6M.js +0 -41
- package/dist/admin/spa/index-DlhleV6M.js.map +0 -1
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,13 +15941,36 @@ 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) {
|
|
15946
|
+
await ctx.collections.health.updateOne(
|
|
15947
|
+
{ _id: "singleton" },
|
|
15948
|
+
{
|
|
15949
|
+
$set: { updatedAt: /* @__PURE__ */ new Date() },
|
|
15950
|
+
$setOnInsert: {
|
|
15951
|
+
_id: "singleton",
|
|
15952
|
+
windowStartedAt: /* @__PURE__ */ new Date(),
|
|
15953
|
+
windowDurationMs: ctx.config.circuitBreaker.windowMinutes * 60 * 1e3,
|
|
15954
|
+
status: "healthy",
|
|
15955
|
+
trippedAt: null,
|
|
15956
|
+
trippedReason: null,
|
|
15957
|
+
manuallyResumedAt: null,
|
|
15958
|
+
counters: { sent: 0, delivered: 0, bounced: 0, hardBounced: 0, softBounced: 0, complained: 0, failedToSend: 0 },
|
|
15959
|
+
rates: { bounceRate: 0, hardBounceRate: 0, complaintRate: 0, failureRate: 0 }
|
|
15960
|
+
}
|
|
15961
|
+
},
|
|
15962
|
+
{ upsert: true }
|
|
15963
|
+
).catch(() => {
|
|
15964
|
+
});
|
|
15737
15965
|
await processNewlyFiredEventTriggers(ctx).catch((err) => {
|
|
15738
15966
|
console.error("mailery: triggers scan failed", err);
|
|
15739
15967
|
});
|
|
15740
15968
|
await sweepStrandedFlowRuns(ctx).catch((err) => {
|
|
15741
15969
|
console.error("mailery: sweep failed", err);
|
|
15742
15970
|
});
|
|
15971
|
+
await sweepStrandedSends(ctx).catch((err) => {
|
|
15972
|
+
console.error("mailery: stranded-send sweep failed", err);
|
|
15973
|
+
});
|
|
15743
15974
|
await drainOutbox(ctx).catch((err) => {
|
|
15744
15975
|
console.error("mailery: outbox drain failed", err);
|
|
15745
15976
|
});
|
|
@@ -15753,6 +15984,24 @@ async function runTick(ctx) {
|
|
|
15753
15984
|
console.error("mailery: soft-bounce promotion failed", err);
|
|
15754
15985
|
});
|
|
15755
15986
|
}
|
|
15987
|
+
async function sweepStrandedSends(ctx) {
|
|
15988
|
+
const cutoff = new Date(Date.now() - STRANDED_SEND_THRESHOLD_MS);
|
|
15989
|
+
const cursor = ctx.collections.sends.find(
|
|
15990
|
+
{ status: "sending", updatedAt: { $lt: cutoff } },
|
|
15991
|
+
{ projection: { _id: 1 } }
|
|
15992
|
+
).limit(500);
|
|
15993
|
+
for await (const row of cursor) {
|
|
15994
|
+
const reset = await ctx.collections.sends.updateOne(
|
|
15995
|
+
{ _id: row._id, status: "sending", updatedAt: { $lt: cutoff } },
|
|
15996
|
+
{ $set: { status: "queued", updatedAt: /* @__PURE__ */ new Date() } }
|
|
15997
|
+
);
|
|
15998
|
+
if (reset.modifiedCount === 0) continue;
|
|
15999
|
+
await ctx.queues.send.add("send", { sendId: String(row._id) }, {
|
|
16000
|
+
attempts: ctx.config.sendRetryAttempts,
|
|
16001
|
+
backoff: { type: "exponential", delay: 6e4 }
|
|
16002
|
+
});
|
|
16003
|
+
}
|
|
16004
|
+
}
|
|
15756
16005
|
async function drainOutbox(ctx) {
|
|
15757
16006
|
const batch = await ctx.collections.outbox.find({ status: "pending" }).sort({ enqueuedAt: 1 }).limit(200).toArray();
|
|
15758
16007
|
for (const row of batch) {
|
|
@@ -15923,12 +16172,11 @@ var Mailer = class _Mailer {
|
|
|
15923
16172
|
collections;
|
|
15924
16173
|
adapter;
|
|
15925
16174
|
providers;
|
|
15926
|
-
redis;
|
|
15927
16175
|
queues;
|
|
15928
16176
|
config;
|
|
15929
16177
|
events;
|
|
15930
|
-
|
|
15931
|
-
|
|
16178
|
+
queueDriver;
|
|
16179
|
+
workersStarted = false;
|
|
15932
16180
|
runnerContext;
|
|
15933
16181
|
constructor(args) {
|
|
15934
16182
|
this.config = args.config;
|
|
@@ -15936,9 +16184,8 @@ var Mailer = class _Mailer {
|
|
|
15936
16184
|
this.collections = args.collections;
|
|
15937
16185
|
this.adapter = args.adapter;
|
|
15938
16186
|
this.providers = args.providers;
|
|
15939
|
-
this.
|
|
15940
|
-
this.queues = args.queues;
|
|
15941
|
-
this.bullQueues = args.bullQueues;
|
|
16187
|
+
this.queueDriver = args.queueDriver;
|
|
16188
|
+
this.queues = args.queueDriver.queues;
|
|
15942
16189
|
this.events = args.events;
|
|
15943
16190
|
this.runnerContext = {
|
|
15944
16191
|
db: this.db,
|
|
@@ -16003,10 +16250,12 @@ var Mailer = class _Mailer {
|
|
|
16003
16250
|
throw new Error("Mailer.fromEnv: no provider configured (set MAILER_SENDGRID_API_KEY, ...)");
|
|
16004
16251
|
}
|
|
16005
16252
|
const defaultProvider = env.MAILER_DEFAULT_PROVIDER ?? Object.keys(providers)[0];
|
|
16253
|
+
const driverEnv = env.MAILER_QUEUE_DRIVER ?? "bull";
|
|
16254
|
+
const queue = driverEnv === "agenda" ? { driver: "agenda" } : driverEnv === "noop" ? { driver: "noop" } : { driver: "bull", redis: { url: required("MAILER_REDIS_URL") } };
|
|
16006
16255
|
return _Mailer.init({
|
|
16007
16256
|
db,
|
|
16008
16257
|
adapter,
|
|
16009
|
-
|
|
16258
|
+
queue,
|
|
16010
16259
|
providers,
|
|
16011
16260
|
defaultProvider,
|
|
16012
16261
|
publicUrl: required("MAILER_PUBLIC_URL"),
|
|
@@ -16022,19 +16271,9 @@ var Mailer = class _Mailer {
|
|
|
16022
16271
|
}
|
|
16023
16272
|
const collections = getCollections(config.db, config.collectionPrefix);
|
|
16024
16273
|
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
|
-
}
|
|
16274
|
+
const queueDriver = await createQueueDriver(config.queue, config.db);
|
|
16275
|
+
if (!config.workerless && config.queue.driver !== "noop") {
|
|
16276
|
+
await queueDriver.scheduleRepeatingTick(config.tickIntervalSeconds);
|
|
16038
16277
|
}
|
|
16039
16278
|
return new _Mailer({
|
|
16040
16279
|
config,
|
|
@@ -16042,9 +16281,7 @@ var Mailer = class _Mailer {
|
|
|
16042
16281
|
collections,
|
|
16043
16282
|
adapter: config.adapter,
|
|
16044
16283
|
providers: config.providers,
|
|
16045
|
-
|
|
16046
|
-
queues,
|
|
16047
|
-
bullQueues,
|
|
16284
|
+
queueDriver,
|
|
16048
16285
|
events: new EventRegistry()
|
|
16049
16286
|
});
|
|
16050
16287
|
}
|
|
@@ -16352,6 +16589,7 @@ var Mailer = class _Mailer {
|
|
|
16352
16589
|
unsubscribedAt: null,
|
|
16353
16590
|
complainedAt: null,
|
|
16354
16591
|
queuedAt: /* @__PURE__ */ new Date(),
|
|
16592
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
16355
16593
|
sentAt: null,
|
|
16356
16594
|
deliveredAt: null
|
|
16357
16595
|
});
|
|
@@ -16383,14 +16621,16 @@ var Mailer = class _Mailer {
|
|
|
16383
16621
|
// Workers
|
|
16384
16622
|
// -------------------------------------------------------------------------
|
|
16385
16623
|
async startWorkers() {
|
|
16386
|
-
if (this.
|
|
16387
|
-
if (
|
|
16624
|
+
if (this.workersStarted) return;
|
|
16625
|
+
if (this.config.queue.driver === "noop") {
|
|
16626
|
+
throw new Error("startWorkers requires a non-noop queue driver");
|
|
16627
|
+
}
|
|
16388
16628
|
const provider = this.providers[this.config.defaultProvider];
|
|
16389
16629
|
const sendRate = provider?.sendRatePerSecond ?? this.config.sendRatePerSecond;
|
|
16390
|
-
this.
|
|
16391
|
-
redis: this.redis,
|
|
16630
|
+
await this.queueDriver.startWorkers({
|
|
16392
16631
|
concurrency: { send: this.config.sendConcurrency },
|
|
16393
16632
|
sendRateLimit: { max: sendRate, durationMs: 1e3 },
|
|
16633
|
+
retryAttempts: this.config.sendRetryAttempts,
|
|
16394
16634
|
handlers: {
|
|
16395
16635
|
tick: async () => {
|
|
16396
16636
|
await runTick(this.runnerContext);
|
|
@@ -16408,6 +16648,7 @@ var Mailer = class _Mailer {
|
|
|
16408
16648
|
}
|
|
16409
16649
|
}
|
|
16410
16650
|
});
|
|
16651
|
+
this.workersStarted = true;
|
|
16411
16652
|
}
|
|
16412
16653
|
/** Process unprocessed webhook events in mailer_webhook_events. */
|
|
16413
16654
|
async processWebhookBacklog() {
|
|
@@ -16434,20 +16675,8 @@ var Mailer = class _Mailer {
|
|
|
16434
16675
|
}
|
|
16435
16676
|
}
|
|
16436
16677
|
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
|
-
}
|
|
16678
|
+
await this.queueDriver.close();
|
|
16679
|
+
this.workersStarted = false;
|
|
16451
16680
|
}
|
|
16452
16681
|
/** Used internally by the admin router and tests; not part of the public API. */
|
|
16453
16682
|
getRunnerContext() {
|
|
@@ -16543,7 +16772,7 @@ async function createTestMailer(opts = {}) {
|
|
|
16543
16772
|
const mailer = await Mailer.init({
|
|
16544
16773
|
db,
|
|
16545
16774
|
adapter,
|
|
16546
|
-
|
|
16775
|
+
queue: { driver: "noop" },
|
|
16547
16776
|
providers: { null: provider },
|
|
16548
16777
|
defaultProvider: "null",
|
|
16549
16778
|
publicUrl: "http://localhost:3000",
|