mailery 0.19.0 → 0.20.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.
@@ -1,4 +1,4 @@
1
- import { Db, ObjectId, Collection, ClientSession } from 'mongodb';
1
+ import { Db, MongoClientOptions, ObjectId, Collection, ClientSession } from 'mongodb';
2
2
  import { z } from 'zod';
3
3
  import Handlebars from 'handlebars';
4
4
  import IORedis from 'ioredis';
@@ -765,10 +765,31 @@ interface BotFilterConfig {
765
765
  */
766
766
  minOpenDelayMs?: number;
767
767
  }
768
+ /**
769
+ * A MongoDB connection mailery opens itself, on its own driver: `Mailer.init`
770
+ * connects, and `mailer.stop()` closes it. For hosts whose code is on another
771
+ * `mongodb` major (every Mongoose 8 app bundles driver 6), where sharing a
772
+ * `Db` is not possible.
773
+ */
774
+ interface MailerMongoConnection {
775
+ uri: string;
776
+ /** Database name. Defaults to the one named in `uri`. */
777
+ dbName?: string;
778
+ clientOptions?: MongoClientOptions;
779
+ }
780
+ /**
781
+ * Builds the contact adapter on the database mailery connected to. Use it with
782
+ * `mongo`, when the host has no `Db` of its own to hand the adapter:
783
+ * `adapter: (db) => new MongoContactAdapter({ db, collection: 'users' })`.
784
+ */
785
+ type ContactAdapterFactory = (db: Db) => ContactAdapter;
768
786
  interface MailerConfig {
769
- db: Db;
787
+ /** An open database on the host's own client. Give exactly one of `db` or `mongo`. */
788
+ db?: Db;
789
+ /** A connection mailery opens and owns. Give exactly one of `db` or `mongo`. */
790
+ mongo?: MailerMongoConnection;
770
791
  collectionPrefix?: string;
771
- adapter: ContactAdapter;
792
+ adapter: ContactAdapter | ContactAdapterFactory;
772
793
  /**
773
794
  * Optional host-provided template variables. `resolve(contact)` runs at
774
795
  * send/preview/test render time; its result is merged into the template
@@ -980,7 +1001,12 @@ interface MailerConfig {
980
1001
  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'>> & {
981
1002
  circuitBreaker: CircuitBreakerThresholds;
982
1003
  broadcastStopRules: BroadcastStopRules;
983
- } & MailerConfig;
1004
+ } & StorageResolvedConfig;
1005
+ /** `MailerConfig` once `Mailer.init` has opened storage: one `db`, one built adapter. */
1006
+ type StorageResolvedConfig = Omit<MailerConfig, 'db' | 'mongo' | 'adapter'> & {
1007
+ db: Db;
1008
+ adapter: ContactAdapter;
1009
+ };
984
1010
 
985
1011
  /**
986
1012
  * Mongo collection helpers + indexes for every mailer-owned collection.
@@ -1717,6 +1743,8 @@ declare class Mailer {
1717
1743
  readonly config: ResolvedConfig;
1718
1744
  readonly events: EventRegistry;
1719
1745
  private queueDriver;
1746
+ /** The client `Mailer.init` opened from `config.mongo`; closed by `stop()`. Null when the host gave `db`. */
1747
+ private ownedClient;
1720
1748
  private workersStarted;
1721
1749
  private runnerContext;
1722
1750
  private constructor();
@@ -1869,4 +1897,4 @@ declare class NullProvider implements MailProvider {
1869
1897
  reset(): void;
1870
1898
  }
1871
1899
 
1872
- 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 };
1900
+ export { type TemplateVersionDoc 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 RedisOptions as G, type HealthDoc as H, type SegmentDefinition as I, type SendDoc as J, type SendStatus as K, type LeadDoc as L, type MailProvider as M, type NormalizedEvent as N, type OutboxDoc as O, type Predicate as P, type SenderDomainConfig as Q, type RunnerContext as R, type SendArgs as S, type TemplateDoc as T, type SenderDomainRegistry as U, type SenderDomainValidation as V, type SubscriptionDoc as W, type SubscriptionStatus as X, type SuppressionDoc as Y, type SuppressionReason as Z, type TemplateKind as _, type Contact as a, type VarsAdapter as a0, type VarsResolveInfo as a1, type WebhookEventDoc as a2, defineVars as a3, ensureIndexes as a4, getCollections as a5, validateSenderDomain as a6, varsJsonSchema as a7, type FlowTrigger as a8, type QueueDriverConfig as a9, 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 ContactAdapterFactory as p, type ContactTagDoc as q, type FlowGoal as r, type FlowRunDoc as s, type FlowRunStatus as t, type FlowVersionDoc as u, type HealthStatus as v, type MailerConfig as w, type MailerMongoConnection as x, NullProvider as y, RESERVED_VAR_KEYS as z };
@@ -1,4 +1,4 @@
1
- import { Db, ObjectId, Collection, ClientSession } from 'mongodb';
1
+ import { Db, MongoClientOptions, ObjectId, Collection, ClientSession } from 'mongodb';
2
2
  import { z } from 'zod';
3
3
  import Handlebars from 'handlebars';
4
4
  import IORedis from 'ioredis';
@@ -765,10 +765,31 @@ interface BotFilterConfig {
765
765
  */
766
766
  minOpenDelayMs?: number;
767
767
  }
768
+ /**
769
+ * A MongoDB connection mailery opens itself, on its own driver: `Mailer.init`
770
+ * connects, and `mailer.stop()` closes it. For hosts whose code is on another
771
+ * `mongodb` major (every Mongoose 8 app bundles driver 6), where sharing a
772
+ * `Db` is not possible.
773
+ */
774
+ interface MailerMongoConnection {
775
+ uri: string;
776
+ /** Database name. Defaults to the one named in `uri`. */
777
+ dbName?: string;
778
+ clientOptions?: MongoClientOptions;
779
+ }
780
+ /**
781
+ * Builds the contact adapter on the database mailery connected to. Use it with
782
+ * `mongo`, when the host has no `Db` of its own to hand the adapter:
783
+ * `adapter: (db) => new MongoContactAdapter({ db, collection: 'users' })`.
784
+ */
785
+ type ContactAdapterFactory = (db: Db) => ContactAdapter;
768
786
  interface MailerConfig {
769
- db: Db;
787
+ /** An open database on the host's own client. Give exactly one of `db` or `mongo`. */
788
+ db?: Db;
789
+ /** A connection mailery opens and owns. Give exactly one of `db` or `mongo`. */
790
+ mongo?: MailerMongoConnection;
770
791
  collectionPrefix?: string;
771
- adapter: ContactAdapter;
792
+ adapter: ContactAdapter | ContactAdapterFactory;
772
793
  /**
773
794
  * Optional host-provided template variables. `resolve(contact)` runs at
774
795
  * send/preview/test render time; its result is merged into the template
@@ -980,7 +1001,12 @@ interface MailerConfig {
980
1001
  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'>> & {
981
1002
  circuitBreaker: CircuitBreakerThresholds;
982
1003
  broadcastStopRules: BroadcastStopRules;
983
- } & MailerConfig;
1004
+ } & StorageResolvedConfig;
1005
+ /** `MailerConfig` once `Mailer.init` has opened storage: one `db`, one built adapter. */
1006
+ type StorageResolvedConfig = Omit<MailerConfig, 'db' | 'mongo' | 'adapter'> & {
1007
+ db: Db;
1008
+ adapter: ContactAdapter;
1009
+ };
984
1010
 
985
1011
  /**
986
1012
  * Mongo collection helpers + indexes for every mailer-owned collection.
@@ -1717,6 +1743,8 @@ declare class Mailer {
1717
1743
  readonly config: ResolvedConfig;
1718
1744
  readonly events: EventRegistry;
1719
1745
  private queueDriver;
1746
+ /** The client `Mailer.init` opened from `config.mongo`; closed by `stop()`. Null when the host gave `db`. */
1747
+ private ownedClient;
1720
1748
  private workersStarted;
1721
1749
  private runnerContext;
1722
1750
  private constructor();
@@ -1869,4 +1897,4 @@ declare class NullProvider implements MailProvider {
1869
1897
  reset(): void;
1870
1898
  }
1871
1899
 
1872
- 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 };
1900
+ export { type TemplateVersionDoc 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 RedisOptions as G, type HealthDoc as H, type SegmentDefinition as I, type SendDoc as J, type SendStatus as K, type LeadDoc as L, type MailProvider as M, type NormalizedEvent as N, type OutboxDoc as O, type Predicate as P, type SenderDomainConfig as Q, type RunnerContext as R, type SendArgs as S, type TemplateDoc as T, type SenderDomainRegistry as U, type SenderDomainValidation as V, type SubscriptionDoc as W, type SubscriptionStatus as X, type SuppressionDoc as Y, type SuppressionReason as Z, type TemplateKind as _, type Contact as a, type VarsAdapter as a0, type VarsResolveInfo as a1, type WebhookEventDoc as a2, defineVars as a3, ensureIndexes as a4, getCollections as a5, validateSenderDomain as a6, varsJsonSchema as a7, type FlowTrigger as a8, type QueueDriverConfig as a9, 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 ContactAdapterFactory as p, type ContactTagDoc as q, type FlowGoal as r, type FlowRunDoc as s, type FlowRunStatus as t, type FlowVersionDoc as u, type HealthStatus as v, type MailerConfig as w, type MailerMongoConnection as x, NullProvider as y, RESERVED_VAR_KEYS as z };
package/dist/testing.cjs CHANGED
@@ -16140,7 +16140,7 @@ function derivePlaintext(html) {
16140
16140
  var PLAIN_TEXT = { noEscape: true };
16141
16141
  async function renderTemplate(template, ctx, opts = {}) {
16142
16142
  const hb = makeHandlebars(opts.helpers);
16143
- const subject = hb.compile(template.subject)(ctx);
16143
+ const subject = hb.compile(template.subject, PLAIN_TEXT)(ctx).replace(/[\r\n]+/g, " ");
16144
16144
  const preheader = hb.compile(template.preheader)(ctx);
16145
16145
  let html;
16146
16146
  if (template.body.html) {
@@ -17525,7 +17525,7 @@ async function runDnsblChecks(ctx, opts = {}) {
17525
17525
  let listedCount = 0;
17526
17526
  async function processPair(p) {
17527
17527
  const queryName = buildQueryName(p.target, p.targetKind, spamhausQueryHost(p.list.host, cfg.spamhausDqsKey));
17528
- const lookup2 = queryName ? await queryDnsbl(resolver, queryName) : { result: "error", returnCodes: [], errorMessage: "unsupported target format" };
17528
+ const lookup2 = queryName ? await queryDnsbl(resolver, queryName, p.list.host) : { result: "error", returnCodes: [], errorMessage: "unsupported target format" };
17529
17529
  if (lookup2.transient) return;
17530
17530
  if (lookup2.result === "listed") listedCount++;
17531
17531
  await ctx.collections.dnsblChecks.updateOne(
@@ -17586,10 +17586,10 @@ function buildQueryName(target, kind, listHost) {
17586
17586
  return `${domain}.${listHost}`;
17587
17587
  }
17588
17588
  var TRANSIENT_DNS_CODES = /* @__PURE__ */ new Set(["ESERVFAIL", "EREFUSED", "ETIMEOUT", "ETIMEDOUT", "ECONNRESET", "ECONNREFUSED"]);
17589
- async function queryDnsbl(resolver, query) {
17589
+ async function queryDnsbl(resolver, query, listHost) {
17590
17590
  try {
17591
17591
  const records = await resolver.resolve4(query);
17592
- return interpretRecords(records);
17592
+ return interpretRecords(records, listHost);
17593
17593
  } catch (err) {
17594
17594
  const code = err?.code;
17595
17595
  if (code === "ENOTFOUND" || code === "ENODATA") {
@@ -17615,10 +17615,17 @@ var REFUSAL_REASONS = {
17615
17615
  "127.255.255.254": "queried through a public or shared DNS resolver, which Spamhaus blocks. Set dnsbl.spamhausDqsKey (free Spamhaus DQS key) or use a private resolver",
17616
17616
  "127.255.255.255": "this resolver has exceeded the free query limit"
17617
17617
  };
17618
- function interpretRecords(records) {
17618
+ function interpretRecords(records, listHost) {
17619
17619
  if (!records || records.length === 0) {
17620
17620
  return { result: "clean", returnCodes: [], errorMessage: null };
17621
17621
  }
17622
+ if (listHost && REGISTRABLE_ONLY_LISTS.has(listHost) && records.every((r) => r === "127.0.0.1")) {
17623
+ return {
17624
+ result: "error",
17625
+ returnCodes: records,
17626
+ errorMessage: `${listHost} refused the query (127.0.0.1): queried through a public or shared DNS resolver, or over the free query limit. Use a private resolver`
17627
+ };
17628
+ }
17622
17629
  const errorish = records.filter((r) => r.startsWith("127.255.255."));
17623
17630
  if (errorish.length === records.length) {
17624
17631
  const reasons = [...new Set(records.map((r) => REFUSAL_REASONS[r] ?? "unrecognised refusal code"))];
@@ -18472,6 +18479,7 @@ async function processScheduledBroadcasts2(ctx) {
18472
18479
  }
18473
18480
 
18474
18481
  // src/server/mailer.ts
18482
+ var STORAGE_REQUIRED = "Mailer.init: give exactly one of `db` (an open Db) or `mongo` (a connection mailery opens)";
18475
18483
  var Mailer = class _Mailer {
18476
18484
  db;
18477
18485
  collections;
@@ -18481,6 +18489,8 @@ var Mailer = class _Mailer {
18481
18489
  config;
18482
18490
  events;
18483
18491
  queueDriver;
18492
+ /** The client `Mailer.init` opened from `config.mongo`; closed by `stop()`. Null when the host gave `db`. */
18493
+ ownedClient;
18484
18494
  workersStarted = false;
18485
18495
  runnerContext;
18486
18496
  constructor(args) {
@@ -18490,6 +18500,7 @@ var Mailer = class _Mailer {
18490
18500
  this.adapter = args.adapter;
18491
18501
  this.providers = args.providers;
18492
18502
  this.queueDriver = args.queueDriver;
18503
+ this.ownedClient = args.ownedClient;
18493
18504
  this.queues = args.queueDriver.queues;
18494
18505
  this.events = args.events;
18495
18506
  this.runnerContext = {
@@ -18586,36 +18597,58 @@ var Mailer = class _Mailer {
18586
18597
  });
18587
18598
  }
18588
18599
  static async init(input) {
18589
- const config = resolveConfig(input);
18590
- if (!resolveProvider(config.providers, config.defaultProvider)) {
18600
+ if (input.db == null === (input.mongo == null)) throw new Error(STORAGE_REQUIRED);
18601
+ if (!resolveProvider(input.providers, input.defaultProvider)) {
18591
18602
  throw new Error(
18592
- `defaultProvider "${config.defaultProvider}" is not a registered provider. Registered: ${registeredProviderNames(config.providers).join(", ") || "(none)"}`
18603
+ `defaultProvider "${input.defaultProvider}" is not a registered provider. Registered: ${registeredProviderNames(input.providers).join(", ") || "(none)"}`
18593
18604
  );
18594
18605
  }
18595
- if (config.defaultTransactionalProvider != null && !resolveProvider(config.providers, config.defaultTransactionalProvider)) {
18606
+ if (input.defaultTransactionalProvider != null && !resolveProvider(input.providers, input.defaultTransactionalProvider)) {
18596
18607
  throw new Error(
18597
- `defaultTransactionalProvider "${config.defaultTransactionalProvider}" is not a registered provider. Registered: ${registeredProviderNames(config.providers).join(", ") || "(none)"}`
18608
+ `defaultTransactionalProvider "${input.defaultTransactionalProvider}" is not a registered provider. Registered: ${registeredProviderNames(input.providers).join(", ") || "(none)"}`
18598
18609
  );
18599
18610
  }
18600
- if (config.varsAdapter) {
18611
+ if (input.varsAdapter) {
18601
18612
  const { assertNoReservedVarKeys: assertNoReservedVarKeys2 } = await Promise.resolve().then(() => (init_vars(), vars_exports));
18602
- assertNoReservedVarKeys2(config.varsAdapter);
18603
- }
18604
- const collections = getCollections(config.db, config.collectionPrefix);
18605
- await ensureIndexes(config.db, config.collectionPrefix);
18606
- const queueDriver = await createQueueDriver(config.queue, config.db);
18607
- if (!config.workerless && config.queue.driver !== "noop") {
18608
- await queueDriver.scheduleRepeatingTick(config.tickIntervalSeconds);
18609
- }
18610
- return new _Mailer({
18611
- config,
18612
- db: config.db,
18613
- collections,
18614
- adapter: config.adapter,
18615
- providers: config.providers,
18616
- queueDriver,
18617
- events: new EventRegistry()
18618
- });
18613
+ assertNoReservedVarKeys2(input.varsAdapter);
18614
+ }
18615
+ let ownedClient = null;
18616
+ let db;
18617
+ if (input.mongo) {
18618
+ const { MongoClient: MongoClient2 } = await import('mongodb');
18619
+ ownedClient = await MongoClient2.connect(input.mongo.uri, input.mongo.clientOptions);
18620
+ db = ownedClient.db(input.mongo.dbName);
18621
+ } else if (input.db) {
18622
+ db = input.db;
18623
+ } else {
18624
+ throw new Error(STORAGE_REQUIRED);
18625
+ }
18626
+ try {
18627
+ const config = resolveConfig({
18628
+ ...input,
18629
+ db,
18630
+ adapter: typeof input.adapter === "function" ? input.adapter(db) : input.adapter
18631
+ });
18632
+ const collections = getCollections(config.db, config.collectionPrefix);
18633
+ await ensureIndexes(config.db, config.collectionPrefix);
18634
+ const queueDriver = await createQueueDriver(config.queue, config.db);
18635
+ if (!config.workerless && config.queue.driver !== "noop") {
18636
+ await queueDriver.scheduleRepeatingTick(config.tickIntervalSeconds);
18637
+ }
18638
+ return new _Mailer({
18639
+ config,
18640
+ db: config.db,
18641
+ collections,
18642
+ adapter: config.adapter,
18643
+ providers: config.providers,
18644
+ queueDriver,
18645
+ events: new EventRegistry(),
18646
+ ownedClient
18647
+ });
18648
+ } catch (err) {
18649
+ await ownedClient?.close();
18650
+ throw err;
18651
+ }
18619
18652
  }
18620
18653
  // -------------------------------------------------------------------------
18621
18654
  // Event registration + firing
@@ -19088,6 +19121,8 @@ var Mailer = class _Mailer {
19088
19121
  async stop() {
19089
19122
  await this.queueDriver.close();
19090
19123
  this.workersStarted = false;
19124
+ await this.ownedClient?.close();
19125
+ this.ownedClient = null;
19091
19126
  }
19092
19127
  /** Used internally by the admin router and tests; not part of the public API. */
19093
19128
  getRunnerContext() {