mailery 0.17.1 → 0.18.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.
@@ -32,17 +32,41 @@ interface AdapterFilter {
32
32
  createdAfter?: Date;
33
33
  createdBefore?: Date;
34
34
  }
35
+ /**
36
+ * Order for `ContactAdapter.query`. `field` names a field on the host's
37
+ * contact record (for MongoContactAdapter, a document path such as
38
+ * `updatedAt`), not a key of the `Contact` projection. Ties break on the
39
+ * contact id, ascending, so the order is total and pagination is stable.
40
+ */
41
+ interface AdapterSort {
42
+ field: string;
43
+ direction: 'asc' | 'desc';
44
+ }
35
45
  interface ContactAdapter {
36
46
  getById(externalId: string): Promise<Contact | null>;
37
47
  getByEmail(email: string): Promise<Contact | null>;
38
48
  getBatch(externalIds: string[]): Promise<Map<string, Contact>>;
49
+ /**
50
+ * One page of contacts matching `filter`. Without `opts.sort` the order is
51
+ * the adapter's own and must be stable across calls (MongoContactAdapter:
52
+ * by id). `opts.sort` is honoured only by adapters that declare
53
+ * `supportsSort: true`; mailery never passes it to one that does not.
54
+ */
39
55
  query(filter: AdapterFilter, opts: {
40
56
  limit: number;
41
57
  cursor?: string;
58
+ sort?: AdapterSort;
42
59
  }): Promise<{
43
60
  contacts: Contact[];
44
61
  nextCursor?: string;
45
62
  }>;
63
+ /**
64
+ * True when `query` honours `opts.sort` (with an opaque cursor that
65
+ * encodes the sort position). Optional: adapters written before 0.18 omit
66
+ * it, and a broadcast with an `order` is then refused at create time
67
+ * rather than silently sent in id order.
68
+ */
69
+ readonly supportsSort?: boolean;
46
70
  count(filter: AdapterFilter): Promise<number>;
47
71
  addTags?(externalId: string, tags: string[]): Promise<void>;
48
72
  removeTags?(externalId: string, tags: string[]): Promise<void>;
@@ -448,7 +472,13 @@ type QueueDriverConfig = {
448
472
  * server-shaped interfaces.
449
473
  */
450
474
  type SubscriptionStatus = 'subscribed' | 'pending_doi' | 'unsubscribed' | 'bounced' | 'complained';
451
- type SendStatus = 'queued' | 'sending' | 'sent' | 'delivered' | 'bounced' | 'complained' | 'failed' | 'suppressed' | 'cancelled';
475
+ type SendStatus = 'queued' | 'sending' | 'sent' | 'delivered' | 'bounced' | 'complained' | 'failed' | 'suppressed' | 'cancelled'
476
+ /**
477
+ * A broadcast send parked because its broadcast is paused (stop rule,
478
+ * circuit breaker, operator). Never dispatched while held; resuming the
479
+ * broadcast re-queues it.
480
+ */
481
+ | 'held';
452
482
  type TemplateKind = 'transactional' | 'marketing';
453
483
  /**
454
484
  * How a template's body goes on the wire.
@@ -466,7 +496,12 @@ type TemplateBodyFormat = 'multipart' | 'text_only';
466
496
  type SuppressionScope = 'all' | 'marketing' | 'transactional';
467
497
  type SuppressionReason = 'unsubscribed' | 'hard_bounce' | 'complaint' | 'manual' | 'list_cleaning' | 'gdpr_forget';
468
498
  type FlowRunStatus = 'active' | 'completed' | 'exited' | 'failed';
469
- type BroadcastStatus = 'draft' | 'scheduled' | 'sending' | 'sent' | 'cancelled' | 'failed';
499
+ /**
500
+ * `paused` — dispatch stopped with the broadcast re-openable: a wave reached
501
+ * its `recipientCap`, a stop rule or the circuit breaker fired, or an
502
+ * operator paused it. `pauseReason` says which; `resume` re-opens it.
503
+ */
504
+ type BroadcastStatus = 'draft' | 'scheduled' | 'sending' | 'paused' | 'sent' | 'cancelled' | 'failed';
470
505
  type HealthStatus = 'healthy' | 'degraded' | 'tripped';
471
506
  type FlowGoal = 'activation' | 'conversion' | 'retention' | 'reactivation' | 'transactional' | 'broadcast';
472
507
 
@@ -678,6 +713,25 @@ interface CircuitBreakerThresholds {
678
713
  windowMinutes: number;
679
714
  minSendsBeforeEval: number;
680
715
  }
716
+ /**
717
+ * Automatic stop rules for one broadcast. A breach pauses the broadcast
718
+ * (`pauseReason.code: 'stop_rule'`): no more enqueues, and its queued sends
719
+ * are held until an explicit resume. Rates are measured over the
720
+ * broadcast's sends with a known outcome (delivered or bounced) and only
721
+ * once `minSample` of them are in. A breach is `rate > threshold`.
722
+ */
723
+ interface BroadcastStopRules {
724
+ /** Default true. */
725
+ enabled: boolean;
726
+ /** Hard bounces, % of outcomes. Default 2. */
727
+ hardBounceRatePct: number;
728
+ /** Complaints (spam reports), % of outcomes. Default 0.1. */
729
+ complaintRatePct: number;
730
+ /** Unsubscribes attributed to the broadcast, % of outcomes. Default 1. */
731
+ unsubscribeRatePct: number;
732
+ /** Outcomes required before any rule is evaluated. Default 100. */
733
+ minSample: number;
734
+ }
681
735
  interface BotFilterConfig {
682
736
  /**
683
737
  * User agents matching this pattern are treated as automated. Applied to
@@ -840,6 +894,12 @@ interface MailerConfig {
840
894
  broadcastConfirmationThreshold?: number;
841
895
  broadcastEnqueueBatchSize?: number;
842
896
  broadcastEnqueueMaxWaiting?: number;
897
+ /**
898
+ * Defaults for every broadcast's automatic stop rules; a broadcast can
899
+ * override any of them (`stopRules` on create/patch/resume). See
900
+ * `BroadcastStopRules`.
901
+ */
902
+ broadcastStopRules?: Partial<BroadcastStopRules>;
843
903
  workerless?: boolean;
844
904
  tickIntervalSeconds?: number;
845
905
  sendConcurrency?: number;
@@ -890,10 +950,27 @@ interface MailerConfig {
890
950
  send: any;
891
951
  error: Error;
892
952
  }) => Promise<void> | void;
953
+ /**
954
+ * Called when a broadcast is paused by a stop rule, the circuit breaker, a
955
+ * reached cap, or an operator. Alerting belongs here: a stop-rule pause
956
+ * waits for a human.
957
+ */
958
+ onBroadcastPaused?: (info: {
959
+ broadcastId: string;
960
+ slug: string;
961
+ reason: {
962
+ code: string;
963
+ message: string;
964
+ at: Date;
965
+ details?: Record<string, unknown>;
966
+ };
967
+ heldSends: number;
968
+ }) => Promise<void> | void;
893
969
  handlebarsHelpers?: Record<string, Handlebars.HelperDelegate>;
894
970
  }
895
971
  type ResolvedConfig = Required<Pick<MailerConfig, 'collectionPrefix' | 'requireDoubleOptIn' | 'unsubscribeTokenLifetimeDays' | 'transactionalRespectUnsubscribe' | 'unsubscribeWriteTimeoutMs' | 'doiTemplateSlug' | 'doiTokenLifetimeDays' | 'broadcastConfirmationThreshold' | 'broadcastEnqueueBatchSize' | 'broadcastEnqueueMaxWaiting' | 'workerless' | 'tickIntervalSeconds' | 'sendConcurrency' | 'sendRatePerSecond' | 'softBouncePromotionThreshold' | 'softBouncePromotionWindowDays' | 'webhookRetryAttempts' | 'sendRetryAttempts' | 'trackOpens' | 'trackClicks' | 'storeTrackingIp' | 'storeRenderedBody' | 'requireSignedTrackingUrls' | 'trackingUrlLifetimeDays'>> & {
896
972
  circuitBreaker: CircuitBreakerThresholds;
973
+ broadcastStopRules: BroadcastStopRules;
897
974
  } & MailerConfig;
898
975
 
899
976
  /**
@@ -1168,6 +1245,12 @@ interface SendDoc {
1168
1245
  updatedAt: Date;
1169
1246
  sentAt: Date | null;
1170
1247
  deliveredAt: Date | null;
1248
+ /**
1249
+ * Broadcast sends: the moment the send was meant to go (scheduledAt, or
1250
+ * the recipient-local slot). A held send is re-queued with the delay that
1251
+ * remains until then.
1252
+ */
1253
+ notBefore?: Date | null;
1171
1254
  }
1172
1255
  interface SuppressionDoc {
1173
1256
  _id?: ObjectId;
@@ -1197,6 +1280,43 @@ interface BroadcastDoc {
1197
1280
  recipientCount: number | null;
1198
1281
  /** When true, dispatch fires each recipient at their local-timezone equivalent of `scheduledAt`. */
1199
1282
  respectRecipientTimezone?: boolean;
1283
+ /**
1284
+ * Native waves: the most send rows this broadcast may have. Dispatch stops
1285
+ * enqueueing when the broadcast's send rows (every status, earlier waves
1286
+ * included) reach it, and parks the broadcast in `paused` with
1287
+ * `pauseReason.code === 'cap_reached'` if eligible recipients remain.
1288
+ * Raising it and resuming sends the next slice. Null or absent: no cap.
1289
+ */
1290
+ recipientCap?: number | null;
1291
+ /** Order recipients are taken in (host-side sort). Null or absent: the adapter's own order. */
1292
+ order?: BroadcastOrder | null;
1293
+ /** Set while `status === 'paused'`. */
1294
+ pausedAt?: Date | null;
1295
+ pauseReason?: BroadcastPauseReason | null;
1296
+ /** Lease held by the one dispatcher allowed to enqueue for this broadcast. */
1297
+ dispatchLeaseId?: string | null;
1298
+ /** Bumped on every (re)start of dispatch; part of the dispatch job id. */
1299
+ dispatchGeneration?: number;
1300
+ /** Why dispatch set status 'failed'. */
1301
+ failureReason?: string | null;
1302
+ /** Per-broadcast overrides of `MailerConfig.broadcastStopRules`. */
1303
+ stopRules?: Partial<{
1304
+ enabled: boolean;
1305
+ hardBounceRatePct: number;
1306
+ complaintRatePct: number;
1307
+ unsubscribeRatePct: number;
1308
+ minSample: number;
1309
+ }> | null;
1310
+ /**
1311
+ * First stop-rule breach seen after the broadcast had nothing left to
1312
+ * hold (status 'sent', no queued sends) — recorded, since there was
1313
+ * nothing to pause.
1314
+ */
1315
+ stopRuleBreach?: {
1316
+ at: Date;
1317
+ sample: number;
1318
+ breaches: StopRuleBreach[];
1319
+ } | null;
1200
1320
  stats: {
1201
1321
  sent: number;
1202
1322
  delivered: number;
@@ -1210,6 +1330,29 @@ interface BroadcastDoc {
1210
1330
  createdBy: string;
1211
1331
  updatedAt: Date;
1212
1332
  }
1333
+ interface BroadcastOrder {
1334
+ /** A host field, e.g. `updatedAt` for "most recently active first" with `direction: 'desc'`. */
1335
+ field: string;
1336
+ direction: 'asc' | 'desc';
1337
+ }
1338
+ interface StopRuleBreach {
1339
+ rule: 'hardBounceRatePct' | 'complaintRatePct' | 'unsubscribeRatePct';
1340
+ count: number;
1341
+ ratePct: number;
1342
+ thresholdPct: number;
1343
+ }
1344
+ interface BroadcastPauseReason {
1345
+ /**
1346
+ * cap_reached — the wave's recipientCap is spent; raise it and resume.
1347
+ * stop_rule — a per-broadcast bounce/complaint/unsubscribe threshold was crossed.
1348
+ * circuit_breaker — the sender domain's marketing circuit breaker tripped.
1349
+ * manual — paused by an operator.
1350
+ */
1351
+ code: 'cap_reached' | 'stop_rule' | 'circuit_breaker' | 'manual';
1352
+ message: string;
1353
+ at: Date;
1354
+ details?: Record<string, unknown>;
1355
+ }
1213
1356
  interface OutboxDoc {
1214
1357
  _id?: ObjectId;
1215
1358
  payload: {
@@ -1713,4 +1856,4 @@ declare class NullProvider implements MailProvider {
1713
1856
  reset(): void;
1714
1857
  }
1715
1858
 
1716
- export { ensureIndexes as $, type AdapterFilter as A, type BotFilterConfig as B, type ContactAdapter as C, type DeliveryWindow as D, type EventDoc as E, type FlowStep as F, type SenderDomainRegistry as G, type HealthDoc as H, type SenderDomainValidation as I, type SubscriptionDoc as J, type SubscriptionStatus as K, type LeadDoc as L, type MailProvider as M, type NormalizedEvent as N, type OutboxDoc as O, type Predicate as P, type SuppressionDoc as Q, type RunnerContext as R, type SendArgs as S, type TemplateDoc as T, type SuppressionReason as U, type TemplateKind as V, type TemplateVersionDoc as W, type VarsAdapter as X, type VarsResolveInfo as Y, type WebhookEventDoc as Z, defineVars as _, type Contact as a, getCollections as a0, validateSenderDomain as a1, varsJsonSchema as a2, type FlowTrigger as a3, type QueueDriverConfig as a4, type SendResult as b, type MailTesterFeedback as c, Mailer as d, type Collections as e, type FlowDoc as f, type SuppressionScope as g, type SegmentFilter as h, type AuditLogDoc as i, type BroadcastDoc as j, type BroadcastStatus as k, type CircuitBreakerThresholds as l, type ContactTagDoc as m, type FlowGoal as n, type FlowRunDoc as o, type FlowRunStatus as p, type FlowVersionDoc as q, type HealthStatus as r, type MailerConfig as s, NullProvider as t, RESERVED_VAR_KEYS as u, type RedisOptions as v, type SegmentDefinition as w, type SendDoc as x, type SendStatus as y, type SenderDomainConfig as z };
1859
+ export { type VarsResolveInfo as $, type AdapterFilter as A, type BotFilterConfig as B, type ContactAdapter as C, type DeliveryWindow as D, type EventDoc as E, type FlowStep as F, type SendDoc as G, type HealthDoc as H, type SendStatus as I, type SenderDomainConfig as J, type SenderDomainRegistry as K, type LeadDoc as L, type MailProvider as M, type NormalizedEvent as N, type OutboxDoc as O, type Predicate as P, type SenderDomainValidation as Q, type RunnerContext as R, type SendArgs as S, type TemplateDoc as T, type SubscriptionDoc as U, type SubscriptionStatus as V, type SuppressionDoc as W, type SuppressionReason as X, type TemplateKind as Y, type TemplateVersionDoc as Z, type VarsAdapter as _, type Contact as a, type WebhookEventDoc as a0, defineVars as a1, ensureIndexes as a2, getCollections as a3, validateSenderDomain as a4, varsJsonSchema as a5, type FlowTrigger as a6, type QueueDriverConfig as a7, type AdapterSort as b, type SendResult as c, type MailTesterFeedback as d, Mailer as e, type Collections as f, type FlowDoc as g, type SuppressionScope as h, type SegmentFilter as i, type AuditLogDoc as j, type BroadcastDoc as k, type BroadcastOrder as l, type BroadcastPauseReason as m, type BroadcastStatus as n, type CircuitBreakerThresholds as o, type ContactTagDoc as p, type FlowGoal as q, type FlowRunDoc as r, type FlowRunStatus as s, type FlowVersionDoc as t, type HealthStatus as u, type MailerConfig as v, NullProvider as w, RESERVED_VAR_KEYS as x, type RedisOptions as y, type SegmentDefinition as z };
@@ -32,17 +32,41 @@ interface AdapterFilter {
32
32
  createdAfter?: Date;
33
33
  createdBefore?: Date;
34
34
  }
35
+ /**
36
+ * Order for `ContactAdapter.query`. `field` names a field on the host's
37
+ * contact record (for MongoContactAdapter, a document path such as
38
+ * `updatedAt`), not a key of the `Contact` projection. Ties break on the
39
+ * contact id, ascending, so the order is total and pagination is stable.
40
+ */
41
+ interface AdapterSort {
42
+ field: string;
43
+ direction: 'asc' | 'desc';
44
+ }
35
45
  interface ContactAdapter {
36
46
  getById(externalId: string): Promise<Contact | null>;
37
47
  getByEmail(email: string): Promise<Contact | null>;
38
48
  getBatch(externalIds: string[]): Promise<Map<string, Contact>>;
49
+ /**
50
+ * One page of contacts matching `filter`. Without `opts.sort` the order is
51
+ * the adapter's own and must be stable across calls (MongoContactAdapter:
52
+ * by id). `opts.sort` is honoured only by adapters that declare
53
+ * `supportsSort: true`; mailery never passes it to one that does not.
54
+ */
39
55
  query(filter: AdapterFilter, opts: {
40
56
  limit: number;
41
57
  cursor?: string;
58
+ sort?: AdapterSort;
42
59
  }): Promise<{
43
60
  contacts: Contact[];
44
61
  nextCursor?: string;
45
62
  }>;
63
+ /**
64
+ * True when `query` honours `opts.sort` (with an opaque cursor that
65
+ * encodes the sort position). Optional: adapters written before 0.18 omit
66
+ * it, and a broadcast with an `order` is then refused at create time
67
+ * rather than silently sent in id order.
68
+ */
69
+ readonly supportsSort?: boolean;
46
70
  count(filter: AdapterFilter): Promise<number>;
47
71
  addTags?(externalId: string, tags: string[]): Promise<void>;
48
72
  removeTags?(externalId: string, tags: string[]): Promise<void>;
@@ -448,7 +472,13 @@ type QueueDriverConfig = {
448
472
  * server-shaped interfaces.
449
473
  */
450
474
  type SubscriptionStatus = 'subscribed' | 'pending_doi' | 'unsubscribed' | 'bounced' | 'complained';
451
- type SendStatus = 'queued' | 'sending' | 'sent' | 'delivered' | 'bounced' | 'complained' | 'failed' | 'suppressed' | 'cancelled';
475
+ type SendStatus = 'queued' | 'sending' | 'sent' | 'delivered' | 'bounced' | 'complained' | 'failed' | 'suppressed' | 'cancelled'
476
+ /**
477
+ * A broadcast send parked because its broadcast is paused (stop rule,
478
+ * circuit breaker, operator). Never dispatched while held; resuming the
479
+ * broadcast re-queues it.
480
+ */
481
+ | 'held';
452
482
  type TemplateKind = 'transactional' | 'marketing';
453
483
  /**
454
484
  * How a template's body goes on the wire.
@@ -466,7 +496,12 @@ type TemplateBodyFormat = 'multipart' | 'text_only';
466
496
  type SuppressionScope = 'all' | 'marketing' | 'transactional';
467
497
  type SuppressionReason = 'unsubscribed' | 'hard_bounce' | 'complaint' | 'manual' | 'list_cleaning' | 'gdpr_forget';
468
498
  type FlowRunStatus = 'active' | 'completed' | 'exited' | 'failed';
469
- type BroadcastStatus = 'draft' | 'scheduled' | 'sending' | 'sent' | 'cancelled' | 'failed';
499
+ /**
500
+ * `paused` — dispatch stopped with the broadcast re-openable: a wave reached
501
+ * its `recipientCap`, a stop rule or the circuit breaker fired, or an
502
+ * operator paused it. `pauseReason` says which; `resume` re-opens it.
503
+ */
504
+ type BroadcastStatus = 'draft' | 'scheduled' | 'sending' | 'paused' | 'sent' | 'cancelled' | 'failed';
470
505
  type HealthStatus = 'healthy' | 'degraded' | 'tripped';
471
506
  type FlowGoal = 'activation' | 'conversion' | 'retention' | 'reactivation' | 'transactional' | 'broadcast';
472
507
 
@@ -678,6 +713,25 @@ interface CircuitBreakerThresholds {
678
713
  windowMinutes: number;
679
714
  minSendsBeforeEval: number;
680
715
  }
716
+ /**
717
+ * Automatic stop rules for one broadcast. A breach pauses the broadcast
718
+ * (`pauseReason.code: 'stop_rule'`): no more enqueues, and its queued sends
719
+ * are held until an explicit resume. Rates are measured over the
720
+ * broadcast's sends with a known outcome (delivered or bounced) and only
721
+ * once `minSample` of them are in. A breach is `rate > threshold`.
722
+ */
723
+ interface BroadcastStopRules {
724
+ /** Default true. */
725
+ enabled: boolean;
726
+ /** Hard bounces, % of outcomes. Default 2. */
727
+ hardBounceRatePct: number;
728
+ /** Complaints (spam reports), % of outcomes. Default 0.1. */
729
+ complaintRatePct: number;
730
+ /** Unsubscribes attributed to the broadcast, % of outcomes. Default 1. */
731
+ unsubscribeRatePct: number;
732
+ /** Outcomes required before any rule is evaluated. Default 100. */
733
+ minSample: number;
734
+ }
681
735
  interface BotFilterConfig {
682
736
  /**
683
737
  * User agents matching this pattern are treated as automated. Applied to
@@ -840,6 +894,12 @@ interface MailerConfig {
840
894
  broadcastConfirmationThreshold?: number;
841
895
  broadcastEnqueueBatchSize?: number;
842
896
  broadcastEnqueueMaxWaiting?: number;
897
+ /**
898
+ * Defaults for every broadcast's automatic stop rules; a broadcast can
899
+ * override any of them (`stopRules` on create/patch/resume). See
900
+ * `BroadcastStopRules`.
901
+ */
902
+ broadcastStopRules?: Partial<BroadcastStopRules>;
843
903
  workerless?: boolean;
844
904
  tickIntervalSeconds?: number;
845
905
  sendConcurrency?: number;
@@ -890,10 +950,27 @@ interface MailerConfig {
890
950
  send: any;
891
951
  error: Error;
892
952
  }) => Promise<void> | void;
953
+ /**
954
+ * Called when a broadcast is paused by a stop rule, the circuit breaker, a
955
+ * reached cap, or an operator. Alerting belongs here: a stop-rule pause
956
+ * waits for a human.
957
+ */
958
+ onBroadcastPaused?: (info: {
959
+ broadcastId: string;
960
+ slug: string;
961
+ reason: {
962
+ code: string;
963
+ message: string;
964
+ at: Date;
965
+ details?: Record<string, unknown>;
966
+ };
967
+ heldSends: number;
968
+ }) => Promise<void> | void;
893
969
  handlebarsHelpers?: Record<string, Handlebars.HelperDelegate>;
894
970
  }
895
971
  type ResolvedConfig = Required<Pick<MailerConfig, 'collectionPrefix' | 'requireDoubleOptIn' | 'unsubscribeTokenLifetimeDays' | 'transactionalRespectUnsubscribe' | 'unsubscribeWriteTimeoutMs' | 'doiTemplateSlug' | 'doiTokenLifetimeDays' | 'broadcastConfirmationThreshold' | 'broadcastEnqueueBatchSize' | 'broadcastEnqueueMaxWaiting' | 'workerless' | 'tickIntervalSeconds' | 'sendConcurrency' | 'sendRatePerSecond' | 'softBouncePromotionThreshold' | 'softBouncePromotionWindowDays' | 'webhookRetryAttempts' | 'sendRetryAttempts' | 'trackOpens' | 'trackClicks' | 'storeTrackingIp' | 'storeRenderedBody' | 'requireSignedTrackingUrls' | 'trackingUrlLifetimeDays'>> & {
896
972
  circuitBreaker: CircuitBreakerThresholds;
973
+ broadcastStopRules: BroadcastStopRules;
897
974
  } & MailerConfig;
898
975
 
899
976
  /**
@@ -1168,6 +1245,12 @@ interface SendDoc {
1168
1245
  updatedAt: Date;
1169
1246
  sentAt: Date | null;
1170
1247
  deliveredAt: Date | null;
1248
+ /**
1249
+ * Broadcast sends: the moment the send was meant to go (scheduledAt, or
1250
+ * the recipient-local slot). A held send is re-queued with the delay that
1251
+ * remains until then.
1252
+ */
1253
+ notBefore?: Date | null;
1171
1254
  }
1172
1255
  interface SuppressionDoc {
1173
1256
  _id?: ObjectId;
@@ -1197,6 +1280,43 @@ interface BroadcastDoc {
1197
1280
  recipientCount: number | null;
1198
1281
  /** When true, dispatch fires each recipient at their local-timezone equivalent of `scheduledAt`. */
1199
1282
  respectRecipientTimezone?: boolean;
1283
+ /**
1284
+ * Native waves: the most send rows this broadcast may have. Dispatch stops
1285
+ * enqueueing when the broadcast's send rows (every status, earlier waves
1286
+ * included) reach it, and parks the broadcast in `paused` with
1287
+ * `pauseReason.code === 'cap_reached'` if eligible recipients remain.
1288
+ * Raising it and resuming sends the next slice. Null or absent: no cap.
1289
+ */
1290
+ recipientCap?: number | null;
1291
+ /** Order recipients are taken in (host-side sort). Null or absent: the adapter's own order. */
1292
+ order?: BroadcastOrder | null;
1293
+ /** Set while `status === 'paused'`. */
1294
+ pausedAt?: Date | null;
1295
+ pauseReason?: BroadcastPauseReason | null;
1296
+ /** Lease held by the one dispatcher allowed to enqueue for this broadcast. */
1297
+ dispatchLeaseId?: string | null;
1298
+ /** Bumped on every (re)start of dispatch; part of the dispatch job id. */
1299
+ dispatchGeneration?: number;
1300
+ /** Why dispatch set status 'failed'. */
1301
+ failureReason?: string | null;
1302
+ /** Per-broadcast overrides of `MailerConfig.broadcastStopRules`. */
1303
+ stopRules?: Partial<{
1304
+ enabled: boolean;
1305
+ hardBounceRatePct: number;
1306
+ complaintRatePct: number;
1307
+ unsubscribeRatePct: number;
1308
+ minSample: number;
1309
+ }> | null;
1310
+ /**
1311
+ * First stop-rule breach seen after the broadcast had nothing left to
1312
+ * hold (status 'sent', no queued sends) — recorded, since there was
1313
+ * nothing to pause.
1314
+ */
1315
+ stopRuleBreach?: {
1316
+ at: Date;
1317
+ sample: number;
1318
+ breaches: StopRuleBreach[];
1319
+ } | null;
1200
1320
  stats: {
1201
1321
  sent: number;
1202
1322
  delivered: number;
@@ -1210,6 +1330,29 @@ interface BroadcastDoc {
1210
1330
  createdBy: string;
1211
1331
  updatedAt: Date;
1212
1332
  }
1333
+ interface BroadcastOrder {
1334
+ /** A host field, e.g. `updatedAt` for "most recently active first" with `direction: 'desc'`. */
1335
+ field: string;
1336
+ direction: 'asc' | 'desc';
1337
+ }
1338
+ interface StopRuleBreach {
1339
+ rule: 'hardBounceRatePct' | 'complaintRatePct' | 'unsubscribeRatePct';
1340
+ count: number;
1341
+ ratePct: number;
1342
+ thresholdPct: number;
1343
+ }
1344
+ interface BroadcastPauseReason {
1345
+ /**
1346
+ * cap_reached — the wave's recipientCap is spent; raise it and resume.
1347
+ * stop_rule — a per-broadcast bounce/complaint/unsubscribe threshold was crossed.
1348
+ * circuit_breaker — the sender domain's marketing circuit breaker tripped.
1349
+ * manual — paused by an operator.
1350
+ */
1351
+ code: 'cap_reached' | 'stop_rule' | 'circuit_breaker' | 'manual';
1352
+ message: string;
1353
+ at: Date;
1354
+ details?: Record<string, unknown>;
1355
+ }
1213
1356
  interface OutboxDoc {
1214
1357
  _id?: ObjectId;
1215
1358
  payload: {
@@ -1713,4 +1856,4 @@ declare class NullProvider implements MailProvider {
1713
1856
  reset(): void;
1714
1857
  }
1715
1858
 
1716
- export { ensureIndexes as $, type AdapterFilter as A, type BotFilterConfig as B, type ContactAdapter as C, type DeliveryWindow as D, type EventDoc as E, type FlowStep as F, type SenderDomainRegistry as G, type HealthDoc as H, type SenderDomainValidation as I, type SubscriptionDoc as J, type SubscriptionStatus as K, type LeadDoc as L, type MailProvider as M, type NormalizedEvent as N, type OutboxDoc as O, type Predicate as P, type SuppressionDoc as Q, type RunnerContext as R, type SendArgs as S, type TemplateDoc as T, type SuppressionReason as U, type TemplateKind as V, type TemplateVersionDoc as W, type VarsAdapter as X, type VarsResolveInfo as Y, type WebhookEventDoc as Z, defineVars as _, type Contact as a, getCollections as a0, validateSenderDomain as a1, varsJsonSchema as a2, type FlowTrigger as a3, type QueueDriverConfig as a4, type SendResult as b, type MailTesterFeedback as c, Mailer as d, type Collections as e, type FlowDoc as f, type SuppressionScope as g, type SegmentFilter as h, type AuditLogDoc as i, type BroadcastDoc as j, type BroadcastStatus as k, type CircuitBreakerThresholds as l, type ContactTagDoc as m, type FlowGoal as n, type FlowRunDoc as o, type FlowRunStatus as p, type FlowVersionDoc as q, type HealthStatus as r, type MailerConfig as s, NullProvider as t, RESERVED_VAR_KEYS as u, type RedisOptions as v, type SegmentDefinition as w, type SendDoc as x, type SendStatus as y, type SenderDomainConfig as z };
1859
+ export { type VarsResolveInfo as $, type AdapterFilter as A, type BotFilterConfig as B, type ContactAdapter as C, type DeliveryWindow as D, type EventDoc as E, type FlowStep as F, type SendDoc as G, type HealthDoc as H, type SendStatus as I, type SenderDomainConfig as J, type SenderDomainRegistry as K, type LeadDoc as L, type MailProvider as M, type NormalizedEvent as N, type OutboxDoc as O, type Predicate as P, type SenderDomainValidation as Q, type RunnerContext as R, type SendArgs as S, type TemplateDoc as T, type SubscriptionDoc as U, type SubscriptionStatus as V, type SuppressionDoc as W, type SuppressionReason as X, type TemplateKind as Y, type TemplateVersionDoc as Z, type VarsAdapter as _, type Contact as a, type WebhookEventDoc as a0, defineVars as a1, ensureIndexes as a2, getCollections as a3, validateSenderDomain as a4, varsJsonSchema as a5, type FlowTrigger as a6, type QueueDriverConfig as a7, type AdapterSort as b, type SendResult as c, type MailTesterFeedback as d, Mailer as e, type Collections as f, type FlowDoc as g, type SuppressionScope as h, type SegmentFilter as i, type AuditLogDoc as j, type BroadcastDoc as k, type BroadcastOrder as l, type BroadcastPauseReason as m, type BroadcastStatus as n, type CircuitBreakerThresholds as o, type ContactTagDoc as p, type FlowGoal as q, type FlowRunDoc as r, type FlowRunStatus as s, type FlowVersionDoc as t, type HealthStatus as u, type MailerConfig as v, NullProvider as w, RESERVED_VAR_KEYS as x, type RedisOptions as y, type SegmentDefinition as z };