bunqueue 2.8.32 → 2.8.33

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.
Files changed (40) hide show
  1. package/README.md +4 -3
  2. package/dist/application/backgroundTasks.js +17 -9
  3. package/dist/application/dependencyProcessor.js +6 -1
  4. package/dist/application/dlqManager.js +1 -1
  5. package/dist/application/lockManager.js +1 -1
  6. package/dist/application/operations/ack.js +23 -1
  7. package/dist/application/operations/jobManagement.js +2 -2
  8. package/dist/application/operations/jobStateTransitions.js +1 -1
  9. package/dist/application/operations/pull.js +125 -96
  10. package/dist/application/operations/push.js +2 -2
  11. package/dist/application/operations/queryOperations.js +74 -70
  12. package/dist/application/queueManager.d.ts +6 -11
  13. package/dist/application/queueManager.js +16 -3
  14. package/dist/application/queueStatsAggregator.d.ts +23 -0
  15. package/dist/application/queueStatsAggregator.js +80 -0
  16. package/dist/application/statsManager.d.ts +2 -26
  17. package/dist/application/statsManager.js +8 -124
  18. package/dist/domain/queue/shard.d.ts +3 -1
  19. package/dist/domain/queue/shard.js +15 -7
  20. package/dist/domain/queue/temporalIndex.d.ts +24 -0
  21. package/dist/domain/queue/temporalIndex.js +100 -0
  22. package/dist/domain/queue/temporalManager.d.ts +2 -11
  23. package/dist/domain/queue/temporalManager.js +27 -44
  24. package/dist/domain/queue/waiterManager.d.ts +11 -13
  25. package/dist/domain/queue/waiterManager.js +80 -49
  26. package/dist/infrastructure/cloud/commands.js +5 -2
  27. package/dist/infrastructure/cloud/snapshotCollector.js +5 -2
  28. package/dist/infrastructure/cloud/statsRefresh.js +5 -2
  29. package/dist/infrastructure/cloud/statsUpdate.js +5 -2
  30. package/dist/infrastructure/persistence/schema.d.ts +2 -2
  31. package/dist/infrastructure/persistence/schema.js +13 -1
  32. package/dist/infrastructure/persistence/sqlite.d.ts +18 -2
  33. package/dist/infrastructure/persistence/sqlite.js +49 -6
  34. package/dist/infrastructure/server/queueCountsScheduler.d.ts +13 -0
  35. package/dist/infrastructure/server/queueCountsScheduler.js +35 -0
  36. package/dist/infrastructure/server/sseHandler.d.ts +1 -0
  37. package/dist/infrastructure/server/sseHandler.js +18 -13
  38. package/dist/infrastructure/server/wsHandler.d.ts +1 -1
  39. package/dist/infrastructure/server/wsHandler.js +18 -15
  40. package/package.json +3 -3
@@ -123,6 +123,17 @@ export function getJobProgress(jobId, ctx) {
123
123
  return null;
124
124
  return { progress: job.progress, message: job.progressMessage };
125
125
  }
126
+ /** Stable total order used by both in-memory collection and SQL queries. */
127
+ function compareJobsByCreatedAt(a, b, asc) {
128
+ if (a.createdAt !== b.createdAt) {
129
+ return asc ? a.createdAt - b.createdAt : b.createdAt - a.createdAt;
130
+ }
131
+ if (a.id === b.id)
132
+ return 0;
133
+ if (asc)
134
+ return a.id < b.id ? -1 : 1;
135
+ return a.id > b.id ? -1 : 1;
136
+ }
126
137
  /** Resolve job state from SQLite when jobIndex has no entry (post-restart recovery). */
127
138
  function resolveStateFromStorage(jobId, storage) {
128
139
  if (!storage)
@@ -197,29 +208,25 @@ export async function getJobState(jobId, ctx) {
197
208
  return 'unknown';
198
209
  }
199
210
  /** Collect completed jobs for a queue from index + storage */
200
- function collectCompletedJobs(queue, ctx, maxCollect) {
211
+ function collectCompletedJobs(queue, ctx) {
201
212
  const jobs = [];
202
213
  for (const [jId, location] of ctx.jobIndex) {
203
214
  if (location.type === 'completed' && location.queueName === queue) {
204
215
  const job = ctx.storage?.getJob(jId) ?? ctx.completedJobsData?.get(jId) ?? null;
205
216
  if (job) {
206
217
  jobs.push(job);
207
- if (jobs.length >= maxCollect)
208
- break;
209
218
  }
210
219
  }
211
220
  }
212
221
  return jobs;
213
222
  }
214
223
  /** Collect active jobs for a queue across all processing shards */
215
- function collectActiveJobs(queue, shardIdx, ctx, maxCollect) {
224
+ function collectActiveJobs(queue, shardIdx, ctx) {
216
225
  const jobs = [];
217
226
  // Own shard first (most likely location)
218
227
  for (const job of ctx.processingShards[shardIdx].values()) {
219
228
  if (job.queue === queue)
220
229
  jobs.push(job);
221
- if (jobs.length >= maxCollect)
222
- return jobs;
223
230
  }
224
231
  // Other shards
225
232
  for (let i = 0; i < ctx.shardCount; i++) {
@@ -228,20 +235,15 @@ function collectActiveJobs(queue, shardIdx, ctx, maxCollect) {
228
235
  for (const job of ctx.processingShards[i].values()) {
229
236
  if (job.queue === queue)
230
237
  jobs.push(job);
231
- if (jobs.length >= maxCollect)
232
- return jobs;
233
238
  }
234
239
  }
235
240
  return jobs;
236
241
  }
237
242
  /** Collect waiting/delayed/prioritized jobs in a single pass */
238
- function collectTemporalJobs(shard, queue, needs, maxCollect) {
243
+ function collectTemporalJobs(shard, queue, needs, now = Date.now()) {
239
244
  const { waiting: needWaiting, prioritized: needPrioritized, delayed: needDelayed } = needs;
240
- const now = Date.now();
241
245
  const jobs = [];
242
246
  for (const j of shard.getQueue(queue).values()) {
243
- if (jobs.length >= maxCollect)
244
- break;
245
247
  const isDelayed = j.runAt > now;
246
248
  if (isDelayed && needDelayed) {
247
249
  jobs.push(j);
@@ -262,8 +264,7 @@ function tagState(jobs, state) {
262
264
  return jobs;
263
265
  }
264
266
  /** Tag temporal jobs with their actual state based on runAt/priority */
265
- function tagTemporalState(jobs) {
266
- const now = Date.now();
267
+ function tagTemporalState(jobs, now = Date.now()) {
267
268
  for (const j of jobs) {
268
269
  const isDelayed = j.runAt > now;
269
270
  j._state = isDelayed
@@ -274,19 +275,15 @@ function tagTemporalState(jobs) {
274
275
  }
275
276
  }
276
277
  /** Collect waiting-children jobs from deps and children maps */
277
- function collectWaitingChildrenFromShard(shard, queue, max) {
278
+ function collectWaitingChildrenFromShard(shard, queue) {
278
279
  const wcJobs = [];
279
280
  for (const job of shard.waitingDeps.values()) {
280
281
  if (job.queue === queue)
281
282
  wcJobs.push(job);
282
- if (wcJobs.length >= max)
283
- return wcJobs;
284
283
  }
285
284
  for (const job of shard.waitingChildren.values()) {
286
285
  if (job.queue === queue)
287
286
  wcJobs.push(job);
288
- if (wcJobs.length >= max)
289
- return wcJobs;
290
287
  }
291
288
  return wcJobs;
292
289
  }
@@ -312,34 +309,33 @@ function resolveStateNeeds(states, paused) {
312
309
  };
313
310
  }
314
311
  /** When paused, the queue's ready jobs (waiting + prioritized) ARE the paused set (#92). */
315
- function collectPausedJobs(shard, queue, maxPerSource) {
316
- const pausedJobs = collectTemporalJobs(shard, queue, { waiting: true, prioritized: true, delayed: false }, maxPerSource);
312
+ function collectPausedJobs(shard, queue, now) {
313
+ const pausedJobs = collectTemporalJobs(shard, queue, { waiting: true, prioritized: true, delayed: false }, now);
317
314
  return tagState(pausedJobs, 'paused');
318
315
  }
319
- function collectJobsByState(queue, shardIdx, states, ctx, maxPerSource = Infinity) {
316
+ function collectJobsByState(queue, shardIdx, states, ctx, now = Date.now()) {
320
317
  const shard = ctx.shards[shardIdx];
321
318
  const jobs = [];
322
319
  const need = resolveStateNeeds(states, shard.getState(queue).paused);
323
320
  if (need.waiting || need.prioritized || need.delayed) {
324
- const temporal = collectTemporalJobs(shard, queue, { waiting: need.waiting, prioritized: need.prioritized, delayed: need.delayed }, maxPerSource);
325
- tagTemporalState(temporal);
321
+ const temporal = collectTemporalJobs(shard, queue, { waiting: need.waiting, prioritized: need.prioritized, delayed: need.delayed }, now);
322
+ tagTemporalState(temporal, now);
326
323
  jobs.push(...temporal);
327
324
  }
328
325
  if (need.paused) {
329
- jobs.push(...collectPausedJobs(shard, queue, maxPerSource));
326
+ jobs.push(...collectPausedJobs(shard, queue, now));
330
327
  }
331
328
  if (need.active) {
332
- jobs.push(...tagState(collectActiveJobs(queue, shardIdx, ctx, maxPerSource), 'active'));
329
+ jobs.push(...tagState(collectActiveJobs(queue, shardIdx, ctx), 'active'));
333
330
  }
334
331
  if (need.failed) {
335
- const dlq = shard.getDlq(queue);
336
- jobs.push(...tagState(maxPerSource < dlq.length ? dlq.slice(0, maxPerSource) : dlq, 'failed'));
332
+ jobs.push(...tagState(shard.getDlq(queue), 'failed'));
337
333
  }
338
334
  if (need.completed) {
339
- jobs.push(...tagState(collectCompletedJobs(queue, ctx, maxPerSource), 'completed'));
335
+ jobs.push(...tagState(collectCompletedJobs(queue, ctx), 'completed'));
340
336
  }
341
337
  if (need.waitingChildren) {
342
- jobs.push(...tagState(collectWaitingChildrenFromShard(shard, queue, maxPerSource), 'waiting-children'));
338
+ jobs.push(...tagState(collectWaitingChildrenFromShard(shard, queue), 'waiting-children'));
343
339
  }
344
340
  return jobs;
345
341
  }
@@ -356,38 +352,34 @@ function collectWaitingChildrenJobs(shard, queue) {
356
352
  }
357
353
  return jobs;
358
354
  }
359
- /** Query SQLite with priority/waiting translation */
360
- function querySqliteWithPriority(storage, queue, sqlFilteredStates, opts) {
361
- const hasPrioritized = sqlFilteredStates.includes('prioritized');
362
- const hasWaiting = sqlFilteredStates.includes('waiting');
363
- if (!hasPrioritized && !hasWaiting) {
364
- if (sqlFilteredStates.length === 1) {
365
- return storage.queryJobs(queue, { state: sqlFilteredStates[0], ...opts });
366
- }
367
- return storage.queryJobs(queue, { states: sqlFilteredStates, ...opts });
368
- }
369
- // Map 'prioritized' to 'waiting' for SQLite, then post-filter by priority
370
- const sqlStates = sqlFilteredStates
371
- .map((s) => (s === 'prioritized' ? 'waiting' : s))
372
- .filter((s, i, arr) => arr.indexOf(s) === i);
373
- const overFetchOpts = { ...opts, limit: opts.limit * 2 };
374
- let jobs = sqlStates.length === 1
375
- ? storage.queryJobs(queue, { state: sqlStates[0], ...overFetchOpts })
376
- : storage.queryJobs(queue, { states: sqlStates, ...overFetchOpts });
377
- if (hasPrioritized && !hasWaiting) {
378
- jobs = jobs.filter((j) => j.priority > 0);
355
+ /** Query SQLite after translating persisted pending rows to their logical state. */
356
+ function querySqliteByLogicalState(storage, queue, sqlFilteredStates, opts) {
357
+ if (opts.excludedIds.size === 0) {
358
+ return storage.queryJobsByLogicalStates(queue, sqlFilteredStates, opts);
379
359
  }
380
- else if (hasWaiting && !hasPrioritized) {
381
- jobs = jobs.filter((j) => j.priority <= 0);
382
- }
383
- return jobs.slice(0, opts.limit);
360
+ // waitingDeps/waitingChildren rows remain persisted as waiting/delayed. Fetch
361
+ // enough rows to account for every possible exclusion, then paginate the
362
+ // logical result so parked jobs cannot create short or shifted pages.
363
+ const pageEnd = opts.offset + opts.limit;
364
+ const jobs = storage.queryJobsByLogicalStates(queue, sqlFilteredStates, {
365
+ limit: pageEnd + opts.excludedIds.size,
366
+ offset: 0,
367
+ asc: opts.asc,
368
+ now: opts.now,
369
+ });
370
+ return jobs.filter((job) => !opts.excludedIds.has(job.id)).slice(opts.offset, pageEnd);
384
371
  }
385
372
  /** Merge SQL rows with in-memory extras (each gathered from index 0), sort by
386
373
  * createdAt, and paginate [start, end) once — so offset-unaware extras don't
387
374
  * duplicate or drop rows across pages (#92). */
388
375
  function mergePage(sqlJobs, extras, start, end, asc) {
389
- const merged = sqlJobs.concat(extras);
390
- merged.sort((a, b) => (asc ? a.createdAt - b.createdAt : b.createdAt - a.createdAt));
376
+ const byId = new Map();
377
+ for (const job of sqlJobs)
378
+ byId.set(job.id, job);
379
+ for (const job of extras)
380
+ byId.set(job.id, job);
381
+ const merged = Array.from(byId.values());
382
+ merged.sort((a, b) => compareJobsByCreatedAt(a, b, asc));
391
383
  return merged.slice(start, end);
392
384
  }
393
385
  /** Get jobs from queue with filters */
@@ -401,10 +393,10 @@ export function getJobs(queue, shardIdx, options, ctx) {
401
393
  : state
402
394
  : [state];
403
395
  const limit = end - start;
396
+ const now = Date.now();
404
397
  if (ctx.storage) {
405
398
  const shard = ctx.shards[shardIdx];
406
399
  const isPaused = shard.getState(queue).paused;
407
- const maxPerSource = end + 1;
408
400
  // Derived sources are NOT offset-aware (the DLQ and the paused/waiting-children
409
401
  // views come from in-memory maps). When any contributes, we must gather [0, end)
410
402
  // from EVERY source, merge, sort, then slice [start, end) exactly once — pushing
@@ -417,6 +409,19 @@ export function getJobs(queue, shardIdx, options, ctx) {
417
409
  const all = ctx.storage.queryJobs(queue, { limit: end, offset: 0, asc });
418
410
  return mergePage(all, dlq, start, end, asc);
419
411
  }
412
+ // jobs-table states. A paused queue reports its waiting/prioritized jobs under
413
+ // 'paused', so they must not also surface in the waiting/prioritized lists (#92).
414
+ const sqlFilteredStates = states.filter((s) => s !== 'failed' &&
415
+ s !== 'waiting-children' &&
416
+ s !== 'paused' &&
417
+ !(isPaused && (s === 'waiting' || s === 'prioritized')));
418
+ // In-memory parked state is authoritative over the persisted row. Initial
419
+ // dependency jobs are stored as waiting/delayed, while parents moved after
420
+ // a pull can still be stored as active.
421
+ const parkedJobs = states.includes('waiting-children') || sqlFilteredStates.length > 0
422
+ ? collectWaitingChildrenJobs(shard, queue)
423
+ : [];
424
+ const parkedIds = new Set(parkedJobs.map((job) => job.id));
420
425
  // States with no jobs-table row are collected from in-memory sources:
421
426
  // - 'failed' -> DLQ (the failed job lives in the dlq table) (#92)
422
427
  // - 'waiting-children' -> deps/children maps
@@ -426,40 +431,39 @@ export function getJobs(queue, shardIdx, options, ctx) {
426
431
  extras.push(...tagState(shard.getDlq(queue), 'failed'));
427
432
  }
428
433
  if (states.includes('waiting-children')) {
429
- extras.push(...collectWaitingChildrenJobs(shard, queue));
434
+ extras.push(...tagState(parkedJobs, 'waiting-children'));
430
435
  }
431
436
  if (states.includes('paused') && isPaused) {
432
- const pausedJobs = collectTemporalJobs(shard, queue, { waiting: true, prioritized: true, delayed: false }, maxPerSource);
437
+ const pausedJobs = collectTemporalJobs(shard, queue, { waiting: true, prioritized: true, delayed: false }, now);
433
438
  extras.push(...tagState(pausedJobs, 'paused'));
434
439
  }
435
- // jobs-table states. A paused queue reports its waiting/prioritized jobs under
436
- // 'paused', so they must not also surface in the waiting/prioritized lists (#92).
437
- const sqlFilteredStates = states.filter((s) => s !== 'failed' &&
438
- s !== 'waiting-children' &&
439
- s !== 'paused' &&
440
- !(isPaused && (s === 'waiting' || s === 'prioritized')));
441
440
  // Fast path: only jobs-table states, no derived sources — SQL paginates directly.
442
441
  if (extras.length === 0) {
443
442
  return sqlFilteredStates.length > 0
444
- ? querySqliteWithPriority(ctx.storage, queue, sqlFilteredStates, {
443
+ ? querySqliteByLogicalState(ctx.storage, queue, sqlFilteredStates, {
445
444
  limit,
446
445
  offset: start,
447
446
  asc,
447
+ now,
448
+ excludedIds: parkedIds,
448
449
  })
449
450
  : [];
450
451
  }
451
452
  const sqlJobs = sqlFilteredStates.length > 0
452
- ? querySqliteWithPriority(ctx.storage, queue, sqlFilteredStates, {
453
+ ? querySqliteByLogicalState(ctx.storage, queue, sqlFilteredStates, {
453
454
  limit: end,
454
455
  offset: 0,
455
456
  asc,
457
+ now,
458
+ excludedIds: parkedIds,
456
459
  })
457
460
  : [];
458
461
  return mergePage(sqlJobs, extras, start, end, asc);
459
462
  }
460
463
  // In-memory path (embedded mode only)
461
- const maxPerSource = end + 1;
462
- const jobs = collectJobsByState(queue, shardIdx, states, ctx, maxPerSource);
463
- jobs.sort((a, b) => (asc ? a.createdAt - b.createdAt : b.createdAt - a.createdAt));
464
+ // Every source must be filtered before the global order is applied. Limiting
465
+ // an insertion-ordered Map here loses newer rows on descending pages.
466
+ const jobs = collectJobsByState(queue, shardIdx, states, ctx, now);
467
+ jobs.sort((a, b) => compareJobsByCreatedAt(a, b, asc));
464
468
  return jobs.slice(start, end);
465
469
  }
@@ -341,24 +341,19 @@ export declare class QueueManager {
341
341
  paused: boolean;
342
342
  counts: {
343
343
  waiting: number;
344
+ prioritized: number;
344
345
  active: number;
345
346
  completed: number;
346
347
  failed: number;
347
348
  delayed: number;
348
349
  };
349
350
  }>;
351
+ /** Get counts for every registered queue with one global aggregation pass. */
352
+ getAllQueueJobCounts(): Map<string, statsMgr.QueueJobCounts>;
353
+ /** Aggregate a selected group of queues without repeating global scans. */
354
+ getQueueJobCountsBatch(queueNames: Iterable<string>): Map<string, statsMgr.QueueJobCounts>;
350
355
  /** Get job counts for a specific queue */
351
- getQueueJobCounts(queueName: string): {
352
- waiting: number;
353
- prioritized: number;
354
- delayed: number;
355
- active: number;
356
- completed: number;
357
- failed: number;
358
- 'waiting-children': number;
359
- totalCompleted: number;
360
- totalFailed: number;
361
- };
356
+ getQueueJobCounts(queueName: string): statsMgr.QueueJobCounts;
362
357
  getMemoryStats(): statsMgr.MemoryStats;
363
358
  /** Get storage health status (disk full detection) */
364
359
  getStorageStatus(): {
@@ -1321,7 +1321,7 @@ export class QueueManager {
1321
1321
  queue.push(parentJob);
1322
1322
  shard.incrementQueued(parentId, false, parentJob.createdAt, parentJob.queue, now);
1323
1323
  this.jobIndex.set(parentId, { type: 'queue', shardIdx: idx, queueName: parentJob.queue });
1324
- shard.notify();
1324
+ shard.notify(parentJob.queue);
1325
1325
  promoted = true;
1326
1326
  }
1327
1327
  });
@@ -1435,7 +1435,7 @@ export class QueueManager {
1435
1435
  shard.getQueue(parentJob.queue).push(parentJob);
1436
1436
  shard.incrementQueued(parentId, false, parentJob.createdAt, parentJob.queue, now);
1437
1437
  this.jobIndex.set(parentId, { type: 'queue', shardIdx: idx, queueName: parentJob.queue });
1438
- shard.notify();
1438
+ shard.notify(parentJob.queue);
1439
1439
  promoted = true;
1440
1440
  }
1441
1441
  });
@@ -1529,13 +1529,17 @@ export class QueueManager {
1529
1529
  const ctx = this.contextFactory.getStatsContext();
1530
1530
  const queues = this.listQueues();
1531
1531
  const result = [];
1532
+ const countsByQueue = statsMgr.getAllQueueJobCounts(queues, ctx);
1532
1533
  for (const name of queues) {
1533
- const c = statsMgr.getQueueJobCounts(name, ctx);
1534
+ const c = countsByQueue.get(name);
1535
+ if (!c)
1536
+ continue;
1534
1537
  result.push({
1535
1538
  name,
1536
1539
  paused: this.isPaused(name),
1537
1540
  counts: {
1538
1541
  waiting: c.waiting,
1542
+ prioritized: c.prioritized,
1539
1543
  active: c.active,
1540
1544
  completed: c.completed,
1541
1545
  failed: c.failed,
@@ -1545,6 +1549,15 @@ export class QueueManager {
1545
1549
  }
1546
1550
  return result;
1547
1551
  }
1552
+ /** Get counts for every registered queue with one global aggregation pass. */
1553
+ getAllQueueJobCounts() {
1554
+ return this.getQueueJobCountsBatch(this.queueNamesCache);
1555
+ }
1556
+ /** Aggregate a selected group of queues without repeating global scans. */
1557
+ getQueueJobCountsBatch(queueNames) {
1558
+ const ctx = this.contextFactory.getStatsContext();
1559
+ return statsMgr.getAllQueueJobCounts(queueNames, ctx);
1560
+ }
1548
1561
  /** Get job counts for a specific queue */
1549
1562
  getQueueJobCounts(queueName) {
1550
1563
  return statsMgr.getQueueJobCounts(queueName, this.contextFactory.getStatsContext());
@@ -0,0 +1,23 @@
1
+ import type { StatsContext } from './types';
2
+ export interface PerQueueStats {
3
+ waiting: number;
4
+ prioritized: number;
5
+ delayed: number;
6
+ active: number;
7
+ dlq: number;
8
+ }
9
+ export interface QueueJobCounts {
10
+ waiting: number;
11
+ prioritized: number;
12
+ delayed: number;
13
+ active: number;
14
+ completed: number;
15
+ failed: number;
16
+ 'waiting-children': number;
17
+ totalCompleted: number;
18
+ totalFailed: number;
19
+ }
20
+ /** Aggregate all requested queues in one pass over every global collection. */
21
+ export declare function getAllQueueJobCounts(queueNames: Iterable<string>, ctx: StatsContext): Map<string, QueueJobCounts>;
22
+ export declare function getPerQueueStats(ctx: StatsContext, queueNames: Set<string>): Map<string, PerQueueStats>;
23
+ export declare function getQueueJobCounts(queueName: string, ctx: StatsContext): QueueJobCounts;
@@ -0,0 +1,80 @@
1
+ import { SHARD_COUNT, shardIndex } from '../shared/hash';
2
+ function createCounts(queue, ctx) {
3
+ const metrics = ctx.perQueueMetrics?.get(queue);
4
+ return {
5
+ waiting: 0,
6
+ prioritized: 0,
7
+ delayed: 0,
8
+ active: 0,
9
+ completed: 0,
10
+ failed: ctx.shards[shardIndex(queue)].getDlqCount(queue),
11
+ 'waiting-children': 0,
12
+ totalCompleted: Number(metrics?.totalCompleted ?? 0n),
13
+ totalFailed: Number(metrics?.totalFailed ?? 0n),
14
+ };
15
+ }
16
+ /** Aggregate all requested queues in one pass over every global collection. */
17
+ export function getAllQueueJobCounts(queueNames, ctx) {
18
+ const result = new Map();
19
+ const now = Date.now();
20
+ for (const name of queueNames) {
21
+ const counts = createCounts(name, ctx);
22
+ const queue = ctx.shards[shardIndex(name)].queues.get(name);
23
+ if (queue) {
24
+ for (const job of queue.values()) {
25
+ if (job.runAt > now)
26
+ counts.delayed++;
27
+ else if (job.priority > 0)
28
+ counts.prioritized++;
29
+ else
30
+ counts.waiting++;
31
+ }
32
+ }
33
+ result.set(name, counts);
34
+ }
35
+ for (const processing of ctx.processingShards) {
36
+ for (const job of processing.values()) {
37
+ const counts = result.get(job.queue);
38
+ if (counts)
39
+ counts.active++;
40
+ }
41
+ }
42
+ for (const [jobId, location] of ctx.jobIndex) {
43
+ if (location.type !== 'completed' || !ctx.completedJobs.has(jobId))
44
+ continue;
45
+ const counts = result.get(location.queueName);
46
+ if (counts)
47
+ counts.completed++;
48
+ }
49
+ for (let i = 0; i < SHARD_COUNT; i++) {
50
+ const shard = ctx.shards[i];
51
+ for (const job of shard.waitingChildren.values()) {
52
+ const counts = result.get(job.queue);
53
+ if (counts)
54
+ counts['waiting-children']++;
55
+ }
56
+ for (const job of shard.waitingDeps.values()) {
57
+ const counts = result.get(job.queue);
58
+ if (counts)
59
+ counts['waiting-children']++;
60
+ }
61
+ }
62
+ return result;
63
+ }
64
+ export function getPerQueueStats(ctx, queueNames) {
65
+ const allCounts = getAllQueueJobCounts(queueNames, ctx);
66
+ const result = new Map();
67
+ for (const [name, counts] of allCounts) {
68
+ result.set(name, {
69
+ waiting: counts.waiting,
70
+ prioritized: counts.prioritized,
71
+ delayed: counts.delayed,
72
+ active: counts.active,
73
+ dlq: counts.failed,
74
+ });
75
+ }
76
+ return result;
77
+ }
78
+ export function getQueueJobCounts(queueName, ctx) {
79
+ return getAllQueueJobCounts([queueName], ctx).get(queueName) ?? createCounts(queueName, ctx);
80
+ }
@@ -3,6 +3,8 @@
3
3
  * Provides system metrics and memory compaction utilities
4
4
  */
5
5
  import type { StatsContext } from './types';
6
+ export { getAllQueueJobCounts, getPerQueueStats, getQueueJobCounts, } from './queueStatsAggregator';
7
+ export type { PerQueueStats, QueueJobCounts } from './queueStatsAggregator';
6
8
  export interface QueueStats {
7
9
  waiting: number;
8
10
  prioritized: number;
@@ -19,13 +21,6 @@ export interface QueueStats {
19
21
  cronJobs: number;
20
22
  cronPending: number;
21
23
  }
22
- export interface PerQueueStats {
23
- waiting: number;
24
- prioritized: number;
25
- delayed: number;
26
- active: number;
27
- dlq: number;
28
- }
29
24
  export interface MemoryStats {
30
25
  jobIndex: number;
31
26
  completedJobs: number;
@@ -57,25 +52,6 @@ export declare function getStats(ctx: StatsContext, cronScheduler: {
57
52
  * Returns counts of entries in all major collections.
58
53
  */
59
54
  export declare function getMemoryStats(ctx: StatsContext): MemoryStats;
60
- /**
61
- * Get per-queue statistics by iterating shards to aggregate per queue name.
62
- * Uses shard hashing to look up each queue in its designated shard.
63
- */
64
- export declare function getPerQueueStats(ctx: StatsContext, queueNames: Set<string>): Map<string, PerQueueStats>;
65
- /**
66
- * Get job counts for a specific queue
67
- */
68
- export declare function getQueueJobCounts(queueName: string, ctx: StatsContext): {
69
- waiting: number;
70
- prioritized: number;
71
- delayed: number;
72
- active: number;
73
- completed: number;
74
- failed: number;
75
- 'waiting-children': number;
76
- totalCompleted: number;
77
- totalFailed: number;
78
- };
79
55
  /**
80
56
  * Force compact all collections to reduce memory usage.
81
57
  * Use after large batch operations or when memory pressure is high.