bunqueue 2.8.39 → 2.8.40

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 (46) hide show
  1. package/dist/application/backgroundTasks.js +3 -0
  2. package/dist/application/operations/jobManagement.js +7 -4
  3. package/dist/application/operations/jobStateTransitions.js +1 -0
  4. package/dist/application/operations/pull.d.ts +2 -2
  5. package/dist/application/operations/pull.js +43 -10
  6. package/dist/application/queueManager.d.ts +4 -4
  7. package/dist/application/queueManager.js +26 -11
  8. package/dist/cli/client.js +6 -47
  9. package/dist/cli/commandRegistry.d.ts +27 -0
  10. package/dist/cli/commandRegistry.js +40 -0
  11. package/dist/cli/commandRouter.d.ts +3 -0
  12. package/dist/cli/commandRouter.js +44 -0
  13. package/dist/cli/commands/backup.js +1 -1
  14. package/dist/cli/commands/core.js +54 -2
  15. package/dist/cli/commands/doctor.d.ts +50 -5
  16. package/dist/cli/commands/doctor.js +125 -108
  17. package/dist/cli/commands/types.js +5 -1
  18. package/dist/cli/globalOptions.d.ts +16 -0
  19. package/dist/cli/globalOptions.js +192 -0
  20. package/dist/cli/help.d.ts +9 -0
  21. package/dist/cli/help.js +42 -21
  22. package/dist/cli/index.d.ts +1 -25
  23. package/dist/cli/index.js +84 -348
  24. package/dist/cli/localOutput.d.ts +22 -0
  25. package/dist/cli/localOutput.js +45 -0
  26. package/dist/client/queue/dlqOps.js +1 -3
  27. package/dist/client/tcp/client.js +10 -8
  28. package/dist/domain/queue/shard.d.ts +2 -2
  29. package/dist/domain/queue/shard.js +3 -3
  30. package/dist/domain/queue/waiterManager.d.ts +2 -2
  31. package/dist/domain/queue/waiterManager.js +26 -5
  32. package/dist/infrastructure/cloud/wsSender.js +4 -4
  33. package/dist/infrastructure/persistence/schema.d.ts +2 -2
  34. package/dist/infrastructure/persistence/schema.js +7 -2
  35. package/dist/infrastructure/persistence/sqlite.d.ts +5 -1
  36. package/dist/infrastructure/persistence/sqlite.js +10 -2
  37. package/dist/infrastructure/persistence/sqliteSerializer.js +1 -1
  38. package/dist/infrastructure/persistence/statements.d.ts +2 -1
  39. package/dist/infrastructure/persistence/statements.js +3 -2
  40. package/dist/infrastructure/server/handlers/core.js +4 -4
  41. package/dist/infrastructure/server/tcp.d.ts +1 -0
  42. package/dist/infrastructure/server/tcp.js +9 -5
  43. package/dist/infrastructure/server/types.d.ts +2 -0
  44. package/dist/shared/msgpack.d.ts +3 -0
  45. package/dist/shared/msgpack.js +70 -0
  46. package/package.json +1 -1
@@ -208,6 +208,9 @@ export function recover(ctx) {
208
208
  if (qs.stallConfig) {
209
209
  ctx.shards[shardIndex(qs.name)].setStallConfig(qs.name, qs.stallConfig);
210
210
  }
211
+ if (qs.dlqConfig) {
212
+ ctx.shards[shardIndex(qs.name)].setDlqConfig(qs.name, qs.dlqConfig);
213
+ }
211
214
  }
212
215
  const now = Date.now();
213
216
  // === PHASE 1: Recover active jobs (were processing when server stopped) ===
@@ -78,10 +78,13 @@ export async function updateJobProgress(jobId, progress, ctx, message) {
78
78
  const job = ctx.processingShards[procIdx].get(jobId);
79
79
  if (!job)
80
80
  return false;
81
- job.progress = Math.max(0, Math.min(100, progress));
82
- if (message !== undefined)
83
- job.progressMessage = message;
84
- job.lastHeartbeat = Date.now();
81
+ const clampedProgress = Math.max(0, Math.min(100, progress));
82
+ const effectiveMessage = message === undefined ? job.progressMessage : message;
83
+ const heartbeat = Date.now();
84
+ ctx.storage?.updateJobProgress(jobId, clampedProgress, effectiveMessage, heartbeat);
85
+ job.progress = clampedProgress;
86
+ job.progressMessage = effectiveMessage;
87
+ job.lastHeartbeat = heartbeat;
85
88
  // Broadcast progress event to internal subscribers
86
89
  ctx.eventsManager.broadcast({
87
90
  eventType: 'progress',
@@ -37,6 +37,7 @@ export async function moveActiveToWait(jobId, ctx) {
37
37
  ctx.jobIndex.set(jobId, { type: 'queue', shardIdx: idx, queueName: job.queue });
38
38
  shard.notify(job.queue);
39
39
  });
40
+ ctx.storage?.updateRunAt(jobId, job.runAt);
40
41
  ctx.eventsManager.broadcast({
41
42
  eventType: 'waiting',
42
43
  jobId,
@@ -29,8 +29,8 @@ export interface PullContext {
29
29
  /**
30
30
  * Pull next job from queue
31
31
  */
32
- export declare function pullJob(queue: string, timeoutMs: number, ctx: PullContext): Promise<Job | null>;
32
+ export declare function pullJob(queue: string, timeoutMs: number, ctx: PullContext, signal?: AbortSignal): Promise<Job | null>;
33
33
  /**
34
34
  * Pull multiple jobs from queue
35
35
  */
36
- export declare function pullJobBatch(queue: string, count: number, timeoutMs: number, ctx: PullContext): Promise<Job[]>;
36
+ export declare function pullJobBatch(queue: string, count: number, timeoutMs: number, ctx: PullContext, signal?: AbortSignal): Promise<Job[]>;
@@ -122,10 +122,11 @@ function tryDequeueNextJob(shard, queue, now, ctx) {
122
122
  }
123
123
  }
124
124
  /** Wait until either a notification arrives, the next delay matures, or the pull deadline. */
125
- async function waitForNextCandidate(shard, queue, deadline, now, nextRunAt) {
125
+ async function waitForNextCandidate(options) {
126
+ const { shard, queue, deadline, now, nextRunAt, signal } = options;
126
127
  const remaining = deadline - now;
127
128
  const untilNextRun = nextRunAt === null ? remaining : Math.max(1, nextRunAt - now);
128
- await shard.waitForJob(queue, Math.min(remaining, untilNextRun));
129
+ await shard.waitForJob(queue, Math.min(remaining, untilNextRun), signal);
129
130
  }
130
131
  /**
131
132
  * Finish the handoff of a dequeued job: persist active state and broadcast.
@@ -217,14 +218,20 @@ async function requeueJob(job, queue, idx, ctx) {
217
218
  /**
218
219
  * Pull next job from queue
219
220
  */
220
- export async function pullJob(queue, timeoutMs, ctx) {
221
+ export async function pullJob(queue, timeoutMs, ctx, signal) {
221
222
  const startNs = Bun.nanoseconds();
222
223
  const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : 0;
223
224
  const idx = shardIndex(queue);
224
225
  while (true) {
225
- const attempt = await tryPullFromShard(queue, idx, ctx);
226
+ if (signal?.aborted)
227
+ return null;
228
+ const attempt = await tryPullFromShard(queue, idx, ctx, signal);
226
229
  const job = attempt.job;
227
230
  if (job) {
231
+ if (signal?.aborted) {
232
+ await requeueJob(job, queue, idx, ctx);
233
+ return null;
234
+ }
228
235
  let delivered = false;
229
236
  try {
230
237
  delivered = finalizeProcessing(job, queue, ctx);
@@ -246,14 +253,23 @@ export async function pullJob(queue, timeoutMs, ctx) {
246
253
  if (deadline === 0 || now >= deadline) {
247
254
  return null;
248
255
  }
249
- await waitForNextCandidate(ctx.shards[idx], queue, deadline, now, attempt.nextRunAt);
256
+ await waitForNextCandidate({
257
+ shard: ctx.shards[idx],
258
+ queue,
259
+ deadline,
260
+ now,
261
+ nextRunAt: attempt.nextRunAt,
262
+ signal,
263
+ });
250
264
  }
251
265
  }
252
266
  /**
253
267
  * Try to pull a job from a specific shard
254
268
  */
255
- async function tryPullFromShard(queue, idx, ctx) {
269
+ async function tryPullFromShard(queue, idx, ctx, signal) {
256
270
  return await withWriteLock(ctx.shardLocks[idx], () => {
271
+ if (signal?.aborted)
272
+ return { job: null, nextRunAt: null };
257
273
  const shard = ctx.shards[idx];
258
274
  const state = shard.getState(queue);
259
275
  if (state.paused) {
@@ -269,14 +285,22 @@ async function tryPullFromShard(queue, idx, ctx) {
269
285
  /**
270
286
  * Pull multiple jobs from queue
271
287
  */
272
- export async function pullJobBatch(queue, count, timeoutMs, ctx) {
288
+ export async function pullJobBatch(queue, count, timeoutMs, ctx, signal) {
273
289
  const startNs = Bun.nanoseconds();
274
290
  const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : 0;
275
291
  const idx = shardIndex(queue);
276
292
  while (true) {
277
- const attempt = await tryPullBatchFromShard(queue, idx, count, ctx);
293
+ if (signal?.aborted)
294
+ return [];
295
+ const attempt = await tryPullBatchFromShard(queue, idx, count, ctx, signal);
278
296
  const jobs = attempt.jobs;
279
297
  if (jobs.length > 0) {
298
+ if (signal?.aborted) {
299
+ for (const job of jobs) {
300
+ await requeueJob(job, queue, idx, ctx);
301
+ }
302
+ return [];
303
+ }
280
304
  let delivered;
281
305
  try {
282
306
  delivered = finalizeProcessingBatch(jobs, queue, ctx);
@@ -305,14 +329,23 @@ export async function pullJobBatch(queue, count, timeoutMs, ctx) {
305
329
  if (deadline === 0 || now >= deadline) {
306
330
  return [];
307
331
  }
308
- await waitForNextCandidate(ctx.shards[idx], queue, deadline, now, attempt.nextRunAt);
332
+ await waitForNextCandidate({
333
+ shard: ctx.shards[idx],
334
+ queue,
335
+ deadline,
336
+ now,
337
+ nextRunAt: attempt.nextRunAt,
338
+ signal,
339
+ });
309
340
  }
310
341
  }
311
342
  /**
312
343
  * Try to pull multiple jobs from a shard
313
344
  */
314
- async function tryPullBatchFromShard(queue, idx, count, ctx) {
345
+ async function tryPullBatchFromShard(queue, idx, count, ctx, signal) {
315
346
  return await withWriteLock(ctx.shardLocks[idx], () => {
347
+ if (signal?.aborted)
348
+ return { jobs: [], nextRunAt: null };
316
349
  const shard = ctx.shards[idx];
317
350
  const state = shard.getState(queue);
318
351
  const jobs = [];
@@ -65,13 +65,13 @@ export declare class QueueManager {
65
65
  private handleRepeat;
66
66
  push(queue: string, input: JobInput): Promise<Job>;
67
67
  pushBatch(queue: string, inputs: JobInput[]): Promise<JobId[]>;
68
- pull(queue: string, timeoutMs?: number): Promise<Job | null>;
69
- pullWithLock(queue: string, owner: string, timeoutMs?: number, lockTtl?: number): Promise<{
68
+ pull(queue: string, timeoutMs?: number, signal?: AbortSignal): Promise<Job | null>;
69
+ pullWithLock(queue: string, owner: string, timeoutMs?: number, lockTtl?: number, signal?: AbortSignal): Promise<{
70
70
  job: Job | null;
71
71
  token: string | null;
72
72
  }>;
73
- pullBatch(queue: string, count: number, timeoutMs?: number): Promise<Job[]>;
74
- pullBatchWithLock(queue: string, count: number, owner: string, timeoutMs?: number, lockTtl?: number): Promise<{
73
+ pullBatch(queue: string, count: number, timeoutMs?: number, signal?: AbortSignal): Promise<Job[]>;
74
+ pullBatchWithLock(queue: string, count: number, owner: string, timeoutMs?: number, lockTtl?: number, signal?: AbortSignal): Promise<{
75
75
  jobs: Job[];
76
76
  tokens: string[];
77
77
  }>;
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { DEFAULT_LOCK_TTL } from '../domain/types/job';
6
6
  import { DEFAULT_STALL_CONFIG } from '../domain/types/stall';
7
+ import { DEFAULT_DLQ_CONFIG } from '../domain/types/dlq';
7
8
  import { Shard } from '../domain/queue/shard';
8
9
  import { SqliteStorage } from '../infrastructure/persistence/sqlite';
9
10
  import { CronScheduler } from '../infrastructure/scheduler/cronScheduler';
@@ -257,21 +258,22 @@ export class QueueManager {
257
258
  this.registerQueueName(queue);
258
259
  return pushJobBatch(queue, inputs, this.contextFactory.getPushContext());
259
260
  }
260
- async pull(queue, timeoutMs = 0) {
261
- return pullJob(queue, timeoutMs, this.contextFactory.getPullContext());
261
+ async pull(queue, timeoutMs = 0, signal) {
262
+ return pullJob(queue, timeoutMs, this.contextFactory.getPullContext(), signal);
262
263
  }
263
- async pullWithLock(queue, owner, timeoutMs = 0, lockTtl = DEFAULT_LOCK_TTL) {
264
- const job = await pullJob(queue, timeoutMs, this.contextFactory.getPullContext());
264
+ async pullWithLock(queue, owner, timeoutMs = 0, lockTtl = DEFAULT_LOCK_TTL, signal) {
265
+ const job = await pullJob(queue, timeoutMs, this.contextFactory.getPullContext(), signal);
265
266
  if (!job)
266
267
  return { job: null, token: null };
267
268
  const token = lockMgr.createLock(job.id, owner, this.contextFactory.getLockContext(), lockTtl);
268
269
  return { job, token };
269
270
  }
270
- async pullBatch(queue, count, timeoutMs = 0) {
271
- return pullJobBatch(queue, count, timeoutMs, this.contextFactory.getPullContext());
271
+ async pullBatch(queue, count, timeoutMs = 0, signal) {
272
+ return pullJobBatch(queue, count, timeoutMs, this.contextFactory.getPullContext(), signal);
272
273
  }
273
- async pullBatchWithLock(queue, count, owner, timeoutMs = 0, lockTtl = DEFAULT_LOCK_TTL) {
274
- const jobs = await pullJobBatch(queue, count, timeoutMs, this.contextFactory.getPullContext());
274
+ // biome-ignore lint/complexity/useMaxParams: preserves the public API while adding cancellation
275
+ async pullBatchWithLock(queue, count, owner, timeoutMs = 0, lockTtl = DEFAULT_LOCK_TTL, signal) {
276
+ const jobs = await pullJobBatch(queue, count, timeoutMs, this.contextFactory.getPullContext(), signal);
275
277
  const tokens = [];
276
278
  for (const job of jobs) {
277
279
  const token = lockMgr.createLock(job.id, owner, this.contextFactory.getLockContext(), lockTtl);
@@ -894,10 +896,16 @@ export class QueueManager {
894
896
  return;
895
897
  const state = this.shards[shardIndex(queue)].getState(queue);
896
898
  const stallConfig = this.shards[shardIndex(queue)].getStallConfig(queue);
899
+ const dlqConfig = this.shards[shardIndex(queue)].getDlqConfig(queue);
897
900
  const hasCustomStallConfig = stallConfig.enabled !== DEFAULT_STALL_CONFIG.enabled ||
898
901
  stallConfig.stallInterval !== DEFAULT_STALL_CONFIG.stallInterval ||
899
902
  stallConfig.maxStalls !== DEFAULT_STALL_CONFIG.maxStalls ||
900
903
  stallConfig.gracePeriod !== DEFAULT_STALL_CONFIG.gracePeriod;
904
+ const hasCustomDlqConfig = dlqConfig.autoRetry !== DEFAULT_DLQ_CONFIG.autoRetry ||
905
+ dlqConfig.autoRetryInterval !== DEFAULT_DLQ_CONFIG.autoRetryInterval ||
906
+ dlqConfig.maxAutoRetries !== DEFAULT_DLQ_CONFIG.maxAutoRetries ||
907
+ dlqConfig.maxAge !== DEFAULT_DLQ_CONFIG.maxAge ||
908
+ dlqConfig.maxEntries !== DEFAULT_DLQ_CONFIG.maxEntries;
901
909
  // When control-state returns fully to default (not paused, no limits), drop
902
910
  // the row instead of persisting an all-default placeholder. Keeps the table
903
911
  // free of noise rows for ephemeral queues that only ever call resume/clear*,
@@ -905,7 +913,8 @@ export class QueueManager {
905
913
  if (!state.paused &&
906
914
  state.rateLimit === null &&
907
915
  state.concurrencyLimit === null &&
908
- !hasCustomStallConfig) {
916
+ !hasCustomStallConfig &&
917
+ !hasCustomDlqConfig) {
909
918
  this.storage.deleteQueueState(queue);
910
919
  return;
911
920
  }
@@ -916,6 +925,7 @@ export class QueueManager {
916
925
  rateLimitDuration: state.rateLimitDuration,
917
926
  rateLimitExpiresAt: state.rateLimitExpiresAt,
918
927
  stallConfig,
928
+ dlqConfig,
919
929
  });
920
930
  }
921
931
  /** Get rate limit and concurrency limit for a queue */
@@ -954,6 +964,7 @@ export class QueueManager {
954
964
  }
955
965
  setDlqConfig(queue, config) {
956
966
  this.shards[shardIndex(queue)].setDlqConfig(queue, config);
967
+ this.persistQueueState(queue);
957
968
  }
958
969
  getDlqConfig(queue) {
959
970
  return this.shards[shardIndex(queue)].getDlqConfig(queue);
@@ -1290,11 +1301,15 @@ export class QueueManager {
1290
1301
  if (!childJob.parentId)
1291
1302
  return;
1292
1303
  if (childJob.continueParentOnFailure) {
1293
- this.continueParentOnChildFailure(childJob, error).catch(() => { });
1304
+ this.continueParentOnChildFailure(childJob, error).catch(() => {
1305
+ // Best-effort parent propagation; child failure is already durable.
1306
+ });
1294
1307
  }
1295
1308
  else {
1296
1309
  // removeDependencyOnFailure or ignoreDependencyOnFailure
1297
- this.removeChildFromParentDeps(childJob, error, childJob.ignoreDependencyOnFailure).catch(() => { });
1310
+ this.removeChildFromParentDeps(childJob, error, childJob.ignoreDependencyOnFailure).catch(() => {
1311
+ // Best-effort parent propagation; child failure is already durable.
1312
+ });
1298
1313
  }
1299
1314
  }
1300
1315
  /**
@@ -3,10 +3,11 @@
3
3
  * Connects to bunQ server and executes commands (msgpack binary protocol)
4
4
  */
5
5
  import { formatOutput, formatError } from './output';
6
- import { pack, unpack } from 'msgpackr';
7
6
  import { FrameParser, FrameSizeError } from '../infrastructure/server/protocol';
8
7
  import { CommandError } from './commands/types';
9
8
  import { buildClientTls } from '../client/tcp/connection';
9
+ import { buildCommand } from './commandRouter';
10
+ import { decodeMessagePack, encodeMessagePack } from '../shared/msgpack';
10
11
  /**
11
12
  * Client-side timeout for a command: base 30s; ONLY the long-poll commands
12
13
  * (PULL/WaitJob, where `timeout` means "how long the server may hold the
@@ -21,7 +22,7 @@ export function clientTimeoutFor(cmd) {
21
22
  return Math.max(30000, cmdTimeout + 10000);
22
23
  }
23
24
  /** Send a command and wait for response */
24
- async function sendCommand(socket, command, timeoutMs = 30000) {
25
+ function sendCommand(socket, command, timeoutMs = 30000) {
25
26
  return new Promise((resolve, reject) => {
26
27
  const timeoutId = setTimeout(() => {
27
28
  socket.data.resolve = null;
@@ -36,11 +37,11 @@ async function sendCommand(socket, command, timeoutMs = 30000) {
36
37
  clearTimeout(timeoutId);
37
38
  reject(error);
38
39
  };
39
- socket.write(FrameParser.frame(pack(command)));
40
+ socket.write(FrameParser.frame(encodeMessagePack(command)));
40
41
  });
41
42
  }
42
43
  /** Create TCP connection */
43
- async function connect(options) {
44
+ function connect(options) {
44
45
  return new Promise((resolve, reject) => {
45
46
  const socketData = {
46
47
  frameParser: new FrameParser(),
@@ -73,7 +74,7 @@ async function connect(options) {
73
74
  for (const frame of frames) {
74
75
  if (socketData.resolve) {
75
76
  try {
76
- const response = unpack(frame);
77
+ const response = decodeMessagePack(frame);
77
78
  socketData.resolve(response);
78
79
  socketData.resolve = null;
79
80
  socketData.reject = null;
@@ -200,45 +201,3 @@ export async function executeCommand(command, args, options) {
200
201
  connection?.close();
201
202
  }
202
203
  }
203
- /** Build a command from CLI arguments */
204
- async function buildCommand(command, args) {
205
- // Import command builders
206
- const { buildCoreCommand } = await import('./commands/core');
207
- const { buildJobCommand } = await import('./commands/job');
208
- const { buildQueueCommand } = await import('./commands/queue');
209
- const { buildDlqCommand } = await import('./commands/dlq');
210
- const { buildCronCommand } = await import('./commands/cron');
211
- const { buildWorkerCommand } = await import('./commands/worker');
212
- const { buildWebhookCommand } = await import('./commands/webhook');
213
- const { buildRateLimitCommand } = await import('./commands/rateLimit');
214
- const { buildMonitorCommand } = await import('./commands/monitor');
215
- switch (command) {
216
- case 'push':
217
- case 'pull':
218
- case 'ack':
219
- case 'fail':
220
- return buildCoreCommand(command, args);
221
- case 'job':
222
- return buildJobCommand(args);
223
- case 'queue':
224
- return buildQueueCommand(args);
225
- case 'dlq':
226
- return buildDlqCommand(args);
227
- case 'cron':
228
- return buildCronCommand(args);
229
- case 'worker':
230
- return buildWorkerCommand(args);
231
- case 'webhook':
232
- return buildWebhookCommand(args);
233
- case 'rate-limit':
234
- case 'concurrency':
235
- return buildRateLimitCommand(command, args);
236
- case 'stats':
237
- case 'metrics':
238
- case 'health':
239
- case 'ping':
240
- return buildMonitorCommand(command);
241
- default:
242
- return null;
243
- }
244
- }
@@ -0,0 +1,27 @@
1
+ /** Canonical CLI command surface used by routing, help conformance, and tests. */
2
+ export declare const CLI_COMMAND_SURFACE: {
3
+ readonly push: readonly [];
4
+ readonly pull: readonly [];
5
+ readonly ack: readonly [];
6
+ readonly fail: readonly [];
7
+ readonly job: readonly ["get", "state", "result", "cancel", "progress", "update", "priority", "promote", "delay", "discard", "logs", "log", "wait"];
8
+ readonly queue: readonly ["list", "pause", "resume", "drain", "obliterate", "clean", "count", "jobs", "paused"];
9
+ readonly 'rate-limit': readonly ["set", "clear"];
10
+ readonly concurrency: readonly ["set", "clear"];
11
+ readonly dlq: readonly ["list", "retry", "purge"];
12
+ readonly cron: readonly ["list", "add", "delete"];
13
+ readonly worker: readonly ["list", "register", "unregister"];
14
+ readonly webhook: readonly ["list", "add", "remove"];
15
+ readonly stats: readonly [];
16
+ readonly metrics: readonly [];
17
+ readonly health: readonly [];
18
+ readonly ping: readonly [];
19
+ };
20
+ export type CliNetworkCommand = keyof typeof CLI_COMMAND_SURFACE;
21
+ /** Commands handled locally instead of being translated to a TCP command. */
22
+ export declare const CLI_LOCAL_COMMAND_SURFACE: {
23
+ readonly start: readonly [];
24
+ readonly version: readonly [];
25
+ readonly doctor: readonly [];
26
+ readonly backup: readonly ["now", "create", "list", "restore", "status"];
27
+ };
@@ -0,0 +1,40 @@
1
+ /** Canonical CLI command surface used by routing, help conformance, and tests. */
2
+ export const CLI_COMMAND_SURFACE = {
3
+ push: [],
4
+ pull: [],
5
+ ack: [],
6
+ fail: [],
7
+ job: [
8
+ 'get',
9
+ 'state',
10
+ 'result',
11
+ 'cancel',
12
+ 'progress',
13
+ 'update',
14
+ 'priority',
15
+ 'promote',
16
+ 'delay',
17
+ 'discard',
18
+ 'logs',
19
+ 'log',
20
+ 'wait',
21
+ ],
22
+ queue: ['list', 'pause', 'resume', 'drain', 'obliterate', 'clean', 'count', 'jobs', 'paused'],
23
+ 'rate-limit': ['set', 'clear'],
24
+ concurrency: ['set', 'clear'],
25
+ dlq: ['list', 'retry', 'purge'],
26
+ cron: ['list', 'add', 'delete'],
27
+ worker: ['list', 'register', 'unregister'],
28
+ webhook: ['list', 'add', 'remove'],
29
+ stats: [],
30
+ metrics: [],
31
+ health: [],
32
+ ping: [],
33
+ };
34
+ /** Commands handled locally instead of being translated to a TCP command. */
35
+ export const CLI_LOCAL_COMMAND_SURFACE = {
36
+ start: [],
37
+ version: [],
38
+ doctor: [],
39
+ backup: ['now', 'create', 'list', 'restore', 'status'],
40
+ };
@@ -0,0 +1,3 @@
1
+ export declare const CLI_NETWORK_COMMANDS: readonly ("push" | "queue" | "dlq" | "job" | "cron" | "worker" | "concurrency" | "ping" | "stats" | "metrics" | "pull" | "ack" | "fail" | "rate-limit" | "health" | "webhook")[];
2
+ /** Build the protocol command for a routed network CLI command. */
3
+ export declare function buildCommand(command: string, args: string[]): Promise<Record<string, unknown> | null>;
@@ -0,0 +1,44 @@
1
+ import { CLI_COMMAND_SURFACE } from './commandRegistry';
2
+ export const CLI_NETWORK_COMMANDS = Object.freeze(Object.keys(CLI_COMMAND_SURFACE));
3
+ /** Build the protocol command for a routed network CLI command. */
4
+ export async function buildCommand(command, args) {
5
+ if (!CLI_NETWORK_COMMANDS.includes(command))
6
+ return null;
7
+ const { buildCoreCommand } = await import('./commands/core');
8
+ const { buildJobCommand } = await import('./commands/job');
9
+ const { buildQueueCommand } = await import('./commands/queue');
10
+ const { buildDlqCommand } = await import('./commands/dlq');
11
+ const { buildCronCommand } = await import('./commands/cron');
12
+ const { buildWorkerCommand } = await import('./commands/worker');
13
+ const { buildWebhookCommand } = await import('./commands/webhook');
14
+ const { buildRateLimitCommand } = await import('./commands/rateLimit');
15
+ const { buildMonitorCommand } = await import('./commands/monitor');
16
+ switch (command) {
17
+ case 'push':
18
+ case 'pull':
19
+ case 'ack':
20
+ case 'fail':
21
+ return buildCoreCommand(command, args);
22
+ case 'job':
23
+ return buildJobCommand(args);
24
+ case 'queue':
25
+ return buildQueueCommand(args);
26
+ case 'dlq':
27
+ return buildDlqCommand(args);
28
+ case 'cron':
29
+ return buildCronCommand(args);
30
+ case 'worker':
31
+ return buildWorkerCommand(args);
32
+ case 'webhook':
33
+ return buildWebhookCommand(args);
34
+ case 'rate-limit':
35
+ case 'concurrency':
36
+ return buildRateLimitCommand(command, args);
37
+ case 'stats':
38
+ case 'metrics':
39
+ case 'health':
40
+ case 'ping':
41
+ return buildMonitorCommand(command);
42
+ }
43
+ return null;
44
+ }
@@ -42,7 +42,7 @@ export async function executeBackupCommand(args) {
42
42
  case 'status':
43
43
  return executeBackupStatus(manager);
44
44
  default:
45
- throw new CommandError(`Unknown backup subcommand: ${subcommand}. Use: now, list, restore, status`);
45
+ throw new CommandError(`Unknown backup subcommand: ${subcommand}. Use: now, create, list, restore, status`);
46
46
  }
47
47
  }
48
48
  async function executeBackupNow(manager) {
@@ -4,6 +4,56 @@
4
4
  */
5
5
  import { parseArgs } from 'node:util';
6
6
  import { CommandError, requireArg, parseJsonArg, parseNumberArg, parseBigIntArg } from './types';
7
+ const PUSH_VALUE_FLAGS = new Set([
8
+ '--priority',
9
+ '-P',
10
+ '--delay',
11
+ '-d',
12
+ '--max-attempts',
13
+ '--backoff',
14
+ '--ttl',
15
+ '--timeout',
16
+ '--unique-key',
17
+ '-u',
18
+ '--job-id',
19
+ '--depends-on',
20
+ '--tags',
21
+ '--group-id',
22
+ '-g',
23
+ ]);
24
+ function protectNegativeJsonData(args) {
25
+ const protectedArgs = [...args];
26
+ const restore = new Map();
27
+ let positionals = 0;
28
+ for (let index = 0; index < args.length; index++) {
29
+ const arg = args[index];
30
+ if (arg === '--')
31
+ break;
32
+ if (PUSH_VALUE_FLAGS.has(arg)) {
33
+ index++;
34
+ continue;
35
+ }
36
+ if (arg.startsWith('-')) {
37
+ if (positionals !== 1)
38
+ continue;
39
+ try {
40
+ if (typeof JSON.parse(arg) !== 'number')
41
+ continue;
42
+ }
43
+ catch {
44
+ continue;
45
+ }
46
+ const sentinel = `bunqueue-negative-json-${index}`;
47
+ protectedArgs[index] = sentinel;
48
+ restore.set(sentinel, arg);
49
+ positionals++;
50
+ continue;
51
+ }
52
+ if (!arg.startsWith('--') && !/^-[a-zA-Z]/.test(arg))
53
+ positionals++;
54
+ }
55
+ return { args: protectedArgs, restore };
56
+ }
7
57
  /** Build a core command from CLI args */
8
58
  export function buildCoreCommand(command, args) {
9
59
  switch (command) {
@@ -21,8 +71,9 @@ export function buildCoreCommand(command, args) {
21
71
  }
22
72
  /** Build PUSH command */
23
73
  function buildPush(args) {
74
+ const protectedInput = protectNegativeJsonData(args);
24
75
  const { values, positionals } = parseArgs({
25
- args,
76
+ args: protectedInput.args,
26
77
  options: {
27
78
  priority: { type: 'string', short: 'P' },
28
79
  delay: { type: 'string', short: 'd' },
@@ -43,7 +94,8 @@ function buildPush(args) {
43
94
  strict: false,
44
95
  });
45
96
  const queue = requireArg(positionals, 0, 'queue');
46
- const data = parseJsonArg(requireArg(positionals, 1, 'data'), 'data');
97
+ const protectedData = requireArg(positionals, 1, 'data');
98
+ const data = parseJsonArg(protectedInput.restore.get(protectedData) ?? protectedData, 'data');
47
99
  const cmd = {
48
100
  cmd: 'PUSH',
49
101
  queue,
@@ -1,5 +1,50 @@
1
- /**
2
- * bunqueue doctor - Diagnostic command
3
- * Checks client/server version, connectivity, health, and queue state
4
- */
5
- export declare function runDoctor(host: string, httpPort: number): Promise<void>;
1
+ export interface HealthData {
2
+ version?: string;
3
+ status?: string;
4
+ uptime?: number;
5
+ queues?: {
6
+ waiting?: number;
7
+ active?: number;
8
+ delayed?: number;
9
+ completed?: number;
10
+ dlq?: number;
11
+ };
12
+ connections?: {
13
+ tcp?: number;
14
+ ws?: number;
15
+ sse?: number;
16
+ };
17
+ memory?: {
18
+ heapUsed?: number;
19
+ heapTotal?: number;
20
+ rss?: number;
21
+ };
22
+ }
23
+ export type DoctorInput = {
24
+ kind: 'ok';
25
+ endpoint: string;
26
+ health: HealthData;
27
+ } | {
28
+ kind: 'http-error';
29
+ endpoint: string;
30
+ status: number;
31
+ } | {
32
+ kind: 'network-error';
33
+ endpoint: string;
34
+ message: string;
35
+ };
36
+ export interface DoctorReport {
37
+ clientVersion: string;
38
+ endpoint: string;
39
+ reachable: boolean;
40
+ health?: HealthData;
41
+ error?: string;
42
+ issues: number;
43
+ fatal: boolean;
44
+ exitCode: 0 | 1;
45
+ }
46
+ export declare function fetchDoctorHealth(host: string, httpPort: number): Promise<DoctorInput>;
47
+ /** Evaluate diagnostics without performing I/O or reading clocks. */
48
+ export declare function evaluateDoctor(input: DoctorInput): DoctorReport;
49
+ export declare function formatDoctorText(report: DoctorReport): string;
50
+ export declare function runDoctor(host: string, httpPort: number): Promise<DoctorReport>;