@prsm/queue 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # Queues
2
+
3
+ You must place your queue definitions in `/queues`. For example:
4
+
5
+ ```bash
6
+ src
7
+ └── app
8
+ └── queues
9
+ ├── mailer.ts
10
+ └── llm-processor.ts
11
+ ```
12
+
13
+ Each queue module is expected to have a default export that is an asynchronous function, which serves as the handler for each queued job. Additionally, a `queue` object must be exported that generates and configures the queue.
14
+
15
+ Here's an example queue definition.
16
+
17
+ ```typescript
18
+ // /src/app/queues/mail.ts
19
+ import Queue from "@prsm/queues";
20
+
21
+ interface MailPayload {
22
+ recipient: string;
23
+ subject: string;
24
+ body: string;
25
+ }
26
+
27
+ // The task to be executed for each queued job when a worker becomes available.
28
+ export default async function ({ recipient, subject, body }: MailPayload) {
29
+ // send an email
30
+ }
31
+
32
+ export const queue = new Queue<MailPayload>({
33
+ // Number of concurrent queue workers.
34
+ concurrency: 1,
35
+
36
+ // Once a worker becomes available, it will wait delay ms before processing the next job.
37
+ delay: 0,
38
+
39
+ // After it is initiated, a task will fail if it does not resolve within timeout ms.
40
+ timeout: 0,
41
+
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
+ groups: {
49
+ concurrency: 3,
50
+ delay: "5s",
51
+ timeout: "1m"
52
+ },
53
+ });
54
+ ```
55
+
56
+ ## Adding jobs to a queue
57
+
58
+ To add a job to the queue, import the queue and call `queue.push`.
59
+
60
+ ```typescript
61
+ import { queue as mailQueue } from "../queues/mail";
62
+ mailQueue.push({ recipient, subject, body });
63
+ ```
64
+
65
+ To track task status, listen for events emitted by the queue. The queue will emit events when:
66
+
67
+ 1. A job is added to the queue
68
+ 2. A job is completed
69
+ 3. A job fails
70
+
71
+ ```typescript
72
+ // /src/app/http/mail/index.ts
73
+ import { queue as mailQueue } from "../queues/mail";
74
+
75
+ const onNew = ({ task }) => console.log("Task created:", task.uuid);
76
+ const onFailed = ({ task }) => console.log("Task failed:", task.uuid);
77
+ const onComplete = ({ task }) => console.log("Task complete:", task.uuid);
78
+
79
+ mailQueue.on("new", onNew);
80
+ mailQueue.on("failed", onFailed);
81
+ mailQueue.on("complete", onComplete);
82
+
83
+ // POST /mail
84
+ export async function post(c: Context, { body: { recipient, subject, body } }) {
85
+ const uuid = mailQueue.push({ recipient, subject, body });
86
+ return Respond.OK(c, { uuid });
87
+ }
88
+
89
+ // /src/app/queues/mail.ts
90
+ export default async function({ recipient, subject, body }) {
91
+ // send a very important email
92
+ }
93
+ ```
94
+
95
+ You can add a grouped queue like:
96
+
97
+ ```typescript
98
+ mailQueue.group(recipient).push({ recipient, subject, body });
99
+ ```
100
+
101
+ As mentioned above, this is a scoped queue. The scoped queue has its own concurrency, delay, and timeout.
package/dist/index.cjs ADDED
@@ -0,0 +1,255 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ default: () => Queue
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/queue.ts
38
+ var import_redis = require("redis");
39
+ var import_events = require("events");
40
+ var import_crypto = require("crypto");
41
+ var import_ms = __toESM(require("@prsm/ms"), 1);
42
+ var Queue = class extends import_events.EventEmitter {
43
+ redis;
44
+ options;
45
+ handler;
46
+ workers = /* @__PURE__ */ new Map();
47
+ groupWorkers = /* @__PURE__ */ new Map();
48
+ cleanupTimer;
49
+ constructor(options = {}) {
50
+ super();
51
+ this.options = {
52
+ concurrency: options.concurrency ?? 1,
53
+ delay: (0, import_ms.default)(options.delay ?? 0),
54
+ timeout: (0, import_ms.default)(options.timeout ?? 0),
55
+ maxRetries: options.maxRetries ?? 3,
56
+ groups: {
57
+ concurrency: options.groups?.concurrency ?? options.concurrency ?? 1,
58
+ delay: (0, import_ms.default)(options.groups?.delay ?? options.delay ?? 0),
59
+ timeout: (0, import_ms.default)(options.groups?.timeout ?? options.timeout ?? 0),
60
+ maxRetries: options.groups?.maxRetries ?? options.maxRetries ?? 3
61
+ },
62
+ redisOptions: options.redisOptions ?? {},
63
+ cleanupInterval: options.cleanupInterval ?? 3e4
64
+ };
65
+ this.redis = (0, import_redis.createClient)(this.options.redisOptions);
66
+ this.redis.on("error", () => {
67
+ });
68
+ this.initializeRedis();
69
+ }
70
+ process(handler) {
71
+ this.handler = handler;
72
+ }
73
+ async push(payload) {
74
+ const task = {
75
+ uuid: (0, import_crypto.randomUUID)(),
76
+ payload,
77
+ createdAt: Date.now(),
78
+ attempts: 0
79
+ };
80
+ await this.redis.lPush("queue:tasks", JSON.stringify(task));
81
+ this.emit("new", { task });
82
+ return task.uuid;
83
+ }
84
+ group(key) {
85
+ return {
86
+ push: async (payload) => {
87
+ const task = {
88
+ uuid: (0, import_crypto.randomUUID)(),
89
+ payload,
90
+ createdAt: Date.now(),
91
+ groupKey: key,
92
+ attempts: 0
93
+ };
94
+ await this.redis.lPush(`queue:groups:${key}`, JSON.stringify(task));
95
+ this.emit("new", { task });
96
+ if (!this.groupWorkers.has(key)) {
97
+ this.groupWorkers.set(key, /* @__PURE__ */ new Map());
98
+ await this.startGroupWorkers(key);
99
+ await new Promise((resolve) => setTimeout(resolve, 50));
100
+ }
101
+ return task.uuid;
102
+ }
103
+ };
104
+ }
105
+ async startWorkers() {
106
+ for (let i = 0; i < this.options.concurrency; i++) {
107
+ this.startWorker(`worker-${i}`);
108
+ }
109
+ }
110
+ async startGroupWorkers(groupKey) {
111
+ const groupWorkers = this.groupWorkers.get(groupKey);
112
+ for (let i = 0; i < this.options.groups.concurrency; i++) {
113
+ const workerId = `group-${groupKey}-worker-${i}`;
114
+ groupWorkers.set(workerId, true);
115
+ this.startGroupWorker(workerId, groupKey);
116
+ }
117
+ await new Promise((resolve) => setTimeout(resolve, 100));
118
+ }
119
+ async startWorker(workerId) {
120
+ this.workers.set(workerId, true);
121
+ while (this.workers.get(workerId)) {
122
+ try {
123
+ if (!this.redis.isOpen) {
124
+ break;
125
+ }
126
+ const taskData = await this.redis.brPop("queue:tasks", 1);
127
+ if (taskData) {
128
+ const task = JSON.parse(taskData.element);
129
+ await this.processTask(task);
130
+ }
131
+ if (this.options.delay > 0) {
132
+ await new Promise((resolve) => setTimeout(resolve, this.options.delay));
133
+ }
134
+ } catch (err) {
135
+ const error = err;
136
+ if (error.message?.includes("closed") || error.message?.includes("ClientClosedError")) {
137
+ break;
138
+ }
139
+ }
140
+ }
141
+ }
142
+ async startGroupWorker(workerId, groupKey) {
143
+ const groupWorkers = this.groupWorkers.get(groupKey);
144
+ while (groupWorkers.get(workerId)) {
145
+ try {
146
+ if (!this.redis.isOpen) {
147
+ break;
148
+ }
149
+ const taskData = await this.redis.brPop(`queue:groups:${groupKey}`, 1);
150
+ if (taskData) {
151
+ const task = JSON.parse(taskData.element);
152
+ await this.processGroupTask(task);
153
+ }
154
+ if (this.options.groups.delay > 0) {
155
+ await new Promise((resolve) => setTimeout(resolve, this.options.groups.delay));
156
+ }
157
+ } catch (err) {
158
+ const error = err;
159
+ if (error.message?.includes("closed") || error.message?.includes("ClientClosedError")) {
160
+ break;
161
+ }
162
+ }
163
+ }
164
+ }
165
+ async processTask(task) {
166
+ task.attempts++;
167
+ try {
168
+ if (!this.handler) {
169
+ this.emit("complete", { task, result: void 0 });
170
+ return;
171
+ }
172
+ const timeoutPromise = this.options.timeout > 0 ? new Promise(
173
+ (_, reject) => setTimeout(() => reject(new Error("Task timeout")), this.options.timeout)
174
+ ) : null;
175
+ const workPromise = Promise.resolve(this.handler(task.payload, task));
176
+ const result = timeoutPromise ? await Promise.race([workPromise, timeoutPromise]) : await workPromise;
177
+ this.emit("complete", { task, result });
178
+ } catch (error) {
179
+ if (task.attempts < this.options.maxRetries) {
180
+ this.emit("retry", { task, error, attempt: task.attempts });
181
+ await this.redis.lPush("queue:tasks", JSON.stringify(task));
182
+ } else {
183
+ this.emit("failed", { task, error });
184
+ }
185
+ }
186
+ }
187
+ async processGroupTask(task) {
188
+ task.attempts++;
189
+ try {
190
+ if (!this.handler) {
191
+ this.emit("complete", { task, result: void 0 });
192
+ return;
193
+ }
194
+ const timeoutPromise = this.options.groups.timeout > 0 ? new Promise(
195
+ (_, reject) => setTimeout(() => reject(new Error("Task timeout")), this.options.groups.timeout)
196
+ ) : null;
197
+ const workPromise = Promise.resolve(this.handler(task.payload, task));
198
+ const result = timeoutPromise ? await Promise.race([workPromise, timeoutPromise]) : await workPromise;
199
+ this.emit("complete", { task, result });
200
+ } catch (error) {
201
+ if (task.attempts < this.options.groups.maxRetries) {
202
+ this.emit("retry", { task, error, attempt: task.attempts });
203
+ await this.redis.lPush(`queue:groups:${task.groupKey}`, JSON.stringify(task));
204
+ } else {
205
+ this.emit("failed", { task, error });
206
+ }
207
+ }
208
+ }
209
+ async initializeRedis() {
210
+ await this.redis.connect();
211
+ this.startWorkers();
212
+ if (this.options.cleanupInterval > 0) {
213
+ this.cleanupTimer = setInterval(() => {
214
+ this.performPeriodicCleanup();
215
+ }, this.options.cleanupInterval);
216
+ }
217
+ }
218
+ async performPeriodicCleanup() {
219
+ try {
220
+ if (!this.redis.isOpen) {
221
+ return;
222
+ }
223
+ const groupKeys = Array.from(this.groupWorkers.keys());
224
+ for (const groupKey of groupKeys) {
225
+ const length = await this.redis.lLen(`queue:groups:${groupKey}`);
226
+ if (length === 0) {
227
+ const keyExists = await this.redis.exists(`queue:groups:${groupKey}`);
228
+ if (keyExists) {
229
+ await this.redis.del(`queue:groups:${groupKey}`);
230
+ }
231
+ const groupWorkers = this.groupWorkers.get(groupKey);
232
+ if (groupWorkers) {
233
+ groupWorkers.clear();
234
+ this.groupWorkers.delete(groupKey);
235
+ }
236
+ }
237
+ }
238
+ } catch (error) {
239
+ }
240
+ }
241
+ async close() {
242
+ if (this.cleanupTimer) {
243
+ clearInterval(this.cleanupTimer);
244
+ }
245
+ this.workers.clear();
246
+ for (const groupWorkers of this.groupWorkers.values()) {
247
+ groupWorkers.clear();
248
+ }
249
+ this.groupWorkers.clear();
250
+ if (this.redis.isOpen) {
251
+ await this.redis.quit();
252
+ }
253
+ }
254
+ };
255
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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"]}
@@ -0,0 +1,54 @@
1
+ import { EventEmitter } from 'events';
2
+
3
+ interface QueueOptions<T = any> {
4
+ concurrency?: number;
5
+ delay?: number | string;
6
+ timeout?: number | string;
7
+ maxRetries?: number;
8
+ groups?: {
9
+ concurrency?: number;
10
+ delay?: number | string;
11
+ timeout?: number | string;
12
+ maxRetries?: number;
13
+ };
14
+ redisOptions?: {
15
+ url?: string;
16
+ host?: string;
17
+ port?: number;
18
+ password?: string;
19
+ };
20
+ cleanupInterval?: number;
21
+ }
22
+ type TaskHandler<T, R = any> = (payload: T, task: Task<T>) => Promise<R> | R;
23
+ interface Task<T = any> {
24
+ uuid: string;
25
+ payload: T;
26
+ createdAt: number;
27
+ groupKey?: string;
28
+ attempts: number;
29
+ }
30
+ declare class Queue<T = any, R = any> extends EventEmitter {
31
+ private redis;
32
+ private options;
33
+ private handler?;
34
+ private workers;
35
+ private groupWorkers;
36
+ private cleanupTimer?;
37
+ constructor(options?: QueueOptions<T>);
38
+ process(handler: TaskHandler<T, R>): void;
39
+ push(payload: T): Promise<string>;
40
+ group(key: string): {
41
+ push: (payload: T) => Promise<string>;
42
+ };
43
+ private startWorkers;
44
+ private startGroupWorkers;
45
+ private startWorker;
46
+ private startGroupWorker;
47
+ private processTask;
48
+ private processGroupTask;
49
+ private initializeRedis;
50
+ private performPeriodicCleanup;
51
+ close(): Promise<void>;
52
+ }
53
+
54
+ export { type QueueOptions, type Task, type TaskHandler, Queue as default };
@@ -0,0 +1,54 @@
1
+ import { EventEmitter } from 'events';
2
+
3
+ interface QueueOptions<T = any> {
4
+ concurrency?: number;
5
+ delay?: number | string;
6
+ timeout?: number | string;
7
+ maxRetries?: number;
8
+ groups?: {
9
+ concurrency?: number;
10
+ delay?: number | string;
11
+ timeout?: number | string;
12
+ maxRetries?: number;
13
+ };
14
+ redisOptions?: {
15
+ url?: string;
16
+ host?: string;
17
+ port?: number;
18
+ password?: string;
19
+ };
20
+ cleanupInterval?: number;
21
+ }
22
+ type TaskHandler<T, R = any> = (payload: T, task: Task<T>) => Promise<R> | R;
23
+ interface Task<T = any> {
24
+ uuid: string;
25
+ payload: T;
26
+ createdAt: number;
27
+ groupKey?: string;
28
+ attempts: number;
29
+ }
30
+ declare class Queue<T = any, R = any> extends EventEmitter {
31
+ private redis;
32
+ private options;
33
+ private handler?;
34
+ private workers;
35
+ private groupWorkers;
36
+ private cleanupTimer?;
37
+ constructor(options?: QueueOptions<T>);
38
+ process(handler: TaskHandler<T, R>): void;
39
+ push(payload: T): Promise<string>;
40
+ group(key: string): {
41
+ push: (payload: T) => Promise<string>;
42
+ };
43
+ private startWorkers;
44
+ private startGroupWorkers;
45
+ private startWorker;
46
+ private startGroupWorker;
47
+ private processTask;
48
+ private processGroupTask;
49
+ private initializeRedis;
50
+ private performPeriodicCleanup;
51
+ close(): Promise<void>;
52
+ }
53
+
54
+ export { type QueueOptions, type Task, type TaskHandler, Queue as default };
package/dist/index.js ADDED
@@ -0,0 +1,222 @@
1
+ // src/queue.ts
2
+ import { createClient } from "redis";
3
+ import { EventEmitter } from "events";
4
+ import { randomUUID } from "crypto";
5
+ import ms from "@prsm/ms";
6
+ var Queue = class extends EventEmitter {
7
+ redis;
8
+ options;
9
+ handler;
10
+ workers = /* @__PURE__ */ new Map();
11
+ groupWorkers = /* @__PURE__ */ new Map();
12
+ cleanupTimer;
13
+ constructor(options = {}) {
14
+ super();
15
+ this.options = {
16
+ concurrency: options.concurrency ?? 1,
17
+ delay: ms(options.delay ?? 0),
18
+ timeout: ms(options.timeout ?? 0),
19
+ maxRetries: options.maxRetries ?? 3,
20
+ groups: {
21
+ concurrency: options.groups?.concurrency ?? options.concurrency ?? 1,
22
+ delay: ms(options.groups?.delay ?? options.delay ?? 0),
23
+ timeout: ms(options.groups?.timeout ?? options.timeout ?? 0),
24
+ maxRetries: options.groups?.maxRetries ?? options.maxRetries ?? 3
25
+ },
26
+ redisOptions: options.redisOptions ?? {},
27
+ cleanupInterval: options.cleanupInterval ?? 3e4
28
+ };
29
+ this.redis = createClient(this.options.redisOptions);
30
+ this.redis.on("error", () => {
31
+ });
32
+ this.initializeRedis();
33
+ }
34
+ process(handler) {
35
+ this.handler = handler;
36
+ }
37
+ async push(payload) {
38
+ const task = {
39
+ uuid: randomUUID(),
40
+ payload,
41
+ createdAt: Date.now(),
42
+ attempts: 0
43
+ };
44
+ await this.redis.lPush("queue:tasks", JSON.stringify(task));
45
+ this.emit("new", { task });
46
+ return task.uuid;
47
+ }
48
+ group(key) {
49
+ return {
50
+ push: async (payload) => {
51
+ const task = {
52
+ uuid: randomUUID(),
53
+ payload,
54
+ createdAt: Date.now(),
55
+ groupKey: key,
56
+ attempts: 0
57
+ };
58
+ await this.redis.lPush(`queue:groups:${key}`, JSON.stringify(task));
59
+ this.emit("new", { task });
60
+ if (!this.groupWorkers.has(key)) {
61
+ this.groupWorkers.set(key, /* @__PURE__ */ new Map());
62
+ await this.startGroupWorkers(key);
63
+ await new Promise((resolve) => setTimeout(resolve, 50));
64
+ }
65
+ return task.uuid;
66
+ }
67
+ };
68
+ }
69
+ async startWorkers() {
70
+ for (let i = 0; i < this.options.concurrency; i++) {
71
+ this.startWorker(`worker-${i}`);
72
+ }
73
+ }
74
+ async startGroupWorkers(groupKey) {
75
+ const groupWorkers = this.groupWorkers.get(groupKey);
76
+ for (let i = 0; i < this.options.groups.concurrency; i++) {
77
+ const workerId = `group-${groupKey}-worker-${i}`;
78
+ groupWorkers.set(workerId, true);
79
+ this.startGroupWorker(workerId, groupKey);
80
+ }
81
+ await new Promise((resolve) => setTimeout(resolve, 100));
82
+ }
83
+ async startWorker(workerId) {
84
+ this.workers.set(workerId, true);
85
+ while (this.workers.get(workerId)) {
86
+ try {
87
+ if (!this.redis.isOpen) {
88
+ break;
89
+ }
90
+ const taskData = await this.redis.brPop("queue:tasks", 1);
91
+ if (taskData) {
92
+ const task = JSON.parse(taskData.element);
93
+ await this.processTask(task);
94
+ }
95
+ if (this.options.delay > 0) {
96
+ await new Promise((resolve) => setTimeout(resolve, this.options.delay));
97
+ }
98
+ } catch (err) {
99
+ const error = err;
100
+ if (error.message?.includes("closed") || error.message?.includes("ClientClosedError")) {
101
+ break;
102
+ }
103
+ }
104
+ }
105
+ }
106
+ async startGroupWorker(workerId, groupKey) {
107
+ const groupWorkers = this.groupWorkers.get(groupKey);
108
+ while (groupWorkers.get(workerId)) {
109
+ try {
110
+ if (!this.redis.isOpen) {
111
+ break;
112
+ }
113
+ const taskData = await this.redis.brPop(`queue:groups:${groupKey}`, 1);
114
+ if (taskData) {
115
+ const task = JSON.parse(taskData.element);
116
+ await this.processGroupTask(task);
117
+ }
118
+ if (this.options.groups.delay > 0) {
119
+ await new Promise((resolve) => setTimeout(resolve, this.options.groups.delay));
120
+ }
121
+ } catch (err) {
122
+ const error = err;
123
+ if (error.message?.includes("closed") || error.message?.includes("ClientClosedError")) {
124
+ break;
125
+ }
126
+ }
127
+ }
128
+ }
129
+ async processTask(task) {
130
+ task.attempts++;
131
+ try {
132
+ if (!this.handler) {
133
+ this.emit("complete", { task, result: void 0 });
134
+ return;
135
+ }
136
+ const timeoutPromise = this.options.timeout > 0 ? new Promise(
137
+ (_, reject) => setTimeout(() => reject(new Error("Task timeout")), this.options.timeout)
138
+ ) : null;
139
+ const workPromise = Promise.resolve(this.handler(task.payload, task));
140
+ const result = timeoutPromise ? await Promise.race([workPromise, timeoutPromise]) : await workPromise;
141
+ this.emit("complete", { task, result });
142
+ } catch (error) {
143
+ if (task.attempts < this.options.maxRetries) {
144
+ this.emit("retry", { task, error, attempt: task.attempts });
145
+ await this.redis.lPush("queue:tasks", JSON.stringify(task));
146
+ } else {
147
+ this.emit("failed", { task, error });
148
+ }
149
+ }
150
+ }
151
+ async processGroupTask(task) {
152
+ task.attempts++;
153
+ try {
154
+ if (!this.handler) {
155
+ this.emit("complete", { task, result: void 0 });
156
+ return;
157
+ }
158
+ const timeoutPromise = this.options.groups.timeout > 0 ? new Promise(
159
+ (_, reject) => setTimeout(() => reject(new Error("Task timeout")), this.options.groups.timeout)
160
+ ) : null;
161
+ const workPromise = Promise.resolve(this.handler(task.payload, task));
162
+ const result = timeoutPromise ? await Promise.race([workPromise, timeoutPromise]) : await workPromise;
163
+ this.emit("complete", { task, result });
164
+ } catch (error) {
165
+ if (task.attempts < this.options.groups.maxRetries) {
166
+ this.emit("retry", { task, error, attempt: task.attempts });
167
+ await this.redis.lPush(`queue:groups:${task.groupKey}`, JSON.stringify(task));
168
+ } else {
169
+ this.emit("failed", { task, error });
170
+ }
171
+ }
172
+ }
173
+ async initializeRedis() {
174
+ await this.redis.connect();
175
+ this.startWorkers();
176
+ if (this.options.cleanupInterval > 0) {
177
+ this.cleanupTimer = setInterval(() => {
178
+ this.performPeriodicCleanup();
179
+ }, this.options.cleanupInterval);
180
+ }
181
+ }
182
+ async performPeriodicCleanup() {
183
+ try {
184
+ if (!this.redis.isOpen) {
185
+ return;
186
+ }
187
+ const groupKeys = Array.from(this.groupWorkers.keys());
188
+ for (const groupKey of groupKeys) {
189
+ const length = await this.redis.lLen(`queue:groups:${groupKey}`);
190
+ if (length === 0) {
191
+ const keyExists = await this.redis.exists(`queue:groups:${groupKey}`);
192
+ if (keyExists) {
193
+ await this.redis.del(`queue:groups:${groupKey}`);
194
+ }
195
+ const groupWorkers = this.groupWorkers.get(groupKey);
196
+ if (groupWorkers) {
197
+ groupWorkers.clear();
198
+ this.groupWorkers.delete(groupKey);
199
+ }
200
+ }
201
+ }
202
+ } catch (error) {
203
+ }
204
+ }
205
+ async close() {
206
+ if (this.cleanupTimer) {
207
+ clearInterval(this.cleanupTimer);
208
+ }
209
+ this.workers.clear();
210
+ for (const groupWorkers of this.groupWorkers.values()) {
211
+ groupWorkers.clear();
212
+ }
213
+ this.groupWorkers.clear();
214
+ if (this.redis.isOpen) {
215
+ await this.redis.quit();
216
+ }
217
+ }
218
+ };
219
+ export {
220
+ Queue as default
221
+ };
222
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":[]}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@prsm/queue",
3
+ "version": "1.0.0",
4
+ "description": "Redis-backed distributed task queue with grouped concurrency, retries, and rate limiting",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "test": "vitest",
27
+ "test:run": "vitest run",
28
+ "prepublishOnly": "npm run build"
29
+ },
30
+ "keywords": [
31
+ "queue",
32
+ "redis",
33
+ "distributed",
34
+ "task",
35
+ "job",
36
+ "worker",
37
+ "rate-limit",
38
+ "concurrency",
39
+ "retry"
40
+ ],
41
+ "author": "",
42
+ "license": "MIT",
43
+ "dependencies": {
44
+ "@prsm/ms": "^1.0.1",
45
+ "redis": "^5.1.1"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^22.15.29",
49
+ "tsup": "^8.5.0",
50
+ "typescript": "^5.8.3",
51
+ "vitest": "^3.1.4"
52
+ },
53
+ "engines": {
54
+ "node": ">=18"
55
+ }
56
+ }