bunqueue 2.8.39 → 2.8.41

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 (48) 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 +5 -26
  5. package/dist/application/operations/pull.js +68 -179
  6. package/dist/application/operations/pullStateTransition.d.ts +50 -0
  7. package/dist/application/operations/pullStateTransition.js +114 -0
  8. package/dist/application/queueManager.d.ts +4 -4
  9. package/dist/application/queueManager.js +26 -11
  10. package/dist/cli/client.js +6 -47
  11. package/dist/cli/commandRegistry.d.ts +27 -0
  12. package/dist/cli/commandRegistry.js +40 -0
  13. package/dist/cli/commandRouter.d.ts +3 -0
  14. package/dist/cli/commandRouter.js +44 -0
  15. package/dist/cli/commands/backup.js +1 -1
  16. package/dist/cli/commands/core.js +54 -2
  17. package/dist/cli/commands/doctor.d.ts +50 -5
  18. package/dist/cli/commands/doctor.js +125 -108
  19. package/dist/cli/commands/types.js +5 -1
  20. package/dist/cli/globalOptions.d.ts +16 -0
  21. package/dist/cli/globalOptions.js +192 -0
  22. package/dist/cli/help.d.ts +9 -0
  23. package/dist/cli/help.js +42 -21
  24. package/dist/cli/index.d.ts +1 -25
  25. package/dist/cli/index.js +84 -348
  26. package/dist/cli/localOutput.d.ts +22 -0
  27. package/dist/cli/localOutput.js +45 -0
  28. package/dist/client/queue/dlqOps.js +1 -3
  29. package/dist/client/tcp/client.js +10 -8
  30. package/dist/domain/queue/shard.d.ts +2 -2
  31. package/dist/domain/queue/shard.js +3 -3
  32. package/dist/domain/queue/waiterManager.d.ts +2 -2
  33. package/dist/domain/queue/waiterManager.js +26 -5
  34. package/dist/infrastructure/cloud/wsSender.js +4 -4
  35. package/dist/infrastructure/persistence/schema.d.ts +2 -2
  36. package/dist/infrastructure/persistence/schema.js +7 -2
  37. package/dist/infrastructure/persistence/sqlite.d.ts +5 -1
  38. package/dist/infrastructure/persistence/sqlite.js +10 -2
  39. package/dist/infrastructure/persistence/sqliteSerializer.js +1 -1
  40. package/dist/infrastructure/persistence/statements.d.ts +2 -1
  41. package/dist/infrastructure/persistence/statements.js +3 -2
  42. package/dist/infrastructure/server/handlers/core.js +4 -4
  43. package/dist/infrastructure/server/tcp.d.ts +1 -0
  44. package/dist/infrastructure/server/tcp.js +9 -5
  45. package/dist/infrastructure/server/types.d.ts +2 -0
  46. package/dist/shared/msgpack.d.ts +3 -0
  47. package/dist/shared/msgpack.js +70 -0
  48. package/package.json +1 -1
@@ -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>;