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.
- package/dist/application/backgroundTasks.js +3 -0
- package/dist/application/operations/jobManagement.js +7 -4
- package/dist/application/operations/jobStateTransitions.js +1 -0
- package/dist/application/operations/pull.d.ts +2 -2
- package/dist/application/operations/pull.js +43 -10
- package/dist/application/queueManager.d.ts +4 -4
- package/dist/application/queueManager.js +26 -11
- package/dist/cli/client.js +6 -47
- package/dist/cli/commandRegistry.d.ts +27 -0
- package/dist/cli/commandRegistry.js +40 -0
- package/dist/cli/commandRouter.d.ts +3 -0
- package/dist/cli/commandRouter.js +44 -0
- package/dist/cli/commands/backup.js +1 -1
- package/dist/cli/commands/core.js +54 -2
- package/dist/cli/commands/doctor.d.ts +50 -5
- package/dist/cli/commands/doctor.js +125 -108
- package/dist/cli/commands/types.js +5 -1
- package/dist/cli/globalOptions.d.ts +16 -0
- package/dist/cli/globalOptions.js +192 -0
- package/dist/cli/help.d.ts +9 -0
- package/dist/cli/help.js +42 -21
- package/dist/cli/index.d.ts +1 -25
- package/dist/cli/index.js +84 -348
- package/dist/cli/localOutput.d.ts +22 -0
- package/dist/cli/localOutput.js +45 -0
- package/dist/client/queue/dlqOps.js +1 -3
- package/dist/client/tcp/client.js +10 -8
- package/dist/domain/queue/shard.d.ts +2 -2
- package/dist/domain/queue/shard.js +3 -3
- package/dist/domain/queue/waiterManager.d.ts +2 -2
- package/dist/domain/queue/waiterManager.js +26 -5
- package/dist/infrastructure/cloud/wsSender.js +4 -4
- package/dist/infrastructure/persistence/schema.d.ts +2 -2
- package/dist/infrastructure/persistence/schema.js +7 -2
- package/dist/infrastructure/persistence/sqlite.d.ts +5 -1
- package/dist/infrastructure/persistence/sqlite.js +10 -2
- package/dist/infrastructure/persistence/sqliteSerializer.js +1 -1
- package/dist/infrastructure/persistence/statements.d.ts +2 -1
- package/dist/infrastructure/persistence/statements.js +3 -2
- package/dist/infrastructure/server/handlers/core.js +4 -4
- package/dist/infrastructure/server/tcp.d.ts +1 -0
- package/dist/infrastructure/server/tcp.js +9 -5
- package/dist/infrastructure/server/types.d.ts +2 -0
- package/dist/shared/msgpack.d.ts +3 -0
- package/dist/shared/msgpack.js +70 -0
- 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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
274
|
-
|
|
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
|
/**
|
package/dist/cli/client.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
40
|
+
socket.write(FrameParser.frame(encodeMessagePack(command)));
|
|
40
41
|
});
|
|
41
42
|
}
|
|
42
43
|
/** Create TCP connection */
|
|
43
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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>;
|