@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 +18 -0
- package/api-surface.md +421 -0
- package/package.json +4 -3
- package/src/commands/QueueWorkCommand.ts +51 -9
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.
|
|
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.
|
|
34
|
-
"@zerotal/orm": "1.
|
|
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: "
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
}
|