queue-inspector-mcp 0.1.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/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/dist/backends/asynq-proto.d.ts +16 -0
- package/dist/backends/asynq-proto.js +144 -0
- package/dist/backends/asynq-proto.js.map +1 -0
- package/dist/backends/asynq.d.ts +45 -0
- package/dist/backends/asynq.js +183 -0
- package/dist/backends/asynq.js.map +1 -0
- package/dist/backends/bullmq.d.ts +46 -0
- package/dist/backends/bullmq.js +279 -0
- package/dist/backends/bullmq.js.map +1 -0
- package/dist/backends/index.d.ts +19 -0
- package/dist/backends/index.js +57 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/backends/lua/deleteTask.lua +41 -0
- package/dist/backends/lua/removeJob.lua +346 -0
- package/dist/backends/lua/reprocessJob.lua +113 -0
- package/dist/backends/lua/runTask.lua +39 -0
- package/dist/backends/lua.d.ts +18 -0
- package/dist/backends/lua.js +18 -0
- package/dist/backends/lua.js.map +1 -0
- package/dist/backends/scripting.d.ts +14 -0
- package/dist/backends/scripting.js +34 -0
- package/dist/backends/scripting.js.map +1 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +32 -0
- package/dist/config.js.map +1 -0
- package/dist/format.d.ts +16 -0
- package/dist/format.js +49 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/redis.d.ts +13 -0
- package/dist/redis.js +30 -0
- package/dist/redis.js.map +1 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +105 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/package.json +56 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-07-08
|
|
8
|
+
|
|
9
|
+
Initial release.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- MCP server (`stdio`) exposing six tools: `list_queues`, `queue_stats`,
|
|
14
|
+
`list_jobs`, `get_job`, `retry_job`, `delete_job`.
|
|
15
|
+
- Asynq backend. Reads the protobuf `TaskMessage` stored in each task hash and
|
|
16
|
+
reports the pending / active / scheduled / retry / archived / completed states.
|
|
17
|
+
Retry and delete run Asynq's own `RunTask` and `DeleteTask` scripts.
|
|
18
|
+
- BullMQ backend. Reports the waiting / active / delayed / prioritized /
|
|
19
|
+
waiting-children / paused / completed / failed states. Retry and delete run
|
|
20
|
+
BullMQ's own `reprocessJob` and `removeJob` scripts.
|
|
21
|
+
- Read-only mode via `--read-only` or `QUEUE_INSPECTOR_READ_ONLY=1`, which skips
|
|
22
|
+
the mutating tools.
|
|
23
|
+
- Configurable Redis URL and key prefixes (`REDIS_URL`, `ASYNQ_PREFIX`,
|
|
24
|
+
`BULL_PREFIX`) and backend selection (`QUEUE_INSPECTOR_BACKENDS`).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yusuf İhsan Görgel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# queue-inspector-mcp
|
|
2
|
+
|
|
3
|
+
An MCP server that lets an agent inspect and operate Redis-backed job queues. It
|
|
4
|
+
speaks to two backends today, [Asynq](https://github.com/hibiken/asynq) (Go) and
|
|
5
|
+
[BullMQ](https://github.com/taskforcesh/bullmq) (Node), reporting per-state
|
|
6
|
+
counts, individual job detail, and moving jobs between states.
|
|
7
|
+
|
|
8
|
+
## Why
|
|
9
|
+
|
|
10
|
+
When a queue misbehaves in production, the useful questions are about jobs, not
|
|
11
|
+
keys: how many tasks are stuck in retry, what error a specific job failed with,
|
|
12
|
+
how many attempts it has left, whether a dead job can be requeued. A plain Redis
|
|
13
|
+
MCP server can only show you keys and raw values; it does not know that an Asynq
|
|
14
|
+
task message is protobuf, that a BullMQ job's state is decided by which sorted
|
|
15
|
+
set it sits in, or how either library moves a job back to the front of the line.
|
|
16
|
+
This server encodes that knowledge so an agent can reason about queue state and
|
|
17
|
+
take safe, state-aware actions.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
Requires Node.js 18 or newer and a reachable Redis.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g queue-inspector-mcp
|
|
25
|
+
# or run without installing:
|
|
26
|
+
npx queue-inspector-mcp
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Configure
|
|
30
|
+
|
|
31
|
+
The server talks MCP over stdio, so it works with any MCP client. Point your
|
|
32
|
+
client at the `queue-inspector-mcp` binary and set `REDIS_URL`.
|
|
33
|
+
|
|
34
|
+
Claude Desktop (`claude_desktop_config.json`):
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"queues": {
|
|
40
|
+
"command": "npx",
|
|
41
|
+
"args": ["-y", "queue-inspector-mcp"],
|
|
42
|
+
"env": { "REDIS_URL": "redis://localhost:6379" }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Claude Code (project `.mcp.json`, or `claude mcp add`):
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"mcpServers": {
|
|
53
|
+
"queues": {
|
|
54
|
+
"command": "npx",
|
|
55
|
+
"args": ["-y", "queue-inspector-mcp"],
|
|
56
|
+
"env": { "REDIS_URL": "redis://localhost:6379", "QUEUE_INSPECTOR_READ_ONLY": "1" }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Configuration
|
|
63
|
+
|
|
64
|
+
| Variable | Default | Purpose |
|
|
65
|
+
| --- | --- | --- |
|
|
66
|
+
| `REDIS_URL` | `redis://localhost:6379` | Redis connection string. Include a database number, e.g. `redis://localhost:6379/2`. |
|
|
67
|
+
| `ASYNQ_PREFIX` | `asynq` | Key prefix Asynq was configured with. |
|
|
68
|
+
| `BULL_PREFIX` | `bull` | Key prefix BullMQ was configured with. |
|
|
69
|
+
| `QUEUE_INSPECTOR_BACKENDS` | `asynq,bullmq` | Restrict which backends are scanned. |
|
|
70
|
+
| `QUEUE_INSPECTOR_READ_ONLY` | unset | Set to `1` (or pass `--read-only`) to omit the mutating tools. |
|
|
71
|
+
|
|
72
|
+
## Tools
|
|
73
|
+
|
|
74
|
+
| Tool | Mutating | Behavior |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| `list_queues` | no | List every detected queue, tagged with its backend. |
|
|
77
|
+
| `queue_stats` | no | Count jobs per state for a queue, using the backend's own state names. |
|
|
78
|
+
| `list_jobs` | no | Page through jobs in one state; returns id, type, attempts, and a truncated last error. |
|
|
79
|
+
| `get_job` | no | Full detail for one job: payload, attempts, retry ceiling, last error, timestamps. |
|
|
80
|
+
| `retry_job` | yes | Move a failed or dead job back to pending/wait so it runs again. |
|
|
81
|
+
| `delete_job` | yes | Permanently delete a job. Active jobs are refused. |
|
|
82
|
+
|
|
83
|
+
When a queue name is unique across the enabled backends, the `backend` argument
|
|
84
|
+
is optional; the server resolves it. If the same name exists in both backends,
|
|
85
|
+
pass `backend` explicitly.
|
|
86
|
+
|
|
87
|
+
## Read-only mode
|
|
88
|
+
|
|
89
|
+
With `--read-only` or `QUEUE_INSPECTOR_READ_ONLY=1`, the server never registers
|
|
90
|
+
`retry_job` or `delete_job`. The mutating tools are absent from `tools/list`
|
|
91
|
+
entirely, so a client cannot call them by mistake. This is the recommended
|
|
92
|
+
configuration for pointing an agent at a production database.
|
|
93
|
+
|
|
94
|
+
## Backend state names
|
|
95
|
+
|
|
96
|
+
The two libraries model job lifecycles differently, so this server does not
|
|
97
|
+
invent a shared vocabulary. It reports each backend's own state names, and each
|
|
98
|
+
state maps to a specific Redis structure.
|
|
99
|
+
|
|
100
|
+
Asynq:
|
|
101
|
+
|
|
102
|
+
| State | Meaning | Redis structure |
|
|
103
|
+
| --- | --- | --- |
|
|
104
|
+
| `pending` | Ready to run, waiting for a worker | list `asynq:{q}:pending` |
|
|
105
|
+
| `active` | Currently being processed | list `asynq:{q}:active` |
|
|
106
|
+
| `scheduled` | Enqueued for a future time | zset `asynq:{q}:scheduled` |
|
|
107
|
+
| `retry` | Failed, waiting to be retried | zset `asynq:{q}:retry` |
|
|
108
|
+
| `archived` | Retries exhausted (the "dead" state) | zset `asynq:{q}:archived` |
|
|
109
|
+
| `completed` | Finished, kept for its retention window | zset `asynq:{q}:completed` |
|
|
110
|
+
|
|
111
|
+
BullMQ:
|
|
112
|
+
|
|
113
|
+
| State | Meaning | Redis structure |
|
|
114
|
+
| --- | --- | --- |
|
|
115
|
+
| `waiting` | Ready to run | list `bull:q:wait` |
|
|
116
|
+
| `active` | Currently being processed | list `bull:q:active` |
|
|
117
|
+
| `delayed` | Scheduled for a future time | zset `bull:q:delayed` |
|
|
118
|
+
| `prioritized` | Waiting, ordered by priority | zset `bull:q:prioritized` |
|
|
119
|
+
| `waiting-children` | Blocked on child jobs (flows) | zset `bull:q:waiting-children` |
|
|
120
|
+
| `paused` | Held while the queue is paused | list `bull:q:paused` |
|
|
121
|
+
| `completed` | Finished successfully | zset `bull:q:completed` |
|
|
122
|
+
| `failed` | Failed after exhausting attempts | zset `bull:q:failed` |
|
|
123
|
+
|
|
124
|
+
Asynq's `archived` is what most people mean by a "dead" job. `list_jobs` returns
|
|
125
|
+
Asynq's terminal sets in Redis (score) order and BullMQ's `completed`/`failed`
|
|
126
|
+
sets most-recent-first.
|
|
127
|
+
|
|
128
|
+
## What this doesn't do
|
|
129
|
+
|
|
130
|
+
- Only Asynq and BullMQ are supported. Sidekiq, Celery, RQ and others are not.
|
|
131
|
+
- No web UI. This is an MCP server for programmatic use; it is not a dashboard.
|
|
132
|
+
- No streaming or watch. Each tool call is a point-in-time read; there is no
|
|
133
|
+
subscription to queue events.
|
|
134
|
+
- `retry_job` and `delete_job` faithfully replicate each library's own mechanism
|
|
135
|
+
rather than reimplementing it. Retry runs Asynq's `Inspector.RunTask` script
|
|
136
|
+
and BullMQ's `Job.retry` (`reprocessJob`) script; delete runs Asynq's
|
|
137
|
+
`Inspector.DeleteTask` script and BullMQ's `Job.remove` (`removeJob`) script.
|
|
138
|
+
As a result the semantics match the libraries: retrying a BullMQ job applies
|
|
139
|
+
only to `failed`/`completed` jobs and does not reset `attemptsMade` (matching
|
|
140
|
+
`Job.retry()`); neither backend can retry or delete an `active` job.
|
|
141
|
+
- `delete_job` removes a single BullMQ job and does not cascade into a flow's
|
|
142
|
+
children.
|
|
143
|
+
- Asynq group aggregation (the `aggregating` state) is not surfaced in this
|
|
144
|
+
release.
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT © Yusuf İhsan Görgel
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface AsynqTaskMessage {
|
|
2
|
+
type: string;
|
|
3
|
+
payload: Buffer;
|
|
4
|
+
id: string;
|
|
5
|
+
queue: string;
|
|
6
|
+
maxRetry: number;
|
|
7
|
+
retried: number;
|
|
8
|
+
errorMsg: string;
|
|
9
|
+
timeoutSecs: number;
|
|
10
|
+
deadlineUnix: number;
|
|
11
|
+
lastFailedAtUnix: number;
|
|
12
|
+
retentionSecs: number;
|
|
13
|
+
completedAtUnix: number;
|
|
14
|
+
groupKey: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function decodeTaskMessage(buf: Buffer): AsynqTaskMessage;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Asynq stores each task's metadata as a protobuf-encoded TaskMessage in the
|
|
2
|
+
// `msg` field of its task hash. Rather than pull in a full protobuf runtime for
|
|
3
|
+
// one flat message, this reads the handful of scalar fields the inspector needs
|
|
4
|
+
// straight off the wire. Field numbers come from the vendored schema in
|
|
5
|
+
// ./proto/asynq_task.proto (from hibiken/asynq, MIT). They are deliberately
|
|
6
|
+
// non-sequential upstream (last_failed_at is 11, not 8), so they are pinned by
|
|
7
|
+
// number here, not inferred from order.
|
|
8
|
+
const FIELD = {
|
|
9
|
+
type: 1,
|
|
10
|
+
payload: 2,
|
|
11
|
+
id: 3,
|
|
12
|
+
queue: 4,
|
|
13
|
+
retry: 5,
|
|
14
|
+
retried: 6,
|
|
15
|
+
errorMsg: 7,
|
|
16
|
+
timeout: 8,
|
|
17
|
+
deadline: 9,
|
|
18
|
+
uniqueKey: 10,
|
|
19
|
+
lastFailedAt: 11,
|
|
20
|
+
retention: 12,
|
|
21
|
+
completedAt: 13,
|
|
22
|
+
groupKey: 14,
|
|
23
|
+
};
|
|
24
|
+
class Reader {
|
|
25
|
+
buf;
|
|
26
|
+
pos = 0;
|
|
27
|
+
constructor(buf) {
|
|
28
|
+
this.buf = buf;
|
|
29
|
+
}
|
|
30
|
+
get done() {
|
|
31
|
+
return this.pos >= this.buf.length;
|
|
32
|
+
}
|
|
33
|
+
/** Reads a base-128 varint. Values used here (ids, counts, unix seconds) stay
|
|
34
|
+
* well inside Number.MAX_SAFE_INTEGER, so a plain number is safe. */
|
|
35
|
+
varint() {
|
|
36
|
+
let result = 0;
|
|
37
|
+
let shift = 0;
|
|
38
|
+
for (;;) {
|
|
39
|
+
if (this.pos >= this.buf.length)
|
|
40
|
+
throw new Error("truncated varint");
|
|
41
|
+
const byte = this.buf[this.pos++];
|
|
42
|
+
result += (byte & 0x7f) * 2 ** shift;
|
|
43
|
+
if ((byte & 0x80) === 0)
|
|
44
|
+
return result;
|
|
45
|
+
shift += 7;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
bytes() {
|
|
49
|
+
const len = this.varint();
|
|
50
|
+
const end = this.pos + len;
|
|
51
|
+
if (end > this.buf.length)
|
|
52
|
+
throw new Error("length-delimited field runs past end of buffer");
|
|
53
|
+
const out = this.buf.subarray(this.pos, end);
|
|
54
|
+
this.pos = end;
|
|
55
|
+
return Buffer.from(out);
|
|
56
|
+
}
|
|
57
|
+
/** Advances past a field whose number we do not care about. */
|
|
58
|
+
skip(wireType) {
|
|
59
|
+
switch (wireType) {
|
|
60
|
+
case 0:
|
|
61
|
+
this.varint();
|
|
62
|
+
return;
|
|
63
|
+
case 1:
|
|
64
|
+
this.pos += 8;
|
|
65
|
+
return;
|
|
66
|
+
case 2:
|
|
67
|
+
this.pos += this.varint();
|
|
68
|
+
return;
|
|
69
|
+
case 5:
|
|
70
|
+
this.pos += 4;
|
|
71
|
+
return;
|
|
72
|
+
default:
|
|
73
|
+
throw new Error(`unsupported protobuf wire type ${wireType}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function decodeTaskMessage(buf) {
|
|
78
|
+
const msg = {
|
|
79
|
+
type: "",
|
|
80
|
+
payload: Buffer.alloc(0),
|
|
81
|
+
id: "",
|
|
82
|
+
queue: "",
|
|
83
|
+
maxRetry: 0,
|
|
84
|
+
retried: 0,
|
|
85
|
+
errorMsg: "",
|
|
86
|
+
timeoutSecs: 0,
|
|
87
|
+
deadlineUnix: 0,
|
|
88
|
+
lastFailedAtUnix: 0,
|
|
89
|
+
retentionSecs: 0,
|
|
90
|
+
completedAtUnix: 0,
|
|
91
|
+
groupKey: "",
|
|
92
|
+
};
|
|
93
|
+
const r = new Reader(buf);
|
|
94
|
+
while (!r.done) {
|
|
95
|
+
const tag = r.varint();
|
|
96
|
+
const field = tag >> 3;
|
|
97
|
+
const wireType = tag & 0x07;
|
|
98
|
+
switch (field) {
|
|
99
|
+
case FIELD.type:
|
|
100
|
+
msg.type = r.bytes().toString("utf8");
|
|
101
|
+
break;
|
|
102
|
+
case FIELD.payload:
|
|
103
|
+
msg.payload = r.bytes();
|
|
104
|
+
break;
|
|
105
|
+
case FIELD.id:
|
|
106
|
+
msg.id = r.bytes().toString("utf8");
|
|
107
|
+
break;
|
|
108
|
+
case FIELD.queue:
|
|
109
|
+
msg.queue = r.bytes().toString("utf8");
|
|
110
|
+
break;
|
|
111
|
+
case FIELD.retry:
|
|
112
|
+
msg.maxRetry = r.varint();
|
|
113
|
+
break;
|
|
114
|
+
case FIELD.retried:
|
|
115
|
+
msg.retried = r.varint();
|
|
116
|
+
break;
|
|
117
|
+
case FIELD.errorMsg:
|
|
118
|
+
msg.errorMsg = r.bytes().toString("utf8");
|
|
119
|
+
break;
|
|
120
|
+
case FIELD.timeout:
|
|
121
|
+
msg.timeoutSecs = r.varint();
|
|
122
|
+
break;
|
|
123
|
+
case FIELD.deadline:
|
|
124
|
+
msg.deadlineUnix = r.varint();
|
|
125
|
+
break;
|
|
126
|
+
case FIELD.lastFailedAt:
|
|
127
|
+
msg.lastFailedAtUnix = r.varint();
|
|
128
|
+
break;
|
|
129
|
+
case FIELD.retention:
|
|
130
|
+
msg.retentionSecs = r.varint();
|
|
131
|
+
break;
|
|
132
|
+
case FIELD.completedAt:
|
|
133
|
+
msg.completedAtUnix = r.varint();
|
|
134
|
+
break;
|
|
135
|
+
case FIELD.groupKey:
|
|
136
|
+
msg.groupKey = r.bytes().toString("utf8");
|
|
137
|
+
break;
|
|
138
|
+
default:
|
|
139
|
+
r.skip(wireType);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return msg;
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=asynq-proto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asynq-proto.js","sourceRoot":"","sources":["../../src/backends/asynq-proto.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,gFAAgF;AAChF,gFAAgF;AAChF,wEAAwE;AACxE,4EAA4E;AAC5E,+EAA+E;AAC/E,wCAAwC;AAkBxC,MAAM,KAAK,GAAG;IACZ,IAAI,EAAE,CAAC;IACP,OAAO,EAAE,CAAC;IACV,EAAE,EAAE,CAAC;IACL,KAAK,EAAE,CAAC;IACR,KAAK,EAAE,CAAC;IACR,OAAO,EAAE,CAAC;IACV,QAAQ,EAAE,CAAC;IACX,OAAO,EAAE,CAAC;IACV,QAAQ,EAAE,CAAC;IACX,SAAS,EAAE,EAAE;IACb,YAAY,EAAE,EAAE;IAChB,SAAS,EAAE,EAAE;IACb,WAAW,EAAE,EAAE;IACf,QAAQ,EAAE,EAAE;CACJ,CAAC;AAEX,MAAM,MAAM;IAEmB;IADrB,GAAG,GAAG,CAAC,CAAC;IAChB,YAA6B,GAAW;QAAX,QAAG,GAAH,GAAG,CAAQ;IAAG,CAAC;IAE5C,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC;IACrC,CAAC;IAED;0EACsE;IACtE,MAAM;QACJ,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,SAAS,CAAC;YACR,IAAI,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM;gBAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAC;YACrE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,CAAE,CAAC;YACnC,MAAM,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC;YACrC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC;gBAAE,OAAO,MAAM,CAAC;YACvC,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;IACH,CAAC;IAED,KAAK;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QAC3B,IAAI,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;QAC7F,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAC7C,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;IAED,+DAA+D;IAC/D,IAAI,CAAC,QAAgB;QACnB,QAAQ,QAAQ,EAAE,CAAC;YACjB,KAAK,CAAC;gBACJ,IAAI,CAAC,MAAM,EAAE,CAAC;gBACd,OAAO;YACT,KAAK,CAAC;gBACJ,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;gBACd,OAAO;YACT,KAAK,CAAC;gBACJ,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;gBAC1B,OAAO;YACT,KAAK,CAAC;gBACJ,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;gBACd,OAAO;YACT;gBACE,MAAM,IAAI,KAAK,CAAC,kCAAkC,QAAQ,EAAE,CAAC,CAAC;QAClE,CAAC;IACH,CAAC;CACF;AAED,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,MAAM,GAAG,GAAqB;QAC5B,IAAI,EAAE,EAAE;QACR,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACxB,EAAE,EAAE,EAAE;QACN,KAAK,EAAE,EAAE;QACT,QAAQ,EAAE,CAAC;QACX,OAAO,EAAE,CAAC;QACV,QAAQ,EAAE,EAAE;QACZ,WAAW,EAAE,CAAC;QACd,YAAY,EAAE,CAAC;QACf,gBAAgB,EAAE,CAAC;QACnB,aAAa,EAAE,CAAC;QAChB,eAAe,EAAE,CAAC;QAClB,QAAQ,EAAE,EAAE;KACb,CAAC;IAEF,MAAM,CAAC,GAAG,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1B,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACf,MAAM,GAAG,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC;QACvB,MAAM,KAAK,GAAG,GAAG,IAAI,CAAC,CAAC;QACvB,MAAM,QAAQ,GAAG,GAAG,GAAG,IAAI,CAAC;QAE5B,QAAQ,KAAK,EAAE,CAAC;YACd,KAAK,KAAK,CAAC,IAAI;gBACb,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;gBACtC,MAAM;YACR,KAAK,KAAK,CAAC,OAAO;gBAChB,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC;gBACxB,MAAM;YACR,KAAK,KAAK,CAAC,EAAE;gBACX,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;gBACpC,MAAM;YACR,KAAK,KAAK,CAAC,KAAK;gBACd,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;gBACvC,MAAM;YACR,KAAK,KAAK,CAAC,KAAK;gBACd,GAAG,CAAC,QAAQ,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC;gBAC1B,MAAM;YACR,KAAK,KAAK,CAAC,OAAO;gBAChB,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC;gBACzB,MAAM;YACR,KAAK,KAAK,CAAC,QAAQ;gBACjB,GAAG,CAAC,QAAQ,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;gBAC1C,MAAM;YACR,KAAK,KAAK,CAAC,OAAO;gBAChB,GAAG,CAAC,WAAW,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC;gBAC7B,MAAM;YACR,KAAK,KAAK,CAAC,QAAQ;gBACjB,GAAG,CAAC,YAAY,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC;gBAC9B,MAAM;YACR,KAAK,KAAK,CAAC,YAAY;gBACrB,GAAG,CAAC,gBAAgB,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC;gBAClC,MAAM;YACR,KAAK,KAAK,CAAC,SAAS;gBAClB,GAAG,CAAC,aAAa,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC;gBAC/B,MAAM;YACR,KAAK,KAAK,CAAC,WAAW;gBACpB,GAAG,CAAC,eAAe,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC;gBACjC,MAAM;YACR,KAAK,KAAK,CAAC,QAAQ;gBACjB,GAAG,CAAC,QAAQ,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;gBAC1C,MAAM;YACR;gBACE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Redis } from "ioredis";
|
|
2
|
+
import { type JobDetail, type JobSummary, type PageOpts, type QueueBackend, type StateCounts } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Adapter for queues managed by Asynq (github.com/hibiken/asynq).
|
|
5
|
+
*
|
|
6
|
+
* Key layout, verified against a running Asynq worker:
|
|
7
|
+
* asynq:queues SET of queue names
|
|
8
|
+
* asynq:{<q>}:pending / :active LIST of task ids
|
|
9
|
+
* asynq:{<q>}:scheduled/:retry/ ZSET task id -> unix seconds (process-at,
|
|
10
|
+
* :archived/:completed next retry, last failure, or expiry)
|
|
11
|
+
* asynq:{<q>}:t:<id> HASH { msg: protobuf TaskMessage, state,
|
|
12
|
+
* pending_since }
|
|
13
|
+
* Task metadata is a protobuf TaskMessage; see ./asynq-proto.ts.
|
|
14
|
+
*/
|
|
15
|
+
export declare class AsynqBackend implements QueueBackend {
|
|
16
|
+
private readonly redis;
|
|
17
|
+
private readonly prefix;
|
|
18
|
+
readonly name: "asynq";
|
|
19
|
+
readonly states: readonly ["pending", "active", "scheduled", "retry", "archived", "completed"];
|
|
20
|
+
private readonly scripts;
|
|
21
|
+
constructor(redis: Redis, prefix?: string);
|
|
22
|
+
private queuesKey;
|
|
23
|
+
private queuePrefix;
|
|
24
|
+
private taskKey;
|
|
25
|
+
private stateKey;
|
|
26
|
+
detectQueues(): Promise<string[]>;
|
|
27
|
+
private assertQueue;
|
|
28
|
+
stats(queue: string): Promise<StateCounts>;
|
|
29
|
+
private assertState;
|
|
30
|
+
listJobs(queue: string, rawState: string, page: PageOpts): Promise<JobSummary[]>;
|
|
31
|
+
getJob(queue: string, id: string): Promise<JobDetail | null>;
|
|
32
|
+
private readTask;
|
|
33
|
+
private readTaskDetail;
|
|
34
|
+
/** Asynq's TaskMessage carries no enqueue time; a pending task does record a
|
|
35
|
+
* `pending_since` field in nanoseconds, which is the closest equivalent. */
|
|
36
|
+
private enqueuedAt;
|
|
37
|
+
retryJob(queue: string, id: string): Promise<{
|
|
38
|
+
ok: true;
|
|
39
|
+
message: string;
|
|
40
|
+
}>;
|
|
41
|
+
deleteJob(queue: string, id: string): Promise<{
|
|
42
|
+
ok: true;
|
|
43
|
+
message: string;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { BackendError, } from "../types.js";
|
|
2
|
+
import { decodePayload, isoFromUnixSeconds, truncate } from "../format.js";
|
|
3
|
+
import { decodeTaskMessage } from "./asynq-proto.js";
|
|
4
|
+
import { attachScripts } from "./scripting.js";
|
|
5
|
+
// State names as Asynq itself uses them, in a natural lifecycle order. Group
|
|
6
|
+
// aggregation ("aggregating") is an advanced feature not surfaced in v0.1.
|
|
7
|
+
const STATES = ["pending", "active", "scheduled", "retry", "archived", "completed"];
|
|
8
|
+
const LIST_STATES = new Set(["pending", "active"]);
|
|
9
|
+
/**
|
|
10
|
+
* Adapter for queues managed by Asynq (github.com/hibiken/asynq).
|
|
11
|
+
*
|
|
12
|
+
* Key layout, verified against a running Asynq worker:
|
|
13
|
+
* asynq:queues SET of queue names
|
|
14
|
+
* asynq:{<q>}:pending / :active LIST of task ids
|
|
15
|
+
* asynq:{<q>}:scheduled/:retry/ ZSET task id -> unix seconds (process-at,
|
|
16
|
+
* :archived/:completed next retry, last failure, or expiry)
|
|
17
|
+
* asynq:{<q>}:t:<id> HASH { msg: protobuf TaskMessage, state,
|
|
18
|
+
* pending_since }
|
|
19
|
+
* Task metadata is a protobuf TaskMessage; see ./asynq-proto.ts.
|
|
20
|
+
*/
|
|
21
|
+
export class AsynqBackend {
|
|
22
|
+
redis;
|
|
23
|
+
prefix;
|
|
24
|
+
name = "asynq";
|
|
25
|
+
states = STATES;
|
|
26
|
+
scripts;
|
|
27
|
+
constructor(redis, prefix = "asynq") {
|
|
28
|
+
this.redis = redis;
|
|
29
|
+
this.prefix = prefix;
|
|
30
|
+
this.scripts = attachScripts(redis);
|
|
31
|
+
}
|
|
32
|
+
queuesKey() {
|
|
33
|
+
return `${this.prefix}:queues`;
|
|
34
|
+
}
|
|
35
|
+
queuePrefix(queue) {
|
|
36
|
+
return `${this.prefix}:{${queue}}:`;
|
|
37
|
+
}
|
|
38
|
+
taskKey(queue, id) {
|
|
39
|
+
return `${this.queuePrefix(queue)}t:${id}`;
|
|
40
|
+
}
|
|
41
|
+
stateKey(queue, state) {
|
|
42
|
+
return `${this.queuePrefix(queue)}${state}`;
|
|
43
|
+
}
|
|
44
|
+
async detectQueues() {
|
|
45
|
+
const queues = await this.redis.smembers(this.queuesKey());
|
|
46
|
+
return queues.sort();
|
|
47
|
+
}
|
|
48
|
+
async assertQueue(queue) {
|
|
49
|
+
const known = await this.redis.sismember(this.queuesKey(), queue);
|
|
50
|
+
if (!known) {
|
|
51
|
+
throw new BackendError(`asynq queue "${queue}" was not found`, "queue_not_found");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async stats(queue) {
|
|
55
|
+
await this.assertQueue(queue);
|
|
56
|
+
const counts = {};
|
|
57
|
+
for (const state of STATES) {
|
|
58
|
+
const key = this.stateKey(queue, state);
|
|
59
|
+
counts[state] = LIST_STATES.has(state)
|
|
60
|
+
? await this.redis.llen(key)
|
|
61
|
+
: await this.redis.zcard(key);
|
|
62
|
+
}
|
|
63
|
+
return counts;
|
|
64
|
+
}
|
|
65
|
+
assertState(state) {
|
|
66
|
+
if (!STATES.includes(state)) {
|
|
67
|
+
throw new BackendError(`unknown asynq state "${state}"; expected one of ${STATES.join(", ")}`, "invalid_state");
|
|
68
|
+
}
|
|
69
|
+
return state;
|
|
70
|
+
}
|
|
71
|
+
async listJobs(queue, rawState, page) {
|
|
72
|
+
await this.assertQueue(queue);
|
|
73
|
+
const state = this.assertState(rawState);
|
|
74
|
+
const key = this.stateKey(queue, state);
|
|
75
|
+
const stop = page.offset + page.limit - 1;
|
|
76
|
+
const ids = LIST_STATES.has(state)
|
|
77
|
+
? await this.redis.lrange(key, page.offset, stop)
|
|
78
|
+
: await this.redis.zrange(key, page.offset, stop);
|
|
79
|
+
const summaries = [];
|
|
80
|
+
for (const id of ids) {
|
|
81
|
+
const detail = await this.readTask(queue, id, state);
|
|
82
|
+
if (detail)
|
|
83
|
+
summaries.push(detail);
|
|
84
|
+
}
|
|
85
|
+
return summaries;
|
|
86
|
+
}
|
|
87
|
+
async getJob(queue, id) {
|
|
88
|
+
await this.assertQueue(queue);
|
|
89
|
+
const state = (await this.redis.hget(this.taskKey(queue, id), "state"));
|
|
90
|
+
if (state === null)
|
|
91
|
+
return null;
|
|
92
|
+
return this.readTaskDetail(queue, id, state);
|
|
93
|
+
}
|
|
94
|
+
async readTask(queue, id, state) {
|
|
95
|
+
const raw = await this.redis.hgetBuffer(this.taskKey(queue, id), "msg");
|
|
96
|
+
if (raw === null)
|
|
97
|
+
return null;
|
|
98
|
+
const msg = decodeTaskMessage(raw);
|
|
99
|
+
return {
|
|
100
|
+
id,
|
|
101
|
+
type: msg.type,
|
|
102
|
+
state,
|
|
103
|
+
enqueuedAt: await this.enqueuedAt(queue, id, state),
|
|
104
|
+
attempts: msg.retried,
|
|
105
|
+
maxRetries: msg.maxRetry,
|
|
106
|
+
lastError: msg.errorMsg ? truncate(msg.errorMsg) : null,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async readTaskDetail(queue, id, state) {
|
|
110
|
+
const raw = await this.redis.hgetBuffer(this.taskKey(queue, id), "msg");
|
|
111
|
+
if (raw === null)
|
|
112
|
+
return null;
|
|
113
|
+
const msg = decodeTaskMessage(raw);
|
|
114
|
+
const decoded = decodePayload(msg.payload);
|
|
115
|
+
const timestamps = {
|
|
116
|
+
enqueuedAt: await this.enqueuedAt(queue, id, state),
|
|
117
|
+
lastFailedAt: isoFromUnixSeconds(msg.lastFailedAtUnix),
|
|
118
|
+
completedAt: isoFromUnixSeconds(msg.completedAtUnix),
|
|
119
|
+
deadline: isoFromUnixSeconds(msg.deadlineUnix),
|
|
120
|
+
};
|
|
121
|
+
if (state === "scheduled" || state === "retry") {
|
|
122
|
+
const score = await this.redis.zscore(this.stateKey(queue, state), id);
|
|
123
|
+
timestamps.nextProcessAt = score ? isoFromUnixSeconds(Number(score)) : null;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
id,
|
|
127
|
+
queue,
|
|
128
|
+
backend: this.name,
|
|
129
|
+
type: msg.type,
|
|
130
|
+
state,
|
|
131
|
+
enqueuedAt: timestamps.enqueuedAt ?? null,
|
|
132
|
+
attempts: msg.retried,
|
|
133
|
+
maxRetries: msg.maxRetry,
|
|
134
|
+
lastError: msg.errorMsg ? truncate(msg.errorMsg) : null,
|
|
135
|
+
fullError: msg.errorMsg || null,
|
|
136
|
+
timestamps,
|
|
137
|
+
...decoded,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** Asynq's TaskMessage carries no enqueue time; a pending task does record a
|
|
141
|
+
* `pending_since` field in nanoseconds, which is the closest equivalent. */
|
|
142
|
+
async enqueuedAt(queue, id, state) {
|
|
143
|
+
if (state !== "pending")
|
|
144
|
+
return null;
|
|
145
|
+
const ns = await this.redis.hget(this.taskKey(queue, id), "pending_since");
|
|
146
|
+
if (!ns)
|
|
147
|
+
return null;
|
|
148
|
+
return new Date(Number(ns) / 1e6).toISOString();
|
|
149
|
+
}
|
|
150
|
+
async retryJob(queue, id) {
|
|
151
|
+
await this.assertQueue(queue);
|
|
152
|
+
const prefix = this.queuePrefix(queue);
|
|
153
|
+
const result = await this.scripts.runTask([this.taskKey(queue, id), `${prefix}pending`, `${prefix}groups`], [id, prefix, `${prefix}g:`]);
|
|
154
|
+
switch (result) {
|
|
155
|
+
case 1:
|
|
156
|
+
return { ok: true, message: `asynq task ${id} moved to pending in queue "${queue}"` };
|
|
157
|
+
case 0:
|
|
158
|
+
throw new BackendError(`asynq task "${id}" was not found in queue "${queue}"`, "job_not_found");
|
|
159
|
+
case -1:
|
|
160
|
+
throw new BackendError(`asynq task "${id}" is currently active and cannot be retried`, "invalid_state");
|
|
161
|
+
case -2:
|
|
162
|
+
throw new BackendError(`asynq task "${id}" is already pending`, "invalid_state");
|
|
163
|
+
default:
|
|
164
|
+
throw new BackendError(`asynq retry returned unexpected code ${result}`, "invalid_state");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async deleteJob(queue, id) {
|
|
168
|
+
await this.assertQueue(queue);
|
|
169
|
+
const prefix = this.queuePrefix(queue);
|
|
170
|
+
const result = await this.scripts.deleteTask([this.taskKey(queue, id), `${prefix}groups`], [id, prefix, `${prefix}g:`]);
|
|
171
|
+
switch (result) {
|
|
172
|
+
case 1:
|
|
173
|
+
return { ok: true, message: `asynq task ${id} deleted from queue "${queue}"` };
|
|
174
|
+
case 0:
|
|
175
|
+
throw new BackendError(`asynq task "${id}" was not found in queue "${queue}"`, "job_not_found");
|
|
176
|
+
case -1:
|
|
177
|
+
throw new BackendError(`asynq task "${id}" is currently active and cannot be deleted`, "invalid_state");
|
|
178
|
+
default:
|
|
179
|
+
throw new BackendError(`asynq delete returned unexpected code ${result}`, "invalid_state");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=asynq.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asynq.js","sourceRoot":"","sources":["../../src/backends/asynq.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Redis } from "ioredis";
|
|
2
|
+
import { type JobDetail, type JobSummary, type PageOpts, type QueueBackend, type StateCounts } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Adapter for queues managed by BullMQ (github.com/taskforcesh/bullmq).
|
|
5
|
+
*
|
|
6
|
+
* Key layout, verified against a running BullMQ worker:
|
|
7
|
+
* bull:<q>:meta HASH queue metadata (version, paused)
|
|
8
|
+
* bull:<q>:wait / :active / :paused LIST of job ids
|
|
9
|
+
* bull:<q>:delayed / :prioritized / ZSET of job ids
|
|
10
|
+
* :completed / :failed
|
|
11
|
+
* bull:<q>:<id> HASH { name, data, opts, timestamp,
|
|
12
|
+
* atm, failedReason, ... }
|
|
13
|
+
* Mutations run BullMQ's own vendored scripts; see ./lua and ./scripting.ts.
|
|
14
|
+
*/
|
|
15
|
+
export declare class BullmqBackend implements QueueBackend {
|
|
16
|
+
private readonly redis;
|
|
17
|
+
private readonly prefix;
|
|
18
|
+
readonly name: "bullmq";
|
|
19
|
+
readonly states: readonly ["waiting", "active", "delayed", "prioritized", "waiting-children", "paused", "completed", "failed"];
|
|
20
|
+
private readonly scripts;
|
|
21
|
+
constructor(redis: Redis, prefix?: string);
|
|
22
|
+
private key;
|
|
23
|
+
private jobKey;
|
|
24
|
+
detectQueues(): Promise<string[]>;
|
|
25
|
+
private assertQueue;
|
|
26
|
+
stats(queue: string): Promise<StateCounts>;
|
|
27
|
+
private assertState;
|
|
28
|
+
private idsForState;
|
|
29
|
+
listJobs(queue: string, rawState: string, page: PageOpts): Promise<JobSummary[]>;
|
|
30
|
+
getJob(queue: string, id: string): Promise<JobDetail | null>;
|
|
31
|
+
private readHash;
|
|
32
|
+
private toSummary;
|
|
33
|
+
private maxRetries;
|
|
34
|
+
private isLifo;
|
|
35
|
+
/** Determines a job's current state by membership, mirroring BullMQ's own
|
|
36
|
+
* getState: check the terminal and scheduling sets, then the lists. */
|
|
37
|
+
private resolveState;
|
|
38
|
+
retryJob(queue: string, id: string): Promise<{
|
|
39
|
+
ok: true;
|
|
40
|
+
message: string;
|
|
41
|
+
}>;
|
|
42
|
+
deleteJob(queue: string, id: string): Promise<{
|
|
43
|
+
ok: true;
|
|
44
|
+
message: string;
|
|
45
|
+
}>;
|
|
46
|
+
}
|