@basaltkit/queue 1.1.0 → 1.2.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.
package/README.md CHANGED
@@ -184,6 +184,20 @@ queuePlugin({
184
184
 
185
185
  If a job reaches a worker that hasn't registered it, `UnknownJobError` is thrown.
186
186
 
187
+ ### CLI commands (`basalt queue:*`)
188
+
189
+ Registering `queuePlugin` also wires three CLI commands (run via the `@basaltkit/cli` runner):
190
+
191
+ ```bash
192
+ basalt queue:work --queue=default --concurrency=5 # run a worker until Ctrl+C
193
+ basalt queue:stats --queue=billing # waiting/active/completed/failed/delayed
194
+ basalt queue:retry --queue=billing --limit=100 # re-enqueue failed jobs
195
+ ```
196
+
197
+ `queue:stats` and `queue:retry` need a driver that can introspect job state — the
198
+ **BullMQ** driver (a Redis `connection`). With the inline `sync` driver they
199
+ report the operation as unsupported (it keeps no job state), rather than guessing.
200
+
187
201
  ### Manual use without a plugin (e.g. in tests)
188
202
 
189
203
  ```ts
package/dist/index.d.ts CHANGED
@@ -39,6 +39,14 @@ interface DriverCapabilities {
39
39
  /** Waits `backoff` between retries (vs retrying immediately). */
40
40
  backoff: boolean;
41
41
  }
42
+ /** Job counts per state, for `basalt queue:stats`. */
43
+ interface QueueStats {
44
+ waiting: number;
45
+ active: number;
46
+ completed: number;
47
+ failed: number;
48
+ delayed: number;
49
+ }
42
50
  /** Queue driver contract. BullMQ in production; sync in tests/dev. */
43
51
  interface QueueDriver {
44
52
  /** Short identifier used in diagnostics (e.g. 'bullmq', 'sync'). */
@@ -52,6 +60,19 @@ interface QueueDriver {
52
60
  startWorker(queue: string, options?: {
53
61
  concurrency?: number;
54
62
  }): void;
63
+ /**
64
+ * Optional: job counts per state, for `basalt queue:stats`. Backends that
65
+ * cannot introspect (e.g. the inline sync driver) omit it — the CLI then
66
+ * reports the operation as unsupported rather than guessing.
67
+ */
68
+ stats?(queue: string): Promise<QueueStats>;
69
+ /**
70
+ * Optional: re-enqueue failed jobs (`basalt queue:retry`). Returns how many
71
+ * were retried. `limit` caps how many are processed (default driver's choice).
72
+ */
73
+ retryFailed?(queue: string, options?: {
74
+ limit?: number;
75
+ }): Promise<number>;
55
76
  close(): Promise<void>;
56
77
  }
57
78
 
@@ -77,6 +98,10 @@ declare class BullmqQueueDriver implements QueueDriver {
77
98
  startWorker(queue: string, options?: {
78
99
  concurrency?: number;
79
100
  }): void;
101
+ stats(queue: string): Promise<QueueStats>;
102
+ retryFailed(queue: string, options?: {
103
+ limit?: number;
104
+ }): Promise<number>;
80
105
  close(): Promise<void>;
81
106
  private queue;
82
107
  }
@@ -201,6 +226,15 @@ declare class QueueManager implements JobDispatcher {
201
226
  work(queue?: string, options?: {
202
227
  concurrency?: number;
203
228
  }): void;
229
+ /** Job counts per state, or `undefined` if the driver can't introspect. */
230
+ stats(queue?: string): Promise<QueueStats | undefined>;
231
+ /**
232
+ * Re-enqueues failed jobs; returns the count, or `undefined` if the driver
233
+ * doesn't support retrying (e.g. the inline sync driver).
234
+ */
235
+ retryFailed(queue?: string, options?: {
236
+ limit?: number;
237
+ }): Promise<number | undefined>;
204
238
  close(): Promise<void>;
205
239
  /** Executes a job received from the driver: validates, restores the context, runs the handler. */
206
240
  private execute;
@@ -281,4 +315,4 @@ interface QueuePluginOptions {
281
315
  }
282
316
  declare function queuePlugin(options?: QueuePluginOptions): _basaltkit_core.BasaltPlugin<unknown>;
283
317
 
284
- export { type AddJobOptions, type BullmqDriverOptions, BullmqQueueDriver, type DispatchOptions, type DriverCapabilities, type JobBackoff, type JobDefinition, type JobExecutor, JobNotRegisteredError, type JobRetention, type JobSchema, JobValidationError, QUEUE, type QueueDriver, QueueManager, type QueueManagerOptions, type QueuePluginOptions, type QueuedListenerOptions, SyncQueueDriver, UnknownJobError, UnsupportedJobOptionError, type UnsupportedPolicy, defineJob, queuePlugin, queuedOn };
318
+ export { type AddJobOptions, type BullmqDriverOptions, BullmqQueueDriver, type DispatchOptions, type DriverCapabilities, type JobBackoff, type JobDefinition, type JobExecutor, JobNotRegisteredError, type JobRetention, type JobSchema, JobValidationError, QUEUE, type QueueDriver, QueueManager, type QueueManagerOptions, type QueuePluginOptions, type QueueStats, type QueuedListenerOptions, SyncQueueDriver, UnknownJobError, UnsupportedJobOptionError, type UnsupportedPolicy, defineJob, queuePlugin, queuedOn };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import { createToken, definePlugin } from "@basaltkit/core";
2
+ import { createToken, definePlugin, ensureMetadata } from "@basaltkit/core";
3
3
 
4
4
  // src/drivers/bullmq.ts
5
5
  import { Queue, Worker } from "bullmq";
@@ -42,6 +42,26 @@ var BullmqQueueDriver = class {
42
42
  })
43
43
  );
44
44
  }
45
+ async stats(queue) {
46
+ const c = await this.queue(queue).getJobCounts("waiting", "active", "completed", "failed", "delayed");
47
+ return {
48
+ waiting: c["waiting"] ?? 0,
49
+ active: c["active"] ?? 0,
50
+ completed: c["completed"] ?? 0,
51
+ failed: c["failed"] ?? 0,
52
+ delayed: c["delayed"] ?? 0
53
+ };
54
+ }
55
+ async retryFailed(queue, options = {}) {
56
+ const limit = options.limit ?? 1e3;
57
+ const failed = await this.queue(queue).getFailed(0, limit - 1);
58
+ let retried = 0;
59
+ for (const job of failed) {
60
+ await job.retry();
61
+ retried++;
62
+ }
63
+ return retried;
64
+ }
45
65
  async close() {
46
66
  await Promise.all(this.workers.map((worker) => worker.close()));
47
67
  await Promise.all([...this.queues.values()].map((queue) => queue.close()));
@@ -262,6 +282,17 @@ var QueueManager = class {
262
282
  work(queue = "default", options = {}) {
263
283
  this.driver.startWorker(queue, options);
264
284
  }
285
+ /** Job counts per state, or `undefined` if the driver can't introspect. */
286
+ async stats(queue = "default") {
287
+ return this.driver.stats?.(queue);
288
+ }
289
+ /**
290
+ * Re-enqueues failed jobs; returns the count, or `undefined` if the driver
291
+ * doesn't support retrying (e.g. the inline sync driver).
292
+ */
293
+ async retryFailed(queue = "default", options = {}) {
294
+ return this.driver.retryFailed?.(queue, options);
295
+ }
265
296
  async close() {
266
297
  await this.driver.close();
267
298
  }
@@ -310,6 +341,7 @@ function queuePlugin(options = {}) {
310
341
  return definePlugin({
311
342
  name: "basalt:queue",
312
343
  register({ container }) {
344
+ registerQueueCommands(container);
313
345
  container.singleton(QUEUE, () => {
314
346
  const driver = options.driver ?? (options.connection ? new BullmqQueueDriver({ connection: options.connection }) : new SyncQueueDriver());
315
347
  const manager = new QueueManager(driver, {
@@ -335,6 +367,54 @@ function queuePlugin(options = {}) {
335
367
  }
336
368
  });
337
369
  }
370
+ function registerQueueCommands(container) {
371
+ const manager = () => container.get(QUEUE);
372
+ const unsupported = "Not supported by the active queue driver \u2014 the inline sync driver keeps no job state. Use the BullMQ driver (a Redis `connection`).";
373
+ ensureMetadata(container).add("commands", {
374
+ name: "queue:work",
375
+ description: "Run a worker that processes jobs for a queue (Ctrl+C to stop)",
376
+ async handle({ io, flags }) {
377
+ const queue = typeof flags["queue"] === "string" ? flags["queue"] : "default";
378
+ const concurrency = typeof flags["concurrency"] === "string" ? Number(flags["concurrency"]) : void 0;
379
+ manager().work(queue, concurrency !== void 0 ? { concurrency } : {});
380
+ io.log(`Worker started on queue "${queue}"${concurrency ? ` (concurrency ${concurrency})` : ""}. Ctrl+C to stop.`);
381
+ await new Promise((resolve) => process.once("SIGINT", resolve));
382
+ }
383
+ });
384
+ ensureMetadata(container).add("commands", {
385
+ name: "queue:stats",
386
+ description: "Show job counts (waiting/active/completed/failed/delayed) for a queue",
387
+ async handle({
388
+ io,
389
+ flags
390
+ }) {
391
+ const queue = typeof flags["queue"] === "string" ? flags["queue"] : "default";
392
+ const stats = await manager().stats(queue);
393
+ if (!stats) {
394
+ io.log(unsupported);
395
+ return;
396
+ }
397
+ io.table([{ queue, ...stats }]);
398
+ }
399
+ });
400
+ ensureMetadata(container).add("commands", {
401
+ name: "queue:retry",
402
+ description: "Re-enqueue failed jobs on a queue",
403
+ async handle({
404
+ io,
405
+ flags
406
+ }) {
407
+ const queue = typeof flags["queue"] === "string" ? flags["queue"] : "default";
408
+ const limit = typeof flags["limit"] === "string" ? Number(flags["limit"]) : void 0;
409
+ const retried = await manager().retryFailed(queue, limit !== void 0 ? { limit } : {});
410
+ if (retried === void 0) {
411
+ io.log(unsupported);
412
+ return;
413
+ }
414
+ io.log(`Re-enqueued ${retried} failed job(s) on "${queue}".`);
415
+ }
416
+ });
417
+ }
338
418
  export {
339
419
  BullmqQueueDriver,
340
420
  JobNotRegisteredError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basaltkit/queue",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Basalt queues on top of BullMQ: declarative jobs with Zod payloads, context propagation (tenant/requestId) to workers and a sync driver for tests.",
5
5
  "license": "MIT",
6
6
  "type": "module",