bunqueue 2.8.31 → 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 (44) hide show
  1. package/README.md +29 -26
  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 +29 -1
  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/client/tcpPool.js +8 -1
  19. package/dist/domain/queue/shard.d.ts +3 -1
  20. package/dist/domain/queue/shard.js +15 -7
  21. package/dist/domain/queue/temporalIndex.d.ts +24 -0
  22. package/dist/domain/queue/temporalIndex.js +100 -0
  23. package/dist/domain/queue/temporalManager.d.ts +2 -11
  24. package/dist/domain/queue/temporalManager.js +27 -44
  25. package/dist/domain/queue/waiterManager.d.ts +11 -13
  26. package/dist/domain/queue/waiterManager.js +80 -49
  27. package/dist/infrastructure/cloud/commands.js +5 -2
  28. package/dist/infrastructure/cloud/snapshotCollector.js +5 -2
  29. package/dist/infrastructure/cloud/statsRefresh.js +5 -2
  30. package/dist/infrastructure/cloud/statsUpdate.js +5 -2
  31. package/dist/infrastructure/persistence/schema.d.ts +2 -2
  32. package/dist/infrastructure/persistence/schema.js +13 -1
  33. package/dist/infrastructure/persistence/sqlite.d.ts +18 -2
  34. package/dist/infrastructure/persistence/sqlite.js +49 -6
  35. package/dist/infrastructure/server/handlers/core.js +13 -7
  36. package/dist/infrastructure/server/handlers/pushBatchValidation.d.ts +23 -0
  37. package/dist/infrastructure/server/handlers/pushBatchValidation.js +70 -0
  38. package/dist/infrastructure/server/queueCountsScheduler.d.ts +13 -0
  39. package/dist/infrastructure/server/queueCountsScheduler.js +35 -0
  40. package/dist/infrastructure/server/sseHandler.d.ts +1 -0
  41. package/dist/infrastructure/server/sseHandler.js +18 -13
  42. package/dist/infrastructure/server/wsHandler.d.ts +1 -1
  43. package/dist/infrastructure/server/wsHandler.js +18 -15
  44. package/package.json +3 -3
@@ -0,0 +1,70 @@
1
+ /**
2
+ * PUSHB Validation
3
+ * Per-job validation for batch pushes with PUSH parity: the same
4
+ * validateJobData/validateJobOptions bounds and the same dependsOn
5
+ * existence gate handlePush enforces (core.ts).
6
+ */
7
+ import { validateJobData, validateJobOptions } from '../protocol';
8
+ /**
9
+ * Validate every job of a PUSHB batch. Returns an error message naming the
10
+ * offending index, or null when the whole batch is valid.
11
+ *
12
+ * The dependsOn gate is EXTENDED beyond the PUSH one: a dependency may also
13
+ * reference ANY OTHER job of the same batch via its custom id (the only ids
14
+ * a client can know before the batch is applied). Order within the batch is
15
+ * deliberately irrelevant: the default-on auto-batcher groups concurrent
16
+ * add() calls in arbitrary order, so a child can legitimately precede its
17
+ * parent in the array, and the readiness layer (waitingDeps) resolves the
18
+ * chain once the whole batch is applied. Self-references are rejected, they
19
+ * can never resolve. jobIndex/completedJobs/depCompletions are hoisted out
20
+ * of the loop because PUSHB is the bulk hot path.
21
+ */
22
+ export function validatePushBatchJobs(jobs, ctx) {
23
+ const jobIndex = ctx.queueManager.getJobIndex();
24
+ const completedJobs = ctx.queueManager.getCompletedJobs();
25
+ const depCompletions = ctx.queueManager.getDepCompletions();
26
+ // Custom ids of ALL jobs in this batch: they become real job ids the
27
+ // moment the batch is applied. Allocated lazily (most batches have neither
28
+ // custom ids nor dependencies).
29
+ let batchIds = null;
30
+ for (const job of jobs) {
31
+ if (job.customId) {
32
+ batchIds ??= new Set();
33
+ batchIds.add(job.customId);
34
+ }
35
+ }
36
+ for (let i = 0; i < jobs.length; i++) {
37
+ const job = jobs[i];
38
+ const dataError = validateJobData(job.data);
39
+ if (dataError)
40
+ return `jobs[${i}]: ${dataError}`;
41
+ const optionsError = validateJobOptions({
42
+ priority: job.priority,
43
+ delay: job.delay,
44
+ timeout: job.timeout,
45
+ maxAttempts: job.maxAttempts,
46
+ backoff: job.backoff,
47
+ ttl: job.ttl,
48
+ });
49
+ if (optionsError)
50
+ return `jobs[${i}]: ${optionsError}`;
51
+ if (job.dependsOn && job.dependsOn.length > 0) {
52
+ for (const depId of job.dependsOn) {
53
+ const key = String(depId);
54
+ if (key === job.customId) {
55
+ return `jobs[${i}]: Job cannot depend on itself: ${key}`;
56
+ }
57
+ const exists = jobIndex.has(depId) ||
58
+ completedJobs.has(depId) ||
59
+ // removeOnComplete parents leave only a bare completion id behind;
60
+ // the gate must honor it exactly like PUSH does.
61
+ depCompletions.has(depId) ||
62
+ (batchIds?.has(key) ?? false);
63
+ if (!exists) {
64
+ return `jobs[${i}]: Dependency job not found: ${key}`;
65
+ }
66
+ }
67
+ }
68
+ }
69
+ return null;
70
+ }
@@ -0,0 +1,13 @@
1
+ import type { QueueManager } from '../../application/queueManager';
2
+ import type { QueueJobCounts } from '../../application/queueStatsAggregator';
3
+ /** Coalesce high-volume job events into one queue-count aggregation per tick. */
4
+ export declare class QueueCountsScheduler {
5
+ private readonly queueManager;
6
+ private readonly emit;
7
+ private readonly pendingQueues;
8
+ private timer;
9
+ constructor(queueManager: QueueManager, emit: (queue: string, counts: QueueJobCounts) => void);
10
+ schedule(queue: string): void;
11
+ stop(): void;
12
+ private flush;
13
+ }
@@ -0,0 +1,35 @@
1
+ const COUNTS_COALESCE_MS = 10;
2
+ /** Coalesce high-volume job events into one queue-count aggregation per tick. */
3
+ export class QueueCountsScheduler {
4
+ queueManager;
5
+ emit;
6
+ pendingQueues = new Set();
7
+ timer = null;
8
+ constructor(queueManager, emit) {
9
+ this.queueManager = queueManager;
10
+ this.emit = emit;
11
+ }
12
+ schedule(queue) {
13
+ this.pendingQueues.add(queue);
14
+ this.timer ??= setTimeout(() => this.flush(), COUNTS_COALESCE_MS);
15
+ }
16
+ stop() {
17
+ if (this.timer)
18
+ clearTimeout(this.timer);
19
+ this.timer = null;
20
+ this.pendingQueues.clear();
21
+ }
22
+ flush() {
23
+ this.timer = null;
24
+ if (this.pendingQueues.size === 0)
25
+ return;
26
+ const queues = Array.from(this.pendingQueues);
27
+ this.pendingQueues.clear();
28
+ const countsByQueue = this.queueManager.getQueueJobCountsBatch(queues);
29
+ for (const queue of queues) {
30
+ const counts = countsByQueue.get(queue);
31
+ if (counts)
32
+ this.emit(queue, counts);
33
+ }
34
+ }
35
+ }
@@ -30,6 +30,7 @@ export declare class SseHandler {
30
30
  private healthInterval;
31
31
  private storageInterval;
32
32
  private queueManager;
33
+ private queueCountsScheduler;
33
34
  private readonly eventBuffer;
34
35
  /** Get client count */
35
36
  get size(): number;
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { throughputTracker } from '../../application/throughputTracker';
15
15
  import { uuid } from '../../shared/hash';
16
+ import { QueueCountsScheduler } from './queueCountsScheduler';
16
17
  const textEncoder = new TextEncoder();
17
18
  /** Tuning constants */
18
19
  const MAX_CLIENTS = 1000;
@@ -45,6 +46,7 @@ export class SseHandler {
45
46
  healthInterval = null;
46
47
  storageInterval = null;
47
48
  queueManager = null;
49
+ queueCountsScheduler = null;
48
50
  eventBuffer = [];
49
51
  /** Get client count */
50
52
  get size() {
@@ -54,6 +56,17 @@ export class SseHandler {
54
56
  /** Start heartbeat + periodic broadcasts (stats, health, storage) */
55
57
  startBroadcasts(qm) {
56
58
  this.queueManager = qm;
59
+ this.queueCountsScheduler ??= new QueueCountsScheduler(qm, (queue, counts) => {
60
+ this.sendTypedEvent('queue:counts', {
61
+ queue,
62
+ waiting: counts.waiting,
63
+ prioritized: counts.prioritized,
64
+ active: counts.active,
65
+ completed: counts.completed,
66
+ failed: counts.failed,
67
+ delayed: counts.delayed,
68
+ });
69
+ });
57
70
  this.heartbeatTimer ??= setInterval(() => {
58
71
  this.sendHeartbeat();
59
72
  }, HEARTBEAT_MS);
@@ -78,6 +91,8 @@ export class SseHandler {
78
91
  }
79
92
  /** Stop all timers */
80
93
  stopTimers() {
94
+ this.queueCountsScheduler?.stop();
95
+ this.queueCountsScheduler = null;
81
96
  if (this.heartbeatTimer) {
82
97
  clearInterval(this.heartbeatTimer);
83
98
  this.heartbeatTimer = null;
@@ -154,19 +169,9 @@ export class SseHandler {
154
169
  }
155
170
  for (const clientId of disconnected)
156
171
  this.clients.delete(clientId);
157
- // Emit queue:counts on every job state change (mirrors wsHandler)
158
- if (this.queueManager) {
159
- const counts = this.queueManager.getQueueJobCounts(event.queue);
160
- this.sendTypedEvent('queue:counts', {
161
- queue: event.queue,
162
- waiting: counts.waiting,
163
- prioritized: counts.prioritized,
164
- active: counts.active,
165
- completed: counts.completed,
166
- failed: counts.failed,
167
- delayed: counts.delayed,
168
- });
169
- }
172
+ // Counts are eventually exact, but coalesced so a PUSHB does not perform
173
+ // one full queue scan per inserted job.
174
+ this.queueCountsScheduler?.schedule(event.queue);
170
175
  }
171
176
  // ── Dashboard / system event broadcasting ──────────────────
172
177
  /** Emit a typed event (dashboard events: worker, queue control, DLQ, etc.) */
@@ -28,7 +28,7 @@ export declare class WsHandler {
28
28
  private statsInterval;
29
29
  private healthInterval;
30
30
  private storageInterval;
31
- private queueManager;
31
+ private queueCountsScheduler;
32
32
  droppedMessages: number;
33
33
  get size(): number;
34
34
  /** Check if a new connection can be accepted */
@@ -14,6 +14,7 @@
14
14
  import { handleCommand } from './handler';
15
15
  import { parseCommand, serializeResponse, errorResponse } from './protocol';
16
16
  import { throughputTracker } from '../../application/throughputTracker';
17
+ import { QueueCountsScheduler } from './queueCountsScheduler';
17
18
  const textDecoder = new TextDecoder();
18
19
  /** Old eventType → new event name */
19
20
  const EVENT_MAP = {
@@ -170,7 +171,7 @@ export class WsHandler {
170
171
  statsInterval = null;
171
172
  healthInterval = null;
172
173
  storageInterval = null;
173
- queueManager = null;
174
+ queueCountsScheduler = null;
174
175
  droppedMessages = 0;
175
176
  get size() {
176
177
  return this.clients.size;
@@ -196,7 +197,17 @@ export class WsHandler {
196
197
  }
197
198
  /** Start periodic broadcasts */
198
199
  startBroadcasts(qm) {
199
- this.queueManager = qm;
200
+ this.queueCountsScheduler ??= new QueueCountsScheduler(qm, (queue, counts) => {
201
+ this.emit('queue:counts', {
202
+ queue,
203
+ waiting: counts.waiting,
204
+ prioritized: counts.prioritized,
205
+ active: counts.active,
206
+ completed: counts.completed,
207
+ failed: counts.failed,
208
+ delayed: counts.delayed,
209
+ });
210
+ });
200
211
  this.statsInterval ??= setInterval(() => {
201
212
  if (this.clients.size > 0)
202
213
  this.broadcastStats(qm);
@@ -212,6 +223,8 @@ export class WsHandler {
212
223
  }
213
224
  /** Stop periodic broadcasts */
214
225
  stopBroadcasts() {
226
+ this.queueCountsScheduler?.stop();
227
+ this.queueCountsScheduler = null;
215
228
  if (this.statsInterval) {
216
229
  clearInterval(this.statsInterval);
217
230
  this.statsInterval = null;
@@ -338,19 +351,9 @@ export class WsHandler {
338
351
  }
339
352
  for (const id of dead)
340
353
  this.clients.delete(id);
341
- // Emit queue:counts on every job state change
342
- if (this.queueManager) {
343
- const counts = this.queueManager.getQueueJobCounts(event.queue);
344
- this.emit('queue:counts', {
345
- queue: event.queue,
346
- waiting: counts.waiting,
347
- prioritized: counts.prioritized,
348
- active: counts.active,
349
- completed: counts.completed,
350
- failed: counts.failed,
351
- delayed: counts.delayed,
352
- });
353
- }
354
+ // Counts are eventually exact, but coalesced so a PUSHB does not perform
355
+ // one full queue scan per inserted job.
356
+ this.queueCountsScheduler?.schedule(event.queue);
354
357
  }
355
358
  // ── Connection lifecycle ─────────────────────────────────
356
359
  onOpen(ws) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bunqueue",
3
- "version": "2.8.31",
4
- "description": "High-performance job queue for Bun & AI agents. SQLite persistence, cron scheduling, priorities, retries, DLQ, webhooks, native MCP server. Zero external dependencies.",
3
+ "version": "2.8.33",
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",
7
7
  "types": "dist/main.d.ts",
@@ -146,4 +146,4 @@
146
146
  "engines": {
147
147
  "bun": ">=1.3.9"
148
148
  }
149
- }
149
+ }