bunqueue 2.8.6 → 2.8.8
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/operations/ack.d.ts +1 -1
- package/dist/application/operations/ack.js +2 -2
- package/dist/application/queueManager.d.ts +1 -1
- package/dist/application/queueManager.js +2 -2
- package/dist/application/statsManager.js +18 -8
- package/dist/cli/client.d.ts +3 -0
- package/dist/cli/client.js +13 -2
- package/dist/cli/commands/server.js +26 -2
- package/dist/cli/help.js +5 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +53 -1
- package/dist/client/queue/dlq.js +1 -1
- package/dist/client/queue/operations/management.js +4 -2
- package/dist/client/queue/queue.js +2 -0
- package/dist/client/queue/scheduler.js +5 -0
- package/dist/client/tcp/client.js +1 -0
- package/dist/client/tcp/connection.d.ts +8 -1
- package/dist/client/tcp/connection.js +27 -1
- package/dist/client/tcp/index.d.ts +1 -1
- package/dist/client/tcp/shared.d.ts +6 -4
- package/dist/client/tcp/shared.js +27 -11
- package/dist/client/tcp/types.d.ts +13 -0
- package/dist/client/tcp/types.js +1 -0
- package/dist/client/tcpPool.js +11 -1
- package/dist/client/types.d.ts +8 -0
- package/dist/client/worker/worker.js +7 -2
- package/dist/client/worker/workerPull.d.ts +2 -0
- package/dist/client/worker/workerPull.js +12 -5
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +1 -1
- package/dist/config/resolve.d.ts +14 -0
- package/dist/config/resolve.js +19 -0
- package/dist/config/types.d.ts +4 -0
- package/dist/domain/types/command.d.ts +4 -0
- package/dist/infrastructure/server/handlers/advanced.js +60 -8
- package/dist/infrastructure/server/handlers/core.js +1 -1
- package/dist/infrastructure/server/handlers/cron.js +1 -0
- package/dist/infrastructure/server/handlers/monitoring.js +7 -2
- package/dist/infrastructure/server/http.d.ts +3 -0
- package/dist/infrastructure/server/http.js +30 -8
- package/dist/infrastructure/server/httpRouteJobs.js +14 -2
- package/dist/infrastructure/server/httpRouteQueueConfig.js +19 -3
- package/dist/infrastructure/server/httpRouteQueues.js +13 -1
- package/dist/infrastructure/server/httpRouteResources.js +4 -0
- package/dist/infrastructure/server/tcp.d.ts +6 -0
- package/dist/infrastructure/server/tcp.js +5 -1
- package/dist/infrastructure/server/tls.d.ts +21 -0
- package/dist/infrastructure/server/tls.js +19 -0
- package/dist/main.js +13 -1
- package/package.json +1 -1
|
@@ -57,7 +57,7 @@ export declare function ackJob(jobId: JobId, result: unknown, ctx: AckContext):
|
|
|
57
57
|
/**
|
|
58
58
|
* Mark job as failed
|
|
59
59
|
*/
|
|
60
|
-
export declare function failJob(jobId: JobId, error: string | undefined, ctx: AckContext): Promise<void>;
|
|
60
|
+
export declare function failJob(jobId: JobId, error: string | undefined, ctx: AckContext, unrecoverable?: boolean): Promise<void>;
|
|
61
61
|
/**
|
|
62
62
|
* Acknowledge multiple jobs - optimized batch processing
|
|
63
63
|
* Groups jobs by shard to minimize lock acquisitions: O(shards) instead of O(n)
|
|
@@ -112,7 +112,7 @@ function moveFailedJobToDlq(job, jobId, error, shard, ctx) {
|
|
|
112
112
|
/**
|
|
113
113
|
* Mark job as failed
|
|
114
114
|
*/
|
|
115
|
-
export async function failJob(jobId, error, ctx) {
|
|
115
|
+
export async function failJob(jobId, error, ctx, unrecoverable = false) {
|
|
116
116
|
const procIdx = processingShardIndex(jobId);
|
|
117
117
|
const job = await withWriteLock(ctx.processingLocks[procIdx], () => {
|
|
118
118
|
const job = ctx.processingShards[procIdx].get(jobId);
|
|
@@ -134,7 +134,7 @@ export async function failJob(jobId, error, ctx) {
|
|
|
134
134
|
await withWriteLock(ctx.shardLocks[idx], () => {
|
|
135
135
|
const shard = ctx.shards[idx];
|
|
136
136
|
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId);
|
|
137
|
-
if (canRetry(job)) {
|
|
137
|
+
if (!unrecoverable && canRetry(job)) {
|
|
138
138
|
const now = Date.now();
|
|
139
139
|
job.runAt = now + calculateBackoff(job);
|
|
140
140
|
shard.getQueue(job.queue).push(job);
|
|
@@ -79,7 +79,7 @@ export declare class QueueManager {
|
|
|
79
79
|
result: unknown;
|
|
80
80
|
token?: string;
|
|
81
81
|
}>): Promise<void>;
|
|
82
|
-
fail(jobId: JobId, error?: string, token?: string): Promise<void>;
|
|
82
|
+
fail(jobId: JobId, error?: string, token?: string, unrecoverable?: boolean): Promise<void>;
|
|
83
83
|
/**
|
|
84
84
|
* Check if a failed lock verification is a genuine ownership conflict.
|
|
85
85
|
* If the job is still in processing with a different lock, throw.
|
|
@@ -358,7 +358,7 @@ export class QueueManager {
|
|
|
358
358
|
lockMgr.releaseLock(item.id, lockCtx, item.token);
|
|
359
359
|
}
|
|
360
360
|
}
|
|
361
|
-
async fail(jobId, error, token) {
|
|
361
|
+
async fail(jobId, error, token, unrecoverable = false) {
|
|
362
362
|
const lockCtx = this.contextFactory.getLockContext();
|
|
363
363
|
if (token && !lockMgr.verifyLock(jobId, token, lockCtx)) {
|
|
364
364
|
this.throwIfOwnershipConflict(jobId, lockCtx);
|
|
@@ -367,7 +367,7 @@ export class QueueManager {
|
|
|
367
367
|
return;
|
|
368
368
|
}
|
|
369
369
|
try {
|
|
370
|
-
await failJob(jobId, error, this.contextFactory.getAckContext());
|
|
370
|
+
await failJob(jobId, error, this.contextFactory.getAckContext(), unrecoverable);
|
|
371
371
|
}
|
|
372
372
|
catch (err) {
|
|
373
373
|
// Job removed from processing by stall detection. The stall retry
|
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
* Provides system metrics and memory compaction utilities
|
|
4
4
|
*/
|
|
5
5
|
import { SHARD_COUNT, shardIndex } from '../shared/hash';
|
|
6
|
+
/** Count jobs belonging to `queueName` across one or more job iterables. */
|
|
7
|
+
function countByQueue(sources, queueName) {
|
|
8
|
+
let count = 0;
|
|
9
|
+
for (const src of sources) {
|
|
10
|
+
for (const job of src)
|
|
11
|
+
if (job.queue === queueName)
|
|
12
|
+
count++;
|
|
13
|
+
}
|
|
14
|
+
return count;
|
|
15
|
+
}
|
|
6
16
|
/**
|
|
7
17
|
* Get queue statistics - uses running counters + priority scan
|
|
8
18
|
*/
|
|
@@ -14,7 +24,10 @@ export function getStats(ctx, cronScheduler) {
|
|
|
14
24
|
delayed += shardStats.delayedJobs;
|
|
15
25
|
dlq += shardStats.dlqJobs;
|
|
16
26
|
active += ctx.processingShards[i].size;
|
|
17
|
-
waitingChildren
|
|
27
|
+
// getJobState reports BOTH waitingChildren (flow parents) and waitingDeps
|
|
28
|
+
// (jobs blocked on dependsOn) as state 'waiting-children', and getJobs lists
|
|
29
|
+
// both — so the count must include both or it undercounts vs state/list (#95 class).
|
|
30
|
+
waitingChildren += ctx.shards[i].waitingChildren.size + ctx.shards[i].waitingDeps.size;
|
|
18
31
|
// Scan queues to split waiting vs prioritized (BullMQ v5 compat)
|
|
19
32
|
for (const queue of ctx.shards[i].queues.values()) {
|
|
20
33
|
for (const job of queue.values()) {
|
|
@@ -174,13 +187,10 @@ export function getQueueJobCounts(queueName, ctx) {
|
|
|
174
187
|
}
|
|
175
188
|
// Count failed (DLQ) jobs for this queue
|
|
176
189
|
const failed = shard.getDlq(queueName).length;
|
|
177
|
-
// Count waiting-children jobs
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
waitingChildrenCount++;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
190
|
+
// Count waiting-children jobs. getJobState/getJobs treat BOTH waitingChildren
|
|
191
|
+
// (flow parents) and waitingDeps (jobs blocked on dependsOn) as 'waiting-children',
|
|
192
|
+
// so count both to stay consistent with state/list (#95 class).
|
|
193
|
+
const waitingChildrenCount = countByQueue([shard.waitingChildren.values(), shard.waitingDeps.values()], queueName);
|
|
184
194
|
// Per-queue cumulative counters
|
|
185
195
|
const perQueue = ctx.perQueueMetrics?.get(queueName);
|
|
186
196
|
const totalCompleted = Number(perQueue?.totalCompleted ?? 0n);
|
package/dist/cli/client.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* CLI TCP Client
|
|
3
3
|
* Connects to bunQ server and executes commands (msgpack binary protocol)
|
|
4
4
|
*/
|
|
5
|
+
import type { ClientTlsOptions } from '../client/tcp/types';
|
|
5
6
|
/** Client options */
|
|
6
7
|
export interface ClientOptions {
|
|
7
8
|
/** Server host */
|
|
@@ -10,6 +11,8 @@ export interface ClientOptions {
|
|
|
10
11
|
port: number;
|
|
11
12
|
/** Auth token */
|
|
12
13
|
token?: string;
|
|
14
|
+
/** TLS to the server (true = system CAs, object = custom CA / no-verify) */
|
|
15
|
+
tls?: boolean | ClientTlsOptions;
|
|
13
16
|
/** Output as JSON */
|
|
14
17
|
json: boolean;
|
|
15
18
|
}
|
package/dist/cli/client.js
CHANGED
|
@@ -6,6 +6,7 @@ import { formatOutput, formatError } from './output';
|
|
|
6
6
|
import { pack, unpack } from 'msgpackr';
|
|
7
7
|
import { FrameParser, FrameSizeError } from '../infrastructure/server/protocol';
|
|
8
8
|
import { CommandError } from './commands/types';
|
|
9
|
+
import { buildClientTls } from '../client/tcp/connection';
|
|
9
10
|
/** Send a command and wait for response */
|
|
10
11
|
async function sendCommand(socket, command) {
|
|
11
12
|
return new Promise((resolve, reject) => {
|
|
@@ -45,7 +46,10 @@ async function connect(options) {
|
|
|
45
46
|
catch (err) {
|
|
46
47
|
if (err instanceof FrameSizeError) {
|
|
47
48
|
if (socketData.reject) {
|
|
48
|
-
|
|
49
|
+
// A plaintext client reading a TLS handshake sees garbage that
|
|
50
|
+
// parses as an absurd frame size — hint at the likely cause.
|
|
51
|
+
socketData.reject(new Error(`Frame too large: ${err.requestedSize} bytes exceeds maximum ${err.maxSize}` +
|
|
52
|
+
' (is the server running with TLS? try --tls, --tls-ca or --tls-no-verify)'));
|
|
49
53
|
socketData.resolve = null;
|
|
50
54
|
socketData.reject = null;
|
|
51
55
|
}
|
|
@@ -95,11 +99,18 @@ async function connect(options) {
|
|
|
95
99
|
reject(new Error(`Failed to connect to ${targetDesc}: ${error.message}`));
|
|
96
100
|
},
|
|
97
101
|
};
|
|
98
|
-
// Connect via TCP
|
|
102
|
+
// Connect via TCP (optionally wrapped in TLS)
|
|
103
|
+
const tlsValue = buildClientTls(options.tls);
|
|
99
104
|
void Bun.connect({
|
|
100
105
|
hostname: options.host,
|
|
101
106
|
port: options.port,
|
|
107
|
+
...(tlsValue !== undefined && { tls: tlsValue }),
|
|
102
108
|
socket: socketHandlers,
|
|
109
|
+
}).catch((err) => {
|
|
110
|
+
// Bun.connect can reject directly (e.g. TLS handshake refused) instead of
|
|
111
|
+
// firing connectError — surface it instead of waiting out the timeout.
|
|
112
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
113
|
+
reject(new Error(`Failed to connect to ${targetDesc}: ${message}`));
|
|
103
114
|
});
|
|
104
115
|
// Handle connection timeout
|
|
105
116
|
const connectionTimeoutId = setTimeout(() => {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { parseArgs } from 'node:util';
|
|
6
6
|
import { printServerHelp } from '../help';
|
|
7
7
|
import { VERSION } from '../../shared/version';
|
|
8
|
-
import { loadConfigFile, resolveServerConfig, resolveCloudConfig } from '../../config';
|
|
8
|
+
import { loadConfigFile, resolveServerConfig, resolveCloudConfig, resolveTlsServerOptions, } from '../../config';
|
|
9
9
|
/** Validate port number */
|
|
10
10
|
function validatePort(value, name, defaultPort) {
|
|
11
11
|
const port = parseInt(value, 10);
|
|
@@ -25,6 +25,8 @@ function parseCliFlags(args) {
|
|
|
25
25
|
host: { type: 'string' },
|
|
26
26
|
'data-path': { type: 'string' },
|
|
27
27
|
'auth-tokens': { type: 'string' },
|
|
28
|
+
'tls-cert': { type: 'string' },
|
|
29
|
+
'tls-key': { type: 'string' },
|
|
28
30
|
config: { type: 'string', short: 'c' },
|
|
29
31
|
},
|
|
30
32
|
allowPositionals: false,
|
|
@@ -46,6 +48,12 @@ function parseCliFlags(args) {
|
|
|
46
48
|
if (values['auth-tokens']) {
|
|
47
49
|
flags.authTokens = values['auth-tokens'].split(',').filter(Boolean);
|
|
48
50
|
}
|
|
51
|
+
if (values['tls-cert']) {
|
|
52
|
+
flags.tlsCertFile = values['tls-cert'];
|
|
53
|
+
}
|
|
54
|
+
if (values['tls-key']) {
|
|
55
|
+
flags.tlsKeyFile = values['tls-key'];
|
|
56
|
+
}
|
|
49
57
|
if (values.config) {
|
|
50
58
|
flags.configPath = values.config;
|
|
51
59
|
}
|
|
@@ -58,7 +66,9 @@ function applyCliFlags(fileConfig, flags) {
|
|
|
58
66
|
flags.httpPort !== undefined ||
|
|
59
67
|
flags.host !== undefined ||
|
|
60
68
|
flags.dataPath !== undefined ||
|
|
61
|
-
flags.authTokens !== undefined
|
|
69
|
+
flags.authTokens !== undefined ||
|
|
70
|
+
flags.tlsCertFile !== undefined ||
|
|
71
|
+
flags.tlsKeyFile !== undefined;
|
|
62
72
|
if (!hasFlags && !fileConfig)
|
|
63
73
|
return null;
|
|
64
74
|
const base = fileConfig ?? {};
|
|
@@ -69,6 +79,8 @@ function applyCliFlags(fileConfig, flags) {
|
|
|
69
79
|
...(flags.tcpPort !== undefined && { tcpPort: flags.tcpPort }),
|
|
70
80
|
...(flags.httpPort !== undefined && { httpPort: flags.httpPort }),
|
|
71
81
|
...(flags.host !== undefined && { host: flags.host }),
|
|
82
|
+
...(flags.tlsCertFile !== undefined && { tlsCertFile: flags.tlsCertFile }),
|
|
83
|
+
...(flags.tlsKeyFile !== undefined && { tlsKeyFile: flags.tlsKeyFile }),
|
|
72
84
|
},
|
|
73
85
|
storage: {
|
|
74
86
|
...base.storage,
|
|
@@ -92,6 +104,15 @@ export async function runServer(args, showHelp) {
|
|
|
92
104
|
const mergedConfig = applyCliFlags(fileConfig, flags);
|
|
93
105
|
const config = resolveServerConfig(mergedConfig);
|
|
94
106
|
const cloudConfig = resolveCloudConfig(mergedConfig, config.dataPath);
|
|
107
|
+
// Resolve TLS — fail fast on partial cert/key before binding anything
|
|
108
|
+
let tlsConfig;
|
|
109
|
+
try {
|
|
110
|
+
tlsConfig = resolveTlsServerOptions(config);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
95
116
|
// Import and start the server components
|
|
96
117
|
const { QueueManager } = await import('../../application/queueManager');
|
|
97
118
|
const { createTcpServer } = await import('../../infrastructure/server/tcp');
|
|
@@ -110,11 +131,13 @@ export async function runServer(args, showHelp) {
|
|
|
110
131
|
port: config.tcpPort,
|
|
111
132
|
hostname: config.hostname,
|
|
112
133
|
authTokens,
|
|
134
|
+
...(tlsConfig && { tls: tlsConfig }),
|
|
113
135
|
});
|
|
114
136
|
httpServer = createHttpServer(qm, {
|
|
115
137
|
port: config.httpPort,
|
|
116
138
|
hostname: config.hostname,
|
|
117
139
|
authTokens,
|
|
140
|
+
...(tlsConfig && { tls: tlsConfig }),
|
|
118
141
|
});
|
|
119
142
|
}
|
|
120
143
|
catch (err) {
|
|
@@ -159,6 +182,7 @@ ${dim}────────────────────────
|
|
|
159
182
|
${green}●${reset} TCP ${tcpDisplay}
|
|
160
183
|
${green}●${reset} HTTP ${httpDisplay}
|
|
161
184
|
${yellow}●${reset} Data ${config.dataPath ?? 'in-memory'}
|
|
185
|
+
${yellow}●${reset} TLS ${tlsConfig ? `${green}enabled${reset}` : `${dim}disabled${reset}`}
|
|
162
186
|
${yellow}●${reset} Auth ${authTokens ? `${green}enabled${reset}` : `${dim}disabled${reset}`}
|
|
163
187
|
${yellow}●${reset} Cloud ${cloudConfig ? `${green}enabled${reset} ${dim}→ ${cloudConfig.url}${reset}` : `${dim}disabled${reset}`}
|
|
164
188
|
|
package/dist/cli/help.js
CHANGED
|
@@ -98,6 +98,9 @@ GLOBAL OPTIONS:
|
|
|
98
98
|
-H, --host <host> Server host (default: localhost)
|
|
99
99
|
-p, --port <port> TCP port (default: 6789)
|
|
100
100
|
-t, --token <token> Authentication token (env: BQ_TOKEN, BUNQUEUE_TOKEN)
|
|
101
|
+
--tls Connect with TLS (verify with system CAs)
|
|
102
|
+
--tls-ca <file> Trust a custom CA cert (implies --tls)
|
|
103
|
+
--tls-no-verify TLS without cert verification (self-signed, dev only)
|
|
101
104
|
--json Output as JSON
|
|
102
105
|
--help Show help
|
|
103
106
|
--version Show version
|
|
@@ -125,6 +128,8 @@ Options:
|
|
|
125
128
|
--host <host> Bind address (default: 0.0.0.0, env: HOST)
|
|
126
129
|
--data-path <path> SQLite database path (env: DATA_PATH)
|
|
127
130
|
--auth-tokens <list> Comma-separated auth tokens (env: AUTH_TOKENS)
|
|
131
|
+
--tls-cert <file> PEM certificate for native TLS (env: TLS_CERT_FILE)
|
|
132
|
+
--tls-key <file> PEM private key for native TLS (env: TLS_KEY_FILE)
|
|
128
133
|
--help Show this help
|
|
129
134
|
|
|
130
135
|
Examples:
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -8,6 +8,11 @@ interface GlobalOptions {
|
|
|
8
8
|
host: string;
|
|
9
9
|
port: number;
|
|
10
10
|
token?: string;
|
|
11
|
+
/** TLS to the server: true = verify with system CAs, object = custom CA / no-verify */
|
|
12
|
+
tls?: boolean | {
|
|
13
|
+
rejectUnauthorized?: boolean;
|
|
14
|
+
caFile?: string;
|
|
15
|
+
};
|
|
11
16
|
json: boolean;
|
|
12
17
|
help: boolean;
|
|
13
18
|
version: boolean;
|
package/dist/cli/index.js
CHANGED
|
@@ -35,6 +35,53 @@ function resolveEnvPort(currentPort) {
|
|
|
35
35
|
function resolveEnvHost(currentHost) {
|
|
36
36
|
return Bun.env.HOST ?? Bun.env.BUNQUEUE_HOST ?? Bun.env.BQ_HOST ?? currentHost;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Handle a `--tls*` global client flag and return the index to continue
|
|
40
|
+
* scanning from. Flags that are NOT client TLS flags (e.g. the server's
|
|
41
|
+
* --tls-cert/--tls-key) are passed through to `commandArgs`.
|
|
42
|
+
*/
|
|
43
|
+
function applyTlsFlag(arg, allArgs, i, state, commandArgs) {
|
|
44
|
+
if (arg === '--tls') {
|
|
45
|
+
state.enabled = true;
|
|
46
|
+
return i;
|
|
47
|
+
}
|
|
48
|
+
if (arg === '--tls-no-verify') {
|
|
49
|
+
state.noVerify = true;
|
|
50
|
+
return i;
|
|
51
|
+
}
|
|
52
|
+
if (arg === '--tls-ca') {
|
|
53
|
+
const nextArg = allArgs[i + 1];
|
|
54
|
+
// A following flag is not a path: don't swallow it (--tls-ca --json ...)
|
|
55
|
+
if (nextArg === undefined || nextArg.startsWith('-')) {
|
|
56
|
+
console.warn('Warning: --tls-ca requires a file path. Option ignored.');
|
|
57
|
+
return i;
|
|
58
|
+
}
|
|
59
|
+
state.caFile = nextArg;
|
|
60
|
+
return i + 1;
|
|
61
|
+
}
|
|
62
|
+
if (arg.startsWith('--tls-ca=')) {
|
|
63
|
+
const val = arg.slice(9);
|
|
64
|
+
if (!val) {
|
|
65
|
+
console.warn('Warning: --tls-ca= requires a file path. Option ignored.');
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
state.caFile = val;
|
|
69
|
+
}
|
|
70
|
+
return i;
|
|
71
|
+
}
|
|
72
|
+
commandArgs.push(arg); // --tls-cert / --tls-key etc. → server flags, pass through
|
|
73
|
+
return i;
|
|
74
|
+
}
|
|
75
|
+
/** Build the GlobalOptions.tls value: --tls-ca / --tls-no-verify imply TLS */
|
|
76
|
+
function buildTlsOption(state) {
|
|
77
|
+
if (state.noVerify || state.caFile !== undefined) {
|
|
78
|
+
return {
|
|
79
|
+
...(state.noVerify && { rejectUnauthorized: false }),
|
|
80
|
+
...(state.caFile !== undefined && { caFile: state.caFile }),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return state.enabled ? true : undefined;
|
|
84
|
+
}
|
|
38
85
|
/** Parse global options from process.argv */
|
|
39
86
|
export function parseGlobalOptions() {
|
|
40
87
|
const allArgs = process.argv.slice(2);
|
|
@@ -47,6 +94,7 @@ export function parseGlobalOptions() {
|
|
|
47
94
|
let version = false;
|
|
48
95
|
let hostExplicit = false;
|
|
49
96
|
let portExplicit = false;
|
|
97
|
+
const tlsState = { enabled: false, noVerify: false };
|
|
50
98
|
const commandArgs = [];
|
|
51
99
|
let i = 0;
|
|
52
100
|
while (i < allArgs.length) {
|
|
@@ -76,6 +124,9 @@ export function parseGlobalOptions() {
|
|
|
76
124
|
token = allArgs[++i];
|
|
77
125
|
}
|
|
78
126
|
}
|
|
127
|
+
else if (arg.startsWith('--tls')) {
|
|
128
|
+
i = applyTlsFlag(arg, allArgs, i, tlsState, commandArgs);
|
|
129
|
+
}
|
|
79
130
|
else if (arg === '--json') {
|
|
80
131
|
json = true;
|
|
81
132
|
}
|
|
@@ -137,7 +188,7 @@ export function parseGlobalOptions() {
|
|
|
137
188
|
if (!hostExplicit)
|
|
138
189
|
host = resolveEnvHost(host);
|
|
139
190
|
return {
|
|
140
|
-
options: { host, port, token, json, help, version },
|
|
191
|
+
options: { host, port, token, tls: buildTlsOption(tlsState), json, help, version },
|
|
141
192
|
commandArgs,
|
|
142
193
|
};
|
|
143
194
|
}
|
|
@@ -231,6 +282,7 @@ export async function main() {
|
|
|
231
282
|
host: options.host,
|
|
232
283
|
port: options.port,
|
|
233
284
|
token: options.token,
|
|
285
|
+
tls: options.tls,
|
|
234
286
|
json: options.json,
|
|
235
287
|
});
|
|
236
288
|
}
|
package/dist/client/queue/dlq.js
CHANGED
|
@@ -64,7 +64,7 @@ export function retryDlq(ctx, id) {
|
|
|
64
64
|
if (ctx.embedded)
|
|
65
65
|
return dlqOps.retryDlqEmbedded(ctx.name, id);
|
|
66
66
|
if (ctx.tcp)
|
|
67
|
-
void ctx.tcp.send({ cmd: 'RetryDlq', queue: ctx.name, id });
|
|
67
|
+
void ctx.tcp.send({ cmd: 'RetryDlq', queue: ctx.name, jobId: id });
|
|
68
68
|
return 0;
|
|
69
69
|
}
|
|
70
70
|
/** Retry DLQ entries by filter */
|
|
@@ -77,7 +77,8 @@ export async function cleanAsync(ctx, grace, limit, type) {
|
|
|
77
77
|
queue: ctx.name,
|
|
78
78
|
grace,
|
|
79
79
|
limit,
|
|
80
|
-
type
|
|
80
|
+
// Handler reads `state`; sending `type` made the state filter a no-op.
|
|
81
|
+
state: type,
|
|
81
82
|
});
|
|
82
83
|
if (!response.ok)
|
|
83
84
|
return [];
|
|
@@ -107,7 +108,8 @@ export async function promoteJobs(ctx, opts) {
|
|
|
107
108
|
});
|
|
108
109
|
if (!response.ok)
|
|
109
110
|
return 0;
|
|
110
|
-
|
|
111
|
+
// Handler returns `count`; reading `promoted` always yielded 0.
|
|
112
|
+
return (response.count ?? 0);
|
|
111
113
|
}
|
|
112
114
|
/** Promote a single job */
|
|
113
115
|
export async function promoteJob(ctx, id) {
|
|
@@ -60,6 +60,7 @@ export class Queue {
|
|
|
60
60
|
this.tcpPool = getSharedPool({
|
|
61
61
|
host: connOpts.host,
|
|
62
62
|
port: connOpts.port,
|
|
63
|
+
tls: connOpts.tls,
|
|
63
64
|
poolSize,
|
|
64
65
|
pingInterval: connOpts.pingInterval,
|
|
65
66
|
commandTimeout: connOpts.commandTimeout,
|
|
@@ -74,6 +75,7 @@ export class Queue {
|
|
|
74
75
|
host: connOpts.host ?? 'localhost',
|
|
75
76
|
port: connOpts.port ?? 6789,
|
|
76
77
|
token,
|
|
78
|
+
tls: connOpts.tls,
|
|
77
79
|
poolSize,
|
|
78
80
|
pingInterval: connOpts.pingInterval,
|
|
79
81
|
commandTimeout: connOpts.commandTimeout,
|
|
@@ -65,6 +65,9 @@ export async function upsertJobScheduler(ctx, schedulerId, repeatOpts, jobTempla
|
|
|
65
65
|
const dedupFields = buildCronDedup(jobTemplate);
|
|
66
66
|
const jobOptions = buildCronJobOptions(ctx.defaultJobOptions, jobTemplate);
|
|
67
67
|
const cronName = toCronName(ctx, schedulerId);
|
|
68
|
+
// Priority of spawned jobs: carried on the top-level Cron field (the handler
|
|
69
|
+
// reads cmd.priority), which buildCronJobOptions does not cover.
|
|
70
|
+
const priority = jobTemplate?.opts?.priority ?? ctx.defaultJobOptions?.priority;
|
|
68
71
|
if (ctx.embedded) {
|
|
69
72
|
const manager = getSharedManager();
|
|
70
73
|
manager.addCron({
|
|
@@ -73,6 +76,7 @@ export async function upsertJobScheduler(ctx, schedulerId, repeatOpts, jobTempla
|
|
|
73
76
|
data,
|
|
74
77
|
schedule: cronPattern,
|
|
75
78
|
repeatEvery,
|
|
79
|
+
priority,
|
|
76
80
|
timezone: repeatOpts.timezone ?? 'UTC',
|
|
77
81
|
skipMissedOnRestart: repeatOpts.skipMissedOnRestart,
|
|
78
82
|
immediately: repeatOpts.immediately,
|
|
@@ -94,6 +98,7 @@ export async function upsertJobScheduler(ctx, schedulerId, repeatOpts, jobTempla
|
|
|
94
98
|
data,
|
|
95
99
|
schedule: cronPattern,
|
|
96
100
|
repeatEvery,
|
|
101
|
+
priority,
|
|
97
102
|
timezone: repeatOpts.timezone,
|
|
98
103
|
skipMissedOnRestart: repeatOpts.skipMissedOnRestart,
|
|
99
104
|
immediately: repeatOpts.immediately,
|
|
@@ -137,6 +137,7 @@ export class TcpClient extends EventEmitter {
|
|
|
137
137
|
const { socket } = await createConnection({
|
|
138
138
|
host: this.options.host,
|
|
139
139
|
port: this.options.port,
|
|
140
|
+
tls: this.options.tls,
|
|
140
141
|
}, this.options.connectTimeout, {
|
|
141
142
|
onData: (frame) => {
|
|
142
143
|
this.handleData(frame);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* TCP Connection Handler
|
|
3
3
|
* Manages low-level socket connection and data handling (msgpack binary protocol)
|
|
4
4
|
*/
|
|
5
|
-
import type { SocketWrapper, PendingCommand } from './types';
|
|
5
|
+
import type { SocketWrapper, PendingCommand, ClientTlsOptions } from './types';
|
|
6
6
|
/** Connection events */
|
|
7
7
|
export interface ConnectionEvents {
|
|
8
8
|
onData: (frame: Uint8Array) => void;
|
|
@@ -20,7 +20,14 @@ export interface ConnectionTarget {
|
|
|
20
20
|
host?: string;
|
|
21
21
|
/** TCP port */
|
|
22
22
|
port?: number;
|
|
23
|
+
/** Enable TLS: true (system CAs) or per-connection TLS options */
|
|
24
|
+
tls?: boolean | ClientTlsOptions;
|
|
23
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Map client TLS options to the `tls` value accepted by Bun.connect.
|
|
28
|
+
* Returns undefined when TLS is disabled (plaintext, the default).
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildClientTls(tls: boolean | ClientTlsOptions | undefined): true | Record<string, unknown> | undefined;
|
|
24
31
|
/**
|
|
25
32
|
* Establish TCP connection to server
|
|
26
33
|
*/
|
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
* Manages low-level socket connection and data handling (msgpack binary protocol)
|
|
4
4
|
*/
|
|
5
5
|
import { FrameParser, FrameSizeError } from '../../infrastructure/server/protocol';
|
|
6
|
+
/**
|
|
7
|
+
* Map client TLS options to the `tls` value accepted by Bun.connect.
|
|
8
|
+
* Returns undefined when TLS is disabled (plaintext, the default).
|
|
9
|
+
*/
|
|
10
|
+
export function buildClientTls(tls) {
|
|
11
|
+
if (!tls)
|
|
12
|
+
return undefined;
|
|
13
|
+
if (tls === true)
|
|
14
|
+
return true;
|
|
15
|
+
return {
|
|
16
|
+
...(tls.rejectUnauthorized !== undefined && { rejectUnauthorized: tls.rejectUnauthorized }),
|
|
17
|
+
...(tls.caFile !== undefined && { ca: Bun.file(tls.caFile) }),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
6
20
|
/**
|
|
7
21
|
* Establish TCP connection to server
|
|
8
22
|
*/
|
|
@@ -82,11 +96,23 @@ export async function createConnection(target, connectTimeout, events) {
|
|
|
82
96
|
}
|
|
83
97
|
},
|
|
84
98
|
};
|
|
85
|
-
// Connect via TCP
|
|
99
|
+
// Connect via TCP (optionally wrapped in TLS — protocol is unchanged)
|
|
100
|
+
const tlsValue = buildClientTls(target.tls);
|
|
86
101
|
void Bun.connect({
|
|
87
102
|
hostname: target.host ?? 'localhost',
|
|
88
103
|
port: target.port ?? 6789,
|
|
104
|
+
...(tlsValue !== undefined && { tls: tlsValue }),
|
|
89
105
|
socket: socketHandlers,
|
|
106
|
+
}).catch((error) => {
|
|
107
|
+
// Bun.connect rejects (instead of firing connectError) for some failure
|
|
108
|
+
// modes, e.g. a TLS handshake refused synchronously. Route it through the
|
|
109
|
+
// same rejection path so callers never hang until the connect timeout.
|
|
110
|
+
if (!connectionResolved) {
|
|
111
|
+
connectionResolved = true;
|
|
112
|
+
cleanup();
|
|
113
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
114
|
+
reject(new Error(`Failed to connect to ${targetDesc}: ${message}`));
|
|
115
|
+
}
|
|
90
116
|
});
|
|
91
117
|
timeoutId = setTimeout(() => {
|
|
92
118
|
if (!connectionResolved) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* TCP Client Module
|
|
3
3
|
* Re-exports all TCP client components
|
|
4
4
|
*/
|
|
5
|
-
export type { ConnectionOptions, ConnectionHealth, PendingCommand, SocketWrapper } from './types';
|
|
5
|
+
export type { ConnectionOptions, ConnectionHealth, PendingCommand, SocketWrapper, ClientTlsOptions, } from './types';
|
|
6
6
|
export { DEFAULT_CONNECTION } from './types';
|
|
7
7
|
export { HealthTracker, type HealthConfig } from './health';
|
|
8
8
|
export { ReconnectManager, type ReconnectConfig } from './reconnect';
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared TCP Client
|
|
3
|
-
*
|
|
2
|
+
* Shared TCP Client Instances
|
|
3
|
+
* One shared client per distinct connection target. Keyed by
|
|
4
|
+
* host/port/token/tls so callers with different configs (notably TLS vs
|
|
5
|
+
* plaintext to the same server) never receive each other's connection.
|
|
4
6
|
*/
|
|
5
7
|
import type { ConnectionOptions } from './types';
|
|
6
8
|
import { TcpClient } from './client';
|
|
7
|
-
/** Get shared TCP client */
|
|
9
|
+
/** Get shared TCP client for the given connection target */
|
|
8
10
|
export declare function getSharedTcpClient(options?: Partial<ConnectionOptions>): TcpClient;
|
|
9
|
-
/** Close shared
|
|
11
|
+
/** Close all shared clients */
|
|
10
12
|
export declare function closeSharedTcpClient(): void;
|
|
@@ -1,19 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared TCP Client
|
|
3
|
-
*
|
|
2
|
+
* Shared TCP Client Instances
|
|
3
|
+
* One shared client per distinct connection target. Keyed by
|
|
4
|
+
* host/port/token/tls so callers with different configs (notably TLS vs
|
|
5
|
+
* plaintext to the same server) never receive each other's connection.
|
|
4
6
|
*/
|
|
5
7
|
import { TcpClient } from './client';
|
|
6
|
-
/** Shared
|
|
7
|
-
|
|
8
|
-
/**
|
|
8
|
+
/** Shared clients keyed by connection target */
|
|
9
|
+
const sharedClients = new Map();
|
|
10
|
+
/** Build the sharing key from the connection-identity options */
|
|
11
|
+
function getClientKey(options) {
|
|
12
|
+
const host = options?.host ?? 'localhost';
|
|
13
|
+
const port = options?.port ?? 6789;
|
|
14
|
+
const token = options?.token ?? '';
|
|
15
|
+
const tokenHash = token ? String(Number(Bun.hash(token)) & 0xffff) : '0';
|
|
16
|
+
const tlsKey = options?.tls ? JSON.stringify(options.tls) : '0';
|
|
17
|
+
return `${host}:${port}:${tokenHash}:${tlsKey}`;
|
|
18
|
+
}
|
|
19
|
+
/** Get shared TCP client for the given connection target */
|
|
9
20
|
export function getSharedTcpClient(options) {
|
|
10
|
-
|
|
11
|
-
|
|
21
|
+
const key = getClientKey(options);
|
|
22
|
+
let client = sharedClients.get(key);
|
|
23
|
+
if (!client) {
|
|
24
|
+
client = new TcpClient(options);
|
|
25
|
+
sharedClients.set(key, client);
|
|
26
|
+
}
|
|
27
|
+
return client;
|
|
12
28
|
}
|
|
13
|
-
/** Close shared
|
|
29
|
+
/** Close all shared clients */
|
|
14
30
|
export function closeSharedTcpClient() {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
sharedClient = null;
|
|
31
|
+
for (const client of sharedClients.values()) {
|
|
32
|
+
client.close();
|
|
18
33
|
}
|
|
34
|
+
sharedClients.clear();
|
|
19
35
|
}
|
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
* TCP Client Types
|
|
3
3
|
* Type definitions for TCP connection management
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* TLS options for client connections.
|
|
7
|
+
* `true` enables TLS with system CA verification; the object form allows
|
|
8
|
+
* trusting a custom CA (self-signed server cert) or disabling verification.
|
|
9
|
+
*/
|
|
10
|
+
export interface ClientTlsOptions {
|
|
11
|
+
/** Verify the server certificate (default: true). Set false for self-signed in dev. */
|
|
12
|
+
rejectUnauthorized?: boolean;
|
|
13
|
+
/** Path to a PEM CA certificate to trust (e.g. the self-signed server cert) */
|
|
14
|
+
caFile?: string;
|
|
15
|
+
}
|
|
5
16
|
/** Connection options */
|
|
6
17
|
export interface ConnectionOptions {
|
|
7
18
|
/** Server host */
|
|
@@ -10,6 +21,8 @@ export interface ConnectionOptions {
|
|
|
10
21
|
port: number;
|
|
11
22
|
/** Auth token */
|
|
12
23
|
token?: string;
|
|
24
|
+
/** Enable TLS: true (system CAs) or per-connection TLS options (default: false) */
|
|
25
|
+
tls?: boolean | ClientTlsOptions;
|
|
13
26
|
/** Max reconnection attempts (default: Infinity) */
|
|
14
27
|
maxReconnectAttempts?: number;
|
|
15
28
|
/** Initial reconnect delay in ms (default: 100) */
|