bunqueue 2.8.3 → 2.8.4
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 -0
- package/dist/application/operations/queryOperations.js +91 -25
- package/dist/bun-only.d.ts +15 -0
- package/dist/bun-only.js +19 -0
- package/dist/cli/index.js +0 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +2 -0
- package/dist/client/queue/operations/counts.js +7 -3
- package/dist/client/workflow/index.d.ts +1 -0
- package/dist/client/workflow/index.js +2 -0
- package/dist/infrastructure/cloud/snapshotHelpers.js +6 -1
- package/dist/infrastructure/persistence/sqlite.js +13 -1
- package/dist/infrastructure/server/handlers/dashboard.js +17 -1
- package/dist/infrastructure/server/handlers/query.js +7 -3
- package/dist/infrastructure/server/httpEndpoints.js +15 -1
- package/dist/mcp/index.js +0 -0
- package/dist/require-bun.d.ts +13 -0
- package/dist/require-bun.js +17 -0
- package/dist/shared/pausedView.d.ts +19 -0
- package/dist/shared/pausedView.js +7 -0
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -25,6 +25,14 @@
|
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
bunqueue is **Bun-only** (`bun >= 1.3.9`). The client, TCP transport and persistence
|
|
31
|
+
rely on Bun's runtime APIs (`Bun.connect`, `Bun.file`, `Bun.hash`, …) and ship as
|
|
32
|
+
ESM with extensionless specifiers, so they do **not** run under Node.js. Importing
|
|
33
|
+
the package from Node fails fast with a clear error pointing here rather than a
|
|
34
|
+
cryptic resolver crash. Install Bun from [bun.sh](https://bun.sh) and run with `bun`.
|
|
35
|
+
|
|
28
36
|
## Quickstart
|
|
29
37
|
|
|
30
38
|
```bash
|
|
@@ -453,6 +461,27 @@ await app.close(); // graceful shutdown
|
|
|
453
461
|
await app.close(true); // force shutdown
|
|
454
462
|
```
|
|
455
463
|
|
|
464
|
+
### Inspecting jobs & counts
|
|
465
|
+
|
|
466
|
+
`getJobCounts()` and the per-state lists follow BullMQ semantics:
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
const counts = await queue.getJobCountsAsync();
|
|
470
|
+
// { waiting, prioritized, active, completed, failed, delayed, paused }
|
|
471
|
+
|
|
472
|
+
// Failed jobs (those that exhausted their attempts) are enumerable by state —
|
|
473
|
+
// the failed list reflects the same jobs that `failed` counts.
|
|
474
|
+
const failed = await queue.getFailedAsync(0, 50);
|
|
475
|
+
const alsoFailed = await queue.getJobsAsync({ state: 'failed', start: 0, end: 50 });
|
|
476
|
+
|
|
477
|
+
// Paused queue: ready jobs are reported under `paused`, never double-counted as
|
|
478
|
+
// `waiting`. While paused, getJobCounts() returns waiting:0 / paused:N, and the
|
|
479
|
+
// jobs are listed by `getJobsAsync({ state: 'paused' })` (not by `waiting`).
|
|
480
|
+
queue.pause();
|
|
481
|
+
const c = await queue.getJobCountsAsync(); // waiting: 0, paused: N
|
|
482
|
+
const pausedJobs = await queue.getJobsAsync({ state: 'paused', start: 0, end: 50 });
|
|
483
|
+
```
|
|
484
|
+
|
|
456
485
|
### Full Example
|
|
457
486
|
|
|
458
487
|
```typescript
|
|
@@ -250,28 +250,55 @@ function collectWaitingChildrenFromShard(shard, queue, max) {
|
|
|
250
250
|
}
|
|
251
251
|
return wcJobs;
|
|
252
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Resolve which sources to collect for a state filter.
|
|
255
|
+
*
|
|
256
|
+
* When the queue is paused, an EXPLICIT waiting/prioritized query returns nothing:
|
|
257
|
+
* those jobs are reported under 'paused' instead (BullMQ semantics, #92). An
|
|
258
|
+
* unfiltered query (states === null) still lists them by their temporal state.
|
|
259
|
+
*/
|
|
260
|
+
function resolveStateNeeds(states, paused) {
|
|
261
|
+
const want = (s) => !states || states.includes(s);
|
|
262
|
+
const suppressReady = !!states && paused;
|
|
263
|
+
return {
|
|
264
|
+
waiting: want('waiting') && !suppressReady,
|
|
265
|
+
prioritized: want('prioritized') && !suppressReady,
|
|
266
|
+
delayed: want('delayed'),
|
|
267
|
+
paused: !!states && states.includes('paused') && paused,
|
|
268
|
+
active: want('active'),
|
|
269
|
+
failed: want('failed'),
|
|
270
|
+
completed: want('completed'),
|
|
271
|
+
waitingChildren: want('waiting-children'),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/** When paused, the queue's ready jobs (waiting + prioritized) ARE the paused set (#92). */
|
|
275
|
+
function collectPausedJobs(shard, queue, maxPerSource) {
|
|
276
|
+
const pausedJobs = collectTemporalJobs(shard, queue, { waiting: true, prioritized: true, delayed: false }, maxPerSource);
|
|
277
|
+
return tagState(pausedJobs, 'paused');
|
|
278
|
+
}
|
|
253
279
|
function collectJobsByState(queue, shardIdx, states, ctx, maxPerSource = Infinity) {
|
|
254
280
|
const shard = ctx.shards[shardIdx];
|
|
255
281
|
const jobs = [];
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (needWaiting || needPrioritized || needDelayed) {
|
|
260
|
-
const temporal = collectTemporalJobs(shard, queue, { waiting: needWaiting, prioritized: needPrioritized, delayed: needDelayed }, maxPerSource);
|
|
282
|
+
const need = resolveStateNeeds(states, shard.getState(queue).paused);
|
|
283
|
+
if (need.waiting || need.prioritized || need.delayed) {
|
|
284
|
+
const temporal = collectTemporalJobs(shard, queue, { waiting: need.waiting, prioritized: need.prioritized, delayed: need.delayed }, maxPerSource);
|
|
261
285
|
tagTemporalState(temporal);
|
|
262
286
|
jobs.push(...temporal);
|
|
263
287
|
}
|
|
264
|
-
if (
|
|
288
|
+
if (need.paused) {
|
|
289
|
+
jobs.push(...collectPausedJobs(shard, queue, maxPerSource));
|
|
290
|
+
}
|
|
291
|
+
if (need.active) {
|
|
265
292
|
jobs.push(...tagState(collectActiveJobs(queue, shardIdx, ctx, maxPerSource), 'active'));
|
|
266
293
|
}
|
|
267
|
-
if (
|
|
294
|
+
if (need.failed) {
|
|
268
295
|
const dlq = shard.getDlq(queue);
|
|
269
296
|
jobs.push(...tagState(maxPerSource < dlq.length ? dlq.slice(0, maxPerSource) : dlq, 'failed'));
|
|
270
297
|
}
|
|
271
|
-
if (
|
|
298
|
+
if (need.completed) {
|
|
272
299
|
jobs.push(...tagState(collectCompletedJobs(queue, ctx, maxPerSource), 'completed'));
|
|
273
300
|
}
|
|
274
|
-
if (
|
|
301
|
+
if (need.waitingChildren) {
|
|
275
302
|
jobs.push(...tagState(collectWaitingChildrenFromShard(shard, queue, maxPerSource), 'waiting-children'));
|
|
276
303
|
}
|
|
277
304
|
return jobs;
|
|
@@ -315,6 +342,14 @@ function querySqliteWithPriority(storage, queue, sqlFilteredStates, opts) {
|
|
|
315
342
|
}
|
|
316
343
|
return jobs.slice(0, opts.limit);
|
|
317
344
|
}
|
|
345
|
+
/** Merge SQL rows with in-memory extras (each gathered from index 0), sort by
|
|
346
|
+
* createdAt, and paginate [start, end) once — so offset-unaware extras don't
|
|
347
|
+
* duplicate or drop rows across pages (#92). */
|
|
348
|
+
function mergePage(sqlJobs, extras, start, end, asc) {
|
|
349
|
+
const merged = sqlJobs.concat(extras);
|
|
350
|
+
merged.sort((a, b) => (asc ? a.createdAt - b.createdAt : b.createdAt - a.createdAt));
|
|
351
|
+
return merged.slice(start, end);
|
|
352
|
+
}
|
|
318
353
|
/** Get jobs from queue with filters */
|
|
319
354
|
export function getJobs(queue, shardIdx, options, ctx) {
|
|
320
355
|
const { state, start = 0, end = 100, asc = true } = options;
|
|
@@ -327,29 +362,60 @@ export function getJobs(queue, shardIdx, options, ctx) {
|
|
|
327
362
|
: [state];
|
|
328
363
|
const limit = end - start;
|
|
329
364
|
if (ctx.storage) {
|
|
365
|
+
const shard = ctx.shards[shardIdx];
|
|
366
|
+
const isPaused = shard.getState(queue).paused;
|
|
367
|
+
const maxPerSource = end + 1;
|
|
368
|
+
// Derived sources are NOT offset-aware (the DLQ and the paused/waiting-children
|
|
369
|
+
// views come from in-memory maps). When any contributes, we must gather [0, end)
|
|
370
|
+
// from EVERY source, merge, sort, then slice [start, end) exactly once — pushing
|
|
371
|
+
// `offset` into the SQL query would drop rows and duplicate across pages (#92).
|
|
330
372
|
if (!states) {
|
|
331
|
-
|
|
373
|
+
const dlq = tagState(shard.getDlq(queue), 'failed');
|
|
374
|
+
if (dlq.length === 0) {
|
|
375
|
+
return ctx.storage.queryJobs(queue, { limit, offset: start, asc });
|
|
376
|
+
}
|
|
377
|
+
const all = ctx.storage.queryJobs(queue, { limit: end, offset: 0, asc });
|
|
378
|
+
return mergePage(all, dlq, start, end, asc);
|
|
379
|
+
}
|
|
380
|
+
// States with no jobs-table row are collected from in-memory sources:
|
|
381
|
+
// - 'failed' -> DLQ (the failed job lives in the dlq table) (#92)
|
|
382
|
+
// - 'waiting-children' -> deps/children maps
|
|
383
|
+
// - 'paused' -> when paused, the would-be-waiting jobs ARE the paused set (#92)
|
|
384
|
+
const extras = [];
|
|
385
|
+
if (states.includes('failed')) {
|
|
386
|
+
extras.push(...tagState(shard.getDlq(queue), 'failed'));
|
|
387
|
+
}
|
|
388
|
+
if (states.includes('waiting-children')) {
|
|
389
|
+
extras.push(...collectWaitingChildrenJobs(shard, queue));
|
|
332
390
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
if (hasWaitingChildren && sqlFilteredStates.length === 0) {
|
|
337
|
-
const jobs = collectWaitingChildrenJobs(ctx.shards[shardIdx], queue);
|
|
338
|
-
jobs.sort((a, b) => (asc ? a.createdAt - b.createdAt : b.createdAt - a.createdAt));
|
|
339
|
-
return jobs.slice(start, end);
|
|
391
|
+
if (states.includes('paused') && isPaused) {
|
|
392
|
+
const pausedJobs = collectTemporalJobs(shard, queue, { waiting: true, prioritized: true, delayed: false }, maxPerSource);
|
|
393
|
+
extras.push(...tagState(pausedJobs, 'paused'));
|
|
340
394
|
}
|
|
341
|
-
|
|
395
|
+
// jobs-table states. A paused queue reports its waiting/prioritized jobs under
|
|
396
|
+
// 'paused', so they must not also surface in the waiting/prioritized lists (#92).
|
|
397
|
+
const sqlFilteredStates = states.filter((s) => s !== 'failed' &&
|
|
398
|
+
s !== 'waiting-children' &&
|
|
399
|
+
s !== 'paused' &&
|
|
400
|
+
!(isPaused && (s === 'waiting' || s === 'prioritized')));
|
|
401
|
+
// Fast path: only jobs-table states, no derived sources — SQL paginates directly.
|
|
402
|
+
if (extras.length === 0) {
|
|
403
|
+
return sqlFilteredStates.length > 0
|
|
404
|
+
? querySqliteWithPriority(ctx.storage, queue, sqlFilteredStates, {
|
|
405
|
+
limit,
|
|
406
|
+
offset: start,
|
|
407
|
+
asc,
|
|
408
|
+
})
|
|
409
|
+
: [];
|
|
410
|
+
}
|
|
411
|
+
const sqlJobs = sqlFilteredStates.length > 0
|
|
342
412
|
? querySqliteWithPriority(ctx.storage, queue, sqlFilteredStates, {
|
|
343
|
-
limit,
|
|
344
|
-
offset:
|
|
413
|
+
limit: end,
|
|
414
|
+
offset: 0,
|
|
345
415
|
asc,
|
|
346
416
|
})
|
|
347
417
|
: [];
|
|
348
|
-
|
|
349
|
-
return jobs;
|
|
350
|
-
const merged = jobs.concat(collectWaitingChildrenJobs(ctx.shards[shardIdx], queue));
|
|
351
|
-
merged.sort((a, b) => (asc ? a.createdAt - b.createdAt : b.createdAt - a.createdAt));
|
|
352
|
-
return merged.slice(0, limit);
|
|
418
|
+
return mergePage(sqlJobs, extras, start, end, asc);
|
|
353
419
|
}
|
|
354
420
|
// In-memory path (embedded mode only)
|
|
355
421
|
const maxPerSource = end + 1;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun-only entry stub.
|
|
3
|
+
*
|
|
4
|
+
* bunqueue is built for the Bun runtime: its TCP transport, persistence and
|
|
5
|
+
* client rely on Bun globals (Bun.connect, Bun.file, Bun.write, Bun.hash, ...)
|
|
6
|
+
* with no Node fallback, and the published ESM uses directory/extensionless
|
|
7
|
+
* relative specifiers that Node's ESM resolver rejects (ERR_UNSUPPORTED_DIR_IMPORT).
|
|
8
|
+
*
|
|
9
|
+
* The package.json `exports` map points the `"node"` condition at this single,
|
|
10
|
+
* self-contained file so a Node import fails fast with a clear, actionable error
|
|
11
|
+
* instead of a cryptic resolver crash or a deep `Bun is not defined`. Bun resolves
|
|
12
|
+
* the real entry via the higher-priority `"bun"` condition and never loads this.
|
|
13
|
+
*
|
|
14
|
+
* Keep this file free of relative imports so it stays resolvable on its own.
|
|
15
|
+
*/
|
package/dist/bun-only.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Bun-only entry stub.
|
|
4
|
+
*
|
|
5
|
+
* bunqueue is built for the Bun runtime: its TCP transport, persistence and
|
|
6
|
+
* client rely on Bun globals (Bun.connect, Bun.file, Bun.write, Bun.hash, ...)
|
|
7
|
+
* with no Node fallback, and the published ESM uses directory/extensionless
|
|
8
|
+
* relative specifiers that Node's ESM resolver rejects (ERR_UNSUPPORTED_DIR_IMPORT).
|
|
9
|
+
*
|
|
10
|
+
* The package.json `exports` map points the `"node"` condition at this single,
|
|
11
|
+
* self-contained file so a Node import fails fast with a clear, actionable error
|
|
12
|
+
* instead of a cryptic resolver crash or a deep `Bun is not defined`. Bun resolves
|
|
13
|
+
* the real entry via the higher-priority `"bun"` condition and never loads this.
|
|
14
|
+
*
|
|
15
|
+
* Keep this file free of relative imports so it stays resolvable on its own.
|
|
16
|
+
*/
|
|
17
|
+
throw new Error('bunqueue is Bun-only and requires the Bun runtime (https://bun.sh). ' +
|
|
18
|
+
'Node.js is not supported: install Bun and run your program with `bun`. ' +
|
|
19
|
+
'See https://github.com/egeominotti/bunqueue#requirements');
|
package/dist/cli/index.js
CHANGED
|
File without changes
|
package/dist/client/index.d.ts
CHANGED
package/dist/client/index.js
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* worker.on('progress', (job, progress) => console.log(progress));
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
|
+
// Bun-only runtime guard — must evaluate before any module touching Bun.* globals.
|
|
22
|
+
import '../require-bun';
|
|
21
23
|
export { defineConfig } from '../config';
|
|
22
24
|
export { Queue } from './queue';
|
|
23
25
|
export { Worker } from './worker';
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* getJobCounts, getWaitingCount, getDelayedCount, etc.
|
|
5
5
|
*/
|
|
6
6
|
import { getSharedManager } from '../../manager';
|
|
7
|
+
import { pausedView } from '../../../shared/pausedView';
|
|
7
8
|
/** Get job counts (sync, embedded only) */
|
|
8
9
|
export function getJobCounts(ctx) {
|
|
9
10
|
if (!ctx.embedded) {
|
|
@@ -21,14 +22,17 @@ export function getJobCounts(ctx) {
|
|
|
21
22
|
// Use queue-specific counts
|
|
22
23
|
const counts = manager.getQueueJobCounts(ctx.name);
|
|
23
24
|
const isPaused = manager.isPaused(ctx.name);
|
|
25
|
+
// When paused, ready jobs (waiting + prioritized) are reported under `paused` —
|
|
26
|
+
// not in their own buckets, which would double-count (#92, BullMQ semantics).
|
|
27
|
+
const pv = pausedView(counts.waiting, counts.prioritized, isPaused);
|
|
24
28
|
return {
|
|
25
|
-
waiting:
|
|
26
|
-
prioritized:
|
|
29
|
+
waiting: pv.waiting,
|
|
30
|
+
prioritized: pv.prioritized,
|
|
27
31
|
active: counts.active,
|
|
28
32
|
completed: counts.completed,
|
|
29
33
|
failed: counts.failed,
|
|
30
34
|
delayed: counts.delayed,
|
|
31
|
-
paused:
|
|
35
|
+
paused: pv.paused,
|
|
32
36
|
};
|
|
33
37
|
}
|
|
34
38
|
/** Get job counts (async, works with TCP) */
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* const run = await engine.start('onboarding', { email: 'user@test.com' });
|
|
17
17
|
* ```
|
|
18
18
|
*/
|
|
19
|
+
// Bun-only runtime guard — must evaluate before any module touching Bun.* globals.
|
|
20
|
+
import '../../require-bun';
|
|
19
21
|
export { Workflow } from './workflow';
|
|
20
22
|
export { Engine } from './engine';
|
|
21
23
|
export { WorkflowEmitter } from './emitter';
|
|
@@ -9,7 +9,11 @@ const prevQueueTotals = new Map();
|
|
|
9
9
|
/** Per-queue previous waiting count for backlog velocity */
|
|
10
10
|
const prevQueueWaiting = new Map();
|
|
11
11
|
// ─── Heavy data collectors (called every ~90s) ───
|
|
12
|
-
/** All job states (BullMQ v5 compatible)
|
|
12
|
+
/** All job states (BullMQ v5 compatible).
|
|
13
|
+
* 'paused' yields jobs only when the queue is actually paused (where 'waiting' /
|
|
14
|
+
* 'prioritized' are suppressed), so a paused queue's ready jobs still appear in
|
|
15
|
+
* the snapshot — under `paused` — instead of vanishing (#92). No double-collect:
|
|
16
|
+
* a job is returned by exactly one of waiting/prioritized/paused. */
|
|
13
17
|
const ALL_STATES = [
|
|
14
18
|
'waiting',
|
|
15
19
|
'active',
|
|
@@ -17,6 +21,7 @@ const ALL_STATES = [
|
|
|
17
21
|
'failed',
|
|
18
22
|
'completed',
|
|
19
23
|
'prioritized',
|
|
24
|
+
'paused',
|
|
20
25
|
'waiting-children',
|
|
21
26
|
];
|
|
22
27
|
/** Convert falsy/empty values to undefined for compact serialization */
|
|
@@ -34,7 +34,19 @@ export class SqliteStorage {
|
|
|
34
34
|
static MAX_RETAINED_LOSSES = 100;
|
|
35
35
|
constructor(config) {
|
|
36
36
|
this.db = new Database(config.path, { create: true });
|
|
37
|
-
|
|
37
|
+
// PRAGMA settings are performance/optimization hints, not correctness
|
|
38
|
+
// requirements. A transient filesystem IOERR (e.g. SQLITE_IOERR_FSTAT from
|
|
39
|
+
// `mmap_size` calling fstat() on the fd during a restart/cleanup race) must
|
|
40
|
+
// NOT propagate out of the constructor — in a deferred/async context Bun
|
|
41
|
+
// reports it as an "Unhandled error between tests" and tears down CI.
|
|
42
|
+
try {
|
|
43
|
+
this.db.run(PRAGMA_SETTINGS);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
storageLog.error('Failed to apply PRAGMA settings', {
|
|
47
|
+
error: err instanceof Error ? err.message : String(err),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
38
50
|
this.migrate();
|
|
39
51
|
this.statements = prepareStatements(this.db);
|
|
40
52
|
this._onCriticalLoss = config.onCriticalLoss;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import * as resp from '../../../domain/types/response';
|
|
7
7
|
import { throughputTracker } from '../../../application/throughputTracker';
|
|
8
8
|
import { latencyTracker } from '../../../application/latencyTracker';
|
|
9
|
+
import { pausedView } from '../../../shared/pausedView';
|
|
9
10
|
/** Dashboard overview - single call for all dashboard data */
|
|
10
11
|
export function handleDashboardOverview(_cmd, ctx, reqId) {
|
|
11
12
|
const stats = ctx.queueManager.getStats();
|
|
@@ -92,8 +93,17 @@ export function handleDashboardQueues(_cmd, ctx, reqId) {
|
|
|
92
93
|
/** Dashboard single queue detail */
|
|
93
94
|
export function handleDashboardQueue(cmd, ctx, reqId) {
|
|
94
95
|
const queue = cmd.queue;
|
|
95
|
-
const
|
|
96
|
+
const rawCounts = ctx.queueManager.getQueueJobCounts(queue);
|
|
96
97
|
const paused = ctx.queueManager.isPaused(queue);
|
|
98
|
+
// Keep the counts consistent with the per-state lists below (#92): a paused
|
|
99
|
+
// queue reports ready jobs under `paused`, not `waiting`/`prioritized`.
|
|
100
|
+
const pv = pausedView(rawCounts.waiting, rawCounts.prioritized, paused);
|
|
101
|
+
const counts = {
|
|
102
|
+
...rawCounts,
|
|
103
|
+
waiting: pv.waiting,
|
|
104
|
+
prioritized: pv.prioritized,
|
|
105
|
+
paused: pv.paused,
|
|
106
|
+
};
|
|
97
107
|
const dlqJobs = ctx.queueManager.getDlq(queue, 10);
|
|
98
108
|
const priorityCounts = ctx.queueManager.getCountsPerPriority(queue);
|
|
99
109
|
const result = {
|
|
@@ -111,13 +121,19 @@ export function handleDashboardQueue(cmd, ctx, reqId) {
|
|
|
111
121
|
};
|
|
112
122
|
if (cmd.includeJobs) {
|
|
113
123
|
const limit = Math.min(cmd.jobsLimit ?? 10, 50);
|
|
124
|
+
// When paused, ready jobs surface under `paused` (waiting/prioritized are
|
|
125
|
+
// empty) — match the counts above (#92).
|
|
114
126
|
const waiting = ctx.queueManager.getJobs(queue, { state: 'waiting', end: limit });
|
|
115
127
|
const active = ctx.queueManager.getJobs(queue, { state: 'active', end: limit });
|
|
116
128
|
const delayed = ctx.queueManager.getJobs(queue, { state: 'delayed', end: limit });
|
|
129
|
+
const pausedJobs = paused
|
|
130
|
+
? ctx.queueManager.getJobs(queue, { state: 'paused', end: limit })
|
|
131
|
+
: [];
|
|
117
132
|
result.jobs = {
|
|
118
133
|
waiting: waiting.map(jobSummary),
|
|
119
134
|
active: active.map(jobSummary),
|
|
120
135
|
delayed: delayed.map(jobSummary),
|
|
136
|
+
paused: pausedJobs.map(jobSummary),
|
|
121
137
|
};
|
|
122
138
|
}
|
|
123
139
|
return resp.data(result, reqId);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import * as resp from '../../../domain/types/response';
|
|
6
6
|
import { jobId } from '../../../domain/types/job';
|
|
7
|
+
import { pausedView } from '../../../shared/pausedView';
|
|
7
8
|
/** Handle GetJob command */
|
|
8
9
|
export async function handleGetJob(cmd, ctx, reqId) {
|
|
9
10
|
const jid = jobId(cmd.id);
|
|
@@ -28,15 +29,18 @@ export function handleGetJobCounts(cmd, ctx, reqId) {
|
|
|
28
29
|
// Get queue-specific counts
|
|
29
30
|
const counts = ctx.queueManager.getQueueJobCounts(cmd.queue);
|
|
30
31
|
const isPaused = ctx.queueManager.isPaused(cmd.queue);
|
|
32
|
+
// When paused, ready jobs (waiting + prioritized) are reported under `paused` —
|
|
33
|
+
// not in their own buckets, which would double-count (#92, BullMQ semantics).
|
|
34
|
+
const pv = pausedView(counts.waiting, counts.prioritized, isPaused);
|
|
31
35
|
return resp.counts({
|
|
32
|
-
waiting:
|
|
33
|
-
prioritized:
|
|
36
|
+
waiting: pv.waiting,
|
|
37
|
+
prioritized: pv.prioritized,
|
|
34
38
|
delayed: counts.delayed,
|
|
35
39
|
active: counts.active,
|
|
36
40
|
completed: counts.completed,
|
|
37
41
|
failed: counts.failed,
|
|
38
42
|
'waiting-children': counts['waiting-children'],
|
|
39
|
-
paused:
|
|
43
|
+
paused: pv.paused,
|
|
40
44
|
}, reqId);
|
|
41
45
|
}
|
|
42
46
|
/** Handle GetCountsPerPriority command */
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { VERSION } from '../../shared/version';
|
|
5
5
|
import { throughputTracker } from '../../application/throughputTracker';
|
|
6
6
|
import { latencyTracker } from '../../application/latencyTracker';
|
|
7
|
+
import { pausedView } from '../../shared/pausedView';
|
|
7
8
|
/** JSON response helper */
|
|
8
9
|
export function jsonResponse(data, status = 200, corsOrigins) {
|
|
9
10
|
const headers = {
|
|
@@ -256,8 +257,17 @@ export function dashboardQueuesEndpoint(queueManager, limit, offset, corsOrigins
|
|
|
256
257
|
}
|
|
257
258
|
/** Dashboard single queue detail endpoint */
|
|
258
259
|
export function dashboardQueueDetailEndpoint(queueManager, queue, includeJobs, corsOrigins) {
|
|
259
|
-
const
|
|
260
|
+
const rawCounts = queueManager.getQueueJobCounts(queue);
|
|
260
261
|
const paused = queueManager.isPaused(queue);
|
|
262
|
+
// Keep counts consistent with the per-state lists below (#92): a paused queue
|
|
263
|
+
// reports ready jobs under `paused`, not `waiting`/`prioritized`.
|
|
264
|
+
const pv = pausedView(rawCounts.waiting, rawCounts.prioritized, paused);
|
|
265
|
+
const counts = {
|
|
266
|
+
...rawCounts,
|
|
267
|
+
waiting: pv.waiting,
|
|
268
|
+
prioritized: pv.prioritized,
|
|
269
|
+
paused: pv.paused,
|
|
270
|
+
};
|
|
261
271
|
const dlqJobs = queueManager.getDlq(queue, 10);
|
|
262
272
|
const priorityCounts = queueManager.getCountsPerPriority(queue);
|
|
263
273
|
const result = {
|
|
@@ -275,9 +285,12 @@ export function dashboardQueueDetailEndpoint(queueManager, queue, includeJobs, c
|
|
|
275
285
|
timestamp: Date.now(),
|
|
276
286
|
};
|
|
277
287
|
if (includeJobs) {
|
|
288
|
+
// When paused, ready jobs surface under `paused` (waiting is empty) — match
|
|
289
|
+
// the counts above (#92).
|
|
278
290
|
const waiting = queueManager.getJobs(queue, { state: 'waiting', end: 10 });
|
|
279
291
|
const active = queueManager.getJobs(queue, { state: 'active', end: 10 });
|
|
280
292
|
const delayed = queueManager.getJobs(queue, { state: 'delayed', end: 10 });
|
|
293
|
+
const pausedJobs = paused ? queueManager.getJobs(queue, { state: 'paused', end: 10 }) : [];
|
|
281
294
|
const toSummary = (j) => ({
|
|
282
295
|
id: j.id,
|
|
283
296
|
priority: j.priority,
|
|
@@ -290,6 +303,7 @@ export function dashboardQueueDetailEndpoint(queueManager, queue, includeJobs, c
|
|
|
290
303
|
waiting: waiting.map(toSummary),
|
|
291
304
|
active: active.map(toSummary),
|
|
292
305
|
delayed: delayed.map(toSummary),
|
|
306
|
+
paused: pausedJobs.map(toSummary),
|
|
293
307
|
};
|
|
294
308
|
}
|
|
295
309
|
return jsonResponse(result, 200, corsOrigins);
|
package/dist/mcp/index.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun-only runtime guard (defense-in-depth for the bundled path).
|
|
3
|
+
*
|
|
4
|
+
* The `"node"` export condition (-> bun-only.ts) already fails fast for a plain
|
|
5
|
+
* `node` import and for node-target bundlers. This guard covers the remaining
|
|
6
|
+
* case: a browser/neutral-target bundler that inlines the real client, whose
|
|
7
|
+
* output is then run on Node. Without it, the failure surfaces deep inside as a
|
|
8
|
+
* cryptic `Bun is not defined` / `Bun.connect is not a function`.
|
|
9
|
+
*
|
|
10
|
+
* Imported FIRST by the client entrypoints so it evaluates before any module
|
|
11
|
+
* that touches `Bun.*` at top level. No-op under Bun. Keep relative-import-free.
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun-only runtime guard (defense-in-depth for the bundled path).
|
|
3
|
+
*
|
|
4
|
+
* The `"node"` export condition (-> bun-only.ts) already fails fast for a plain
|
|
5
|
+
* `node` import and for node-target bundlers. This guard covers the remaining
|
|
6
|
+
* case: a browser/neutral-target bundler that inlines the real client, whose
|
|
7
|
+
* output is then run on Node. Without it, the failure surfaces deep inside as a
|
|
8
|
+
* cryptic `Bun is not defined` / `Bun.connect is not a function`.
|
|
9
|
+
*
|
|
10
|
+
* Imported FIRST by the client entrypoints so it evaluates before any module
|
|
11
|
+
* that touches `Bun.*` at top level. No-op under Bun. Keep relative-import-free.
|
|
12
|
+
*/
|
|
13
|
+
if (typeof globalThis.Bun === 'undefined') {
|
|
14
|
+
throw new Error('bunqueue is Bun-only and requires the Bun runtime (https://bun.sh). ' +
|
|
15
|
+
'Node.js is not supported: install Bun and run your program with `bun`.');
|
|
16
|
+
}
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BullMQ paused-view counts (#92).
|
|
3
|
+
*
|
|
4
|
+
* When a queue is paused, none of its ready jobs (waiting + prioritized) can be
|
|
5
|
+
* pulled, so they are reported under `paused` — and NOT in their own buckets, to
|
|
6
|
+
* avoid double-counting a single job. This mirrors the per-state lists, where a
|
|
7
|
+
* paused queue lists those jobs under `paused` and returns nothing for `waiting`
|
|
8
|
+
* / `prioritized`. Delayed and active jobs keep their own state.
|
|
9
|
+
*
|
|
10
|
+
* Single source of truth shared by every count surface that exposes the BullMQ
|
|
11
|
+
* `getJobCounts` shape (client SDK, TCP handler, dashboard detail), so they can
|
|
12
|
+
* never drift apart.
|
|
13
|
+
*/
|
|
14
|
+
export interface PausedDerivedCounts {
|
|
15
|
+
waiting: number;
|
|
16
|
+
prioritized: number;
|
|
17
|
+
paused: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function pausedView(waiting: number, prioritized: number, isPaused: boolean): PausedDerivedCounts;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunqueue",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.4",
|
|
4
4
|
"description": "High-performance job queue for Bun & AI agents. SQLite persistence, cron scheduling, priorities, retries, DLQ, webhooks, native MCP server. Zero external dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -12,22 +12,32 @@
|
|
|
12
12
|
"exports": {
|
|
13
13
|
".": {
|
|
14
14
|
"types": "./dist/main.d.ts",
|
|
15
|
+
"bun": "./dist/main.js",
|
|
16
|
+
"node": "./dist/bun-only.js",
|
|
15
17
|
"import": "./dist/main.js"
|
|
16
18
|
},
|
|
17
19
|
"./client": {
|
|
18
20
|
"types": "./dist/client/index.d.ts",
|
|
21
|
+
"bun": "./dist/client/index.js",
|
|
22
|
+
"node": "./dist/bun-only.js",
|
|
19
23
|
"import": "./dist/client/index.js"
|
|
20
24
|
},
|
|
21
25
|
"./queue": {
|
|
22
26
|
"types": "./dist/application/queueManager.d.ts",
|
|
27
|
+
"bun": "./dist/application/queueManager.js",
|
|
28
|
+
"node": "./dist/bun-only.js",
|
|
23
29
|
"import": "./dist/application/queueManager.js"
|
|
24
30
|
},
|
|
25
31
|
"./mcp": {
|
|
26
32
|
"types": "./dist/mcp/index.d.ts",
|
|
33
|
+
"bun": "./dist/mcp/index.js",
|
|
34
|
+
"node": "./dist/bun-only.js",
|
|
27
35
|
"import": "./dist/mcp/index.js"
|
|
28
36
|
},
|
|
29
37
|
"./workflow": {
|
|
30
38
|
"types": "./dist/client/workflow/index.d.ts",
|
|
39
|
+
"bun": "./dist/client/workflow/index.js",
|
|
40
|
+
"node": "./dist/bun-only.js",
|
|
31
41
|
"import": "./dist/client/workflow/index.js"
|
|
32
42
|
},
|
|
33
43
|
"./package.json": "./package.json"
|
|
@@ -137,7 +147,6 @@
|
|
|
137
147
|
},
|
|
138
148
|
"homepage": "https://bunqueue.dev",
|
|
139
149
|
"engines": {
|
|
140
|
-
"node": ">=18.0.0",
|
|
141
150
|
"bun": ">=1.3.9"
|
|
142
151
|
}
|
|
143
152
|
}
|