@vercube/queue 1.3.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/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/QueueStrategy-DwLPpfFM.mjs +237 -0
- package/dist/QueueStrategy-saWSBeU6.d.mts +715 -0
- package/dist/Strategies/BullMQStrategy.d.mts +161 -0
- package/dist/Strategies/BullMQStrategy.mjs +328 -0
- package/dist/Strategies/KafkaStrategy.d.mts +152 -0
- package/dist/Strategies/KafkaStrategy.mjs +280 -0
- package/dist/Strategies/MemoryStrategy.d.mts +139 -0
- package/dist/Strategies/MemoryStrategy.mjs +305 -0
- package/dist/Strategies/RabbitMQStrategy.d.mts +205 -0
- package/dist/Strategies/RabbitMQStrategy.mjs +388 -0
- package/dist/decorate-C0p0FnUM.mjs +25 -0
- package/dist/index.d.mts +953 -0
- package/dist/index.mjs +1719 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present - Vercube
|
|
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,60 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/vercube/vercube/refs/heads/main/.github/assets/cover.png" width="100%" alt="Vercube - Unleash your server development." />
|
|
3
|
+
<br>
|
|
4
|
+
<br>
|
|
5
|
+
|
|
6
|
+
# @vercube/queue
|
|
7
|
+
|
|
8
|
+
### Background jobs for Vercube apps
|
|
9
|
+
|
|
10
|
+
[&labelColor=%23000&color=%232f2f2f>)](https://deepwiki.com/vercube/vercube)
|
|
11
|
+
&labelColor=%23000&color=%232e2e2e&link=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40vercube%2Fqueue>)
|
|
12
|
+
&labelColor=%23000&color=%232f2f2f>)
|
|
13
|
+
&labelColor=%23000&color=%232f2f2f>)
|
|
14
|
+
|
|
15
|
+
**One job model over BullMQ, RabbitMQ, Kafka or plain memory - publish with `add()`, consume with a decorator, and let the module handle retries, timeouts and validation.**
|
|
16
|
+
|
|
17
|
+
[Website](https://vercube.dev) • [Documentation](https://vercube.dev/docs/getting-started)
|
|
18
|
+
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
## ✨ Features
|
|
22
|
+
|
|
23
|
+
- **One API, four transports** - BullMQ, RabbitMQ, Kafka and an in-memory strategy, mounted side by side
|
|
24
|
+
- **Decorator driven consumers** - `@Consumer()` on the class, `@Job()` on the method, `@AnyJob()` for everything else
|
|
25
|
+
- **Retries that work everywhere** - attempts, fixed or exponential backoff, and timeouts, even on transports without them
|
|
26
|
+
- **Payload validation** - any Standard Schema validates a job before the handler runs
|
|
27
|
+
- **Type-safe queues** - augment the registry and every `add()` is checked against it
|
|
28
|
+
- **Devtools ready** - queues, handlers and processed jobs show up in `@vercube/devtools`
|
|
29
|
+
|
|
30
|
+
## 📦 Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pnpm add @vercube/queue
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Install the client of the transport you use, they are all optional:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pnpm add bullmq # BullMQ, backed by Redis
|
|
40
|
+
pnpm add amqplib # RabbitMQ
|
|
41
|
+
pnpm add kafkajs # Kafka
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 📖 Usage
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
@Consumer({ queue: 'emails', concurrency: 5 })
|
|
48
|
+
export class EmailConsumer {
|
|
49
|
+
@Job('welcome', { attempts: 3, backoff: { type: 'exponential', delay: 1000 } })
|
|
50
|
+
public async welcome(payload: { userId: string }): Promise<void> {
|
|
51
|
+
await this.mailer.send(payload.userId);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Check out the full [documentation](https://vercube.dev/docs/modules/queue/overview)
|
|
57
|
+
|
|
58
|
+
## 📜 License
|
|
59
|
+
|
|
60
|
+
[MIT](https://github.com/vercube/vercube/blob/main/LICENSE)
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
//#region src/Errors/QueueError.ts
|
|
2
|
+
/**
|
|
3
|
+
* Error thrown by the queue module.
|
|
4
|
+
* Wraps transport errors with a stable shape so callers can tell what failed
|
|
5
|
+
* and whether the job is worth retrying.
|
|
6
|
+
*/
|
|
7
|
+
var QueueError = class QueueError extends Error {
|
|
8
|
+
/** The original error that caused this one. */
|
|
9
|
+
cause;
|
|
10
|
+
/** The queue operation that failed, for example `publish` or `consume`. */
|
|
11
|
+
operation;
|
|
12
|
+
/** Whether processing the job again could succeed. */
|
|
13
|
+
retryable;
|
|
14
|
+
/** Additional non-sensitive context about the failure. */
|
|
15
|
+
metadata;
|
|
16
|
+
/**
|
|
17
|
+
* @param message - Human readable description of the failure.
|
|
18
|
+
* @param operation - Queue operation that failed.
|
|
19
|
+
* @param cause - Underlying error, when there is one.
|
|
20
|
+
* @param metadata - Additional non-sensitive context.
|
|
21
|
+
* @param retryable - Whether processing the job again could succeed, defaults to true.
|
|
22
|
+
*/
|
|
23
|
+
constructor(message, operation, cause, metadata, retryable = true) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "QueueError";
|
|
26
|
+
this.operation = operation;
|
|
27
|
+
this.cause = cause;
|
|
28
|
+
this.metadata = metadata;
|
|
29
|
+
this.retryable = retryable;
|
|
30
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, QueueError);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/Utils/Job.ts
|
|
35
|
+
/**
|
|
36
|
+
* Job name a handler registers under to receive every job of its queue that no
|
|
37
|
+
* other handler claims.
|
|
38
|
+
*/
|
|
39
|
+
const WILDCARD_JOB = "*";
|
|
40
|
+
/** Header carrying the job name across transports that have no native notion of one. */
|
|
41
|
+
const JOB_HEADER = "x-job";
|
|
42
|
+
/** Header carrying the current attempt number. */
|
|
43
|
+
const ATTEMPT_HEADER = "x-attempt";
|
|
44
|
+
/** Header carrying the total number of attempts the publisher asked for. */
|
|
45
|
+
const ATTEMPTS_HEADER = "x-attempts";
|
|
46
|
+
/** Header carrying the partition or routing key a job was published with. */
|
|
47
|
+
const KEY_HEADER = "x-key";
|
|
48
|
+
/** Header carrying the priority a job was published with. */
|
|
49
|
+
const PRIORITY_HEADER = "x-priority";
|
|
50
|
+
/**
|
|
51
|
+
* Most attempts a job may ever take.
|
|
52
|
+
*
|
|
53
|
+
* On transports that do not retry natively the budget is read off the wire, so a
|
|
54
|
+
* producer could otherwise ask for an arbitrarily large one and turn a single
|
|
55
|
+
* poison message into an unbounded republish loop.
|
|
56
|
+
*/
|
|
57
|
+
const MAX_ATTEMPTS = 50;
|
|
58
|
+
/**
|
|
59
|
+
* Longest a retry may be held back, one day.
|
|
60
|
+
*
|
|
61
|
+
* An exponential backoff over a large attempt count overflows to `Infinity`,
|
|
62
|
+
* which `setTimeout` clamps to one millisecond: the backoff meant to slow
|
|
63
|
+
* retries down would make them as fast as the runtime allows.
|
|
64
|
+
*/
|
|
65
|
+
const MAX_BACKOFF_MS = 864e5;
|
|
66
|
+
/**
|
|
67
|
+
* Reads a positive integer from a raw header value.
|
|
68
|
+
*
|
|
69
|
+
* @param raw - Header value as received from the transport, in any shape.
|
|
70
|
+
* @param fallback - Value returned when the header is absent or unusable.
|
|
71
|
+
* @param max - Largest value accepted, so a value off the wire cannot be unbounded.
|
|
72
|
+
* @returns The parsed integer, or the fallback.
|
|
73
|
+
*/
|
|
74
|
+
function readNumericHeader(raw, fallback, max = Number.MAX_SAFE_INTEGER) {
|
|
75
|
+
if (raw === null || raw === void 0) return fallback;
|
|
76
|
+
const value = Number(typeof raw === "object" ? String(raw) : raw);
|
|
77
|
+
return Number.isFinite(value) && value > 0 ? Math.min(Math.floor(value), max) : fallback;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Normalizes transport headers into plain strings, so handlers never have to deal
|
|
81
|
+
* with buffers or numbers coming from the wire.
|
|
82
|
+
*
|
|
83
|
+
* @param headers - Raw headers as received from the transport.
|
|
84
|
+
* @returns Headers with string values only.
|
|
85
|
+
*/
|
|
86
|
+
function normalizeHeaders(headers) {
|
|
87
|
+
if (!headers) return {};
|
|
88
|
+
const normalized = {};
|
|
89
|
+
for (const [key, value] of Object.entries(headers)) if (value !== null && value !== void 0) normalized[key] = String(value);
|
|
90
|
+
return normalized;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Computes how long to wait before the next attempt of a job.
|
|
94
|
+
*
|
|
95
|
+
* @param backoff - Backoff policy, a number being a fixed delay in milliseconds.
|
|
96
|
+
* @param attempt - Attempt that just failed, starting at 1.
|
|
97
|
+
* @returns Delay in milliseconds, zero when no backoff is configured and never above {@link MAX_BACKOFF_MS}.
|
|
98
|
+
*/
|
|
99
|
+
function resolveBackoff(backoff, attempt) {
|
|
100
|
+
if (!backoff) return 0;
|
|
101
|
+
if (typeof backoff === "number") return Math.min(Math.max(0, backoff), MAX_BACKOFF_MS);
|
|
102
|
+
const delay = Math.max(0, backoff.delay);
|
|
103
|
+
const resolved = backoff.type === "exponential" ? delay * 2 ** Math.max(0, attempt - 1) : delay;
|
|
104
|
+
return Math.min(resolved, MAX_BACKOFF_MS);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Serializes a payload for transports that only carry bytes.
|
|
108
|
+
*
|
|
109
|
+
* @param payload - Payload to serialize.
|
|
110
|
+
* @returns The payload as a UTF-8 JSON buffer.
|
|
111
|
+
* @throws {QueueError} When the payload cannot be serialized.
|
|
112
|
+
*/
|
|
113
|
+
function encodePayload(payload) {
|
|
114
|
+
try {
|
|
115
|
+
return Buffer.from(JSON.stringify(payload ?? null), "utf8");
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw new QueueError("Job payload is not serializable", "encode", error, void 0, false);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Deserializes a payload received from a transport that only carries bytes.
|
|
122
|
+
* Content that is not JSON is returned as text, so foreign producers do not
|
|
123
|
+
* break the consumer.
|
|
124
|
+
*
|
|
125
|
+
* @param content - Raw bytes received from the transport.
|
|
126
|
+
* @returns The parsed payload, or the raw text when it is not JSON.
|
|
127
|
+
*/
|
|
128
|
+
function decodePayload(content) {
|
|
129
|
+
if (content === null || content === void 0) return null;
|
|
130
|
+
const text = typeof content === "string" ? content : Buffer.from(content).toString("utf8");
|
|
131
|
+
if (text.length === 0) return null;
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(text);
|
|
134
|
+
} catch {
|
|
135
|
+
return text;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Generates an id for transports that do not assign one themselves.
|
|
140
|
+
*
|
|
141
|
+
* @returns A unique job id.
|
|
142
|
+
*/
|
|
143
|
+
function generateJobId() {
|
|
144
|
+
return globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Waits for the given number of milliseconds.
|
|
148
|
+
*
|
|
149
|
+
* @param ms - Milliseconds to wait, values below one resolve immediately.
|
|
150
|
+
* @returns Resolves once the delay elapsed.
|
|
151
|
+
*/
|
|
152
|
+
function delay(ms) {
|
|
153
|
+
if (ms <= 0) return Promise.resolve();
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
setTimeout(resolve, ms).unref?.();
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Drops the keys whose value is undefined.
|
|
160
|
+
*
|
|
161
|
+
* Options objects are built by spreading whatever the caller set, which leaves
|
|
162
|
+
* own properties holding `undefined` behind. A library that merges options with
|
|
163
|
+
* `Object.assign` cannot tell those apart from a deliberate value, so they have
|
|
164
|
+
* to go before the object is handed over.
|
|
165
|
+
*
|
|
166
|
+
* @param source - Object to clean up.
|
|
167
|
+
* @returns A copy without the undefined entries.
|
|
168
|
+
*/
|
|
169
|
+
function prune(source) {
|
|
170
|
+
const pruned = {};
|
|
171
|
+
for (const [key, value] of Object.entries(source)) if (value !== void 0) pruned[key] = value;
|
|
172
|
+
return pruned;
|
|
173
|
+
}
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/Services/QueueStrategy.ts
|
|
176
|
+
/**
|
|
177
|
+
* Base class every queue transport implements.
|
|
178
|
+
*
|
|
179
|
+
* A strategy owns the connection to a broker and translates between the broker's
|
|
180
|
+
* own vocabulary and the module's job model. It stays deliberately thin: routing
|
|
181
|
+
* jobs to handlers, retries, timeouts and metrics all live in the
|
|
182
|
+
* {@link QueueManager}, so every transport behaves the same way.
|
|
183
|
+
*
|
|
184
|
+
* @typeParam InitOptions - Options the strategy needs to connect. Use `undefined`
|
|
185
|
+
* for strategies that need none.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```ts
|
|
189
|
+
* export class LogStrategy extends QueueStrategy {
|
|
190
|
+
* public readonly transport = 'log';
|
|
191
|
+
*
|
|
192
|
+
* public initialize(): void {}
|
|
193
|
+
*
|
|
194
|
+
* public async publish(request: QueueTypes.PublishRequest): Promise<QueueTypes.JobRef> {
|
|
195
|
+
* console.log(request.queue, request.job, request.payload);
|
|
196
|
+
* return { id: '1', queue: request.queue, job: request.job, strategy: this.transport };
|
|
197
|
+
* }
|
|
198
|
+
*
|
|
199
|
+
* public async consume(): Promise<QueueTypes.ConsumerHandle> {
|
|
200
|
+
* throw new Error('This strategy only publishes');
|
|
201
|
+
* }
|
|
202
|
+
*
|
|
203
|
+
* public async close(): Promise<void> {}
|
|
204
|
+
* }
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
var QueueStrategy = class {
|
|
208
|
+
/**
|
|
209
|
+
* What the transport can do on its own. Anything reported as unsupported is
|
|
210
|
+
* either emulated by the manager or ignored.
|
|
211
|
+
*/
|
|
212
|
+
get capabilities() {
|
|
213
|
+
return {
|
|
214
|
+
retries: false,
|
|
215
|
+
delay: false,
|
|
216
|
+
priority: false,
|
|
217
|
+
progress: false,
|
|
218
|
+
stats: false,
|
|
219
|
+
peek: false
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Publishes many jobs of the same kind. The default implementation publishes
|
|
224
|
+
* them one by one, transports with a batch API should override it.
|
|
225
|
+
*
|
|
226
|
+
* @param requests - Jobs to publish, all targeting the same queue.
|
|
227
|
+
* @returns References to the published jobs, in the same order.
|
|
228
|
+
* @throws {QueueError} When the jobs cannot be published.
|
|
229
|
+
*/
|
|
230
|
+
async publishMany(requests) {
|
|
231
|
+
const refs = [];
|
|
232
|
+
for (const request of requests) refs.push(await this.publish(request));
|
|
233
|
+
return refs;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
//#endregion
|
|
237
|
+
export { resolveBackoff as _, KEY_HEADER as a, PRIORITY_HEADER as c, delay as d, encodePayload as f, readNumericHeader as g, prune as h, JOB_HEADER as i, WILDCARD_JOB as l, normalizeHeaders as m, ATTEMPTS_HEADER as n, MAX_ATTEMPTS as o, generateJobId as p, ATTEMPT_HEADER as r, MAX_BACKOFF_MS as s, QueueStrategy as t, decodePayload as u, QueueError as v };
|