bunqueue 2.8.19 → 2.8.21

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.
@@ -70,7 +70,7 @@ export async function checkExpiredLocks(ctx) {
70
70
  * Process a single expired lock (called with both locks already held)
71
71
  * Lock hierarchy already satisfied: shardLock -> processingLock held by caller
72
72
  */
73
- // eslint-disable-next-line max-params
73
+ // biome-ignore lint/complexity/useMaxParams: lock-processing inner fn needs the full lock context (7 params)
74
74
  function processExpiredLockInner(jobId, lock, job, shardIdx, procIdx, ctx, now) {
75
75
  const shard = ctx.shards[shardIdx];
76
76
  const queue = shard.getQueue(job.queue);
@@ -14,11 +14,11 @@ export function createMonitoringState() {
14
14
  };
15
15
  }
16
16
  /** Config from env vars */
17
- const QUEUE_IDLE_THRESHOLD_MS = parseInt(process.env.QUEUE_IDLE_THRESHOLD_MS ?? '30000');
18
- const QUEUE_SIZE_THRESHOLD = parseInt(process.env.QUEUE_SIZE_THRESHOLD ?? '0'); // 0 = disabled
19
- const MEMORY_WARNING_MB = parseInt(process.env.MEMORY_WARNING_MB ?? '0'); // 0 = disabled
20
- const STORAGE_WARNING_MB = parseInt(process.env.STORAGE_WARNING_MB ?? '0'); // 0 = disabled
21
- const WORKER_OVERLOAD_THRESHOLD_MS = parseInt(process.env.WORKER_OVERLOAD_THRESHOLD_MS ?? '30000');
17
+ const QUEUE_IDLE_THRESHOLD_MS = parseInt(process.env.QUEUE_IDLE_THRESHOLD_MS ?? '30000', 10);
18
+ const QUEUE_SIZE_THRESHOLD = parseInt(process.env.QUEUE_SIZE_THRESHOLD ?? '0', 10); // 0 = disabled
19
+ const MEMORY_WARNING_MB = parseInt(process.env.MEMORY_WARNING_MB ?? '0', 10); // 0 = disabled
20
+ const STORAGE_WARNING_MB = parseInt(process.env.STORAGE_WARNING_MB ?? '0', 10); // 0 = disabled
21
+ const WORKER_OVERLOAD_THRESHOLD_MS = parseInt(process.env.WORKER_OVERLOAD_THRESHOLD_MS ?? '30000', 10);
22
22
  /** Check all monitoring conditions — called from cleanup interval (10s) */
23
23
  export function runMonitoringChecks(ctx) {
24
24
  if (!ctx.dashboardEmit)
@@ -11,7 +11,7 @@ import { bootServer } from '../../infrastructure/server/bootstrap';
11
11
  /** Validate port number */
12
12
  function validatePort(value, name, defaultPort) {
13
13
  const port = parseInt(value, 10);
14
- if (isNaN(port) || port < 1 || port > 65535) {
14
+ if (Number.isNaN(port) || port < 1 || port > 65535) {
15
15
  console.warn(`Warning: Invalid ${name} "${value}". Using default ${defaultPort}.`);
16
16
  return defaultPort;
17
17
  }
package/dist/cli/index.js CHANGED
@@ -22,7 +22,7 @@ function resolveEnvPort(currentPort) {
22
22
  if (!envPort)
23
23
  return currentPort;
24
24
  const parsed = parseInt(envPort, 10);
25
- if (isNaN(parsed) || parsed < 1 || parsed > 65535) {
25
+ if (Number.isNaN(parsed) || parsed < 1 || parsed > 65535) {
26
26
  console.warn(`Warning: Invalid env port "${envPort}". Using ${currentPort}.`);
27
27
  return currentPort;
28
28
  }
@@ -129,7 +129,7 @@ function applyPortFlag(allArgs, i, state) {
129
129
  return i;
130
130
  }
131
131
  const parsed = parseInt(nextArg, 10);
132
- if (isNaN(parsed) || parsed < 1 || parsed > 65535) {
132
+ if (Number.isNaN(parsed) || parsed < 1 || parsed > 65535) {
133
133
  console.warn(`Warning: Invalid port "${nextArg}". Using default port 6789.`);
134
134
  state.port = 6789;
135
135
  }
@@ -208,7 +208,7 @@ export function parseGlobalOptions() {
208
208
  else if (arg.startsWith('--port=')) {
209
209
  const raw = arg.slice(7);
210
210
  const parsed = parseInt(raw, 10);
211
- if (isNaN(parsed) || parsed < 1 || parsed > 65535) {
211
+ if (Number.isNaN(parsed) || parsed < 1 || parsed > 65535) {
212
212
  console.warn(`Warning: Invalid port "${raw}". Using default port 6789.`);
213
213
  hp.port = 6789;
214
214
  }
@@ -8,7 +8,7 @@ interface ManagementContext {
8
8
  embedded: boolean;
9
9
  tcp: TcpConnectionPool | null;
10
10
  }
11
- /** Remove a job (sync) */
11
+ /** Remove a job (sync, fire-and-forget; use removeAsync to await the removal) */
12
12
  export declare function remove(ctx: ManagementContext, id: string): void;
13
13
  /** Remove a job (async) */
14
14
  export declare function removeAsync(ctx: ManagementContext, id: string): Promise<void>;
@@ -1,4 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-floating-promises, @typescript-eslint/no-unnecessary-condition */
2
1
  /**
3
2
  * Queue Management Operations
4
3
  * remove, retry, clean, promote, updateProgress, logs
@@ -6,17 +5,21 @@
6
5
  import { getSharedManager } from '../../manager';
7
6
  import { jobId } from '../../../domain/types/job';
8
7
  // ============ Remove Operations ============
9
- /** Remove a job (sync) */
8
+ /** Remove a job (sync, fire-and-forget; use removeAsync to await the removal) */
10
9
  export function remove(ctx, id) {
10
+ // `void`: the cancellation is intentionally not awaited here (sync API).
11
11
  if (ctx.embedded)
12
- getSharedManager().cancel(jobId(id));
12
+ void getSharedManager().cancel(jobId(id));
13
13
  else
14
- ctx.tcp.send({ cmd: 'Cancel', id });
14
+ void ctx.tcp.send({ cmd: 'Cancel', id });
15
15
  }
16
16
  /** Remove a job (async) */
17
17
  export async function removeAsync(ctx, id) {
18
18
  if (ctx.embedded) {
19
- getSharedManager().cancel(jobId(id));
19
+ // Must await: cancel() does the removal inside an async write-lock, so without
20
+ // the await the returned promise resolves before the job is actually removed
21
+ // (and any cancel error is swallowed) — inconsistent with the TCP path below.
22
+ await getSharedManager().cancel(jobId(id));
20
23
  return;
21
24
  }
22
25
  await ctx.tcp.send({ cmd: 'Cancel', id });
@@ -17,7 +17,17 @@ export class TemporalManager {
17
17
  * Ordered by createdAt for efficient cleanQueue range queries
18
18
  * Uses equality check on jobId to prevent duplicate entries for the same job
19
19
  */
20
- temporalIndex = new SkipList((a, b) => a.createdAt - b.createdAt, 16, 0.5, (a, b) => a.jobId === b.jobId);
20
+ // Total-order comparator: createdAt first, then jobId as a tie-break. Without
21
+ // the tie-break, a batch of jobs sharing one createdAt (the addBulk case —
22
+ // `now` is captured once) makes every node compare-equal, which (a) turns
23
+ // SkipList.insert's duplicate-check scan into O(n) per insert => O(n²) for the
24
+ // batch, and (b) makes SkipList.delete remove the WRONG same-createdAt node
25
+ // (it stops at the first compare-equal node). A total order fixes both: the
26
+ // dedup scan and delete both resolve to the exact (createdAt, jobId) node in
27
+ // O(log n). jobId is a string (UUIDv7 by default, or any custom id), so its
28
+ // lexicographic comparison is a valid total order in every case. Equality is
29
+ // still by jobId (now reached only for a true duplicate).
30
+ temporalIndex = new SkipList((a, b) => a.createdAt - b.createdAt || (a.jobId < b.jobId ? -1 : a.jobId > b.jobId ? 1 : 0), 16, 0.5, (a, b) => a.jobId === b.jobId);
21
31
  /** Set of delayed job IDs for tracking when they become ready */
22
32
  delayedJobIds = new Set();
23
33
  /**
@@ -22,7 +22,6 @@ export declare class CloudAgent {
22
22
  private statsUpdateTimer;
23
23
  private unsubscribeEvents;
24
24
  private sequenceId;
25
- private snapshotCount;
26
25
  private stopped;
27
26
  private serverHandles?;
28
27
  /** Event buffer — flushed into each HTTP snapshot */
@@ -27,7 +27,6 @@ export class CloudAgent {
27
27
  statsUpdateTimer = null;
28
28
  unsubscribeEvents = null;
29
29
  sequenceId = 0;
30
- snapshotCount = 0;
31
30
  stopped = false;
32
31
  serverHandles;
33
32
  /** Event buffer — flushed into each HTTP snapshot */
@@ -164,7 +163,6 @@ export class CloudAgent {
164
163
  /** Collect and send a snapshot via HTTP */
165
164
  async sendSnapshot(_forceHeavy = false) {
166
165
  try {
167
- this.snapshotCount++;
168
166
  const snapshot = await collectSnapshot({
169
167
  queueManager: this.queueManager,
170
168
  instanceId: this.instanceId,
@@ -253,8 +253,8 @@ async function routeRequest(req, path, ctx, corsOrigins) {
253
253
  }
254
254
  if (path === '/dashboard/queues' && method === 'GET') {
255
255
  const url = new URL(req.url);
256
- const limit = Math.min(Math.max(parseInt(url.searchParams.get('limit') ?? '100') || 100, 1), 500);
257
- const offset = Math.max(parseInt(url.searchParams.get('offset') ?? '0') || 0, 0);
256
+ const limit = Math.min(Math.max(parseInt(url.searchParams.get('limit') ?? '100', 10) || 100, 1), 500);
257
+ const offset = Math.max(parseInt(url.searchParams.get('offset') ?? '0', 10) || 0, 0);
258
258
  return dashboardQueuesEndpoint(ctx.queueManager, limit, offset, corsOrigins);
259
259
  }
260
260
  const dashQueueMatch = path.match(RE_DASHBOARD_QUEUE_DETAIL);
@@ -60,7 +60,7 @@ async function routeJobOps(req, path, method, ctx, cors) {
60
60
  return jsonResponse(r, r.ok ? 200 : 400, cors);
61
61
  }
62
62
  if (method === 'GET') {
63
- const timeout = parseInt(new URL(req.url).searchParams.get('timeout') ?? '0');
63
+ const timeout = parseInt(new URL(req.url).searchParams.get('timeout') ?? '0', 10);
64
64
  const r = await handleCommand({ cmd: 'PULL', queue, timeout }, ctx);
65
65
  return jsonResponse(r, 200, cors);
66
66
  }
@@ -125,8 +125,8 @@ async function routeJobOps(req, path, method, ctx, cors) {
125
125
  : stateValues;
126
126
  const limitParam = url.searchParams.get('limit');
127
127
  const offsetParam = url.searchParams.get('offset');
128
- const limit = limitParam ? parseInt(limitParam) : undefined;
129
- const offset = offsetParam ? parseInt(offsetParam) : undefined;
128
+ const limit = limitParam ? parseInt(limitParam, 10) : undefined;
129
+ const offset = offsetParam ? parseInt(offsetParam, 10) : undefined;
130
130
  const r = await handleCommand({
131
131
  cmd: 'GetJobs',
132
132
  queue,
@@ -116,6 +116,13 @@ export class SseHandler {
116
116
  // ── Job event broadcasting ─────────────────────────────────
117
117
  /** Broadcast job event to matching clients (with typed SSE event field) */
118
118
  broadcast(event) {
119
+ // No clients => nothing to send, and nothing to buffer that anyone could
120
+ // replay. Mirrors wsHandler.broadcast. Without this, every job event still
121
+ // paid JSON.stringify + encode + ring buffer + an O(queue size) per-event
122
+ // getQueueJobCounts, turning a bulk push into O(N²) even with no dashboard
123
+ // attached (the common high-throughput case).
124
+ if (this.clients.size === 0)
125
+ return;
119
126
  const id = ++this.eventId;
120
127
  const eventName = EVENT_MAP[event.eventType] ?? `job:${event.eventType}`;
121
128
  const eventData = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunqueue",
3
- "version": "2.8.19",
3
+ "version": "2.8.21",
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",
@@ -62,12 +62,13 @@
62
62
  "bench:latency": "bun run bench/latency.ts",
63
63
  "bench:internal": "bun run benchmarks/index.ts",
64
64
  "bench:compare": "bun run bench/comparison/run.ts",
65
- "lint": "eslint src/",
66
- "lint:fix": "eslint src/ --fix",
67
- "format": "prettier --write \"src/**/*.ts\"",
68
- "format:check": "prettier --check \"src/**/*.ts\"",
65
+ "lint": "biome lint src",
66
+ "lint:fix": "biome lint --write src",
67
+ "format": "biome format --write src",
68
+ "format:check": "biome format src",
69
69
  "typecheck": "tsc --noEmit",
70
- "check": "bun run typecheck && bun run lint && bun run format:check",
70
+ "check:biome": "biome check src",
71
+ "check": "bun run typecheck && biome check src",
71
72
  "prepublishOnly": "bun run build:lib"
72
73
  },
73
74
  "dependencies": {
@@ -75,17 +76,13 @@
75
76
  "msgpackr": "^1.11.8"
76
77
  },
77
78
  "devDependencies": {
78
- "@eslint/js": "^10.0.1",
79
+ "@biomejs/biome": "^2.5.0",
79
80
  "@modelcontextprotocol/sdk": "^1.26.0",
80
81
  "@types/bun": "^1.3.9",
81
82
  "bullmq": "^5.70.1",
82
83
  "elysia": "^1.4.25",
83
- "eslint": "^10.0.1",
84
- "eslint-config-prettier": "^10.1.8",
85
84
  "ioredis": "^5.9.3",
86
- "prettier": "^3.8.1",
87
85
  "typescript": "^5.9.3",
88
- "typescript-eslint": "8.56.1",
89
86
  "zod": "^4.3.6"
90
87
  },
91
88
  "peerDependencies": {