bunqueue 2.8.38 → 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 (64) hide show
  1. package/dist/application/backgroundTasks.js +9 -0
  2. package/dist/application/cleanupTasks.js +31 -11
  3. package/dist/application/contextFactory.d.ts +3 -0
  4. package/dist/application/contextFactory.js +6 -0
  5. package/dist/application/dependencyResultTracker.d.ts +17 -0
  6. package/dist/application/dependencyResultTracker.js +71 -0
  7. package/dist/application/operations/ack.d.ts +2 -0
  8. package/dist/application/operations/ack.js +6 -0
  9. package/dist/application/operations/ackHelpers.d.ts +2 -1
  10. package/dist/application/operations/ackHelpers.js +6 -0
  11. package/dist/application/operations/jobManagement.d.ts +2 -0
  12. package/dist/application/operations/jobManagement.js +10 -4
  13. package/dist/application/operations/jobStateTransitions.js +1 -0
  14. package/dist/application/operations/pull.d.ts +2 -2
  15. package/dist/application/operations/pull.js +43 -10
  16. package/dist/application/operations/push.d.ts +2 -0
  17. package/dist/application/operations/pushInsert.d.ts +4 -0
  18. package/dist/application/operations/pushInsert.js +6 -0
  19. package/dist/application/operations/queryOperations.d.ts +2 -0
  20. package/dist/application/operations/queryOperations.js +5 -1
  21. package/dist/application/operations/queueControl.d.ts +2 -0
  22. package/dist/application/operations/queueControl.js +4 -0
  23. package/dist/application/queueManager.d.ts +5 -4
  24. package/dist/application/queueManager.js +37 -11
  25. package/dist/application/types.d.ts +2 -0
  26. package/dist/cli/client.js +6 -47
  27. package/dist/cli/commandRegistry.d.ts +27 -0
  28. package/dist/cli/commandRegistry.js +40 -0
  29. package/dist/cli/commandRouter.d.ts +3 -0
  30. package/dist/cli/commandRouter.js +44 -0
  31. package/dist/cli/commands/backup.js +1 -1
  32. package/dist/cli/commands/core.js +54 -2
  33. package/dist/cli/commands/doctor.d.ts +50 -5
  34. package/dist/cli/commands/doctor.js +125 -108
  35. package/dist/cli/commands/types.js +5 -1
  36. package/dist/cli/globalOptions.d.ts +16 -0
  37. package/dist/cli/globalOptions.js +192 -0
  38. package/dist/cli/help.d.ts +9 -0
  39. package/dist/cli/help.js +42 -21
  40. package/dist/cli/index.d.ts +1 -25
  41. package/dist/cli/index.js +84 -348
  42. package/dist/cli/localOutput.d.ts +22 -0
  43. package/dist/cli/localOutput.js +45 -0
  44. package/dist/client/queue/dlqOps.js +1 -3
  45. package/dist/client/tcp/client.js +10 -8
  46. package/dist/domain/queue/shard.d.ts +2 -2
  47. package/dist/domain/queue/shard.js +3 -3
  48. package/dist/domain/queue/waiterManager.d.ts +2 -2
  49. package/dist/domain/queue/waiterManager.js +26 -5
  50. package/dist/infrastructure/cloud/wsSender.js +4 -4
  51. package/dist/infrastructure/persistence/schema.d.ts +2 -2
  52. package/dist/infrastructure/persistence/schema.js +7 -2
  53. package/dist/infrastructure/persistence/sqlite.d.ts +5 -1
  54. package/dist/infrastructure/persistence/sqlite.js +10 -2
  55. package/dist/infrastructure/persistence/sqliteSerializer.js +1 -1
  56. package/dist/infrastructure/persistence/statements.d.ts +2 -1
  57. package/dist/infrastructure/persistence/statements.js +3 -2
  58. package/dist/infrastructure/server/handlers/core.js +4 -4
  59. package/dist/infrastructure/server/tcp.d.ts +1 -0
  60. package/dist/infrastructure/server/tcp.js +23 -12
  61. package/dist/infrastructure/server/types.d.ts +2 -0
  62. package/dist/shared/msgpack.d.ts +3 -0
  63. package/dist/shared/msgpack.js +70 -0
  64. package/package.json +1 -1
@@ -31,6 +31,7 @@ export declare class QueueManager {
31
31
  private readonly depCompletions;
32
32
  private readonly timedOutJobs;
33
33
  private readonly jobResults;
34
+ private readonly dependencyResults;
34
35
  private readonly customIdMap;
35
36
  private readonly jobLogs;
36
37
  private readonly pendingDepChecks;
@@ -64,13 +65,13 @@ export declare class QueueManager {
64
65
  private handleRepeat;
65
66
  push(queue: string, input: JobInput): Promise<Job>;
66
67
  pushBatch(queue: string, inputs: JobInput[]): Promise<JobId[]>;
67
- pull(queue: string, timeoutMs?: number): Promise<Job | null>;
68
- 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<{
69
70
  job: Job | null;
70
71
  token: string | null;
71
72
  }>;
72
- pullBatch(queue: string, count: number, timeoutMs?: number): Promise<Job[]>;
73
- 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<{
74
75
  jobs: Job[];
75
76
  tokens: string[];
76
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';
@@ -32,6 +33,7 @@ import * as statsMgr from './statsManager';
32
33
  import { ContextFactory } from './contextFactory';
33
34
  import { processPendingDependencies } from './dependencyProcessor';
34
35
  import { handleTaskError, handleTaskSuccess } from './taskErrorTracking';
36
+ import { DependencyResultTracker } from './dependencyResultTracker';
35
37
  /**
36
38
  * QueueManager - Central coordinator
37
39
  */
@@ -58,6 +60,7 @@ export class QueueManager {
58
60
  // current token and bypasses the stale-token recovery path entirely.
59
61
  timedOutJobs;
60
62
  jobResults;
63
+ dependencyResults = new DependencyResultTracker();
61
64
  customIdMap;
62
65
  jobLogs;
63
66
  // Deferred dependency resolution queue
@@ -178,6 +181,7 @@ export class QueueManager {
178
181
  depCompletions: this.depCompletions,
179
182
  timedOutJobs: this.timedOutJobs,
180
183
  jobResults: this.jobResults,
184
+ dependencyResults: this.dependencyResults,
181
185
  customIdMap: this.customIdMap,
182
186
  jobLogs: this.jobLogs,
183
187
  jobLocks: this.jobLocks,
@@ -254,21 +258,22 @@ export class QueueManager {
254
258
  this.registerQueueName(queue);
255
259
  return pushJobBatch(queue, inputs, this.contextFactory.getPushContext());
256
260
  }
257
- async pull(queue, timeoutMs = 0) {
258
- return pullJob(queue, timeoutMs, this.contextFactory.getPullContext());
261
+ async pull(queue, timeoutMs = 0, signal) {
262
+ return pullJob(queue, timeoutMs, this.contextFactory.getPullContext(), signal);
259
263
  }
260
- async pullWithLock(queue, owner, timeoutMs = 0, lockTtl = DEFAULT_LOCK_TTL) {
261
- 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);
262
266
  if (!job)
263
267
  return { job: null, token: null };
264
268
  const token = lockMgr.createLock(job.id, owner, this.contextFactory.getLockContext(), lockTtl);
265
269
  return { job, token };
266
270
  }
267
- async pullBatch(queue, count, timeoutMs = 0) {
268
- 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);
269
273
  }
270
- async pullBatchWithLock(queue, count, owner, timeoutMs = 0, lockTtl = DEFAULT_LOCK_TTL) {
271
- 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);
272
277
  const tokens = [];
273
278
  for (const job of jobs) {
274
279
  const token = lockMgr.createLock(job.id, owner, this.contextFactory.getLockContext(), lockTtl);
@@ -541,6 +546,9 @@ export class QueueManager {
541
546
  ctx.jobIndex.delete(jId);
542
547
  ctx.storage?.deleteJob(jId);
543
548
  }
549
+ if (result !== undefined)
550
+ ctx.dependencyResults.retain(jId, result);
551
+ ctx.dependencyResults.releaseConsumer(jId);
544
552
  ctx.totalCompleted.value++;
545
553
  ctx.broadcast({
546
554
  eventType: 'completed',
@@ -769,6 +777,7 @@ export class QueueManager {
769
777
  this.jobResults.delete(jid);
770
778
  this.jobLogs.delete(jid);
771
779
  this.jobLocks.delete(jid);
780
+ this.dependencyResults.releaseConsumer(jid);
772
781
  this.failedChildrenValues.delete(jid);
773
782
  this.ignoredChildrenFailures.delete(jid);
774
783
  this.pendingDepChecks.delete(jid);
@@ -887,10 +896,16 @@ export class QueueManager {
887
896
  return;
888
897
  const state = this.shards[shardIndex(queue)].getState(queue);
889
898
  const stallConfig = this.shards[shardIndex(queue)].getStallConfig(queue);
899
+ const dlqConfig = this.shards[shardIndex(queue)].getDlqConfig(queue);
890
900
  const hasCustomStallConfig = stallConfig.enabled !== DEFAULT_STALL_CONFIG.enabled ||
891
901
  stallConfig.stallInterval !== DEFAULT_STALL_CONFIG.stallInterval ||
892
902
  stallConfig.maxStalls !== DEFAULT_STALL_CONFIG.maxStalls ||
893
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;
894
909
  // When control-state returns fully to default (not paused, no limits), drop
895
910
  // the row instead of persisting an all-default placeholder. Keeps the table
896
911
  // free of noise rows for ephemeral queues that only ever call resume/clear*,
@@ -898,7 +913,8 @@ export class QueueManager {
898
913
  if (!state.paused &&
899
914
  state.rateLimit === null &&
900
915
  state.concurrencyLimit === null &&
901
- !hasCustomStallConfig) {
916
+ !hasCustomStallConfig &&
917
+ !hasCustomDlqConfig) {
902
918
  this.storage.deleteQueueState(queue);
903
919
  return;
904
920
  }
@@ -909,6 +925,7 @@ export class QueueManager {
909
925
  rateLimitDuration: state.rateLimitDuration,
910
926
  rateLimitExpiresAt: state.rateLimitExpiresAt,
911
927
  stallConfig,
928
+ dlqConfig,
912
929
  });
913
930
  }
914
931
  /** Get rate limit and concurrency limit for a queue */
@@ -947,6 +964,7 @@ export class QueueManager {
947
964
  }
948
965
  setDlqConfig(queue, config) {
949
966
  this.shards[shardIndex(queue)].setDlqConfig(queue, config);
967
+ this.persistQueueState(queue);
950
968
  }
951
969
  getDlqConfig(queue) {
952
970
  return this.shards[shardIndex(queue)].getDlqConfig(queue);
@@ -1259,6 +1277,7 @@ export class QueueManager {
1259
1277
  // Parent reached a terminal (DLQ) state — release its flow-failure tracking.
1260
1278
  this.failedChildrenValues.delete(parentId);
1261
1279
  this.ignoredChildrenFailures.delete(parentId);
1280
+ this.dependencyResults.releaseConsumer(parentId);
1262
1281
  // Broadcast failed event for parent
1263
1282
  this.eventsManager.broadcast({
1264
1283
  eventType: 'failed',
@@ -1282,11 +1301,15 @@ export class QueueManager {
1282
1301
  if (!childJob.parentId)
1283
1302
  return;
1284
1303
  if (childJob.continueParentOnFailure) {
1285
- this.continueParentOnChildFailure(childJob, error).catch(() => { });
1304
+ this.continueParentOnChildFailure(childJob, error).catch(() => {
1305
+ // Best-effort parent propagation; child failure is already durable.
1306
+ });
1286
1307
  }
1287
1308
  else {
1288
1309
  // removeDependencyOnFailure or ignoreDependencyOnFailure
1289
- 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
+ });
1290
1313
  }
1291
1314
  }
1292
1315
  /**
@@ -1396,6 +1419,7 @@ export class QueueManager {
1396
1419
  if (depIndex !== -1) {
1397
1420
  parentJob.dependsOn.splice(depIndex, 1);
1398
1421
  shard.unregisterDependencies(parentId, [childJob.id]);
1422
+ this.dependencyResults.releaseDependency(parentId, childJob.id);
1399
1423
  }
1400
1424
  // If no more pending deps, the parent is ready to be promoted
1401
1425
  readyToPromote =
@@ -1449,6 +1473,7 @@ export class QueueManager {
1449
1473
  if (depIndex !== -1) {
1450
1474
  parentJob.dependsOn.splice(depIndex, 1);
1451
1475
  shard.unregisterDependencies(parentId, [childJobId]);
1476
+ this.dependencyResults.releaseDependency(parentId, childJobId);
1452
1477
  }
1453
1478
  const allDone = parentJob.dependsOn.length === 0 ||
1454
1479
  parentJob.dependsOn.every((dep) => this.completedJobs.has(dep));
@@ -1615,6 +1640,7 @@ export class QueueManager {
1615
1640
  this.depCompletions.clear();
1616
1641
  this.timedOutJobs.clear();
1617
1642
  this.jobResults.clear();
1643
+ this.dependencyResults.clear();
1618
1644
  this.jobLogs.clear();
1619
1645
  this.customIdMap.clear();
1620
1646
  this.pendingDepChecks.clear();
@@ -12,6 +12,7 @@ import type { EventsManager } from './eventsManager';
12
12
  import type { WebhookManager } from './webhookManager';
13
13
  import type { WorkerManager } from './workerManager';
14
14
  import type { MonitoringState } from './monitoringChecks';
15
+ import type { DependencyResultTracker } from './dependencyResultTracker';
15
16
  /** Queue Manager configuration */
16
17
  export interface QueueManagerConfig {
17
18
  dataPath?: string;
@@ -52,6 +53,7 @@ export interface QueueManagerState {
52
53
  readonly jobIndex: Map<JobId, JobLocation>;
53
54
  readonly completedJobs: BoundedSet<JobId>;
54
55
  readonly jobResults: LRUMap<JobId, unknown>;
56
+ readonly dependencyResults: DependencyResultTracker;
55
57
  readonly customIdMap: LRUMap<string, JobId>;
56
58
  readonly jobLogs: LRUMap<JobId, JobLogEntry[]>;
57
59
  readonly jobLocks: Map<JobId, JobLock>;
@@ -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>;