@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 +17 -0
- package/package.json +3 -3
- package/src/commands/QueueWorkCommand.ts +51 -9
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.
|
|
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.
|
|
35
|
-
"@zerotal/orm": "1.7.
|
|
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: "
|
|
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
|
}
|