bunqueue 2.8.34 → 2.8.36

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 (63) hide show
  1. package/README.md +14 -2
  2. package/dist/application/backgroundTasks.js +37 -7
  3. package/dist/application/contextFactory.js +5 -0
  4. package/dist/application/dlqManager.d.ts +6 -0
  5. package/dist/application/dlqManager.js +35 -14
  6. package/dist/application/lockManager.js +22 -8
  7. package/dist/application/operations/customId.d.ts +31 -0
  8. package/dist/application/operations/customId.js +59 -0
  9. package/dist/application/operations/jobClaim.d.ts +17 -0
  10. package/dist/application/operations/jobClaim.js +20 -0
  11. package/dist/application/operations/jobManagement.d.ts +4 -5
  12. package/dist/application/operations/jobManagement.js +15 -116
  13. package/dist/application/operations/jobMoveOperations.d.ts +9 -0
  14. package/dist/application/operations/jobMoveOperations.js +94 -0
  15. package/dist/application/operations/jobStateTransitions.js +3 -0
  16. package/dist/application/operations/pull.js +5 -0
  17. package/dist/application/operations/push.js +8 -103
  18. package/dist/application/operations/pushInsert.d.ts +12 -0
  19. package/dist/application/operations/pushInsert.js +25 -0
  20. package/dist/application/operations/pushLocks.d.ts +14 -0
  21. package/dist/application/operations/pushLocks.js +43 -0
  22. package/dist/application/operations/queueControl.d.ts +1 -1
  23. package/dist/application/operations/queueControl.js +1 -1
  24. package/dist/application/queueManager.d.ts +2 -2
  25. package/dist/application/queueManager.js +30 -10
  26. package/dist/application/stallDetection.js +17 -8
  27. package/dist/client/queue/dlq.d.ts +23 -1
  28. package/dist/client/queue/dlq.js +75 -0
  29. package/dist/client/queue/helpers.js +2 -0
  30. package/dist/client/queue/jobProxy.d.ts +1 -1
  31. package/dist/client/queue/operations/control.d.ts +17 -0
  32. package/dist/client/queue/operations/control.js +41 -0
  33. package/dist/client/queue/operations/counts.d.ts +1 -0
  34. package/dist/client/queue/operations/counts.js +3 -0
  35. package/dist/client/queue/queue.d.ts +13 -0
  36. package/dist/client/queue/queue.js +39 -0
  37. package/dist/client/queue/rateLimit.d.ts +18 -3
  38. package/dist/client/queue/rateLimit.js +45 -10
  39. package/dist/client/queue/stall.d.ts +2 -0
  40. package/dist/client/queue/stall.js +12 -0
  41. package/dist/domain/queue/dependencyTracker.d.ts +5 -0
  42. package/dist/domain/queue/dependencyTracker.js +20 -0
  43. package/dist/domain/queue/limiterManager.d.ts +14 -2
  44. package/dist/domain/queue/limiterManager.js +32 -5
  45. package/dist/domain/queue/shard.d.ts +3 -2
  46. package/dist/domain/queue/shard.js +11 -2
  47. package/dist/domain/types/command.d.ts +4 -0
  48. package/dist/domain/types/queue.d.ts +4 -0
  49. package/dist/domain/types/queue.js +2 -0
  50. package/dist/infrastructure/persistence/schema.d.ts +2 -2
  51. package/dist/infrastructure/persistence/schema.js +38 -2
  52. package/dist/infrastructure/persistence/sqlite.d.ts +20 -1
  53. package/dist/infrastructure/persistence/sqlite.js +62 -6
  54. package/dist/infrastructure/persistence/sqliteBatch.js +8 -6
  55. package/dist/infrastructure/persistence/sqliteSerializer.d.ts +6 -0
  56. package/dist/infrastructure/persistence/sqliteSerializer.js +9 -1
  57. package/dist/infrastructure/persistence/statements.d.ts +8 -1
  58. package/dist/infrastructure/persistence/statements.js +7 -5
  59. package/dist/infrastructure/scheduler/cronScheduler.d.ts +5 -0
  60. package/dist/infrastructure/scheduler/cronScheduler.js +39 -13
  61. package/dist/infrastructure/server/handlers/advanced.js +11 -2
  62. package/dist/infrastructure/server/httpRouteQueueConfig.js +2 -0
  63. package/package.json +4 -1
@@ -283,6 +283,35 @@ export class CronScheduler {
283
283
  else {
284
284
  newNextRun = executionTime;
285
285
  }
286
+ // Skip decision comes BEFORE the executions increment: a skipped fire
287
+ // pushes no job, so it must not consume the maxLimit budget (a cron
288
+ // with maxLimit=N must deliver N jobs, not N-minus-skips — worst case
289
+ // skipIfNoWorker used to burn the whole budget with zero deliveries).
290
+ // The schedule still advances so the heap doesn't hot-loop on it.
291
+ const skipReason = this.getSkipReason(cron, now);
292
+ if (skipReason) {
293
+ if (this.persistCron) {
294
+ try {
295
+ this.persistCron(cron.name, cron.executions, newNextRun);
296
+ }
297
+ catch (persistErr) {
298
+ // Benign here: nothing was pushed, in-memory advance is enough;
299
+ // the next successful persist catches the row up.
300
+ cronLog.error('Failed to persist cron nextRun on skip', {
301
+ name: cron.name,
302
+ error: String(persistErr),
303
+ });
304
+ }
305
+ }
306
+ cron.nextRun = newNextRun;
307
+ this.dashboardEmit?.('cron:skipped', {
308
+ name: cron.name,
309
+ queue: cron.queue,
310
+ reason: skipReason,
311
+ });
312
+ toReinsert.push(entry);
313
+ continue;
314
+ }
286
315
  // PERSIST STATE FIRST (before pushing job to prevent duplicates)
287
316
  if (this.persistCron) {
288
317
  try {
@@ -339,27 +368,24 @@ export class CronScheduler {
339
368
  this.scheduleNext();
340
369
  }
341
370
  /** Fire a cron job with overlap and worker detection */
342
- async fireCronJob(cron, now) {
371
+ /**
372
+ * Reasons a due cron must not push right now. Evaluated BEFORE the
373
+ * executions increment in tick(), so skips never consume maxLimit budget.
374
+ */
375
+ getSkipReason(cron, now) {
343
376
  // skipIfNoWorker: skip if no workers registered for this queue
344
377
  if (cron.skipIfNoWorker && this.hasWorkers && !this.hasWorkers(cron.queue)) {
345
- this.dashboardEmit?.('cron:skipped', {
346
- name: cron.name,
347
- queue: cron.queue,
348
- reason: 'no-worker',
349
- });
350
- return;
378
+ return 'no-worker';
351
379
  }
352
380
  // Overlap detection: skip if last fire was within repeatEvery window
353
381
  const lastFire = this.lastFiredAt.get(cron.name);
354
382
  const interval = cron.repeatEvery ?? 60000;
355
383
  if (lastFire && now - lastFire < interval * 0.8) {
356
- this.dashboardEmit?.('cron:skipped', {
357
- name: cron.name,
358
- queue: cron.queue,
359
- reason: 'overlap',
360
- });
361
- return;
384
+ return 'overlap';
362
385
  }
386
+ return null;
387
+ }
388
+ async fireCronJob(cron, now) {
363
389
  // preventOverlap: auto-set uniqueKey to block pushes while previous job is active
364
390
  const effectiveUniqueKey = cron.uniqueKey ?? (cron.preventOverlap ? `cron:${cron.name}` : undefined);
365
391
  const opts = cron.jobOptions;
@@ -172,8 +172,17 @@ export function handleRateLimit(cmd, ctx, reqId) {
172
172
  const limit = toFiniteNumber(cmd.limit);
173
173
  if (limit === undefined)
174
174
  return resp.error('limit must be a finite number', reqId);
175
- ctx.queueManager.setRateLimit(cmd.queue, limit);
176
- ctx.queueManager.emitDashboardEvent('ratelimit:set', { queue: cmd.queue, max: limit });
175
+ // Optional window/TTL: invalid values degrade to the defaults (1s window,
176
+ // permanent limit) inside the limiter rather than failing the command.
177
+ const duration = toFiniteNumber(cmd.duration);
178
+ const ttl = toFiniteNumber(cmd.ttl);
179
+ ctx.queueManager.setRateLimit(cmd.queue, limit, duration, ttl);
180
+ ctx.queueManager.emitDashboardEvent('ratelimit:set', {
181
+ queue: cmd.queue,
182
+ max: limit,
183
+ ...(duration !== undefined && { duration }),
184
+ ...(ttl !== undefined && { ttl }),
185
+ });
177
186
  return resp.ok(undefined, reqId);
178
187
  }
179
188
  /** Handle RateLimitClear command */
@@ -76,6 +76,8 @@ export async function routeQueueConfigRoutes(req, path, method, ctx, cors) {
76
76
  cmd: 'RateLimit',
77
77
  queue,
78
78
  limit: body['limit'],
79
+ duration: body['duration'],
80
+ ttl: body['ttl'],
79
81
  }, ctx);
80
82
  return jsonResponse(r, 200, cors);
81
83
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunqueue",
3
- "version": "2.8.34",
3
+ "version": "2.8.36",
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,7 +57,9 @@
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:model": "BUNQUEUE_EMBEDDED=1 bun test test/model-based/queue-model.test.ts",
60
61
  "test:sandbox": "bun run scripts/test-sandbox.ts",
62
+ "test:sandbox:sdk": "bun run scripts/test-sdk-sandbox.ts",
61
63
  "bench": "bun run bench/throughput.ts && bun run bench/latency.ts",
62
64
  "bench:throughput": "bun run bench/throughput.ts",
63
65
  "bench:latency": "bun run bench/latency.ts",
@@ -82,6 +84,7 @@
82
84
  "@types/bun": "^1.3.9",
83
85
  "bullmq": "^5.79.3",
84
86
  "elysia": "^1.4.25",
87
+ "fast-check": "^4.9.0",
85
88
  "ioredis": "^5.11.1",
86
89
  "typescript": "^5.9.3",
87
90
  "zod": "^4.3.6"