@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 +113 -69
- package/dist/index.cjs +69 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +69 -34
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,101 +1,145 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @prsm/queue
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Redis-backed distributed task queue with grouped concurrency, retries, and rate limiting.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
4
6
|
|
|
5
7
|
```bash
|
|
6
|
-
|
|
7
|
-
└── app
|
|
8
|
-
└── queues
|
|
9
|
-
├── mailer.ts
|
|
10
|
-
└── llm-processor.ts
|
|
8
|
+
npm install @prsm/queue
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
## Quick Start
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
```ts
|
|
14
|
+
import Queue from '@prsm/queue'
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
const queue = new Queue({
|
|
17
|
+
concurrency: 2,
|
|
18
|
+
maxRetries: 3
|
|
19
|
+
})
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
body: string;
|
|
25
|
-
}
|
|
21
|
+
queue.process(async (payload) => {
|
|
22
|
+
return await doWork(payload)
|
|
23
|
+
})
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
25
|
+
queue.on('complete', ({ task, result }) => {
|
|
26
|
+
console.log('Done:', task.uuid, result)
|
|
27
|
+
})
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
queue.on('failed', ({ task, error }) => {
|
|
30
|
+
console.log('Failed after retries:', task.uuid, error.message)
|
|
31
|
+
})
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
await queue.push({ userId: 123, action: 'sync' })
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Options
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
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:
|
|
50
|
-
delay:
|
|
51
|
-
timeout:
|
|
46
|
+
concurrency: 1, // workers per group
|
|
47
|
+
delay: '50ms',
|
|
48
|
+
timeout: '10s',
|
|
49
|
+
maxRetries: 3
|
|
52
50
|
},
|
|
53
|
-
});
|
|
54
|
-
```
|
|
55
51
|
|
|
56
|
-
|
|
52
|
+
redisOptions: {
|
|
53
|
+
host: 'localhost',
|
|
54
|
+
port: 6379
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
```
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
## Process Handler
|
|
59
60
|
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
68
|
+
Throw an error to trigger retry. After `maxRetries`, the task fails permanently.
|
|
66
69
|
|
|
67
|
-
|
|
68
|
-
2. A job is completed
|
|
69
|
-
3. A job fails
|
|
70
|
+
## Grouped Queues
|
|
70
71
|
|
|
71
|
-
|
|
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
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
74
|
+
```ts
|
|
75
|
+
const queue = new Queue({
|
|
76
|
+
groups: { concurrency: 1, delay: '50ms' }
|
|
77
|
+
})
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
queue.process(async (payload) => {
|
|
80
|
+
return await callExternalAPI(payload)
|
|
81
|
+
})
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
|
141
|
+
await Promise.all(ready);
|
|
118
142
|
}
|
|
119
143
|
async startWorker(workerId) {
|
|
120
144
|
this.workers.set(workerId, true);
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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 (!
|
|
147
|
-
|
|
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
|
-
|
|
162
|
+
this._inFlight++;
|
|
163
|
+
await processFn(task);
|
|
153
164
|
}
|
|
154
|
-
if (
|
|
155
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
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
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -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":"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","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
|
|
105
|
+
await Promise.all(ready);
|
|
82
106
|
}
|
|
83
107
|
async startWorker(workerId) {
|
|
84
108
|
this.workers.set(workerId, true);
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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 (!
|
|
111
|
-
|
|
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
|
-
|
|
126
|
+
this._inFlight++;
|
|
127
|
+
await processFn(task);
|
|
117
128
|
}
|
|
118
|
-
if (
|
|
119
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
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":[]}
|