@prsm/queue 1.0.0 → 1.0.1
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 +48 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +48 -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,13 @@ 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;
|
|
49
51
|
constructor(options = {}) {
|
|
50
52
|
super();
|
|
51
53
|
this.options = {
|
|
@@ -65,11 +67,17 @@ var Queue = class extends import_events.EventEmitter {
|
|
|
65
67
|
this.redis = (0, import_redis.createClient)(this.options.redisOptions);
|
|
66
68
|
this.redis.on("error", () => {
|
|
67
69
|
});
|
|
68
|
-
this.initializeRedis();
|
|
70
|
+
this._ready = this.initializeRedis();
|
|
69
71
|
}
|
|
72
|
+
/** resolves when redis is connected and all workers are ready to accept tasks */
|
|
73
|
+
ready() {
|
|
74
|
+
return this._ready;
|
|
75
|
+
}
|
|
76
|
+
/** register the handler that processes each task. only one handler per queue. */
|
|
70
77
|
process(handler) {
|
|
71
78
|
this.handler = handler;
|
|
72
79
|
}
|
|
80
|
+
/** push a task to the main queue. returns the task uuid. */
|
|
73
81
|
async push(payload) {
|
|
74
82
|
const task = {
|
|
75
83
|
uuid: (0, import_crypto.randomUUID)(),
|
|
@@ -81,6 +89,7 @@ var Queue = class extends import_events.EventEmitter {
|
|
|
81
89
|
this.emit("new", { task });
|
|
82
90
|
return task.uuid;
|
|
83
91
|
}
|
|
92
|
+
/** returns a scoped pusher for a named group. each group gets its own worker pool, spun up on first push. */
|
|
84
93
|
group(key) {
|
|
85
94
|
return {
|
|
86
95
|
push: async (payload) => {
|
|
@@ -96,63 +105,59 @@ var Queue = class extends import_events.EventEmitter {
|
|
|
96
105
|
if (!this.groupWorkers.has(key)) {
|
|
97
106
|
this.groupWorkers.set(key, /* @__PURE__ */ new Map());
|
|
98
107
|
await this.startGroupWorkers(key);
|
|
99
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
100
108
|
}
|
|
101
109
|
return task.uuid;
|
|
102
110
|
}
|
|
103
111
|
};
|
|
104
112
|
}
|
|
113
|
+
async createWorkerClient() {
|
|
114
|
+
const client = this.redis.duplicate();
|
|
115
|
+
client.on("error", () => {
|
|
116
|
+
});
|
|
117
|
+
await client.connect();
|
|
118
|
+
this.workerClients.push(client);
|
|
119
|
+
return client;
|
|
120
|
+
}
|
|
105
121
|
async startWorkers() {
|
|
122
|
+
const ready = [];
|
|
106
123
|
for (let i = 0; i < this.options.concurrency; i++) {
|
|
107
|
-
this.startWorker(`worker-${i}`);
|
|
124
|
+
ready.push(this.startWorker(`worker-${i}`));
|
|
108
125
|
}
|
|
126
|
+
await Promise.all(ready);
|
|
109
127
|
}
|
|
110
128
|
async startGroupWorkers(groupKey) {
|
|
111
129
|
const groupWorkers = this.groupWorkers.get(groupKey);
|
|
130
|
+
const ready = [];
|
|
112
131
|
for (let i = 0; i < this.options.groups.concurrency; i++) {
|
|
113
132
|
const workerId = `group-${groupKey}-worker-${i}`;
|
|
114
133
|
groupWorkers.set(workerId, true);
|
|
115
|
-
this.startGroupWorker(workerId, groupKey);
|
|
134
|
+
ready.push(this.startGroupWorker(workerId, groupKey));
|
|
116
135
|
}
|
|
117
|
-
await
|
|
136
|
+
await Promise.all(ready);
|
|
118
137
|
}
|
|
119
138
|
async startWorker(workerId) {
|
|
120
139
|
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
|
-
}
|
|
140
|
+
const client = await this.createWorkerClient();
|
|
141
|
+
this.runWorkerLoop(workerId, client, "queue:tasks", this.workers, (task) => this.processTask(task));
|
|
141
142
|
}
|
|
142
143
|
async startGroupWorker(workerId, groupKey) {
|
|
143
144
|
const groupWorkers = this.groupWorkers.get(groupKey);
|
|
144
|
-
|
|
145
|
+
const client = await this.createWorkerClient();
|
|
146
|
+
this.runWorkerLoop(workerId, client, `queue:groups:${groupKey}`, groupWorkers, (task) => this.processGroupTask(task));
|
|
147
|
+
}
|
|
148
|
+
async runWorkerLoop(workerId, client, key, activeMap, processFn) {
|
|
149
|
+
const isGrouped = key.startsWith("queue:groups:");
|
|
150
|
+
const delay = isGrouped ? this.options.groups.delay : this.options.delay;
|
|
151
|
+
while (activeMap.get(workerId)) {
|
|
145
152
|
try {
|
|
146
|
-
if (!
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
const taskData = await this.redis.brPop(`queue:groups:${groupKey}`, 1);
|
|
153
|
+
if (!client.isOpen) break;
|
|
154
|
+
const taskData = await client.brPop(key, 1);
|
|
150
155
|
if (taskData) {
|
|
151
156
|
const task = JSON.parse(taskData.element);
|
|
152
|
-
await
|
|
157
|
+
await processFn(task);
|
|
153
158
|
}
|
|
154
|
-
if (
|
|
155
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
159
|
+
if (delay > 0) {
|
|
160
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
156
161
|
}
|
|
157
162
|
} catch (err) {
|
|
158
163
|
const error = err;
|
|
@@ -208,7 +213,7 @@ var Queue = class extends import_events.EventEmitter {
|
|
|
208
213
|
}
|
|
209
214
|
async initializeRedis() {
|
|
210
215
|
await this.redis.connect();
|
|
211
|
-
this.startWorkers();
|
|
216
|
+
await this.startWorkers();
|
|
212
217
|
if (this.options.cleanupInterval > 0) {
|
|
213
218
|
this.cleanupTimer = setInterval(() => {
|
|
214
219
|
this.performPeriodicCleanup();
|
|
@@ -238,7 +243,10 @@ var Queue = class extends import_events.EventEmitter {
|
|
|
238
243
|
} catch (error) {
|
|
239
244
|
}
|
|
240
245
|
}
|
|
246
|
+
/** shuts down all workers and disconnects from redis. waits for initialization to complete first. */
|
|
241
247
|
async close() {
|
|
248
|
+
await this._ready.catch(() => {
|
|
249
|
+
});
|
|
242
250
|
if (this.cleanupTimer) {
|
|
243
251
|
clearInterval(this.cleanupTimer);
|
|
244
252
|
}
|
|
@@ -247,6 +255,12 @@ var Queue = class extends import_events.EventEmitter {
|
|
|
247
255
|
groupWorkers.clear();
|
|
248
256
|
}
|
|
249
257
|
this.groupWorkers.clear();
|
|
258
|
+
for (const client of this.workerClients) {
|
|
259
|
+
if (client.isOpen) {
|
|
260
|
+
await client.disconnect();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
this.workerClients = [];
|
|
250
264
|
if (this.redis.isOpen) {
|
|
251
265
|
await this.redis.quit();
|
|
252
266
|
}
|
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\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 /** 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 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 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 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,EAER,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;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,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;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;AAAA,IACxC,SAAS,OAAO;AACd,UAAI,KAAK,WAAW,KAAK,QAAQ,YAAY;AAC3C,aAAK,KAAK,SAAS,EAAE,MAAM,OAAO,SAAS,KAAK,SAAS,CAAC;AAC1D,cAAM,KAAK,MAAM,MAAM,eAAe,KAAK,UAAU,IAAI,CAAC;AAAA,MAC5D,OAAO;AACL,aAAK,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAAA,MACrC;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;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;AAAA,IACxC,SAAS,OAAO;AACd,UAAI,KAAK,WAAW,KAAK,QAAQ,OAAO,YAAY;AAClD,aAAK,KAAK,SAAS,EAAE,MAAM,OAAO,SAAS,KAAK,SAAS,CAAC;AAC1D,cAAM,KAAK,MAAM,MAAM,gBAAgB,KAAK,QAAQ,IAAI,KAAK,UAAU,IAAI,CAAC;AAAA,MAC9E,OAAO;AACL,aAAK,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAAA,MACrC;AAAA,IACF;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,51 @@ 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;
|
|
37
48
|
constructor(options?: QueueOptions<T>);
|
|
49
|
+
/** resolves when redis is connected and all workers are ready to accept tasks */
|
|
50
|
+
ready(): Promise<void>;
|
|
51
|
+
/** register the handler that processes each task. only one handler per queue. */
|
|
38
52
|
process(handler: TaskHandler<T, R>): void;
|
|
53
|
+
/** push a task to the main queue. returns the task uuid. */
|
|
39
54
|
push(payload: T): Promise<string>;
|
|
55
|
+
/** returns a scoped pusher for a named group. each group gets its own worker pool, spun up on first push. */
|
|
40
56
|
group(key: string): {
|
|
41
57
|
push: (payload: T) => Promise<string>;
|
|
42
58
|
};
|
|
59
|
+
private createWorkerClient;
|
|
43
60
|
private startWorkers;
|
|
44
61
|
private startGroupWorkers;
|
|
45
62
|
private startWorker;
|
|
46
63
|
private startGroupWorker;
|
|
64
|
+
private runWorkerLoop;
|
|
47
65
|
private processTask;
|
|
48
66
|
private processGroupTask;
|
|
49
67
|
private initializeRedis;
|
|
50
68
|
private performPeriodicCleanup;
|
|
69
|
+
/** shuts down all workers and disconnects from redis. waits for initialization to complete first. */
|
|
51
70
|
close(): Promise<void>;
|
|
52
71
|
}
|
|
53
72
|
|
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,51 @@ 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;
|
|
37
48
|
constructor(options?: QueueOptions<T>);
|
|
49
|
+
/** resolves when redis is connected and all workers are ready to accept tasks */
|
|
50
|
+
ready(): Promise<void>;
|
|
51
|
+
/** register the handler that processes each task. only one handler per queue. */
|
|
38
52
|
process(handler: TaskHandler<T, R>): void;
|
|
53
|
+
/** push a task to the main queue. returns the task uuid. */
|
|
39
54
|
push(payload: T): Promise<string>;
|
|
55
|
+
/** returns a scoped pusher for a named group. each group gets its own worker pool, spun up on first push. */
|
|
40
56
|
group(key: string): {
|
|
41
57
|
push: (payload: T) => Promise<string>;
|
|
42
58
|
};
|
|
59
|
+
private createWorkerClient;
|
|
43
60
|
private startWorkers;
|
|
44
61
|
private startGroupWorkers;
|
|
45
62
|
private startWorker;
|
|
46
63
|
private startGroupWorker;
|
|
64
|
+
private runWorkerLoop;
|
|
47
65
|
private processTask;
|
|
48
66
|
private processGroupTask;
|
|
49
67
|
private initializeRedis;
|
|
50
68
|
private performPeriodicCleanup;
|
|
69
|
+
/** shuts down all workers and disconnects from redis. waits for initialization to complete first. */
|
|
51
70
|
close(): Promise<void>;
|
|
52
71
|
}
|
|
53
72
|
|
package/dist/index.js
CHANGED
|
@@ -5,11 +5,13 @@ 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;
|
|
13
15
|
constructor(options = {}) {
|
|
14
16
|
super();
|
|
15
17
|
this.options = {
|
|
@@ -29,11 +31,17 @@ var Queue = class extends EventEmitter {
|
|
|
29
31
|
this.redis = createClient(this.options.redisOptions);
|
|
30
32
|
this.redis.on("error", () => {
|
|
31
33
|
});
|
|
32
|
-
this.initializeRedis();
|
|
34
|
+
this._ready = this.initializeRedis();
|
|
33
35
|
}
|
|
36
|
+
/** resolves when redis is connected and all workers are ready to accept tasks */
|
|
37
|
+
ready() {
|
|
38
|
+
return this._ready;
|
|
39
|
+
}
|
|
40
|
+
/** register the handler that processes each task. only one handler per queue. */
|
|
34
41
|
process(handler) {
|
|
35
42
|
this.handler = handler;
|
|
36
43
|
}
|
|
44
|
+
/** push a task to the main queue. returns the task uuid. */
|
|
37
45
|
async push(payload) {
|
|
38
46
|
const task = {
|
|
39
47
|
uuid: randomUUID(),
|
|
@@ -45,6 +53,7 @@ var Queue = class extends EventEmitter {
|
|
|
45
53
|
this.emit("new", { task });
|
|
46
54
|
return task.uuid;
|
|
47
55
|
}
|
|
56
|
+
/** returns a scoped pusher for a named group. each group gets its own worker pool, spun up on first push. */
|
|
48
57
|
group(key) {
|
|
49
58
|
return {
|
|
50
59
|
push: async (payload) => {
|
|
@@ -60,63 +69,59 @@ var Queue = class extends EventEmitter {
|
|
|
60
69
|
if (!this.groupWorkers.has(key)) {
|
|
61
70
|
this.groupWorkers.set(key, /* @__PURE__ */ new Map());
|
|
62
71
|
await this.startGroupWorkers(key);
|
|
63
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
64
72
|
}
|
|
65
73
|
return task.uuid;
|
|
66
74
|
}
|
|
67
75
|
};
|
|
68
76
|
}
|
|
77
|
+
async createWorkerClient() {
|
|
78
|
+
const client = this.redis.duplicate();
|
|
79
|
+
client.on("error", () => {
|
|
80
|
+
});
|
|
81
|
+
await client.connect();
|
|
82
|
+
this.workerClients.push(client);
|
|
83
|
+
return client;
|
|
84
|
+
}
|
|
69
85
|
async startWorkers() {
|
|
86
|
+
const ready = [];
|
|
70
87
|
for (let i = 0; i < this.options.concurrency; i++) {
|
|
71
|
-
this.startWorker(`worker-${i}`);
|
|
88
|
+
ready.push(this.startWorker(`worker-${i}`));
|
|
72
89
|
}
|
|
90
|
+
await Promise.all(ready);
|
|
73
91
|
}
|
|
74
92
|
async startGroupWorkers(groupKey) {
|
|
75
93
|
const groupWorkers = this.groupWorkers.get(groupKey);
|
|
94
|
+
const ready = [];
|
|
76
95
|
for (let i = 0; i < this.options.groups.concurrency; i++) {
|
|
77
96
|
const workerId = `group-${groupKey}-worker-${i}`;
|
|
78
97
|
groupWorkers.set(workerId, true);
|
|
79
|
-
this.startGroupWorker(workerId, groupKey);
|
|
98
|
+
ready.push(this.startGroupWorker(workerId, groupKey));
|
|
80
99
|
}
|
|
81
|
-
await
|
|
100
|
+
await Promise.all(ready);
|
|
82
101
|
}
|
|
83
102
|
async startWorker(workerId) {
|
|
84
103
|
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
|
-
}
|
|
104
|
+
const client = await this.createWorkerClient();
|
|
105
|
+
this.runWorkerLoop(workerId, client, "queue:tasks", this.workers, (task) => this.processTask(task));
|
|
105
106
|
}
|
|
106
107
|
async startGroupWorker(workerId, groupKey) {
|
|
107
108
|
const groupWorkers = this.groupWorkers.get(groupKey);
|
|
108
|
-
|
|
109
|
+
const client = await this.createWorkerClient();
|
|
110
|
+
this.runWorkerLoop(workerId, client, `queue:groups:${groupKey}`, groupWorkers, (task) => this.processGroupTask(task));
|
|
111
|
+
}
|
|
112
|
+
async runWorkerLoop(workerId, client, key, activeMap, processFn) {
|
|
113
|
+
const isGrouped = key.startsWith("queue:groups:");
|
|
114
|
+
const delay = isGrouped ? this.options.groups.delay : this.options.delay;
|
|
115
|
+
while (activeMap.get(workerId)) {
|
|
109
116
|
try {
|
|
110
|
-
if (!
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
const taskData = await this.redis.brPop(`queue:groups:${groupKey}`, 1);
|
|
117
|
+
if (!client.isOpen) break;
|
|
118
|
+
const taskData = await client.brPop(key, 1);
|
|
114
119
|
if (taskData) {
|
|
115
120
|
const task = JSON.parse(taskData.element);
|
|
116
|
-
await
|
|
121
|
+
await processFn(task);
|
|
117
122
|
}
|
|
118
|
-
if (
|
|
119
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
123
|
+
if (delay > 0) {
|
|
124
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
120
125
|
}
|
|
121
126
|
} catch (err) {
|
|
122
127
|
const error = err;
|
|
@@ -172,7 +177,7 @@ var Queue = class extends EventEmitter {
|
|
|
172
177
|
}
|
|
173
178
|
async initializeRedis() {
|
|
174
179
|
await this.redis.connect();
|
|
175
|
-
this.startWorkers();
|
|
180
|
+
await this.startWorkers();
|
|
176
181
|
if (this.options.cleanupInterval > 0) {
|
|
177
182
|
this.cleanupTimer = setInterval(() => {
|
|
178
183
|
this.performPeriodicCleanup();
|
|
@@ -202,7 +207,10 @@ var Queue = class extends EventEmitter {
|
|
|
202
207
|
} catch (error) {
|
|
203
208
|
}
|
|
204
209
|
}
|
|
210
|
+
/** shuts down all workers and disconnects from redis. waits for initialization to complete first. */
|
|
205
211
|
async close() {
|
|
212
|
+
await this._ready.catch(() => {
|
|
213
|
+
});
|
|
206
214
|
if (this.cleanupTimer) {
|
|
207
215
|
clearInterval(this.cleanupTimer);
|
|
208
216
|
}
|
|
@@ -211,6 +219,12 @@ var Queue = class extends EventEmitter {
|
|
|
211
219
|
groupWorkers.clear();
|
|
212
220
|
}
|
|
213
221
|
this.groupWorkers.clear();
|
|
222
|
+
for (const client of this.workerClients) {
|
|
223
|
+
if (client.isOpen) {
|
|
224
|
+
await client.disconnect();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
this.workerClients = [];
|
|
214
228
|
if (this.redis.isOpen) {
|
|
215
229
|
await this.redis.quit();
|
|
216
230
|
}
|
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\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 /** 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 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 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 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":[]}
|