bunqueue-client 0.1.2 → 0.1.3
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/README.md +45 -3
- package/dist/bunqueue/aging.d.ts +17 -0
- package/dist/bunqueue/aging.js +53 -0
- package/dist/bunqueue/batch.d.ts +17 -0
- package/dist/bunqueue/batch.js +60 -0
- package/dist/bunqueue/bunqueue-api.d.ts +87 -0
- package/dist/bunqueue/bunqueue-api.js +116 -0
- package/dist/bunqueue/bunqueue.d.ts +57 -0
- package/dist/bunqueue/bunqueue.js +182 -0
- package/dist/bunqueue/cancellation.d.ts +20 -0
- package/dist/bunqueue/cancellation.js +48 -0
- package/dist/bunqueue/circuit-breaker.d.ts +21 -0
- package/dist/bunqueue/circuit-breaker.js +69 -0
- package/dist/bunqueue/dedup-debounce.d.ts +14 -0
- package/dist/bunqueue/dedup-debounce.js +37 -0
- package/dist/bunqueue/dlq-rate-limit.d.ts +33 -0
- package/dist/bunqueue/dlq-rate-limit.js +65 -0
- package/dist/bunqueue/rate-gate.d.ts +21 -0
- package/dist/bunqueue/rate-gate.js +48 -0
- package/dist/bunqueue/retry.d.ts +9 -0
- package/dist/bunqueue/retry.js +56 -0
- package/dist/bunqueue/triggers.d.ts +18 -0
- package/dist/bunqueue/triggers.js +42 -0
- package/dist/bunqueue/ttl.d.ts +18 -0
- package/dist/bunqueue/ttl.js +31 -0
- package/dist/bunqueue/types.d.ts +151 -0
- package/dist/bunqueue/types.js +6 -0
- package/dist/connection.d.ts +9 -1
- package/dist/connection.js +32 -47
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/job.d.ts +2 -0
- package/dist/job.js +4 -0
- package/dist/queue-admin.js +2 -1
- package/dist/socket-factory.d.ts +4 -0
- package/dist/socket-factory.js +47 -0
- package/dist/worker-base.d.ts +13 -2
- package/dist/worker-base.js +29 -4
- package/dist/worker.js +8 -0
- package/package.json +1 -1
- package/src/bunqueue/aging.ts +62 -0
- package/src/bunqueue/batch.ts +79 -0
- package/src/bunqueue/bunqueue-api.ts +212 -0
- package/src/bunqueue/bunqueue.ts +210 -0
- package/src/bunqueue/cancellation.ts +54 -0
- package/src/bunqueue/circuit-breaker.ts +81 -0
- package/src/bunqueue/dedup-debounce.ts +43 -0
- package/src/bunqueue/dlq-rate-limit.ts +89 -0
- package/src/bunqueue/rate-gate.ts +53 -0
- package/src/bunqueue/retry.ts +72 -0
- package/src/bunqueue/triggers.ts +51 -0
- package/src/bunqueue/ttl.ts +38 -0
- package/src/bunqueue/types.ts +171 -0
- package/src/connection.ts +38 -46
- package/src/index.ts +22 -1
- package/src/job.ts +5 -0
- package/src/queue-admin.ts +2 -1
- package/src/socket-factory.ts +51 -0
- package/src/worker-base.ts +30 -4
- package/src/worker.ts +9 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bunqueue Simple Mode — type definitions.
|
|
3
|
+
* 1:1 port of src/client/bunqueue/types.ts from the official client, minus
|
|
4
|
+
* the embedded-mode options (this SDK is TCP-only).
|
|
5
|
+
*/
|
|
6
|
+
import type { TlsOption } from '../connection.js';
|
|
7
|
+
import type { Job } from '../job.js';
|
|
8
|
+
import type { JobOptions } from '../types.js';
|
|
9
|
+
import type { Processor } from '../worker-types.js';
|
|
10
|
+
/** Connection options accepted by Simple Mode (mirrors ConnectionOptions). */
|
|
11
|
+
export interface BunqueueConnection {
|
|
12
|
+
host?: string;
|
|
13
|
+
port?: number;
|
|
14
|
+
token?: string;
|
|
15
|
+
tls?: TlsOption;
|
|
16
|
+
}
|
|
17
|
+
/** Middleware function: receives job and next(), returns result. */
|
|
18
|
+
export type BunqueueMiddleware<T = unknown, R = unknown> = (job: Job<T>, next: () => Promise<R>) => Promise<R>;
|
|
19
|
+
/** Retry strategy for advanced backoff. */
|
|
20
|
+
export type RetryStrategy = 'fixed' | 'exponential' | 'jitter' | 'fibonacci' | 'custom';
|
|
21
|
+
/** Advanced retry configuration (in-process; the job stays active). */
|
|
22
|
+
export interface RetryConfig {
|
|
23
|
+
/** Max attempts (default: 3). */
|
|
24
|
+
maxAttempts?: number;
|
|
25
|
+
/** Base delay in ms (default: 1000). */
|
|
26
|
+
delay?: number;
|
|
27
|
+
/** Strategy (default: exponential). */
|
|
28
|
+
strategy?: RetryStrategy;
|
|
29
|
+
/** Custom backoff function: attempt, error -> delay in ms. */
|
|
30
|
+
customBackoff?: (attempt: number, error: Error) => number;
|
|
31
|
+
/** Only retry if this returns true. */
|
|
32
|
+
retryIf?: (error: Error, attempt: number) => boolean;
|
|
33
|
+
}
|
|
34
|
+
/** Circuit breaker configuration. */
|
|
35
|
+
export interface CircuitBreakerConfig {
|
|
36
|
+
/** Max consecutive failures before opening (default: 5). */
|
|
37
|
+
threshold?: number;
|
|
38
|
+
/** Time in ms before half-open retry (default: 30000). */
|
|
39
|
+
resetTimeout?: number;
|
|
40
|
+
onOpen?: (failures: number) => void;
|
|
41
|
+
onClose?: () => void;
|
|
42
|
+
onHalfOpen?: () => void;
|
|
43
|
+
}
|
|
44
|
+
/** Circuit breaker state. */
|
|
45
|
+
export type CircuitState = 'closed' | 'open' | 'half-open';
|
|
46
|
+
/** Event trigger rule. */
|
|
47
|
+
export interface TriggerRule<T = unknown> {
|
|
48
|
+
/** Job name that triggers this rule. */
|
|
49
|
+
on: string;
|
|
50
|
+
/** Event type (default: completed). */
|
|
51
|
+
event?: 'completed' | 'failed';
|
|
52
|
+
/** Job name to create. */
|
|
53
|
+
create: string;
|
|
54
|
+
/** Data builder from the triggering job. */
|
|
55
|
+
data: (result: unknown, job: Job<T>) => T;
|
|
56
|
+
/** Optional job options. */
|
|
57
|
+
opts?: JobOptions;
|
|
58
|
+
/** Optional condition. */
|
|
59
|
+
condition?: (result: unknown, job: Job<T>) => boolean;
|
|
60
|
+
}
|
|
61
|
+
/** Priority aging configuration. */
|
|
62
|
+
export interface PriorityAgingConfig {
|
|
63
|
+
/** Check interval in ms (default: 60000). */
|
|
64
|
+
interval?: number;
|
|
65
|
+
/** Min age in ms before boost (default: 60000). */
|
|
66
|
+
minAge?: number;
|
|
67
|
+
/** Priority boost per interval (default: 1). */
|
|
68
|
+
boost?: number;
|
|
69
|
+
/** Max priority cap (default: 100). */
|
|
70
|
+
maxPriority?: number;
|
|
71
|
+
/** Max jobs to scan per tick (default: 100). */
|
|
72
|
+
maxScan?: number;
|
|
73
|
+
}
|
|
74
|
+
/** Batch processor function. */
|
|
75
|
+
export type BatchProcessor<T = unknown, R = unknown> = (jobs: Array<Job<T>>) => Promise<R[]>;
|
|
76
|
+
/** Batch processing configuration. */
|
|
77
|
+
export interface BatchConfig<T = unknown, R = unknown> {
|
|
78
|
+
/** Batch size (default: 10). */
|
|
79
|
+
size: number;
|
|
80
|
+
/** Max wait in ms before flushing a partial batch (default: 5000). */
|
|
81
|
+
timeout?: number;
|
|
82
|
+
processor: BatchProcessor<T, R>;
|
|
83
|
+
}
|
|
84
|
+
/** Job TTL configuration. */
|
|
85
|
+
export interface JobTtlConfig {
|
|
86
|
+
/** Default TTL in ms (0 = no TTL). */
|
|
87
|
+
defaultTtl?: number;
|
|
88
|
+
/** Per-job-name TTL overrides. */
|
|
89
|
+
perName?: Record<string, number>;
|
|
90
|
+
}
|
|
91
|
+
/** Deduplication defaults for Simple Mode. */
|
|
92
|
+
export interface BunqueueDeduplicationConfig {
|
|
93
|
+
/** Default deduplication TTL in ms (default: 3600000 = 1 hour). */
|
|
94
|
+
ttl?: number;
|
|
95
|
+
/** Extend TTL when a duplicate arrives (default: false). */
|
|
96
|
+
extend?: boolean;
|
|
97
|
+
/** Replace data when a duplicate arrives in delayed state (default: false). */
|
|
98
|
+
replace?: boolean;
|
|
99
|
+
}
|
|
100
|
+
/** Debounce defaults for Simple Mode. */
|
|
101
|
+
export interface BunqueueDebounceConfig {
|
|
102
|
+
/** Debounce TTL in ms. */
|
|
103
|
+
ttl: number;
|
|
104
|
+
}
|
|
105
|
+
/** DLQ configuration for Simple Mode (forwarded to the server). */
|
|
106
|
+
export interface BunqueueDlqConfig {
|
|
107
|
+
autoRetry?: boolean;
|
|
108
|
+
autoRetryInterval?: number;
|
|
109
|
+
maxAutoRetries?: number;
|
|
110
|
+
maxAge?: number | null;
|
|
111
|
+
maxEntries?: number;
|
|
112
|
+
}
|
|
113
|
+
/** Client-side rate limiter options (max job starts per duration window). */
|
|
114
|
+
export interface RateLimiterOptions {
|
|
115
|
+
max: number;
|
|
116
|
+
duration: number;
|
|
117
|
+
/** Group jobs by this data field (e.g. per-customer limits). */
|
|
118
|
+
groupKey?: string;
|
|
119
|
+
}
|
|
120
|
+
/** Bunqueue Simple Mode options. */
|
|
121
|
+
export interface BunqueueOptions<T = unknown, R = unknown> {
|
|
122
|
+
processor?: Processor<T, R>;
|
|
123
|
+
routes?: Record<string, Processor<T, R>>;
|
|
124
|
+
batch?: BatchConfig<T, R>;
|
|
125
|
+
concurrency?: number;
|
|
126
|
+
connection?: BunqueueConnection;
|
|
127
|
+
defaultJobOptions?: JobOptions;
|
|
128
|
+
autorun?: boolean;
|
|
129
|
+
/** Worker heartbeat interval in ms (official-client unit). */
|
|
130
|
+
heartbeatInterval?: number;
|
|
131
|
+
batchSize?: number;
|
|
132
|
+
/** Long-poll timeout in ms. */
|
|
133
|
+
pollTimeout?: number;
|
|
134
|
+
limiter?: RateLimiterOptions;
|
|
135
|
+
removeOnComplete?: boolean;
|
|
136
|
+
removeOnFail?: boolean;
|
|
137
|
+
retry?: RetryConfig;
|
|
138
|
+
circuitBreaker?: CircuitBreakerConfig;
|
|
139
|
+
ttl?: JobTtlConfig;
|
|
140
|
+
priorityAging?: PriorityAgingConfig;
|
|
141
|
+
/** Job deduplication defaults, merged into every add(). */
|
|
142
|
+
deduplication?: BunqueueDeduplicationConfig;
|
|
143
|
+
/** Job debouncing defaults, merged into every add(). */
|
|
144
|
+
debounce?: BunqueueDebounceConfig;
|
|
145
|
+
/** Rate limiting for the worker (alias of limiter, takes precedence). */
|
|
146
|
+
rateLimit?: RateLimiterOptions;
|
|
147
|
+
/** Dead letter queue auto-management (forwarded to the server). */
|
|
148
|
+
dlq?: BunqueueDlqConfig;
|
|
149
|
+
/** Namespace prefix prepended to the queue name on the server. */
|
|
150
|
+
prefixKey?: string;
|
|
151
|
+
}
|
package/dist/connection.d.ts
CHANGED
|
@@ -22,12 +22,20 @@ export declare class Connection {
|
|
|
22
22
|
private pending;
|
|
23
23
|
private reqCounter;
|
|
24
24
|
private parser;
|
|
25
|
+
private connectGeneration;
|
|
26
|
+
private failedAttempts;
|
|
27
|
+
private nextAttemptAt;
|
|
25
28
|
constructor(options?: ConnectionOptions);
|
|
26
29
|
get isConnected(): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Monotonic counter, incremented on every successful (re)connect. Consumers
|
|
32
|
+
* that hold per-connection server state (e.g. a Worker registration) compare
|
|
33
|
+
* it between operations and re-establish that state after a reconnect.
|
|
34
|
+
*/
|
|
35
|
+
get generation(): number;
|
|
27
36
|
/** Open the socket (and authenticate) if not already connected. */
|
|
28
37
|
connect(): Promise<void>;
|
|
29
38
|
private doConnect;
|
|
30
|
-
private openSocket;
|
|
31
39
|
/**
|
|
32
40
|
* Send a command and await its response. Rejects with CommandError when
|
|
33
41
|
* the server answers ok=false. Reconnects lazily if the link was lost.
|
package/dist/connection.js
CHANGED
|
@@ -5,12 +5,10 @@
|
|
|
5
5
|
* pipelining (many in-flight commands per socket). Uses only `node:`
|
|
6
6
|
* builtins (net/tls), which Node, Bun and Deno all support.
|
|
7
7
|
*/
|
|
8
|
-
import { readFileSync } from 'node:fs';
|
|
9
|
-
import { connect as netConnect } from 'node:net';
|
|
10
|
-
import { connect as tlsConnect } from 'node:tls';
|
|
11
8
|
import { pack, unpack } from 'msgpackr';
|
|
12
9
|
import { AuthError, CommandError, CommandTimeoutError, ConnectionClosedError } from './errors.js';
|
|
13
10
|
import { compact, FrameParser, frame, PROTOCOL_VERSION } from './frame.js';
|
|
11
|
+
import { openSocket } from './socket-factory.js';
|
|
14
12
|
/** A single pipelined TCP connection to a bunqueue server. */
|
|
15
13
|
export class Connection {
|
|
16
14
|
host;
|
|
@@ -26,6 +24,9 @@ export class Connection {
|
|
|
26
24
|
pending = new Map();
|
|
27
25
|
reqCounter = 0;
|
|
28
26
|
parser = new FrameParser();
|
|
27
|
+
connectGeneration = 0;
|
|
28
|
+
failedAttempts = 0;
|
|
29
|
+
nextAttemptAt = 0;
|
|
29
30
|
constructor(options = {}) {
|
|
30
31
|
this.host = options.host ?? 'localhost';
|
|
31
32
|
this.port = options.port ?? 6789;
|
|
@@ -37,21 +38,46 @@ export class Connection {
|
|
|
37
38
|
get isConnected() {
|
|
38
39
|
return this.connected;
|
|
39
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Monotonic counter, incremented on every successful (re)connect. Consumers
|
|
43
|
+
* that hold per-connection server state (e.g. a Worker registration) compare
|
|
44
|
+
* it between operations and re-establish that state after a reconnect.
|
|
45
|
+
*/
|
|
46
|
+
get generation() {
|
|
47
|
+
return this.connectGeneration;
|
|
48
|
+
}
|
|
40
49
|
/** Open the socket (and authenticate) if not already connected. */
|
|
41
50
|
async connect() {
|
|
42
51
|
if (this.connected)
|
|
43
52
|
return;
|
|
44
53
|
if (this.closed)
|
|
45
54
|
throw new ConnectionClosedError('connection closed by client');
|
|
55
|
+
// Fast-fail while inside the reconnect backoff window: without this, a
|
|
56
|
+
// producer calling add() against a downed server pays the full connect
|
|
57
|
+
// timeout on EVERY call (reconnect storm).
|
|
58
|
+
if (Date.now() < this.nextAttemptAt) {
|
|
59
|
+
throw new ConnectionClosedError(`server unreachable, retry in ${this.nextAttemptAt - Date.now()}ms`);
|
|
60
|
+
}
|
|
46
61
|
if (this.connecting)
|
|
47
62
|
return this.connecting;
|
|
48
|
-
this.connecting = this.doConnect()
|
|
63
|
+
this.connecting = this.doConnect()
|
|
64
|
+
.then(() => {
|
|
65
|
+
this.failedAttempts = 0;
|
|
66
|
+
this.nextAttemptAt = 0;
|
|
67
|
+
})
|
|
68
|
+
.catch((err) => {
|
|
69
|
+
this.failedAttempts += 1;
|
|
70
|
+
const backoff = Math.min(500 * 2 ** (this.failedAttempts - 1), 5000);
|
|
71
|
+
this.nextAttemptAt = Date.now() + backoff;
|
|
72
|
+
throw err;
|
|
73
|
+
})
|
|
74
|
+
.finally(() => {
|
|
49
75
|
this.connecting = null;
|
|
50
76
|
});
|
|
51
77
|
return this.connecting;
|
|
52
78
|
}
|
|
53
79
|
async doConnect() {
|
|
54
|
-
const socket = await this.
|
|
80
|
+
const socket = await openSocket(this.host, this.port, this.tls, this.connectTimeoutMs);
|
|
55
81
|
socket.setNoDelay(true);
|
|
56
82
|
this.parser.clear();
|
|
57
83
|
this.socket = socket;
|
|
@@ -59,6 +85,7 @@ export class Connection {
|
|
|
59
85
|
socket.on('error', () => this.teardown());
|
|
60
86
|
socket.on('close', () => this.teardown());
|
|
61
87
|
this.connected = true;
|
|
88
|
+
this.connectGeneration += 1;
|
|
62
89
|
if (this.token) {
|
|
63
90
|
try {
|
|
64
91
|
await this.call({ cmd: 'Auth', token: this.token });
|
|
@@ -71,48 +98,6 @@ export class Connection {
|
|
|
71
98
|
}
|
|
72
99
|
}
|
|
73
100
|
}
|
|
74
|
-
openSocket() {
|
|
75
|
-
return new Promise((resolve, reject) => {
|
|
76
|
-
let settled = false;
|
|
77
|
-
const timer = setTimeout(() => {
|
|
78
|
-
if (settled)
|
|
79
|
-
return;
|
|
80
|
-
settled = true;
|
|
81
|
-
socket.destroy();
|
|
82
|
-
reject(new ConnectionClosedError(`connect timeout to ${this.host}:${this.port}`));
|
|
83
|
-
}, this.connectTimeoutMs);
|
|
84
|
-
const onError = (err) => {
|
|
85
|
-
if (settled)
|
|
86
|
-
return;
|
|
87
|
-
settled = true;
|
|
88
|
-
clearTimeout(timer);
|
|
89
|
-
reject(new ConnectionClosedError(`connect failed: ${err.message}`));
|
|
90
|
-
};
|
|
91
|
-
const onReady = () => {
|
|
92
|
-
if (settled)
|
|
93
|
-
return;
|
|
94
|
-
settled = true;
|
|
95
|
-
clearTimeout(timer);
|
|
96
|
-
socket.off('error', onError);
|
|
97
|
-
resolve(socket);
|
|
98
|
-
};
|
|
99
|
-
let socket;
|
|
100
|
-
if (this.tls) {
|
|
101
|
-
const tlsOpts = { host: this.host, port: this.port };
|
|
102
|
-
if (typeof this.tls === 'object') {
|
|
103
|
-
if (this.tls.caFile)
|
|
104
|
-
tlsOpts.ca = readFileSync(this.tls.caFile);
|
|
105
|
-
if (this.tls.rejectUnauthorized === false)
|
|
106
|
-
tlsOpts.rejectUnauthorized = false;
|
|
107
|
-
}
|
|
108
|
-
socket = tlsConnect(tlsOpts, onReady);
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
socket = netConnect({ host: this.host, port: this.port }, onReady);
|
|
112
|
-
}
|
|
113
|
-
socket.on('error', onError);
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
101
|
/**
|
|
117
102
|
* Send a command and await its response. Rejects with CommandError when
|
|
118
103
|
* the server answers ok=false. Reconnects lazily if the link was lost.
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* bunqueue-client — cross-runtime TypeScript SDK for bunqueue (TCP mode).
|
|
3
3
|
* Works on Node.js ≥20, Bun and Deno ≥2 (uses only node: builtins + msgpackr).
|
|
4
4
|
*/
|
|
5
|
+
export { Bunqueue } from './bunqueue/bunqueue.js';
|
|
6
|
+
export type { DlqFilter, DlqStats } from './bunqueue/dlq-rate-limit.js';
|
|
7
|
+
export type { BatchConfig, BatchProcessor, BunqueueConnection, BunqueueDebounceConfig, BunqueueDeduplicationConfig, BunqueueDlqConfig, BunqueueMiddleware, BunqueueOptions, CircuitBreakerConfig, CircuitState, JobTtlConfig, PriorityAgingConfig, RateLimiterOptions, RetryConfig, RetryStrategy, TriggerRule, } from './bunqueue/types.js';
|
|
5
8
|
export type { Command, ConnectionOptions, Response, TlsOption } from './connection.js';
|
|
6
9
|
export { Connection } from './connection.js';
|
|
7
10
|
export { AuthError, BunqueueError, CommandError, CommandTimeoutError, ConnectionClosedError, UnrecoverableError, } from './errors.js';
|
|
@@ -16,4 +19,4 @@ export type { SchedulerOptions } from './queue-admin.js';
|
|
|
16
19
|
export type { BackoffOptions, DeduplicationOptions, JobCounts, JobOptions, JobStateName, RepeatOptions, } from './types.js';
|
|
17
20
|
export { Worker } from './worker.js';
|
|
18
21
|
export type { Processor, WorkerOptions } from './worker-types.js';
|
|
19
|
-
export declare const __version__ = "0.1.
|
|
22
|
+
export declare const __version__ = "0.1.2";
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* bunqueue-client — cross-runtime TypeScript SDK for bunqueue (TCP mode).
|
|
3
3
|
* Works on Node.js ≥20, Bun and Deno ≥2 (uses only node: builtins + msgpackr).
|
|
4
4
|
*/
|
|
5
|
+
// Simple Mode (Bunqueue): all-in-one Queue + Worker, 1:1 with the official client
|
|
6
|
+
export { Bunqueue } from './bunqueue/bunqueue.js';
|
|
5
7
|
export { Connection } from './connection.js';
|
|
6
8
|
export { AuthError, BunqueueError, CommandError, CommandTimeoutError, ConnectionClosedError, UnrecoverableError, } from './errors.js';
|
|
7
9
|
export { FlowProducer } from './flow.js';
|
|
@@ -9,4 +11,4 @@ export { MAX_FRAME_SIZE, PROTOCOL_VERSION } from './frame.js';
|
|
|
9
11
|
export { Job } from './job.js';
|
|
10
12
|
export { Queue } from './queue.js';
|
|
11
13
|
export { Worker } from './worker.js';
|
|
12
|
-
export const __version__ = '0.1.
|
|
14
|
+
export const __version__ = '0.1.2';
|
package/dist/job.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export declare class Job<T = unknown> {
|
|
|
20
20
|
get attempts(): number;
|
|
21
21
|
get maxAttempts(): number;
|
|
22
22
|
get createdAt(): number | undefined;
|
|
23
|
+
/** Creation time in epoch ms (BullMQ-parity alias of createdAt). */
|
|
24
|
+
get timestamp(): number;
|
|
23
25
|
get startedAt(): number | null;
|
|
24
26
|
get completedAt(): number | null;
|
|
25
27
|
get customId(): string | null;
|
package/dist/job.js
CHANGED
|
@@ -44,6 +44,10 @@ export class Job {
|
|
|
44
44
|
get createdAt() {
|
|
45
45
|
return this.raw.createdAt === undefined ? undefined : Number(this.raw.createdAt);
|
|
46
46
|
}
|
|
47
|
+
/** Creation time in epoch ms (BullMQ-parity alias of createdAt). */
|
|
48
|
+
get timestamp() {
|
|
49
|
+
return this.createdAt ?? Date.now();
|
|
50
|
+
}
|
|
47
51
|
get startedAt() {
|
|
48
52
|
return this.raw.startedAt === undefined || this.raw.startedAt === null
|
|
49
53
|
? null
|
package/dist/queue-admin.js
CHANGED
|
@@ -138,6 +138,7 @@ export const adminMethods = {
|
|
|
138
138
|
return (data.webhooks ?? response.webhooks ?? []);
|
|
139
139
|
},
|
|
140
140
|
async setWebhookEnabled(webhookId, enabled) {
|
|
141
|
-
|
|
141
|
+
// wire field is `id` (SetWebhookEnabledCommand), unlike RemoveWebhook's `webhookId`
|
|
142
|
+
await this.call({ cmd: 'SetWebhookEnabled', id: webhookId, enabled });
|
|
142
143
|
},
|
|
143
144
|
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Socket factory: opens a plain or TLS socket with a connect timeout. */
|
|
2
|
+
import { type Socket } from 'node:net';
|
|
3
|
+
import type { TlsOption } from './connection-types.js';
|
|
4
|
+
export declare function openSocket(host: string, port: number, tls: TlsOption, connectTimeoutMs: number): Promise<Socket>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Socket factory: opens a plain or TLS socket with a connect timeout. */
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { connect as netConnect } from 'node:net';
|
|
4
|
+
import { connect as tlsConnect } from 'node:tls';
|
|
5
|
+
import { ConnectionClosedError } from './errors.js';
|
|
6
|
+
export function openSocket(host, port, tls, connectTimeoutMs) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
let settled = false;
|
|
9
|
+
const timer = setTimeout(() => {
|
|
10
|
+
if (settled)
|
|
11
|
+
return;
|
|
12
|
+
settled = true;
|
|
13
|
+
socket.destroy();
|
|
14
|
+
reject(new ConnectionClosedError(`connect timeout to ${host}:${port}`));
|
|
15
|
+
}, connectTimeoutMs);
|
|
16
|
+
const onError = (err) => {
|
|
17
|
+
if (settled)
|
|
18
|
+
return;
|
|
19
|
+
settled = true;
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
reject(new ConnectionClosedError(`connect failed: ${err.message}`));
|
|
22
|
+
};
|
|
23
|
+
const onReady = () => {
|
|
24
|
+
if (settled)
|
|
25
|
+
return;
|
|
26
|
+
settled = true;
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
socket.off('error', onError);
|
|
29
|
+
resolve(socket);
|
|
30
|
+
};
|
|
31
|
+
let socket;
|
|
32
|
+
if (tls) {
|
|
33
|
+
const tlsOpts = { host: host, port: port };
|
|
34
|
+
if (typeof tls === 'object') {
|
|
35
|
+
if (tls.caFile)
|
|
36
|
+
tlsOpts.ca = readFileSync(tls.caFile);
|
|
37
|
+
if (tls.rejectUnauthorized === false)
|
|
38
|
+
tlsOpts.rejectUnauthorized = false;
|
|
39
|
+
}
|
|
40
|
+
socket = tlsConnect(tlsOpts, onReady);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
socket = netConnect({ host: host, port: port }, onReady);
|
|
44
|
+
}
|
|
45
|
+
socket.on('error', onError);
|
|
46
|
+
});
|
|
47
|
+
}
|
package/dist/worker-base.d.ts
CHANGED
|
@@ -26,8 +26,10 @@ export declare class WorkerBase extends EventEmitter {
|
|
|
26
26
|
protected failedCount: number;
|
|
27
27
|
protected readyPromise: Promise<void>;
|
|
28
28
|
protected readyResolve: (() => void) | null;
|
|
29
|
+
protected readyFired: boolean;
|
|
29
30
|
protected loopPromise: Promise<void> | null;
|
|
30
31
|
protected heartbeatTimer: ReturnType<typeof setInterval> | null;
|
|
32
|
+
protected registeredGeneration: number;
|
|
31
33
|
constructor(queue: string, opts?: WorkerOptions);
|
|
32
34
|
pause(): void;
|
|
33
35
|
resume(): void;
|
|
@@ -35,6 +37,13 @@ export declare class WorkerBase extends EventEmitter {
|
|
|
35
37
|
isPaused(): boolean;
|
|
36
38
|
isClosed(): boolean;
|
|
37
39
|
waitUntilReady(): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* 'ready' is replayed to listeners attached after it fired: with autorun the
|
|
42
|
+
* loop starts inside the constructor, so a plain once-only event could be
|
|
43
|
+
* missed by `new Worker(...).on('ready', ...)` patterns.
|
|
44
|
+
*/
|
|
45
|
+
on(event: string | symbol, listener: (...args: unknown[]) => void): this;
|
|
46
|
+
once(event: string | symbol, listener: (...args: unknown[]) => void): this;
|
|
38
47
|
/**
|
|
39
48
|
* Cooperative cancel of a locally active job (mirrors the official client):
|
|
40
49
|
* marks the job and emits 'cancelled'; the processor is expected to check
|
|
@@ -43,8 +52,10 @@ export declare class WorkerBase extends EventEmitter {
|
|
|
43
52
|
cancelJob(jobId: string, reason?: string): boolean;
|
|
44
53
|
cancelAllJobs(reason?: string): void;
|
|
45
54
|
isJobCancelled(jobId: string): boolean;
|
|
46
|
-
/** Graceful shutdown: stop pulling, wait for in-flight jobs.
|
|
47
|
-
|
|
55
|
+
/** Graceful shutdown: stop pulling, wait for in-flight jobs.
|
|
56
|
+
* With `force` the wait for in-flight jobs is skipped (parity with the
|
|
57
|
+
* official client's `close(force)`). */
|
|
58
|
+
close(force?: boolean): Promise<void>;
|
|
48
59
|
protected safeCall(command: Record<string, unknown> & {
|
|
49
60
|
cmd: string;
|
|
50
61
|
}): Promise<void>;
|
package/dist/worker-base.js
CHANGED
|
@@ -28,8 +28,10 @@ export class WorkerBase extends EventEmitter {
|
|
|
28
28
|
failedCount = 0;
|
|
29
29
|
readyPromise;
|
|
30
30
|
readyResolve = null;
|
|
31
|
+
readyFired = false;
|
|
31
32
|
loopPromise = null;
|
|
32
33
|
heartbeatTimer = null;
|
|
34
|
+
registeredGeneration = -1;
|
|
33
35
|
constructor(queue, opts = {}) {
|
|
34
36
|
super();
|
|
35
37
|
if ((opts.concurrency ?? 4) < 1)
|
|
@@ -70,6 +72,23 @@ export class WorkerBase extends EventEmitter {
|
|
|
70
72
|
async waitUntilReady() {
|
|
71
73
|
await this.readyPromise;
|
|
72
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* 'ready' is replayed to listeners attached after it fired: with autorun the
|
|
77
|
+
* loop starts inside the constructor, so a plain once-only event could be
|
|
78
|
+
* missed by `new Worker(...).on('ready', ...)` patterns.
|
|
79
|
+
*/
|
|
80
|
+
on(event, listener) {
|
|
81
|
+
if (event === 'ready' && this.readyFired)
|
|
82
|
+
listener();
|
|
83
|
+
return super.on(event, listener);
|
|
84
|
+
}
|
|
85
|
+
once(event, listener) {
|
|
86
|
+
if (event === 'ready' && this.readyFired) {
|
|
87
|
+
listener();
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
return super.once(event, listener);
|
|
91
|
+
}
|
|
73
92
|
/**
|
|
74
93
|
* Cooperative cancel of a locally active job (mirrors the official client):
|
|
75
94
|
* marks the job and emits 'cancelled'; the processor is expected to check
|
|
@@ -92,20 +111,26 @@ export class WorkerBase extends EventEmitter {
|
|
|
92
111
|
isJobCancelled(jobId) {
|
|
93
112
|
return this.cancelledJobs.has(jobId);
|
|
94
113
|
}
|
|
95
|
-
/** Graceful shutdown: stop pulling, wait for in-flight jobs.
|
|
96
|
-
|
|
114
|
+
/** Graceful shutdown: stop pulling, wait for in-flight jobs.
|
|
115
|
+
* With `force` the wait for in-flight jobs is skipped (parity with the
|
|
116
|
+
* official client's `close(force)`). */
|
|
117
|
+
async close(force = false) {
|
|
97
118
|
if (this.closedFlag)
|
|
98
119
|
return;
|
|
99
120
|
this.stopped = true;
|
|
100
121
|
if (this.loopPromise)
|
|
101
122
|
await this.loopPromise;
|
|
102
|
-
while (this.active.size > 0)
|
|
123
|
+
while (!force && this.active.size > 0)
|
|
103
124
|
await sleep(20);
|
|
104
125
|
if (this.heartbeatTimer) {
|
|
105
126
|
clearInterval(this.heartbeatTimer);
|
|
106
127
|
this.heartbeatTimer = null;
|
|
107
128
|
}
|
|
108
|
-
|
|
129
|
+
// Only unregister when the loop actually registered (autorun: false and
|
|
130
|
+
// never run() means the server does not know this worker).
|
|
131
|
+
if (this.registeredGeneration >= 0) {
|
|
132
|
+
await this.safeCall({ cmd: 'UnregisterWorker', workerId: this.workerId });
|
|
133
|
+
}
|
|
109
134
|
this.connection.close();
|
|
110
135
|
this.closedFlag = true;
|
|
111
136
|
this.running = false;
|
package/dist/worker.js
CHANGED
|
@@ -32,6 +32,7 @@ export class Worker extends WorkerBase {
|
|
|
32
32
|
async loop() {
|
|
33
33
|
await this.register();
|
|
34
34
|
this.readyResolve?.();
|
|
35
|
+
this.readyFired = true;
|
|
35
36
|
this.emit('ready');
|
|
36
37
|
this.startHeartbeat();
|
|
37
38
|
let backoffIdx = 0;
|
|
@@ -63,6 +64,12 @@ export class Worker extends WorkerBase {
|
|
|
63
64
|
await sleep(20);
|
|
64
65
|
return;
|
|
65
66
|
}
|
|
67
|
+
// The registration is per-connection server state: after a reconnect the
|
|
68
|
+
// server no longer knows this worker (ListWorkers, skipIfNoWorker crons).
|
|
69
|
+
// Detect the new connection generation and re-register before pulling.
|
|
70
|
+
if (this.connection.isConnected && this.connection.generation !== this.registeredGeneration) {
|
|
71
|
+
await this.register();
|
|
72
|
+
}
|
|
66
73
|
const response = await this.connection.call({
|
|
67
74
|
cmd: 'PULLB',
|
|
68
75
|
queue: this.queue,
|
|
@@ -147,5 +154,6 @@ export class Worker extends WorkerBase {
|
|
|
147
154
|
pid: process.pid,
|
|
148
155
|
startedAt: Date.now(),
|
|
149
156
|
});
|
|
157
|
+
this.registeredGeneration = this.connection.generation;
|
|
150
158
|
}
|
|
151
159
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bunqueue Simple Mode — priority aging.
|
|
3
|
+
* Port of src/client/bunqueue/aging.ts: automatically boosts the priority of
|
|
4
|
+
* old waiting jobs. Same semantics; the job scan goes over TCP (GetJobs on
|
|
5
|
+
* the waiting and prioritized states) instead of the embedded manager.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Queue } from '../queue.js';
|
|
9
|
+
import type { PriorityAgingConfig } from './types.js';
|
|
10
|
+
|
|
11
|
+
export class PriorityAger<T = unknown> {
|
|
12
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
13
|
+
private readonly config: PriorityAgingConfig;
|
|
14
|
+
private readonly queue: Queue<T>;
|
|
15
|
+
|
|
16
|
+
constructor(config: PriorityAgingConfig, queue: Queue<T>) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.queue = queue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
start(): void {
|
|
22
|
+
const interval = this.config.interval ?? 60000;
|
|
23
|
+
this.timer = setInterval(() => {
|
|
24
|
+
void this.tick();
|
|
25
|
+
}, interval);
|
|
26
|
+
this.timer.unref?.();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private async tick(): Promise<void> {
|
|
30
|
+
const minAge = this.config.minAge ?? 60000;
|
|
31
|
+
const boost = this.config.boost ?? 1;
|
|
32
|
+
const maxPriority = this.config.maxPriority ?? 100;
|
|
33
|
+
const maxScan = this.config.maxScan ?? 100;
|
|
34
|
+
|
|
35
|
+
// Both waiting and prioritized jobs (priority > 0 means "prioritized")
|
|
36
|
+
const [waiting, prioritized] = await Promise.all([
|
|
37
|
+
this.queue.getJobs({ state: 'waiting', start: 0, end: maxScan }),
|
|
38
|
+
this.queue.getJobs({ state: 'prioritized', start: 0, end: maxScan }),
|
|
39
|
+
]);
|
|
40
|
+
const jobs = [...waiting, ...prioritized];
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
|
|
43
|
+
for (const job of jobs) {
|
|
44
|
+
const age = now - job.timestamp;
|
|
45
|
+
if (age >= minAge && job.priority < maxPriority) {
|
|
46
|
+
const newPriority = Math.min(job.priority + boost, maxPriority);
|
|
47
|
+
try {
|
|
48
|
+
await this.queue.changeJobPriority(job.id, { priority: newPriority });
|
|
49
|
+
} catch {
|
|
50
|
+
// Best-effort — the job may have been processed meanwhile
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
destroy(): void {
|
|
57
|
+
if (this.timer) {
|
|
58
|
+
clearInterval(this.timer);
|
|
59
|
+
this.timer = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|