bunqueue-client 0.1.0
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/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/connection-types.d.ts +24 -0
- package/dist/connection-types.js +2 -0
- package/dist/connection.d.ts +44 -0
- package/dist/connection.js +229 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.js +25 -0
- package/dist/flow-types.d.ts +34 -0
- package/dist/flow-types.js +2 -0
- package/dist/flow.d.ts +31 -0
- package/dist/flow.js +186 -0
- package/dist/frame.d.ts +16 -0
- package/dist/frame.js +57 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +12 -0
- package/dist/job.d.ts +56 -0
- package/dist/job.js +151 -0
- package/dist/queue-admin.d.ts +63 -0
- package/dist/queue-admin.js +143 -0
- package/dist/queue-control.d.ts +44 -0
- package/dist/queue-control.js +85 -0
- package/dist/queue-query.d.ts +58 -0
- package/dist/queue-query.js +165 -0
- package/dist/queue.d.ts +50 -0
- package/dist/queue.js +77 -0
- package/dist/types.d.ts +74 -0
- package/dist/types.js +50 -0
- package/dist/worker-base.d.ts +51 -0
- package/dist/worker-base.js +122 -0
- package/dist/worker-types.d.ts +27 -0
- package/dist/worker-types.js +5 -0
- package/dist/worker.d.ts +20 -0
- package/dist/worker.js +151 -0
- package/package.json +64 -0
- package/src/connection-types.ts +21 -0
- package/src/connection.ts +242 -0
- package/src/errors.ts +26 -0
- package/src/flow-types.ts +40 -0
- package/src/flow.ts +236 -0
- package/src/frame.ts +62 -0
- package/src/index.ts +41 -0
- package/src/job.ts +194 -0
- package/src/queue-admin.ts +213 -0
- package/src/queue-control.ts +145 -0
- package/src/queue-query.ts +220 -0
- package/src/queue.ts +112 -0
- package/src/types.ts +133 -0
- package/src/worker-base.ts +132 -0
- package/src/worker-types.ts +32 -0
- package/src/worker.ts +174 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** Shared option types + wire mapping, mirroring the official client. */
|
|
2
|
+
export interface BackoffOptions {
|
|
3
|
+
type: 'fixed' | 'exponential';
|
|
4
|
+
delay: number;
|
|
5
|
+
maxDelay?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface DeduplicationOptions {
|
|
8
|
+
id: string;
|
|
9
|
+
ttl?: number;
|
|
10
|
+
extend?: boolean;
|
|
11
|
+
replace?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface RepeatOptions {
|
|
14
|
+
every?: number;
|
|
15
|
+
limit?: number;
|
|
16
|
+
pattern?: string;
|
|
17
|
+
count?: number;
|
|
18
|
+
startDate?: number;
|
|
19
|
+
endDate?: number;
|
|
20
|
+
tz?: string;
|
|
21
|
+
immediately?: boolean;
|
|
22
|
+
offset?: number;
|
|
23
|
+
jobId?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface JobOptions {
|
|
26
|
+
priority?: number;
|
|
27
|
+
delay?: number;
|
|
28
|
+
/** Max attempts (server name: maxAttempts). */
|
|
29
|
+
attempts?: number;
|
|
30
|
+
backoff?: number | BackoffOptions;
|
|
31
|
+
ttl?: number;
|
|
32
|
+
timeout?: number;
|
|
33
|
+
/** Custom job id (idempotent add). */
|
|
34
|
+
jobId?: string;
|
|
35
|
+
deduplication?: DeduplicationOptions;
|
|
36
|
+
dependsOn?: string[];
|
|
37
|
+
parentId?: string;
|
|
38
|
+
childrenIds?: string[];
|
|
39
|
+
tags?: string[];
|
|
40
|
+
groupId?: string;
|
|
41
|
+
lifo?: boolean;
|
|
42
|
+
removeOnComplete?: boolean;
|
|
43
|
+
removeOnFail?: boolean;
|
|
44
|
+
stallTimeout?: number;
|
|
45
|
+
durable?: boolean;
|
|
46
|
+
repeat?: RepeatOptions;
|
|
47
|
+
debounce?: {
|
|
48
|
+
id: string;
|
|
49
|
+
ttl?: number;
|
|
50
|
+
};
|
|
51
|
+
stackTraceLimit?: number;
|
|
52
|
+
keepLogs?: number;
|
|
53
|
+
sizeLimit?: number;
|
|
54
|
+
timestamp?: number;
|
|
55
|
+
failParentOnFailure?: boolean;
|
|
56
|
+
removeDependencyOnFailure?: boolean;
|
|
57
|
+
ignoreDependencyOnFailure?: boolean;
|
|
58
|
+
continueParentOnFailure?: boolean;
|
|
59
|
+
}
|
|
60
|
+
/** Map SDK JobOptions to the PUSH/PUSHB wire fields (undefined keys dropped). */
|
|
61
|
+
export declare function wireJobOptions(opts?: JobOptions): Record<string, unknown>;
|
|
62
|
+
/** The job name travels inside `data`, mirroring the official client. */
|
|
63
|
+
export declare function jobPayload(name: string, data: unknown): Record<string, unknown>;
|
|
64
|
+
export interface JobCounts {
|
|
65
|
+
waiting: number;
|
|
66
|
+
prioritized: number;
|
|
67
|
+
delayed: number;
|
|
68
|
+
active: number;
|
|
69
|
+
completed: number;
|
|
70
|
+
failed: number;
|
|
71
|
+
'waiting-children': number;
|
|
72
|
+
paused: number;
|
|
73
|
+
}
|
|
74
|
+
export type JobStateName = 'waiting' | 'prioritized' | 'delayed' | 'active' | 'completed' | 'failed' | 'waiting-children';
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Shared option types + wire mapping, mirroring the official client. */
|
|
2
|
+
import { compact } from './frame.js';
|
|
3
|
+
/** Map SDK JobOptions to the PUSH/PUSHB wire fields (undefined keys dropped). */
|
|
4
|
+
export function wireJobOptions(opts = {}) {
|
|
5
|
+
return compact({
|
|
6
|
+
priority: opts.priority,
|
|
7
|
+
delay: opts.delay,
|
|
8
|
+
maxAttempts: opts.attempts,
|
|
9
|
+
backoff: opts.backoff,
|
|
10
|
+
ttl: opts.ttl,
|
|
11
|
+
timeout: opts.timeout,
|
|
12
|
+
jobId: opts.jobId,
|
|
13
|
+
uniqueKey: opts.deduplication?.id,
|
|
14
|
+
dedup: opts.deduplication
|
|
15
|
+
? compact({
|
|
16
|
+
ttl: opts.deduplication.ttl,
|
|
17
|
+
extend: opts.deduplication.extend,
|
|
18
|
+
replace: opts.deduplication.replace,
|
|
19
|
+
})
|
|
20
|
+
: undefined,
|
|
21
|
+
dependsOn: opts.dependsOn,
|
|
22
|
+
parentId: opts.parentId,
|
|
23
|
+
childrenIds: opts.childrenIds,
|
|
24
|
+
tags: opts.tags,
|
|
25
|
+
groupId: opts.groupId,
|
|
26
|
+
lifo: opts.lifo,
|
|
27
|
+
removeOnComplete: typeof opts.removeOnComplete === 'boolean' ? opts.removeOnComplete : undefined,
|
|
28
|
+
removeOnFail: typeof opts.removeOnFail === 'boolean' ? opts.removeOnFail : undefined,
|
|
29
|
+
stallTimeout: opts.stallTimeout,
|
|
30
|
+
durable: opts.durable,
|
|
31
|
+
repeat: opts.repeat,
|
|
32
|
+
debounceId: opts.debounce?.id,
|
|
33
|
+
debounceTtl: opts.debounce?.ttl,
|
|
34
|
+
stackTraceLimit: opts.stackTraceLimit,
|
|
35
|
+
keepLogs: opts.keepLogs,
|
|
36
|
+
sizeLimit: opts.sizeLimit,
|
|
37
|
+
timestamp: opts.timestamp,
|
|
38
|
+
failParentOnFailure: opts.failParentOnFailure,
|
|
39
|
+
removeDependencyOnFailure: opts.removeDependencyOnFailure,
|
|
40
|
+
ignoreDependencyOnFailure: opts.ignoreDependencyOnFailure,
|
|
41
|
+
continueParentOnFailure: opts.continueParentOnFailure,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** The job name travels inside `data`, mirroring the official client. */
|
|
45
|
+
export function jobPayload(name, data) {
|
|
46
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
47
|
+
return { name, ...data };
|
|
48
|
+
}
|
|
49
|
+
return data === undefined || data === null ? { name } : { name, payload: data };
|
|
50
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkerBase: lifecycle state, cooperative cancellation and safe command
|
|
3
|
+
* dispatch shared by the Worker pull loop (worker.ts).
|
|
4
|
+
*/
|
|
5
|
+
import { EventEmitter } from 'node:events';
|
|
6
|
+
import { Connection } from './connection.js';
|
|
7
|
+
import { type WorkerOptions } from './worker-types.js';
|
|
8
|
+
export declare class WorkerBase extends EventEmitter {
|
|
9
|
+
readonly queue: string;
|
|
10
|
+
readonly concurrency: number;
|
|
11
|
+
readonly batchSize: number;
|
|
12
|
+
readonly pollTimeoutMs: number;
|
|
13
|
+
readonly lockTtlMs: number;
|
|
14
|
+
readonly heartbeatIntervalS: number;
|
|
15
|
+
readonly workerId: string;
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly connection: Connection;
|
|
18
|
+
protected readonly active: Map<string, string>;
|
|
19
|
+
protected readonly cancelledJobs: Set<string>;
|
|
20
|
+
protected stopped: boolean;
|
|
21
|
+
protected paused: boolean;
|
|
22
|
+
protected closedFlag: boolean;
|
|
23
|
+
protected running: boolean;
|
|
24
|
+
protected wasBusy: boolean;
|
|
25
|
+
protected processed: number;
|
|
26
|
+
protected failedCount: number;
|
|
27
|
+
protected readyPromise: Promise<void>;
|
|
28
|
+
protected readyResolve: (() => void) | null;
|
|
29
|
+
protected loopPromise: Promise<void> | null;
|
|
30
|
+
protected heartbeatTimer: ReturnType<typeof setInterval> | null;
|
|
31
|
+
constructor(queue: string, opts?: WorkerOptions);
|
|
32
|
+
pause(): void;
|
|
33
|
+
resume(): void;
|
|
34
|
+
isRunning(): boolean;
|
|
35
|
+
isPaused(): boolean;
|
|
36
|
+
isClosed(): boolean;
|
|
37
|
+
waitUntilReady(): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Cooperative cancel of a locally active job (mirrors the official client):
|
|
40
|
+
* marks the job and emits 'cancelled'; the processor is expected to check
|
|
41
|
+
* isJobCancelled() and abort. Returns false if the job is not active here.
|
|
42
|
+
*/
|
|
43
|
+
cancelJob(jobId: string, reason?: string): boolean;
|
|
44
|
+
cancelAllJobs(reason?: string): void;
|
|
45
|
+
isJobCancelled(jobId: string): boolean;
|
|
46
|
+
/** Graceful shutdown: stop pulling, wait for in-flight jobs. */
|
|
47
|
+
close(): Promise<void>;
|
|
48
|
+
protected safeCall(command: Record<string, unknown> & {
|
|
49
|
+
cmd: string;
|
|
50
|
+
}): Promise<void>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkerBase: lifecycle state, cooperative cancellation and safe command
|
|
3
|
+
* dispatch shared by the Worker pull loop (worker.ts).
|
|
4
|
+
*/
|
|
5
|
+
import { randomBytes } from 'node:crypto';
|
|
6
|
+
import { EventEmitter } from 'node:events';
|
|
7
|
+
import { hostname } from 'node:os';
|
|
8
|
+
import { Connection } from './connection.js';
|
|
9
|
+
import { MAX_POLL_TIMEOUT_MS, sleep } from './worker-types.js';
|
|
10
|
+
export class WorkerBase extends EventEmitter {
|
|
11
|
+
queue;
|
|
12
|
+
concurrency;
|
|
13
|
+
batchSize;
|
|
14
|
+
pollTimeoutMs;
|
|
15
|
+
lockTtlMs;
|
|
16
|
+
heartbeatIntervalS;
|
|
17
|
+
workerId;
|
|
18
|
+
name;
|
|
19
|
+
connection;
|
|
20
|
+
active = new Map(); // job id -> lock token
|
|
21
|
+
cancelledJobs = new Set();
|
|
22
|
+
stopped = false;
|
|
23
|
+
paused = false;
|
|
24
|
+
closedFlag = false;
|
|
25
|
+
running = false;
|
|
26
|
+
wasBusy = false;
|
|
27
|
+
processed = 0;
|
|
28
|
+
failedCount = 0;
|
|
29
|
+
readyPromise;
|
|
30
|
+
readyResolve = null;
|
|
31
|
+
loopPromise = null;
|
|
32
|
+
heartbeatTimer = null;
|
|
33
|
+
constructor(queue, opts = {}) {
|
|
34
|
+
super();
|
|
35
|
+
if ((opts.concurrency ?? 4) < 1)
|
|
36
|
+
throw new Error('concurrency must be >= 1');
|
|
37
|
+
this.queue = queue;
|
|
38
|
+
this.concurrency = opts.concurrency ?? 4;
|
|
39
|
+
this.batchSize = Math.max(1, opts.batchSize ?? 10);
|
|
40
|
+
this.pollTimeoutMs = Math.min(opts.pollTimeoutMs ?? 5000, MAX_POLL_TIMEOUT_MS);
|
|
41
|
+
this.lockTtlMs = opts.lockTtlMs ?? 30_000;
|
|
42
|
+
this.heartbeatIntervalS = opts.heartbeatIntervalS ?? 10;
|
|
43
|
+
this.workerId = `ts-${hostname()}-${process.pid}-${randomBytes(4).toString('hex')}`;
|
|
44
|
+
this.name = opts.name ?? this.workerId;
|
|
45
|
+
this.connection = new Connection({
|
|
46
|
+
host: opts.host,
|
|
47
|
+
port: opts.port,
|
|
48
|
+
token: opts.token,
|
|
49
|
+
tls: opts.tls,
|
|
50
|
+
});
|
|
51
|
+
this.readyPromise = new Promise((resolve) => {
|
|
52
|
+
this.readyResolve = resolve;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
pause() {
|
|
56
|
+
this.paused = true;
|
|
57
|
+
}
|
|
58
|
+
resume() {
|
|
59
|
+
this.paused = false;
|
|
60
|
+
}
|
|
61
|
+
isRunning() {
|
|
62
|
+
return this.running && !this.closedFlag;
|
|
63
|
+
}
|
|
64
|
+
isPaused() {
|
|
65
|
+
return this.paused;
|
|
66
|
+
}
|
|
67
|
+
isClosed() {
|
|
68
|
+
return this.closedFlag;
|
|
69
|
+
}
|
|
70
|
+
async waitUntilReady() {
|
|
71
|
+
await this.readyPromise;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Cooperative cancel of a locally active job (mirrors the official client):
|
|
75
|
+
* marks the job and emits 'cancelled'; the processor is expected to check
|
|
76
|
+
* isJobCancelled() and abort. Returns false if the job is not active here.
|
|
77
|
+
*/
|
|
78
|
+
cancelJob(jobId, reason) {
|
|
79
|
+
if (this.active.has(jobId)) {
|
|
80
|
+
this.cancelledJobs.add(jobId);
|
|
81
|
+
this.emit('cancelled', { jobId, reason: reason ?? 'Job cancelled by worker' });
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
cancelAllJobs(reason) {
|
|
87
|
+
for (const jobId of this.active.keys()) {
|
|
88
|
+
this.cancelledJobs.add(jobId);
|
|
89
|
+
this.emit('cancelled', { jobId, reason: reason ?? 'All jobs cancelled' });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
isJobCancelled(jobId) {
|
|
93
|
+
return this.cancelledJobs.has(jobId);
|
|
94
|
+
}
|
|
95
|
+
/** Graceful shutdown: stop pulling, wait for in-flight jobs. */
|
|
96
|
+
async close() {
|
|
97
|
+
if (this.closedFlag)
|
|
98
|
+
return;
|
|
99
|
+
this.stopped = true;
|
|
100
|
+
if (this.loopPromise)
|
|
101
|
+
await this.loopPromise;
|
|
102
|
+
while (this.active.size > 0)
|
|
103
|
+
await sleep(20);
|
|
104
|
+
if (this.heartbeatTimer) {
|
|
105
|
+
clearInterval(this.heartbeatTimer);
|
|
106
|
+
this.heartbeatTimer = null;
|
|
107
|
+
}
|
|
108
|
+
await this.safeCall({ cmd: 'UnregisterWorker', workerId: this.workerId });
|
|
109
|
+
this.connection.close();
|
|
110
|
+
this.closedFlag = true;
|
|
111
|
+
this.running = false;
|
|
112
|
+
this.emit('closed');
|
|
113
|
+
}
|
|
114
|
+
async safeCall(command) {
|
|
115
|
+
try {
|
|
116
|
+
await this.connection.call(command);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
this.emit('error', err);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Worker option types and shared constants. */
|
|
2
|
+
import type { TlsOption } from './connection.js';
|
|
3
|
+
import type { Job } from './job.js';
|
|
4
|
+
export type Processor<T = unknown, R = unknown> = (job: Job<T>) => R | Promise<R>;
|
|
5
|
+
export interface WorkerOptions {
|
|
6
|
+
host?: string;
|
|
7
|
+
port?: number;
|
|
8
|
+
token?: string;
|
|
9
|
+
tls?: TlsOption;
|
|
10
|
+
/** Max jobs processed in parallel (default 4). */
|
|
11
|
+
concurrency?: number;
|
|
12
|
+
/** Max jobs fetched per PULLB (default 10, capped by free slots). */
|
|
13
|
+
batchSize?: number;
|
|
14
|
+
/** Server-side long-poll timeout in ms (default 5000, max 30000). */
|
|
15
|
+
pollTimeoutMs?: number;
|
|
16
|
+
/** Job lock TTL in ms (default 30000). */
|
|
17
|
+
lockTtlMs?: number;
|
|
18
|
+
/** Worker + job heartbeat interval in seconds (default 10). */
|
|
19
|
+
heartbeatIntervalS?: number;
|
|
20
|
+
/** Start the loop at construction (default true, mirrors the TS client). */
|
|
21
|
+
autorun?: boolean;
|
|
22
|
+
name?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare const MAX_POLL_TIMEOUT_MS = 30000;
|
|
25
|
+
export declare const MAX_STACK_LINES = 20;
|
|
26
|
+
export declare const RECONNECT_BACKOFF_MS: number[];
|
|
27
|
+
export declare const sleep: (ms: number) => Promise<void>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Worker option types and shared constants. */
|
|
2
|
+
export const MAX_POLL_TIMEOUT_MS = 30_000;
|
|
3
|
+
export const MAX_STACK_LINES = 20;
|
|
4
|
+
export const RECONNECT_BACKOFF_MS = [500, 1000, 2000, 5000];
|
|
5
|
+
export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
package/dist/worker.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker: pulls jobs over TCP and runs a processor with bounded concurrency.
|
|
3
|
+
*
|
|
4
|
+
* All queue semantics (retry, backoff, DLQ, stall detection, priorities)
|
|
5
|
+
* live in the bunqueue server — the worker pulls, heartbeats and acks/fails.
|
|
6
|
+
* Lifecycle state and cooperative cancel live in WorkerBase.
|
|
7
|
+
*/
|
|
8
|
+
import { WorkerBase } from './worker-base.js';
|
|
9
|
+
import { type Processor, type WorkerOptions } from './worker-types.js';
|
|
10
|
+
export declare class Worker<T = unknown, R = unknown> extends WorkerBase {
|
|
11
|
+
private readonly processor;
|
|
12
|
+
constructor(queue: string, processor: Processor<T, R>, opts?: WorkerOptions);
|
|
13
|
+
/** Start the pull loop (no-op if already running). */
|
|
14
|
+
run(): void;
|
|
15
|
+
private loop;
|
|
16
|
+
private pollOnce;
|
|
17
|
+
private runJob;
|
|
18
|
+
private startHeartbeat;
|
|
19
|
+
private register;
|
|
20
|
+
}
|
package/dist/worker.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker: pulls jobs over TCP and runs a processor with bounded concurrency.
|
|
3
|
+
*
|
|
4
|
+
* All queue semantics (retry, backoff, DLQ, stall detection, priorities)
|
|
5
|
+
* live in the bunqueue server — the worker pulls, heartbeats and acks/fails.
|
|
6
|
+
* Lifecycle state and cooperative cancel live in WorkerBase.
|
|
7
|
+
*/
|
|
8
|
+
import { hostname } from 'node:os';
|
|
9
|
+
import { CommandTimeoutError, ConnectionClosedError, UnrecoverableError } from './errors.js';
|
|
10
|
+
import { compact } from './frame.js';
|
|
11
|
+
import { Job } from './job.js';
|
|
12
|
+
import { WorkerBase } from './worker-base.js';
|
|
13
|
+
import { MAX_STACK_LINES, RECONNECT_BACKOFF_MS, sleep, } from './worker-types.js';
|
|
14
|
+
export class Worker extends WorkerBase {
|
|
15
|
+
processor;
|
|
16
|
+
constructor(queue, processor, opts = {}) {
|
|
17
|
+
super(queue, opts);
|
|
18
|
+
this.processor = processor;
|
|
19
|
+
if (opts.autorun !== false)
|
|
20
|
+
this.run();
|
|
21
|
+
}
|
|
22
|
+
/** Start the pull loop (no-op if already running). */
|
|
23
|
+
run() {
|
|
24
|
+
if (this.running || this.closedFlag)
|
|
25
|
+
return;
|
|
26
|
+
this.running = true;
|
|
27
|
+
this.loopPromise = this.loop().catch((err) => {
|
|
28
|
+
this.emit('error', err);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// -------------------------------------------------------------------- loop
|
|
32
|
+
async loop() {
|
|
33
|
+
await this.register();
|
|
34
|
+
this.readyResolve?.();
|
|
35
|
+
this.emit('ready');
|
|
36
|
+
this.startHeartbeat();
|
|
37
|
+
let backoffIdx = 0;
|
|
38
|
+
while (!this.stopped) {
|
|
39
|
+
if (this.paused) {
|
|
40
|
+
await sleep(50);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
await this.pollOnce();
|
|
45
|
+
backoffIdx = 0;
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
this.emit('error', err);
|
|
49
|
+
if (err instanceof ConnectionClosedError || err instanceof CommandTimeoutError) {
|
|
50
|
+
const delay = RECONNECT_BACKOFF_MS[Math.min(backoffIdx, RECONNECT_BACKOFF_MS.length - 1)];
|
|
51
|
+
backoffIdx += 1;
|
|
52
|
+
await sleep(delay);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
await sleep(200);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async pollOnce() {
|
|
61
|
+
const free = this.concurrency - this.active.size;
|
|
62
|
+
if (free <= 0) {
|
|
63
|
+
await sleep(20);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const response = await this.connection.call({
|
|
67
|
+
cmd: 'PULLB',
|
|
68
|
+
queue: this.queue,
|
|
69
|
+
count: Math.min(free, this.batchSize),
|
|
70
|
+
timeout: this.pollTimeoutMs,
|
|
71
|
+
owner: this.workerId,
|
|
72
|
+
lockTtl: this.lockTtlMs,
|
|
73
|
+
}, this.pollTimeoutMs + 10_000);
|
|
74
|
+
const jobs = (response.jobs ?? []);
|
|
75
|
+
const tokens = (response.tokens ?? []);
|
|
76
|
+
if (jobs.length === 0) {
|
|
77
|
+
if (this.wasBusy && this.active.size === 0) {
|
|
78
|
+
this.wasBusy = false;
|
|
79
|
+
this.emit('drained');
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
this.wasBusy = true;
|
|
84
|
+
for (let i = 0; i < jobs.length; i++) {
|
|
85
|
+
const jobId = String(jobs[i].id);
|
|
86
|
+
this.active.set(jobId, tokens[i]);
|
|
87
|
+
// fire-and-forget: bounded by the free-slot accounting above
|
|
88
|
+
void this.runJob(jobs[i], tokens[i]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async runJob(raw, token) {
|
|
92
|
+
const job = new Job(raw, this.connection, token, (j, p) => this.emit('progress', j, p));
|
|
93
|
+
this.emit('active', job);
|
|
94
|
+
try {
|
|
95
|
+
const result = await this.processor(job);
|
|
96
|
+
this.processed += 1;
|
|
97
|
+
await this.safeCall(compact({ cmd: 'ACK', id: job.id, token, result: result ?? undefined }));
|
|
98
|
+
this.emit('completed', job, result);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
this.failedCount += 1;
|
|
102
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
103
|
+
const stack = (error.stack ?? error.message).split('\n').slice(-MAX_STACK_LINES);
|
|
104
|
+
await this.safeCall(compact({
|
|
105
|
+
cmd: 'FAIL',
|
|
106
|
+
id: job.id,
|
|
107
|
+
token,
|
|
108
|
+
error: error.message || error.name,
|
|
109
|
+
stack,
|
|
110
|
+
unrecoverable: err instanceof UnrecoverableError ? true : undefined,
|
|
111
|
+
}));
|
|
112
|
+
this.emit('failed', job, error);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
this.active.delete(job.id);
|
|
116
|
+
this.cancelledJobs.delete(job.id);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// --------------------------------------------------------------- heartbeat
|
|
120
|
+
startHeartbeat() {
|
|
121
|
+
this.heartbeatTimer = setInterval(() => {
|
|
122
|
+
void (async () => {
|
|
123
|
+
await this.safeCall({
|
|
124
|
+
cmd: 'Heartbeat',
|
|
125
|
+
id: this.workerId,
|
|
126
|
+
activeJobs: this.active.size,
|
|
127
|
+
processed: this.processed,
|
|
128
|
+
failed: this.failedCount,
|
|
129
|
+
});
|
|
130
|
+
if (this.active.size > 0) {
|
|
131
|
+
const ids = [...this.active.keys()];
|
|
132
|
+
const tokens = ids.map((id) => this.active.get(id));
|
|
133
|
+
await this.safeCall({ cmd: 'JobHeartbeatB', ids, tokens });
|
|
134
|
+
}
|
|
135
|
+
})();
|
|
136
|
+
}, this.heartbeatIntervalS * 1000);
|
|
137
|
+
this.heartbeatTimer.unref?.(); // don't keep the process alive for heartbeats
|
|
138
|
+
}
|
|
139
|
+
async register() {
|
|
140
|
+
await this.safeCall({
|
|
141
|
+
cmd: 'RegisterWorker',
|
|
142
|
+
name: this.name,
|
|
143
|
+
queues: [this.queue],
|
|
144
|
+
concurrency: this.concurrency,
|
|
145
|
+
workerId: this.workerId,
|
|
146
|
+
hostname: hostname(),
|
|
147
|
+
pid: process.pid,
|
|
148
|
+
startedAt: Date.now(),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bunqueue-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cross-runtime TypeScript client for the bunqueue job queue server — Node.js, Bun, Deno and Cloudflare Workers",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc -p tsconfig.json",
|
|
27
|
+
"test": "bun tests/e2e.ts",
|
|
28
|
+
"test:integration": "bun tests/integration.ts",
|
|
29
|
+
"lint": "biome lint src tests",
|
|
30
|
+
"format": "biome format --write src tests",
|
|
31
|
+
"check": "biome check src tests",
|
|
32
|
+
"test:workers": "cd tests/workers && node run.mjs"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"queue",
|
|
36
|
+
"jobs",
|
|
37
|
+
"worker",
|
|
38
|
+
"bunqueue",
|
|
39
|
+
"task-queue",
|
|
40
|
+
"background-jobs",
|
|
41
|
+
"nodejs",
|
|
42
|
+
"deno",
|
|
43
|
+
"bun",
|
|
44
|
+
"cloudflare-workers"
|
|
45
|
+
],
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"msgpackr": "^1.11.8"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"typescript": "^5.7.0",
|
|
51
|
+
"@types/node": "^22.0.0"
|
|
52
|
+
},
|
|
53
|
+
"author": "Egeo Minotti",
|
|
54
|
+
"homepage": "https://bunqueue.dev/guide/sdks/",
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "git+https://github.com/egeominotti/bunqueue.git",
|
|
58
|
+
"directory": "sdk/typescript"
|
|
59
|
+
},
|
|
60
|
+
"bugs": {
|
|
61
|
+
"url": "https://github.com/egeominotti/bunqueue/issues"
|
|
62
|
+
},
|
|
63
|
+
"sideEffects": false
|
|
64
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Connection option and message types. */
|
|
2
|
+
|
|
3
|
+
export type TlsOption = boolean | { caFile?: string; rejectUnauthorized?: boolean } | undefined;
|
|
4
|
+
|
|
5
|
+
export interface ConnectionOptions {
|
|
6
|
+
host?: string;
|
|
7
|
+
port?: number;
|
|
8
|
+
token?: string;
|
|
9
|
+
tls?: TlsOption;
|
|
10
|
+
connectTimeoutMs?: number;
|
|
11
|
+
commandTimeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type Command = Record<string, unknown> & { cmd: string };
|
|
15
|
+
export type Response = Record<string, unknown> & { ok: boolean };
|
|
16
|
+
|
|
17
|
+
export interface Pending {
|
|
18
|
+
resolve: (response: Response) => void;
|
|
19
|
+
reject: (error: Error) => void;
|
|
20
|
+
timer: ReturnType<typeof setTimeout>;
|
|
21
|
+
}
|