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.d.cts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Db } from 'mongodb';
|
|
2
|
-
import { C as ContactAdapter, a as Contact, A as AdapterFilter, c as Mailer, r as NullProvider } from './null-
|
|
3
|
-
import 'ioredis';
|
|
2
|
+
import { C as ContactAdapter, a as Contact, A as AdapterFilter, c as Mailer, r as NullProvider } from './null-CWw3Gpbl.cjs';
|
|
4
3
|
import 'zod';
|
|
5
4
|
import 'handlebars';
|
|
5
|
+
import 'ioredis';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* MemoryContactAdapter — drop-in in-process ContactAdapter for tests.
|
|
@@ -49,7 +49,7 @@ interface TestMailerOptions {
|
|
|
49
49
|
seedContacts?: Contact[];
|
|
50
50
|
provider?: NullProvider;
|
|
51
51
|
/** Override Mailer config (excluding required fields the harness fills in). */
|
|
52
|
-
config?: Partial<Omit<Parameters<typeof Mailer.init>[0], 'db' | 'adapter' | '
|
|
52
|
+
config?: Partial<Omit<Parameters<typeof Mailer.init>[0], 'db' | 'adapter' | 'queue' | 'providers' | 'defaultProvider'>>;
|
|
53
53
|
}
|
|
54
54
|
interface TestMailerHarness {
|
|
55
55
|
mailer: Mailer;
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Db } from 'mongodb';
|
|
2
|
-
import { C as ContactAdapter, a as Contact, A as AdapterFilter, c as Mailer, r as NullProvider } from './null-
|
|
3
|
-
import 'ioredis';
|
|
2
|
+
import { C as ContactAdapter, a as Contact, A as AdapterFilter, c as Mailer, r as NullProvider } from './null-CWw3Gpbl.js';
|
|
4
3
|
import 'zod';
|
|
5
4
|
import 'handlebars';
|
|
5
|
+
import 'ioredis';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* MemoryContactAdapter — drop-in in-process ContactAdapter for tests.
|
|
@@ -49,7 +49,7 @@ interface TestMailerOptions {
|
|
|
49
49
|
seedContacts?: Contact[];
|
|
50
50
|
provider?: NullProvider;
|
|
51
51
|
/** Override Mailer config (excluding required fields the harness fills in). */
|
|
52
|
-
config?: Partial<Omit<Parameters<typeof Mailer.init>[0], 'db' | 'adapter' | '
|
|
52
|
+
config?: Partial<Omit<Parameters<typeof Mailer.init>[0], 'db' | 'adapter' | 'queue' | 'providers' | 'defaultProvider'>>;
|
|
53
53
|
}
|
|
54
54
|
interface TestMailerHarness {
|
|
55
55
|
mailer: Mailer;
|
package/dist/testing.js
CHANGED
|
@@ -2,7 +2,6 @@ import { MongoClient, ObjectId } from 'mongodb';
|
|
|
2
2
|
import crypto2 from 'crypto';
|
|
3
3
|
import sgMail from '@sendgrid/mail';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { Queue, Worker } from 'bullmq';
|
|
6
5
|
import IORedis from 'ioredis';
|
|
7
6
|
import Handlebars from 'handlebars';
|
|
8
7
|
import { convert } from 'html-to-text';
|
|
@@ -9490,10 +9489,10 @@ var require_dist2 = __commonJS({
|
|
|
9490
9489
|
const proxyResponsePromise = (0, parse_proxy_response_1.parseProxyResponse)(socket);
|
|
9491
9490
|
socket.write(`${payload}\r
|
|
9492
9491
|
`);
|
|
9493
|
-
const { connect, buffered } = await proxyResponsePromise;
|
|
9494
|
-
req.emit("proxyConnect",
|
|
9495
|
-
this.emit("proxyConnect",
|
|
9496
|
-
if (
|
|
9492
|
+
const { connect: connect2, buffered } = await proxyResponsePromise;
|
|
9493
|
+
req.emit("proxyConnect", connect2);
|
|
9494
|
+
this.emit("proxyConnect", connect2, req);
|
|
9495
|
+
if (connect2.statusCode === 200) {
|
|
9497
9496
|
req.once("socket", resume);
|
|
9498
9497
|
if (opts.secureEndpoint) {
|
|
9499
9498
|
debug("Upgrading socket connection to TLS");
|
|
@@ -14374,7 +14373,8 @@ async function ensureIndexes(db, prefix = "mailer_") {
|
|
|
14374
14373
|
{ key: { flowRunId: 1 }, sparse: true },
|
|
14375
14374
|
{ key: { broadcastId: 1 }, sparse: true },
|
|
14376
14375
|
{ key: { providerMessageId: 1 }, sparse: true },
|
|
14377
|
-
{ key: { status: 1, queuedAt: 1 } }
|
|
14376
|
+
{ key: { status: 1, queuedAt: 1 } },
|
|
14377
|
+
{ key: { status: 1, updatedAt: 1 } }
|
|
14378
14378
|
]),
|
|
14379
14379
|
c.suppressions.createIndexes([
|
|
14380
14380
|
{ key: { email: 1, scope: 1 }, unique: true, partialFilterExpression: { email: { $type: "string" } } },
|
|
@@ -14466,6 +14466,104 @@ function signDoiToken(payload, secret) {
|
|
|
14466
14466
|
const hmac = crypto2.createHmac("sha256", secret).update(bodyB64).digest();
|
|
14467
14467
|
return `${bodyB64}.${b64url(hmac)}`;
|
|
14468
14468
|
}
|
|
14469
|
+
var QUEUE_NAMES = {
|
|
14470
|
+
tick: "mailer-tick",
|
|
14471
|
+
advance: "mailer-advance",
|
|
14472
|
+
send: "mailer-send",
|
|
14473
|
+
webhook: "mailer-webhook"
|
|
14474
|
+
};
|
|
14475
|
+
var BullDriver = class _BullDriver {
|
|
14476
|
+
queues;
|
|
14477
|
+
redis;
|
|
14478
|
+
bullQueues;
|
|
14479
|
+
workers = null;
|
|
14480
|
+
bull;
|
|
14481
|
+
static async create(redisConfig) {
|
|
14482
|
+
let bull;
|
|
14483
|
+
try {
|
|
14484
|
+
bull = await import('bullmq');
|
|
14485
|
+
} catch {
|
|
14486
|
+
throw new Error(
|
|
14487
|
+
"mailery: queue driver 'bull' requires the 'bullmq' peer dependency. Run `npm install bullmq ioredis`."
|
|
14488
|
+
);
|
|
14489
|
+
}
|
|
14490
|
+
const redis = isRedisLike(redisConfig) ? redisConfig : connect(redisConfig);
|
|
14491
|
+
return new _BullDriver(bull, redis);
|
|
14492
|
+
}
|
|
14493
|
+
constructor(bull, redis) {
|
|
14494
|
+
this.bull = bull;
|
|
14495
|
+
this.redis = redis;
|
|
14496
|
+
const opts = { connection: redis };
|
|
14497
|
+
this.bullQueues = {
|
|
14498
|
+
tick: new bull.Queue(QUEUE_NAMES.tick, opts),
|
|
14499
|
+
advance: new bull.Queue(QUEUE_NAMES.advance, opts),
|
|
14500
|
+
send: new bull.Queue(QUEUE_NAMES.send, opts),
|
|
14501
|
+
webhook: new bull.Queue(QUEUE_NAMES.webhook, opts)
|
|
14502
|
+
};
|
|
14503
|
+
this.queues = {
|
|
14504
|
+
tick: adaptBullQueue(this.bullQueues.tick),
|
|
14505
|
+
advance: adaptBullQueue(this.bullQueues.advance),
|
|
14506
|
+
send: adaptBullQueue(this.bullQueues.send),
|
|
14507
|
+
webhook: adaptBullQueue(this.bullQueues.webhook)
|
|
14508
|
+
};
|
|
14509
|
+
}
|
|
14510
|
+
async scheduleRepeatingTick(intervalSeconds) {
|
|
14511
|
+
await this.bullQueues.tick.upsertJobScheduler(
|
|
14512
|
+
"mailer-tick-repeat",
|
|
14513
|
+
{ every: intervalSeconds * 1e3 },
|
|
14514
|
+
{ name: "tick", data: {} }
|
|
14515
|
+
);
|
|
14516
|
+
}
|
|
14517
|
+
async startWorkers(opts) {
|
|
14518
|
+
if (this.workers) return;
|
|
14519
|
+
const base = { connection: this.redis };
|
|
14520
|
+
const { Worker } = this.bull;
|
|
14521
|
+
const tick = new Worker(
|
|
14522
|
+
QUEUE_NAMES.tick,
|
|
14523
|
+
async (job) => opts.handlers.tick(job.data),
|
|
14524
|
+
{ ...base, concurrency: 1 }
|
|
14525
|
+
);
|
|
14526
|
+
const advance = new Worker(
|
|
14527
|
+
QUEUE_NAMES.advance,
|
|
14528
|
+
async (job) => opts.handlers.advance(job.data),
|
|
14529
|
+
{ ...base, concurrency: 10 }
|
|
14530
|
+
);
|
|
14531
|
+
const send = new Worker(
|
|
14532
|
+
QUEUE_NAMES.send,
|
|
14533
|
+
async (job) => opts.handlers.send(job.data),
|
|
14534
|
+
{
|
|
14535
|
+
...base,
|
|
14536
|
+
concurrency: opts.concurrency.send,
|
|
14537
|
+
limiter: opts.sendRateLimit ? { max: opts.sendRateLimit.max, duration: opts.sendRateLimit.durationMs } : void 0
|
|
14538
|
+
}
|
|
14539
|
+
);
|
|
14540
|
+
const webhook = new Worker(
|
|
14541
|
+
QUEUE_NAMES.webhook,
|
|
14542
|
+
async (job) => opts.handlers.webhook(job.data),
|
|
14543
|
+
{ ...base, concurrency: 4 }
|
|
14544
|
+
);
|
|
14545
|
+
this.workers = { tick, advance, send, webhook };
|
|
14546
|
+
}
|
|
14547
|
+
async stopWorkers() {
|
|
14548
|
+
if (!this.workers) return;
|
|
14549
|
+
await Promise.all([
|
|
14550
|
+
this.workers.tick.close(),
|
|
14551
|
+
this.workers.advance.close(),
|
|
14552
|
+
this.workers.send.close(),
|
|
14553
|
+
this.workers.webhook.close()
|
|
14554
|
+
]);
|
|
14555
|
+
this.workers = null;
|
|
14556
|
+
}
|
|
14557
|
+
async close() {
|
|
14558
|
+
await this.stopWorkers();
|
|
14559
|
+
await Promise.all([
|
|
14560
|
+
this.bullQueues.tick.close(),
|
|
14561
|
+
this.bullQueues.advance.close(),
|
|
14562
|
+
this.bullQueues.send.close(),
|
|
14563
|
+
this.bullQueues.webhook.close()
|
|
14564
|
+
]);
|
|
14565
|
+
}
|
|
14566
|
+
};
|
|
14469
14567
|
function adaptBullQueue(q) {
|
|
14470
14568
|
return {
|
|
14471
14569
|
add: (name, data, opts) => q.add(name, data, opts),
|
|
@@ -14473,39 +14571,16 @@ function adaptBullQueue(q) {
|
|
|
14473
14571
|
close: () => q.close()
|
|
14474
14572
|
};
|
|
14475
14573
|
}
|
|
14476
|
-
function
|
|
14477
|
-
return
|
|
14478
|
-
add: async () => void 0,
|
|
14479
|
-
getWaitingCount: async () => 0,
|
|
14480
|
-
close: async () => void 0
|
|
14481
|
-
};
|
|
14482
|
-
}
|
|
14483
|
-
function noopQueues() {
|
|
14484
|
-
return {
|
|
14485
|
-
tick: noopQueueAPI(),
|
|
14486
|
-
advance: noopQueueAPI(),
|
|
14487
|
-
send: noopQueueAPI(),
|
|
14488
|
-
webhook: noopQueueAPI()
|
|
14489
|
-
};
|
|
14490
|
-
}
|
|
14491
|
-
function namespacedQueueNames(prefix) {
|
|
14492
|
-
return {
|
|
14493
|
-
tick: "mailer-tick",
|
|
14494
|
-
advance: "mailer-advance",
|
|
14495
|
-
send: "mailer-send",
|
|
14496
|
-
webhook: "mailer-webhook"
|
|
14497
|
-
};
|
|
14574
|
+
function isRedisLike(x) {
|
|
14575
|
+
return !!x && typeof x === "object" && typeof x.get === "function" && typeof x.set === "function";
|
|
14498
14576
|
}
|
|
14499
|
-
function
|
|
14500
|
-
if (isRedisLike(opts)) return opts;
|
|
14577
|
+
function connect(opts) {
|
|
14501
14578
|
const config = {
|
|
14502
14579
|
maxRetriesPerRequest: null,
|
|
14503
14580
|
// BullMQ requirement
|
|
14504
14581
|
enableReadyCheck: false
|
|
14505
14582
|
};
|
|
14506
|
-
if (opts.url)
|
|
14507
|
-
return new IORedis(opts.url, config);
|
|
14508
|
-
}
|
|
14583
|
+
if (opts.url) return new IORedis(opts.url, config);
|
|
14509
14584
|
return new IORedis({
|
|
14510
14585
|
...config,
|
|
14511
14586
|
host: opts.host ?? "127.0.0.1",
|
|
@@ -14516,69 +14591,198 @@ function makeRedis(opts) {
|
|
|
14516
14591
|
tls: opts.tls ? {} : void 0
|
|
14517
14592
|
});
|
|
14518
14593
|
}
|
|
14519
|
-
|
|
14520
|
-
|
|
14521
|
-
|
|
14522
|
-
|
|
14523
|
-
|
|
14524
|
-
|
|
14525
|
-
|
|
14526
|
-
|
|
14527
|
-
|
|
14528
|
-
|
|
14529
|
-
|
|
14530
|
-
|
|
14594
|
+
|
|
14595
|
+
// src/server/queues/agenda.ts
|
|
14596
|
+
var QUEUE_NAMES2 = {
|
|
14597
|
+
tick: "mailer-tick",
|
|
14598
|
+
advance: "mailer-advance",
|
|
14599
|
+
send: "mailer-send",
|
|
14600
|
+
webhook: "mailer-webhook"
|
|
14601
|
+
};
|
|
14602
|
+
var AgendaDriver = class _AgendaDriver {
|
|
14603
|
+
queues;
|
|
14604
|
+
agenda;
|
|
14605
|
+
agendaMod;
|
|
14606
|
+
sendLimiter = null;
|
|
14607
|
+
started = false;
|
|
14608
|
+
static async create(opts) {
|
|
14609
|
+
let agendaMod;
|
|
14610
|
+
let backendMod;
|
|
14611
|
+
try {
|
|
14612
|
+
agendaMod = await import('agenda');
|
|
14613
|
+
backendMod = await import('@agendajs/mongo-backend');
|
|
14614
|
+
} catch {
|
|
14615
|
+
throw new Error(
|
|
14616
|
+
"mailery: queue driver 'agenda' requires the 'agenda' and '@agendajs/mongo-backend' peer dependencies. Run `npm install agenda @agendajs/mongo-backend bottleneck`."
|
|
14617
|
+
);
|
|
14618
|
+
}
|
|
14619
|
+
const backend = new backendMod.MongoBackend({
|
|
14620
|
+
mongo: opts.db,
|
|
14621
|
+
collection: opts.collectionName ?? "_mailerJobs"
|
|
14622
|
+
});
|
|
14623
|
+
const agenda = new agendaMod.Agenda({
|
|
14624
|
+
backend,
|
|
14625
|
+
processEvery: `${opts.processEverySeconds ?? 5} seconds`,
|
|
14626
|
+
defaultLockLifetime: (opts.lockLifetimeSeconds ?? 10 * 60) * 1e3,
|
|
14627
|
+
maxConcurrency: 50,
|
|
14628
|
+
defaultConcurrency: 5
|
|
14629
|
+
});
|
|
14630
|
+
return new _AgendaDriver(agenda, agendaMod, opts.db);
|
|
14631
|
+
}
|
|
14632
|
+
db;
|
|
14633
|
+
constructor(agenda, agendaMod, db) {
|
|
14634
|
+
this.agenda = agenda;
|
|
14635
|
+
this.agendaMod = agendaMod;
|
|
14636
|
+
this.db = db;
|
|
14637
|
+
this.queues = {
|
|
14638
|
+
tick: this.makeQueueAPI(QUEUE_NAMES2.tick),
|
|
14639
|
+
advance: this.makeQueueAPI(QUEUE_NAMES2.advance),
|
|
14640
|
+
send: this.makeQueueAPI(QUEUE_NAMES2.send),
|
|
14641
|
+
webhook: this.makeQueueAPI(QUEUE_NAMES2.webhook)
|
|
14642
|
+
};
|
|
14643
|
+
}
|
|
14644
|
+
makeQueueAPI(name) {
|
|
14645
|
+
return {
|
|
14646
|
+
add: async (_jobName, data, opts) => {
|
|
14647
|
+
const payload = { ...data };
|
|
14648
|
+
if (opts?.jobId) {
|
|
14649
|
+
payload.__jobId = opts.jobId;
|
|
14650
|
+
if (await this.findPending(name, opts.jobId)) return;
|
|
14651
|
+
}
|
|
14652
|
+
const job = this.agenda.create(name, payload);
|
|
14653
|
+
if (opts?.delay) job.schedule(new Date(Date.now() + opts.delay));
|
|
14654
|
+
await job.save();
|
|
14655
|
+
},
|
|
14656
|
+
getWaitingCount: async () => {
|
|
14657
|
+
return this.jobsCollection().countDocuments({
|
|
14658
|
+
name,
|
|
14659
|
+
$or: [{ lockedAt: null }, { lockedAt: { $exists: false } }],
|
|
14660
|
+
nextRunAt: { $lte: /* @__PURE__ */ new Date() }
|
|
14661
|
+
});
|
|
14662
|
+
},
|
|
14663
|
+
close: async () => {
|
|
14664
|
+
}
|
|
14665
|
+
};
|
|
14666
|
+
}
|
|
14667
|
+
/** Direct access to the Mongo collection Agenda persists jobs into. */
|
|
14668
|
+
jobsCollection() {
|
|
14669
|
+
return this.db.collection(this.collectionName());
|
|
14670
|
+
}
|
|
14671
|
+
collectionName() {
|
|
14672
|
+
return "_mailerJobs";
|
|
14673
|
+
}
|
|
14674
|
+
async findPending(name, jobId) {
|
|
14675
|
+
return this.jobsCollection().findOne({
|
|
14676
|
+
name,
|
|
14677
|
+
"data.__jobId": jobId,
|
|
14678
|
+
$or: [{ lastFinishedAt: null }, { lastFinishedAt: { $exists: false } }]
|
|
14679
|
+
});
|
|
14680
|
+
}
|
|
14681
|
+
async scheduleRepeatingTick(intervalSeconds) {
|
|
14682
|
+
if (!this.started) {
|
|
14683
|
+
this.agenda.define(QUEUE_NAMES2.tick, async () => {
|
|
14684
|
+
}, { concurrency: 1 });
|
|
14685
|
+
await this.agenda.start();
|
|
14686
|
+
this.started = true;
|
|
14687
|
+
}
|
|
14688
|
+
await this.agenda.every(`${intervalSeconds} seconds`, QUEUE_NAMES2.tick);
|
|
14689
|
+
}
|
|
14690
|
+
async startWorkers(opts) {
|
|
14691
|
+
const exp = this.agendaMod.backoffStrategies.exponential;
|
|
14692
|
+
if (opts.sendRateLimit) {
|
|
14693
|
+
try {
|
|
14694
|
+
const Bottleneck = (await import('bottleneck')).default;
|
|
14695
|
+
this.sendLimiter = new Bottleneck({
|
|
14696
|
+
minTime: Math.ceil(opts.sendRateLimit.durationMs / opts.sendRateLimit.max),
|
|
14697
|
+
maxConcurrent: opts.concurrency.send
|
|
14698
|
+
});
|
|
14699
|
+
} catch {
|
|
14700
|
+
throw new Error(
|
|
14701
|
+
"mailery: queue driver 'agenda' with sendRateLimit requires the 'bottleneck' peer dependency. Run `npm install bottleneck`."
|
|
14702
|
+
);
|
|
14703
|
+
}
|
|
14704
|
+
}
|
|
14705
|
+
const retryBackoff = exp({ delay: 6e4, maxRetries: Math.max(0, opts.retryAttempts - 1), factor: 2 });
|
|
14706
|
+
this.agenda.define(QUEUE_NAMES2.tick, async (job) => {
|
|
14707
|
+
await opts.handlers.tick(job.attrs.data);
|
|
14708
|
+
}, { concurrency: 1 });
|
|
14709
|
+
this.agenda.define(QUEUE_NAMES2.advance, async (job) => {
|
|
14710
|
+
await opts.handlers.advance(job.attrs.data);
|
|
14711
|
+
}, { concurrency: 10, backoff: retryBackoff });
|
|
14712
|
+
this.agenda.define(QUEUE_NAMES2.send, async (job) => {
|
|
14713
|
+
const data = job.attrs.data;
|
|
14714
|
+
if (this.sendLimiter) {
|
|
14715
|
+
await this.sendLimiter.schedule(() => opts.handlers.send(data));
|
|
14716
|
+
} else {
|
|
14717
|
+
await opts.handlers.send(data);
|
|
14718
|
+
}
|
|
14719
|
+
}, { concurrency: opts.concurrency.send, backoff: retryBackoff });
|
|
14720
|
+
this.agenda.define(QUEUE_NAMES2.webhook, async (job) => {
|
|
14721
|
+
await opts.handlers.webhook(job.attrs.data);
|
|
14722
|
+
}, { concurrency: 4, backoff: retryBackoff });
|
|
14723
|
+
if (!this.started) {
|
|
14724
|
+
await this.agenda.start();
|
|
14725
|
+
this.started = true;
|
|
14726
|
+
}
|
|
14727
|
+
}
|
|
14728
|
+
async stopWorkers() {
|
|
14729
|
+
if (!this.started) return;
|
|
14730
|
+
await this.agenda.stop();
|
|
14731
|
+
this.started = false;
|
|
14732
|
+
if (this.sendLimiter) {
|
|
14733
|
+
await this.sendLimiter.stop({ dropWaitingJobs: true }).catch(() => {
|
|
14734
|
+
});
|
|
14735
|
+
this.sendLimiter = null;
|
|
14736
|
+
}
|
|
14737
|
+
}
|
|
14738
|
+
async close() {
|
|
14739
|
+
await this.stopWorkers();
|
|
14740
|
+
}
|
|
14741
|
+
};
|
|
14742
|
+
|
|
14743
|
+
// src/server/queues/noop.ts
|
|
14744
|
+
function noopQueueAPI() {
|
|
14531
14745
|
return {
|
|
14532
|
-
|
|
14533
|
-
|
|
14534
|
-
|
|
14535
|
-
send: adaptBullQueue(bullQueues.send),
|
|
14536
|
-
webhook: adaptBullQueue(bullQueues.webhook)
|
|
14537
|
-
},
|
|
14538
|
-
bullQueues
|
|
14746
|
+
add: async () => void 0,
|
|
14747
|
+
getWaitingCount: async () => 0,
|
|
14748
|
+
close: async () => void 0
|
|
14539
14749
|
};
|
|
14540
14750
|
}
|
|
14541
|
-
|
|
14542
|
-
|
|
14543
|
-
|
|
14544
|
-
|
|
14545
|
-
|
|
14546
|
-
|
|
14547
|
-
}
|
|
14548
|
-
function createWorkers(input) {
|
|
14549
|
-
const names = namespacedQueueNames();
|
|
14550
|
-
const base = { connection: input.redis };
|
|
14551
|
-
const tick = new Worker(names.tick, async (job) => input.handlers.tick(job.data), {
|
|
14552
|
-
...base,
|
|
14553
|
-
concurrency: 1
|
|
14554
|
-
// single tick driver per worker process
|
|
14555
|
-
});
|
|
14556
|
-
const advance = new Worker(
|
|
14557
|
-
names.advance,
|
|
14558
|
-
async (job) => input.handlers.advance(job.data),
|
|
14559
|
-
{ ...base, concurrency: 10 }
|
|
14560
|
-
);
|
|
14561
|
-
const sendOpts = {
|
|
14562
|
-
...base,
|
|
14563
|
-
concurrency: input.concurrency.send,
|
|
14564
|
-
limiter: input.sendRateLimit ? { max: input.sendRateLimit.max, duration: input.sendRateLimit.durationMs } : void 0
|
|
14751
|
+
var NoopDriver = class {
|
|
14752
|
+
queues = {
|
|
14753
|
+
tick: noopQueueAPI(),
|
|
14754
|
+
advance: noopQueueAPI(),
|
|
14755
|
+
send: noopQueueAPI(),
|
|
14756
|
+
webhook: noopQueueAPI()
|
|
14565
14757
|
};
|
|
14566
|
-
|
|
14567
|
-
|
|
14568
|
-
|
|
14569
|
-
|
|
14570
|
-
|
|
14571
|
-
|
|
14572
|
-
|
|
14573
|
-
}
|
|
14574
|
-
|
|
14575
|
-
|
|
14576
|
-
|
|
14577
|
-
async function
|
|
14578
|
-
|
|
14579
|
-
|
|
14580
|
-
|
|
14581
|
-
|
|
14758
|
+
async scheduleRepeatingTick(_intervalSeconds) {
|
|
14759
|
+
}
|
|
14760
|
+
async startWorkers(_opts) {
|
|
14761
|
+
}
|
|
14762
|
+
async stopWorkers() {
|
|
14763
|
+
}
|
|
14764
|
+
async close() {
|
|
14765
|
+
}
|
|
14766
|
+
};
|
|
14767
|
+
|
|
14768
|
+
// src/server/queues/index.ts
|
|
14769
|
+
async function createQueueDriver(config, fallbackDb) {
|
|
14770
|
+
switch (config.driver) {
|
|
14771
|
+
case "bull":
|
|
14772
|
+
return BullDriver.create(config.redis);
|
|
14773
|
+
case "agenda":
|
|
14774
|
+
return AgendaDriver.create({
|
|
14775
|
+
db: config.db ?? fallbackDb,
|
|
14776
|
+
processEverySeconds: config.processEverySeconds,
|
|
14777
|
+
lockLifetimeSeconds: config.lockLifetimeSeconds,
|
|
14778
|
+
collectionName: config.collectionName
|
|
14779
|
+
});
|
|
14780
|
+
case "noop":
|
|
14781
|
+
return new NoopDriver();
|
|
14782
|
+
default: {
|
|
14783
|
+
throw new Error(`mailery: unknown queue driver`);
|
|
14784
|
+
}
|
|
14785
|
+
}
|
|
14582
14786
|
}
|
|
14583
14787
|
|
|
14584
14788
|
// src/server/runner/triggers.ts
|
|
@@ -15065,7 +15269,8 @@ async function dispatchSend(sendId, ctx) {
|
|
|
15065
15269
|
status: "sending",
|
|
15066
15270
|
fromName: rendered.fromName,
|
|
15067
15271
|
fromEmail: rendered.fromEmail,
|
|
15068
|
-
subject: rendered.subject
|
|
15272
|
+
subject: rendered.subject,
|
|
15273
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
15069
15274
|
}
|
|
15070
15275
|
}
|
|
15071
15276
|
);
|
|
@@ -15139,6 +15344,7 @@ function buildRenderContext(contact, run, vars, ctx) {
|
|
|
15139
15344
|
};
|
|
15140
15345
|
}
|
|
15141
15346
|
function newSendDoc(input) {
|
|
15347
|
+
const now = /* @__PURE__ */ new Date();
|
|
15142
15348
|
return {
|
|
15143
15349
|
_id: input._id,
|
|
15144
15350
|
dedupeKey: input.dedupeKey,
|
|
@@ -15169,7 +15375,8 @@ function newSendDoc(input) {
|
|
|
15169
15375
|
clickedLinks: [],
|
|
15170
15376
|
unsubscribedAt: null,
|
|
15171
15377
|
complainedAt: null,
|
|
15172
|
-
queuedAt:
|
|
15378
|
+
queuedAt: now,
|
|
15379
|
+
updatedAt: now,
|
|
15173
15380
|
sentAt: null,
|
|
15174
15381
|
deliveredAt: null
|
|
15175
15382
|
};
|
|
@@ -15651,6 +15858,7 @@ async function buildSendDoc(broadcast, template, contact, ctx, scheduledMs, resp
|
|
|
15651
15858
|
unsubscribedAt: null,
|
|
15652
15859
|
complainedAt: null,
|
|
15653
15860
|
queuedAt: /* @__PURE__ */ new Date(),
|
|
15861
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
15654
15862
|
sentAt: null,
|
|
15655
15863
|
deliveredAt: null
|
|
15656
15864
|
};
|
|
@@ -15723,6 +15931,7 @@ async function promoteSoftBounces(ctx) {
|
|
|
15723
15931
|
}
|
|
15724
15932
|
|
|
15725
15933
|
// src/server/runner/tick.ts
|
|
15934
|
+
var STRANDED_SEND_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
15726
15935
|
async function runTick(ctx) {
|
|
15727
15936
|
await processNewlyFiredEventTriggers(ctx).catch((err) => {
|
|
15728
15937
|
console.error("mailery: triggers scan failed", err);
|
|
@@ -15730,6 +15939,9 @@ async function runTick(ctx) {
|
|
|
15730
15939
|
await sweepStrandedFlowRuns(ctx).catch((err) => {
|
|
15731
15940
|
console.error("mailery: sweep failed", err);
|
|
15732
15941
|
});
|
|
15942
|
+
await sweepStrandedSends(ctx).catch((err) => {
|
|
15943
|
+
console.error("mailery: stranded-send sweep failed", err);
|
|
15944
|
+
});
|
|
15733
15945
|
await drainOutbox(ctx).catch((err) => {
|
|
15734
15946
|
console.error("mailery: outbox drain failed", err);
|
|
15735
15947
|
});
|
|
@@ -15743,6 +15955,24 @@ async function runTick(ctx) {
|
|
|
15743
15955
|
console.error("mailery: soft-bounce promotion failed", err);
|
|
15744
15956
|
});
|
|
15745
15957
|
}
|
|
15958
|
+
async function sweepStrandedSends(ctx) {
|
|
15959
|
+
const cutoff = new Date(Date.now() - STRANDED_SEND_THRESHOLD_MS);
|
|
15960
|
+
const cursor = ctx.collections.sends.find(
|
|
15961
|
+
{ status: "sending", updatedAt: { $lt: cutoff } },
|
|
15962
|
+
{ projection: { _id: 1 } }
|
|
15963
|
+
).limit(500);
|
|
15964
|
+
for await (const row of cursor) {
|
|
15965
|
+
const reset = await ctx.collections.sends.updateOne(
|
|
15966
|
+
{ _id: row._id, status: "sending", updatedAt: { $lt: cutoff } },
|
|
15967
|
+
{ $set: { status: "queued", updatedAt: /* @__PURE__ */ new Date() } }
|
|
15968
|
+
);
|
|
15969
|
+
if (reset.modifiedCount === 0) continue;
|
|
15970
|
+
await ctx.queues.send.add("send", { sendId: String(row._id) }, {
|
|
15971
|
+
attempts: ctx.config.sendRetryAttempts,
|
|
15972
|
+
backoff: { type: "exponential", delay: 6e4 }
|
|
15973
|
+
});
|
|
15974
|
+
}
|
|
15975
|
+
}
|
|
15746
15976
|
async function drainOutbox(ctx) {
|
|
15747
15977
|
const batch = await ctx.collections.outbox.find({ status: "pending" }).sort({ enqueuedAt: 1 }).limit(200).toArray();
|
|
15748
15978
|
for (const row of batch) {
|
|
@@ -15913,12 +16143,11 @@ var Mailer = class _Mailer {
|
|
|
15913
16143
|
collections;
|
|
15914
16144
|
adapter;
|
|
15915
16145
|
providers;
|
|
15916
|
-
redis;
|
|
15917
16146
|
queues;
|
|
15918
16147
|
config;
|
|
15919
16148
|
events;
|
|
15920
|
-
|
|
15921
|
-
|
|
16149
|
+
queueDriver;
|
|
16150
|
+
workersStarted = false;
|
|
15922
16151
|
runnerContext;
|
|
15923
16152
|
constructor(args) {
|
|
15924
16153
|
this.config = args.config;
|
|
@@ -15926,9 +16155,8 @@ var Mailer = class _Mailer {
|
|
|
15926
16155
|
this.collections = args.collections;
|
|
15927
16156
|
this.adapter = args.adapter;
|
|
15928
16157
|
this.providers = args.providers;
|
|
15929
|
-
this.
|
|
15930
|
-
this.queues = args.queues;
|
|
15931
|
-
this.bullQueues = args.bullQueues;
|
|
16158
|
+
this.queueDriver = args.queueDriver;
|
|
16159
|
+
this.queues = args.queueDriver.queues;
|
|
15932
16160
|
this.events = args.events;
|
|
15933
16161
|
this.runnerContext = {
|
|
15934
16162
|
db: this.db,
|
|
@@ -15993,10 +16221,12 @@ var Mailer = class _Mailer {
|
|
|
15993
16221
|
throw new Error("Mailer.fromEnv: no provider configured (set MAILER_SENDGRID_API_KEY, ...)");
|
|
15994
16222
|
}
|
|
15995
16223
|
const defaultProvider = env.MAILER_DEFAULT_PROVIDER ?? Object.keys(providers)[0];
|
|
16224
|
+
const driverEnv = env.MAILER_QUEUE_DRIVER ?? "bull";
|
|
16225
|
+
const queue = driverEnv === "agenda" ? { driver: "agenda" } : driverEnv === "noop" ? { driver: "noop" } : { driver: "bull", redis: { url: required("MAILER_REDIS_URL") } };
|
|
15996
16226
|
return _Mailer.init({
|
|
15997
16227
|
db,
|
|
15998
16228
|
adapter,
|
|
15999
|
-
|
|
16229
|
+
queue,
|
|
16000
16230
|
providers,
|
|
16001
16231
|
defaultProvider,
|
|
16002
16232
|
publicUrl: required("MAILER_PUBLIC_URL"),
|
|
@@ -16012,19 +16242,9 @@ var Mailer = class _Mailer {
|
|
|
16012
16242
|
}
|
|
16013
16243
|
const collections = getCollections(config.db, config.collectionPrefix);
|
|
16014
16244
|
await ensureIndexes(config.db, config.collectionPrefix);
|
|
16015
|
-
|
|
16016
|
-
|
|
16017
|
-
|
|
16018
|
-
if (config.redis === null) {
|
|
16019
|
-
queues = noopQueues();
|
|
16020
|
-
} else {
|
|
16021
|
-
redis = makeRedis(config.redis);
|
|
16022
|
-
const created = createQueues(redis);
|
|
16023
|
-
queues = created.queues;
|
|
16024
|
-
bullQueues = created.bullQueues;
|
|
16025
|
-
if (!config.workerless) {
|
|
16026
|
-
await scheduleTick(bullQueues, config.tickIntervalSeconds);
|
|
16027
|
-
}
|
|
16245
|
+
const queueDriver = await createQueueDriver(config.queue, config.db);
|
|
16246
|
+
if (!config.workerless && config.queue.driver !== "noop") {
|
|
16247
|
+
await queueDriver.scheduleRepeatingTick(config.tickIntervalSeconds);
|
|
16028
16248
|
}
|
|
16029
16249
|
return new _Mailer({
|
|
16030
16250
|
config,
|
|
@@ -16032,9 +16252,7 @@ var Mailer = class _Mailer {
|
|
|
16032
16252
|
collections,
|
|
16033
16253
|
adapter: config.adapter,
|
|
16034
16254
|
providers: config.providers,
|
|
16035
|
-
|
|
16036
|
-
queues,
|
|
16037
|
-
bullQueues,
|
|
16255
|
+
queueDriver,
|
|
16038
16256
|
events: new EventRegistry()
|
|
16039
16257
|
});
|
|
16040
16258
|
}
|
|
@@ -16342,6 +16560,7 @@ var Mailer = class _Mailer {
|
|
|
16342
16560
|
unsubscribedAt: null,
|
|
16343
16561
|
complainedAt: null,
|
|
16344
16562
|
queuedAt: /* @__PURE__ */ new Date(),
|
|
16563
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
16345
16564
|
sentAt: null,
|
|
16346
16565
|
deliveredAt: null
|
|
16347
16566
|
});
|
|
@@ -16373,14 +16592,16 @@ var Mailer = class _Mailer {
|
|
|
16373
16592
|
// Workers
|
|
16374
16593
|
// -------------------------------------------------------------------------
|
|
16375
16594
|
async startWorkers() {
|
|
16376
|
-
if (this.
|
|
16377
|
-
if (
|
|
16595
|
+
if (this.workersStarted) return;
|
|
16596
|
+
if (this.config.queue.driver === "noop") {
|
|
16597
|
+
throw new Error("startWorkers requires a non-noop queue driver");
|
|
16598
|
+
}
|
|
16378
16599
|
const provider = this.providers[this.config.defaultProvider];
|
|
16379
16600
|
const sendRate = provider?.sendRatePerSecond ?? this.config.sendRatePerSecond;
|
|
16380
|
-
this.
|
|
16381
|
-
redis: this.redis,
|
|
16601
|
+
await this.queueDriver.startWorkers({
|
|
16382
16602
|
concurrency: { send: this.config.sendConcurrency },
|
|
16383
16603
|
sendRateLimit: { max: sendRate, durationMs: 1e3 },
|
|
16604
|
+
retryAttempts: this.config.sendRetryAttempts,
|
|
16384
16605
|
handlers: {
|
|
16385
16606
|
tick: async () => {
|
|
16386
16607
|
await runTick(this.runnerContext);
|
|
@@ -16398,6 +16619,7 @@ var Mailer = class _Mailer {
|
|
|
16398
16619
|
}
|
|
16399
16620
|
}
|
|
16400
16621
|
});
|
|
16622
|
+
this.workersStarted = true;
|
|
16401
16623
|
}
|
|
16402
16624
|
/** Process unprocessed webhook events in mailer_webhook_events. */
|
|
16403
16625
|
async processWebhookBacklog() {
|
|
@@ -16424,20 +16646,8 @@ var Mailer = class _Mailer {
|
|
|
16424
16646
|
}
|
|
16425
16647
|
}
|
|
16426
16648
|
async stop() {
|
|
16427
|
-
|
|
16428
|
-
|
|
16429
|
-
this.workers = null;
|
|
16430
|
-
}
|
|
16431
|
-
if (this.bullQueues) {
|
|
16432
|
-
await closeBullQueues(this.bullQueues);
|
|
16433
|
-
this.bullQueues = null;
|
|
16434
|
-
} else {
|
|
16435
|
-
await closeQueues(this.queues);
|
|
16436
|
-
}
|
|
16437
|
-
if (this.redis) {
|
|
16438
|
-
await this.redis.quit().catch(() => {
|
|
16439
|
-
});
|
|
16440
|
-
}
|
|
16649
|
+
await this.queueDriver.close();
|
|
16650
|
+
this.workersStarted = false;
|
|
16441
16651
|
}
|
|
16442
16652
|
/** Used internally by the admin router and tests; not part of the public API. */
|
|
16443
16653
|
getRunnerContext() {
|
|
@@ -16533,7 +16743,7 @@ async function createTestMailer(opts = {}) {
|
|
|
16533
16743
|
const mailer = await Mailer.init({
|
|
16534
16744
|
db,
|
|
16535
16745
|
adapter,
|
|
16536
|
-
|
|
16746
|
+
queue: { driver: "noop" },
|
|
16537
16747
|
providers: { null: provider },
|
|
16538
16748
|
defaultProvider: "null",
|
|
16539
16749
|
publicUrl: "http://localhost:3000",
|