mailery 0.5.1 → 0.7.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 +3 -1
- package/dist/admin/spa/{index-Dmsla2eL.js → index-CjQTOX9H.js} +8 -8
- package/dist/admin/spa/index-CjQTOX9H.js.map +1 -0
- package/dist/admin/spa/index.html +1 -1
- package/dist/admin/spa/{template-editor-Bn28OPMU.js → template-editor-yH0Oz4Ix.js} +105 -105
- package/dist/admin/spa/template-editor-yH0Oz4Ix.js.map +1 -0
- package/dist/index.cjs +453 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -4
- package/dist/index.d.ts +26 -4
- package/dist/index.js +451 -31
- package/dist/index.js.map +1 -1
- package/dist/{null-BYB6v4Iv.d.cts → null-B0rPgE5_.d.cts} +126 -3
- package/dist/{null-BYB6v4Iv.d.ts → null-B0rPgE5_.d.ts} +126 -3
- package/dist/testing.cjs +305 -17
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +305 -17
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
- package/dist/admin/spa/index-Dmsla2eL.js.map +0 -1
- package/dist/admin/spa/template-editor-Bn28OPMU.js.map +0 -1
package/dist/testing.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Db } from 'mongodb';
|
|
2
|
-
import { C as ContactAdapter, a as Contact, A as AdapterFilter, d as Mailer, s as NullProvider } from './null-
|
|
2
|
+
import { C as ContactAdapter, a as Contact, A as AdapterFilter, d as Mailer, s as NullProvider } from './null-B0rPgE5_.cjs';
|
|
3
3
|
import 'zod';
|
|
4
4
|
import 'handlebars';
|
|
5
5
|
import 'ioredis';
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Db } from 'mongodb';
|
|
2
|
-
import { C as ContactAdapter, a as Contact, A as AdapterFilter, d as Mailer, s as NullProvider } from './null-
|
|
2
|
+
import { C as ContactAdapter, a as Contact, A as AdapterFilter, d as Mailer, s as NullProvider } from './null-B0rPgE5_.js';
|
|
3
3
|
import 'zod';
|
|
4
4
|
import 'handlebars';
|
|
5
5
|
import 'ioredis';
|
package/dist/testing.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { MongoClient, ObjectId } from 'mongodb';
|
|
2
3
|
import crypto2 from 'crypto';
|
|
3
4
|
import sgMail from '@sendgrid/mail';
|
|
4
|
-
import { z } from 'zod';
|
|
5
5
|
import IORedis from 'ioredis';
|
|
6
6
|
import Handlebars from 'handlebars';
|
|
7
7
|
import { convert } from 'html-to-text';
|
|
@@ -50,6 +50,55 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
50
50
|
));
|
|
51
51
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
52
52
|
|
|
53
|
+
// src/server/adapters/vars.ts
|
|
54
|
+
var vars_exports = {};
|
|
55
|
+
__export(vars_exports, {
|
|
56
|
+
RESERVED_VAR_KEYS: () => RESERVED_VAR_KEYS,
|
|
57
|
+
assertNoReservedVarKeys: () => assertNoReservedVarKeys,
|
|
58
|
+
defineVars: () => defineVars,
|
|
59
|
+
resolveVars: () => resolveVars,
|
|
60
|
+
varsJsonSchema: () => varsJsonSchema
|
|
61
|
+
});
|
|
62
|
+
function defineVars(adapter) {
|
|
63
|
+
return adapter;
|
|
64
|
+
}
|
|
65
|
+
function assertNoReservedVarKeys(adapter) {
|
|
66
|
+
const json = varsJsonSchema(adapter);
|
|
67
|
+
const props = json && typeof json === "object" ? json.properties : void 0;
|
|
68
|
+
if (!props) return;
|
|
69
|
+
const clashes = RESERVED_VAR_KEYS.filter((k) => k in props);
|
|
70
|
+
if (clashes.length > 0) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`varsAdapter schema declares reserved key(s): ${clashes.join(", ")}. These names are provided by mailery itself \u2014 rename them in your schema.`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function varsJsonSchema(adapter) {
|
|
77
|
+
return z.toJSONSchema(adapter.schema, { io: "output" });
|
|
78
|
+
}
|
|
79
|
+
async function resolveVars(adapter, contact, info) {
|
|
80
|
+
if (!adapter) return {};
|
|
81
|
+
const resolved = await adapter.resolve(contact, info);
|
|
82
|
+
if (!resolved || typeof resolved !== "object") return {};
|
|
83
|
+
const out = { ...resolved };
|
|
84
|
+
for (const k of RESERVED_VAR_KEYS) delete out[k];
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
var RESERVED_VAR_KEYS;
|
|
88
|
+
var init_vars = __esm({
|
|
89
|
+
"src/server/adapters/vars.ts"() {
|
|
90
|
+
RESERVED_VAR_KEYS = [
|
|
91
|
+
"contact",
|
|
92
|
+
"vars",
|
|
93
|
+
"event",
|
|
94
|
+
"unsubscribeUrl",
|
|
95
|
+
"viewInBrowserUrl",
|
|
96
|
+
"preferenceCenterUrl",
|
|
97
|
+
"senderAddress"
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
53
102
|
// src/server/adapters/mongo.ts
|
|
54
103
|
var mongo_exports = {};
|
|
55
104
|
__export(mongo_exports, {
|
|
@@ -2992,19 +3041,19 @@ var require_range = __commonJS({
|
|
|
2992
3041
|
var replaceCaret = (comp, options2) => {
|
|
2993
3042
|
debug("caret", comp, options2);
|
|
2994
3043
|
const r = options2.loose ? re[t.CARETLOOSE] : re[t.CARET];
|
|
2995
|
-
const
|
|
3044
|
+
const z3 = options2.includePrerelease ? "-0" : "";
|
|
2996
3045
|
return comp.replace(r, (_, M, m, p, pr) => {
|
|
2997
3046
|
debug("caret", comp, _, M, m, p, pr);
|
|
2998
3047
|
let ret;
|
|
2999
3048
|
if (isX(M)) {
|
|
3000
3049
|
ret = "";
|
|
3001
3050
|
} else if (isX(m)) {
|
|
3002
|
-
ret = `>=${M}.0.0${
|
|
3051
|
+
ret = `>=${M}.0.0${z3} <${+M + 1}.0.0-0`;
|
|
3003
3052
|
} else if (isX(p)) {
|
|
3004
3053
|
if (M === "0") {
|
|
3005
|
-
ret = `>=${M}.${m}.0${
|
|
3054
|
+
ret = `>=${M}.${m}.0${z3} <${M}.${+m + 1}.0-0`;
|
|
3006
3055
|
} else {
|
|
3007
|
-
ret = `>=${M}.${m}.0${
|
|
3056
|
+
ret = `>=${M}.${m}.0${z3} <${+M + 1}.0.0-0`;
|
|
3008
3057
|
}
|
|
3009
3058
|
} else if (pr) {
|
|
3010
3059
|
debug("replaceCaret pr", pr);
|
|
@@ -3021,9 +3070,9 @@ var require_range = __commonJS({
|
|
|
3021
3070
|
debug("no pr");
|
|
3022
3071
|
if (M === "0") {
|
|
3023
3072
|
if (m === "0") {
|
|
3024
|
-
ret = `>=${M}.${m}.${p}${
|
|
3073
|
+
ret = `>=${M}.${m}.${p}${z3} <${M}.${m}.${+p + 1}-0`;
|
|
3025
3074
|
} else {
|
|
3026
|
-
ret = `>=${M}.${m}.${p}${
|
|
3075
|
+
ret = `>=${M}.${m}.${p}${z3} <${M}.${+m + 1}.0-0`;
|
|
3027
3076
|
}
|
|
3028
3077
|
} else {
|
|
3029
3078
|
ret = `>=${M}.${m}.${p} <${+M + 1}.0.0-0`;
|
|
@@ -14143,6 +14192,12 @@ var tagInputSchema = z.object({
|
|
|
14143
14192
|
externalId: externalIdSchema,
|
|
14144
14193
|
tag: z.string().min(1).max(128)
|
|
14145
14194
|
});
|
|
14195
|
+
var abortFlowInputSchema = z.object({
|
|
14196
|
+
flowSlug: slugSchema,
|
|
14197
|
+
externalId: externalIdSchema,
|
|
14198
|
+
reason: z.string().min(1).max(200).optional()
|
|
14199
|
+
});
|
|
14200
|
+
var abortAllFlowsInputSchema = abortFlowInputSchema.omit({ flowSlug: true });
|
|
14146
14201
|
var sendOneOffInputSchema = z.object({
|
|
14147
14202
|
templateSlug: slugSchema,
|
|
14148
14203
|
externalId: externalIdSchema,
|
|
@@ -14172,7 +14227,13 @@ var flowStepSchema = z.lazy(
|
|
|
14172
14227
|
type: z.literal("send"),
|
|
14173
14228
|
templateSlug: slugSchema,
|
|
14174
14229
|
providerOverride: z.string().optional(),
|
|
14175
|
-
vars: z.record(z.string(), z.unknown()).optional()
|
|
14230
|
+
vars: z.record(z.string(), z.unknown()).optional(),
|
|
14231
|
+
delivery: z.object({
|
|
14232
|
+
weekdaysOnly: z.boolean().optional(),
|
|
14233
|
+
timeOfDay: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "expected HH:mm").optional(),
|
|
14234
|
+
useContactTimezone: z.boolean().optional(),
|
|
14235
|
+
timezone: z.string().optional()
|
|
14236
|
+
}).optional()
|
|
14176
14237
|
}),
|
|
14177
14238
|
z.object({
|
|
14178
14239
|
type: z.literal("tag"),
|
|
@@ -14907,6 +14968,7 @@ async function tryEnterFlow(flow, event, ctx) {
|
|
|
14907
14968
|
flowSlug: flow.slug,
|
|
14908
14969
|
flowVersion: flow.version,
|
|
14909
14970
|
emailAtEntry: sub.emailAtSubscribe,
|
|
14971
|
+
triggerEvent: { name: event.name, properties: event.properties ?? {}, occurredAt: event.occurredAt },
|
|
14910
14972
|
enteredAt: /* @__PURE__ */ new Date(),
|
|
14911
14973
|
status: "active",
|
|
14912
14974
|
currentStepIndex: 0,
|
|
@@ -15004,6 +15066,105 @@ function effectiveLowerBound(ctx, opts) {
|
|
|
15004
15066
|
}
|
|
15005
15067
|
return null;
|
|
15006
15068
|
}
|
|
15069
|
+
|
|
15070
|
+
// src/server/runner/delivery-window.ts
|
|
15071
|
+
var TIME_OF_DAY_GRACE_MS = 60 * 6e4;
|
|
15072
|
+
function computeDeliveryTime(now, window2, contactTimezone) {
|
|
15073
|
+
const tz = pickTimezone(window2, contactTimezone);
|
|
15074
|
+
let candidate = now;
|
|
15075
|
+
if (window2.timeOfDay) {
|
|
15076
|
+
const [hh, mm] = window2.timeOfDay.split(":").map(Number);
|
|
15077
|
+
const local = localParts(candidate, tz);
|
|
15078
|
+
const todaySlot = utcFromLocal(local.y, local.mo, local.d, hh, mm, tz);
|
|
15079
|
+
if (candidate.getTime() < todaySlot.getTime()) {
|
|
15080
|
+
candidate = todaySlot;
|
|
15081
|
+
} else if (candidate.getTime() - todaySlot.getTime() > TIME_OF_DAY_GRACE_MS) {
|
|
15082
|
+
const next = addLocalDays(local, 1);
|
|
15083
|
+
candidate = utcFromLocal(next.y, next.mo, next.d, hh, mm, tz);
|
|
15084
|
+
}
|
|
15085
|
+
}
|
|
15086
|
+
if (window2.weekdaysOnly) {
|
|
15087
|
+
for (let guard = 0; guard < 3; guard++) {
|
|
15088
|
+
const local = localParts(candidate, tz);
|
|
15089
|
+
if (local.weekday !== "Sat" && local.weekday !== "Sun") break;
|
|
15090
|
+
const shift = local.weekday === "Sat" ? 2 : 1;
|
|
15091
|
+
const moved = addLocalDays(local, shift);
|
|
15092
|
+
candidate = utcFromLocal(moved.y, moved.mo, moved.d, local.hh, local.mi, tz);
|
|
15093
|
+
}
|
|
15094
|
+
}
|
|
15095
|
+
return candidate;
|
|
15096
|
+
}
|
|
15097
|
+
function pickTimezone(window2, contactTimezone) {
|
|
15098
|
+
const candidates = [
|
|
15099
|
+
window2.useContactTimezone ? contactTimezone : void 0,
|
|
15100
|
+
window2.timezone,
|
|
15101
|
+
"UTC"
|
|
15102
|
+
];
|
|
15103
|
+
for (const tz of candidates) {
|
|
15104
|
+
if (tz && isValidTimezone(tz)) return tz;
|
|
15105
|
+
}
|
|
15106
|
+
return "UTC";
|
|
15107
|
+
}
|
|
15108
|
+
var validatedZones = /* @__PURE__ */ new Map();
|
|
15109
|
+
function isValidTimezone(tz) {
|
|
15110
|
+
const cached = validatedZones.get(tz);
|
|
15111
|
+
if (cached !== void 0) return cached;
|
|
15112
|
+
let ok = true;
|
|
15113
|
+
try {
|
|
15114
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
15115
|
+
} catch {
|
|
15116
|
+
ok = false;
|
|
15117
|
+
}
|
|
15118
|
+
validatedZones.set(tz, ok);
|
|
15119
|
+
return ok;
|
|
15120
|
+
}
|
|
15121
|
+
var partFormatters = /* @__PURE__ */ new Map();
|
|
15122
|
+
function formatterFor(tz) {
|
|
15123
|
+
let f = partFormatters.get(tz);
|
|
15124
|
+
if (!f) {
|
|
15125
|
+
f = new Intl.DateTimeFormat("en-US", {
|
|
15126
|
+
timeZone: tz,
|
|
15127
|
+
year: "numeric",
|
|
15128
|
+
month: "2-digit",
|
|
15129
|
+
day: "2-digit",
|
|
15130
|
+
hour: "2-digit",
|
|
15131
|
+
minute: "2-digit",
|
|
15132
|
+
second: "2-digit",
|
|
15133
|
+
weekday: "short",
|
|
15134
|
+
hour12: false
|
|
15135
|
+
});
|
|
15136
|
+
partFormatters.set(tz, f);
|
|
15137
|
+
}
|
|
15138
|
+
return f;
|
|
15139
|
+
}
|
|
15140
|
+
function localParts(date, tz) {
|
|
15141
|
+
const parts = {};
|
|
15142
|
+
for (const p of formatterFor(tz).formatToParts(date)) parts[p.type] = p.value;
|
|
15143
|
+
return {
|
|
15144
|
+
y: Number(parts.year),
|
|
15145
|
+
mo: Number(parts.month),
|
|
15146
|
+
d: Number(parts.day),
|
|
15147
|
+
hh: Number(parts.hour) % 24,
|
|
15148
|
+
// Intl emits '24' for midnight in some locales
|
|
15149
|
+
mi: Number(parts.minute),
|
|
15150
|
+
ss: Number(parts.second),
|
|
15151
|
+
weekday: parts.weekday
|
|
15152
|
+
};
|
|
15153
|
+
}
|
|
15154
|
+
function utcFromLocal(y, mo, d, hh, mi, tz) {
|
|
15155
|
+
let ts = Date.UTC(y, mo - 1, d, hh, mi, 0);
|
|
15156
|
+
for (let i = 0; i < 2; i++) {
|
|
15157
|
+
const p = localParts(new Date(ts), tz);
|
|
15158
|
+
const asUtc = Date.UTC(p.y, p.mo - 1, p.d, p.hh, p.mi, p.ss);
|
|
15159
|
+
const offset = asUtc - ts;
|
|
15160
|
+
ts = Date.UTC(y, mo - 1, d, hh, mi, 0) - offset;
|
|
15161
|
+
}
|
|
15162
|
+
return new Date(ts);
|
|
15163
|
+
}
|
|
15164
|
+
function addLocalDays(p, days) {
|
|
15165
|
+
const dt = new Date(Date.UTC(p.y, p.mo - 1, p.d + days));
|
|
15166
|
+
return { y: dt.getUTCFullYear(), mo: dt.getUTCMonth() + 1, d: dt.getUTCDate() };
|
|
15167
|
+
}
|
|
15007
15168
|
async function compileTemplate(mjml) {
|
|
15008
15169
|
const out = await mjml2html(mjml, { validationLevel: "soft", minify: false });
|
|
15009
15170
|
const plainText = derivePlaintext(out.html);
|
|
@@ -15121,6 +15282,9 @@ function makeHandlebars(extra) {
|
|
|
15121
15282
|
return hb;
|
|
15122
15283
|
}
|
|
15123
15284
|
|
|
15285
|
+
// src/server/runner/send.ts
|
|
15286
|
+
init_vars();
|
|
15287
|
+
|
|
15124
15288
|
// src/server/runner/suppression.ts
|
|
15125
15289
|
var SCOPES_BY_KIND = {
|
|
15126
15290
|
marketing: ["all", "marketing"],
|
|
@@ -15401,13 +15565,29 @@ async function dispatchSend(sendId, ctx) {
|
|
|
15401
15565
|
return;
|
|
15402
15566
|
}
|
|
15403
15567
|
const run = send.flowRunId ? await ctx.collections.flowRuns.findOne({ _id: send.flowRunId }) : null;
|
|
15404
|
-
|
|
15405
|
-
|
|
15406
|
-
|
|
15407
|
-
|
|
15408
|
-
|
|
15409
|
-
|
|
15410
|
-
|
|
15568
|
+
if (run && run.status === "exited" && run.exitReason?.startsWith("aborted_by_host")) {
|
|
15569
|
+
await ctx.collections.sends.updateOne(
|
|
15570
|
+
{ _id: send._id },
|
|
15571
|
+
{ $set: { status: "cancelled", errorMessage: `cancelled: ${run.exitReason}`, updatedAt: /* @__PURE__ */ new Date() } }
|
|
15572
|
+
);
|
|
15573
|
+
return;
|
|
15574
|
+
}
|
|
15575
|
+
let renderCtx;
|
|
15576
|
+
let rendered;
|
|
15577
|
+
try {
|
|
15578
|
+
const resolved = await resolveVars(ctx.varsAdapter, contact, {
|
|
15579
|
+
reason: "send",
|
|
15580
|
+
templateSlug: template.slug,
|
|
15581
|
+
flowSlug: run?.flowSlug,
|
|
15582
|
+
eventName: run?.triggerEvent?.name,
|
|
15583
|
+
eventProperties: run?.triggerEvent?.properties
|
|
15584
|
+
});
|
|
15585
|
+
renderCtx = buildRenderContext(contact, run, send.vars ?? {}, ctx, resolved);
|
|
15586
|
+
rendered = await renderTemplate(template, renderCtx, { helpers: ctx.handlebarsHelpers });
|
|
15587
|
+
} catch (err) {
|
|
15588
|
+
await markFailed(send._id, `render error: ${String(err?.message ?? err)}`, ctx);
|
|
15589
|
+
throw err;
|
|
15590
|
+
}
|
|
15411
15591
|
const tracking = applyTracking(rendered.html, {
|
|
15412
15592
|
sendId: String(send._id),
|
|
15413
15593
|
publicUrl: ctx.config.publicUrl,
|
|
@@ -15483,7 +15663,7 @@ function pickProviderName(stepOverride, tpl, ctx) {
|
|
|
15483
15663
|
}
|
|
15484
15664
|
return ctx.config.defaultProvider;
|
|
15485
15665
|
}
|
|
15486
|
-
function buildRenderContext(contact, run, vars, ctx) {
|
|
15666
|
+
function buildRenderContext(contact, run, vars, ctx, resolved = {}) {
|
|
15487
15667
|
const scope = "marketing";
|
|
15488
15668
|
const expiresAt = new Date(Date.now() + ctx.config.unsubscribeTokenLifetimeDays * 24 * 60 * 60 * 1e3);
|
|
15489
15669
|
const token = signUnsubscribeToken(
|
|
@@ -15492,8 +15672,10 @@ function buildRenderContext(contact, run, vars, ctx) {
|
|
|
15492
15672
|
);
|
|
15493
15673
|
const unsubscribeUrl = `${ctx.config.publicUrl}/m/unsub/${token}`;
|
|
15494
15674
|
return {
|
|
15675
|
+
...resolved,
|
|
15495
15676
|
contact,
|
|
15496
15677
|
vars,
|
|
15678
|
+
event: run?.triggerEvent?.properties ?? {},
|
|
15497
15679
|
unsubscribeUrl,
|
|
15498
15680
|
senderAddress: ctx.config.senderAddress
|
|
15499
15681
|
};
|
|
@@ -15578,8 +15760,15 @@ async function processOneRunStep(runId, ctx) {
|
|
|
15578
15760
|
return handleCondition(run, step, contact, ctx);
|
|
15579
15761
|
case "branch":
|
|
15580
15762
|
return handleBranch(run, step, contact, ctx);
|
|
15581
|
-
case "send":
|
|
15763
|
+
case "send": {
|
|
15764
|
+
if (step.delivery) {
|
|
15765
|
+
const deliverAt = computeDeliveryTime(/* @__PURE__ */ new Date(), step.delivery, contact.timezone);
|
|
15766
|
+
if (deliverAt.getTime() > Date.now() + 3e4) {
|
|
15767
|
+
return deferSendForWindow(run, deliverAt, ctx);
|
|
15768
|
+
}
|
|
15769
|
+
}
|
|
15582
15770
|
return handleSend(run, step, contact, flow, ctx);
|
|
15771
|
+
}
|
|
15583
15772
|
case "tag":
|
|
15584
15773
|
return handleTag(run, step, ctx);
|
|
15585
15774
|
case "fire_event":
|
|
@@ -15730,6 +15919,34 @@ async function handleWebhookStep(run, step, ctx) {
|
|
|
15730
15919
|
}
|
|
15731
15920
|
}
|
|
15732
15921
|
}
|
|
15922
|
+
async function deferSendForWindow(run, deliverAt, ctx) {
|
|
15923
|
+
const updated = await ctx.collections.flowRuns.findOneAndUpdate(
|
|
15924
|
+
// Only write once per deferral — if nextActionAt already points at (or
|
|
15925
|
+
// past) the slot, another worker/tick got here first.
|
|
15926
|
+
{ _id: run._id, currentStepIndex: run.currentStepIndex, nextActionAt: { $lt: deliverAt } },
|
|
15927
|
+
{
|
|
15928
|
+
$set: { nextActionAt: deliverAt, updatedAt: /* @__PURE__ */ new Date() },
|
|
15929
|
+
$push: {
|
|
15930
|
+
history: {
|
|
15931
|
+
stepIndex: run.currentStepIndex,
|
|
15932
|
+
action: "send_deferred",
|
|
15933
|
+
at: /* @__PURE__ */ new Date(),
|
|
15934
|
+
details: { until: deliverAt }
|
|
15935
|
+
}
|
|
15936
|
+
}
|
|
15937
|
+
},
|
|
15938
|
+
{ returnDocument: "after" }
|
|
15939
|
+
);
|
|
15940
|
+
if (!updated) return;
|
|
15941
|
+
await ctx.queues.advance.add(
|
|
15942
|
+
"advance",
|
|
15943
|
+
{ flowRunId: String(run._id) },
|
|
15944
|
+
{
|
|
15945
|
+
delay: Math.max(0, deliverAt.getTime() - Date.now()),
|
|
15946
|
+
jobId: `advance:${run._id}:${run.currentStepIndex}:window:${deliverAt.getTime()}`
|
|
15947
|
+
}
|
|
15948
|
+
);
|
|
15949
|
+
}
|
|
15733
15950
|
async function advanceStep(run, ctx, log, opts = {}) {
|
|
15734
15951
|
const stepInc = opts.stepInc ?? 1;
|
|
15735
15952
|
const updated = await ctx.collections.flowRuns.findOneAndUpdate(
|
|
@@ -16982,6 +17199,7 @@ var Mailer = class _Mailer {
|
|
|
16982
17199
|
db: this.db,
|
|
16983
17200
|
collections: this.collections,
|
|
16984
17201
|
adapter: this.adapter,
|
|
17202
|
+
varsAdapter: this.config.varsAdapter,
|
|
16985
17203
|
providers: this.providers,
|
|
16986
17204
|
queues: this.queues,
|
|
16987
17205
|
config: this.config,
|
|
@@ -17061,6 +17279,10 @@ var Mailer = class _Mailer {
|
|
|
17061
17279
|
if (!config.providers[config.defaultProvider]) {
|
|
17062
17280
|
throw new Error(`defaultProvider "${config.defaultProvider}" not in providers map`);
|
|
17063
17281
|
}
|
|
17282
|
+
if (config.varsAdapter) {
|
|
17283
|
+
const { assertNoReservedVarKeys: assertNoReservedVarKeys2 } = await Promise.resolve().then(() => (init_vars(), vars_exports));
|
|
17284
|
+
assertNoReservedVarKeys2(config.varsAdapter);
|
|
17285
|
+
}
|
|
17064
17286
|
const collections = getCollections(config.db, config.collectionPrefix);
|
|
17065
17287
|
await ensureIndexes(config.db, config.collectionPrefix);
|
|
17066
17288
|
const queueDriver = await createQueueDriver(config.queue, config.db);
|
|
@@ -17276,6 +17498,72 @@ var Mailer = class _Mailer {
|
|
|
17276
17498
|
await this.collections.contactTags.deleteOne({ externalId: parsed.externalId, tag: parsed.tag });
|
|
17277
17499
|
}
|
|
17278
17500
|
}
|
|
17501
|
+
// -------------------------------------------------------------------------
|
|
17502
|
+
// Flow abort
|
|
17503
|
+
// -------------------------------------------------------------------------
|
|
17504
|
+
/**
|
|
17505
|
+
* Abort every active run of one flow for a contact, immediately. Runs parked
|
|
17506
|
+
* in a `wait` exit too — their delayed wake-up jobs find the run exited and
|
|
17507
|
+
* no-op. Also cancels any of the flow's emails still sitting in the send
|
|
17508
|
+
* queue for this contact (queued or awaiting retry), so an abort means no
|
|
17509
|
+
* further mail, not just no further steps.
|
|
17510
|
+
*
|
|
17511
|
+
* No-op (returns zero counts) when nothing is active. Safe to call from the
|
|
17512
|
+
* same handler that processes the business event ("user upgraded").
|
|
17513
|
+
*/
|
|
17514
|
+
async abortFlow(flowSlug, externalId, opts = {}) {
|
|
17515
|
+
const parsed = abortFlowInputSchema.parse({ flowSlug, externalId, reason: opts.reason });
|
|
17516
|
+
const flow = await this.collections.flows.findOne(
|
|
17517
|
+
{ slug: parsed.flowSlug },
|
|
17518
|
+
{ projection: { _id: 1 } }
|
|
17519
|
+
);
|
|
17520
|
+
if (!flow) throw new Error(`abortFlow: unknown flow slug "${parsed.flowSlug}"`);
|
|
17521
|
+
const result = await this.abortActiveRuns(
|
|
17522
|
+
{ externalId: parsed.externalId, flowId: flow._id },
|
|
17523
|
+
parsed.reason ? `aborted_by_host:${parsed.reason}` : "aborted_by_host"
|
|
17524
|
+
);
|
|
17525
|
+
if (result.abortedRuns > 0 || result.cancelledSends > 0) {
|
|
17526
|
+
await this.audit({
|
|
17527
|
+
actor: "host",
|
|
17528
|
+
action: "flow.abort",
|
|
17529
|
+
resource: { collection: "mailer_flow_runs", slug: parsed.flowSlug },
|
|
17530
|
+
diffSummary: `abortFlow slug=${parsed.flowSlug} externalId=${parsed.externalId} runs=${result.abortedRuns} sends=${result.cancelledSends}${parsed.reason ? ` reason=${parsed.reason}` : ""}`
|
|
17531
|
+
});
|
|
17532
|
+
}
|
|
17533
|
+
return result;
|
|
17534
|
+
}
|
|
17535
|
+
/**
|
|
17536
|
+
* Abort every active flow run for a contact across all flows. Same semantics
|
|
17537
|
+
* as `abortFlow` — for "stop everything" events (account deleted, churned).
|
|
17538
|
+
*/
|
|
17539
|
+
async abortAllFlows(externalId, opts = {}) {
|
|
17540
|
+
const parsed = abortAllFlowsInputSchema.parse({ externalId, reason: opts.reason });
|
|
17541
|
+
const result = await this.abortActiveRuns(
|
|
17542
|
+
{ externalId: parsed.externalId },
|
|
17543
|
+
parsed.reason ? `aborted_by_host:${parsed.reason}` : "aborted_by_host"
|
|
17544
|
+
);
|
|
17545
|
+
if (result.abortedRuns > 0 || result.cancelledSends > 0) {
|
|
17546
|
+
await this.audit({
|
|
17547
|
+
actor: "host",
|
|
17548
|
+
action: "flow.abort_all",
|
|
17549
|
+
resource: { collection: "mailer_flow_runs" },
|
|
17550
|
+
diffSummary: `abortAllFlows externalId=${parsed.externalId} runs=${result.abortedRuns} sends=${result.cancelledSends}${parsed.reason ? ` reason=${parsed.reason}` : ""}`
|
|
17551
|
+
});
|
|
17552
|
+
}
|
|
17553
|
+
return result;
|
|
17554
|
+
}
|
|
17555
|
+
async abortActiveRuns(filter, exitReason) {
|
|
17556
|
+
const runs = await this.collections.flowRuns.find({ ...filter, status: "active" }).toArray();
|
|
17557
|
+
if (runs.length === 0) return { abortedRuns: 0, cancelledSends: 0 };
|
|
17558
|
+
for (const run of runs) {
|
|
17559
|
+
await exitFlowRun(run, exitReason, this.runnerContext);
|
|
17560
|
+
}
|
|
17561
|
+
const cancelled = await this.collections.sends.updateMany(
|
|
17562
|
+
{ flowRunId: { $in: runs.map((r) => r._id) }, status: { $in: ["queued", "failed"] } },
|
|
17563
|
+
{ $set: { status: "cancelled", errorMessage: `cancelled: ${exitReason}`, updatedAt: /* @__PURE__ */ new Date() } }
|
|
17564
|
+
);
|
|
17565
|
+
return { abortedRuns: runs.length, cancelledSends: cancelled.modifiedCount };
|
|
17566
|
+
}
|
|
17279
17567
|
/**
|
|
17280
17568
|
* GDPR right-to-erasure. Hard-deletes the contact's PII and leaves a hashed
|
|
17281
17569
|
* suppression row to block re-import. INVARIANT 9.
|