@prsm/queue 2.1.1 → 3.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 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
+ group?: 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
  ```
@@ -178,15 +178,15 @@ Each tenant gets up to 2 concurrent LLM calls with a 50ms pause between them. To
178
178
 
179
179
  ## WebSocket Integration with [mesh](https://github.com/nvms/mesh)
180
180
 
181
- Queue events are local-only - only the server that processes a task emits `complete`/`failed`. Use [mesh](https://github.com/nvms/mesh) to push results to connected clients in real time.
181
+ Queue events are local-only - only the server that processes a task emits `complete`/`failed`. Use [@prsm/realtime](https://github.com/nvms/realtime) to push results to connected clients in real time.
182
182
 
183
183
  Send results to a specific client:
184
184
 
185
185
  ```js
186
186
  import Queue from '@prsm/queue'
187
- import { MeshServer } from '@mesh-kit/server'
187
+ import { RealtimeServer } from '@prsm/realtime'
188
188
 
189
- const mesh = new MeshServer({ redis: { host: 'localhost', port: 6379 } })
189
+ const realtime = new RealtimeServer({ redis: { host: 'localhost', port: 6379 } })
190
190
  const queue = new Queue({ concurrency: 5, groups: { concurrency: 1 } })
191
191
 
192
192
  queue.process(async (payload) => {
@@ -194,26 +194,26 @@ queue.process(async (payload) => {
194
194
  })
195
195
 
196
196
  queue.on('complete', ({ task, result }) => {
197
- mesh.sendTo(task.payload.connectionId, 'job:complete', result)
197
+ realtime.sendTo(task.payload.connectionId, 'job:complete', result)
198
198
  })
199
199
 
200
200
  queue.on('failed', ({ task, error }) => {
201
- mesh.sendTo(task.payload.connectionId, 'job:failed', { error: error.message })
201
+ realtime.sendTo(task.payload.connectionId, 'job:failed', { error: error.message })
202
202
  })
203
203
 
204
- mesh.exposeCommand('generate-report', async (ctx) => {
205
- const taskId = await queue.group(ctx.connection.id).push({
204
+ realtime.exposeCommand('generate-report', async (ctx) => {
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
 
212
212
  await queue.ready()
213
- await mesh.listen(8080)
213
+ await realtime.listen(8080)
214
214
  ```
215
215
 
216
- Both queue and mesh use the same Redis instance. No key conflicts (`queue:*` vs `mesh:*`).
216
+ Both queue and realtime use the same Redis instance. No key conflicts (`queue:*` vs `mesh:*`).
217
217
 
218
218
  ## Horizontal Scaling
219
219
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prsm/queue",
3
- "version": "2.1.1",
3
+ "version": "3.0.1",
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
@@ -20,7 +20,7 @@ import ms from "@prsm/ms"
20
20
  * @property {string} uuid
21
21
  * @property {any} payload
22
22
  * @property {number} createdAt
23
- * @property {string} [groupKey]
23
+ * @property {string} [group]
24
24
  * @property {number} attempts
25
25
  */
26
26
 
@@ -151,14 +151,17 @@ 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
+ const task = group
160
+ ? { uuid: randomUUID(), payload, createdAt: Date.now(), group, attempts: 0 }
161
+ : { uuid: randomUUID(), payload, createdAt: Date.now(), attempts: 0 }
159
162
  this._pushed++
160
163
  try {
161
- await this._redis.lPush("queue:tasks", JSON.stringify(task))
164
+ await this._enqueue(task, group)
162
165
  } catch (err) {
163
166
  this._pushed--
164
167
  throw err
@@ -169,16 +172,18 @@ export default class Queue extends EventEmitter {
169
172
 
170
173
  /**
171
174
  * @param {any} payload
172
- * @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]
173
176
  * @returns {Promise<any>}
174
177
  */
175
- pushAndWait(payload, timeout = 0) {
178
+ pushAndWait(payload, { group, timeout = 0 } = {}) {
176
179
  if (this._closed) return Promise.reject(new Error("Queue is closed"))
177
- const task = { uuid: randomUUID(), payload, createdAt: Date.now(), attempts: 0 }
180
+ const task = group
181
+ ? { uuid: randomUUID(), payload, createdAt: Date.now(), group, attempts: 0 }
182
+ : { uuid: randomUUID(), payload, createdAt: Date.now(), attempts: 0 }
178
183
  this._pushed++
179
184
  const result = this._awaitTask(task.uuid, timeout)
180
185
  result.catch(() => {})
181
- return this._redis.lPush("queue:tasks", JSON.stringify(task)).then(() => {
186
+ return this._enqueue(task, group).then(() => {
182
187
  this.emit("new", { task })
183
188
  return result
184
189
  }, (err) => {
@@ -187,44 +192,17 @@ export default class Queue extends EventEmitter {
187
192
  })
188
193
  }
189
194
 
190
- /**
191
- * @param {string} key
192
- * @returns {{ push: (payload: any) => Promise<string>, pushAndWait: (payload: any, timeout?: number|string) => Promise<any> }}
193
- */
194
- group(key) {
195
- const makeTask = (payload) => {
196
- if (this._closed) throw new Error("Queue is closed")
197
- return { uuid: randomUUID(), payload, createdAt: Date.now(), groupKey: key, attempts: 0 }
198
- }
199
-
200
- const commit = (task) => {
201
- return this._redis.lPush(`queue:groups:${key}`, JSON.stringify(task)).then(async () => {
202
- this.emit("new", { task })
203
- if (!this._groupWorkers.has(key)) {
204
- this._groupWorkers.set(key, new Map())
205
- this._groupInFlight.set(key, 0)
206
- await this._startGroupWorkers(key)
207
- }
208
- }, (err) => {
209
- this._pushed--
210
- throw err
211
- })
212
- }
213
-
214
- return {
215
- push: async (payload) => {
216
- const task = makeTask(payload)
217
- this._pushed++
218
- await commit(task)
219
- return task.uuid
220
- },
221
- pushAndWait: (payload, timeout = 0) => {
222
- const task = makeTask(payload)
223
- this._pushed++
224
- const result = this._awaitTask(task.uuid, timeout)
225
- result.catch(() => {})
226
- return commit(task).then(() => result)
227
- },
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))
228
206
  }
229
207
  }
230
208
 
@@ -360,13 +338,13 @@ export default class Queue extends EventEmitter {
360
338
  timeout: this._options.groups.timeout,
361
339
  maxRetries: this._options.groups.maxRetries,
362
340
  retryKey: `queue:groups:${groupKey}`,
363
- groupKey,
341
+ group: groupKey,
364
342
  }
365
343
  this._runWorkerLoop(workerId, client, `queue:groups:${groupKey}`, groupWorkers, opts)
366
344
  }
367
345
 
368
346
  async _runWorkerLoop(workerId, client, key, activeMap, opts) {
369
- const delay = opts.groupKey ? this._options.groups.delay : this._options.delay
347
+ const delay = opts.group ? this._options.groups.delay : this._options.delay
370
348
 
371
349
  while (activeMap.get(workerId)) {
372
350
  try {
@@ -393,17 +371,17 @@ export default class Queue extends EventEmitter {
393
371
  }
394
372
 
395
373
  this._inFlight++
396
- if (opts.groupKey) {
397
- this._groupInFlight.set(opts.groupKey, (this._groupInFlight.get(opts.groupKey) || 0) + 1)
374
+ if (opts.group) {
375
+ this._groupInFlight.set(opts.group, (this._groupInFlight.get(opts.group) || 0) + 1)
398
376
  }
399
377
 
400
378
  try {
401
379
  await this._processTask(task, opts)
402
380
  } finally {
403
- if (opts.groupKey) {
404
- const count = (this._groupInFlight.get(opts.groupKey) || 1) - 1
405
- if (count <= 0) this._groupInFlight.delete(opts.groupKey)
406
- else this._groupInFlight.set(opts.groupKey, count)
381
+ if (opts.group) {
382
+ const count = (this._groupInFlight.get(opts.group) || 1) - 1
383
+ if (count <= 0) this._groupInFlight.delete(opts.group)
384
+ else this._groupInFlight.set(opts.group, count)
407
385
  }
408
386
  if (leaseId) await this._releaseGlobal(leaseId).catch(() => {})
409
387
  }
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>} */
@@ -4752,7 +4752,7 @@ export type Task = {
4752
4752
  uuid: string;
4753
4753
  payload: any;
4754
4754
  createdAt: number;
4755
- groupKey?: string;
4755
+ group?: string;
4756
4756
  attempts: number;
4757
4757
  };
4758
4758
  export type TaskHandler = (payload: any, task: Task) => Promise<any> | any;