@zerotal/queue 1.7.0 → 1.7.3

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,23 @@ 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
+
11
28
  ## [1.5.0] — 2026-08-15
12
29
 
13
30
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/queue",
3
- "version": "1.7.0",
3
+ "version": "1.7.3",
4
4
  "license": "MIT",
5
5
  "maturity": "stable",
6
6
  "private": false,
@@ -31,8 +31,8 @@
31
31
  "typecheck": "tsc --noEmit"
32
32
  },
33
33
  "dependencies": {
34
- "@zerotal/core": "1.7.0",
35
- "@zerotal/orm": "1.7.0"
34
+ "@zerotal/core": "1.7.3",
35
+ "@zerotal/orm": "1.7.3"
36
36
  },
37
37
  "devDependencies": {
38
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
  }