@prsm/queue 2.1.0 → 3.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 CHANGED
@@ -110,7 +110,7 @@ Throw an error to trigger retry. After `maxRetries`, the task fails permanently.
110
110
 
111
111
  ## Grouped Queues
112
112
 
113
- Isolated concurrency per key - perfect for per-tenant throttling.
113
+ Isolated concurrency per key - perfect for per-tenant throttling. Pass `{ group }` as the second argument to `push` or `pushAndWait`.
114
114
 
115
115
  ```js
116
116
  const queue = new Queue({
@@ -124,11 +124,11 @@ queue.process(async (payload) => {
124
124
 
125
125
  await queue.ready()
126
126
 
127
- await queue.group('tenant-123').push({ action: 'sync' })
128
- await queue.group('tenant-456').push({ action: 'sync' })
127
+ await queue.push({ action: 'sync' }, { group: 'tenant-123' })
128
+ await queue.push({ action: 'sync' }, { group: 'tenant-456' })
129
129
  ```
130
130
 
131
- Each tenant processes independently. One slow tenant won't block others. Total concurrent tasks across all tenants is capped by `concurrency`.
131
+ Each tenant processes independently. One slow tenant won't block others. Total concurrent tasks across all tenants is capped by `concurrency`. When the group is conditional, just omit the option - no branching needed.
132
132
 
133
133
  ## Events
134
134
 
@@ -147,7 +147,7 @@ queue.on('drain', () => {})
147
147
  uuid: string,
148
148
  payload: any,
149
149
  createdAt: number,
150
- groupKey?: string, // present when pushed via group()
150
+ groupKey?: string, // present when pushed with { group }
151
151
  attempts: number
152
152
  }
153
153
  ```
@@ -169,7 +169,7 @@ queue.process(async ({ prompt }) => {
169
169
 
170
170
  app.post('/api/generate', async (req, res) => {
171
171
  const { tenantId, prompt } = req.body
172
- const taskId = await queue.group(tenantId).push({ prompt })
172
+ const taskId = await queue.push({ prompt }, { group: tenantId })
173
173
  res.json({ queued: true, taskId })
174
174
  })
175
175
  ```
@@ -202,10 +202,10 @@ queue.on('failed', ({ task, error }) => {
202
202
  })
203
203
 
204
204
  mesh.exposeCommand('generate-report', async (ctx) => {
205
- const taskId = await queue.group(ctx.connection.id).push({
205
+ const taskId = await queue.push({
206
206
  connectionId: ctx.connection.id,
207
207
  ...ctx.payload,
208
- })
208
+ }, { group: ctx.connection.id })
209
209
  return { queued: true, taskId }
210
210
  })
211
211
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prsm/queue",
3
- "version": "2.1.0",
3
+ "version": "3.0.0",
4
4
  "description": "Redis-backed distributed task queue with grouped concurrency, retries, and rate limiting",
5
5
  "type": "module",
6
6
  "exports": {
package/src/queue.js CHANGED
@@ -151,68 +151,58 @@ export default class Queue extends EventEmitter {
151
151
 
152
152
  /**
153
153
  * @param {any} payload
154
+ * @param {{ group?: string }} [options]
154
155
  * @returns {Promise<string>}
155
156
  */
156
- async push(payload) {
157
+ async push(payload, { group } = {}) {
157
158
  if (this._closed) throw new Error("Queue is closed")
158
- const task = { uuid: randomUUID(), payload, createdAt: Date.now(), attempts: 0 }
159
- await this._redis.lPush("queue:tasks", JSON.stringify(task))
159
+ const task = group
160
+ ? { uuid: randomUUID(), payload, createdAt: Date.now(), groupKey: group, attempts: 0 }
161
+ : { uuid: randomUUID(), payload, createdAt: Date.now(), attempts: 0 }
160
162
  this._pushed++
163
+ try {
164
+ await this._enqueue(task, group)
165
+ } catch (err) {
166
+ this._pushed--
167
+ throw err
168
+ }
161
169
  this.emit("new", { task })
162
170
  return task.uuid
163
171
  }
164
172
 
165
173
  /**
166
174
  * @param {any} payload
167
- * @param {number|string} [timeout] - max time to wait for result, ms or string like "30s" (default 0, no limit)
175
+ * @param {{ group?: string, timeout?: number|string }} [options]
168
176
  * @returns {Promise<any>}
169
177
  */
170
- pushAndWait(payload, timeout = 0) {
178
+ pushAndWait(payload, { group, timeout = 0 } = {}) {
171
179
  if (this._closed) return Promise.reject(new Error("Queue is closed"))
172
- const task = { uuid: randomUUID(), payload, createdAt: Date.now(), attempts: 0 }
180
+ const task = group
181
+ ? { uuid: randomUUID(), payload, createdAt: Date.now(), groupKey: group, attempts: 0 }
182
+ : { uuid: randomUUID(), payload, createdAt: Date.now(), attempts: 0 }
183
+ this._pushed++
173
184
  const result = this._awaitTask(task.uuid, timeout)
174
185
  result.catch(() => {})
175
- return this._redis.lPush("queue:tasks", JSON.stringify(task)).then(() => {
176
- this._pushed++
186
+ return this._enqueue(task, group).then(() => {
177
187
  this.emit("new", { task })
178
188
  return result
189
+ }, (err) => {
190
+ this._pushed--
191
+ throw err
179
192
  })
180
193
  }
181
194
 
182
- /**
183
- * @param {string} key
184
- * @returns {{ push: (payload: any) => Promise<string>, pushAndWait: (payload: any, timeout?: number|string) => Promise<any> }}
185
- */
186
- group(key) {
187
- const makeTask = (payload) => {
188
- if (this._closed) throw new Error("Queue is closed")
189
- return { uuid: randomUUID(), payload, createdAt: Date.now(), groupKey: key, attempts: 0 }
190
- }
191
-
192
- const commit = (task) => {
193
- return this._redis.lPush(`queue:groups:${key}`, JSON.stringify(task)).then(async () => {
194
- this._pushed++
195
- this.emit("new", { task })
196
- if (!this._groupWorkers.has(key)) {
197
- this._groupWorkers.set(key, new Map())
198
- this._groupInFlight.set(key, 0)
199
- await this._startGroupWorkers(key)
200
- }
201
- })
202
- }
203
-
204
- return {
205
- push: async (payload) => {
206
- const task = makeTask(payload)
207
- await commit(task)
208
- return task.uuid
209
- },
210
- pushAndWait: (payload, timeout = 0) => {
211
- const task = makeTask(payload)
212
- const result = this._awaitTask(task.uuid, timeout)
213
- result.catch(() => {})
214
- return commit(task).then(() => result)
215
- },
195
+ /** @private */
196
+ async _enqueue(task, group) {
197
+ if (group) {
198
+ await this._redis.lPush(`queue:groups:${group}`, JSON.stringify(task))
199
+ if (!this._groupWorkers.has(group)) {
200
+ this._groupWorkers.set(group, new Map())
201
+ this._groupInFlight.set(group, 0)
202
+ await this._startGroupWorkers(group)
203
+ }
204
+ } else {
205
+ await this._redis.lPush("queue:tasks", JSON.stringify(task))
216
206
  }
217
207
  }
218
208
 
@@ -259,6 +249,7 @@ export default class Queue extends EventEmitter {
259
249
  await this._readyPromise.catch(() => {})
260
250
 
261
251
  if (this._cleanupTimer) clearInterval(this._cleanupTimer)
252
+ clearTimeout(this._drainTimer)
262
253
 
263
254
  this._workers.clear()
264
255
  for (const groupWorkers of this._groupWorkers.values()) groupWorkers.clear()
@@ -502,7 +493,10 @@ export default class Queue extends EventEmitter {
502
493
  }
503
494
 
504
495
  _emitDrain() {
505
- if (this._inFlight === 0 && this._totalSettled >= this._pushed) this.emit("drain")
496
+ clearTimeout(this._drainTimer)
497
+ this._drainTimer = setTimeout(() => {
498
+ if (this._inFlight === 0 && this._totalSettled >= this._pushed) this.emit("drain")
499
+ }, 0)
506
500
  }
507
501
 
508
502
  async _periodicCleanup() {
package/types/queue.d.ts CHANGED
@@ -2357,23 +2357,23 @@ export default class Queue extends EventEmitter<[never]> {
2357
2357
  process(handler: TaskHandler): void;
2358
2358
  /**
2359
2359
  * @param {any} payload
2360
+ * @param {{ group?: string }} [options]
2360
2361
  * @returns {Promise<string>}
2361
2362
  */
2362
- push(payload: any): Promise<string>;
2363
+ push(payload: any, { group }?: {
2364
+ group?: string;
2365
+ }): Promise<string>;
2363
2366
  /**
2364
2367
  * @param {any} payload
2365
- * @param {number|string} [timeout] - max time to wait for result, ms or string like "30s" (default 0, no limit)
2368
+ * @param {{ group?: string, timeout?: number|string }} [options]
2366
2369
  * @returns {Promise<any>}
2367
2370
  */
2368
- pushAndWait(payload: any, timeout?: number | string): Promise<any>;
2369
- /**
2370
- * @param {string} key
2371
- * @returns {{ push: (payload: any) => Promise<string>, pushAndWait: (payload: any, timeout?: number|string) => Promise<any> }}
2372
- */
2373
- group(key: string): {
2374
- push: (payload: any) => Promise<string>;
2375
- pushAndWait: (payload: any, timeout?: number | string) => Promise<any>;
2376
- };
2371
+ pushAndWait(payload: any, { group, timeout }?: {
2372
+ group?: string;
2373
+ timeout?: number | string;
2374
+ }): Promise<any>;
2375
+ /** @private */
2376
+ private _enqueue;
2377
2377
  /** @private */
2378
2378
  private _awaitTask;
2379
2379
  /** @returns {Promise<void>} */
@@ -4704,6 +4704,7 @@ export default class Queue extends EventEmitter<[never]> {
4704
4704
  _processTask(task: any, opts: any): Promise<void>;
4705
4705
  _settle(): void;
4706
4706
  _emitDrain(): void;
4707
+ _drainTimer: NodeJS.Timeout;
4707
4708
  _periodicCleanup(): Promise<void>;
4708
4709
  }
4709
4710
  export type QueueOptions = {