@prsm/queue 1.0.0 → 1.0.2

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/README.md CHANGED
@@ -1,101 +1,145 @@
1
- # Queues
1
+ # @prsm/queue
2
2
 
3
- You must place your queue definitions in `/queues`. For example:
3
+ Redis-backed distributed task queue with grouped concurrency, retries, and rate limiting.
4
+
5
+ ## Installation
4
6
 
5
7
  ```bash
6
- src
7
- └── app
8
- └── queues
9
- ├── mailer.ts
10
- └── llm-processor.ts
8
+ npm install @prsm/queue
11
9
  ```
12
10
 
13
- Each queue module is expected to have a default export that is an asynchronous function, which serves as the handler for each queued job. Additionally, a `queue` object must be exported that generates and configures the queue.
11
+ ## Quick Start
14
12
 
15
- Here's an example queue definition.
13
+ ```ts
14
+ import Queue from '@prsm/queue'
16
15
 
17
- ```typescript
18
- // /src/app/queues/mail.ts
19
- import Queue from "@prsm/queues";
16
+ const queue = new Queue({
17
+ concurrency: 2,
18
+ maxRetries: 3
19
+ })
20
20
 
21
- interface MailPayload {
22
- recipient: string;
23
- subject: string;
24
- body: string;
25
- }
21
+ queue.process(async (payload) => {
22
+ return await doWork(payload)
23
+ })
26
24
 
27
- // The task to be executed for each queued job when a worker becomes available.
28
- export default async function ({ recipient, subject, body }: MailPayload) {
29
- // send an email
30
- }
25
+ queue.on('complete', ({ task, result }) => {
26
+ console.log('Done:', task.uuid, result)
27
+ })
31
28
 
32
- export const queue = new Queue<MailPayload>({
33
- // Number of concurrent queue workers.
34
- concurrency: 1,
29
+ queue.on('failed', ({ task, error }) => {
30
+ console.log('Failed after retries:', task.uuid, error.message)
31
+ })
35
32
 
36
- // Once a worker becomes available, it will wait delay ms before processing the next job.
37
- delay: 0,
33
+ await queue.push({ userId: 123, action: 'sync' })
34
+ ```
35
+
36
+ ## Options
38
37
 
39
- // After it is initiated, a task will fail if it does not resolve within timeout ms.
40
- timeout: 0,
38
+ ```ts
39
+ const queue = new Queue({
40
+ concurrency: 2, // worker count
41
+ delay: '100ms', // pause between tasks (string or ms)
42
+ timeout: '30s', // max task duration
43
+ maxRetries: 3, // attempts before failing
41
44
 
42
- // Groups enable scoped queues based on a key, such as a recipient's address.
43
- // This can be useful, for instance, if we want to restrict the number of emails we send to a particular
44
- // recipient during a specific time period.
45
- //
46
- // By default, a group will inherit the queue's config above, but you can
47
- // override that config here.
48
45
  groups: {
49
- concurrency: 3,
50
- delay: "5s",
51
- timeout: "1m"
46
+ concurrency: 1, // workers per group
47
+ delay: '50ms',
48
+ timeout: '10s',
49
+ maxRetries: 3
52
50
  },
53
- });
54
- ```
55
51
 
56
- ## Adding jobs to a queue
52
+ redisOptions: {
53
+ host: 'localhost',
54
+ port: 6379
55
+ }
56
+ })
57
+ ```
57
58
 
58
- To add a job to the queue, import the queue and call `queue.push`.
59
+ ## Process Handler
59
60
 
60
- ```typescript
61
- import { queue as mailQueue } from "../queues/mail";
62
- mailQueue.push({ recipient, subject, body });
61
+ ```ts
62
+ queue.process(async (payload, task) => {
63
+ console.log('Task:', task.uuid, 'Attempt:', task.attempts)
64
+ return await someWork(payload)
65
+ })
63
66
  ```
64
67
 
65
- To track task status, listen for events emitted by the queue. The queue will emit events when:
68
+ Throw an error to trigger retry. After `maxRetries`, the task fails permanently.
66
69
 
67
- 1. A job is added to the queue
68
- 2. A job is completed
69
- 3. A job fails
70
+ ## Grouped Queues
70
71
 
71
- ```typescript
72
- // /src/app/http/mail/index.ts
73
- import { queue as mailQueue } from "../queues/mail";
72
+ Isolated concurrency per key - perfect for per-tenant rate limiting.
74
73
 
75
- const onNew = ({ task }) => console.log("Task created:", task.uuid);
76
- const onFailed = ({ task }) => console.log("Task failed:", task.uuid);
77
- const onComplete = ({ task }) => console.log("Task complete:", task.uuid);
74
+ ```ts
75
+ const queue = new Queue({
76
+ groups: { concurrency: 1, delay: '50ms' }
77
+ })
78
78
 
79
- mailQueue.on("new", onNew);
80
- mailQueue.on("failed", onFailed);
81
- mailQueue.on("complete", onComplete);
79
+ queue.process(async (payload) => {
80
+ return await callExternalAPI(payload)
81
+ })
82
82
 
83
- // POST /mail
84
- export async function post(c: Context, { body: { recipient, subject, body } }) {
85
- const uuid = mailQueue.push({ recipient, subject, body });
86
- return Respond.OK(c, { uuid });
87
- }
83
+ await queue.group('tenant-123').push({ action: 'sync' })
84
+ await queue.group('tenant-456').push({ action: 'sync' })
85
+ ```
86
+
87
+ Each tenant processes independently. One slow tenant won't block others.
88
+
89
+ ## Events
90
+
91
+ ```ts
92
+ queue.on('new', ({ task }) => {})
93
+ queue.on('complete', ({ task, result }) => {})
94
+ queue.on('retry', ({ task, error, attempt }) => {})
95
+ queue.on('failed', ({ task, error }) => {})
96
+ ```
97
+
98
+ ## Task Object
88
99
 
89
- // /src/app/queues/mail.ts
90
- export default async function({ recipient, subject, body }) {
91
- // send a very important email
100
+ ```ts
101
+ interface Task<T> {
102
+ uuid: string
103
+ payload: T
104
+ createdAt: number
105
+ groupKey?: string
106
+ attempts: number
92
107
  }
93
108
  ```
94
109
 
95
- You can add a grouped queue like:
110
+ ## Rate Limiting Example
111
+
112
+ 20 LLM calls/sec per tenant:
113
+
114
+ ```ts
115
+ const queue = new Queue({
116
+ groups: { concurrency: 20, delay: '50ms' },
117
+ maxRetries: 3
118
+ })
119
+
120
+ queue.process(async ({ prompt }) => {
121
+ return await llm.complete(prompt)
122
+ })
123
+
124
+ app.post('/api/generate', async (req, res) => {
125
+ const { tenantId, prompt } = req.body
126
+ const taskId = await queue.group(tenantId).push({ prompt })
127
+ res.json({ queued: true, taskId })
128
+ })
129
+ ```
130
+
131
+ ## Horizontal Scaling
132
+
133
+ Multiple servers can push to the same queue. Redis coordinates via atomic operations - no duplicate processing.
96
134
 
97
- ```typescript
98
- mailQueue.group(recipient).push({ recipient, subject, body });
135
+ Note: events are local. Only the server that processes a task emits `complete`/`failed`. Use Redis pub/sub or WebSockets to broadcast results across servers.
136
+
137
+ ## Cleanup
138
+
139
+ ```ts
140
+ await queue.close()
99
141
  ```
100
142
 
101
- As mentioned above, this is a scoped queue. The scoped queue has its own concurrency, delay, and timeout.
143
+ ## License
144
+
145
+ MIT
package/dist/index.cjs CHANGED
@@ -41,11 +41,15 @@ var import_crypto = require("crypto");
41
41
  var import_ms = __toESM(require("@prsm/ms"), 1);
42
42
  var Queue = class extends import_events.EventEmitter {
43
43
  redis;
44
+ workerClients = [];
44
45
  options;
45
46
  handler;
46
47
  workers = /* @__PURE__ */ new Map();
47
48
  groupWorkers = /* @__PURE__ */ new Map();
48
49
  cleanupTimer;
50
+ _ready;
51
+ _inFlight = 0;
52
+ _totalSettled = 0;
49
53
  constructor(options = {}) {
50
54
  super();
51
55
  this.options = {
@@ -65,11 +69,20 @@ var Queue = class extends import_events.EventEmitter {
65
69
  this.redis = (0, import_redis.createClient)(this.options.redisOptions);
66
70
  this.redis.on("error", () => {
67
71
  });
68
- this.initializeRedis();
72
+ this._ready = this.initializeRedis();
69
73
  }
74
+ /** resolves when redis is connected and all workers are ready to accept tasks */
75
+ ready() {
76
+ return this._ready;
77
+ }
78
+ get inFlight() {
79
+ return this._inFlight;
80
+ }
81
+ /** register the handler that processes each task. only one handler per queue. */
70
82
  process(handler) {
71
83
  this.handler = handler;
72
84
  }
85
+ /** push a task to the main queue. returns the task uuid. */
73
86
  async push(payload) {
74
87
  const task = {
75
88
  uuid: (0, import_crypto.randomUUID)(),
@@ -81,6 +94,7 @@ var Queue = class extends import_events.EventEmitter {
81
94
  this.emit("new", { task });
82
95
  return task.uuid;
83
96
  }
97
+ /** returns a scoped pusher for a named group. each group gets its own worker pool, spun up on first push. */
84
98
  group(key) {
85
99
  return {
86
100
  push: async (payload) => {
@@ -96,63 +110,60 @@ var Queue = class extends import_events.EventEmitter {
96
110
  if (!this.groupWorkers.has(key)) {
97
111
  this.groupWorkers.set(key, /* @__PURE__ */ new Map());
98
112
  await this.startGroupWorkers(key);
99
- await new Promise((resolve) => setTimeout(resolve, 50));
100
113
  }
101
114
  return task.uuid;
102
115
  }
103
116
  };
104
117
  }
118
+ async createWorkerClient() {
119
+ const client = this.redis.duplicate();
120
+ client.on("error", () => {
121
+ });
122
+ await client.connect();
123
+ this.workerClients.push(client);
124
+ return client;
125
+ }
105
126
  async startWorkers() {
127
+ const ready = [];
106
128
  for (let i = 0; i < this.options.concurrency; i++) {
107
- this.startWorker(`worker-${i}`);
129
+ ready.push(this.startWorker(`worker-${i}`));
108
130
  }
131
+ await Promise.all(ready);
109
132
  }
110
133
  async startGroupWorkers(groupKey) {
111
134
  const groupWorkers = this.groupWorkers.get(groupKey);
135
+ const ready = [];
112
136
  for (let i = 0; i < this.options.groups.concurrency; i++) {
113
137
  const workerId = `group-${groupKey}-worker-${i}`;
114
138
  groupWorkers.set(workerId, true);
115
- this.startGroupWorker(workerId, groupKey);
139
+ ready.push(this.startGroupWorker(workerId, groupKey));
116
140
  }
117
- await new Promise((resolve) => setTimeout(resolve, 100));
141
+ await Promise.all(ready);
118
142
  }
119
143
  async startWorker(workerId) {
120
144
  this.workers.set(workerId, true);
121
- while (this.workers.get(workerId)) {
122
- try {
123
- if (!this.redis.isOpen) {
124
- break;
125
- }
126
- const taskData = await this.redis.brPop("queue:tasks", 1);
127
- if (taskData) {
128
- const task = JSON.parse(taskData.element);
129
- await this.processTask(task);
130
- }
131
- if (this.options.delay > 0) {
132
- await new Promise((resolve) => setTimeout(resolve, this.options.delay));
133
- }
134
- } catch (err) {
135
- const error = err;
136
- if (error.message?.includes("closed") || error.message?.includes("ClientClosedError")) {
137
- break;
138
- }
139
- }
140
- }
145
+ const client = await this.createWorkerClient();
146
+ this.runWorkerLoop(workerId, client, "queue:tasks", this.workers, (task) => this.processTask(task));
141
147
  }
142
148
  async startGroupWorker(workerId, groupKey) {
143
149
  const groupWorkers = this.groupWorkers.get(groupKey);
144
- while (groupWorkers.get(workerId)) {
150
+ const client = await this.createWorkerClient();
151
+ this.runWorkerLoop(workerId, client, `queue:groups:${groupKey}`, groupWorkers, (task) => this.processGroupTask(task));
152
+ }
153
+ async runWorkerLoop(workerId, client, key, activeMap, processFn) {
154
+ const isGrouped = key.startsWith("queue:groups:");
155
+ const delay = isGrouped ? this.options.groups.delay : this.options.delay;
156
+ while (activeMap.get(workerId)) {
145
157
  try {
146
- if (!this.redis.isOpen) {
147
- break;
148
- }
149
- const taskData = await this.redis.brPop(`queue:groups:${groupKey}`, 1);
158
+ if (!client.isOpen) break;
159
+ const taskData = await client.brPop(key, 1);
150
160
  if (taskData) {
151
161
  const task = JSON.parse(taskData.element);
152
- await this.processGroupTask(task);
162
+ this._inFlight++;
163
+ await processFn(task);
153
164
  }
154
- if (this.options.groups.delay > 0) {
155
- await new Promise((resolve) => setTimeout(resolve, this.options.groups.delay));
165
+ if (delay > 0) {
166
+ await new Promise((resolve) => setTimeout(resolve, delay));
156
167
  }
157
168
  } catch (err) {
158
169
  const error = err;
@@ -167,6 +178,7 @@ var Queue = class extends import_events.EventEmitter {
167
178
  try {
168
179
  if (!this.handler) {
169
180
  this.emit("complete", { task, result: void 0 });
181
+ this.settle();
170
182
  return;
171
183
  }
172
184
  const timeoutPromise = this.options.timeout > 0 ? new Promise(
@@ -175,12 +187,15 @@ var Queue = class extends import_events.EventEmitter {
175
187
  const workPromise = Promise.resolve(this.handler(task.payload, task));
176
188
  const result = timeoutPromise ? await Promise.race([workPromise, timeoutPromise]) : await workPromise;
177
189
  this.emit("complete", { task, result });
190
+ this.settle();
178
191
  } catch (error) {
179
192
  if (task.attempts < this.options.maxRetries) {
180
193
  this.emit("retry", { task, error, attempt: task.attempts });
194
+ this._inFlight--;
181
195
  await this.redis.lPush("queue:tasks", JSON.stringify(task));
182
196
  } else {
183
197
  this.emit("failed", { task, error });
198
+ this.settle();
184
199
  }
185
200
  }
186
201
  }
@@ -189,6 +204,7 @@ var Queue = class extends import_events.EventEmitter {
189
204
  try {
190
205
  if (!this.handler) {
191
206
  this.emit("complete", { task, result: void 0 });
207
+ this.settle();
192
208
  return;
193
209
  }
194
210
  const timeoutPromise = this.options.groups.timeout > 0 ? new Promise(
@@ -197,18 +213,28 @@ var Queue = class extends import_events.EventEmitter {
197
213
  const workPromise = Promise.resolve(this.handler(task.payload, task));
198
214
  const result = timeoutPromise ? await Promise.race([workPromise, timeoutPromise]) : await workPromise;
199
215
  this.emit("complete", { task, result });
216
+ this.settle();
200
217
  } catch (error) {
201
218
  if (task.attempts < this.options.groups.maxRetries) {
202
219
  this.emit("retry", { task, error, attempt: task.attempts });
220
+ this._inFlight--;
203
221
  await this.redis.lPush(`queue:groups:${task.groupKey}`, JSON.stringify(task));
204
222
  } else {
205
223
  this.emit("failed", { task, error });
224
+ this.settle();
206
225
  }
207
226
  }
208
227
  }
228
+ settle() {
229
+ this._inFlight--;
230
+ this._totalSettled++;
231
+ if (this._inFlight === 0 && this._totalSettled > 0) {
232
+ this.emit("drain");
233
+ }
234
+ }
209
235
  async initializeRedis() {
210
236
  await this.redis.connect();
211
- this.startWorkers();
237
+ await this.startWorkers();
212
238
  if (this.options.cleanupInterval > 0) {
213
239
  this.cleanupTimer = setInterval(() => {
214
240
  this.performPeriodicCleanup();
@@ -238,7 +264,10 @@ var Queue = class extends import_events.EventEmitter {
238
264
  } catch (error) {
239
265
  }
240
266
  }
267
+ /** shuts down all workers and disconnects from redis. waits for initialization to complete first. */
241
268
  async close() {
269
+ await this._ready.catch(() => {
270
+ });
242
271
  if (this.cleanupTimer) {
243
272
  clearInterval(this.cleanupTimer);
244
273
  }
@@ -247,6 +276,12 @@ var Queue = class extends import_events.EventEmitter {
247
276
  groupWorkers.clear();
248
277
  }
249
278
  this.groupWorkers.clear();
279
+ for (const client of this.workerClients) {
280
+ if (client.isOpen) {
281
+ await client.disconnect();
282
+ }
283
+ }
284
+ this.workerClients = [];
250
285
  if (this.redis.isOpen) {
251
286
  await this.redis.quit();
252
287
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/queue.ts"],"sourcesContent":["export { default } from './queue.js'\nexport type { QueueOptions, Task, TaskHandler } from './queue.js'\n","import { createClient, RedisClientType } from 'redis'\nimport { EventEmitter } from 'events'\nimport { randomUUID } from 'crypto'\nimport ms from '@prsm/ms'\n\nexport interface QueueOptions<T = any> {\n concurrency?: number\n delay?: number | string\n timeout?: number | string\n maxRetries?: number\n groups?: {\n concurrency?: number\n delay?: number | string\n timeout?: number | string\n maxRetries?: number\n }\n redisOptions?: {\n url?: string\n host?: string\n port?: number\n password?: string\n }\n cleanupInterval?: number\n}\n\nexport type TaskHandler<T, R = any> = (payload: T, task: Task<T>) => Promise<R> | R\n\nexport interface Task<T = any> {\n uuid: string\n payload: T\n createdAt: number\n groupKey?: string\n attempts: number\n}\n\n\nexport default class Queue<T = any, R = any> extends EventEmitter {\n private redis: RedisClientType\n private options: {\n concurrency: number\n delay: number\n timeout: number\n maxRetries: number\n groups: {\n concurrency: number\n delay: number\n timeout: number\n maxRetries: number\n }\n redisOptions: object\n cleanupInterval: number\n }\n private handler?: TaskHandler<T, R>\n private workers = new Map<string, boolean>()\n private groupWorkers = new Map<string, Map<string, boolean>>()\n private cleanupTimer?: NodeJS.Timeout\n\n constructor(options: QueueOptions<T> = {}) {\n super()\n\n this.options = {\n concurrency: options.concurrency ?? 1,\n delay: ms(options.delay ?? 0),\n timeout: ms(options.timeout ?? 0),\n maxRetries: options.maxRetries ?? 3,\n groups: {\n concurrency: options.groups?.concurrency ?? options.concurrency ?? 1,\n delay: ms(options.groups?.delay ?? options.delay ?? 0),\n timeout: ms(options.groups?.timeout ?? options.timeout ?? 0),\n maxRetries: options.groups?.maxRetries ?? options.maxRetries ?? 3\n },\n redisOptions: options.redisOptions ?? {},\n cleanupInterval: options.cleanupInterval ?? 30000\n }\n\n this.redis = createClient(this.options.redisOptions)\n this.redis.on('error', () => {})\n this.initializeRedis()\n }\n\n process(handler: TaskHandler<T, R>) {\n this.handler = handler\n }\n\n async push(payload: T): Promise<string> {\n const task: Task<T> = {\n uuid: randomUUID(),\n payload,\n createdAt: Date.now(),\n attempts: 0\n }\n\n await this.redis.lPush('queue:tasks', JSON.stringify(task))\n this.emit('new', { task })\n\n return task.uuid\n }\n\n group(key: string) {\n return {\n push: async (payload: T): Promise<string> => {\n const task: Task<T> = {\n uuid: randomUUID(),\n payload,\n createdAt: Date.now(),\n groupKey: key,\n attempts: 0\n }\n\n await this.redis.lPush(`queue:groups:${key}`, JSON.stringify(task))\n this.emit('new', { task })\n\n if (!this.groupWorkers.has(key)) {\n this.groupWorkers.set(key, new Map())\n await this.startGroupWorkers(key)\n await new Promise(resolve => setTimeout(resolve, 50))\n }\n\n return task.uuid\n }\n }\n }\n\n private async startWorkers() {\n for (let i = 0; i < this.options.concurrency; i++) {\n this.startWorker(`worker-${i}`)\n }\n }\n\n private async startGroupWorkers(groupKey: string) {\n const groupWorkers = this.groupWorkers.get(groupKey)!\n for (let i = 0; i < this.options.groups.concurrency; i++) {\n const workerId = `group-${groupKey}-worker-${i}`\n groupWorkers.set(workerId, true)\n this.startGroupWorker(workerId, groupKey)\n }\n // give workers more time to start and be ready\n await new Promise(resolve => setTimeout(resolve, 100))\n }\n\n private async startWorker(workerId: string) {\n this.workers.set(workerId, true)\n\n while (this.workers.get(workerId)) {\n try {\n if (!this.redis.isOpen) {\n break\n }\n\n const taskData = await this.redis.brPop('queue:tasks', 1)\n\n if (taskData) {\n const task: Task<T> = JSON.parse(taskData.element)\n await this.processTask(task)\n }\n\n if (this.options.delay > 0) {\n await new Promise(resolve => setTimeout(resolve, this.options.delay))\n }\n } catch (err) {\n const error = err as Error\n if (error.message?.includes('closed') || error.message?.includes('ClientClosedError')) {\n break\n }\n }\n }\n }\n\n private async startGroupWorker(workerId: string, groupKey: string) {\n const groupWorkers = this.groupWorkers.get(groupKey)!\n\n while (groupWorkers.get(workerId)) {\n try {\n if (!this.redis.isOpen) {\n break\n }\n\n const taskData = await this.redis.brPop(`queue:groups:${groupKey}`, 1)\n\n if (taskData) {\n const task: Task<T> = JSON.parse(taskData.element)\n await this.processGroupTask(task)\n }\n\n if (this.options.groups.delay > 0) {\n await new Promise(resolve => setTimeout(resolve, this.options.groups.delay))\n }\n } catch (err) {\n const error = err as Error\n if (error.message?.includes('closed') || error.message?.includes('ClientClosedError')) {\n break\n }\n }\n }\n }\n\n private async processTask(task: Task<T>) {\n task.attempts++\n\n try {\n if (!this.handler) {\n this.emit('complete', { task, result: undefined })\n return\n }\n\n const timeoutPromise = this.options.timeout > 0\n ? new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error('Task timeout')), this.options.timeout)\n )\n : null\n\n const workPromise = Promise.resolve(this.handler(task.payload, task))\n\n const result = timeoutPromise\n ? await Promise.race([workPromise, timeoutPromise])\n : await workPromise\n\n this.emit('complete', { task, result })\n } catch (error) {\n if (task.attempts < this.options.maxRetries) {\n this.emit('retry', { task, error, attempt: task.attempts })\n await this.redis.lPush('queue:tasks', JSON.stringify(task))\n } else {\n this.emit('failed', { task, error })\n }\n }\n }\n\n private async processGroupTask(task: Task<T>) {\n task.attempts++\n\n try {\n if (!this.handler) {\n this.emit('complete', { task, result: undefined })\n return\n }\n\n const timeoutPromise = this.options.groups.timeout > 0\n ? new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error('Task timeout')), this.options.groups.timeout)\n )\n : null\n\n const workPromise = Promise.resolve(this.handler(task.payload, task))\n\n const result = timeoutPromise\n ? await Promise.race([workPromise, timeoutPromise])\n : await workPromise\n\n this.emit('complete', { task, result })\n } catch (error) {\n if (task.attempts < this.options.groups.maxRetries) {\n this.emit('retry', { task, error, attempt: task.attempts })\n await this.redis.lPush(`queue:groups:${task.groupKey}`, JSON.stringify(task))\n } else {\n this.emit('failed', { task, error })\n }\n }\n }\n\n private async initializeRedis() {\n await this.redis.connect()\n this.startWorkers()\n\n // start periodic cleanup after redis is connected\n if (this.options.cleanupInterval > 0) {\n this.cleanupTimer = setInterval(() => {\n this.performPeriodicCleanup()\n }, this.options.cleanupInterval)\n }\n }\n\n private async performPeriodicCleanup() {\n try {\n if (!this.redis.isOpen) {\n return\n }\n\n const groupKeys = Array.from(this.groupWorkers.keys())\n\n for (const groupKey of groupKeys) {\n const length = await this.redis.lLen(`queue:groups:${groupKey}`)\n\n if (length === 0) {\n const keyExists = await this.redis.exists(`queue:groups:${groupKey}`)\n if (keyExists) {\n await this.redis.del(`queue:groups:${groupKey}`)\n }\n\n const groupWorkers = this.groupWorkers.get(groupKey)\n if (groupWorkers) {\n groupWorkers.clear()\n this.groupWorkers.delete(groupKey)\n }\n }\n }\n } catch (error) {\n // cleanup error handled silently\n }\n }\n\n async close() {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer)\n }\n this.workers.clear()\n for (const groupWorkers of this.groupWorkers.values()) {\n groupWorkers.clear()\n }\n this.groupWorkers.clear()\n\n if (this.redis.isOpen) {\n await this.redis.quit()\n }\n }\n}\n"],"mappings":"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","names":["ms"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/queue.ts"],"sourcesContent":["export { default } from './queue.js'\nexport type { QueueOptions, Task, TaskHandler } from './queue.js'\n","import { createClient, RedisClientType } from 'redis'\nimport { EventEmitter } from 'events'\nimport { randomUUID } from 'crypto'\nimport ms from '@prsm/ms'\n\nexport interface QueueOptions<T = any> {\n /** number of tasks to process in parallel. @default 1 */\n concurrency?: number\n /** wait time between finishing one task and picking up the next (ms or string like \"500ms\"). @default 0 */\n delay?: number | string\n /** max time a single task handler can run before it's killed with a \"Task timeout\" error (ms or string like \"30s\"). 0 = no limit. @default 0 */\n timeout?: number | string\n /** how many times to re-attempt a failed task before emitting \"failed\". @default 3 */\n maxRetries?: number\n /** overrides for grouped queues, falls back to top-level values */\n groups?: {\n concurrency?: number\n delay?: number | string\n timeout?: number | string\n maxRetries?: number\n }\n redisOptions?: {\n url?: string\n host?: string\n port?: number\n password?: string\n }\n /** how often to clean up empty group keys in redis (ms). 0 = disabled. @default 30000 */\n cleanupInterval?: number\n}\n\n/** called for each task, return value is passed to the \"complete\" event */\nexport type TaskHandler<T, R = any> = (payload: T, task: Task<T>) => Promise<R> | R\n\nexport interface Task<T = any> {\n uuid: string\n payload: T\n createdAt: number\n /** present when the task was pushed via queue.group(key).push() */\n groupKey?: string\n /** number of times this task has been attempted so far */\n attempts: number\n}\n\n\nexport default class Queue<T = any, R = any> extends EventEmitter {\n private redis: RedisClientType\n private workerClients: RedisClientType[] = []\n private options: {\n concurrency: number\n delay: number\n timeout: number\n maxRetries: number\n groups: {\n concurrency: number\n delay: number\n timeout: number\n maxRetries: number\n }\n redisOptions: object\n cleanupInterval: number\n }\n private handler?: TaskHandler<T, R>\n private workers = new Map<string, boolean>()\n private groupWorkers = new Map<string, Map<string, boolean>>()\n private cleanupTimer?: NodeJS.Timeout\n private _ready: Promise<void>\n private _inFlight = 0\n private _totalSettled = 0\n\n constructor(options: QueueOptions<T> = {}) {\n super()\n\n this.options = {\n concurrency: options.concurrency ?? 1,\n delay: ms(options.delay ?? 0),\n timeout: ms(options.timeout ?? 0),\n maxRetries: options.maxRetries ?? 3,\n groups: {\n concurrency: options.groups?.concurrency ?? options.concurrency ?? 1,\n delay: ms(options.groups?.delay ?? options.delay ?? 0),\n timeout: ms(options.groups?.timeout ?? options.timeout ?? 0),\n maxRetries: options.groups?.maxRetries ?? options.maxRetries ?? 3\n },\n redisOptions: options.redisOptions ?? {},\n cleanupInterval: options.cleanupInterval ?? 30000\n }\n\n this.redis = createClient(this.options.redisOptions)\n this.redis.on('error', () => {})\n this._ready = this.initializeRedis()\n }\n\n /** resolves when redis is connected and all workers are ready to accept tasks */\n ready() {\n return this._ready\n }\n\n get inFlight() {\n return this._inFlight\n }\n\n /** register the handler that processes each task. only one handler per queue. */\n process(handler: TaskHandler<T, R>) {\n this.handler = handler\n }\n\n /** push a task to the main queue. returns the task uuid. */\n async push(payload: T): Promise<string> {\n const task: Task<T> = {\n uuid: randomUUID(),\n payload,\n createdAt: Date.now(),\n attempts: 0\n }\n\n await this.redis.lPush('queue:tasks', JSON.stringify(task))\n this.emit('new', { task })\n\n return task.uuid\n }\n\n /** returns a scoped pusher for a named group. each group gets its own worker pool, spun up on first push. */\n group(key: string) {\n return {\n push: async (payload: T): Promise<string> => {\n const task: Task<T> = {\n uuid: randomUUID(),\n payload,\n createdAt: Date.now(),\n groupKey: key,\n attempts: 0\n }\n\n await this.redis.lPush(`queue:groups:${key}`, JSON.stringify(task))\n this.emit('new', { task })\n\n if (!this.groupWorkers.has(key)) {\n this.groupWorkers.set(key, new Map())\n await this.startGroupWorkers(key)\n }\n\n return task.uuid\n }\n }\n }\n\n private async createWorkerClient(): Promise<RedisClientType> {\n const client = this.redis.duplicate() as RedisClientType\n client.on('error', () => {})\n await client.connect()\n this.workerClients.push(client)\n return client\n }\n\n private async startWorkers() {\n const ready = []\n for (let i = 0; i < this.options.concurrency; i++) {\n ready.push(this.startWorker(`worker-${i}`))\n }\n await Promise.all(ready)\n }\n\n private async startGroupWorkers(groupKey: string) {\n const groupWorkers = this.groupWorkers.get(groupKey)!\n const ready = []\n for (let i = 0; i < this.options.groups.concurrency; i++) {\n const workerId = `group-${groupKey}-worker-${i}`\n groupWorkers.set(workerId, true)\n ready.push(this.startGroupWorker(workerId, groupKey))\n }\n await Promise.all(ready)\n }\n\n private async startWorker(workerId: string) {\n this.workers.set(workerId, true)\n const client = await this.createWorkerClient()\n\n this.runWorkerLoop(workerId, client, 'queue:tasks', this.workers, (task) => this.processTask(task))\n }\n\n private async startGroupWorker(workerId: string, groupKey: string) {\n const groupWorkers = this.groupWorkers.get(groupKey)!\n const client = await this.createWorkerClient()\n\n this.runWorkerLoop(workerId, client, `queue:groups:${groupKey}`, groupWorkers, (task) => this.processGroupTask(task))\n }\n\n private async runWorkerLoop(\n workerId: string,\n client: RedisClientType,\n key: string,\n activeMap: Map<string, boolean>,\n processFn: (task: Task<T>) => Promise<void>\n ) {\n const isGrouped = key.startsWith('queue:groups:')\n const delay = isGrouped ? this.options.groups.delay : this.options.delay\n\n while (activeMap.get(workerId)) {\n try {\n if (!client.isOpen) break\n\n const taskData = await client.brPop(key, 1)\n\n if (taskData) {\n const task: Task<T> = JSON.parse(taskData.element)\n this._inFlight++\n await processFn(task)\n }\n\n if (delay > 0) {\n await new Promise(resolve => setTimeout(resolve, delay))\n }\n } catch (err) {\n const error = err as Error\n if (error.message?.includes('closed') || error.message?.includes('ClientClosedError')) {\n break\n }\n }\n }\n }\n\n private async processTask(task: Task<T>) {\n task.attempts++\n\n try {\n if (!this.handler) {\n this.emit('complete', { task, result: undefined })\n this.settle()\n return\n }\n\n const timeoutPromise = this.options.timeout > 0\n ? new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error('Task timeout')), this.options.timeout)\n )\n : null\n\n const workPromise = Promise.resolve(this.handler(task.payload, task))\n\n const result = timeoutPromise\n ? await Promise.race([workPromise, timeoutPromise])\n : await workPromise\n\n this.emit('complete', { task, result })\n this.settle()\n } catch (error) {\n if (task.attempts < this.options.maxRetries) {\n this.emit('retry', { task, error, attempt: task.attempts })\n this._inFlight--\n await this.redis.lPush('queue:tasks', JSON.stringify(task))\n } else {\n this.emit('failed', { task, error })\n this.settle()\n }\n }\n }\n\n private async processGroupTask(task: Task<T>) {\n task.attempts++\n\n try {\n if (!this.handler) {\n this.emit('complete', { task, result: undefined })\n this.settle()\n return\n }\n\n const timeoutPromise = this.options.groups.timeout > 0\n ? new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error('Task timeout')), this.options.groups.timeout)\n )\n : null\n\n const workPromise = Promise.resolve(this.handler(task.payload, task))\n\n const result = timeoutPromise\n ? await Promise.race([workPromise, timeoutPromise])\n : await workPromise\n\n this.emit('complete', { task, result })\n this.settle()\n } catch (error) {\n if (task.attempts < this.options.groups.maxRetries) {\n this.emit('retry', { task, error, attempt: task.attempts })\n this._inFlight--\n await this.redis.lPush(`queue:groups:${task.groupKey}`, JSON.stringify(task))\n } else {\n this.emit('failed', { task, error })\n this.settle()\n }\n }\n }\n\n private settle() {\n this._inFlight--\n this._totalSettled++\n if (this._inFlight === 0 && this._totalSettled > 0) {\n this.emit('drain')\n }\n }\n\n private async initializeRedis() {\n await this.redis.connect()\n await this.startWorkers()\n\n if (this.options.cleanupInterval > 0) {\n this.cleanupTimer = setInterval(() => {\n this.performPeriodicCleanup()\n }, this.options.cleanupInterval)\n }\n }\n\n private async performPeriodicCleanup() {\n try {\n if (!this.redis.isOpen) {\n return\n }\n\n const groupKeys = Array.from(this.groupWorkers.keys())\n\n for (const groupKey of groupKeys) {\n const length = await this.redis.lLen(`queue:groups:${groupKey}`)\n\n if (length === 0) {\n const keyExists = await this.redis.exists(`queue:groups:${groupKey}`)\n if (keyExists) {\n await this.redis.del(`queue:groups:${groupKey}`)\n }\n\n const groupWorkers = this.groupWorkers.get(groupKey)\n if (groupWorkers) {\n groupWorkers.clear()\n this.groupWorkers.delete(groupKey)\n }\n }\n }\n } catch (error) {\n // cleanup error handled silently\n }\n }\n\n /** shuts down all workers and disconnects from redis. waits for initialization to complete first. */\n async close() {\n await this._ready.catch(() => {})\n\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer)\n }\n this.workers.clear()\n for (const groupWorkers of this.groupWorkers.values()) {\n groupWorkers.clear()\n }\n this.groupWorkers.clear()\n\n for (const client of this.workerClients) {\n if (client.isOpen) {\n await client.disconnect()\n }\n }\n this.workerClients = []\n\n if (this.redis.isOpen) {\n await this.redis.quit()\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAA8C;AAC9C,oBAA6B;AAC7B,oBAA2B;AAC3B,gBAAe;AA0Cf,IAAqB,QAArB,cAAqD,2BAAa;AAAA,EACxD;AAAA,EACA,gBAAmC,CAAC;AAAA,EACpC;AAAA,EAcA;AAAA,EACA,UAAU,oBAAI,IAAqB;AAAA,EACnC,eAAe,oBAAI,IAAkC;AAAA,EACrD;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,gBAAgB;AAAA,EAExB,YAAY,UAA2B,CAAC,GAAG;AACzC,UAAM;AAEN,SAAK,UAAU;AAAA,MACb,aAAa,QAAQ,eAAe;AAAA,MACpC,WAAO,UAAAA,SAAG,QAAQ,SAAS,CAAC;AAAA,MAC5B,aAAS,UAAAA,SAAG,QAAQ,WAAW,CAAC;AAAA,MAChC,YAAY,QAAQ,cAAc;AAAA,MAClC,QAAQ;AAAA,QACN,aAAa,QAAQ,QAAQ,eAAe,QAAQ,eAAe;AAAA,QACnE,WAAO,UAAAA,SAAG,QAAQ,QAAQ,SAAS,QAAQ,SAAS,CAAC;AAAA,QACrD,aAAS,UAAAA,SAAG,QAAQ,QAAQ,WAAW,QAAQ,WAAW,CAAC;AAAA,QAC3D,YAAY,QAAQ,QAAQ,cAAc,QAAQ,cAAc;AAAA,MAClE;AAAA,MACA,cAAc,QAAQ,gBAAgB,CAAC;AAAA,MACvC,iBAAiB,QAAQ,mBAAmB;AAAA,IAC9C;AAEA,SAAK,YAAQ,2BAAa,KAAK,QAAQ,YAAY;AACnD,SAAK,MAAM,GAAG,SAAS,MAAM;AAAA,IAAC,CAAC;AAC/B,SAAK,SAAS,KAAK,gBAAgB;AAAA,EACrC;AAAA;AAAA,EAGA,QAAQ;AACN,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,WAAW;AACb,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,QAAQ,SAA4B;AAClC,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA,EAGA,MAAM,KAAK,SAA6B;AACtC,UAAM,OAAgB;AAAA,MACpB,UAAM,0BAAW;AAAA,MACjB;AAAA,MACA,WAAW,KAAK,IAAI;AAAA,MACpB,UAAU;AAAA,IACZ;AAEA,UAAM,KAAK,MAAM,MAAM,eAAe,KAAK,UAAU,IAAI,CAAC;AAC1D,SAAK,KAAK,OAAO,EAAE,KAAK,CAAC;AAEzB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,MAAM,KAAa;AACjB,WAAO;AAAA,MACL,MAAM,OAAO,YAAgC;AAC3C,cAAM,OAAgB;AAAA,UACpB,UAAM,0BAAW;AAAA,UACjB;AAAA,UACA,WAAW,KAAK,IAAI;AAAA,UACpB,UAAU;AAAA,UACV,UAAU;AAAA,QACZ;AAEA,cAAM,KAAK,MAAM,MAAM,gBAAgB,GAAG,IAAI,KAAK,UAAU,IAAI,CAAC;AAClE,aAAK,KAAK,OAAO,EAAE,KAAK,CAAC;AAEzB,YAAI,CAAC,KAAK,aAAa,IAAI,GAAG,GAAG;AAC/B,eAAK,aAAa,IAAI,KAAK,oBAAI,IAAI,CAAC;AACpC,gBAAM,KAAK,kBAAkB,GAAG;AAAA,QAClC;AAEA,eAAO,KAAK;AAAA,MACd;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,qBAA+C;AAC3D,UAAM,SAAS,KAAK,MAAM,UAAU;AACpC,WAAO,GAAG,SAAS,MAAM;AAAA,IAAC,CAAC;AAC3B,UAAM,OAAO,QAAQ;AACrB,SAAK,cAAc,KAAK,MAAM;AAC9B,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,eAAe;AAC3B,UAAM,QAAQ,CAAC;AACf,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,aAAa,KAAK;AACjD,YAAM,KAAK,KAAK,YAAY,UAAU,CAAC,EAAE,CAAC;AAAA,IAC5C;AACA,UAAM,QAAQ,IAAI,KAAK;AAAA,EACzB;AAAA,EAEA,MAAc,kBAAkB,UAAkB;AAChD,UAAM,eAAe,KAAK,aAAa,IAAI,QAAQ;AACnD,UAAM,QAAQ,CAAC;AACf,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,OAAO,aAAa,KAAK;AACxD,YAAM,WAAW,SAAS,QAAQ,WAAW,CAAC;AAC9C,mBAAa,IAAI,UAAU,IAAI;AAC/B,YAAM,KAAK,KAAK,iBAAiB,UAAU,QAAQ,CAAC;AAAA,IACtD;AACA,UAAM,QAAQ,IAAI,KAAK;AAAA,EACzB;AAAA,EAEA,MAAc,YAAY,UAAkB;AAC1C,SAAK,QAAQ,IAAI,UAAU,IAAI;AAC/B,UAAM,SAAS,MAAM,KAAK,mBAAmB;AAE7C,SAAK,cAAc,UAAU,QAAQ,eAAe,KAAK,SAAS,CAAC,SAAS,KAAK,YAAY,IAAI,CAAC;AAAA,EACpG;AAAA,EAEA,MAAc,iBAAiB,UAAkB,UAAkB;AACjE,UAAM,eAAe,KAAK,aAAa,IAAI,QAAQ;AACnD,UAAM,SAAS,MAAM,KAAK,mBAAmB;AAE7C,SAAK,cAAc,UAAU,QAAQ,gBAAgB,QAAQ,IAAI,cAAc,CAAC,SAAS,KAAK,iBAAiB,IAAI,CAAC;AAAA,EACtH;AAAA,EAEA,MAAc,cACZ,UACA,QACA,KACA,WACA,WACA;AACA,UAAM,YAAY,IAAI,WAAW,eAAe;AAChD,UAAM,QAAQ,YAAY,KAAK,QAAQ,OAAO,QAAQ,KAAK,QAAQ;AAEnE,WAAO,UAAU,IAAI,QAAQ,GAAG;AAC9B,UAAI;AACF,YAAI,CAAC,OAAO,OAAQ;AAEpB,cAAM,WAAW,MAAM,OAAO,MAAM,KAAK,CAAC;AAE1C,YAAI,UAAU;AACZ,gBAAM,OAAgB,KAAK,MAAM,SAAS,OAAO;AACjD,eAAK;AACL,gBAAM,UAAU,IAAI;AAAA,QACtB;AAEA,YAAI,QAAQ,GAAG;AACb,gBAAM,IAAI,QAAQ,aAAW,WAAW,SAAS,KAAK,CAAC;AAAA,QACzD;AAAA,MACF,SAAS,KAAK;AACZ,cAAM,QAAQ;AACd,YAAI,MAAM,SAAS,SAAS,QAAQ,KAAK,MAAM,SAAS,SAAS,mBAAmB,GAAG;AACrF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,YAAY,MAAe;AACvC,SAAK;AAEL,QAAI;AACF,UAAI,CAAC,KAAK,SAAS;AACjB,aAAK,KAAK,YAAY,EAAE,MAAM,QAAQ,OAAU,CAAC;AACjD,aAAK,OAAO;AACZ;AAAA,MACF;AAEA,YAAM,iBAAiB,KAAK,QAAQ,UAAU,IAC1C,IAAI;AAAA,QAAe,CAAC,GAAG,WACrB,WAAW,MAAM,OAAO,IAAI,MAAM,cAAc,CAAC,GAAG,KAAK,QAAQ,OAAO;AAAA,MAC1E,IACA;AAEJ,YAAM,cAAc,QAAQ,QAAQ,KAAK,QAAQ,KAAK,SAAS,IAAI,CAAC;AAEpE,YAAM,SAAS,iBACX,MAAM,QAAQ,KAAK,CAAC,aAAa,cAAc,CAAC,IAChD,MAAM;AAEV,WAAK,KAAK,YAAY,EAAE,MAAM,OAAO,CAAC;AACtC,WAAK,OAAO;AAAA,IACd,SAAS,OAAO;AACd,UAAI,KAAK,WAAW,KAAK,QAAQ,YAAY;AAC3C,aAAK,KAAK,SAAS,EAAE,MAAM,OAAO,SAAS,KAAK,SAAS,CAAC;AAC1D,aAAK;AACL,cAAM,KAAK,MAAM,MAAM,eAAe,KAAK,UAAU,IAAI,CAAC;AAAA,MAC5D,OAAO;AACL,aAAK,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AACnC,aAAK,OAAO;AAAA,MACd;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,iBAAiB,MAAe;AAC5C,SAAK;AAEL,QAAI;AACF,UAAI,CAAC,KAAK,SAAS;AACjB,aAAK,KAAK,YAAY,EAAE,MAAM,QAAQ,OAAU,CAAC;AACjD,aAAK,OAAO;AACZ;AAAA,MACF;AAEA,YAAM,iBAAiB,KAAK,QAAQ,OAAO,UAAU,IACjD,IAAI;AAAA,QAAe,CAAC,GAAG,WACrB,WAAW,MAAM,OAAO,IAAI,MAAM,cAAc,CAAC,GAAG,KAAK,QAAQ,OAAO,OAAO;AAAA,MACjF,IACA;AAEJ,YAAM,cAAc,QAAQ,QAAQ,KAAK,QAAQ,KAAK,SAAS,IAAI,CAAC;AAEpE,YAAM,SAAS,iBACX,MAAM,QAAQ,KAAK,CAAC,aAAa,cAAc,CAAC,IAChD,MAAM;AAEV,WAAK,KAAK,YAAY,EAAE,MAAM,OAAO,CAAC;AACtC,WAAK,OAAO;AAAA,IACd,SAAS,OAAO;AACd,UAAI,KAAK,WAAW,KAAK,QAAQ,OAAO,YAAY;AAClD,aAAK,KAAK,SAAS,EAAE,MAAM,OAAO,SAAS,KAAK,SAAS,CAAC;AAC1D,aAAK;AACL,cAAM,KAAK,MAAM,MAAM,gBAAgB,KAAK,QAAQ,IAAI,KAAK,UAAU,IAAI,CAAC;AAAA,MAC9E,OAAO;AACL,aAAK,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AACnC,aAAK,OAAO;AAAA,MACd;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,SAAS;AACf,SAAK;AACL,SAAK;AACL,QAAI,KAAK,cAAc,KAAK,KAAK,gBAAgB,GAAG;AAClD,WAAK,KAAK,OAAO;AAAA,IACnB;AAAA,EACF;AAAA,EAEA,MAAc,kBAAkB;AAC9B,UAAM,KAAK,MAAM,QAAQ;AACzB,UAAM,KAAK,aAAa;AAExB,QAAI,KAAK,QAAQ,kBAAkB,GAAG;AACpC,WAAK,eAAe,YAAY,MAAM;AACpC,aAAK,uBAAuB;AAAA,MAC9B,GAAG,KAAK,QAAQ,eAAe;AAAA,IACjC;AAAA,EACF;AAAA,EAEA,MAAc,yBAAyB;AACrC,QAAI;AACF,UAAI,CAAC,KAAK,MAAM,QAAQ;AACtB;AAAA,MACF;AAEA,YAAM,YAAY,MAAM,KAAK,KAAK,aAAa,KAAK,CAAC;AAErD,iBAAW,YAAY,WAAW;AAChC,cAAM,SAAS,MAAM,KAAK,MAAM,KAAK,gBAAgB,QAAQ,EAAE;AAE/D,YAAI,WAAW,GAAG;AAChB,gBAAM,YAAY,MAAM,KAAK,MAAM,OAAO,gBAAgB,QAAQ,EAAE;AACpE,cAAI,WAAW;AACb,kBAAM,KAAK,MAAM,IAAI,gBAAgB,QAAQ,EAAE;AAAA,UACjD;AAEA,gBAAM,eAAe,KAAK,aAAa,IAAI,QAAQ;AACnD,cAAI,cAAc;AAChB,yBAAa,MAAM;AACnB,iBAAK,aAAa,OAAO,QAAQ;AAAA,UACnC;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AAAA,IAEhB;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,QAAQ;AACZ,UAAM,KAAK,OAAO,MAAM,MAAM;AAAA,IAAC,CAAC;AAEhC,QAAI,KAAK,cAAc;AACrB,oBAAc,KAAK,YAAY;AAAA,IACjC;AACA,SAAK,QAAQ,MAAM;AACnB,eAAW,gBAAgB,KAAK,aAAa,OAAO,GAAG;AACrD,mBAAa,MAAM;AAAA,IACrB;AACA,SAAK,aAAa,MAAM;AAExB,eAAW,UAAU,KAAK,eAAe;AACvC,UAAI,OAAO,QAAQ;AACjB,cAAM,OAAO,WAAW;AAAA,MAC1B;AAAA,IACF;AACA,SAAK,gBAAgB,CAAC;AAEtB,QAAI,KAAK,MAAM,QAAQ;AACrB,YAAM,KAAK,MAAM,KAAK;AAAA,IACxB;AAAA,EACF;AACF;","names":["ms"]}
package/dist/index.d.cts CHANGED
@@ -1,10 +1,15 @@
1
1
  import { EventEmitter } from 'events';
2
2
 
3
3
  interface QueueOptions<T = any> {
4
+ /** number of tasks to process in parallel. @default 1 */
4
5
  concurrency?: number;
6
+ /** wait time between finishing one task and picking up the next (ms or string like "500ms"). @default 0 */
5
7
  delay?: number | string;
8
+ /** max time a single task handler can run before it's killed with a "Task timeout" error (ms or string like "30s"). 0 = no limit. @default 0 */
6
9
  timeout?: number | string;
10
+ /** how many times to re-attempt a failed task before emitting "failed". @default 3 */
7
11
  maxRetries?: number;
12
+ /** overrides for grouped queues, falls back to top-level values */
8
13
  groups?: {
9
14
  concurrency?: number;
10
15
  delay?: number | string;
@@ -17,37 +22,55 @@ interface QueueOptions<T = any> {
17
22
  port?: number;
18
23
  password?: string;
19
24
  };
25
+ /** how often to clean up empty group keys in redis (ms). 0 = disabled. @default 30000 */
20
26
  cleanupInterval?: number;
21
27
  }
28
+ /** called for each task, return value is passed to the "complete" event */
22
29
  type TaskHandler<T, R = any> = (payload: T, task: Task<T>) => Promise<R> | R;
23
30
  interface Task<T = any> {
24
31
  uuid: string;
25
32
  payload: T;
26
33
  createdAt: number;
34
+ /** present when the task was pushed via queue.group(key).push() */
27
35
  groupKey?: string;
36
+ /** number of times this task has been attempted so far */
28
37
  attempts: number;
29
38
  }
30
39
  declare class Queue<T = any, R = any> extends EventEmitter {
31
40
  private redis;
41
+ private workerClients;
32
42
  private options;
33
43
  private handler?;
34
44
  private workers;
35
45
  private groupWorkers;
36
46
  private cleanupTimer?;
47
+ private _ready;
48
+ private _inFlight;
49
+ private _totalSettled;
37
50
  constructor(options?: QueueOptions<T>);
51
+ /** resolves when redis is connected and all workers are ready to accept tasks */
52
+ ready(): Promise<void>;
53
+ get inFlight(): number;
54
+ /** register the handler that processes each task. only one handler per queue. */
38
55
  process(handler: TaskHandler<T, R>): void;
56
+ /** push a task to the main queue. returns the task uuid. */
39
57
  push(payload: T): Promise<string>;
58
+ /** returns a scoped pusher for a named group. each group gets its own worker pool, spun up on first push. */
40
59
  group(key: string): {
41
60
  push: (payload: T) => Promise<string>;
42
61
  };
62
+ private createWorkerClient;
43
63
  private startWorkers;
44
64
  private startGroupWorkers;
45
65
  private startWorker;
46
66
  private startGroupWorker;
67
+ private runWorkerLoop;
47
68
  private processTask;
48
69
  private processGroupTask;
70
+ private settle;
49
71
  private initializeRedis;
50
72
  private performPeriodicCleanup;
73
+ /** shuts down all workers and disconnects from redis. waits for initialization to complete first. */
51
74
  close(): Promise<void>;
52
75
  }
53
76
 
package/dist/index.d.ts CHANGED
@@ -1,10 +1,15 @@
1
1
  import { EventEmitter } from 'events';
2
2
 
3
3
  interface QueueOptions<T = any> {
4
+ /** number of tasks to process in parallel. @default 1 */
4
5
  concurrency?: number;
6
+ /** wait time between finishing one task and picking up the next (ms or string like "500ms"). @default 0 */
5
7
  delay?: number | string;
8
+ /** max time a single task handler can run before it's killed with a "Task timeout" error (ms or string like "30s"). 0 = no limit. @default 0 */
6
9
  timeout?: number | string;
10
+ /** how many times to re-attempt a failed task before emitting "failed". @default 3 */
7
11
  maxRetries?: number;
12
+ /** overrides for grouped queues, falls back to top-level values */
8
13
  groups?: {
9
14
  concurrency?: number;
10
15
  delay?: number | string;
@@ -17,37 +22,55 @@ interface QueueOptions<T = any> {
17
22
  port?: number;
18
23
  password?: string;
19
24
  };
25
+ /** how often to clean up empty group keys in redis (ms). 0 = disabled. @default 30000 */
20
26
  cleanupInterval?: number;
21
27
  }
28
+ /** called for each task, return value is passed to the "complete" event */
22
29
  type TaskHandler<T, R = any> = (payload: T, task: Task<T>) => Promise<R> | R;
23
30
  interface Task<T = any> {
24
31
  uuid: string;
25
32
  payload: T;
26
33
  createdAt: number;
34
+ /** present when the task was pushed via queue.group(key).push() */
27
35
  groupKey?: string;
36
+ /** number of times this task has been attempted so far */
28
37
  attempts: number;
29
38
  }
30
39
  declare class Queue<T = any, R = any> extends EventEmitter {
31
40
  private redis;
41
+ private workerClients;
32
42
  private options;
33
43
  private handler?;
34
44
  private workers;
35
45
  private groupWorkers;
36
46
  private cleanupTimer?;
47
+ private _ready;
48
+ private _inFlight;
49
+ private _totalSettled;
37
50
  constructor(options?: QueueOptions<T>);
51
+ /** resolves when redis is connected and all workers are ready to accept tasks */
52
+ ready(): Promise<void>;
53
+ get inFlight(): number;
54
+ /** register the handler that processes each task. only one handler per queue. */
38
55
  process(handler: TaskHandler<T, R>): void;
56
+ /** push a task to the main queue. returns the task uuid. */
39
57
  push(payload: T): Promise<string>;
58
+ /** returns a scoped pusher for a named group. each group gets its own worker pool, spun up on first push. */
40
59
  group(key: string): {
41
60
  push: (payload: T) => Promise<string>;
42
61
  };
62
+ private createWorkerClient;
43
63
  private startWorkers;
44
64
  private startGroupWorkers;
45
65
  private startWorker;
46
66
  private startGroupWorker;
67
+ private runWorkerLoop;
47
68
  private processTask;
48
69
  private processGroupTask;
70
+ private settle;
49
71
  private initializeRedis;
50
72
  private performPeriodicCleanup;
73
+ /** shuts down all workers and disconnects from redis. waits for initialization to complete first. */
51
74
  close(): Promise<void>;
52
75
  }
53
76
 
package/dist/index.js CHANGED
@@ -5,11 +5,15 @@ import { randomUUID } from "crypto";
5
5
  import ms from "@prsm/ms";
6
6
  var Queue = class extends EventEmitter {
7
7
  redis;
8
+ workerClients = [];
8
9
  options;
9
10
  handler;
10
11
  workers = /* @__PURE__ */ new Map();
11
12
  groupWorkers = /* @__PURE__ */ new Map();
12
13
  cleanupTimer;
14
+ _ready;
15
+ _inFlight = 0;
16
+ _totalSettled = 0;
13
17
  constructor(options = {}) {
14
18
  super();
15
19
  this.options = {
@@ -29,11 +33,20 @@ var Queue = class extends EventEmitter {
29
33
  this.redis = createClient(this.options.redisOptions);
30
34
  this.redis.on("error", () => {
31
35
  });
32
- this.initializeRedis();
36
+ this._ready = this.initializeRedis();
33
37
  }
38
+ /** resolves when redis is connected and all workers are ready to accept tasks */
39
+ ready() {
40
+ return this._ready;
41
+ }
42
+ get inFlight() {
43
+ return this._inFlight;
44
+ }
45
+ /** register the handler that processes each task. only one handler per queue. */
34
46
  process(handler) {
35
47
  this.handler = handler;
36
48
  }
49
+ /** push a task to the main queue. returns the task uuid. */
37
50
  async push(payload) {
38
51
  const task = {
39
52
  uuid: randomUUID(),
@@ -45,6 +58,7 @@ var Queue = class extends EventEmitter {
45
58
  this.emit("new", { task });
46
59
  return task.uuid;
47
60
  }
61
+ /** returns a scoped pusher for a named group. each group gets its own worker pool, spun up on first push. */
48
62
  group(key) {
49
63
  return {
50
64
  push: async (payload) => {
@@ -60,63 +74,60 @@ var Queue = class extends EventEmitter {
60
74
  if (!this.groupWorkers.has(key)) {
61
75
  this.groupWorkers.set(key, /* @__PURE__ */ new Map());
62
76
  await this.startGroupWorkers(key);
63
- await new Promise((resolve) => setTimeout(resolve, 50));
64
77
  }
65
78
  return task.uuid;
66
79
  }
67
80
  };
68
81
  }
82
+ async createWorkerClient() {
83
+ const client = this.redis.duplicate();
84
+ client.on("error", () => {
85
+ });
86
+ await client.connect();
87
+ this.workerClients.push(client);
88
+ return client;
89
+ }
69
90
  async startWorkers() {
91
+ const ready = [];
70
92
  for (let i = 0; i < this.options.concurrency; i++) {
71
- this.startWorker(`worker-${i}`);
93
+ ready.push(this.startWorker(`worker-${i}`));
72
94
  }
95
+ await Promise.all(ready);
73
96
  }
74
97
  async startGroupWorkers(groupKey) {
75
98
  const groupWorkers = this.groupWorkers.get(groupKey);
99
+ const ready = [];
76
100
  for (let i = 0; i < this.options.groups.concurrency; i++) {
77
101
  const workerId = `group-${groupKey}-worker-${i}`;
78
102
  groupWorkers.set(workerId, true);
79
- this.startGroupWorker(workerId, groupKey);
103
+ ready.push(this.startGroupWorker(workerId, groupKey));
80
104
  }
81
- await new Promise((resolve) => setTimeout(resolve, 100));
105
+ await Promise.all(ready);
82
106
  }
83
107
  async startWorker(workerId) {
84
108
  this.workers.set(workerId, true);
85
- while (this.workers.get(workerId)) {
86
- try {
87
- if (!this.redis.isOpen) {
88
- break;
89
- }
90
- const taskData = await this.redis.brPop("queue:tasks", 1);
91
- if (taskData) {
92
- const task = JSON.parse(taskData.element);
93
- await this.processTask(task);
94
- }
95
- if (this.options.delay > 0) {
96
- await new Promise((resolve) => setTimeout(resolve, this.options.delay));
97
- }
98
- } catch (err) {
99
- const error = err;
100
- if (error.message?.includes("closed") || error.message?.includes("ClientClosedError")) {
101
- break;
102
- }
103
- }
104
- }
109
+ const client = await this.createWorkerClient();
110
+ this.runWorkerLoop(workerId, client, "queue:tasks", this.workers, (task) => this.processTask(task));
105
111
  }
106
112
  async startGroupWorker(workerId, groupKey) {
107
113
  const groupWorkers = this.groupWorkers.get(groupKey);
108
- while (groupWorkers.get(workerId)) {
114
+ const client = await this.createWorkerClient();
115
+ this.runWorkerLoop(workerId, client, `queue:groups:${groupKey}`, groupWorkers, (task) => this.processGroupTask(task));
116
+ }
117
+ async runWorkerLoop(workerId, client, key, activeMap, processFn) {
118
+ const isGrouped = key.startsWith("queue:groups:");
119
+ const delay = isGrouped ? this.options.groups.delay : this.options.delay;
120
+ while (activeMap.get(workerId)) {
109
121
  try {
110
- if (!this.redis.isOpen) {
111
- break;
112
- }
113
- const taskData = await this.redis.brPop(`queue:groups:${groupKey}`, 1);
122
+ if (!client.isOpen) break;
123
+ const taskData = await client.brPop(key, 1);
114
124
  if (taskData) {
115
125
  const task = JSON.parse(taskData.element);
116
- await this.processGroupTask(task);
126
+ this._inFlight++;
127
+ await processFn(task);
117
128
  }
118
- if (this.options.groups.delay > 0) {
119
- await new Promise((resolve) => setTimeout(resolve, this.options.groups.delay));
129
+ if (delay > 0) {
130
+ await new Promise((resolve) => setTimeout(resolve, delay));
120
131
  }
121
132
  } catch (err) {
122
133
  const error = err;
@@ -131,6 +142,7 @@ var Queue = class extends EventEmitter {
131
142
  try {
132
143
  if (!this.handler) {
133
144
  this.emit("complete", { task, result: void 0 });
145
+ this.settle();
134
146
  return;
135
147
  }
136
148
  const timeoutPromise = this.options.timeout > 0 ? new Promise(
@@ -139,12 +151,15 @@ var Queue = class extends EventEmitter {
139
151
  const workPromise = Promise.resolve(this.handler(task.payload, task));
140
152
  const result = timeoutPromise ? await Promise.race([workPromise, timeoutPromise]) : await workPromise;
141
153
  this.emit("complete", { task, result });
154
+ this.settle();
142
155
  } catch (error) {
143
156
  if (task.attempts < this.options.maxRetries) {
144
157
  this.emit("retry", { task, error, attempt: task.attempts });
158
+ this._inFlight--;
145
159
  await this.redis.lPush("queue:tasks", JSON.stringify(task));
146
160
  } else {
147
161
  this.emit("failed", { task, error });
162
+ this.settle();
148
163
  }
149
164
  }
150
165
  }
@@ -153,6 +168,7 @@ var Queue = class extends EventEmitter {
153
168
  try {
154
169
  if (!this.handler) {
155
170
  this.emit("complete", { task, result: void 0 });
171
+ this.settle();
156
172
  return;
157
173
  }
158
174
  const timeoutPromise = this.options.groups.timeout > 0 ? new Promise(
@@ -161,18 +177,28 @@ var Queue = class extends EventEmitter {
161
177
  const workPromise = Promise.resolve(this.handler(task.payload, task));
162
178
  const result = timeoutPromise ? await Promise.race([workPromise, timeoutPromise]) : await workPromise;
163
179
  this.emit("complete", { task, result });
180
+ this.settle();
164
181
  } catch (error) {
165
182
  if (task.attempts < this.options.groups.maxRetries) {
166
183
  this.emit("retry", { task, error, attempt: task.attempts });
184
+ this._inFlight--;
167
185
  await this.redis.lPush(`queue:groups:${task.groupKey}`, JSON.stringify(task));
168
186
  } else {
169
187
  this.emit("failed", { task, error });
188
+ this.settle();
170
189
  }
171
190
  }
172
191
  }
192
+ settle() {
193
+ this._inFlight--;
194
+ this._totalSettled++;
195
+ if (this._inFlight === 0 && this._totalSettled > 0) {
196
+ this.emit("drain");
197
+ }
198
+ }
173
199
  async initializeRedis() {
174
200
  await this.redis.connect();
175
- this.startWorkers();
201
+ await this.startWorkers();
176
202
  if (this.options.cleanupInterval > 0) {
177
203
  this.cleanupTimer = setInterval(() => {
178
204
  this.performPeriodicCleanup();
@@ -202,7 +228,10 @@ var Queue = class extends EventEmitter {
202
228
  } catch (error) {
203
229
  }
204
230
  }
231
+ /** shuts down all workers and disconnects from redis. waits for initialization to complete first. */
205
232
  async close() {
233
+ await this._ready.catch(() => {
234
+ });
206
235
  if (this.cleanupTimer) {
207
236
  clearInterval(this.cleanupTimer);
208
237
  }
@@ -211,6 +240,12 @@ var Queue = class extends EventEmitter {
211
240
  groupWorkers.clear();
212
241
  }
213
242
  this.groupWorkers.clear();
243
+ for (const client of this.workerClients) {
244
+ if (client.isOpen) {
245
+ await client.disconnect();
246
+ }
247
+ }
248
+ this.workerClients = [];
214
249
  if (this.redis.isOpen) {
215
250
  await this.redis.quit();
216
251
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/queue.ts"],"sourcesContent":["import { createClient, RedisClientType } from 'redis'\nimport { EventEmitter } from 'events'\nimport { randomUUID } from 'crypto'\nimport ms from '@prsm/ms'\n\nexport interface QueueOptions<T = any> {\n concurrency?: number\n delay?: number | string\n timeout?: number | string\n maxRetries?: number\n groups?: {\n concurrency?: number\n delay?: number | string\n timeout?: number | string\n maxRetries?: number\n }\n redisOptions?: {\n url?: string\n host?: string\n port?: number\n password?: string\n }\n cleanupInterval?: number\n}\n\nexport type TaskHandler<T, R = any> = (payload: T, task: Task<T>) => Promise<R> | R\n\nexport interface Task<T = any> {\n uuid: string\n payload: T\n createdAt: number\n groupKey?: string\n attempts: number\n}\n\n\nexport default class Queue<T = any, R = any> extends EventEmitter {\n private redis: RedisClientType\n private options: {\n concurrency: number\n delay: number\n timeout: number\n maxRetries: number\n groups: {\n concurrency: number\n delay: number\n timeout: number\n maxRetries: number\n }\n redisOptions: object\n cleanupInterval: number\n }\n private handler?: TaskHandler<T, R>\n private workers = new Map<string, boolean>()\n private groupWorkers = new Map<string, Map<string, boolean>>()\n private cleanupTimer?: NodeJS.Timeout\n\n constructor(options: QueueOptions<T> = {}) {\n super()\n\n this.options = {\n concurrency: options.concurrency ?? 1,\n delay: ms(options.delay ?? 0),\n timeout: ms(options.timeout ?? 0),\n maxRetries: options.maxRetries ?? 3,\n groups: {\n concurrency: options.groups?.concurrency ?? options.concurrency ?? 1,\n delay: ms(options.groups?.delay ?? options.delay ?? 0),\n timeout: ms(options.groups?.timeout ?? options.timeout ?? 0),\n maxRetries: options.groups?.maxRetries ?? options.maxRetries ?? 3\n },\n redisOptions: options.redisOptions ?? {},\n cleanupInterval: options.cleanupInterval ?? 30000\n }\n\n this.redis = createClient(this.options.redisOptions)\n this.redis.on('error', () => {})\n this.initializeRedis()\n }\n\n process(handler: TaskHandler<T, R>) {\n this.handler = handler\n }\n\n async push(payload: T): Promise<string> {\n const task: Task<T> = {\n uuid: randomUUID(),\n payload,\n createdAt: Date.now(),\n attempts: 0\n }\n\n await this.redis.lPush('queue:tasks', JSON.stringify(task))\n this.emit('new', { task })\n\n return task.uuid\n }\n\n group(key: string) {\n return {\n push: async (payload: T): Promise<string> => {\n const task: Task<T> = {\n uuid: randomUUID(),\n payload,\n createdAt: Date.now(),\n groupKey: key,\n attempts: 0\n }\n\n await this.redis.lPush(`queue:groups:${key}`, JSON.stringify(task))\n this.emit('new', { task })\n\n if (!this.groupWorkers.has(key)) {\n this.groupWorkers.set(key, new Map())\n await this.startGroupWorkers(key)\n await new Promise(resolve => setTimeout(resolve, 50))\n }\n\n return task.uuid\n }\n }\n }\n\n private async startWorkers() {\n for (let i = 0; i < this.options.concurrency; i++) {\n this.startWorker(`worker-${i}`)\n }\n }\n\n private async startGroupWorkers(groupKey: string) {\n const groupWorkers = this.groupWorkers.get(groupKey)!\n for (let i = 0; i < this.options.groups.concurrency; i++) {\n const workerId = `group-${groupKey}-worker-${i}`\n groupWorkers.set(workerId, true)\n this.startGroupWorker(workerId, groupKey)\n }\n // give workers more time to start and be ready\n await new Promise(resolve => setTimeout(resolve, 100))\n }\n\n private async startWorker(workerId: string) {\n this.workers.set(workerId, true)\n\n while (this.workers.get(workerId)) {\n try {\n if (!this.redis.isOpen) {\n break\n }\n\n const taskData = await this.redis.brPop('queue:tasks', 1)\n\n if (taskData) {\n const task: Task<T> = JSON.parse(taskData.element)\n await this.processTask(task)\n }\n\n if (this.options.delay > 0) {\n await new Promise(resolve => setTimeout(resolve, this.options.delay))\n }\n } catch (err) {\n const error = err as Error\n if (error.message?.includes('closed') || error.message?.includes('ClientClosedError')) {\n break\n }\n }\n }\n }\n\n private async startGroupWorker(workerId: string, groupKey: string) {\n const groupWorkers = this.groupWorkers.get(groupKey)!\n\n while (groupWorkers.get(workerId)) {\n try {\n if (!this.redis.isOpen) {\n break\n }\n\n const taskData = await this.redis.brPop(`queue:groups:${groupKey}`, 1)\n\n if (taskData) {\n const task: Task<T> = JSON.parse(taskData.element)\n await this.processGroupTask(task)\n }\n\n if (this.options.groups.delay > 0) {\n await new Promise(resolve => setTimeout(resolve, this.options.groups.delay))\n }\n } catch (err) {\n const error = err as Error\n if (error.message?.includes('closed') || error.message?.includes('ClientClosedError')) {\n break\n }\n }\n }\n }\n\n private async processTask(task: Task<T>) {\n task.attempts++\n\n try {\n if (!this.handler) {\n this.emit('complete', { task, result: undefined })\n return\n }\n\n const timeoutPromise = this.options.timeout > 0\n ? new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error('Task timeout')), this.options.timeout)\n )\n : null\n\n const workPromise = Promise.resolve(this.handler(task.payload, task))\n\n const result = timeoutPromise\n ? await Promise.race([workPromise, timeoutPromise])\n : await workPromise\n\n this.emit('complete', { task, result })\n } catch (error) {\n if (task.attempts < this.options.maxRetries) {\n this.emit('retry', { task, error, attempt: task.attempts })\n await this.redis.lPush('queue:tasks', JSON.stringify(task))\n } else {\n this.emit('failed', { task, error })\n }\n }\n }\n\n private async processGroupTask(task: Task<T>) {\n task.attempts++\n\n try {\n if (!this.handler) {\n this.emit('complete', { task, result: undefined })\n return\n }\n\n const timeoutPromise = this.options.groups.timeout > 0\n ? new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error('Task timeout')), this.options.groups.timeout)\n )\n : null\n\n const workPromise = Promise.resolve(this.handler(task.payload, task))\n\n const result = timeoutPromise\n ? await Promise.race([workPromise, timeoutPromise])\n : await workPromise\n\n this.emit('complete', { task, result })\n } catch (error) {\n if (task.attempts < this.options.groups.maxRetries) {\n this.emit('retry', { task, error, attempt: task.attempts })\n await this.redis.lPush(`queue:groups:${task.groupKey}`, JSON.stringify(task))\n } else {\n this.emit('failed', { task, error })\n }\n }\n }\n\n private async initializeRedis() {\n await this.redis.connect()\n this.startWorkers()\n\n // start periodic cleanup after redis is connected\n if (this.options.cleanupInterval > 0) {\n this.cleanupTimer = setInterval(() => {\n this.performPeriodicCleanup()\n }, this.options.cleanupInterval)\n }\n }\n\n private async performPeriodicCleanup() {\n try {\n if (!this.redis.isOpen) {\n return\n }\n\n const groupKeys = Array.from(this.groupWorkers.keys())\n\n for (const groupKey of groupKeys) {\n const length = await this.redis.lLen(`queue:groups:${groupKey}`)\n\n if (length === 0) {\n const keyExists = await this.redis.exists(`queue:groups:${groupKey}`)\n if (keyExists) {\n await this.redis.del(`queue:groups:${groupKey}`)\n }\n\n const groupWorkers = this.groupWorkers.get(groupKey)\n if (groupWorkers) {\n groupWorkers.clear()\n this.groupWorkers.delete(groupKey)\n }\n }\n }\n } catch (error) {\n // cleanup error handled silently\n }\n }\n\n async close() {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer)\n }\n this.workers.clear()\n for (const groupWorkers of this.groupWorkers.values()) {\n groupWorkers.clear()\n }\n this.groupWorkers.clear()\n\n if (this.redis.isOpen) {\n await this.redis.quit()\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/queue.ts"],"sourcesContent":["import { createClient, RedisClientType } from 'redis'\nimport { EventEmitter } from 'events'\nimport { randomUUID } from 'crypto'\nimport ms from '@prsm/ms'\n\nexport interface QueueOptions<T = any> {\n /** number of tasks to process in parallel. @default 1 */\n concurrency?: number\n /** wait time between finishing one task and picking up the next (ms or string like \"500ms\"). @default 0 */\n delay?: number | string\n /** max time a single task handler can run before it's killed with a \"Task timeout\" error (ms or string like \"30s\"). 0 = no limit. @default 0 */\n timeout?: number | string\n /** how many times to re-attempt a failed task before emitting \"failed\". @default 3 */\n maxRetries?: number\n /** overrides for grouped queues, falls back to top-level values */\n groups?: {\n concurrency?: number\n delay?: number | string\n timeout?: number | string\n maxRetries?: number\n }\n redisOptions?: {\n url?: string\n host?: string\n port?: number\n password?: string\n }\n /** how often to clean up empty group keys in redis (ms). 0 = disabled. @default 30000 */\n cleanupInterval?: number\n}\n\n/** called for each task, return value is passed to the \"complete\" event */\nexport type TaskHandler<T, R = any> = (payload: T, task: Task<T>) => Promise<R> | R\n\nexport interface Task<T = any> {\n uuid: string\n payload: T\n createdAt: number\n /** present when the task was pushed via queue.group(key).push() */\n groupKey?: string\n /** number of times this task has been attempted so far */\n attempts: number\n}\n\n\nexport default class Queue<T = any, R = any> extends EventEmitter {\n private redis: RedisClientType\n private workerClients: RedisClientType[] = []\n private options: {\n concurrency: number\n delay: number\n timeout: number\n maxRetries: number\n groups: {\n concurrency: number\n delay: number\n timeout: number\n maxRetries: number\n }\n redisOptions: object\n cleanupInterval: number\n }\n private handler?: TaskHandler<T, R>\n private workers = new Map<string, boolean>()\n private groupWorkers = new Map<string, Map<string, boolean>>()\n private cleanupTimer?: NodeJS.Timeout\n private _ready: Promise<void>\n private _inFlight = 0\n private _totalSettled = 0\n\n constructor(options: QueueOptions<T> = {}) {\n super()\n\n this.options = {\n concurrency: options.concurrency ?? 1,\n delay: ms(options.delay ?? 0),\n timeout: ms(options.timeout ?? 0),\n maxRetries: options.maxRetries ?? 3,\n groups: {\n concurrency: options.groups?.concurrency ?? options.concurrency ?? 1,\n delay: ms(options.groups?.delay ?? options.delay ?? 0),\n timeout: ms(options.groups?.timeout ?? options.timeout ?? 0),\n maxRetries: options.groups?.maxRetries ?? options.maxRetries ?? 3\n },\n redisOptions: options.redisOptions ?? {},\n cleanupInterval: options.cleanupInterval ?? 30000\n }\n\n this.redis = createClient(this.options.redisOptions)\n this.redis.on('error', () => {})\n this._ready = this.initializeRedis()\n }\n\n /** resolves when redis is connected and all workers are ready to accept tasks */\n ready() {\n return this._ready\n }\n\n get inFlight() {\n return this._inFlight\n }\n\n /** register the handler that processes each task. only one handler per queue. */\n process(handler: TaskHandler<T, R>) {\n this.handler = handler\n }\n\n /** push a task to the main queue. returns the task uuid. */\n async push(payload: T): Promise<string> {\n const task: Task<T> = {\n uuid: randomUUID(),\n payload,\n createdAt: Date.now(),\n attempts: 0\n }\n\n await this.redis.lPush('queue:tasks', JSON.stringify(task))\n this.emit('new', { task })\n\n return task.uuid\n }\n\n /** returns a scoped pusher for a named group. each group gets its own worker pool, spun up on first push. */\n group(key: string) {\n return {\n push: async (payload: T): Promise<string> => {\n const task: Task<T> = {\n uuid: randomUUID(),\n payload,\n createdAt: Date.now(),\n groupKey: key,\n attempts: 0\n }\n\n await this.redis.lPush(`queue:groups:${key}`, JSON.stringify(task))\n this.emit('new', { task })\n\n if (!this.groupWorkers.has(key)) {\n this.groupWorkers.set(key, new Map())\n await this.startGroupWorkers(key)\n }\n\n return task.uuid\n }\n }\n }\n\n private async createWorkerClient(): Promise<RedisClientType> {\n const client = this.redis.duplicate() as RedisClientType\n client.on('error', () => {})\n await client.connect()\n this.workerClients.push(client)\n return client\n }\n\n private async startWorkers() {\n const ready = []\n for (let i = 0; i < this.options.concurrency; i++) {\n ready.push(this.startWorker(`worker-${i}`))\n }\n await Promise.all(ready)\n }\n\n private async startGroupWorkers(groupKey: string) {\n const groupWorkers = this.groupWorkers.get(groupKey)!\n const ready = []\n for (let i = 0; i < this.options.groups.concurrency; i++) {\n const workerId = `group-${groupKey}-worker-${i}`\n groupWorkers.set(workerId, true)\n ready.push(this.startGroupWorker(workerId, groupKey))\n }\n await Promise.all(ready)\n }\n\n private async startWorker(workerId: string) {\n this.workers.set(workerId, true)\n const client = await this.createWorkerClient()\n\n this.runWorkerLoop(workerId, client, 'queue:tasks', this.workers, (task) => this.processTask(task))\n }\n\n private async startGroupWorker(workerId: string, groupKey: string) {\n const groupWorkers = this.groupWorkers.get(groupKey)!\n const client = await this.createWorkerClient()\n\n this.runWorkerLoop(workerId, client, `queue:groups:${groupKey}`, groupWorkers, (task) => this.processGroupTask(task))\n }\n\n private async runWorkerLoop(\n workerId: string,\n client: RedisClientType,\n key: string,\n activeMap: Map<string, boolean>,\n processFn: (task: Task<T>) => Promise<void>\n ) {\n const isGrouped = key.startsWith('queue:groups:')\n const delay = isGrouped ? this.options.groups.delay : this.options.delay\n\n while (activeMap.get(workerId)) {\n try {\n if (!client.isOpen) break\n\n const taskData = await client.brPop(key, 1)\n\n if (taskData) {\n const task: Task<T> = JSON.parse(taskData.element)\n this._inFlight++\n await processFn(task)\n }\n\n if (delay > 0) {\n await new Promise(resolve => setTimeout(resolve, delay))\n }\n } catch (err) {\n const error = err as Error\n if (error.message?.includes('closed') || error.message?.includes('ClientClosedError')) {\n break\n }\n }\n }\n }\n\n private async processTask(task: Task<T>) {\n task.attempts++\n\n try {\n if (!this.handler) {\n this.emit('complete', { task, result: undefined })\n this.settle()\n return\n }\n\n const timeoutPromise = this.options.timeout > 0\n ? new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error('Task timeout')), this.options.timeout)\n )\n : null\n\n const workPromise = Promise.resolve(this.handler(task.payload, task))\n\n const result = timeoutPromise\n ? await Promise.race([workPromise, timeoutPromise])\n : await workPromise\n\n this.emit('complete', { task, result })\n this.settle()\n } catch (error) {\n if (task.attempts < this.options.maxRetries) {\n this.emit('retry', { task, error, attempt: task.attempts })\n this._inFlight--\n await this.redis.lPush('queue:tasks', JSON.stringify(task))\n } else {\n this.emit('failed', { task, error })\n this.settle()\n }\n }\n }\n\n private async processGroupTask(task: Task<T>) {\n task.attempts++\n\n try {\n if (!this.handler) {\n this.emit('complete', { task, result: undefined })\n this.settle()\n return\n }\n\n const timeoutPromise = this.options.groups.timeout > 0\n ? new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error('Task timeout')), this.options.groups.timeout)\n )\n : null\n\n const workPromise = Promise.resolve(this.handler(task.payload, task))\n\n const result = timeoutPromise\n ? await Promise.race([workPromise, timeoutPromise])\n : await workPromise\n\n this.emit('complete', { task, result })\n this.settle()\n } catch (error) {\n if (task.attempts < this.options.groups.maxRetries) {\n this.emit('retry', { task, error, attempt: task.attempts })\n this._inFlight--\n await this.redis.lPush(`queue:groups:${task.groupKey}`, JSON.stringify(task))\n } else {\n this.emit('failed', { task, error })\n this.settle()\n }\n }\n }\n\n private settle() {\n this._inFlight--\n this._totalSettled++\n if (this._inFlight === 0 && this._totalSettled > 0) {\n this.emit('drain')\n }\n }\n\n private async initializeRedis() {\n await this.redis.connect()\n await this.startWorkers()\n\n if (this.options.cleanupInterval > 0) {\n this.cleanupTimer = setInterval(() => {\n this.performPeriodicCleanup()\n }, this.options.cleanupInterval)\n }\n }\n\n private async performPeriodicCleanup() {\n try {\n if (!this.redis.isOpen) {\n return\n }\n\n const groupKeys = Array.from(this.groupWorkers.keys())\n\n for (const groupKey of groupKeys) {\n const length = await this.redis.lLen(`queue:groups:${groupKey}`)\n\n if (length === 0) {\n const keyExists = await this.redis.exists(`queue:groups:${groupKey}`)\n if (keyExists) {\n await this.redis.del(`queue:groups:${groupKey}`)\n }\n\n const groupWorkers = this.groupWorkers.get(groupKey)\n if (groupWorkers) {\n groupWorkers.clear()\n this.groupWorkers.delete(groupKey)\n }\n }\n }\n } catch (error) {\n // cleanup error handled silently\n }\n }\n\n /** shuts down all workers and disconnects from redis. waits for initialization to complete first. */\n async close() {\n await this._ready.catch(() => {})\n\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer)\n }\n this.workers.clear()\n for (const groupWorkers of this.groupWorkers.values()) {\n groupWorkers.clear()\n }\n this.groupWorkers.clear()\n\n for (const client of this.workerClients) {\n if (client.isOpen) {\n await client.disconnect()\n }\n }\n this.workerClients = []\n\n if (this.redis.isOpen) {\n await this.redis.quit()\n }\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prsm/queue",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Redis-backed distributed task queue with grouped concurrency, retries, and rate limiting",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",