bunqueue 2.8.33 → 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.
@@ -29,8 +29,6 @@ export declare function updateJobProgress(jobId: JobId, progress: number, ctx: J
29
29
  export declare function updateJobData(jobId: JobId, data: unknown, ctx: JobManagementContext): Promise<boolean>;
30
30
  /** Change job priority */
31
31
  export declare function changeJobPriority(jobId: JobId, priority: number, ctx: JobManagementContext, lifo?: boolean): Promise<boolean>;
32
- /** Promote delayed job to waiting */
33
- export declare function promoteJob(jobId: JobId, ctx: JobManagementContext): Promise<boolean>;
34
32
  /** Move active job back to delayed */
35
33
  export declare function moveJobToDelayed(jobId: JobId, delay: number, ctx: JobManagementContext): Promise<boolean>;
36
34
  /** Discard job to DLQ */
@@ -162,20 +162,6 @@ export async function changeJobPriority(jobId, priority, ctx, lifo) {
162
162
  return q.updatePriority(jobId, priority, lifo);
163
163
  });
164
164
  }
165
- /** Promote delayed job to waiting */
166
- export async function promoteJob(jobId, ctx) {
167
- const location = ctx.jobIndex.get(jobId);
168
- if (location?.type !== 'queue')
169
- return false;
170
- return withWriteLock(ctx.shardLocks[location.shardIdx], () => {
171
- const q = ctx.shards[location.shardIdx].getQueue(location.queueName);
172
- const job = q.find(jobId);
173
- if (!job || job.runAt <= Date.now())
174
- return false;
175
- q.updateRunAt(jobId, Date.now());
176
- return true;
177
- });
178
- }
179
165
  /** Move active job back to delayed */
180
166
  export async function moveJobToDelayed(jobId, delay, ctx) {
181
167
  const location = ctx.jobIndex.get(jobId);
@@ -0,0 +1,7 @@
1
+ /** Delayed-job promotion operations shared by embedded and server modes. */
2
+ import type { JobId } from '../../domain/types/job';
3
+ import type { JobManagementContext } from './jobManagement';
4
+ /** Promote one delayed job and persist the transition before resolving. */
5
+ export declare function promoteJob(jobId: JobId, ctx: JobManagementContext): Promise<boolean>;
6
+ /** Promote the oldest delayed jobs from the live shard, never an eventual SQL listing. */
7
+ export declare function promoteJobs(queueName: string, count: number | undefined, ctx: JobManagementContext): Promise<number>;
@@ -0,0 +1,58 @@
1
+ /** Delayed-job promotion operations shared by embedded and server modes. */
2
+ import { shardIndex } from '../../shared/hash';
3
+ import { withWriteLock } from '../../shared/lock';
4
+ function compareOldestFirst(a, b) {
5
+ if (a.createdAt !== b.createdAt)
6
+ return a.createdAt - b.createdAt;
7
+ if (a.id === b.id)
8
+ return 0;
9
+ return a.id < b.id ? -1 : 1;
10
+ }
11
+ /** Promote one delayed job and persist the transition before resolving. */
12
+ export async function promoteJob(jobId, ctx) {
13
+ const location = ctx.jobIndex.get(jobId);
14
+ if (location?.type !== 'queue')
15
+ return false;
16
+ const promotedAt = Date.now();
17
+ const success = await withWriteLock(ctx.shardLocks[location.shardIdx], () => {
18
+ const shard = ctx.shards[location.shardIdx];
19
+ const queue = shard.getQueue(location.queueName);
20
+ const job = queue.find(jobId);
21
+ if (!job || job.runAt <= promotedAt || !queue.updateRunAt(jobId, promotedAt))
22
+ return false;
23
+ shard.promoteDelayed(jobId);
24
+ shard.notify(location.queueName);
25
+ return true;
26
+ });
27
+ if (success)
28
+ ctx.storage?.updateRunAt(jobId, promotedAt);
29
+ return success;
30
+ }
31
+ /** Promote the oldest delayed jobs from the live shard, never an eventual SQL listing. */
32
+ export async function promoteJobs(queueName, count, ctx) {
33
+ const idx = shardIndex(queueName);
34
+ const promotedAt = Date.now();
35
+ const promotedIds = await withWriteLock(ctx.shardLocks[idx], () => {
36
+ const shard = ctx.shards[idx];
37
+ const queue = shard.getQueue(queueName);
38
+ const delayed = queue
39
+ .values()
40
+ .filter((job) => job.runAt > promotedAt)
41
+ .sort(compareOldestFirst);
42
+ const limit = count === undefined ? delayed.length : Math.max(0, Math.floor(count));
43
+ const ids = [];
44
+ for (let i = 0; i < Math.min(limit, delayed.length); i++) {
45
+ const id = delayed[i].id;
46
+ if (!queue.updateRunAt(id, promotedAt))
47
+ continue;
48
+ shard.promoteDelayed(id);
49
+ ids.push(id);
50
+ }
51
+ if (ids.length > 0)
52
+ shard.notifyBatch(queueName, ids.length);
53
+ return ids;
54
+ });
55
+ for (const id of promotedIds)
56
+ ctx.storage?.updateRunAt(id, promotedAt);
57
+ return promotedIds.length;
58
+ }
@@ -237,6 +237,7 @@ export declare class QueueManager {
237
237
  updateJobData(jobId: JobId, data: unknown): Promise<boolean>;
238
238
  changePriority(jobId: JobId, priority: number, lifo?: boolean): Promise<boolean>;
239
239
  promote(jobId: JobId): Promise<boolean>;
240
+ promoteJobs(queue: string, count?: number): Promise<number>;
240
241
  moveToDelayed(jobId: JobId, delay: number): Promise<boolean>;
241
242
  changeDelay(jobId: JobId, delay: number): Promise<boolean>;
242
243
  moveActiveToWait(jobId: JobId): Promise<boolean>;
@@ -17,6 +17,7 @@ import { pullJob, pullJobBatch } from './operations/pull';
17
17
  import { ackJob, ackJobBatch, ackJobBatchWithResults, failJob } from './operations/ack';
18
18
  import * as queueControl from './operations/queueControl';
19
19
  import * as jobMgmt from './operations/jobManagement';
20
+ import * as jobPromotion from './operations/jobPromotion';
20
21
  import * as jobTransitions from './operations/jobStateTransitions';
21
22
  import * as queryOps from './operations/queryOperations';
22
23
  import * as dlqOps from './dlqManager';
@@ -975,7 +976,10 @@ export class QueueManager {
975
976
  return jobMgmt.changeJobPriority(jobId, priority, this.contextFactory.getJobMgmtContext(), lifo);
976
977
  }
977
978
  async promote(jobId) {
978
- return jobMgmt.promoteJob(jobId, this.contextFactory.getJobMgmtContext());
979
+ return jobPromotion.promoteJob(jobId, this.contextFactory.getJobMgmtContext());
980
+ }
981
+ async promoteJobs(queue, count) {
982
+ return jobPromotion.promoteJobs(queue, count, this.contextFactory.getJobMgmtContext());
979
983
  }
980
984
  async moveToDelayed(jobId, delay) {
981
985
  // moveToDelayed and changeDelay share identical routing in this engine: a job
@@ -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',
@@ -116,6 +116,8 @@ export declare class Shard {
116
116
  };
117
117
  incrementQueued(jobId: JobId, isDelayed: boolean, createdAt?: number, queue?: string, runAt?: number): void;
118
118
  decrementQueued(jobId: JobId): void;
119
+ /** Remove a promoted job from delayed tracking without changing queue depth. */
120
+ promoteDelayed(jobId: JobId): void;
119
121
  incrementDlq(): void;
120
122
  decrementDlq(count?: number): void;
121
123
  refreshDelayedCount(now: number): void;
@@ -302,6 +302,11 @@ export class Shard {
302
302
  decrementQueued(jobId) {
303
303
  this.counters.decrementQueued(jobId);
304
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
+ }
305
310
  incrementDlq() {
306
311
  this.counters.incrementDlq();
307
312
  }
@@ -281,14 +281,7 @@ export async function handleMoveToWait(cmd, ctx, reqId) {
281
281
  }
282
282
  /** Handle PromoteJobs command - promote all delayed jobs in a queue */
283
283
  export async function handlePromoteJobs(cmd, ctx, reqId) {
284
- const delayed = ctx.queueManager.getJobs(cmd.queue, { state: 'delayed' });
285
- const limit = cmd.count ?? delayed.length;
286
- let count = 0;
287
- for (let i = 0; i < Math.min(limit, delayed.length); i++) {
288
- const success = await ctx.queueManager.promote(delayed[i].id);
289
- if (success)
290
- count++;
291
- }
284
+ const count = await ctx.queueManager.promoteJobs(cmd.queue, cmd.count);
292
285
  return { ok: true, count, reqId };
293
286
  }
294
287
  // ============ Flow Dependency Commands ============
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunqueue",
3
- "version": "2.8.33",
3
+ "version": "2.8.34",
4
4
  "description": "High-performance job queue for Bun & AI agents. SQLite persistence, cron scheduling, priorities, retries, DLQ, webhooks, native MCP server. Zero external infrastructure.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -57,6 +57,7 @@
57
57
  "build": "bun build --compile --minify src/main.ts --outfile dist/bunqueue",
58
58
  "build:lib": "tsc -p tsconfig.build.json",
59
59
  "test": "BUNQUEUE_EMBEDDED=1 bun test",
60
+ "test:sandbox": "bun run scripts/test-sandbox.ts",
60
61
  "bench": "bun run bench/throughput.ts && bun run bench/latency.ts",
61
62
  "bench:throughput": "bun run bench/throughput.ts",
62
63
  "bench:latency": "bun run bench/latency.ts",