@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.
@@ -0,0 +1,305 @@
1
+ import { g as readNumericHeader, p as generateJobId, r as ATTEMPT_HEADER, t as QueueStrategy } from "../QueueStrategy-DwLPpfFM.mjs";
2
+ //#region src/Strategies/MemoryStrategy.ts
3
+ /**
4
+ * In-process queue implementation.
5
+ *
6
+ * Jobs never leave the running process, which makes this the strategy to use in
7
+ * tests, in examples and for background work that may be lost on restart. It is
8
+ * also the reference implementation: delays, priorities and progress all behave
9
+ * exactly as the module documents them.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * await queueManager.mount({ strategy: MemoryStrategy });
14
+ * ```
15
+ */
16
+ var MemoryStrategy = class extends QueueStrategy {
17
+ /** Transport this strategy talks to. */
18
+ transport = "memory";
19
+ /** Queues created so far, indexed by name */
20
+ fQueues = /* @__PURE__ */ new Map();
21
+ /** Monotonic counter keeping jobs of equal priority in publish order */
22
+ fSequence = 0;
23
+ /** Callbacks waiting for a condition on the queues to hold */
24
+ fWaiters = [];
25
+ /**
26
+ * Everything the module offers is supported, jobs simply live in memory.
27
+ *
28
+ * @returns {QueueTypes.Capabilities} What this strategy supports
29
+ */
30
+ get capabilities() {
31
+ return {
32
+ retries: false,
33
+ delay: true,
34
+ priority: true,
35
+ progress: true,
36
+ stats: true,
37
+ peek: true
38
+ };
39
+ }
40
+ /**
41
+ * Nothing to connect to.
42
+ *
43
+ * @returns {void}
44
+ */
45
+ initialize() {}
46
+ /**
47
+ * Adds a job to an in-memory queue, honouring its delay and priority.
48
+ *
49
+ * @param {QueueTypes.PublishRequest} request - Job to publish
50
+ * @returns {Promise<QueueTypes.JobRef>} Reference to the published job
51
+ */
52
+ async publish(request) {
53
+ const queue = this.queueFor(request.queue);
54
+ const job = {
55
+ id: request.options.jobId ?? generateJobId(),
56
+ job: request.job,
57
+ payload: request.payload,
58
+ headers: request.headers,
59
+ attempt: readNumericHeader(request.headers[ATTEMPT_HEADER], 1),
60
+ priority: request.options.priority ?? 0,
61
+ sequence: this.fSequence++
62
+ };
63
+ const delay = request.options.delay ?? 0;
64
+ if (delay > 0) {
65
+ const timer = setTimeout(() => {
66
+ queue.delayed.delete(timer);
67
+ queue.waiting.push(job);
68
+ this.pump(request.queue);
69
+ }, delay);
70
+ timer.unref?.();
71
+ queue.delayed.set(timer, {
72
+ job,
73
+ at: Date.now() + delay
74
+ });
75
+ } else {
76
+ queue.waiting.push(job);
77
+ this.pump(request.queue);
78
+ }
79
+ return {
80
+ id: job.id,
81
+ queue: request.queue,
82
+ job: request.job,
83
+ strategy: this.transport
84
+ };
85
+ }
86
+ /**
87
+ * Starts processing a queue. Only one consumer per queue is kept, a second
88
+ * call replaces the first.
89
+ *
90
+ * @param {QueueTypes.ConsumeRequest} request - Queue to consume, its concurrency and the dispatch callback
91
+ * @returns {Promise<QueueTypes.ConsumerHandle>} Handle used to stop the consumer again
92
+ */
93
+ async consume(request) {
94
+ const queue = this.queueFor(request.queue);
95
+ queue.consumer = {
96
+ concurrency: Math.max(1, request.concurrency),
97
+ dispatch: request.dispatch
98
+ };
99
+ this.pump(request.queue);
100
+ return {
101
+ queue: request.queue,
102
+ stop: async () => {
103
+ queue.consumer = void 0;
104
+ await this.waitFor(() => queue.active === 0);
105
+ }
106
+ };
107
+ }
108
+ /**
109
+ * Reads the counters of a queue.
110
+ *
111
+ * @param {string} name - Queue to read
112
+ * @returns {Promise<QueueTypes.QueueStats>} Counters of that queue
113
+ */
114
+ async stats(name) {
115
+ const queue = this.fQueues.get(name);
116
+ return {
117
+ waiting: queue?.waiting.length ?? 0,
118
+ active: queue?.active ?? 0,
119
+ completed: queue?.completed ?? 0,
120
+ failed: queue?.failed ?? 0,
121
+ delayed: queue?.delayed.size ?? 0
122
+ };
123
+ }
124
+ /**
125
+ * Shows what a queue is holding: the jobs waiting to run and the ones still
126
+ * waiting for their delay.
127
+ *
128
+ * @param {QueueTypes.PeekRequest} request - Queue to look at, how many messages and which states
129
+ * @returns {Promise<QueueTypes.PeekedMessage[]>} The messages found, in the order they would run
130
+ */
131
+ async peek(request) {
132
+ const queue = this.fQueues.get(request.queue);
133
+ if (!queue) return [];
134
+ const messages = [];
135
+ if (request.states.includes("waiting")) for (const job of [...queue.waiting].sort((a, b) => a.priority - b.priority || a.sequence - b.sequence)) messages.push(this.describe(job, "waiting"));
136
+ if (request.states.includes("delayed")) for (const { job, at } of queue.delayed.values()) messages.push({
137
+ ...this.describe(job, "delayed"),
138
+ availableAt: at
139
+ });
140
+ return messages.slice(0, request.limit);
141
+ }
142
+ /**
143
+ * Drops every queue, pending job and delay timer.
144
+ *
145
+ * @returns {Promise<void>} Resolves once everything is dropped
146
+ */
147
+ async close() {
148
+ for (const queue of this.fQueues.values()) {
149
+ for (const timer of queue.delayed.keys()) clearTimeout(timer);
150
+ queue.delayed.clear();
151
+ queue.waiting.length = 0;
152
+ queue.consumer = void 0;
153
+ }
154
+ this.fQueues.clear();
155
+ this.notify();
156
+ }
157
+ /**
158
+ * Waits until no job is waiting, delayed or running.
159
+ * Handy in tests, where publishing and processing happen in the same process.
160
+ *
161
+ * @returns {Promise<void>} Resolves once every queue ran dry
162
+ */
163
+ async idle() {
164
+ return this.waitFor(() => this.isIdle());
165
+ }
166
+ /**
167
+ * Hands as many waiting jobs to the consumer as its concurrency allows.
168
+ *
169
+ * @param {string} name - Queue to pump
170
+ * @returns {void}
171
+ */
172
+ pump(name) {
173
+ const queue = this.fQueues.get(name);
174
+ if (!queue?.consumer) return;
175
+ while (queue.consumer && queue.active < queue.consumer.concurrency && queue.waiting.length > 0) {
176
+ const job = this.takeNext(queue);
177
+ if (!job) break;
178
+ queue.active++;
179
+ this.run(name, queue, job);
180
+ }
181
+ this.notify();
182
+ }
183
+ /**
184
+ * Runs a single job and keeps the queue moving afterwards.
185
+ *
186
+ * A rejected dispatch means the job failed for good: the manager has already
187
+ * applied the retry policy, so nothing is put back on the queue.
188
+ *
189
+ * @param {string} name - Queue the job belongs to
190
+ * @param {MemoryQueue} queue - State of that queue
191
+ * @param {MemoryJob} job - Job to run
192
+ * @returns {Promise<void>} Resolves once the job settled
193
+ */
194
+ async run(name, queue, job) {
195
+ try {
196
+ await queue.consumer?.dispatch({
197
+ id: job.id,
198
+ job: job.job,
199
+ payload: job.payload,
200
+ headers: job.headers,
201
+ attempt: job.attempt,
202
+ raw: job,
203
+ updateProgress: (progress) => {
204
+ job.progress = progress;
205
+ }
206
+ });
207
+ queue.completed++;
208
+ } catch {
209
+ queue.failed++;
210
+ } finally {
211
+ queue.active--;
212
+ this.pump(name);
213
+ this.notify();
214
+ }
215
+ }
216
+ /**
217
+ * Describes a job the way the module reads a peeked message.
218
+ *
219
+ * @param {MemoryJob} job - Job sitting on the queue
220
+ * @param {QueueTypes.PeekState} state - Where it is sitting
221
+ * @returns {QueueTypes.PeekedMessage} The message, ready to be inspected
222
+ */
223
+ describe(job, state) {
224
+ return {
225
+ id: job.id,
226
+ job: job.job,
227
+ state,
228
+ attempt: job.attempt,
229
+ payload: job.payload,
230
+ headers: job.headers
231
+ };
232
+ }
233
+ /**
234
+ * Picks the job to run next: lowest priority value first, publish order otherwise.
235
+ *
236
+ * @param {MemoryQueue} queue - Queue to take from
237
+ * @returns {MemoryJob | undefined} The next job, or undefined when the queue is empty
238
+ */
239
+ takeNext(queue) {
240
+ if (queue.waiting.length === 0) return;
241
+ let index = 0;
242
+ for (let i = 1; i < queue.waiting.length; i++) {
243
+ const candidate = queue.waiting[i];
244
+ const best = queue.waiting[index];
245
+ if (candidate.priority < best.priority || candidate.priority === best.priority && candidate.sequence < best.sequence) index = i;
246
+ }
247
+ return queue.waiting.splice(index, 1)[0];
248
+ }
249
+ /**
250
+ * Returns the state of a queue, creating it on first use.
251
+ *
252
+ * @param {string} name - Queue name
253
+ * @returns {MemoryQueue} State of that queue
254
+ */
255
+ queueFor(name) {
256
+ let queue = this.fQueues.get(name);
257
+ if (!queue) {
258
+ queue = {
259
+ waiting: [],
260
+ delayed: /* @__PURE__ */ new Map(),
261
+ active: 0,
262
+ completed: 0,
263
+ failed: 0
264
+ };
265
+ this.fQueues.set(name, queue);
266
+ }
267
+ return queue;
268
+ }
269
+ /**
270
+ * @returns {boolean} True when no job is waiting, delayed or running
271
+ */
272
+ isIdle() {
273
+ for (const queue of this.fQueues.values()) if (queue.active > 0 || queue.waiting.length > 0 || queue.delayed.size > 0) return false;
274
+ return true;
275
+ }
276
+ /**
277
+ * Waits until a condition on the queues holds.
278
+ *
279
+ * @param {() => boolean} ready - Condition to wait for, checked after every job settles
280
+ * @returns {Promise<void>} Resolves once the condition holds
281
+ */
282
+ waitFor(ready) {
283
+ if (ready()) return Promise.resolve();
284
+ return new Promise((resolve) => {
285
+ this.fWaiters.push({
286
+ ready,
287
+ resolve
288
+ });
289
+ });
290
+ }
291
+ /**
292
+ * Wakes everyone whose condition now holds.
293
+ *
294
+ * @returns {void}
295
+ */
296
+ notify() {
297
+ if (this.fWaiters.length === 0) return;
298
+ const pending = [];
299
+ for (const waiter of this.fWaiters) if (waiter.ready()) waiter.resolve();
300
+ else pending.push(waiter);
301
+ this.fWaiters = pending;
302
+ }
303
+ };
304
+ //#endregion
305
+ export { MemoryStrategy };
@@ -0,0 +1,205 @@
1
+ import { n as QueueTypes, t as QueueStrategy } from "../QueueStrategy-saWSBeU6.mjs";
2
+ import { Options, RecoveryOptions, SocketOptions } from "amqplib";
3
+ //#region src/Strategies/RabbitMQStrategy.d.ts
4
+ /** Options the RabbitMQ strategy connects with. */
5
+ export interface RabbitMQStrategyOptions {
6
+ /** Broker to connect to, as an `amqp://` URL or as connection fields. */
7
+ url: string | Options.Connect;
8
+ /** Socket options handed to amqplib. */
9
+ socketOptions?: SocketOptions;
10
+ /**
11
+ * Automatic reconnection, on by default. Pass options to tune the backoff,
12
+ * or false to fail fast instead.
13
+ * @default true
14
+ */
15
+ recovery?: RecoveryOptions | boolean;
16
+ /**
17
+ * Options every queue is asserted with.
18
+ * @default { durable: true }
19
+ */
20
+ queueOptions?: Options.AssertQueue;
21
+ /**
22
+ * Options every message is published with.
23
+ * @default { persistent: true }
24
+ */
25
+ publishOptions?: Options.Publish;
26
+ /**
27
+ * Number of unacknowledged messages a consumer may hold. Defaults to the
28
+ * concurrency the consumer was started with.
29
+ */
30
+ prefetch?: number;
31
+ }
32
+ /**
33
+ * RabbitMQ backed queue implementation.
34
+ *
35
+ * Jobs are plain AMQP messages: the job name travels in the message `type`
36
+ * property, the module's bookkeeping in the headers, and the payload as JSON.
37
+ * RabbitMQ has no notion of attempts or delays, so the manager owns them - which
38
+ * also means a job that has run out of attempts is nacked without requeue, and
39
+ * ends up wherever the queue's dead letter exchange points.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * await queueManager.mount({
44
+ * strategy: RabbitMQStrategy,
45
+ * initOptions: { url: 'amqp://localhost' },
46
+ * });
47
+ * ```
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * // a queue that dead-letters what it cannot process
52
+ * await queueManager.mount({
53
+ * strategy: RabbitMQStrategy,
54
+ * initOptions: {
55
+ * url: 'amqp://localhost',
56
+ * queueOptions: { durable: true, deadLetterExchange: 'failed' },
57
+ * },
58
+ * });
59
+ * ```
60
+ */
61
+ export declare class RabbitMQStrategy extends QueueStrategy<RabbitMQStrategyOptions> {
62
+ /** Transport this strategy talks to. */
63
+ readonly transport: string;
64
+ /** Logger instance */
65
+ private gLogger;
66
+ /** Options the strategy was initialized with */
67
+ private fOptions;
68
+ /** The connection, recovering on its own unless recovery was turned off */
69
+ private fConnection;
70
+ /** Channel every publish goes through */
71
+ private fPublishChannel;
72
+ /** Running consumers, indexed by queue name */
73
+ private fConsumers;
74
+ /**
75
+ * Queues the application asked to consume, indexed by queue name.
76
+ *
77
+ * Kept apart from {@link RabbitMQStrategy.fConsumers}, which holds the live
78
+ * channels: those belong to one connection and are thrown away when it is
79
+ * replaced, while what should be consumed does not change with a reconnect.
80
+ */
81
+ private fRequests;
82
+ /**
83
+ * Bumped on every connection. A consumer that finishes starting after its
84
+ * connection was replaced belongs to a dead one and is discarded.
85
+ */
86
+ private fGeneration;
87
+ /** Queues already asserted on the current connection */
88
+ private fAsserted;
89
+ /**
90
+ * RabbitMQ carries priorities and message counts, while attempts and delays
91
+ * are left to the manager.
92
+ *
93
+ * @returns {QueueTypes.Capabilities} What this strategy supports
94
+ */
95
+ get capabilities(): QueueTypes.Capabilities;
96
+ /**
97
+ * Opens the connection to the broker.
98
+ *
99
+ * @param {RabbitMQStrategyOptions} options - Broker to connect to and the defaults to use
100
+ * @returns {Promise<void>} Resolves once the connection is open
101
+ * @throws {QueueError} When no broker is given, or the connection cannot be opened
102
+ */
103
+ initialize(options: RabbitMQStrategyOptions): Promise<void>;
104
+ /**
105
+ * Sends a job to a queue.
106
+ *
107
+ * @param {QueueTypes.PublishRequest} request - Job to publish
108
+ * @returns {Promise<QueueTypes.JobRef>} Reference to the published job
109
+ * @throws {QueueError} When the job cannot be sent
110
+ */
111
+ publish(request: QueueTypes.PublishRequest): Promise<QueueTypes.JobRef>;
112
+ /**
113
+ * Starts consuming a queue on its own channel, so its prefetch is its own.
114
+ *
115
+ * A job whose dispatch rejects is nacked without requeue: the manager has
116
+ * already decided it is not worth another attempt.
117
+ *
118
+ * @param {QueueTypes.ConsumeRequest} request - Queue to consume, its concurrency and the dispatch callback
119
+ * @returns {Promise<QueueTypes.ConsumerHandle>} Handle used to stop the consumer again
120
+ * @throws {QueueError} When the consumer cannot be started
121
+ */
122
+ consume(request: QueueTypes.ConsumeRequest): Promise<QueueTypes.ConsumerHandle>;
123
+ /**
124
+ * Opens a channel for a queue and starts delivering its messages.
125
+ *
126
+ * @param {QueueTypes.ConsumeRequest} request - Queue to consume, its concurrency and the dispatch callback
127
+ * @param {number} generation - Connection this consumer is being started for
128
+ * @returns {Promise<boolean>} Whether the consumer was installed, false when it was no longer wanted
129
+ * @throws {Error} When the channel or the consumer cannot be created
130
+ */
131
+ private startConsumer;
132
+ /**
133
+ * Starts a consumer again on a recovered connection.
134
+ *
135
+ * Failing here is reported rather than thrown: nothing is waiting on a
136
+ * recovery, and a queue that cannot be consumed again has to be visible.
137
+ *
138
+ * @param {QueueTypes.ConsumeRequest} request - What the consumer was started with
139
+ * @param {number} generation - Connection being recovered onto
140
+ * @returns {Promise<void>} Resolves once the consumer is running again, or once the failure was reported
141
+ */
142
+ private resume;
143
+ /**
144
+ * Reads how many messages are waiting on a queue.
145
+ *
146
+ * @param {string} queue - Queue to read
147
+ * @returns {Promise<QueueTypes.QueueStats>} Counters of that queue
148
+ * @throws {QueueError} When the queue cannot be inspected
149
+ */
150
+ stats(queue: string): Promise<QueueTypes.QueueStats>;
151
+ /**
152
+ * Stops every consumer and closes the connection.
153
+ *
154
+ * @returns {Promise<void>} Resolves once the connection is closed
155
+ */
156
+ close(): Promise<void>;
157
+ /**
158
+ * Runs a single delivery and acknowledges it according to the outcome.
159
+ *
160
+ * @param {QueueTypes.ConsumeRequest} request - The consumer the message belongs to
161
+ * @param {ConsumeMessage} message - The delivery
162
+ * @returns {Promise<void>} Resolves once the message has been settled
163
+ */
164
+ private handle;
165
+ /**
166
+ * Cancels a consumer, waits for its in-flight messages and closes its channel.
167
+ *
168
+ * @param {string} queue - Queue whose consumer is stopped
169
+ * @returns {Promise<void>} Resolves once the channel is closed
170
+ */
171
+ private stopConsumer;
172
+ /**
173
+ * Returns the channel every publish goes through, opening it on first use and
174
+ * after a reconnection.
175
+ *
176
+ * @returns {Promise<Channel>} The publish channel
177
+ * @throws {QueueError} When the strategy is not connected
178
+ */
179
+ private publishChannel;
180
+ /**
181
+ * Declares a queue once per connection.
182
+ *
183
+ * @param {Channel} channel - Channel to declare it on
184
+ * @param {string} queue - Queue to declare
185
+ * @returns {Promise<void>} Resolves once the queue exists
186
+ */
187
+ private assertQueue;
188
+ /**
189
+ * Returns the options the strategy was initialized with.
190
+ *
191
+ * @param {string} operation - Operation asking for them, reported in the error
192
+ * @returns {RabbitMQStrategyOptions} The options
193
+ * @throws {QueueError} When the strategy has not been initialized
194
+ */
195
+ private requireOptions;
196
+ /**
197
+ * Returns the open connection.
198
+ *
199
+ * @param {string} operation - Operation asking for it, reported in the error
200
+ * @returns {ChannelModel | RecoveringChannelModel} The connection
201
+ * @throws {QueueError} When the strategy is not connected
202
+ */
203
+ private requireConnection;
204
+ }
205
+ //#endregion