@zerotal/queue 1.6.3 → 1.7.2

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/CHANGELOG.md CHANGED
@@ -8,6 +8,24 @@ follows the Zerotal monorepo's unified versioning.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ### Fixed
12
+
13
+ - **`queue:work` listened on one hardcoded queue, so queued notifications were never sent.**
14
+ The `--queue` flag defaulted to the literal string `"default"` and the command never read
15
+ `queue.queues` — the key `QueueProvider`'s in-process pool has always used. A job may pin its
16
+ own queue, and `SendNotificationJob` sets `"notifications"`, so following both documented
17
+ steps — `Notify.queue(user, notification)`, then `bun zt queue:work` — put the mail on a queue
18
+ nobody was reading. Nothing errored. The job sat as pending forever and the worker reported
19
+ "Queue is empty."
20
+
21
+ With no `--queue`, the worker now drains every queue in `queue.queues`, falling back to
22
+ `["default"]`. The flag still overrides it and now accepts a comma-separated list
23
+ (`--queue=emails,reports`) in priority order, so one worker can take a subset without a second
24
+ process. `--once` stops at the first queue with a claimable job rather than taking one job per
25
+ queue, and the idle sleep is only taken when *no* queue had anything — sleeping after the first
26
+ empty one would leave a busy second queue waiting out the poll interval.
27
+
28
+
11
29
  ## [1.5.0] — 2026-08-15
12
30
 
13
31
  ### Added
package/api-surface.md ADDED
@@ -0,0 +1,421 @@
1
+ # @zerotal/queue — public API surface
2
+
3
+ <!-- AUTO-GENERATED by scripts/api-surface.ts. Do not edit by hand.
4
+ Run `bun run api:surface` to regenerate after an intentional API change. -->
5
+
6
+ ## . `(./src/index.ts)`
7
+
8
+ class Batch = {
9
+ new (_r: BatchRecord): Batch
10
+ createdAt: string
11
+ failed: () => boolean
12
+ failedJobIds: number[]
13
+ failedJobs: number
14
+ finished: () => boolean
15
+ finishedAt: string | undefined
16
+ id: string
17
+ name: string
18
+ options: BatchOptions
19
+ pendingJobs: number
20
+ progress: () => number
21
+ totalJobs: number
22
+ }
23
+
24
+ class Bus = {
25
+ new (): Bus
26
+ static batch: (jobs: Job[]) => PendingBatch
27
+ static chain: (jobs: Job[]) => { dispatch(): Promise<void>;}
28
+ static reset: () => void
29
+ static setManager: (manager: QueueManager) => void
30
+ }
31
+
32
+ class Job = {
33
+ new (): Job
34
+ _chain: SerializedJob[] | undefined
35
+ batchId: string | undefined
36
+ className: string
37
+ debounceKey: () => string
38
+ handle: () => Promise<void>
39
+ payload: () => Record<string, unknown>
40
+ readonly debounce?: number
41
+ readonly maxAttempts: number
42
+ readonly queue: string
43
+ readonly retryDelay: number
44
+ }
45
+
46
+ class JobRan = {
47
+ new (className: string, queue: string, status: 'dispatched' | 'completed' | 'failed' | 'retried', durationMs: number, error?: string | undefined): JobRan
48
+ readonly className: string
49
+ readonly durationMs: number
50
+ readonly error?: string | undefined
51
+ readonly queue: string
52
+ readonly status: 'dispatched' | 'completed' | 'failed' | 'retried'
53
+ }
54
+
55
+ class PendingBatch = {
56
+ new (_jobs: Job[], _dispatcher: BatchDispatcher): PendingBatch
57
+ batchName: string
58
+ catch: (job: Job | Job[]) => PendingBatch
59
+ dispatch: () => Promise<Batch>
60
+ finally: (job: Job | Job[]) => PendingBatch
61
+ jobs: Job[]
62
+ name: (n: string) => PendingBatch
63
+ options: BatchOptions
64
+ then: (job: Job | Job[]) => PendingBatch
65
+ }
66
+
67
+ class QueueBatchingUnsupportedError = {
68
+ new (): QueueBatchingUnsupportedError
69
+ readonly code: string
70
+ readonly context?: Record<string, unknown> | undefined
71
+ readonly status: number
72
+ }
73
+
74
+ class QueueDebounceUnsupportedError = {
75
+ new (className: string, driver: string): QueueDebounceUnsupportedError
76
+ readonly code: string
77
+ readonly context?: Record<string, unknown> | undefined
78
+ readonly status: number
79
+ }
80
+
81
+ class QueueError = {
82
+ new (message: string, code?: string, status?: number, context?: Record<string, unknown>): QueueError
83
+ readonly code: string
84
+ readonly context?: Record<string, unknown> | undefined
85
+ readonly status: number
86
+ }
87
+
88
+ class QueueFake = {
89
+ new (app: Application, original: Binding): QueueFake
90
+ static install: () => QueueFake
91
+ assertDispatched: <T extends Job>(JobClass: new (...args: never[]) => T, callback?: (job: T) => boolean) => void
92
+ assertDispatchedCount: (count: number) => void
93
+ assertNotDispatched: <T extends Job>(JobClass: new (...args: never[]) => T) => void
94
+ assertNothingDispatched: () => void
95
+ dispatch: (job: Job) => Promise<void>
96
+ dispatched: () => Job[]
97
+ drain: () => Promise<void>
98
+ flush: () => Promise<void>
99
+ isShuttingDown: boolean
100
+ processNext: () => Promise<boolean>
101
+ restore: () => void
102
+ size: () => Promise<number>
103
+ }
104
+
105
+ class QueueManager = {
106
+ new (driver: QueueDriver): QueueManager
107
+ activeJobCount: number
108
+ clearFailed: (queue?: string) => Promise<void>
109
+ dispatch: (job: Job) => Promise<void>
110
+ dispatchBatch: (pending: PendingBatch) => Promise<Batch>
111
+ drain: () => Promise<void>
112
+ failed: (queue?: string) => Promise<FailedRecord[]>
113
+ flush: () => Promise<void>
114
+ forgetFailed: (id: number) => Promise<void>
115
+ isShuttingDown: boolean
116
+ pending: (queue?: string, limit?: number, offset?: number) => Promise<JobRecord[]>
117
+ processNext: (queue?: string) => Promise<boolean>
118
+ queues: () => Promise<{ queue: string; pending: number;}[]>
119
+ retryFailed: (id: number) => Promise<boolean>
120
+ setWorkerPool: (pool: JobExecutor) => void
121
+ size: (queue?: string) => Promise<number>
122
+ stats: () => QueueStats
123
+ }
124
+
125
+ class QueueNotInitializedError = {
126
+ new (): QueueNotInitializedError
127
+ readonly code: string
128
+ readonly context?: Record<string, unknown> | undefined
129
+ readonly status: number
130
+ }
131
+
132
+ class QueueProvider = {
133
+ new (app: Application): QueueProvider
134
+ static dependsOn?: (new (app: Application) => ServiceProvider)[]
135
+ static environments: AppEnvironment[]
136
+ static priority?: number
137
+ static provides: readonly ['queue']
138
+ devProcesses: () => DevProcessDefinition[]
139
+ doctorChecks: () => DoctorCheck[]
140
+ onBooted: () => Promise<void>
141
+ onBooting: () => Promise<void>
142
+ onRegister: () => void
143
+ onRequestProcessed: (_ctx: HttpContext) => Promise<void>
144
+ onRequestReceived: (_ctx: HttpContext) => Promise<void>
145
+ onResponseSent: (_ctx: HttpContext) => Promise<void>
146
+ onStarted: () => Promise<void>
147
+ onStarting: () => Promise<void>
148
+ onStopped: () => Promise<void>
149
+ onStopping: () => Promise<void>
150
+ replContext: () => Record<string, unknown>
151
+ }
152
+
153
+ class QueueShuttingDownError = {
154
+ new (className: string): QueueShuttingDownError
155
+ readonly code: string
156
+ readonly context?: Record<string, unknown> | undefined
157
+ readonly status: number
158
+ }
159
+
160
+ class RedisDriver = {
161
+ new (prefix?: string, options?: { visibilityTimeout?: number; client?: RedisClientLike; }): RedisDriver
162
+ clearFailed: (queue?: string) => Promise<void>
163
+ delete: (record: JobRecord) => Promise<void>
164
+ deleteFailedRecord: (id: number, queue?: string) => Promise<void>
165
+ fail: (record: JobRecord, error: string) => Promise<void>
166
+ flush: () => Promise<void>
167
+ listFailed: (queue?: string) => Promise<FailedRecord[]>
168
+ pop: (queue?: string) => Promise<JobRecord | null>
169
+ push: (record: Omit<JobRecord, 'id' | 'createdAt'>) => Promise<void>
170
+ pushDebounced: (record: Omit<JobRecord, 'id' | 'createdAt'>, key: string) => Promise<void>
171
+ retry: (record: JobRecord) => Promise<void>
172
+ size: (queue?: string) => Promise<number>
173
+ }
174
+
175
+ class SqliteDriver = {
176
+ new (sql: SQLInstance, options?: { visibilityTimeout?: number; }): SqliteDriver
177
+ clearFailed: (queue?: string) => Promise<void>
178
+ createBatch: (id: string, name: string, totalJobs: number, optionsJson: string) => Promise<void>
179
+ delete: (record: JobRecord) => Promise<void>
180
+ deleteFailedRecord: (id: number) => Promise<void>
181
+ fail: (record: JobRecord, error: string) => Promise<void>
182
+ flush: () => Promise<void>
183
+ getBatch: (batchId: string) => Promise<BatchRecord | null>
184
+ listFailed: (queue?: string) => Promise<FailedRecord[]>
185
+ listPending: (queue?: string, limit?: number, offset?: number) => Promise<JobRecord[]>
186
+ pop: (queue?: string) => Promise<JobRecord | null>
187
+ push: (record: Omit<JobRecord, 'id' | 'createdAt'>) => Promise<void>
188
+ pushDebounced: (record: Omit<JobRecord, 'id' | 'createdAt'>, key: string) => Promise<void>
189
+ queues: () => Promise<{ queue: string; pending: number;}[]>
190
+ recordBatchJobComplete: (batchId: string) => Promise<BatchStatus>
191
+ recordBatchJobFailed: (batchId: string, jobId: number) => Promise<BatchStatus>
192
+ retry: (record: JobRecord) => Promise<void>
193
+ size: (queue?: string) => Promise<number>
194
+ }
195
+
196
+ class SyncDriver = {
197
+ new (): SyncDriver
198
+ static _pendingJob: Job | undefined
199
+ clearFailed: (queue?: string) => Promise<void>
200
+ delete: (_record: JobRecord) => Promise<void>
201
+ deleteFailedRecord: (id: number) => Promise<void>
202
+ fail: (record: JobRecord, error: string) => Promise<void>
203
+ failedJobs: FailedEntry[]
204
+ flush: () => Promise<void>
205
+ listFailed: (queue?: string) => Promise<FailedRecord[]>
206
+ pop: () => Promise<JobRecord | null>
207
+ push: (record: Omit<JobRecord, 'id' | 'createdAt'>) => Promise<void>
208
+ retry: (record: JobRecord) => Promise<void>
209
+ size: () => Promise<number>
210
+ }
211
+
212
+ class WorkerPool = {
213
+ new (opts: WorkerPoolOptions): WorkerPool
214
+ run: (record: JobRecord) => Promise<WorkerResult>
215
+ start: () => Promise<void>
216
+ terminate: () => Promise<void>
217
+ }
218
+
219
+ const JobRegistry = { _map: Map<string, JobClass>; register(JobClass: JobClass): void; resolve(className: string): JobClass | undefined; all(): ReadonlyMap<string, JobClass>;}
220
+
221
+ const Queue = QueueManager
222
+
223
+ function QueueConfig = (options?: Partial<QueueConfigShape>) => QueueConfigShape
224
+
225
+ function queueStats = () => QueueStats
226
+
227
+ interface BatchOptions = {
228
+ catchJobs?: SerializedJob[] | undefined
229
+ finallyJobs?: SerializedJob[] | undefined
230
+ thenJobs?: SerializedJob[] | undefined
231
+ }
232
+
233
+ interface BatchRecord = {
234
+ createdAt: string
235
+ failedJobIds: number[]
236
+ failedJobs: number
237
+ finishedAt: string | undefined
238
+ id: string
239
+ name: string
240
+ options: BatchOptions
241
+ pendingJobs: number
242
+ totalJobs: number
243
+ }
244
+
245
+ interface JobRecord = {
246
+ attempts: number
247
+ availableAt: number
248
+ batchId?: string | undefined
249
+ className: string
250
+ createdAt: string
251
+ debounceKey?: string | undefined
252
+ id: number
253
+ maxAttempts: number
254
+ payload: string
255
+ queue: string
256
+ retryDelay: number
257
+ }
258
+
259
+ interface QueueConfigShape = {
260
+ driver: 'sqlite' | 'redis' | 'sync'
261
+ pollInterval: number
262
+ queues: string[]
263
+ workerBootstrap?: string
264
+ workers: number
265
+ }
266
+
267
+ interface QueueDriver = {
268
+ clearFailed: (queue?: string) => Promise<void>
269
+ createBatch?: (id: string, name: string, totalJobs: number, optionsJson: string) => Promise<void>
270
+ delete: (record: JobRecord) => Promise<void>
271
+ deleteFailedRecord: (id: number) => Promise<void>
272
+ fail: (record: JobRecord, error: string) => Promise<void>
273
+ flush: () => Promise<void>
274
+ getBatch?: (batchId: string) => Promise<BatchRecord | null>
275
+ listFailed: (queue?: string) => Promise<FailedRecord[]>
276
+ listPending?: (queue?: string, limit?: number, offset?: number) => Promise<JobRecord[]>
277
+ pop: (queue?: string) => Promise<JobRecord | null>
278
+ push: (record: Omit<JobRecord, 'id' | 'createdAt'>) => Promise<void>
279
+ pushDebounced?: (record: Omit<JobRecord, 'id' | 'createdAt'>, key: string) => Promise<void>
280
+ queues?: () => Promise<{ queue: string; pending: number;}[]>
281
+ recordBatchJobComplete?: (batchId: string) => Promise<BatchStatus>
282
+ recordBatchJobFailed?: (batchId: string, jobId: number) => Promise<BatchStatus>
283
+ retry: (record: JobRecord) => Promise<void>
284
+ size: (queue?: string) => Promise<number>
285
+ }
286
+
287
+ interface QueueStats = {
288
+ failedLast5m: number
289
+ failedTotal: number
290
+ processedLast5m: number
291
+ processedTotal: number
292
+ }
293
+
294
+ interface WorkerPoolOptions = {
295
+ bootstrapPath?: string | undefined
296
+ maxPending?: number | undefined
297
+ maxRespawns?: number | undefined
298
+ size: number
299
+ }
300
+
301
+ type BatchStatus = 'failed' | 'pending' | 'complete'
302
+
303
+ type JobStatus = 'dispatched' | 'completed' | 'failed' | 'retried'
304
+
305
+ type SerializedJob = { className: string; payload: Record<string, unknown>;}
306
+
307
+ type WorkerResult = { success: true;} | { success: false; error: string;}
308
+
309
+ ## ./commands `(./src/commands/index.ts)`
310
+
311
+ class QueueFailedCommand = {
312
+ new (): QueueFailedCommand
313
+ static args: ArgDef[]
314
+ static commandName: string
315
+ static description: string
316
+ static flags: { name: string; short: string; type: 'string'; description: string; default: string;}[]
317
+ static needsApp: boolean
318
+ _readLine: () => Promise<string>
319
+ _writer: OutputWriter
320
+ app: unknown
321
+ args: Record<string, string>
322
+ ask: (question: string, defaultValue?: string) => Promise<string>
323
+ choice: (question: string, options: string[]) => Promise<string>
324
+ confirm: (question: string, defaultValue?: boolean) => Promise<boolean>
325
+ dim: (msg: string) => void
326
+ error: (msg: string) => void
327
+ flags: Record<string, string | number | boolean>
328
+ info: (msg: string) => void
329
+ line: (msg: string) => void
330
+ newLine: () => void
331
+ run: () => Promise<void>
332
+ secret: (question: string) => Promise<string>
333
+ section: (title: string) => void
334
+ table: (rows: [string, string][], indent?: number) => void
335
+ warn: (msg: string) => void
336
+ write: (msg: string) => void
337
+ }
338
+
339
+ class QueueFlushCommand = {
340
+ new (): QueueFlushCommand
341
+ static args: ArgDef[]
342
+ static commandName: string
343
+ static description: string
344
+ static flags: ({ name: string; short: string; type: 'string'; description: string; default: string;} | { name: string; type: 'boolean'; description: string; default: boolean; short?: never;})[]
345
+ static needsApp: boolean
346
+ _readLine: () => Promise<string>
347
+ _writer: OutputWriter
348
+ app: unknown
349
+ args: Record<string, string>
350
+ ask: (question: string, defaultValue?: string) => Promise<string>
351
+ choice: (question: string, options: string[]) => Promise<string>
352
+ confirm: (question: string, defaultValue?: boolean) => Promise<boolean>
353
+ dim: (msg: string) => void
354
+ error: (msg: string) => void
355
+ flags: Record<string, string | number | boolean>
356
+ info: (msg: string) => void
357
+ line: (msg: string) => void
358
+ newLine: () => void
359
+ run: () => Promise<void>
360
+ secret: (question: string) => Promise<string>
361
+ section: (title: string) => void
362
+ table: (rows: [string, string][], indent?: number) => void
363
+ warn: (msg: string) => void
364
+ write: (msg: string) => void
365
+ }
366
+
367
+ class QueueRetryCommand = {
368
+ new (): QueueRetryCommand
369
+ static args: { name: string; required: boolean; description: string;}[]
370
+ static commandName: string
371
+ static description: string
372
+ static flags: FlagDef[]
373
+ static needsApp: boolean
374
+ _readLine: () => Promise<string>
375
+ _writer: OutputWriter
376
+ app: unknown
377
+ args: Record<string, string>
378
+ ask: (question: string, defaultValue?: string) => Promise<string>
379
+ choice: (question: string, options: string[]) => Promise<string>
380
+ confirm: (question: string, defaultValue?: boolean) => Promise<boolean>
381
+ dim: (msg: string) => void
382
+ error: (msg: string) => void
383
+ flags: Record<string, string | number | boolean>
384
+ info: (msg: string) => void
385
+ line: (msg: string) => void
386
+ newLine: () => void
387
+ run: () => Promise<void>
388
+ secret: (question: string) => Promise<string>
389
+ section: (title: string) => void
390
+ table: (rows: [string, string][], indent?: number) => void
391
+ warn: (msg: string) => void
392
+ write: (msg: string) => void
393
+ }
394
+
395
+ class QueueWorkCommand = {
396
+ new (): QueueWorkCommand
397
+ static args: ArgDef[]
398
+ static commandName: string
399
+ static description: string
400
+ static flags: ({ name: string; short: string; type: 'string'; description: string; default: string;} | { name: string; type: 'boolean'; description: string; default: boolean; short?: never;})[]
401
+ static needsApp: boolean
402
+ _readLine: () => Promise<string>
403
+ _writer: OutputWriter
404
+ app: unknown
405
+ args: Record<string, string>
406
+ ask: (question: string, defaultValue?: string) => Promise<string>
407
+ choice: (question: string, options: string[]) => Promise<string>
408
+ confirm: (question: string, defaultValue?: boolean) => Promise<boolean>
409
+ dim: (msg: string) => void
410
+ error: (msg: string) => void
411
+ flags: Record<string, string | number | boolean>
412
+ info: (msg: string) => void
413
+ line: (msg: string) => void
414
+ newLine: () => void
415
+ run: () => Promise<void>
416
+ secret: (question: string) => Promise<string>
417
+ section: (title: string) => void
418
+ table: (rows: [string, string][], indent?: number) => void
419
+ warn: (msg: string) => void
420
+ write: (msg: string) => void
421
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/queue",
3
- "version": "1.6.3",
3
+ "version": "1.7.2",
4
4
  "license": "MIT",
5
5
  "maturity": "stable",
6
6
  "private": false,
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "files": [
15
15
  "CHANGELOG.md",
16
+ "api-surface.md",
16
17
  "src",
17
18
  "!src/**/*.test.ts",
18
19
  "!src/**/*.test.tsx",
@@ -30,8 +31,8 @@
30
31
  "typecheck": "tsc --noEmit"
31
32
  },
32
33
  "dependencies": {
33
- "@zerotal/core": "1.6.3",
34
- "@zerotal/orm": "1.6.3"
34
+ "@zerotal/core": "1.7.2",
35
+ "@zerotal/orm": "1.7.2"
35
36
  },
36
37
  "devDependencies": {
37
38
  "typescript": "^5.8.0"
@@ -1,5 +1,6 @@
1
1
  import type { Application } from "@zerotal/core";
2
2
  import { Command } from "@zerotal/core";
3
+ import type { ConfigManager } from "@zerotal/core/config";
3
4
  import type { QueueManager } from "../QueueManager.ts";
4
5
 
5
6
  export class QueueWorkCommand extends Command {
@@ -12,8 +13,8 @@ export class QueueWorkCommand extends Command {
12
13
  name: "queue",
13
14
  short: "q",
14
15
  type: "string" as const,
15
- description: "Queue name to process",
16
- default: "default",
16
+ description: "Queue name(s) to process, comma-separated. Defaults to config queue.queues",
17
+ default: "",
17
18
  },
18
19
  {
19
20
  name: "once",
@@ -23,8 +24,35 @@ export class QueueWorkCommand extends Command {
23
24
  },
24
25
  ];
25
26
 
27
+ /**
28
+ * The queues to drain: the flag if given, else the app's own `queue.queues`.
29
+ *
30
+ * Defaulting to the literal string `"default"` was a silent delivery failure.
31
+ * A job may pin its own queue — `SendNotificationJob` sets `"notifications"` —
32
+ * and `config/queue.ts` lists exactly that queue so the in-process pool drains
33
+ * it (`QueueProvider` reads this same key and loops over every entry). But this
34
+ * command ignored the config and listened on `"default"` alone, so following
35
+ * both documented steps — `Notify.queue(...)`, then `zt queue:work` — queued
36
+ * the mail and then never sent it. Nothing errors: the job sits in a queue
37
+ * nobody is reading, forever.
38
+ *
39
+ * Comma-separated, so one worker can take a subset without a second process.
40
+ * Order is priority order, matching the provider's loop.
41
+ */
42
+ private targetQueues(app: Application): string[] {
43
+ const flag = ((this.flags["queue"] as string) ?? "").trim();
44
+ if (flag) {
45
+ const named = flag
46
+ .split(",")
47
+ .map((q) => q.trim())
48
+ .filter(Boolean);
49
+ if (named.length) return named;
50
+ }
51
+ const config = app.container.makeSync("config") as ConfigManager;
52
+ return config.get<string[]>("queue.queues", ["default"]);
53
+ }
54
+
26
55
  async run(): Promise<void> {
27
- const queueName = this.flags["queue"] as string;
28
56
  const once = this.flags["once"] as boolean;
29
57
  const app = this.app as Application | undefined;
30
58
  if (!app) {
@@ -33,22 +61,36 @@ export class QueueWorkCommand extends Command {
33
61
  }
34
62
 
35
63
  const manager = app.container.makeSync("queue") as QueueManager;
64
+ const queues = this.targetQueues(app);
36
65
 
37
66
  if (once) {
38
- const processed = await manager.processNext(queueName);
39
- this.info(processed ? "Processed 1 job." : "Queue is empty.");
67
+ // The first queue with anything claimable wins, so `--once` drains one job
68
+ // rather than one job *per queue*.
69
+ for (const q of queues) {
70
+ if (await manager.processNext(q)) {
71
+ this.info("Processed 1 job.");
72
+ return;
73
+ }
74
+ }
75
+ this.info("Queue is empty.");
40
76
  return;
41
77
  }
42
78
 
43
- this.info(`Processing queue "${queueName}" — Ctrl+C to stop.`);
79
+ this.info(`Processing queue(s) [${queues.join(", ")}] — Ctrl+C to stop.`);
44
80
  while (!manager.isShuttingDown) {
45
81
  // Sleep whenever nothing was *claimable* this tick — not merely when the
46
82
  // queue is empty. size() counts not-yet-due delayed jobs and jobs reserved
47
83
  // by other workers, so keying the sleep off size() busy-spins at 100% CPU
48
84
  // whenever a delayed/reserved job exists but nothing is ready.
49
- const processed = await manager
50
- .processNext(queueName)
51
- .catch((err) => (console.error(err), false));
85
+ //
86
+ // "Nothing claimable" means across every queue: sleeping after the first
87
+ // empty one would leave a busy second queue waiting out the poll interval.
88
+ let processed = false;
89
+ for (const q of queues) {
90
+ if (manager.isShuttingDown) break;
91
+ const did = await manager.processNext(q).catch((err) => (console.error(err), false));
92
+ processed = processed || did;
93
+ }
52
94
  if (!processed) {
53
95
  await new Promise<void>((r) => setTimeout(r, 500));
54
96
  }