bunqueue 2.8.52 → 2.8.54

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 (80) hide show
  1. package/dist/application/backgroundTasks.js +5 -1
  2. package/dist/application/cleanupTasks.js +1 -1
  3. package/dist/application/clientTracking.js +1 -1
  4. package/dist/application/contextFactory.js +1 -0
  5. package/dist/application/dlqManager.d.ts +7 -1
  6. package/dist/application/dlqManager.js +16 -5
  7. package/dist/application/lockManager.js +3 -3
  8. package/dist/application/operations/ack.js +2 -2
  9. package/dist/application/operations/ackHelpers.js +1 -1
  10. package/dist/application/operations/jobManagement.js +6 -4
  11. package/dist/application/operations/jobMoveOperations.js +3 -3
  12. package/dist/application/operations/jobStateTransitions.d.ts +1 -1
  13. package/dist/application/operations/jobStateTransitions.js +16 -2
  14. package/dist/application/queueManager.d.ts +22 -2
  15. package/dist/application/queueManager.js +48 -6
  16. package/dist/application/stallDetection.js +3 -3
  17. package/dist/client/flowJobDependencies.js +3 -1
  18. package/dist/client/flowJobMoveMethods.js +7 -2
  19. package/dist/client/jobConversion.d.ts +3 -3
  20. package/dist/client/jobConversion.js +10 -6
  21. package/dist/client/jobConversionHelpers.d.ts +1 -1
  22. package/dist/client/jobConversionHelpers.js +3 -6
  23. package/dist/client/jobConversionTypes.d.ts +4 -0
  24. package/dist/client/jobDeduplication.d.ts +6 -0
  25. package/dist/client/jobDeduplication.js +14 -0
  26. package/dist/client/queue/bullmqCompat.d.ts +18 -8
  27. package/dist/client/queue/bullmqCompat.js +39 -71
  28. package/dist/client/queue/deduplication.d.ts +2 -0
  29. package/dist/client/queue/deduplication.js +20 -11
  30. package/dist/client/queue/dlq.d.ts +11 -6
  31. package/dist/client/queue/dlq.js +47 -4
  32. package/dist/client/queue/dlqJobMethods.d.ts +9 -0
  33. package/dist/client/queue/dlqJobMethods.js +176 -0
  34. package/dist/client/queue/dlqOps.d.ts +2 -1
  35. package/dist/client/queue/dlqOps.js +2 -2
  36. package/dist/client/queue/jobMove.js +6 -1
  37. package/dist/client/queue/jobProxy.js +10 -5
  38. package/dist/client/queue/operations/add.js +17 -0
  39. package/dist/client/queue/operations/index.d.ts +1 -0
  40. package/dist/client/queue/operations/index.js +1 -0
  41. package/dist/client/queue/operations/management.d.ts +0 -4
  42. package/dist/client/queue/operations/management.js +33 -5
  43. package/dist/client/queue/operations/query.d.ts +1 -20
  44. package/dist/client/queue/operations/query.js +18 -53
  45. package/dist/client/queue/operations/queryStates.d.ts +13 -0
  46. package/dist/client/queue/operations/queryStates.js +32 -0
  47. package/dist/client/queue/operations/queryTcpPages.d.ts +14 -0
  48. package/dist/client/queue/operations/queryTcpPages.js +40 -0
  49. package/dist/client/queue/queue.d.ts +3 -0
  50. package/dist/client/queue/queue.js +23 -11
  51. package/dist/client/queue/rateLimit.d.ts +4 -4
  52. package/dist/client/queue/rateLimit.js +17 -8
  53. package/dist/client/queue/workers.d.ts +12 -0
  54. package/dist/client/queue/workers.js +20 -2
  55. package/dist/client/queueGroup.d.ts +11 -0
  56. package/dist/client/queueGroup.js +28 -1
  57. package/dist/client/sandboxed/worker.js +1 -1
  58. package/dist/client/worker/processor.js +1 -1
  59. package/dist/client/worker/processorHandlers.d.ts +1 -5
  60. package/dist/client/worker/processorHandlers.js +5 -7
  61. package/dist/client/worker/worker.d.ts +1 -1
  62. package/dist/client/worker/worker.js +37 -13
  63. package/dist/domain/queue/limiterManager.d.ts +11 -0
  64. package/dist/domain/queue/limiterManager.js +28 -0
  65. package/dist/domain/queue/shard.d.ts +10 -2
  66. package/dist/domain/queue/shard.js +23 -4
  67. package/dist/domain/queue/uniqueKeyManager.d.ts +3 -1
  68. package/dist/domain/queue/uniqueKeyManager.js +9 -1
  69. package/dist/domain/types/command.d.ts +34 -1
  70. package/dist/domain/types/queue.d.ts +5 -0
  71. package/dist/domain/types/queue.js +13 -0
  72. package/dist/infrastructure/persistence/sqlite.d.ts +4 -0
  73. package/dist/infrastructure/persistence/sqlite.js +16 -0
  74. package/dist/infrastructure/server/handlerRoutes.js +14 -1
  75. package/dist/infrastructure/server/handlers/dlq.d.ts +3 -0
  76. package/dist/infrastructure/server/handlers/dlq.js +18 -4
  77. package/dist/infrastructure/server/handlers/introspection.d.ts +19 -0
  78. package/dist/infrastructure/server/handlers/introspection.js +21 -0
  79. package/dist/infrastructure/server/handlers/query.js +1 -1
  80. package/package.json +1 -1
@@ -22,12 +22,15 @@ import { getSharedManager } from './manager';
22
22
  */
23
23
  export class QueueGroup {
24
24
  prefix;
25
+ queues = new Map();
25
26
  constructor(namespace) {
26
27
  this.prefix = namespace.endsWith(':') ? namespace : `${namespace}:`;
27
28
  }
28
29
  /** Get a queue within this group */
29
30
  getQueue(name, opts) {
30
- return new Queue(this.prefix + name, opts);
31
+ const queue = new Queue(this.prefix + name, opts);
32
+ this.queues.set(name, queue);
33
+ return queue;
31
34
  }
32
35
  /** Create a worker for a queue in this group */
33
36
  getWorker(name, processor, opts) {
@@ -77,4 +80,28 @@ export class QueueGroup {
77
80
  }
78
81
  }
79
82
  }
83
+ /** List queues created through this group, including remote queues. */
84
+ async listQueuesAsync() {
85
+ const names = new Set(this.queues.keys());
86
+ for (const name of this.listQueues())
87
+ names.add(name);
88
+ return [...names].sort();
89
+ }
90
+ /** Pause every queue created through this group. */
91
+ async pauseAllAsync() {
92
+ await Promise.all([...this.queues.values()].map((queue) => queue.pauseAsync()));
93
+ }
94
+ /** Resume every queue created through this group. */
95
+ async resumeAllAsync() {
96
+ await Promise.all([...this.queues.values()].map((queue) => queue.resumeAsync()));
97
+ }
98
+ /** Drain waiting jobs from every queue and return the aggregate count. */
99
+ async drainAllAsync() {
100
+ const counts = await Promise.all([...this.queues.values()].map((queue) => queue.drainAsync()));
101
+ return counts.reduce((total, count) => total + count, 0);
102
+ }
103
+ /** Remove all data from every queue created through this group. */
104
+ async obliterateAllAsync() {
105
+ await Promise.all([...this.queues.values()].map((queue) => queue.obliterateAsync()));
106
+ }
80
107
  }
@@ -550,7 +550,7 @@ export class SandboxedWorker extends EventEmitter {
550
550
  discard: createDiscardHandler(embedded, tcp),
551
551
  getDependencies: createGetDependenciesHandler(embedded, tcp, domainJob),
552
552
  getDependenciesCount: createGetDependenciesCountHandler(embedded, tcp, domainJob),
553
- removeDeduplicationKey: createRemoveDeduplicationKeyHandler(),
553
+ removeDeduplicationKey: createRemoveDeduplicationKeyHandler(embedded, tcp),
554
554
  moveToCompleted,
555
555
  moveToFailed,
556
556
  });
@@ -55,7 +55,7 @@ export async function processJob(internalJob, config) {
55
55
  discard: createDiscardHandler(embedded, tcp),
56
56
  getDependencies: createGetDependenciesHandler(embedded, tcp, internalJob),
57
57
  getDependenciesCount: createGetDependenciesCountHandler(embedded, tcp, internalJob),
58
- removeDeduplicationKey: createRemoveDeduplicationKeyHandler(),
58
+ removeDeduplicationKey: createRemoveDeduplicationKeyHandler(embedded, tcp),
59
59
  token: token ?? undefined,
60
60
  });
61
61
  jobHolder.current = job;
@@ -58,8 +58,4 @@ export declare function createDiscardHandler(embedded: boolean, tcp: TcpConnecti
58
58
  */
59
59
  export declare function createGetDependenciesHandler(embedded: boolean, tcp: TcpConnection | null, internalJob: InternalJob): (_id: string) => Promise<JobDependencies>;
60
60
  export declare function createGetDependenciesCountHandler(embedded: boolean, tcp: TcpConnection | null, internalJob: InternalJob): (_id: string) => Promise<JobDependenciesCount>;
61
- /**
62
- * removeDeduplicationKey — no server primitive; throw explicit error so callers
63
- * learn this isn't supported rather than silently getting `false`.
64
- */
65
- export declare function createRemoveDeduplicationKeyHandler(): (_id: string) => Promise<boolean>;
61
+ export declare function createRemoveDeduplicationKeyHandler(embedded: boolean, tcp: TcpConnection | null): (id: string) => Promise<boolean>;
@@ -5,6 +5,7 @@
5
5
  import { jobId } from '../../domain/types/job';
6
6
  import { getSharedManager } from '../manager';
7
7
  import { UnrecoverableError } from '../errors';
8
+ import { removeJobDeduplicationKey } from '../jobDeduplication';
8
9
  export function createProgressHandler(embedded, tcp, emitter, jobHolder) {
9
10
  return async (id, progress, message) => {
10
11
  if (embedded) {
@@ -306,7 +307,8 @@ export function createMoveToWaitingChildrenHandler(embedded, tcp) {
306
307
  }
307
308
  if (!tcp)
308
309
  return false;
309
- throw new Error('moveToWaitingChildren is not supported in TCP mode — no server command available');
310
+ const response = await tcp.send({ cmd: 'MoveToWaitingChildren', id });
311
+ return response.ok === true;
310
312
  };
311
313
  }
312
314
  export function createWaitUntilFinishedHandler(embedded, tcp) {
@@ -391,10 +393,6 @@ export function createGetDependenciesCountHandler(embedded, tcp, internalJob) {
391
393
  };
392
394
  };
393
395
  }
394
- /**
395
- * removeDeduplicationKey no server primitive; throw explicit error so callers
396
- * learn this isn't supported rather than silently getting `false`.
397
- */
398
- export function createRemoveDeduplicationKeyHandler() {
399
- return () => Promise.reject(new Error('removeDeduplicationKey is not implemented — no server primitive available'));
396
+ export function createRemoveDeduplicationKeyHandler(embedded, tcp) {
397
+ return (id) => removeJobDeduplicationKey(id, embedded, tcp);
400
398
  }
@@ -152,7 +152,7 @@ export declare class Worker<T = unknown, R = unknown> extends EventEmitter {
152
152
  private handlePullError;
153
153
  /** Register this worker with the server (TCP only, fire-and-forget) */
154
154
  private registerWithServer;
155
- /** Start periodic worker-level heartbeat (separate from job heartbeat) */
155
+ /** Start periodic worker-level heartbeat (separate from job heartbeat). */
156
156
  private startWorkerHeartbeat;
157
157
  private getHeartbeatDeps;
158
158
  private getPullConfig;
@@ -183,8 +183,17 @@ export class Worker extends EventEmitter {
183
183
  if (this.embedded && !this.stalledUnsubscribe && !this.opts.skipStalledCheck) {
184
184
  this.subscribeToStalledEvents();
185
185
  }
186
- // Register worker with server (TCP only)
187
- if (!this.embedded && this.tcp && !this.registered) {
186
+ // Register the worker in the selected broker runtime.
187
+ if (this.embedded && !this.registered) {
188
+ getSharedManager().registerWorker(this.queueKey, [this.queueKey], this.opts.concurrency, {
189
+ workerId: this.workerId,
190
+ hostname: hostname(),
191
+ pid: process.pid,
192
+ startedAt: this.startedAt,
193
+ });
194
+ this.registered = true;
195
+ }
196
+ else if (this.tcp && !this.registered) {
188
197
  this.registerWithServer();
189
198
  }
190
199
  if (this.opts.heartbeatInterval > 0 && !this.opts.skipLockRenewal) {
@@ -199,10 +208,10 @@ export class Worker extends EventEmitter {
199
208
  else {
200
209
  const deps = this.getHeartbeatDeps();
201
210
  this.heartbeatTimer = startHeartbeat(deps, this.opts.heartbeatInterval);
202
- // Worker-level heartbeat (separate from job heartbeat)
203
- this.startWorkerHeartbeat();
204
211
  }
205
212
  }
213
+ if (this.opts.heartbeatInterval > 0)
214
+ this.startWorkerHeartbeat();
206
215
  this.poll();
207
216
  }
208
217
  /** Subscribe to stalled events from QueueManager (BullMQ v5 compatible) */
@@ -485,13 +494,18 @@ export class Worker extends EventEmitter {
485
494
  await this.ackBatcher.flush();
486
495
  await this.ackBatcher.waitForInFlight();
487
496
  this.ackBatcher.stop();
488
- // Unregister from server before closing connection
489
- if (!this.embedded && this.tcp && this.registered) {
490
- try {
491
- await this.tcp.send({ cmd: 'UnregisterWorker', workerId: this.workerId });
497
+ // Unregister before closing the transport/runtime.
498
+ if (this.registered) {
499
+ if (this.embedded) {
500
+ getSharedManager().unregisterWorker(this.workerId);
492
501
  }
493
- catch {
494
- // Best-effort unregister — server will cleanup via stale timeout
502
+ else if (this.tcp) {
503
+ try {
504
+ await this.tcp.send({ cmd: 'UnregisterWorker', workerId: this.workerId });
505
+ }
506
+ catch {
507
+ // Best-effort unregister — server will cleanup via stale timeout
508
+ }
495
509
  }
496
510
  this.registered = false;
497
511
  }
@@ -852,12 +866,22 @@ export class Worker extends EventEmitter {
852
866
  this.emit('error', Object.assign(error, { context: 'worker-register' }));
853
867
  });
854
868
  }
855
- /** Start periodic worker-level heartbeat (separate from job heartbeat) */
869
+ /** Start periodic worker-level heartbeat (separate from job heartbeat). */
856
870
  startWorkerHeartbeat() {
857
- if (this.workerHeartbeatTimer || !this.tcp)
871
+ if (this.workerHeartbeatTimer)
858
872
  return;
859
873
  this.workerHeartbeatTimer = setInterval(() => {
860
- if (!this.tcp || !this.registered)
874
+ if (!this.registered)
875
+ return;
876
+ if (this.embedded) {
877
+ getSharedManager().workerManager.heartbeat(this.workerId, {
878
+ activeJobs: this.activeJobs,
879
+ processed: this.processedCount,
880
+ failed: this.failedCount,
881
+ });
882
+ return;
883
+ }
884
+ if (!this.tcp)
861
885
  return;
862
886
  void this.tcp
863
887
  .send({
@@ -38,6 +38,13 @@ export declare class LimiterManager {
38
38
  expireRateLimitIfNeeded(queue: string): void;
39
39
  /** Try to acquire rate limit token */
40
40
  tryAcquireRateLimit(queue: string): boolean;
41
+ /** Read the active rate-limit configuration after applying lazy expiry. */
42
+ getRateLimit(queue: string): {
43
+ max: number;
44
+ duration: number;
45
+ } | null;
46
+ /** Read the remaining temporary-limit TTL or token-bucket cooldown. */
47
+ getRateLimitTtl(queue: string, maxJobs?: number): number;
41
48
  /** Set concurrency limit for queue */
42
49
  setConcurrency(queue: string, limit: number): void;
43
50
  /** Clear concurrency limit */
@@ -46,6 +53,10 @@ export declare class LimiterManager {
46
53
  tryAcquireConcurrency(queue: string): boolean;
47
54
  /** Release concurrency slot */
48
55
  releaseConcurrency(queue: string): void;
56
+ /** Read the configured global concurrency limit. */
57
+ getConcurrency(queue: string): number | null;
58
+ /** Whether all configured global concurrency slots are currently occupied. */
59
+ isConcurrencyMaxed(queue: string): boolean;
49
60
  /** Get all queue names with state */
50
61
  getQueueNames(): string[];
51
62
  /** Delete queue data */
@@ -78,6 +78,25 @@ export class LimiterManager {
78
78
  const limiter = this.rateLimiters.get(queue);
79
79
  return !limiter || limiter.tryAcquire();
80
80
  }
81
+ /** Read the active rate-limit configuration after applying lazy expiry. */
82
+ getRateLimit(queue) {
83
+ this.expireRateLimitIfNeeded(queue);
84
+ const state = this.queueState.get(queue);
85
+ if (state?.rateLimit === null || state?.rateLimit === undefined)
86
+ return null;
87
+ return { max: state.rateLimit, duration: state.rateLimitDuration ?? 1000 };
88
+ }
89
+ /** Read the remaining temporary-limit TTL or token-bucket cooldown. */
90
+ getRateLimitTtl(queue, maxJobs) {
91
+ this.expireRateLimitIfNeeded(queue);
92
+ const state = this.queueState.get(queue);
93
+ if (state?.rateLimit === null || state?.rateLimit === undefined)
94
+ return -2;
95
+ if (state.rateLimitExpiresAt !== null) {
96
+ return Math.max(0, state.rateLimitExpiresAt - Date.now());
97
+ }
98
+ return this.rateLimiters.get(queue)?.getTtl(maxJobs) ?? -2;
99
+ }
81
100
  // ============ Concurrency Limiting ============
82
101
  /** Set concurrency limit for queue */
83
102
  setConcurrency(queue, limit) {
@@ -107,6 +126,15 @@ export class LimiterManager {
107
126
  releaseConcurrency(queue) {
108
127
  this.concurrencyLimiters.get(queue)?.release();
109
128
  }
129
+ /** Read the configured global concurrency limit. */
130
+ getConcurrency(queue) {
131
+ return this.queueState.get(queue)?.concurrencyLimit ?? null;
132
+ }
133
+ /** Whether all configured global concurrency slots are currently occupied. */
134
+ isConcurrencyMaxed(queue) {
135
+ const limiter = this.concurrencyLimiters.get(queue);
136
+ return limiter ? limiter.getActive() >= limiter.getLimit() : false;
137
+ }
110
138
  // ============ Queue Management ============
111
139
  /** Get all queue names with state */
112
140
  getQueueNames() {
@@ -63,7 +63,8 @@ export declare class Shard {
63
63
  registerUniqueKey(queue: string, key: string, jobId: JobId): void;
64
64
  registerUniqueKeyWithTtl(queue: string, key: string, jobId: JobId, ttl?: number): void;
65
65
  extendUniqueKeyTtl(queue: string, key: string, ttl: number): boolean;
66
- releaseUniqueKey(queue: string, key: string): void;
66
+ releaseUniqueKey(queue: string, key: string): boolean;
67
+ releaseUniqueKeyIfOwned(queue: string, key: string, ownerId: JobId): boolean;
67
68
  cleanExpiredUniqueKeys(): number;
68
69
  get uniqueKeys(): Map<string, Map<string, UniqueKeyEntry>>;
69
70
  isGroupActive(queue: string, groupId: string): boolean;
@@ -73,13 +74,20 @@ export declare class Shard {
73
74
  clearRateLimit(queue: string): void;
74
75
  expireRateLimitIfNeeded(queue: string): void;
75
76
  tryAcquireRateLimit(queue: string): boolean;
77
+ getRateLimit(queue: string): {
78
+ max: number;
79
+ duration: number;
80
+ } | null;
81
+ getRateLimitTtl(queue: string, maxJobs?: number): number;
76
82
  setConcurrency(queue: string, limit: number): void;
77
83
  clearConcurrency(queue: string): void;
78
84
  tryAcquireConcurrency(queue: string): boolean;
79
85
  releaseConcurrency(queue: string): void;
86
+ getConcurrency(queue: string): number | null;
87
+ isConcurrencyMaxed(queue: string): boolean;
80
88
  get queueState(): Map<string, QueueState>;
81
89
  clearQueueLimiters(queue: string): void;
82
- releaseJobResources(queue: string, uniqueKey: string | null, groupId: string | null): void;
90
+ releaseJobResources(queue: string, uniqueKey: string | null, groupId: string | null, ownerId?: JobId): void;
83
91
  get waitingDeps(): Map<JobId, Job>;
84
92
  get dependencyIndex(): Map<JobId, Set<JobId>>;
85
93
  get waitingChildren(): Map<JobId, Job>;
@@ -114,7 +114,10 @@ export class Shard {
114
114
  return this.uniqueKeyManager.extendTtl(queue, key, ttl);
115
115
  }
116
116
  releaseUniqueKey(queue, key) {
117
- this.uniqueKeyManager.release(queue, key);
117
+ return this.uniqueKeyManager.release(queue, key);
118
+ }
119
+ releaseUniqueKeyIfOwned(queue, key, ownerId) {
120
+ return this.uniqueKeyManager.releaseIfOwned(queue, key, ownerId);
118
121
  }
119
122
  cleanExpiredUniqueKeys() {
120
123
  return this.uniqueKeyManager.cleanExpired();
@@ -150,6 +153,12 @@ export class Shard {
150
153
  tryAcquireRateLimit(queue) {
151
154
  return this.limiterManager.tryAcquireRateLimit(queue);
152
155
  }
156
+ getRateLimit(queue) {
157
+ return this.limiterManager.getRateLimit(queue);
158
+ }
159
+ getRateLimitTtl(queue, maxJobs) {
160
+ return this.limiterManager.getRateLimitTtl(queue, maxJobs);
161
+ }
153
162
  setConcurrency(queue, limit) {
154
163
  this.limiterManager.setConcurrency(queue, limit);
155
164
  }
@@ -162,6 +171,12 @@ export class Shard {
162
171
  releaseConcurrency(queue) {
163
172
  this.limiterManager.releaseConcurrency(queue);
164
173
  }
174
+ getConcurrency(queue) {
175
+ return this.limiterManager.getConcurrency(queue);
176
+ }
177
+ isConcurrencyMaxed(queue) {
178
+ return this.limiterManager.isConcurrencyMaxed(queue);
179
+ }
165
180
  get queueState() {
166
181
  return this.limiterManager.getStateMap();
167
182
  }
@@ -169,9 +184,13 @@ export class Shard {
169
184
  this.limiterManager.deleteQueue(queue);
170
185
  }
171
186
  // ============ Resource Release ============
172
- releaseJobResources(queue, uniqueKey, groupId) {
173
- if (uniqueKey)
174
- this.releaseUniqueKey(queue, uniqueKey);
187
+ releaseJobResources(queue, uniqueKey, groupId, ownerId) {
188
+ if (uniqueKey) {
189
+ if (ownerId)
190
+ this.releaseUniqueKeyIfOwned(queue, uniqueKey, ownerId);
191
+ else
192
+ this.releaseUniqueKey(queue, uniqueKey);
193
+ }
175
194
  if (groupId)
176
195
  this.releaseGroup(queue, groupId);
177
196
  this.releaseConcurrency(queue);
@@ -21,7 +21,9 @@ export declare class UniqueKeyManager {
21
21
  /** Extend TTL for an existing unique key */
22
22
  extendTtl(queue: string, key: string, ttl: number): boolean;
23
23
  /** Release unique key */
24
- release(queue: string, key: string): void;
24
+ release(queue: string, key: string): boolean;
25
+ /** Release a key only when it is still owned by the given job. */
26
+ releaseIfOwned(queue: string, key: string, ownerId: JobId): boolean;
25
27
  /** Clean expired unique keys (call periodically) */
26
28
  cleanExpired(): number;
27
29
  /** Clear all keys for a queue */
@@ -59,7 +59,15 @@ export class UniqueKeyManager {
59
59
  }
60
60
  /** Release unique key */
61
61
  release(queue, key) {
62
- this.keys.get(queue)?.delete(key);
62
+ return this.keys.get(queue)?.delete(key) ?? false;
63
+ }
64
+ /** Release a key only when it is still owned by the given job. */
65
+ releaseIfOwned(queue, key, ownerId) {
66
+ const queueKeys = this.keys.get(queue);
67
+ const entry = queueKeys?.get(key);
68
+ if (!entry || entry.jobId !== ownerId)
69
+ return false;
70
+ return queueKeys?.delete(key) ?? false;
63
71
  }
64
72
  /** Clean expired unique keys (call periodically) */
65
73
  cleanExpired() {
@@ -5,6 +5,7 @@
5
5
  import type { JobInput, JobState } from './job';
6
6
  import type { AtomicFlowJobInput } from './flow';
7
7
  import type { CronJobOptions } from './cron';
8
+ import type { DlqFilter } from './dlq';
8
9
  /** Base command interface */
9
10
  interface BaseCommand {
10
11
  readonly cmd: string;
@@ -143,6 +144,7 @@ export interface GetJobsCommand extends BaseCommand {
143
144
  readonly state?: JobState | JobState[];
144
145
  readonly limit?: number;
145
146
  readonly offset?: number;
147
+ readonly asc?: boolean;
146
148
  }
147
149
  export interface GetJobCountsCommand extends BaseCommand {
148
150
  readonly cmd: 'GetJobCounts';
@@ -238,6 +240,11 @@ export interface DlqCommand extends BaseCommand {
238
240
  readonly cmd: 'Dlq';
239
241
  readonly queue: string;
240
242
  readonly count?: number;
243
+ readonly filter?: DlqFilter;
244
+ }
245
+ export interface GetDlqStatsCommand extends BaseCommand {
246
+ readonly cmd: 'GetDlqStats';
247
+ readonly queue: string;
241
248
  }
242
249
  export interface RetryDlqCommand extends BaseCommand {
243
250
  readonly cmd: 'RetryDlq';
@@ -245,6 +252,7 @@ export interface RetryDlqCommand extends BaseCommand {
245
252
  readonly jobId?: string;
246
253
  /** Cap the number of DLQ entries retried (omit = retry all). #111-class. */
247
254
  readonly count?: number;
255
+ readonly filter?: DlqFilter;
248
256
  }
249
257
  export interface PurgeDlqCommand extends BaseCommand {
250
258
  readonly cmd: 'PurgeDlq';
@@ -254,6 +262,31 @@ export interface RetryCompletedCommand extends BaseCommand {
254
262
  readonly cmd: 'RetryCompleted';
255
263
  readonly queue: string;
256
264
  readonly id?: string;
265
+ readonly count?: number;
266
+ readonly timestamp?: number;
267
+ }
268
+ export interface GetQueueLimitsCommand extends BaseCommand {
269
+ readonly cmd: 'GetQueueLimits';
270
+ readonly queue: string;
271
+ readonly maxJobs?: number;
272
+ }
273
+ export interface GetDeduplicationJobIdCommand extends BaseCommand {
274
+ readonly cmd: 'GetDeduplicationJobId';
275
+ readonly queue: string;
276
+ readonly deduplicationId: string;
277
+ }
278
+ export interface RemoveDeduplicationKeyCommand extends BaseCommand {
279
+ readonly cmd: 'RemoveDeduplicationKey';
280
+ readonly queue: string;
281
+ readonly deduplicationId: string;
282
+ }
283
+ export interface RemoveJobDeduplicationKeyCommand extends BaseCommand {
284
+ readonly cmd: 'RemoveJobDeduplicationKey';
285
+ readonly id: string;
286
+ }
287
+ export interface MoveToWaitingChildrenCommand extends BaseCommand {
288
+ readonly cmd: 'MoveToWaitingChildren';
289
+ readonly id: string;
257
290
  }
258
291
  export interface RateLimitCommand extends BaseCommand {
259
292
  readonly cmd: 'RateLimit';
@@ -516,7 +549,7 @@ export interface HelloCommand extends BaseCommand {
516
549
  readonly capabilities?: 'pipelining'[];
517
550
  }
518
551
  /** Union of all commands */
519
- export type Command = PushCommand | PushBatchCommand | PushFlowCommand | PullCommand | PullBatchCommand | AckCommand | AckBatchCommand | FailCommand | GetJobCommand | GetStateCommand | GetResultCommand | GetJobsCommand | GetJobCountsCommand | GetCountsPerPriorityCommand | GetJobByCustomIdCommand | CountCommand | GetProgressCommand | CancelCommand | ProgressCommand | UpdateCommand | ChangePriorityCommand | PromoteCommand | WaitJobCommand | MoveToDelayedCommand | DiscardCommand | PauseCommand | ResumeCommand | IsPausedCommand | DrainCommand | ObliterateCommand | ListQueuesCommand | CleanCommand | DlqCommand | RetryDlqCommand | PurgeDlqCommand | RetryCompletedCommand | RateLimitCommand | SetConcurrencyCommand | RateLimitClearCommand | ClearConcurrencyCommand | SetStallConfigCommand | GetStallConfigCommand | SetDlqConfigCommand | GetDlqConfigCommand | CronCommand | CronDeleteCommand | CronListCommand | AddLogCommand | GetLogsCommand | HeartbeatCommand | JobHeartbeatCommand | JobHeartbeatBatchCommand | PingCommand | RegisterWorkerCommand | UnregisterWorkerCommand | ListWorkersCommand | AddWebhookCommand | RemoveWebhookCommand | ListWebhooksCommand | StatsCommand | MetricsCommand | PrometheusCommand | CronGetCommand | GetChildrenValuesCommand | StorageStatusCommand | ClearLogsCommand | ExtendLockCommand | ExtendLocksCommand | ChangeDelayCommand | SetWebhookEnabledCommand | CompactMemoryCommand | UpdateParentCommand | MoveToWaitCommand | PromoteJobsCommand | DashboardOverviewCommand | DashboardQueuesCommand | DashboardQueueCommand | AuthCommand | GetFailedChildrenValuesCommand | GetIgnoredChildrenFailuresCommand | RemoveChildDependencyCommand | RemoveUnprocessedChildrenCommand | HelloCommand;
552
+ export type Command = PushCommand | PushBatchCommand | PushFlowCommand | PullCommand | PullBatchCommand | AckCommand | AckBatchCommand | FailCommand | GetJobCommand | GetStateCommand | GetResultCommand | GetJobsCommand | GetJobCountsCommand | GetCountsPerPriorityCommand | GetJobByCustomIdCommand | CountCommand | GetProgressCommand | CancelCommand | ProgressCommand | UpdateCommand | ChangePriorityCommand | PromoteCommand | WaitJobCommand | MoveToDelayedCommand | DiscardCommand | PauseCommand | ResumeCommand | IsPausedCommand | DrainCommand | ObliterateCommand | ListQueuesCommand | CleanCommand | DlqCommand | GetDlqStatsCommand | RetryDlqCommand | PurgeDlqCommand | RetryCompletedCommand | GetQueueLimitsCommand | GetDeduplicationJobIdCommand | RemoveDeduplicationKeyCommand | RemoveJobDeduplicationKeyCommand | MoveToWaitingChildrenCommand | RateLimitCommand | SetConcurrencyCommand | RateLimitClearCommand | ClearConcurrencyCommand | SetStallConfigCommand | GetStallConfigCommand | SetDlqConfigCommand | GetDlqConfigCommand | CronCommand | CronDeleteCommand | CronListCommand | AddLogCommand | GetLogsCommand | HeartbeatCommand | JobHeartbeatCommand | JobHeartbeatBatchCommand | PingCommand | RegisterWorkerCommand | UnregisterWorkerCommand | ListWorkersCommand | AddWebhookCommand | RemoveWebhookCommand | ListWebhooksCommand | StatsCommand | MetricsCommand | PrometheusCommand | CronGetCommand | GetChildrenValuesCommand | StorageStatusCommand | ClearLogsCommand | ExtendLockCommand | ExtendLocksCommand | ChangeDelayCommand | SetWebhookEnabledCommand | CompactMemoryCommand | UpdateParentCommand | MoveToWaitCommand | PromoteJobsCommand | DashboardOverviewCommand | DashboardQueuesCommand | DashboardQueueCommand | AuthCommand | GetFailedChildrenValuesCommand | GetIgnoredChildrenFailuresCommand | RemoveChildDependencyCommand | RemoveUnprocessedChildrenCommand | HelloCommand;
520
553
  /** Extract command type */
521
554
  export type CommandType = Command['cmd'];
522
555
  export {};
@@ -29,6 +29,11 @@ export declare class RateLimiter {
29
29
  private refill;
30
30
  /** Get current token count */
31
31
  getTokens(): number;
32
+ /**
33
+ * Milliseconds until the bucket can admit another job. When `maxJobs` is
34
+ * provided, report 0 until at least that many tokens have been consumed.
35
+ */
36
+ getTtl(maxJobs?: number): number;
32
37
  }
33
38
  /** Concurrency limiter */
34
39
  export declare class ConcurrencyLimiter {
@@ -48,6 +48,19 @@ export class RateLimiter {
48
48
  this.refill();
49
49
  return this.tokens;
50
50
  }
51
+ /**
52
+ * Milliseconds until the bucket can admit another job. When `maxJobs` is
53
+ * provided, report 0 until at least that many tokens have been consumed.
54
+ */
55
+ getTtl(maxJobs) {
56
+ const tokens = this.getTokens();
57
+ const consumed = this.capacity - tokens;
58
+ if (maxJobs !== undefined && consumed < maxJobs)
59
+ return 0;
60
+ if (tokens >= 1 || this.refillRate <= 0)
61
+ return 0;
62
+ return Math.ceil(((1 - tokens) / this.refillRate) * 1000);
63
+ }
51
64
  }
52
65
  /** Concurrency limiter */
53
66
  export class ConcurrencyLimiter {
@@ -97,6 +97,8 @@ export declare class SqliteStorage {
97
97
  */
98
98
  private flushIfBuffered;
99
99
  markActive(jobId: JobId, startedAt: number, timeline?: JobTimelineEntry[]): void;
100
+ /** Persist a manually parked parent so recovery does not requeue it. */
101
+ markWaitingChildren(jobId: JobId, timeline?: JobTimelineEntry[]): void;
100
102
  markCompleted(jobId: JobId, completedAt: number, timeline?: JobTimelineEntry[]): void;
101
103
  markFailed(job: Job, error: string | null): void;
102
104
  /** Save DLQ entry with full metadata */
@@ -122,6 +124,8 @@ export declare class SqliteStorage {
122
124
  deleteJob(jobId: JobId): void;
123
125
  /** Update a job's data blob (e.g. after adding __parentId) */
124
126
  updateJobData(jobId: JobId, data: unknown): void;
127
+ /** Clear a released deduplication key so recovery cannot resurrect it. */
128
+ clearJobUniqueKey(jobId: JobId): void;
125
129
  /** Persist priority ordering, including its LIFO tie-break, across recovery. */
126
130
  updateJobPriority(jobId: JobId, priority: number, lifo: boolean): void;
127
131
  /** Persist progress and its heartbeat as one active-job mutation. */
@@ -289,6 +289,15 @@ export class SqliteStorage {
289
289
  .run('active', startedAt, timeline && timeline.length > 0 ? pack(timeline) : null, jobId);
290
290
  });
291
291
  }
292
+ /** Persist a manually parked parent so recovery does not requeue it. */
293
+ markWaitingChildren(jobId, timeline) {
294
+ this.flushIfBuffered(jobId);
295
+ this.safeWrite(() => {
296
+ this.db
297
+ .prepare('UPDATE jobs SET state = ?, started_at = NULL, timeline = ? WHERE id = ?')
298
+ .run('waiting-children', timeline && timeline.length > 0 ? pack(timeline) : null, jobId);
299
+ });
300
+ }
292
301
  markCompleted(jobId, completedAt, timeline) {
293
302
  this.flushIfBuffered(jobId);
294
303
  this.safeWrite(() => {
@@ -432,6 +441,13 @@ export class SqliteStorage {
432
441
  this.db.prepare('UPDATE jobs SET data = ? WHERE id = ?').run(pack(data), jobId);
433
442
  });
434
443
  }
444
+ /** Clear a released deduplication key so recovery cannot resurrect it. */
445
+ clearJobUniqueKey(jobId) {
446
+ this.flushIfBuffered(jobId);
447
+ this.safeWrite(() => {
448
+ this.db.prepare('UPDATE jobs SET unique_key = NULL WHERE id = ?').run(jobId);
449
+ });
450
+ }
435
451
  /** Persist priority ordering, including its LIFO tie-break, across recovery. */
436
452
  updateJobPriority(jobId, priority, lifo) {
437
453
  this.flushIfBuffered(jobId);
@@ -6,7 +6,8 @@ import { handlePush, handlePushBatch, handlePull, handlePullBatch, handleAck, ha
6
6
  import { handlePushFlow } from './handlers/flow';
7
7
  import { handleGetJob, handleGetState, handleGetResult, handleGetJobCounts, handleGetCountsPerPriority, handleGetJobByCustomId, handleGetJobs, handleGetChildrenValues, } from './handlers/query';
8
8
  import { handleCancel, handleProgress, handleGetProgress, handlePause, handleResume, handleDrain, handleStats, handleMetrics, handleStorageStatus, } from './handlers/management';
9
- import { handleDlq, handleRetryDlq, handlePurgeDlq, handleRetryCompleted } from './handlers/dlq';
9
+ import { handleDlq, handleGetDlqStats, handleRetryDlq, handlePurgeDlq, handleRetryCompleted, } from './handlers/dlq';
10
+ import { handleGetQueueLimits, handleGetDeduplicationJobId, handleRemoveDeduplicationKey, handleRemoveJobDeduplicationKey, handleMoveToWaitingChildren, } from './handlers/introspection';
10
11
  import { handleCron, handleCronGet, handleCronDelete, handleCronList } from './handlers/cron';
11
12
  import { handleUpdate, handleUpdateParent, handleChangePriority, handlePromote, handleMoveToDelayed, handleDiscard, handleWaitJob, handleIsPaused, handleObliterate, handleListQueues, handleClean, handleCount, handleRateLimit, handleRateLimitClear, handleSetConcurrency, handleClearConcurrency, handleChangeDelay, handleMoveToWait, handlePromoteJobs, handleSetStallConfig, handleGetStallConfig, handleSetDlqConfig, handleGetDlqConfig, handleGetFailedChildrenValues, handleGetIgnoredChildrenFailures, handleRemoveChildDependency, handleRemoveUnprocessedChildren, } from './handlers/advanced';
12
13
  import { handleDashboardOverview, handleDashboardQueues, handleDashboardQueue, } from './handlers/dashboard';
@@ -59,6 +60,10 @@ export async function routeQueryCommand(cmd, ctx, reqId) {
59
60
  return handleGetProgress(cmd, ctx, reqId);
60
61
  case 'GetChildrenValues':
61
62
  return handleGetChildrenValues(cmd, ctx, reqId);
63
+ case 'GetQueueLimits':
64
+ return handleGetQueueLimits(cmd, ctx, reqId);
65
+ case 'GetDeduplicationJobId':
66
+ return handleGetDeduplicationJobId(cmd, ctx, reqId);
62
67
  default:
63
68
  return null;
64
69
  }
@@ -103,6 +108,12 @@ export async function routeManagementCommand(cmd, ctx, reqId) {
103
108
  return handleRemoveChildDependency(cmd, ctx, reqId);
104
109
  case 'RemoveUnprocessedChildren':
105
110
  return handleRemoveUnprocessedChildren(cmd, ctx, reqId);
111
+ case 'RemoveDeduplicationKey':
112
+ return handleRemoveDeduplicationKey(cmd, ctx, reqId);
113
+ case 'RemoveJobDeduplicationKey':
114
+ return handleRemoveJobDeduplicationKey(cmd, ctx, reqId);
115
+ case 'MoveToWaitingChildren':
116
+ return handleMoveToWaitingChildren(cmd, ctx, reqId);
106
117
  default:
107
118
  return null;
108
119
  }
@@ -135,6 +146,8 @@ export function routeDlqCommand(cmd, ctx, reqId) {
135
146
  switch (cmd.cmd) {
136
147
  case 'Dlq':
137
148
  return handleDlq(cmd, ctx, reqId);
149
+ case 'GetDlqStats':
150
+ return handleGetDlqStats(cmd, ctx, reqId);
138
151
  case 'RetryDlq':
139
152
  return handleRetryDlq(cmd, ctx, reqId);
140
153
  case 'PurgeDlq':
@@ -9,6 +9,9 @@ import type { HandlerContext } from '../types';
9
9
  export declare function handleDlq(cmd: Extract<Command, {
10
10
  cmd: 'Dlq';
11
11
  }>, ctx: HandlerContext, reqId?: string): Response;
12
+ export declare function handleGetDlqStats(cmd: Extract<Command, {
13
+ cmd: 'GetDlqStats';
14
+ }>, ctx: HandlerContext, reqId?: string): Response;
12
15
  /** Handle RetryDlq command - retry DLQ jobs */
13
16
  export declare function handleRetryDlq(cmd: Extract<Command, {
14
17
  cmd: 'RetryDlq';
@@ -6,14 +6,25 @@ import * as resp from '../../../domain/types/response';
6
6
  import { jobId } from '../../../domain/types/job';
7
7
  /** Handle Dlq command - get DLQ jobs */
8
8
  export function handleDlq(cmd, ctx, reqId) {
9
- const jobs = ctx.queueManager.getDlq(cmd.queue, cmd.count);
10
- return resp.jobs(jobs, reqId);
9
+ const entries = ctx.queueManager.getDlqEntries(cmd.queue, cmd.filter);
10
+ const selected = cmd.count === undefined ? entries : entries.slice(0, Math.max(0, cmd.count));
11
+ return {
12
+ ok: true,
13
+ jobs: selected.map((entry) => entry.job),
14
+ entries: selected,
15
+ reqId,
16
+ };
17
+ }
18
+ export function handleGetDlqStats(cmd, ctx, reqId) {
19
+ return resp.data({ stats: ctx.queueManager.getDlqStats(cmd.queue) }, reqId);
11
20
  }
12
21
  /** Handle RetryDlq command - retry DLQ jobs */
13
22
  export function handleRetryDlq(cmd, ctx, reqId) {
14
23
  const jid = cmd.jobId ? jobId(cmd.jobId) : undefined;
15
24
  // #111-class: honour the caller's `count` cap instead of retrying the whole DLQ.
16
- const count = ctx.queueManager.retryDlq(cmd.queue, jid, cmd.count);
25
+ const count = cmd.filter
26
+ ? ctx.queueManager.retryDlqByFilter(cmd.queue, cmd.filter)
27
+ : ctx.queueManager.retryDlq(cmd.queue, jid, cmd.count);
17
28
  if (count > 0) {
18
29
  const event = jid ? 'dlq:retried' : 'dlq:retry-all';
19
30
  const data = { queue: cmd.queue, count };
@@ -33,6 +44,9 @@ export function handlePurgeDlq(cmd, ctx, reqId) {
33
44
  /** Handle RetryCompleted command - retry completed jobs */
34
45
  export function handleRetryCompleted(cmd, ctx, reqId) {
35
46
  const jid = cmd.id ? jobId(cmd.id) : undefined;
36
- const count = ctx.queueManager.retryCompleted(cmd.queue, jid);
47
+ const count = ctx.queueManager.retryCompleted(cmd.queue, jid, {
48
+ limit: cmd.count,
49
+ timestamp: cmd.timestamp,
50
+ });
37
51
  return { ok: true, count, reqId };
38
52
  }