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
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue query surface: job lookup, state, results, counts, logs, children.
|
|
3
|
+
* Methods are merged onto Queue.prototype by queue.ts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { CommandError } from './errors.js';
|
|
7
|
+
import { compact } from './frame.js';
|
|
8
|
+
import { Job } from './job.js';
|
|
9
|
+
import type { Queue } from './queue.js';
|
|
10
|
+
import type { JobCounts } from './types.js';
|
|
11
|
+
|
|
12
|
+
type Ctx = Queue<unknown>;
|
|
13
|
+
type Raw = Record<string, unknown>;
|
|
14
|
+
|
|
15
|
+
function unwrapValues<R>(response: Raw): Record<string, R> {
|
|
16
|
+
const data = (response.data ?? {}) as Raw;
|
|
17
|
+
return (data.values ?? response.values ?? data ?? {}) as Record<string, R>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const queryMethods = {
|
|
21
|
+
async getJob<T = unknown>(this: Ctx, id: string): Promise<Job<T> | null> {
|
|
22
|
+
try {
|
|
23
|
+
const response = await this.call({ cmd: 'GetJob', id });
|
|
24
|
+
const raw = response.job as Raw | null;
|
|
25
|
+
return raw ? new Job<T>(raw, this.connection) : null;
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (err instanceof CommandError) return null; // server: 'Job not found'
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
async getJobByCustomId<T = unknown>(this: Ctx, customId: string): Promise<Job<T> | null> {
|
|
33
|
+
try {
|
|
34
|
+
const response = await this.call({ cmd: 'GetJobByCustomId', customId });
|
|
35
|
+
const raw = response.job as Raw | null;
|
|
36
|
+
return raw ? new Job<T>(raw, this.connection) : null;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
if (err instanceof CommandError) return null;
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
/** BullMQ v5: resolve the job id registered under a deduplication id. */
|
|
44
|
+
async getDeduplicationJobId(this: Ctx, deduplicationId: string): Promise<string | null> {
|
|
45
|
+
const job = await this.getJobByCustomId(deduplicationId);
|
|
46
|
+
return job ? job.id : null;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async getJobs<T = unknown>(
|
|
50
|
+
this: Ctx,
|
|
51
|
+
opts: { state?: string | string[]; start?: number; end?: number } = {}
|
|
52
|
+
): Promise<Job<T>[]> {
|
|
53
|
+
const start = opts.start ?? 0;
|
|
54
|
+
const end = opts.end !== undefined && opts.end >= 0 ? opts.end : 1000;
|
|
55
|
+
const response = await this.call(
|
|
56
|
+
compact({
|
|
57
|
+
cmd: 'GetJobs',
|
|
58
|
+
queue: this.name,
|
|
59
|
+
state: opts.state,
|
|
60
|
+
offset: start,
|
|
61
|
+
limit: end - start,
|
|
62
|
+
}) as { cmd: string }
|
|
63
|
+
);
|
|
64
|
+
const jobs = (response.jobs ?? []) as Raw[];
|
|
65
|
+
return jobs.map((raw) => new Job<T>(raw, this.connection));
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
getWaiting<T = unknown>(this: Ctx, start?: number, end?: number): Promise<Job<T>[]> {
|
|
69
|
+
return this.getJobs({ state: ['waiting', 'prioritized'], start, end });
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
getDelayed<T = unknown>(this: Ctx, start?: number, end?: number): Promise<Job<T>[]> {
|
|
73
|
+
return this.getJobs({ state: 'delayed', start, end });
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
getActive<T = unknown>(this: Ctx, start?: number, end?: number): Promise<Job<T>[]> {
|
|
77
|
+
return this.getJobs({ state: 'active', start, end });
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
getCompleted<T = unknown>(this: Ctx, start?: number, end?: number): Promise<Job<T>[]> {
|
|
81
|
+
return this.getJobs({ state: 'completed', start, end });
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
getFailed<T = unknown>(this: Ctx, start?: number, end?: number): Promise<Job<T>[]> {
|
|
85
|
+
return this.getJobs({ state: 'failed', start, end });
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
getPrioritized<T = unknown>(this: Ctx, start?: number, end?: number): Promise<Job<T>[]> {
|
|
89
|
+
return this.getJobs({ state: 'prioritized', start, end });
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
getWaitingChildren<T = unknown>(this: Ctx, start?: number, end?: number): Promise<Job<T>[]> {
|
|
93
|
+
return this.getJobs({ state: 'waiting-children', start, end });
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
async getJobState(this: Ctx, id: string): Promise<string> {
|
|
97
|
+
const response = await this.call({ cmd: 'GetState', id });
|
|
98
|
+
return String(response.state);
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
async getResult<R = unknown>(this: Ctx, id: string): Promise<R> {
|
|
102
|
+
const response = await this.call({ cmd: 'GetResult', id });
|
|
103
|
+
return response.result as R;
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
async getChildrenValues<R = unknown>(this: Ctx, id: string): Promise<Record<string, R>> {
|
|
107
|
+
return unwrapValues<R>(await this.call({ cmd: 'GetChildrenValues', id }));
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
async getFailedChildrenValues(this: Ctx, id: string): Promise<Record<string, string>> {
|
|
111
|
+
return unwrapValues<string>(await this.call({ cmd: 'GetFailedChildrenValues', id }));
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
async getIgnoredChildrenFailures(this: Ctx, id: string): Promise<Record<string, string>> {
|
|
115
|
+
return unwrapValues<string>(await this.call({ cmd: 'GetIgnoredChildrenFailures', id }));
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
/** Detach a child job from its parent's dependency list. */
|
|
119
|
+
async removeChildDependency(this: Ctx, id: string): Promise<void> {
|
|
120
|
+
await this.call({ cmd: 'RemoveChildDependency', id });
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
/** Remove all still-unprocessed children of a parent job. */
|
|
124
|
+
async removeUnprocessedChildren(this: Ctx, id: string): Promise<void> {
|
|
125
|
+
await this.call({ cmd: 'RemoveUnprocessedChildren', id });
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
/** Block until the job finishes; returns its result. */
|
|
129
|
+
async waitForJob<R = unknown>(this: Ctx, id: string, ttlMs = 30_000): Promise<R> {
|
|
130
|
+
const response = await this.call({ cmd: 'WaitJob', id, timeout: ttlMs }, ttlMs + 5000);
|
|
131
|
+
return response.result as R;
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
/** BullMQ v5 alias for waitForJob (queueEvents param unused over TCP). */
|
|
135
|
+
waitJobUntilFinished<R = unknown>(
|
|
136
|
+
this: Ctx,
|
|
137
|
+
id: string,
|
|
138
|
+
_queueEvents?: unknown,
|
|
139
|
+
ttlMs?: number
|
|
140
|
+
): Promise<R> {
|
|
141
|
+
return this.waitForJob<R>(id, ttlMs);
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
async getProgress(this: Ctx, id: string): Promise<{ progress: number; message: string | null }> {
|
|
145
|
+
const response = await this.call({ cmd: 'GetProgress', id });
|
|
146
|
+
return {
|
|
147
|
+
progress: Number(response.progress ?? 0),
|
|
148
|
+
message: (response.message as string | null) ?? null,
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
// ------------------------------------------------------------------- counts
|
|
153
|
+
|
|
154
|
+
async getJobCounts(this: Ctx): Promise<JobCounts> {
|
|
155
|
+
const response = await this.call({ cmd: 'GetJobCounts', queue: this.name });
|
|
156
|
+
return response.counts as JobCounts;
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
async getWaitingCount(this: Ctx): Promise<number> {
|
|
160
|
+
const counts = await this.getJobCounts();
|
|
161
|
+
return counts.waiting + counts.prioritized;
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
async getActiveCount(this: Ctx): Promise<number> {
|
|
165
|
+
return (await this.getJobCounts()).active;
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
async getCompletedCount(this: Ctx): Promise<number> {
|
|
169
|
+
return (await this.getJobCounts()).completed;
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
async getFailedCount(this: Ctx): Promise<number> {
|
|
173
|
+
return (await this.getJobCounts()).failed;
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
async getDelayedCount(this: Ctx): Promise<number> {
|
|
177
|
+
return (await this.getJobCounts()).delayed;
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
async getPrioritizedCount(this: Ctx): Promise<number> {
|
|
181
|
+
return (await this.getJobCounts()).prioritized;
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
async getWaitingChildrenCount(this: Ctx): Promise<number> {
|
|
185
|
+
return (await this.getJobCounts())['waiting-children'];
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
async count(this: Ctx): Promise<number> {
|
|
189
|
+
const response = await this.call({ cmd: 'Count', queue: this.name });
|
|
190
|
+
return Number(response.count ?? 0);
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
async getCountsPerPriority(this: Ctx): Promise<Record<string, number>> {
|
|
194
|
+
const response = await this.call({ cmd: 'GetCountsPerPriority', queue: this.name });
|
|
195
|
+
return (response.counts ?? response.data ?? {}) as Record<string, number>;
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
// --------------------------------------------------------------------- logs
|
|
199
|
+
|
|
200
|
+
async addJobLog(this: Ctx, id: string, message: string): Promise<void> {
|
|
201
|
+
await this.call({ cmd: 'AddLog', id, message });
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
async getJobLogs(this: Ctx, id: string, start?: number, end?: number): Promise<string[]> {
|
|
205
|
+
const response = await this.call(
|
|
206
|
+
compact({ cmd: 'GetLogs', id, start, end }) as {
|
|
207
|
+
cmd: string;
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
const data = (response.data ?? {}) as Raw;
|
|
211
|
+
const logs = (data.logs ?? response.logs ?? []) as unknown[];
|
|
212
|
+
return logs.map((row) => (typeof row === 'string' ? row : String((row as Raw).message ?? row)));
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
async clearJobLogs(this: Ctx, id: string, keepLogs?: number): Promise<void> {
|
|
216
|
+
await this.call(compact({ cmd: 'ClearLogs', id, keepLogs }) as { cmd: string });
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export type QueueQueryApi = typeof queryMethods;
|
package/src/queue.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue: producer + management API over the bunqueue TCP protocol.
|
|
3
|
+
* Method surface mirrors the official TS client (TCP mode).
|
|
4
|
+
*
|
|
5
|
+
* The class is composed from area modules kept under 250 lines each:
|
|
6
|
+
* queue-query.ts (query/counts/logs), queue-control.ts (state control,
|
|
7
|
+
* job mutations) and queue-admin.ts (DLQ, configs, schedulers, webhooks,
|
|
8
|
+
* monitoring). Methods are merged onto the prototype; declaration merging
|
|
9
|
+
* exposes them on the type.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Connection, type Response, type TlsOption } from './connection.js';
|
|
13
|
+
import { Job } from './job.js';
|
|
14
|
+
import { adminMethods, type QueueAdminApi } from './queue-admin.js';
|
|
15
|
+
import { controlMethods, type QueueControlApi } from './queue-control.js';
|
|
16
|
+
import { type QueueQueryApi, queryMethods } from './queue-query.js';
|
|
17
|
+
import { type JobOptions, jobPayload, wireJobOptions } from './types.js';
|
|
18
|
+
|
|
19
|
+
export interface QueueOptions {
|
|
20
|
+
host?: string;
|
|
21
|
+
port?: number;
|
|
22
|
+
token?: string;
|
|
23
|
+
tls?: TlsOption;
|
|
24
|
+
connection?: Connection;
|
|
25
|
+
commandTimeoutMs?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface BulkJobEntry<T = unknown> {
|
|
29
|
+
name: string;
|
|
30
|
+
data: T;
|
|
31
|
+
opts?: JobOptions;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// biome-ignore lint/suspicious/noUnsafeDeclarationMerging: prototype-mixin composition — Object.assign at the bottom installs exactly the methods the merged interface declares
|
|
35
|
+
export class Queue<T = unknown> {
|
|
36
|
+
readonly name: string;
|
|
37
|
+
readonly connection: Connection;
|
|
38
|
+
private readonly ownsConnection: boolean;
|
|
39
|
+
|
|
40
|
+
constructor(name: string, opts: QueueOptions = {}) {
|
|
41
|
+
this.name = name;
|
|
42
|
+
this.connection =
|
|
43
|
+
opts.connection ??
|
|
44
|
+
new Connection({
|
|
45
|
+
host: opts.host,
|
|
46
|
+
port: opts.port,
|
|
47
|
+
token: opts.token,
|
|
48
|
+
tls: opts.tls,
|
|
49
|
+
commandTimeoutMs: opts.commandTimeoutMs,
|
|
50
|
+
});
|
|
51
|
+
this.ownsConnection = opts.connection === undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Send a raw command on this queue's connection (used by area modules). */
|
|
55
|
+
call(command: Record<string, unknown> & { cmd: string }, timeoutMs?: number): Promise<Response> {
|
|
56
|
+
return this.connection.call(command, timeoutMs);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ------------------------------------------------------------------ produce
|
|
60
|
+
|
|
61
|
+
/** Add a job; returns a Job stub carrying the assigned id. */
|
|
62
|
+
async add(name: string, data: T, opts?: JobOptions): Promise<Job<T>> {
|
|
63
|
+
const payload = jobPayload(name, data);
|
|
64
|
+
const response = await this.call({
|
|
65
|
+
cmd: 'PUSH',
|
|
66
|
+
queue: this.name,
|
|
67
|
+
data: payload,
|
|
68
|
+
...wireJobOptions(opts),
|
|
69
|
+
});
|
|
70
|
+
return new Job<T>({ id: response.id, queue: this.name, data: payload }, this.connection);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Add many jobs in one round-trip; returns Job stubs. */
|
|
74
|
+
async addBulk(jobs: BulkJobEntry<T>[]): Promise<Job<T>[]> {
|
|
75
|
+
const inputs = jobs.map((entry) => ({
|
|
76
|
+
data: jobPayload(entry.name, entry.data),
|
|
77
|
+
...wireJobOptions(entry.opts),
|
|
78
|
+
}));
|
|
79
|
+
const response = await this.call({ cmd: 'PUSHB', queue: this.name, jobs: inputs });
|
|
80
|
+
const ids = (response.ids ?? []) as string[];
|
|
81
|
+
return ids.map(
|
|
82
|
+
(id, i) => new Job<T>({ id, queue: this.name, data: inputs[i].data }, this.connection)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------- lifecycle
|
|
87
|
+
|
|
88
|
+
ping(): Promise<boolean> {
|
|
89
|
+
return this.connection.ping();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async waitUntilReady(): Promise<void> {
|
|
93
|
+
await this.connection.connect();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
close(): void {
|
|
97
|
+
if (this.ownsConnection) this.connection.close();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** BullMQ v5 alias for close(). */
|
|
101
|
+
async disconnect(): Promise<void> {
|
|
102
|
+
this.close();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Merge the area-module methods into the class (runtime + type level).
|
|
107
|
+
* The declaration merging is intentional and safe: Object.assign below
|
|
108
|
+
* installs exactly the methods the interface declares. The unused type
|
|
109
|
+
* parameter is required — merged interfaces must repeat the class generics. */
|
|
110
|
+
// biome-ignore lint/correctness/noUnusedVariables: generic must match the class declaration
|
|
111
|
+
export interface Queue<T = unknown> extends QueueQueryApi, QueueControlApi, QueueAdminApi {}
|
|
112
|
+
Object.assign(Queue.prototype, queryMethods, controlMethods, adminMethods);
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/** Shared option types + wire mapping, mirroring the official client. */
|
|
2
|
+
|
|
3
|
+
import { compact } from './frame.js';
|
|
4
|
+
|
|
5
|
+
export interface BackoffOptions {
|
|
6
|
+
type: 'fixed' | 'exponential';
|
|
7
|
+
delay: number;
|
|
8
|
+
maxDelay?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DeduplicationOptions {
|
|
12
|
+
id: string;
|
|
13
|
+
ttl?: number;
|
|
14
|
+
extend?: boolean;
|
|
15
|
+
replace?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RepeatOptions {
|
|
19
|
+
every?: number;
|
|
20
|
+
limit?: number;
|
|
21
|
+
pattern?: string;
|
|
22
|
+
count?: number;
|
|
23
|
+
startDate?: number;
|
|
24
|
+
endDate?: number;
|
|
25
|
+
tz?: string;
|
|
26
|
+
immediately?: boolean;
|
|
27
|
+
offset?: number;
|
|
28
|
+
jobId?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface JobOptions {
|
|
32
|
+
priority?: number;
|
|
33
|
+
delay?: number;
|
|
34
|
+
/** Max attempts (server name: maxAttempts). */
|
|
35
|
+
attempts?: number;
|
|
36
|
+
backoff?: number | BackoffOptions;
|
|
37
|
+
ttl?: number;
|
|
38
|
+
timeout?: number;
|
|
39
|
+
/** Custom job id (idempotent add). */
|
|
40
|
+
jobId?: string;
|
|
41
|
+
deduplication?: DeduplicationOptions;
|
|
42
|
+
dependsOn?: string[];
|
|
43
|
+
parentId?: string;
|
|
44
|
+
childrenIds?: string[];
|
|
45
|
+
tags?: string[];
|
|
46
|
+
groupId?: string;
|
|
47
|
+
lifo?: boolean;
|
|
48
|
+
removeOnComplete?: boolean;
|
|
49
|
+
removeOnFail?: boolean;
|
|
50
|
+
stallTimeout?: number;
|
|
51
|
+
durable?: boolean;
|
|
52
|
+
repeat?: RepeatOptions;
|
|
53
|
+
debounce?: { id: string; ttl?: number };
|
|
54
|
+
stackTraceLimit?: number;
|
|
55
|
+
keepLogs?: number;
|
|
56
|
+
sizeLimit?: number;
|
|
57
|
+
timestamp?: number;
|
|
58
|
+
failParentOnFailure?: boolean;
|
|
59
|
+
removeDependencyOnFailure?: boolean;
|
|
60
|
+
ignoreDependencyOnFailure?: boolean;
|
|
61
|
+
continueParentOnFailure?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Map SDK JobOptions to the PUSH/PUSHB wire fields (undefined keys dropped). */
|
|
65
|
+
export function wireJobOptions(opts: JobOptions = {}): Record<string, unknown> {
|
|
66
|
+
return compact({
|
|
67
|
+
priority: opts.priority,
|
|
68
|
+
delay: opts.delay,
|
|
69
|
+
maxAttempts: opts.attempts,
|
|
70
|
+
backoff: opts.backoff,
|
|
71
|
+
ttl: opts.ttl,
|
|
72
|
+
timeout: opts.timeout,
|
|
73
|
+
jobId: opts.jobId,
|
|
74
|
+
uniqueKey: opts.deduplication?.id,
|
|
75
|
+
dedup: opts.deduplication
|
|
76
|
+
? compact({
|
|
77
|
+
ttl: opts.deduplication.ttl,
|
|
78
|
+
extend: opts.deduplication.extend,
|
|
79
|
+
replace: opts.deduplication.replace,
|
|
80
|
+
})
|
|
81
|
+
: undefined,
|
|
82
|
+
dependsOn: opts.dependsOn,
|
|
83
|
+
parentId: opts.parentId,
|
|
84
|
+
childrenIds: opts.childrenIds,
|
|
85
|
+
tags: opts.tags,
|
|
86
|
+
groupId: opts.groupId,
|
|
87
|
+
lifo: opts.lifo,
|
|
88
|
+
removeOnComplete:
|
|
89
|
+
typeof opts.removeOnComplete === 'boolean' ? opts.removeOnComplete : undefined,
|
|
90
|
+
removeOnFail: typeof opts.removeOnFail === 'boolean' ? opts.removeOnFail : undefined,
|
|
91
|
+
stallTimeout: opts.stallTimeout,
|
|
92
|
+
durable: opts.durable,
|
|
93
|
+
repeat: opts.repeat,
|
|
94
|
+
debounceId: opts.debounce?.id,
|
|
95
|
+
debounceTtl: opts.debounce?.ttl,
|
|
96
|
+
stackTraceLimit: opts.stackTraceLimit,
|
|
97
|
+
keepLogs: opts.keepLogs,
|
|
98
|
+
sizeLimit: opts.sizeLimit,
|
|
99
|
+
timestamp: opts.timestamp,
|
|
100
|
+
failParentOnFailure: opts.failParentOnFailure,
|
|
101
|
+
removeDependencyOnFailure: opts.removeDependencyOnFailure,
|
|
102
|
+
ignoreDependencyOnFailure: opts.ignoreDependencyOnFailure,
|
|
103
|
+
continueParentOnFailure: opts.continueParentOnFailure,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The job name travels inside `data`, mirroring the official client. */
|
|
108
|
+
export function jobPayload(name: string, data: unknown): Record<string, unknown> {
|
|
109
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
110
|
+
return { name, ...(data as Record<string, unknown>) };
|
|
111
|
+
}
|
|
112
|
+
return data === undefined || data === null ? { name } : { name, payload: data };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface JobCounts {
|
|
116
|
+
waiting: number;
|
|
117
|
+
prioritized: number;
|
|
118
|
+
delayed: number;
|
|
119
|
+
active: number;
|
|
120
|
+
completed: number;
|
|
121
|
+
failed: number;
|
|
122
|
+
'waiting-children': number;
|
|
123
|
+
paused: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type JobStateName =
|
|
127
|
+
| 'waiting'
|
|
128
|
+
| 'prioritized'
|
|
129
|
+
| 'delayed'
|
|
130
|
+
| 'active'
|
|
131
|
+
| 'completed'
|
|
132
|
+
| 'failed'
|
|
133
|
+
| 'waiting-children';
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkerBase: lifecycle state, cooperative cancellation and safe command
|
|
3
|
+
* dispatch shared by the Worker pull loop (worker.ts).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { randomBytes } from 'node:crypto';
|
|
7
|
+
import { EventEmitter } from 'node:events';
|
|
8
|
+
import { hostname } from 'node:os';
|
|
9
|
+
import { Connection } from './connection.js';
|
|
10
|
+
import { MAX_POLL_TIMEOUT_MS, sleep, type WorkerOptions } from './worker-types.js';
|
|
11
|
+
|
|
12
|
+
export class WorkerBase extends EventEmitter {
|
|
13
|
+
readonly queue: string;
|
|
14
|
+
readonly concurrency: number;
|
|
15
|
+
readonly batchSize: number;
|
|
16
|
+
readonly pollTimeoutMs: number;
|
|
17
|
+
readonly lockTtlMs: number;
|
|
18
|
+
readonly heartbeatIntervalS: number;
|
|
19
|
+
readonly workerId: string;
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly connection: Connection;
|
|
22
|
+
|
|
23
|
+
protected readonly active = new Map<string, string>(); // job id -> lock token
|
|
24
|
+
protected readonly cancelledJobs = new Set<string>();
|
|
25
|
+
protected stopped = false;
|
|
26
|
+
protected paused = false;
|
|
27
|
+
protected closedFlag = false;
|
|
28
|
+
protected running = false;
|
|
29
|
+
protected wasBusy = false;
|
|
30
|
+
protected processed = 0;
|
|
31
|
+
protected failedCount = 0;
|
|
32
|
+
protected readyPromise: Promise<void>;
|
|
33
|
+
protected readyResolve: (() => void) | null = null;
|
|
34
|
+
protected loopPromise: Promise<void> | null = null;
|
|
35
|
+
protected heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
36
|
+
|
|
37
|
+
constructor(queue: string, opts: WorkerOptions = {}) {
|
|
38
|
+
super();
|
|
39
|
+
if ((opts.concurrency ?? 4) < 1) throw new Error('concurrency must be >= 1');
|
|
40
|
+
this.queue = queue;
|
|
41
|
+
this.concurrency = opts.concurrency ?? 4;
|
|
42
|
+
this.batchSize = Math.max(1, opts.batchSize ?? 10);
|
|
43
|
+
this.pollTimeoutMs = Math.min(opts.pollTimeoutMs ?? 5000, MAX_POLL_TIMEOUT_MS);
|
|
44
|
+
this.lockTtlMs = opts.lockTtlMs ?? 30_000;
|
|
45
|
+
this.heartbeatIntervalS = opts.heartbeatIntervalS ?? 10;
|
|
46
|
+
this.workerId = `ts-${hostname()}-${process.pid}-${randomBytes(4).toString('hex')}`;
|
|
47
|
+
this.name = opts.name ?? this.workerId;
|
|
48
|
+
this.connection = new Connection({
|
|
49
|
+
host: opts.host,
|
|
50
|
+
port: opts.port,
|
|
51
|
+
token: opts.token,
|
|
52
|
+
tls: opts.tls,
|
|
53
|
+
});
|
|
54
|
+
this.readyPromise = new Promise((resolve) => {
|
|
55
|
+
this.readyResolve = resolve;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
pause(): void {
|
|
60
|
+
this.paused = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
resume(): void {
|
|
64
|
+
this.paused = false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
isRunning(): boolean {
|
|
68
|
+
return this.running && !this.closedFlag;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
isPaused(): boolean {
|
|
72
|
+
return this.paused;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
isClosed(): boolean {
|
|
76
|
+
return this.closedFlag;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async waitUntilReady(): Promise<void> {
|
|
80
|
+
await this.readyPromise;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Cooperative cancel of a locally active job (mirrors the official client):
|
|
85
|
+
* marks the job and emits 'cancelled'; the processor is expected to check
|
|
86
|
+
* isJobCancelled() and abort. Returns false if the job is not active here.
|
|
87
|
+
*/
|
|
88
|
+
cancelJob(jobId: string, reason?: string): boolean {
|
|
89
|
+
if (this.active.has(jobId)) {
|
|
90
|
+
this.cancelledJobs.add(jobId);
|
|
91
|
+
this.emit('cancelled', { jobId, reason: reason ?? 'Job cancelled by worker' });
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
cancelAllJobs(reason?: string): void {
|
|
98
|
+
for (const jobId of this.active.keys()) {
|
|
99
|
+
this.cancelledJobs.add(jobId);
|
|
100
|
+
this.emit('cancelled', { jobId, reason: reason ?? 'All jobs cancelled' });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
isJobCancelled(jobId: string): boolean {
|
|
105
|
+
return this.cancelledJobs.has(jobId);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Graceful shutdown: stop pulling, wait for in-flight jobs. */
|
|
109
|
+
async close(): Promise<void> {
|
|
110
|
+
if (this.closedFlag) return;
|
|
111
|
+
this.stopped = true;
|
|
112
|
+
if (this.loopPromise) await this.loopPromise;
|
|
113
|
+
while (this.active.size > 0) await sleep(20);
|
|
114
|
+
if (this.heartbeatTimer) {
|
|
115
|
+
clearInterval(this.heartbeatTimer);
|
|
116
|
+
this.heartbeatTimer = null;
|
|
117
|
+
}
|
|
118
|
+
await this.safeCall({ cmd: 'UnregisterWorker', workerId: this.workerId });
|
|
119
|
+
this.connection.close();
|
|
120
|
+
this.closedFlag = true;
|
|
121
|
+
this.running = false;
|
|
122
|
+
this.emit('closed');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
protected async safeCall(command: Record<string, unknown> & { cmd: string }): Promise<void> {
|
|
126
|
+
try {
|
|
127
|
+
await this.connection.call(command);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
this.emit('error', err);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Worker option types and shared constants. */
|
|
2
|
+
|
|
3
|
+
import type { TlsOption } from './connection.js';
|
|
4
|
+
import type { Job } from './job.js';
|
|
5
|
+
|
|
6
|
+
export type Processor<T = unknown, R = unknown> = (job: Job<T>) => R | Promise<R>;
|
|
7
|
+
|
|
8
|
+
export interface WorkerOptions {
|
|
9
|
+
host?: string;
|
|
10
|
+
port?: number;
|
|
11
|
+
token?: string;
|
|
12
|
+
tls?: TlsOption;
|
|
13
|
+
/** Max jobs processed in parallel (default 4). */
|
|
14
|
+
concurrency?: number;
|
|
15
|
+
/** Max jobs fetched per PULLB (default 10, capped by free slots). */
|
|
16
|
+
batchSize?: number;
|
|
17
|
+
/** Server-side long-poll timeout in ms (default 5000, max 30000). */
|
|
18
|
+
pollTimeoutMs?: number;
|
|
19
|
+
/** Job lock TTL in ms (default 30000). */
|
|
20
|
+
lockTtlMs?: number;
|
|
21
|
+
/** Worker + job heartbeat interval in seconds (default 10). */
|
|
22
|
+
heartbeatIntervalS?: number;
|
|
23
|
+
/** Start the loop at construction (default true, mirrors the TS client). */
|
|
24
|
+
autorun?: boolean;
|
|
25
|
+
name?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const MAX_POLL_TIMEOUT_MS = 30_000;
|
|
29
|
+
export const MAX_STACK_LINES = 20;
|
|
30
|
+
export const RECONNECT_BACKOFF_MS = [500, 1000, 2000, 5000];
|
|
31
|
+
|
|
32
|
+
export const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|