@boringnode/queue 0.0.1-alpha → 0.0.1-alpha.1

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 CHANGED
@@ -0,0 +1,334 @@
1
+ # @boringnode/queue
2
+
3
+ A simple and efficient queue system for Node.js applications. Built for simplicity and ease of use, `@boringnode/queue` allows you to dispatch background jobs and process them asynchronously with support for multiple queue adapters.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @boringnode/queue
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - **Multiple Queue Adapters**: Support for Redis (production) and Sync (testing/development)
14
+ - **Type-Safe Jobs**: Define jobs as TypeScript classes with typed payloads
15
+ - **Delayed Jobs**: Schedule jobs to run after a specific delay
16
+ - **Multiple Queues**: Organize jobs into different queues for better organization
17
+ - **Worker Management**: Process jobs with configurable concurrency
18
+ - **Auto-Discovery**: Automatically discover and register jobs from specified locations
19
+ - **Priority Queues**: Process high-priority jobs first
20
+ - **Retry with Backoff**: Automatic retries with exponential, linear, or fixed backoff strategies
21
+ - **Job Timeout**: Automatically fail or retry jobs that exceed a time limit
22
+
23
+ ## Quick Start
24
+
25
+ ### 1. Define a Job
26
+
27
+ Create a job by extending the `Job` class:
28
+
29
+ ```typescript
30
+ import { Job } from '@boringnode/queue'
31
+ import type { JobOptions } from '@boringnode/queue/types/main'
32
+
33
+ interface SendEmailPayload {
34
+ to: string
35
+ }
36
+
37
+ export default class SendEmailJob extends Job<SendEmailPayload> {
38
+ static readonly jobName = 'SendEmailJob'
39
+
40
+ static options: JobOptions = {
41
+ queue: 'email',
42
+ }
43
+
44
+ async execute(): Promise<void> {
45
+ console.log(`Sending email to: ${this.payload.to}`)
46
+ }
47
+ }
48
+ ```
49
+
50
+ ### 2. Configure the Queue Manager
51
+
52
+ ```typescript
53
+ import { QueueManager } from '@boringnode/queue'
54
+ import { redis } from '@boringnode/queue/drivers/redis_adapter'
55
+ import { sync } from '@boringnode/queue/drivers/sync_adapter'
56
+ import { Redis } from 'ioredis'
57
+
58
+ const redisConnection = new Redis({
59
+ host: 'localhost',
60
+ port: 6379,
61
+ keyPrefix: 'boringnode::queue::',
62
+ db: 0,
63
+ })
64
+
65
+ const config = {
66
+ default: 'redis',
67
+
68
+ adapters: {
69
+ sync: sync(),
70
+ redis: redis(redisConnection),
71
+ },
72
+
73
+ worker: {
74
+ concurrency: 5,
75
+ pollingInterval: '10ms',
76
+ },
77
+
78
+ locations: ['./app/jobs/**/*.ts'],
79
+ }
80
+
81
+ await QueueManager.init(config)
82
+ ```
83
+
84
+ ### 3. Dispatch Jobs
85
+
86
+ ```typescript
87
+ import SendEmailJob from './jobs/send_email_job.ts'
88
+
89
+ // Dispatch immediately
90
+ await SendEmailJob.dispatch({ to: 'user@example.com' })
91
+
92
+ // Dispatch with delay
93
+ await SendEmailJob.dispatch({ to: 'user@example.com' }).in('5m')
94
+ ```
95
+
96
+ ### 4. Start a Worker
97
+
98
+ Create a worker to process jobs:
99
+
100
+ ```typescript
101
+ import { Worker } from '@boringnode/queue'
102
+
103
+ const worker = new Worker(config)
104
+ await worker.start(['default', 'email', 'reports'])
105
+ ```
106
+
107
+ ## Configuration
108
+
109
+ ### Queue Manager Options
110
+
111
+ ```typescript
112
+ interface QueueManagerConfig {
113
+ // Default adapter to use
114
+ default: string
115
+
116
+ // Available queue adapters
117
+ adapters: {
118
+ [key: string]: QueueAdapter
119
+ }
120
+
121
+ // Worker configuration
122
+ worker: {
123
+ concurrency: number
124
+ pollingInterval: string
125
+ }
126
+
127
+ // Job discovery locations
128
+ locations: string[]
129
+ }
130
+ ```
131
+
132
+ ### Job Options
133
+
134
+ Configure individual jobs with the `options` property:
135
+
136
+ ```typescript
137
+ static options: JobOptions = {
138
+ queue: 'email', // Queue name (default: 'default')
139
+ adapter: 'redis', // Override default adapter
140
+ priority: 1, // Lower number = higher priority (default: 5)
141
+ maxRetries: 3, // Maximum retry attempts
142
+ timeout: '30s', // Job timeout duration
143
+ failOnTimeout: true, // Fail permanently on timeout (default: false, will retry)
144
+ }
145
+ ```
146
+
147
+ ## Adapters
148
+
149
+ ### Redis Adapter
150
+
151
+ For production use with distributed systems:
152
+
153
+ ```typescript
154
+ import { redis } from '@boringnode/queue/drivers/redis_adapter'
155
+ import { Redis } from 'ioredis'
156
+
157
+ const redisConnection = new Redis({
158
+ host: 'localhost',
159
+ port: 6379,
160
+ keyPrefix: 'boringnode::queue::',
161
+ })
162
+
163
+ const adapter = redis(redisConnection)
164
+ ```
165
+
166
+ ### Sync Adapter
167
+
168
+ For testing and development:
169
+
170
+ ```typescript
171
+ import { sync } from '@boringnode/queue/drivers/sync_adapter'
172
+
173
+ const adapter = sync()
174
+ ```
175
+
176
+ ## Worker Configuration
177
+
178
+ Workers process jobs from one or more queues:
179
+
180
+ ```typescript
181
+ const worker = new Worker(config)
182
+
183
+ // Process specific queues
184
+ await worker.start(['default', 'email', 'reports'])
185
+
186
+ // Worker will:
187
+ // - Process jobs with configured concurrency
188
+ // - Poll queues at the configured interval
189
+ // - Execute jobs in the order they were queued
190
+ ```
191
+
192
+ ## Delayed Jobs
193
+
194
+ Schedule jobs to run in the future:
195
+
196
+ ```typescript
197
+ // Various time formats
198
+ await SendEmailJob.dispatch(payload).in('30s') // 30 seconds
199
+ await SendEmailJob.dispatch(payload).in('5m') // 5 minutes
200
+ await SendEmailJob.dispatch(payload).in('2h') // 2 hours
201
+ await SendEmailJob.dispatch(payload).in('1d') // 1 day
202
+ ```
203
+
204
+ ## Priority
205
+
206
+ Jobs with lower priority numbers are processed first:
207
+
208
+ ```typescript
209
+ export default class UrgentJob extends Job<Payload> {
210
+ static readonly jobName = 'UrgentJob'
211
+
212
+ static options: JobOptions = {
213
+ priority: 1, // Processed before default priority (5)
214
+ }
215
+
216
+ async execute(): Promise<void> {
217
+ // ...
218
+ }
219
+ }
220
+ ```
221
+
222
+ ## Retry and Backoff
223
+
224
+ Configure automatic retries with backoff strategies:
225
+
226
+ ```typescript
227
+ import { exponentialBackoff, linearBackoff, fixedBackoff } from '@boringnode/queue'
228
+
229
+ export default class ReliableJob extends Job<Payload> {
230
+ static readonly jobName = 'ReliableJob'
231
+
232
+ static options: JobOptions = {
233
+ maxRetries: 5,
234
+ retry: {
235
+ backoff: () => exponentialBackoff({
236
+ baseDelay: '1s',
237
+ maxDelay: '1m',
238
+ multiplier: 2,
239
+ jitter: true,
240
+ }),
241
+ },
242
+ }
243
+
244
+ async execute(): Promise<void> {
245
+ // ...
246
+ }
247
+ }
248
+ ```
249
+
250
+ Available backoff strategies:
251
+
252
+ - `exponentialBackoff({ baseDelay, maxDelay, multiplier, jitter })` - Exponential increase
253
+ - `linearBackoff({ baseDelay, maxDelay, multiplier })` - Linear increase
254
+ - `fixedBackoff({ baseDelay, jitter })` - Fixed delay between retries
255
+
256
+ ## Job Timeout
257
+
258
+ Set a maximum execution time for jobs:
259
+
260
+ ```typescript
261
+ export default class LimitedJob extends Job<Payload> {
262
+ static readonly jobName = 'LimitedJob'
263
+
264
+ static options: JobOptions = {
265
+ timeout: '30s', // Maximum execution time
266
+ failOnTimeout: false, // Retry on timeout (default)
267
+ }
268
+
269
+ async execute(): Promise<void> {
270
+ // Long running operation...
271
+ }
272
+ }
273
+ ```
274
+
275
+ You can also set a global timeout in the worker configuration:
276
+
277
+ ```typescript
278
+ const config = {
279
+ worker: {
280
+ timeout: '1m', // Default timeout for all jobs
281
+ },
282
+ }
283
+ ```
284
+
285
+ ## Job Discovery
286
+
287
+ The queue manager automatically discovers and registers jobs from the specified locations:
288
+
289
+ ```typescript
290
+ const config = {
291
+ locations: [
292
+ './app/jobs/**/*.ts',
293
+ './modules/**/jobs/**/*.ts',
294
+ ],
295
+ }
296
+ ```
297
+
298
+ Jobs must:
299
+ - Extend the `Job` class
300
+ - Have a static `jobName` property
301
+ - Implement the `execute` method
302
+ - Be exported as default
303
+
304
+ ## Benchmarks
305
+
306
+ Performance comparison with BullMQ using realistic jobs (5ms simulated work per job):
307
+
308
+ | Jobs | Concurrency | @boringnode/queue | BullMQ | Diff |
309
+ |------|-------------|-------------------|--------|--------------|
310
+ | 100 | 1 | 562ms | 596ms | 5.7% faster |
311
+ | 100 | 5 | 116ms | 117ms | ~same |
312
+ | 100 | 10 | 62ms | 62ms | ~same |
313
+ | 500 | 1 | 2728ms | 2798ms | 2.5% faster |
314
+ | 500 | 5 | 565ms | 565ms | ~same |
315
+ | 500 | 10 | 287ms | 288ms | ~same |
316
+ | 1000 | 1 | 5450ms | 5547ms | 1.7% faster |
317
+ | 1000 | 5 | 1096ms | 1116ms | 1.8% faster |
318
+ | 1000 | 10 | 565ms | 579ms | 2.4% faster |
319
+ | 100K | 5 | 110.5s | 112.3s | 1.5% faster |
320
+ | 100K | 10 | 56.2s | 57.5s | 2.1% faster |
321
+ | 100K | 20 | 29.1s | 29.6s | 1.7% faster |
322
+
323
+ Run benchmarks yourself:
324
+
325
+ ```bash
326
+ # Realistic benchmark (5ms job duration)
327
+ npm run benchmark -- --realistic
328
+
329
+ # Pure dequeue overhead (no-op jobs)
330
+ npm run benchmark
331
+
332
+ # Custom job duration
333
+ npm run benchmark -- --duration=10
334
+ ```
package/build/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
- import { Q as QueueManagerConfig, W as WorkerCycle, A as Adapter, R as RetryConfig } from './job-CcAUWe8j.js';
2
- export { J as Job, c as customBackoff, e as exponentialBackoff, f as fixedBackoff, l as linearBackoff } from './job-CcAUWe8j.js';
3
- import './src/contracts/lease_manager.js';
1
+ import { Q as QueueManagerConfig, W as WorkerCycle, A as Adapter, R as RetryConfig } from './job-Bd_c2lFK.js';
2
+ export { J as Job, c as customBackoff, e as exponentialBackoff, f as fixedBackoff, l as linearBackoff } from './job-Bd_c2lFK.js';
4
3
 
5
4
  declare class Worker {
6
5
  #private;
package/build/index.js CHANGED
@@ -34,15 +34,16 @@ import debug from "#src/debug";
34
34
  import { parse } from "#src/utils";
35
35
  import * as errors from "#src/exceptions";
36
36
  import { QueueManager } from "#src/queue_manager";
37
+ import { JobPool } from "#src/job_pool";
37
38
  import { Locator } from "#src/locator";
38
39
  var Worker = class {
39
40
  #id;
40
41
  #config;
41
42
  #adapter;
42
- #leaseManager;
43
43
  #running = false;
44
44
  #initialized = false;
45
45
  #generator;
46
+ #pool;
46
47
  get id() {
47
48
  return this.#id;
48
49
  }
@@ -58,11 +59,7 @@ var Worker = class {
58
59
  debug("initializing worker %s", this.#id);
59
60
  await QueueManager.init(this.#config);
60
61
  this.#adapter = QueueManager.use();
61
- this.#leaseManager = this.#adapter.createLeaseManager({
62
- workerId: this.#id,
63
- leaseTimeout: parse(this.#config.worker?.leaseTimeout || "5m"),
64
- renewalInterval: parse(this.#config.worker?.renewalInterval || "5m")
65
- });
62
+ this.#adapter.setWorkerId(this.#id);
66
63
  this.#initialized = true;
67
64
  debug("worker %s initialized", this.#id);
68
65
  }
@@ -93,8 +90,9 @@ var Worker = class {
93
90
  async stop() {
94
91
  debug("stopping worker %s", this.#id);
95
92
  this.#running = false;
96
- if (this.#leaseManager) {
97
- await this.#leaseManager.destroy();
93
+ if (this.#pool) {
94
+ debug("worker %s: waiting for %d running jobs to complete", this.#id, this.#pool.size);
95
+ await this.#pool.drain();
98
96
  }
99
97
  if (this.#adapter) {
100
98
  await this.#adapter.destroy();
@@ -114,56 +112,58 @@ var Worker = class {
114
112
  return result.value;
115
113
  }
116
114
  async *process(queues) {
117
- const concurrency = this.#config.worker?.concurrency || 1;
118
- runningLoop: while (this.#running) {
115
+ const pollingInterval = parse(this.#config.worker?.pollingInterval || "2s");
116
+ this.#pool = new JobPool();
117
+ while (this.#running) {
119
118
  try {
120
- for (const queue of queues) {
121
- const jobs = await this.#acquireJobs(queue, concurrency);
122
- if (jobs.length > 0) {
123
- for (const job of jobs) {
124
- yield { type: "started", queue, job };
125
- }
126
- const results = await Promise.allSettled(jobs.map((job) => this.#execute(job, queue)));
127
- for (const job of jobs) {
128
- yield { type: "completed", queue, job };
129
- }
130
- const hasError = results.some((r) => r.status === "rejected");
131
- if (hasError) {
132
- const error = results.find((r) => r.status === "rejected");
133
- yield { type: "error", error: error.reason, suggestedDelay: parse("5s") };
134
- }
135
- continue runningLoop;
136
- }
119
+ yield* this.#fillPool(queues);
120
+ if (this.#pool.isEmpty()) {
121
+ yield { type: "idle", suggestedDelay: pollingInterval };
122
+ continue;
137
123
  }
138
- const pollingInterval = parse(this.#config.worker?.pollingInterval || "2s");
139
- yield { type: "idle", suggestedDelay: pollingInterval };
124
+ const completed = await this.#pool.waitForNextCompletion();
125
+ yield { type: "completed", queue: completed.queue, job: completed.job };
140
126
  } catch (error) {
141
127
  yield { type: "error", error, suggestedDelay: parse("5s") };
142
128
  }
143
129
  }
144
130
  }
131
+ async *#fillPool(queues) {
132
+ const concurrency = this.#config.worker?.concurrency || 1;
133
+ const slotsAvailable = concurrency - this.#pool.size;
134
+ if (slotsAvailable <= 0) return;
135
+ const popPromises = Array.from({ length: slotsAvailable }, () => this.#acquireNextJob(queues));
136
+ const results = await Promise.all(popPromises);
137
+ for (const result of results) {
138
+ if (!result) continue;
139
+ const { job, queue } = result;
140
+ const promise = this.#execute(job, queue);
141
+ this.#pool.add(job, queue, promise);
142
+ yield { type: "started", queue, job };
143
+ }
144
+ }
145
145
  async #execute(job, queue) {
146
146
  const startTime = performance.now();
147
147
  debug("worker %s: executing job %s (%s)", this.#id, job.id, job.name);
148
- let JobClass;
149
- try {
150
- JobClass = Locator.getOrThrow(job.name);
151
- } catch (error) {
152
- debug("worker %s: job class %s not found for job %s", this.#id, job.name, job.id);
153
- throw error;
154
- }
155
- const instance = new JobClass(job.payload);
156
- const options = JobClass.options || {};
148
+ const { instance, options, timeout } = await this.#initJob(job, queue);
157
149
  try {
158
- await instance.execute();
159
- await job._lease.commit();
150
+ await this.#executeWithTimeout(instance, timeout);
151
+ await this.#adapter.completeJob(job.id, queue);
160
152
  const duration = (performance.now() - startTime).toFixed(2);
161
153
  debug("worker %s: successfully executed job %s in %dms", this.#id, job.id, duration);
162
154
  } catch (e) {
155
+ const isTimeout = e instanceof errors.E_JOB_TIMEOUT;
156
+ if (isTimeout && options.failOnTimeout) {
157
+ debug("worker %s: job %s timed out and failOnTimeout is set", this.#id, job.id);
158
+ await this.#adapter.failJob(job.id, queue, e);
159
+ await instance.failed?.(e);
160
+ return;
161
+ }
163
162
  const mergedConfig = QueueManager.getMergedRetryConfig(queue, options.retry);
164
163
  if (typeof mergedConfig.maxRetries === "undefined" || mergedConfig.maxRetries <= 0) {
165
164
  debug("worker %s: job %s has no retries configured, marking as failed", this.#id, job.id);
166
- await instance.failed(e);
165
+ await this.#adapter.failJob(job.id, queue, e);
166
+ await instance.failed?.(e);
167
167
  return;
168
168
  }
169
169
  if (job.attempts >= mergedConfig.maxRetries) {
@@ -173,76 +173,65 @@ var Worker = class {
173
173
  job.id,
174
174
  mergedConfig.maxRetries
175
175
  );
176
+ await this.#adapter.failJob(job.id, queue, e);
176
177
  const exception = new errors.E_JOB_MAX_ATTEMPTS_REACHED([job.name]);
177
- await instance.failed(exception);
178
+ await instance.failed?.(exception);
178
179
  return;
179
180
  }
180
181
  if (mergedConfig.backoff) {
181
182
  const strategy = mergedConfig.backoff();
182
183
  const nextRetryAt = strategy.getNextRetryAt(job.attempts + 1);
183
184
  debug("worker %s: job %s will retry at %s", this.#id, job.id, nextRetryAt.toISOString());
184
- await this.#rollbackJobWithBackoff(job, queue, nextRetryAt);
185
+ await this.#adapter.retryJob(job.id, queue, nextRetryAt);
185
186
  return;
186
187
  }
187
- await this.#rollbackJob(job, queue);
188
+ await this.#adapter.retryJob(job.id, queue);
188
189
  }
189
190
  }
190
- async #acquireJobs(queue, count) {
191
- const jobs = [];
192
- for (let i = 0; i < count; i++) {
193
- const job = await this.#adapter.popFrom(queue);
194
- if (!job) {
195
- break;
196
- }
197
- debug("worker %s: attempting to acquire lease for job %s", this.#id, job.id);
198
- try {
199
- const acquired = await this.#leaseManager.acquire(job.id);
200
- if (!acquired) {
201
- debug("worker %s: failed to acquire lease for job %s", this.#id, job.id);
202
- await this.#adapter.pushOn(queue, job);
203
- continue;
204
- }
205
- debug("worker %s: acquired lease for job %s", this.#id, job.id);
206
- jobs.push({
207
- ...job,
208
- _lease: {
209
- commit: () => this.#commitJob(job.id),
210
- rollback: () => this.#rollbackJob(job, queue)
211
- }
212
- });
213
- } catch (error) {
214
- console.log(error);
215
- throw error;
216
- }
191
+ async #initJob(job, queue) {
192
+ try {
193
+ const JobClass = Locator.getOrThrow(job.name);
194
+ const instance = new JobClass(job.payload);
195
+ const options = JobClass.options || {};
196
+ const timeout = this.#getJobTimeout(options);
197
+ return { instance, options, timeout };
198
+ } catch (error) {
199
+ debug("worker %s: failed to initialize job %s (%s)", this.#id, job.id, job.name);
200
+ await this.#adapter.failJob(job.id, queue, error);
201
+ throw error;
217
202
  }
218
- return jobs;
219
203
  }
220
- #commitJob(jobId) {
221
- debug("worker %s: committing job %s", this.#id, jobId);
222
- return this.#leaseManager.release(jobId);
204
+ #getJobTimeout(options) {
205
+ if (options.timeout !== void 0) {
206
+ return parse(options.timeout);
207
+ }
208
+ if (this.#config.worker?.timeout !== void 0) {
209
+ return parse(this.#config.worker.timeout);
210
+ }
211
+ return void 0;
223
212
  }
224
- async #rollbackJob(job, queue) {
225
- debug("worker %s: rolling back job %s", this.#id, job.id);
226
- const updatedJob = {
227
- ...job,
228
- attempts: (job.attempts || 0) + 1
229
- };
230
- await Promise.all([this.#leaseManager.release(job.id), this.#adapter.pushOn(queue, updatedJob)]);
213
+ async #executeWithTimeout(instance, timeout) {
214
+ if (!timeout) {
215
+ return instance.execute();
216
+ }
217
+ const signal = AbortSignal.timeout(timeout);
218
+ const abortPromise = new Promise((_, reject) => {
219
+ signal.addEventListener("abort", () => {
220
+ reject(new errors.E_JOB_TIMEOUT([instance.constructor.name, timeout]));
221
+ });
222
+ });
223
+ await Promise.race([instance.execute(signal), abortPromise]);
231
224
  }
232
- async #rollbackJobWithBackoff(job, queue, nextRetryAt) {
233
- debug("worker %s: rolling back job %s with backoff", this.#id, job.id);
234
- const updatedJob = {
235
- ...job,
236
- attempts: (job.attempts || 0) + 1,
237
- nextRetryAt
238
- };
239
- await this.#leaseManager.release(job.id);
240
- const delay = nextRetryAt.getTime() - Date.now();
241
- if (delay > 0) {
242
- await this.#adapter.pushLaterOn(queue, updatedJob, delay);
243
- } else {
244
- await this.#adapter.pushOn(queue, updatedJob);
225
+ async #acquireNextJob(queues) {
226
+ for (const queue of queues) {
227
+ const job = await this.#adapter.popFrom(queue);
228
+ if (!job) {
229
+ continue;
230
+ }
231
+ debug("worker %s: acquired job %s", this.#id, job.id);
232
+ return { job, queue };
245
233
  }
234
+ return null;
246
235
  }
247
236
  async #setupGracefulShutdown() {
248
237
  const shutdown = async () => {
@@ -262,11 +251,13 @@ import { Locator as Locator2 } from "#src/locator";
262
251
  var QueueManagerSingleton = class {
263
252
  #defaultAdapter;
264
253
  #adapters = {};
254
+ #adapterInstances = /* @__PURE__ */ new Map();
265
255
  #globalRetryConfig;
266
256
  #queueConfigs = /* @__PURE__ */ new Map();
267
257
  async init(config) {
268
258
  debug2("initializing queue manager with config: %O", config);
269
259
  this.#validateConfig(config);
260
+ this.#adapterInstances.clear();
270
261
  this.#defaultAdapter = config.default;
271
262
  this.#adapters = config.adapters;
272
263
  this.#globalRetryConfig = config.retry;
@@ -282,13 +273,19 @@ var QueueManagerSingleton = class {
282
273
  if (!adapter) {
283
274
  adapter = this.#defaultAdapter;
284
275
  }
285
- const adapterInstance = this.#adapters[adapter];
286
- if (!adapterInstance) {
276
+ const cached = this.#adapterInstances.get(adapter);
277
+ if (cached) {
278
+ return cached;
279
+ }
280
+ const adapterFactory = this.#adapters[adapter];
281
+ if (!adapterFactory) {
287
282
  throw new errors2.E_CONFIGURATION_ERROR([`Adapter "${adapter}" is not registered`]);
288
283
  }
289
284
  debug2('using adapter "%s"', adapter);
290
285
  try {
291
- return adapterInstance();
286
+ const instance = adapterFactory();
287
+ this.#adapterInstances.set(adapter, instance);
288
+ return instance;
292
289
  } catch (error) {
293
290
  throw new Error();
294
291
  }
@@ -325,10 +322,11 @@ var QueueManagerSingleton = class {
325
322
  }
326
323
  }
327
324
  async destroy() {
328
- for (const adapterName in this.#adapters) {
329
- const adapter = this.#adapters[adapterName]();
325
+ for (const [name, adapter] of this.#adapterInstances) {
326
+ debug2('destroying adapter "%s"', name);
330
327
  await adapter.destroy();
331
328
  }
329
+ this.#adapterInstances.clear();
332
330
  }
333
331
  };
334
332
  var QueueManager2 = new QueueManagerSingleton();