bunqueue 2.8.32 → 2.8.34

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 (45) 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.d.ts +0 -2
  8. package/dist/application/operations/jobManagement.js +2 -16
  9. package/dist/application/operations/jobPromotion.d.ts +7 -0
  10. package/dist/application/operations/jobPromotion.js +58 -0
  11. package/dist/application/operations/jobStateTransitions.js +1 -1
  12. package/dist/application/operations/pull.js +125 -96
  13. package/dist/application/operations/push.js +2 -2
  14. package/dist/application/operations/queryOperations.js +74 -70
  15. package/dist/application/queueManager.d.ts +7 -11
  16. package/dist/application/queueManager.js +21 -4
  17. package/dist/application/queueStatsAggregator.d.ts +23 -0
  18. package/dist/application/queueStatsAggregator.js +80 -0
  19. package/dist/application/statsManager.d.ts +2 -26
  20. package/dist/application/statsManager.js +8 -124
  21. package/dist/client/queue/operations/management.js +1 -11
  22. package/dist/domain/queue/shard.d.ts +5 -1
  23. package/dist/domain/queue/shard.js +20 -7
  24. package/dist/domain/queue/temporalIndex.d.ts +24 -0
  25. package/dist/domain/queue/temporalIndex.js +100 -0
  26. package/dist/domain/queue/temporalManager.d.ts +2 -11
  27. package/dist/domain/queue/temporalManager.js +27 -44
  28. package/dist/domain/queue/waiterManager.d.ts +11 -13
  29. package/dist/domain/queue/waiterManager.js +80 -49
  30. package/dist/infrastructure/cloud/commands.js +5 -2
  31. package/dist/infrastructure/cloud/snapshotCollector.js +5 -2
  32. package/dist/infrastructure/cloud/statsRefresh.js +5 -2
  33. package/dist/infrastructure/cloud/statsUpdate.js +5 -2
  34. package/dist/infrastructure/persistence/schema.d.ts +2 -2
  35. package/dist/infrastructure/persistence/schema.js +13 -1
  36. package/dist/infrastructure/persistence/sqlite.d.ts +18 -2
  37. package/dist/infrastructure/persistence/sqlite.js +49 -6
  38. package/dist/infrastructure/server/handlers/advanced.js +1 -8
  39. package/dist/infrastructure/server/queueCountsScheduler.d.ts +13 -0
  40. package/dist/infrastructure/server/queueCountsScheduler.js +35 -0
  41. package/dist/infrastructure/server/sseHandler.d.ts +1 -0
  42. package/dist/infrastructure/server/sseHandler.js +18 -13
  43. package/dist/infrastructure/server/wsHandler.d.ts +1 -1
  44. package/dist/infrastructure/server/wsHandler.js +18 -15
  45. package/package.json +4 -3
@@ -2,17 +2,8 @@
2
2
  * Stats Manager - Queue statistics and memory management
3
3
  * Provides system metrics and memory compaction utilities
4
4
  */
5
- import { SHARD_COUNT, shardIndex } from '../shared/hash';
6
- /** Count jobs belonging to `queueName` across one or more job iterables. */
7
- function countByQueue(sources, queueName) {
8
- let count = 0;
9
- for (const src of sources) {
10
- for (const job of src)
11
- if (job.queue === queueName)
12
- count++;
13
- }
14
- return count;
15
- }
5
+ import { SHARD_COUNT } from '../shared/hash';
6
+ export { getAllQueueJobCounts, getPerQueueStats, getQueueJobCounts, } from './queueStatsAggregator';
16
7
  /**
17
8
  * Get queue statistics - uses running counters + priority scan
18
9
  */
@@ -21,7 +12,6 @@ export function getStats(ctx, cronScheduler) {
21
12
  const now = Date.now();
22
13
  for (let i = 0; i < SHARD_COUNT; i++) {
23
14
  const shardStats = ctx.shards[i].getStats();
24
- delayed += shardStats.delayedJobs;
25
15
  dlq += shardStats.dlqJobs;
26
16
  active += ctx.processingShards[i].size;
27
17
  // getJobState reports BOTH waitingChildren (flow parents) and waitingDeps
@@ -31,14 +21,12 @@ export function getStats(ctx, cronScheduler) {
31
21
  // Scan queues to split waiting vs prioritized (BullMQ v5 compat)
32
22
  for (const queue of ctx.shards[i].queues.values()) {
33
23
  for (const job of queue.values()) {
34
- if (job.runAt <= now) {
35
- if (job.priority > 0) {
36
- prioritized++;
37
- }
38
- else {
39
- waiting++;
40
- }
41
- }
24
+ if (job.runAt > now)
25
+ delayed++;
26
+ else if (job.priority > 0)
27
+ prioritized++;
28
+ else
29
+ waiting++;
42
30
  }
43
31
  }
44
32
  }
@@ -103,110 +91,6 @@ export function getMemoryStats(ctx) {
103
91
  delayedHeapTotal,
104
92
  };
105
93
  }
106
- /**
107
- * Get per-queue statistics by iterating shards to aggregate per queue name.
108
- * Uses shard hashing to look up each queue in its designated shard.
109
- */
110
- export function getPerQueueStats(ctx, queueNames) {
111
- const result = new Map();
112
- const now = Date.now();
113
- for (const name of queueNames) {
114
- const idx = shardIndex(name);
115
- const shard = ctx.shards[idx];
116
- const queue = shard.queues.get(name);
117
- let waiting = 0;
118
- let prioritized = 0;
119
- let delayed = 0;
120
- if (queue) {
121
- for (const job of queue.values()) {
122
- if (job.runAt > now) {
123
- delayed++;
124
- }
125
- else if (job.priority > 0) {
126
- prioritized++;
127
- }
128
- else {
129
- waiting++;
130
- }
131
- }
132
- }
133
- const dlq = shard.getDlqCount(name);
134
- result.set(name, { waiting, prioritized, delayed, active: 0, dlq });
135
- }
136
- // Count active jobs per queue from all processing shards
137
- for (let i = 0; i < SHARD_COUNT; i++) {
138
- for (const job of ctx.processingShards[i].values()) {
139
- const entry = result.get(job.queue);
140
- if (entry) {
141
- entry.active++;
142
- }
143
- }
144
- }
145
- return result;
146
- }
147
- /**
148
- * Get job counts for a specific queue
149
- */
150
- export function getQueueJobCounts(queueName, ctx) {
151
- const idx = shardIndex(queueName);
152
- const shard = ctx.shards[idx];
153
- const queue = shard.queues.get(queueName);
154
- const now = Date.now();
155
- // Count waiting vs prioritized vs delayed jobs in the queue
156
- let waiting = 0;
157
- let prioritized = 0;
158
- let delayed = 0;
159
- if (queue) {
160
- for (const job of queue.values()) {
161
- if (job.runAt > now) {
162
- delayed++;
163
- }
164
- else if (job.priority > 0) {
165
- prioritized++;
166
- }
167
- else {
168
- waiting++;
169
- }
170
- }
171
- }
172
- // Count active jobs (processing) for this queue
173
- let active = 0;
174
- for (const procShard of ctx.processingShards) {
175
- for (const job of procShard.values()) {
176
- if (job.queue === queueName) {
177
- active++;
178
- }
179
- }
180
- }
181
- // Count completed jobs for this queue
182
- let completed = 0;
183
- for (const [jobId, loc] of ctx.jobIndex) {
184
- if (loc.type === 'completed' && loc.queueName === queueName && ctx.completedJobs.has(jobId)) {
185
- completed++;
186
- }
187
- }
188
- // Count failed (DLQ) jobs for this queue
189
- const failed = shard.getDlq(queueName).length;
190
- // Count waiting-children jobs. getJobState/getJobs treat BOTH waitingChildren
191
- // (flow parents) and waitingDeps (jobs blocked on dependsOn) as 'waiting-children',
192
- // so count both to stay consistent with state/list (#95 class).
193
- const waitingChildrenCount = countByQueue([shard.waitingChildren.values(), shard.waitingDeps.values()], queueName);
194
- // Per-queue cumulative counters
195
- const perQueue = ctx.perQueueMetrics?.get(queueName);
196
- const totalCompleted = Number(perQueue?.totalCompleted ?? 0n);
197
- const totalFailed = Number(perQueue?.totalFailed ?? 0n);
198
- return {
199
- waiting,
200
- prioritized,
201
- delayed,
202
- active,
203
- completed,
204
- failed,
205
- 'waiting-children': waitingChildrenCount,
206
- totalCompleted,
207
- totalFailed,
208
- };
209
- }
210
94
  /**
211
95
  * Force compact all collections to reduce memory usage.
212
96
  * Use after large batch operations or when memory pressure is high.
@@ -93,17 +93,7 @@ export async function cleanAsync(ctx, grace, limit, type) {
93
93
  /** Promote delayed jobs to waiting */
94
94
  export async function promoteJobs(ctx, opts) {
95
95
  if (ctx.embedded) {
96
- // Get delayed jobs and promote them one by one
97
- const manager = getSharedManager();
98
- const jobs = manager.getJobs(ctx.name, { state: 'delayed' });
99
- const count = opts?.count ?? jobs.length;
100
- let promoted = 0;
101
- for (let i = 0; i < Math.min(count, jobs.length); i++) {
102
- const success = await manager.promote(jobs[i].id);
103
- if (success)
104
- promoted++;
105
- }
106
- return promoted;
96
+ return getSharedManager().promoteJobs(ctx.name, opts?.count);
107
97
  }
108
98
  const response = await ctx.tcp.send({
109
99
  cmd: 'PromoteJobs',
@@ -48,9 +48,11 @@ export declare class Shard {
48
48
  /** Active FIFO groups per queue */
49
49
  readonly activeGroups: Map<string, Set<string>>;
50
50
  constructor();
51
- notify(): void;
51
+ notify(queue?: string): void;
52
52
  notifyBatch(count: number): void;
53
+ notifyBatch(queue: string, count: number): void;
53
54
  waitForJob(timeoutMs: number): Promise<void>;
55
+ waitForJob(queue: string, timeoutMs: number): Promise<void>;
54
56
  getQueue(name: string): IndexedPriorityQueue;
55
57
  getState(name: string): QueueState;
56
58
  isPaused(name: string): boolean;
@@ -114,6 +116,8 @@ export declare class Shard {
114
116
  };
115
117
  incrementQueued(jobId: JobId, isDelayed: boolean, createdAt?: number, queue?: string, runAt?: number): void;
116
118
  decrementQueued(jobId: JobId): void;
119
+ /** Remove a promoted job from delayed tracking without changing queue depth. */
120
+ promoteDelayed(jobId: JobId): void;
117
121
  incrementDlq(): void;
118
122
  decrementDlq(count?: number): void;
119
123
  refreshDelayedCount(now: number): void;
@@ -58,14 +58,22 @@ export class Shard {
58
58
  });
59
59
  }
60
60
  // ============ Waiter Management (delegated) ============
61
- notify() {
62
- this.waiterManager.notify();
61
+ notify(queue) {
62
+ this.waiterManager.notify(queue);
63
63
  }
64
- notifyBatch(count) {
65
- this.waiterManager.notifyBatch(count);
64
+ notifyBatch(queueOrCount, maybeCount) {
65
+ if (typeof queueOrCount === 'number') {
66
+ this.waiterManager.notifyBatch(queueOrCount);
67
+ }
68
+ else {
69
+ this.waiterManager.notifyBatch(queueOrCount, maybeCount ?? 0);
70
+ }
66
71
  }
67
- waitForJob(timeoutMs) {
68
- return this.waiterManager.waitForJob(timeoutMs);
72
+ waitForJob(queueOrTimeout, maybeTimeout) {
73
+ if (typeof queueOrTimeout === 'number') {
74
+ return this.waiterManager.waitForJob(queueOrTimeout);
75
+ }
76
+ return this.waiterManager.waitForJob(queueOrTimeout, maybeTimeout ?? 0);
69
77
  }
70
78
  // ============ Queue Operations ============
71
79
  getQueue(name) {
@@ -87,7 +95,7 @@ export class Shard {
87
95
  }
88
96
  resume(name) {
89
97
  this.limiterManager.resume(name);
90
- this.waiterManager.notify();
98
+ this.waiterManager.notify(name);
91
99
  }
92
100
  // ============ Unique Key Management (delegated) ============
93
101
  isUniqueAvailable(queue, key) {
@@ -294,6 +302,11 @@ export class Shard {
294
302
  decrementQueued(jobId) {
295
303
  this.counters.decrementQueued(jobId);
296
304
  }
305
+ /** Remove a promoted job from delayed tracking without changing queue depth. */
306
+ promoteDelayed(jobId) {
307
+ this.temporalManager.removeDelayed(jobId);
308
+ this.counters.syncDelayedCount();
309
+ }
297
310
  incrementDlq() {
298
311
  this.counters.incrementDlq();
299
312
  }
@@ -0,0 +1,24 @@
1
+ import type { JobId } from '../types/job';
2
+ export interface TemporalEntry {
3
+ createdAt: number;
4
+ jobId: JobId;
5
+ queue: string;
6
+ }
7
+ /** Queue-local temporal indexes with direct lookup by job ID. */
8
+ export declare class TemporalIndex {
9
+ private readonly indexesByQueue;
10
+ private readonly entriesByJobId;
11
+ private entryCount;
12
+ get size(): number;
13
+ add(createdAt: number, jobId: JobId, queue: string): void;
14
+ getOldJobs(queue: string, threshold: number, limit: number): Array<{
15
+ jobId: JobId;
16
+ createdAt: number;
17
+ }>;
18
+ remove(jobId: JobId): void;
19
+ clearQueue(queue: string): void;
20
+ cleanOrphaned(validJobIds: Set<JobId>): number;
21
+ clear(): void;
22
+ private removeFromQueueIndex;
23
+ private detachJobEntry;
24
+ }
@@ -0,0 +1,100 @@
1
+ import { SkipList } from '../../shared/skipList';
2
+ const compareEntries = (a, b) => a.createdAt - b.createdAt || (a.jobId < b.jobId ? -1 : a.jobId > b.jobId ? 1 : 0);
3
+ const createQueueIndex = () => new SkipList(compareEntries);
4
+ /** Queue-local temporal indexes with direct lookup by job ID. */
5
+ export class TemporalIndex {
6
+ indexesByQueue = new Map();
7
+ entriesByJobId = new Map();
8
+ entryCount = 0;
9
+ get size() {
10
+ return this.entryCount;
11
+ }
12
+ add(createdAt, jobId, queue) {
13
+ const jobEntries = this.entriesByJobId.get(jobId);
14
+ if (jobEntries?.some((entry) => entry.createdAt === createdAt))
15
+ return;
16
+ const entry = { createdAt, jobId, queue };
17
+ let queueIndex = this.indexesByQueue.get(queue);
18
+ if (!queueIndex) {
19
+ queueIndex = createQueueIndex();
20
+ this.indexesByQueue.set(queue, queueIndex);
21
+ }
22
+ queueIndex.insert(entry);
23
+ if (jobEntries)
24
+ jobEntries.push(entry);
25
+ else
26
+ this.entriesByJobId.set(jobId, [entry]);
27
+ this.entryCount++;
28
+ }
29
+ getOldJobs(queue, threshold, limit) {
30
+ if (limit <= 0)
31
+ return [];
32
+ const queueIndex = this.indexesByQueue.get(queue);
33
+ if (!queueIndex)
34
+ return [];
35
+ return queueIndex
36
+ .takeWhile((entry) => entry.createdAt <= threshold, limit)
37
+ .map(({ jobId, createdAt }) => ({ jobId, createdAt }));
38
+ }
39
+ remove(jobId) {
40
+ const jobEntries = this.entriesByJobId.get(jobId);
41
+ if (!jobEntries?.length)
42
+ return;
43
+ let targetIndex = 0;
44
+ for (let i = 1; i < jobEntries.length; i++) {
45
+ if (compareEntries(jobEntries[i], jobEntries[targetIndex]) < 0)
46
+ targetIndex = i;
47
+ }
48
+ const [entry] = jobEntries.splice(targetIndex, 1);
49
+ this.removeFromQueueIndex(entry);
50
+ if (jobEntries.length === 0)
51
+ this.entriesByJobId.delete(jobId);
52
+ this.entryCount--;
53
+ }
54
+ clearQueue(queue) {
55
+ const queueIndex = this.indexesByQueue.get(queue);
56
+ if (!queueIndex)
57
+ return;
58
+ for (const entry of queueIndex.values())
59
+ this.detachJobEntry(entry);
60
+ this.entryCount -= queueIndex.size;
61
+ this.indexesByQueue.delete(queue);
62
+ }
63
+ cleanOrphaned(validJobIds) {
64
+ let removed = 0;
65
+ for (const [jobId, entries] of this.entriesByJobId) {
66
+ if (validJobIds.has(jobId))
67
+ continue;
68
+ for (const entry of entries) {
69
+ this.removeFromQueueIndex(entry);
70
+ removed++;
71
+ }
72
+ this.entriesByJobId.delete(jobId);
73
+ }
74
+ this.entryCount -= removed;
75
+ return removed;
76
+ }
77
+ clear() {
78
+ this.indexesByQueue.clear();
79
+ this.entriesByJobId.clear();
80
+ this.entryCount = 0;
81
+ }
82
+ removeFromQueueIndex(entry) {
83
+ const queueIndex = this.indexesByQueue.get(entry.queue);
84
+ if (!queueIndex)
85
+ return;
86
+ queueIndex.delete(entry);
87
+ if (queueIndex.isEmpty)
88
+ this.indexesByQueue.delete(entry.queue);
89
+ }
90
+ detachJobEntry(entry) {
91
+ const jobEntries = this.entriesByJobId.get(entry.jobId);
92
+ if (!jobEntries)
93
+ return;
94
+ const index = jobEntries.indexOf(entry);
95
+ if (index !== -1)
96
+ jobEntries.splice(index, 1);
97
+ if (jobEntries.length === 0)
98
+ this.entriesByJobId.delete(entry.jobId);
99
+ }
100
+ }
@@ -6,22 +6,12 @@
6
6
  * - Delayed heap (MinHeap) for O(k) delayed job refresh
7
7
  */
8
8
  import type { JobId } from '../types/job';
9
- /** Entry in the temporal index */
10
- export interface TemporalEntry {
11
- createdAt: number;
12
- jobId: JobId;
13
- queue: string;
14
- }
9
+ export type { TemporalEntry } from './temporalIndex';
15
10
  /**
16
11
  * Manages temporal indexing for efficient job cleanup
17
12
  * and delayed job tracking for O(k) refresh
18
13
  */
19
14
  export declare class TemporalManager {
20
- /**
21
- * Temporal index: Skip List for O(log n) insert/delete
22
- * Ordered by createdAt for efficient cleanQueue range queries
23
- * Uses equality check on jobId to prevent duplicate entries for the same job
24
- */
25
15
  private readonly temporalIndex;
26
16
  /** Set of delayed job IDs for tracking when they become ready */
27
17
  private readonly delayedJobIds;
@@ -65,6 +55,7 @@ export declare class TemporalManager {
65
55
  * Returns number of jobs that became ready
66
56
  */
67
57
  refreshDelayed(now: number): number;
58
+ private maybeCompactDelayedHeap;
68
59
  /** Get delayed job count */
69
60
  get delayedCount(): number;
70
61
  /** Clear all delayed tracking */
@@ -5,29 +5,15 @@
5
5
  * - Temporal index (SkipList) for efficient cleanQueue range queries
6
6
  * - Delayed heap (MinHeap) for O(k) delayed job refresh
7
7
  */
8
- import { SkipList } from '../../shared/skipList';
9
8
  import { MinHeap } from '../../shared/minHeap';
9
+ import { TemporalIndex } from './temporalIndex';
10
+ const DELAYED_COMPACTION_MIN_STALE = 256;
10
11
  /**
11
12
  * Manages temporal indexing for efficient job cleanup
12
13
  * and delayed job tracking for O(k) refresh
13
14
  */
14
15
  export class TemporalManager {
15
- /**
16
- * Temporal index: Skip List for O(log n) insert/delete
17
- * Ordered by createdAt for efficient cleanQueue range queries
18
- * Uses equality check on jobId to prevent duplicate entries for the same job
19
- */
20
- // Total-order comparator: createdAt first, then jobId as a tie-break. Without
21
- // the tie-break, a batch of jobs sharing one createdAt (the addBulk case —
22
- // `now` is captured once) makes every node compare-equal, which (a) turns
23
- // SkipList.insert's duplicate-check scan into O(n) per insert => O(n²) for the
24
- // batch, and (b) makes SkipList.delete remove the WRONG same-createdAt node
25
- // (it stops at the first compare-equal node). A total order fixes both: the
26
- // dedup scan and delete both resolve to the exact (createdAt, jobId) node in
27
- // O(log n). jobId is a string (UUIDv7 by default, or any custom id), so its
28
- // lexicographic comparison is a valid total order in every case. Equality is
29
- // still by jobId (now reached only for a true duplicate).
30
- temporalIndex = new SkipList((a, b) => a.createdAt - b.createdAt || (a.jobId < b.jobId ? -1 : a.jobId > b.jobId ? 1 : 0), 16, 0.5, (a, b) => a.jobId === b.jobId);
16
+ temporalIndex = new TemporalIndex();
31
17
  /** Set of delayed job IDs for tracking when they become ready */
32
18
  delayedJobIds = new Set();
33
19
  /**
@@ -40,7 +26,7 @@ export class TemporalManager {
40
26
  // ============ Temporal Index Operations ============
41
27
  /** Add job to temporal index - O(log n) */
42
28
  addToIndex(createdAt, jobId, queue) {
43
- this.temporalIndex.insert({ createdAt, jobId, queue });
29
+ this.temporalIndex.add(createdAt, jobId, queue);
44
30
  }
45
31
  /**
46
32
  * Get old jobs from temporal index - O(log n + k)
@@ -49,36 +35,22 @@ export class TemporalManager {
49
35
  getOldJobs(queue, thresholdMs, limit) {
50
36
  const now = Date.now();
51
37
  const threshold = now - thresholdMs;
52
- const result = [];
53
- for (const entry of this.temporalIndex.values()) {
54
- if (entry.createdAt > threshold)
55
- break;
56
- if (entry.queue === queue) {
57
- result.push({ jobId: entry.jobId, createdAt: entry.createdAt });
58
- if (result.length >= limit)
59
- break;
60
- }
61
- }
62
- return result;
38
+ return this.temporalIndex.getOldJobs(queue, threshold, limit);
63
39
  }
64
40
  /** Remove job from temporal index */
65
41
  removeFromIndex(jobId) {
66
- this.temporalIndex.deleteWhere((e) => e.jobId === jobId);
42
+ this.temporalIndex.remove(jobId);
67
43
  }
68
44
  /** Clear temporal index for a queue */
69
45
  clearIndexForQueue(queue) {
70
- this.temporalIndex.removeAll((e) => e.queue === queue);
46
+ this.temporalIndex.clearQueue(queue);
71
47
  }
72
48
  /**
73
49
  * Clean orphaned temporal index entries.
74
50
  * Removes entries for jobs that no longer exist.
75
51
  */
76
52
  cleanOrphaned(validJobIds) {
77
- if (this.temporalIndex.size === 0)
78
- return 0;
79
- const beforeSize = this.temporalIndex.size;
80
- this.temporalIndex.removeAll((e) => !validJobIds.has(e.jobId));
81
- return beforeSize - this.temporalIndex.size;
53
+ return this.temporalIndex.cleanOrphaned(validJobIds);
82
54
  }
83
55
  /** Get temporal index size */
84
56
  get indexSize() {
@@ -94,15 +66,15 @@ export class TemporalManager {
94
66
  this.delayedJobIds.add(jobId);
95
67
  this.delayedHeap.push({ jobId, runAt });
96
68
  this.delayedRunAt.set(jobId, runAt);
69
+ this.maybeCompactDelayedHeap();
97
70
  }
98
71
  /** Remove a delayed job (lazy removal from heap) */
99
72
  removeDelayed(jobId) {
100
- if (this.delayedJobIds.has(jobId)) {
101
- this.delayedJobIds.delete(jobId);
102
- this.delayedRunAt.delete(jobId);
103
- return true;
104
- }
105
- return false;
73
+ if (!this.delayedJobIds.delete(jobId))
74
+ return false;
75
+ this.delayedRunAt.delete(jobId);
76
+ this.maybeCompactDelayedHeap();
77
+ return true;
106
78
  }
107
79
  /**
108
80
  * Refresh delayed jobs that have become ready
@@ -127,8 +99,20 @@ export class TemporalManager {
127
99
  this.delayedRunAt.delete(top.jobId);
128
100
  count++;
129
101
  }
102
+ this.maybeCompactDelayedHeap();
130
103
  return count;
131
104
  }
105
+ maybeCompactDelayedHeap() {
106
+ const liveCount = this.delayedRunAt.size;
107
+ if (liveCount === 0) {
108
+ this.delayedHeap.clear();
109
+ return;
110
+ }
111
+ const staleCount = this.delayedHeap.size - liveCount;
112
+ if (staleCount < DELAYED_COMPACTION_MIN_STALE || staleCount < liveCount)
113
+ return;
114
+ this.delayedHeap.buildFrom(Array.from(this.delayedRunAt, ([jobId, runAt]) => ({ jobId, runAt })));
115
+ }
132
116
  /** Get delayed job count */
133
117
  get delayedCount() {
134
118
  return this.delayedJobIds.size;
@@ -143,8 +127,7 @@ export class TemporalManager {
143
127
  /** Clear all data */
144
128
  clear() {
145
129
  this.clearDelayed();
146
- // Note: SkipList doesn't have a clear method, so we recreate by removing all
147
- this.temporalIndex.removeAll(() => true);
130
+ this.temporalIndex.clear();
148
131
  }
149
132
  // ============ Debug Info ============
150
133
  /** Get internal structure sizes for memory debugging */
@@ -1,20 +1,18 @@
1
1
  /**
2
- * WaiterManager - Manages job availability notifications
3
- * Handles worker polling with timeout-based waiting
2
+ * WaiterManager - Manages queue-scoped job availability notifications.
3
+ * Handles worker polling with timeout-based waiting.
4
4
  */
5
5
  export declare class WaiterManager {
6
- /** Waiter entries with cancellation flag for O(1) cleanup */
7
- private readonly waiters;
8
- /** Pending notification counter - incremented when notify() is called with no waiters */
9
- private pendingNotifications;
10
- /** Notify that multiple jobs are available - wakes up to `count` waiters */
6
+ private readonly queues;
7
+ private activeWaiters;
8
+ notify(queue?: string): void;
11
9
  notifyBatch(count: number): void;
12
- /** Notify that jobs are available - wakes first non-cancelled waiter */
13
- notify(): void;
14
- /** Wait for a job to become available (with timeout) */
10
+ notifyBatch(queue: string, count: number): void;
15
11
  waitForJob(timeoutMs: number): Promise<void>;
16
- /** Remove all cancelled waiters from the array */
17
- private cleanupWaiters;
18
- /** Current number of waiters */
12
+ waitForJob(queue: string, timeoutMs: number): Promise<void>;
13
+ private notifyQueue;
14
+ private createQueue;
15
+ private compact;
16
+ private deleteIdleQueue;
19
17
  get length(): number;
20
18
  }