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.
- package/README.md +29 -26
- package/dist/application/backgroundTasks.js +17 -9
- package/dist/application/dependencyProcessor.js +6 -1
- package/dist/application/dlqManager.js +1 -1
- package/dist/application/lockManager.js +1 -1
- package/dist/application/operations/ack.js +23 -1
- package/dist/application/operations/jobManagement.js +29 -1
- package/dist/application/operations/jobStateTransitions.js +1 -1
- package/dist/application/operations/pull.js +125 -96
- package/dist/application/operations/push.js +2 -2
- package/dist/application/operations/queryOperations.js +74 -70
- package/dist/application/queueManager.d.ts +6 -11
- package/dist/application/queueManager.js +16 -3
- package/dist/application/queueStatsAggregator.d.ts +23 -0
- package/dist/application/queueStatsAggregator.js +80 -0
- package/dist/application/statsManager.d.ts +2 -26
- package/dist/application/statsManager.js +8 -124
- package/dist/client/tcpPool.js +8 -1
- package/dist/domain/queue/shard.d.ts +3 -1
- package/dist/domain/queue/shard.js +15 -7
- package/dist/domain/queue/temporalIndex.d.ts +24 -0
- package/dist/domain/queue/temporalIndex.js +100 -0
- package/dist/domain/queue/temporalManager.d.ts +2 -11
- package/dist/domain/queue/temporalManager.js +27 -44
- package/dist/domain/queue/waiterManager.d.ts +11 -13
- package/dist/domain/queue/waiterManager.js +80 -49
- package/dist/infrastructure/cloud/commands.js +5 -2
- package/dist/infrastructure/cloud/snapshotCollector.js +5 -2
- package/dist/infrastructure/cloud/statsRefresh.js +5 -2
- package/dist/infrastructure/cloud/statsUpdate.js +5 -2
- package/dist/infrastructure/persistence/schema.d.ts +2 -2
- package/dist/infrastructure/persistence/schema.js +13 -1
- package/dist/infrastructure/persistence/sqlite.d.ts +18 -2
- package/dist/infrastructure/persistence/sqlite.js +49 -6
- package/dist/infrastructure/server/handlers/core.js +13 -7
- package/dist/infrastructure/server/handlers/pushBatchValidation.d.ts +23 -0
- package/dist/infrastructure/server/handlers/pushBatchValidation.js +70 -0
- package/dist/infrastructure/server/queueCountsScheduler.d.ts +13 -0
- package/dist/infrastructure/server/queueCountsScheduler.js +35 -0
- package/dist/infrastructure/server/sseHandler.d.ts +1 -0
- package/dist/infrastructure/server/sseHandler.js +18 -13
- package/dist/infrastructure/server/wsHandler.d.ts +1 -1
- package/dist/infrastructure/server/wsHandler.js +18 -15
- 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
|
+
}
|
|
@@ -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
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
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.
|
|
4
|
-
"description": "High-performance job queue for Bun & AI agents. SQLite persistence, cron scheduling, priorities, retries, DLQ, webhooks, native MCP server. Zero external
|
|
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
|
+
}
|