@iskra-bun/worker-kit 0.1.0 → 0.3.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/CHANGELOG.md +68 -0
- package/README.md +8 -2
- package/dist/index.d.ts +153 -18
- package/dist/index.js +251 -28
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
- package/src/index.ts +335 -31
- package/src/types.ts +79 -10
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { Queue, Worker } from "bullmq";
|
|
2
|
+
import { Queue, QueueEvents, UnrecoverableError, Worker } from "bullmq";
|
|
3
3
|
|
|
4
4
|
// src/errors.ts
|
|
5
5
|
import { IskraError, ErrorCodes } from "@iskra-bun/core";
|
|
@@ -17,13 +17,23 @@ var JobError = class extends IskraError {
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
// src/index.ts
|
|
20
|
-
var
|
|
20
|
+
var RETENTION = {
|
|
21
|
+
removeOnComplete: { count: 1e3 },
|
|
22
|
+
removeOnFail: { age: 7 * 24 * 60 * 60, count: 5e3 }
|
|
23
|
+
};
|
|
24
|
+
var WorkerManager = class _WorkerManager {
|
|
21
25
|
name = "WorkerManager";
|
|
22
26
|
app = null;
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
28
|
handlers = /* @__PURE__ */ new Map();
|
|
24
29
|
queue = null;
|
|
25
30
|
worker = null;
|
|
31
|
+
queueEvents = null;
|
|
32
|
+
stopped = false;
|
|
26
33
|
options;
|
|
34
|
+
/** Tope de tamaño (bytes) del payload serializado de un job. */
|
|
35
|
+
static MAX_PAYLOAD_BYTES = 1024 * 1024;
|
|
36
|
+
// 1 MB
|
|
27
37
|
constructor(options) {
|
|
28
38
|
this.options = options;
|
|
29
39
|
}
|
|
@@ -33,7 +43,7 @@ var WorkerManager = class {
|
|
|
33
43
|
try {
|
|
34
44
|
this.queue = new Queue(this.options.queueName || "iskra-jobs", {
|
|
35
45
|
connection,
|
|
36
|
-
defaultJobOptions: this.mapJobOptions(this.options.defaultJobOptions)
|
|
46
|
+
defaultJobOptions: { ...RETENTION, ...this.mapJobOptions(this.options.defaultJobOptions) }
|
|
37
47
|
});
|
|
38
48
|
} catch (err) {
|
|
39
49
|
throw new QueueError("Failed to initialize BullMQ queue", {
|
|
@@ -41,16 +51,20 @@ var WorkerManager = class {
|
|
|
41
51
|
context: { queueName: this.options.queueName || "iskra-jobs" }
|
|
42
52
|
});
|
|
43
53
|
}
|
|
54
|
+
this.queue.on("error", (err) => this.logConnectionError("queue", err));
|
|
44
55
|
}
|
|
45
56
|
/**
|
|
46
|
-
* Registra un handler para un tipo de job.
|
|
57
|
+
* Registra un handler para un tipo de job. El handler puede devolver un
|
|
58
|
+
* valor `R` que queda disponible como resultado del job.
|
|
47
59
|
*/
|
|
48
60
|
register(jobName, handler) {
|
|
49
61
|
this.handlers.set(jobName, handler);
|
|
50
62
|
return this;
|
|
51
63
|
}
|
|
52
64
|
/**
|
|
53
|
-
* Encola un job para ser procesado.
|
|
65
|
+
* Encola un job para ser procesado. Devuelve un descriptor que, además de
|
|
66
|
+
* los datos del job, expone `result()` para esperar el valor de retorno del
|
|
67
|
+
* handler.
|
|
54
68
|
*/
|
|
55
69
|
async enqueue(name, data, opts) {
|
|
56
70
|
if (!this.queue) {
|
|
@@ -58,22 +72,46 @@ var WorkerManager = class {
|
|
|
58
72
|
context: { jobName: name }
|
|
59
73
|
});
|
|
60
74
|
}
|
|
75
|
+
this.validateEnqueue(name, data, opts);
|
|
61
76
|
const job = await this.queue.add(name, data, this.mapJobOptions(opts));
|
|
62
77
|
this.app?.logger.debug({ jobId: job.id, jobName: name }, "Job enqueued");
|
|
63
|
-
return
|
|
78
|
+
return this.buildDescriptor(job, name, data);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Programa un job repetible (cron o intervalo). Conveniencia sobre
|
|
82
|
+
* `enqueue` con la opción `repeat` ya configurada.
|
|
83
|
+
*
|
|
84
|
+
* @param repeat patrón cron (string) o `{ every: ms }`/`{ pattern: cron }`.
|
|
85
|
+
*/
|
|
86
|
+
async schedule(name, data, repeat, opts) {
|
|
87
|
+
return this.enqueue(name, data, { ...opts, repeat });
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* `consume: false`, or `concurrency: 0` (which used to fall back to 1, so
|
|
91
|
+
* a service meant to only enqueue consumed jobs it had no handler for).
|
|
92
|
+
*/
|
|
93
|
+
get producerOnly() {
|
|
94
|
+
return this.options.consume === false || this.options.concurrency === 0;
|
|
64
95
|
}
|
|
65
96
|
async start() {
|
|
97
|
+
if (this.producerOnly) {
|
|
98
|
+
this.app?.logger.info(
|
|
99
|
+
{ queue: this.options.queueName || "iskra-jobs" },
|
|
100
|
+
"WorkerManager started in producer-only mode (consume: false / concurrency: 0)"
|
|
101
|
+
);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
66
104
|
const connection = this.parseConnection();
|
|
67
105
|
this.worker = new Worker(
|
|
68
106
|
this.options.queueName || "iskra-jobs",
|
|
69
107
|
async (job) => {
|
|
70
108
|
const handler = this.handlers.get(job.name);
|
|
71
109
|
if (!handler) {
|
|
72
|
-
this.app?.logger.
|
|
73
|
-
|
|
110
|
+
this.app?.logger.error({ jobName: job.name, jobId: job.id }, "No handler registered for job");
|
|
111
|
+
throw new UnrecoverableError(`No handler registered for job "${job.name}"`);
|
|
74
112
|
}
|
|
75
113
|
try {
|
|
76
|
-
await handler({
|
|
114
|
+
return await handler({
|
|
77
115
|
id: job.id,
|
|
78
116
|
name: job.name,
|
|
79
117
|
data: job.data,
|
|
@@ -84,7 +122,7 @@ var WorkerManager = class {
|
|
|
84
122
|
cause: err instanceof Error ? err : new Error(String(err)),
|
|
85
123
|
context: { jobId: job.id, jobName: job.name, attemptsMade: job.attemptsMade }
|
|
86
124
|
});
|
|
87
|
-
this.app?.logger.
|
|
125
|
+
this.app?.logger.debug({ err: jobErr }, jobErr.message);
|
|
88
126
|
throw err;
|
|
89
127
|
}
|
|
90
128
|
},
|
|
@@ -97,47 +135,232 @@ var WorkerManager = class {
|
|
|
97
135
|
this.app?.logger.debug({ jobId: job.id, jobName: job.name }, "Job completed");
|
|
98
136
|
});
|
|
99
137
|
this.worker.on("failed", (job, err) => {
|
|
100
|
-
this.
|
|
138
|
+
this.onFailed(job, err);
|
|
101
139
|
});
|
|
102
|
-
this.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
140
|
+
this.worker.on("error", (err) => this.logConnectionError("worker", err));
|
|
141
|
+
this.app?.logger.info(
|
|
142
|
+
{
|
|
143
|
+
queue: this.options.queueName || "iskra-jobs",
|
|
144
|
+
concurrency: this.options.concurrency || 1
|
|
145
|
+
},
|
|
146
|
+
"WorkerManager started"
|
|
147
|
+
);
|
|
106
148
|
}
|
|
107
149
|
async stop() {
|
|
108
|
-
|
|
150
|
+
this.stopped = true;
|
|
109
151
|
if (this.worker) {
|
|
110
|
-
|
|
152
|
+
try {
|
|
153
|
+
await this.worker.close();
|
|
154
|
+
} catch (err) {
|
|
155
|
+
this.app?.logger.error(
|
|
156
|
+
{ err: err instanceof Error ? err : new Error(String(err)) },
|
|
157
|
+
"WorkerManager: error while closing worker"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (this.queueEvents) {
|
|
162
|
+
try {
|
|
163
|
+
await this.queueEvents.close();
|
|
164
|
+
} catch (err) {
|
|
165
|
+
this.app?.logger.error(
|
|
166
|
+
{ err: err instanceof Error ? err : new Error(String(err)) },
|
|
167
|
+
"WorkerManager: error while closing queue events"
|
|
168
|
+
);
|
|
169
|
+
}
|
|
111
170
|
}
|
|
112
171
|
if (this.queue) {
|
|
113
|
-
|
|
172
|
+
try {
|
|
173
|
+
await this.queue.close();
|
|
174
|
+
} catch (err) {
|
|
175
|
+
this.app?.logger.error(
|
|
176
|
+
{ err: err instanceof Error ? err : new Error(String(err)) },
|
|
177
|
+
"WorkerManager: error while closing queue"
|
|
178
|
+
);
|
|
179
|
+
}
|
|
114
180
|
}
|
|
115
|
-
await Promise.all(closePromises);
|
|
116
181
|
this.app?.logger.info("WorkerManager stopped");
|
|
117
182
|
}
|
|
183
|
+
logConnectionError(source, err) {
|
|
184
|
+
this.app?.logger.error({ err, source }, "WorkerManager: BullMQ connection error");
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Turns a redis:// or rediss:// URL into ioredis options, keeping the ACL
|
|
188
|
+
* username, the percent-decoded password, the db index and TLS (rediss).
|
|
189
|
+
*/
|
|
118
190
|
parseConnection() {
|
|
119
191
|
if (typeof this.options.connection === "string") {
|
|
120
192
|
const url = new URL(this.options.connection);
|
|
121
193
|
return {
|
|
122
|
-
host
|
|
194
|
+
// URL keeps the brackets of an IPv6 host ("[::1]"); ioredis wants the bare address.
|
|
195
|
+
host: url.hostname.replace(/^\[(.*)\]$/, "$1"),
|
|
123
196
|
port: Number(url.port) || 6379,
|
|
124
|
-
|
|
125
|
-
|
|
197
|
+
username: url.username ? decodeURIComponent(url.username) : void 0,
|
|
198
|
+
password: url.password ? decodeURIComponent(url.password) : void 0,
|
|
199
|
+
db: url.pathname ? Number(url.pathname.slice(1)) || 0 : 0,
|
|
200
|
+
...url.protocol === "rediss:" ? { tls: {} } : {}
|
|
126
201
|
};
|
|
127
202
|
}
|
|
128
203
|
return this.options.connection;
|
|
129
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Only the options that were given: BullMQ merges `{ ...defaultJobOptions,
|
|
207
|
+
* ...opts }`, so an explicit `undefined` erased the queue default (a job
|
|
208
|
+
* enqueued with just `{ priority }`, and every scheduled job, lost its
|
|
209
|
+
* attempts, backoff and removeOn* settings).
|
|
210
|
+
*/
|
|
130
211
|
mapJobOptions(opts) {
|
|
131
212
|
if (!opts) return void 0;
|
|
213
|
+
const mapped = {};
|
|
214
|
+
for (const key of ["attempts", "delay", "priority", "backoff", "removeOnComplete", "removeOnFail"]) {
|
|
215
|
+
if (opts[key] !== void 0) mapped[key] = opts[key];
|
|
216
|
+
}
|
|
217
|
+
if (opts.repeat !== void 0) {
|
|
218
|
+
mapped.repeat = this.mapRepeat(opts.repeat);
|
|
219
|
+
}
|
|
220
|
+
return mapped;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Normaliza una RepeatSpec a la forma `repeat` de BullMQ:
|
|
224
|
+
* - string → `{ pattern: cron }`
|
|
225
|
+
* - `{ every }` / `{ pattern }` → se reenvían tal cual.
|
|
226
|
+
*/
|
|
227
|
+
mapRepeat(repeat) {
|
|
228
|
+
if (typeof repeat === "string") {
|
|
229
|
+
return { pattern: repeat };
|
|
230
|
+
}
|
|
231
|
+
return { ...repeat };
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Valida la entrada de `enqueue` ANTES de tocar Redis, para evitar que
|
|
235
|
+
* entrada no confiable inunde la queue, almacene payloads gigantes o
|
|
236
|
+
* programe repeticiones malformadas. Lanza `QueueError` ante cualquier
|
|
237
|
+
* problema; no muta nada.
|
|
238
|
+
*/
|
|
239
|
+
validateEnqueue(name, data, opts) {
|
|
240
|
+
if (!this.producerOnly && !this.handlers.has(name)) {
|
|
241
|
+
throw new QueueError(`No handler registered for job "${name}"`, {
|
|
242
|
+
context: { jobName: name }
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
this.validatePayloadSize(name, data);
|
|
246
|
+
if (opts?.repeat !== void 0) {
|
|
247
|
+
this.validateRepeat(name, opts.repeat);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/** Rechaza payloads cuya serialización JSON excede el tope configurado. */
|
|
251
|
+
validatePayloadSize(name, data) {
|
|
252
|
+
let serialized;
|
|
253
|
+
try {
|
|
254
|
+
serialized = JSON.stringify(data ?? null);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
throw new QueueError(`Job "${name}" data is not serializable`, {
|
|
257
|
+
cause: err instanceof Error ? err : new Error(String(err)),
|
|
258
|
+
context: { jobName: name }
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const size = Buffer.byteLength(serialized, "utf8");
|
|
262
|
+
if (size > _WorkerManager.MAX_PAYLOAD_BYTES) {
|
|
263
|
+
throw new QueueError(
|
|
264
|
+
`Job "${name}" payload too large: ${size} bytes (max ${_WorkerManager.MAX_PAYLOAD_BYTES})`,
|
|
265
|
+
{ context: { jobName: name, size, max: _WorkerManager.MAX_PAYLOAD_BYTES } }
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** Rechaza specs de repetición vacías, intervalos no positivos o crons en blanco. */
|
|
270
|
+
validateRepeat(name, repeat) {
|
|
271
|
+
if (typeof repeat === "string") {
|
|
272
|
+
if (repeat.trim().length === 0) {
|
|
273
|
+
throw new QueueError(`Job "${name}" has an empty cron repeat pattern`, {
|
|
274
|
+
context: { jobName: name }
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const hasEvery = "every" in repeat;
|
|
280
|
+
const hasPattern = "pattern" in repeat;
|
|
281
|
+
if (!hasEvery && !hasPattern) {
|
|
282
|
+
throw new QueueError(`Job "${name}" repeat spec must define "every" or "pattern"`, {
|
|
283
|
+
context: { jobName: name }
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
if (hasEvery && !(typeof repeat.every === "number" && repeat.every > 0)) {
|
|
287
|
+
throw new QueueError(`Job "${name}" repeat "every" must be a positive number`, {
|
|
288
|
+
context: { jobName: name, every: repeat.every }
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
if (hasPattern && (typeof repeat.pattern !== "string" || repeat.pattern.trim().length === 0)) {
|
|
292
|
+
throw new QueueError(`Job "${name}" repeat "pattern" must be a non-empty cron string`, {
|
|
293
|
+
context: { jobName: name }
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Maneja el evento `failed` del worker. Loggea el fallo y, si el job agotó
|
|
299
|
+
* todos sus reintentos y `deadLetter` está activado, emite
|
|
300
|
+
* `worker:dead-letter` en el bus de eventos de la App.
|
|
301
|
+
*/
|
|
302
|
+
onFailed(job, err) {
|
|
303
|
+
this.app?.logger.error(
|
|
304
|
+
{ jobId: job?.id, jobName: job?.name, attemptsMade: job?.attemptsMade, err },
|
|
305
|
+
"Job failed"
|
|
306
|
+
);
|
|
307
|
+
if (!this.options.deadLetter || !job) return;
|
|
308
|
+
const maxAttempts = job.opts?.attempts ?? 1;
|
|
309
|
+
if (job.attemptsMade < maxAttempts && err?.name !== "UnrecoverableError") return;
|
|
310
|
+
const payload = {
|
|
311
|
+
jobId: job.id,
|
|
312
|
+
name: job.name,
|
|
313
|
+
data: job.data,
|
|
314
|
+
failedReason: job.failedReason ?? err?.message,
|
|
315
|
+
attemptsMade: job.attemptsMade
|
|
316
|
+
};
|
|
317
|
+
this.app?.events.emit("worker:dead-letter", payload);
|
|
318
|
+
this.app?.logger.warn(
|
|
319
|
+
{ jobId: job.id, jobName: job.name, attemptsMade: job.attemptsMade },
|
|
320
|
+
"Job routed to dead-letter"
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Construye el descriptor de un job, incluyendo el helper `result()` que
|
|
325
|
+
* espera el valor de retorno del handler vía `job.waitUntilFinished`.
|
|
326
|
+
*/
|
|
327
|
+
buildDescriptor(job, name, data) {
|
|
132
328
|
return {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
329
|
+
id: job.id,
|
|
330
|
+
name,
|
|
331
|
+
data,
|
|
332
|
+
// async so a post-stop getQueueEvents() throw surfaces as a rejected
|
|
333
|
+
// promise rather than a synchronous throw.
|
|
334
|
+
result: async (ttlMs) => {
|
|
335
|
+
const queueEvents = this.getQueueEvents();
|
|
336
|
+
return job.waitUntilFinished(queueEvents, ttlMs);
|
|
337
|
+
}
|
|
139
338
|
};
|
|
140
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Devuelve (creando perezosamente) una instancia compartida de QueueEvents
|
|
342
|
+
* usada para esperar resultados de jobs.
|
|
343
|
+
*/
|
|
344
|
+
getQueueEvents() {
|
|
345
|
+
if (this.stopped) {
|
|
346
|
+
throw new QueueError("WorkerManager is stopped; cannot open QueueEvents", {
|
|
347
|
+
context: { queueName: this.options.queueName || "iskra-jobs" }
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
if (!this.queueEvents) {
|
|
351
|
+
try {
|
|
352
|
+
this.queueEvents = new QueueEvents(this.options.queueName || "iskra-jobs", {
|
|
353
|
+
connection: this.parseConnection()
|
|
354
|
+
});
|
|
355
|
+
} catch (err) {
|
|
356
|
+
throw new QueueError("Failed to initialize BullMQ QueueEvents", {
|
|
357
|
+
cause: err instanceof Error ? err : new Error(String(err)),
|
|
358
|
+
context: { queueName: this.options.queueName || "iskra-jobs" }
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return this.queueEvents;
|
|
363
|
+
}
|
|
141
364
|
};
|
|
142
365
|
export {
|
|
143
366
|
JobError,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/errors.ts"],"sourcesContent":["import type { Driver, App } from '@iskra-bun/core';\nimport { Queue, Worker, type Job as BullJob } from 'bullmq';\nimport { QueueError, JobError } from './errors';\nimport type { WorkerManagerOptions, JobOptions, JobHandler } from './types';\n\nexport type { WorkerManagerOptions, JobOptions, JobHandler } from './types';\nexport * from './errors';\n\nexport class WorkerManager implements Driver {\n name = 'WorkerManager';\n private app: App | null = null;\n private handlers: Map<string, JobHandler> = new Map();\n private queue: Queue | null = null;\n private worker: Worker | null = null;\n private options: WorkerManagerOptions;\n\n constructor(options: WorkerManagerOptions) {\n this.options = options;\n }\n\n async init(app: App) {\n this.app = app;\n\n const connection = this.parseConnection();\n\n try {\n this.queue = new Queue(this.options.queueName || 'iskra-jobs', {\n connection,\n defaultJobOptions: this.mapJobOptions(this.options.defaultJobOptions),\n });\n } catch (err) {\n throw new QueueError('Failed to initialize BullMQ queue', {\n cause: err instanceof Error ? err : new Error(String(err)),\n context: { queueName: this.options.queueName || 'iskra-jobs' },\n });\n }\n }\n\n /**\n * Registra un handler para un tipo de job.\n */\n register(jobName: string, handler: JobHandler) {\n this.handlers.set(jobName, handler);\n return this;\n }\n\n /**\n * Encola un job para ser procesado.\n */\n async enqueue(name: string, data: any, opts?: JobOptions) {\n if (!this.queue) {\n throw new QueueError('Queue not initialized. Did you call init()?', {\n context: { jobName: name },\n });\n }\n\n const job = await this.queue.add(name, data, this.mapJobOptions(opts));\n this.app?.logger.debug({ jobId: job.id, jobName: name }, 'Job enqueued');\n return { id: job.id!, name, data };\n }\n\n async start() {\n const connection = this.parseConnection();\n\n this.worker = new Worker(\n this.options.queueName || 'iskra-jobs',\n async (job: BullJob) => {\n const handler = this.handlers.get(job.name);\n if (!handler) {\n this.app?.logger.warn({ jobName: job.name, jobId: job.id }, 'No handler registered for job');\n return;\n }\n\n try {\n await handler({\n id: job.id!,\n name: job.name,\n data: job.data,\n attemptsMade: job.attemptsMade,\n });\n } catch (err) {\n const jobErr = new JobError(`Job \"${job.name}\" failed`, {\n cause: err instanceof Error ? err : new Error(String(err)),\n context: { jobId: job.id, jobName: job.name, attemptsMade: job.attemptsMade },\n });\n this.app?.logger.error({ err: jobErr }, jobErr.message);\n throw err; // Re-throw para que BullMQ maneje el retry\n }\n },\n {\n connection,\n concurrency: this.options.concurrency || 1,\n },\n );\n\n this.worker.on('completed', (job) => {\n this.app?.logger.debug({ jobId: job.id, jobName: job.name }, 'Job completed');\n });\n\n this.worker.on('failed', (job, err) => {\n this.app?.logger.error({ jobId: job?.id, jobName: job?.name, err }, 'Job failed');\n });\n\n this.app?.logger.info({\n queue: this.options.queueName || 'iskra-jobs',\n concurrency: this.options.concurrency || 1,\n }, 'WorkerManager started');\n }\n\n async stop() {\n const closePromises: Promise<void>[] = [];\n\n if (this.worker) {\n closePromises.push(this.worker.close());\n }\n if (this.queue) {\n closePromises.push(this.queue.close());\n }\n\n await Promise.all(closePromises);\n this.app?.logger.info('WorkerManager stopped');\n }\n\n private parseConnection() {\n if (typeof this.options.connection === 'string') {\n const url = new URL(this.options.connection);\n return {\n host: url.hostname,\n port: Number(url.port) || 6379,\n password: url.password || undefined,\n db: url.pathname ? Number(url.pathname.slice(1)) || 0 : 0,\n };\n }\n return this.options.connection;\n }\n\n private mapJobOptions(opts?: JobOptions) {\n if (!opts) return undefined;\n return {\n attempts: opts.attempts,\n delay: opts.delay,\n priority: opts.priority,\n backoff: opts.backoff,\n removeOnComplete: opts.removeOnComplete,\n removeOnFail: opts.removeOnFail,\n };\n }\n}\n","import { IskraError, ErrorCodes } from '@iskra-bun/core';\n\n// ─── Queue Error ─────────────────────────────────────────────────────────────\n\nexport class QueueError extends IskraError {\n constructor(message: string, options?: { cause?: Error; context?: Record<string, unknown> }) {\n super(message, { code: ErrorCodes.QUEUE_ERROR, ...options });\n this.name = 'QueueError';\n }\n}\n\n// ─── Job Error ───────────────────────────────────────────────────────────────\n\nexport class JobError extends IskraError {\n constructor(message: string, options?: { cause?: Error; context?: Record<string, unknown> }) {\n super(message, { code: ErrorCodes.JOB_ERROR, ...options });\n this.name = 'JobError';\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/errors.ts"],"sourcesContent":["import type { Driver, App } from '@iskra-bun/core';\nimport { Queue, QueueEvents, UnrecoverableError, Worker, type Job as BullJob } from 'bullmq';\nimport { QueueError, JobError } from './errors';\nimport type {\n WorkerManagerOptions,\n JobOptions,\n JobHandler,\n RepeatSpec,\n JobDescriptor,\n DeadLetterPayload,\n} from './types';\n\nexport type {\n WorkerManagerOptions,\n JobOptions,\n JobHandler,\n RepeatSpec,\n JobDescriptor,\n DeadLetterPayload,\n} from './types';\nexport * from './errors';\n\n/**\n * The finished jobs kept in Redis, payload included, unless a job or\n * `defaultJobOptions` says otherwise: BullMQ's own default is to keep every\n * completed and failed job forever.\n */\nconst RETENTION = {\n removeOnComplete: { count: 1000 },\n removeOnFail: { age: 7 * 24 * 60 * 60, count: 5000 },\n};\n\nexport class WorkerManager implements Driver {\n name = 'WorkerManager';\n private app: App | null = null;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private handlers: Map<string, JobHandler<any, any>> = new Map();\n private queue: Queue | null = null;\n private worker: Worker | null = null;\n private queueEvents: QueueEvents | null = null;\n private stopped = false;\n private options: WorkerManagerOptions;\n\n /** Tope de tamaño (bytes) del payload serializado de un job. */\n private static readonly MAX_PAYLOAD_BYTES = 1024 * 1024; // 1 MB\n\n constructor(options: WorkerManagerOptions) {\n this.options = options;\n }\n\n async init(app: App) {\n this.app = app;\n\n const connection = this.parseConnection();\n\n try {\n this.queue = new Queue(this.options.queueName || 'iskra-jobs', {\n connection,\n defaultJobOptions: { ...RETENTION, ...this.mapJobOptions(this.options.defaultJobOptions) },\n });\n } catch (err) {\n throw new QueueError('Failed to initialize BullMQ queue', {\n cause: err instanceof Error ? err : new Error(String(err)),\n context: { queueName: this.options.queueName || 'iskra-jobs' },\n });\n }\n // Without an 'error' listener BullMQ prints connection errors to the\n // console; route them through the app logger instead.\n this.queue.on('error', (err) => this.logConnectionError('queue', err));\n }\n\n /**\n * Registra un handler para un tipo de job. El handler puede devolver un\n * valor `R` que queda disponible como resultado del job.\n */\n register<T = unknown, R = void>(jobName: string, handler: JobHandler<T, R>) {\n this.handlers.set(jobName, handler);\n return this;\n }\n\n /**\n * Encola un job para ser procesado. Devuelve un descriptor que, además de\n * los datos del job, expone `result()` para esperar el valor de retorno del\n * handler.\n */\n async enqueue<T = unknown, R = unknown>(name: string, data: T, opts?: JobOptions): Promise<JobDescriptor<T, R>> {\n if (!this.queue) {\n throw new QueueError('Queue not initialized. Did you call init()?', {\n context: { jobName: name },\n });\n }\n\n this.validateEnqueue(name, data, opts);\n\n const job = await this.queue.add(name, data, this.mapJobOptions(opts));\n this.app?.logger.debug({ jobId: job.id, jobName: name }, 'Job enqueued');\n return this.buildDescriptor<T, R>(job, name, data);\n }\n\n /**\n * Programa un job repetible (cron o intervalo). Conveniencia sobre\n * `enqueue` con la opción `repeat` ya configurada.\n *\n * @param repeat patrón cron (string) o `{ every: ms }`/`{ pattern: cron }`.\n */\n async schedule<T = unknown, R = unknown>(\n name: string,\n data: T,\n repeat: RepeatSpec,\n opts?: JobOptions,\n ): Promise<JobDescriptor<T, R>> {\n return this.enqueue<T, R>(name, data, { ...opts, repeat });\n }\n\n /**\n * `consume: false`, or `concurrency: 0` (which used to fall back to 1, so\n * a service meant to only enqueue consumed jobs it had no handler for).\n */\n private get producerOnly(): boolean {\n return this.options.consume === false || this.options.concurrency === 0;\n }\n\n async start() {\n if (this.producerOnly) {\n this.app?.logger.info(\n { queue: this.options.queueName || 'iskra-jobs' },\n 'WorkerManager started in producer-only mode (consume: false / concurrency: 0)',\n );\n return;\n }\n\n const connection = this.parseConnection();\n\n this.worker = new Worker(\n this.options.queueName || 'iskra-jobs',\n async (job: BullJob) => {\n const handler = this.handlers.get(job.name);\n if (!handler) {\n // Returning would mark the job completed and silently drop\n // it. Fail it permanently instead (no retries), so it stays\n // in the failed set and reaches dead-letter handling.\n this.app?.logger.error({ jobName: job.name, jobId: job.id }, 'No handler registered for job');\n throw new UnrecoverableError(`No handler registered for job \"${job.name}\"`);\n }\n\n try {\n return await handler({\n id: job.id!,\n name: job.name,\n data: job.data,\n attemptsMade: job.attemptsMade,\n });\n } catch (err) {\n const jobErr = new JobError(`Job \"${job.name}\" failed`, {\n cause: err instanceof Error ? err : new Error(String(err)),\n context: { jobId: job.id, jobName: job.name, attemptsMade: job.attemptsMade },\n });\n // Debug only: BullMQ then emits `failed`, and onFailed logs\n // the failure once at error level.\n this.app?.logger.debug({ err: jobErr }, jobErr.message);\n throw err; // Re-throw para que BullMQ maneje el retry\n }\n },\n {\n connection,\n concurrency: this.options.concurrency || 1,\n },\n );\n\n this.worker.on('completed', (job) => {\n this.app?.logger.debug({ jobId: job.id, jobName: job.name }, 'Job completed');\n });\n\n this.worker.on('failed', (job, err) => {\n this.onFailed(job, err);\n });\n\n this.worker.on('error', (err) => this.logConnectionError('worker', err));\n\n this.app?.logger.info(\n {\n queue: this.options.queueName || 'iskra-jobs',\n concurrency: this.options.concurrency || 1,\n },\n 'WorkerManager started',\n );\n }\n\n async stop() {\n // Mark as stopped first so any in-flight result()/getQueueEvents() call\n // throws instead of lazily opening a fresh, never-closed QueueEvents.\n this.stopped = true;\n\n // Close the worker first (without force) so BullMQ waits for any\n // in-flight job to finish before tearing down its Redis connections.\n // Only then close the queue — closing them concurrently can cut the\n // queue connection out from under a still-draining worker.\n if (this.worker) {\n try {\n await this.worker.close();\n } catch (err) {\n this.app?.logger.error(\n { err: err instanceof Error ? err : new Error(String(err)) },\n 'WorkerManager: error while closing worker',\n );\n }\n }\n\n if (this.queueEvents) {\n try {\n await this.queueEvents.close();\n } catch (err) {\n this.app?.logger.error(\n { err: err instanceof Error ? err : new Error(String(err)) },\n 'WorkerManager: error while closing queue events',\n );\n }\n }\n\n if (this.queue) {\n try {\n await this.queue.close();\n } catch (err) {\n this.app?.logger.error(\n { err: err instanceof Error ? err : new Error(String(err)) },\n 'WorkerManager: error while closing queue',\n );\n }\n }\n\n this.app?.logger.info('WorkerManager stopped');\n }\n\n private logConnectionError(source: string, err: Error) {\n this.app?.logger.error({ err, source }, 'WorkerManager: BullMQ connection error');\n }\n\n /**\n * Turns a redis:// or rediss:// URL into ioredis options, keeping the ACL\n * username, the percent-decoded password, the db index and TLS (rediss).\n */\n private parseConnection() {\n if (typeof this.options.connection === 'string') {\n const url = new URL(this.options.connection);\n return {\n // URL keeps the brackets of an IPv6 host (\"[::1]\"); ioredis wants the bare address.\n host: url.hostname.replace(/^\\[(.*)\\]$/, '$1'),\n port: Number(url.port) || 6379,\n username: url.username ? decodeURIComponent(url.username) : undefined,\n password: url.password ? decodeURIComponent(url.password) : undefined,\n db: url.pathname ? Number(url.pathname.slice(1)) || 0 : 0,\n ...(url.protocol === 'rediss:' ? { tls: {} } : {}),\n };\n }\n return this.options.connection;\n }\n\n /**\n * Only the options that were given: BullMQ merges `{ ...defaultJobOptions,\n * ...opts }`, so an explicit `undefined` erased the queue default (a job\n * enqueued with just `{ priority }`, and every scheduled job, lost its\n * attempts, backoff and removeOn* settings).\n */\n private mapJobOptions(opts?: JobOptions) {\n if (!opts) return undefined;\n const mapped: Record<string, unknown> = {};\n for (const key of ['attempts', 'delay', 'priority', 'backoff', 'removeOnComplete', 'removeOnFail'] as const) {\n if (opts[key] !== undefined) mapped[key] = opts[key];\n }\n if (opts.repeat !== undefined) {\n mapped.repeat = this.mapRepeat(opts.repeat);\n }\n return mapped;\n }\n\n /**\n * Normaliza una RepeatSpec a la forma `repeat` de BullMQ:\n * - string → `{ pattern: cron }`\n * - `{ every }` / `{ pattern }` → se reenvían tal cual.\n */\n private mapRepeat(repeat: RepeatSpec) {\n if (typeof repeat === 'string') {\n return { pattern: repeat };\n }\n return { ...repeat };\n }\n\n /**\n * Valida la entrada de `enqueue` ANTES de tocar Redis, para evitar que\n * entrada no confiable inunde la queue, almacene payloads gigantes o\n * programe repeticiones malformadas. Lanza `QueueError` ante cualquier\n * problema; no muta nada.\n */\n private validateEnqueue(name: string, data: unknown, opts?: JobOptions) {\n // A consuming instance only accepts jobs it can process itself; a\n // producer-only instance (consume: false) enqueues for other workers.\n if (!this.producerOnly && !this.handlers.has(name)) {\n throw new QueueError(`No handler registered for job \"${name}\"`, {\n context: { jobName: name },\n });\n }\n\n this.validatePayloadSize(name, data);\n\n if (opts?.repeat !== undefined) {\n this.validateRepeat(name, opts.repeat);\n }\n }\n\n /** Rechaza payloads cuya serialización JSON excede el tope configurado. */\n private validatePayloadSize(name: string, data: unknown) {\n let serialized: string;\n try {\n serialized = JSON.stringify(data ?? null);\n } catch (err) {\n throw new QueueError(`Job \"${name}\" data is not serializable`, {\n cause: err instanceof Error ? err : new Error(String(err)),\n context: { jobName: name },\n });\n }\n\n const size = Buffer.byteLength(serialized, 'utf8');\n if (size > WorkerManager.MAX_PAYLOAD_BYTES) {\n throw new QueueError(\n `Job \"${name}\" payload too large: ${size} bytes (max ${WorkerManager.MAX_PAYLOAD_BYTES})`,\n { context: { jobName: name, size, max: WorkerManager.MAX_PAYLOAD_BYTES } },\n );\n }\n }\n\n /** Rechaza specs de repetición vacías, intervalos no positivos o crons en blanco. */\n private validateRepeat(name: string, repeat: RepeatSpec) {\n if (typeof repeat === 'string') {\n if (repeat.trim().length === 0) {\n throw new QueueError(`Job \"${name}\" has an empty cron repeat pattern`, {\n context: { jobName: name },\n });\n }\n return;\n }\n\n const hasEvery = 'every' in repeat;\n const hasPattern = 'pattern' in repeat;\n if (!hasEvery && !hasPattern) {\n throw new QueueError(`Job \"${name}\" repeat spec must define \"every\" or \"pattern\"`, {\n context: { jobName: name },\n });\n }\n\n if (hasEvery && !(typeof repeat.every === 'number' && repeat.every > 0)) {\n throw new QueueError(`Job \"${name}\" repeat \"every\" must be a positive number`, {\n context: { jobName: name, every: repeat.every },\n });\n }\n\n if (hasPattern && (typeof repeat.pattern !== 'string' || repeat.pattern.trim().length === 0)) {\n throw new QueueError(`Job \"${name}\" repeat \"pattern\" must be a non-empty cron string`, {\n context: { jobName: name },\n });\n }\n }\n\n /**\n * Maneja el evento `failed` del worker. Loggea el fallo y, si el job agotó\n * todos sus reintentos y `deadLetter` está activado, emite\n * `worker:dead-letter` en el bus de eventos de la App.\n */\n private onFailed(job: BullJob | undefined, err: Error) {\n this.app?.logger.error(\n { jobId: job?.id, jobName: job?.name, attemptsMade: job?.attemptsMade, err },\n 'Job failed',\n );\n\n if (!this.options.deadLetter || !job) return;\n\n // BullMQ default attempts is 1 when unspecified.\n //\n // Assumed BullMQ `attemptsMade` semantics at the `failed` event: on a\n // job's TERMINAL failure (all retries exhausted) BullMQ reports\n // `attemptsMade == opts.attempts`, so `attemptsMade < maxAttempts`\n // identifies a non-terminal failure with a retry still pending. This is\n // verified against bullmq 5.78 (see test/dead-letter-attempts*.test.ts);\n // a future bump that changes `attemptsMade` reporting will fail those\n // tests loudly rather than silently skip dead-lettering.\n const maxAttempts = job.opts?.attempts ?? 1;\n // An UnrecoverableError (e.g. no handler) is terminal regardless of attempts left.\n if (job.attemptsMade < maxAttempts && err?.name !== 'UnrecoverableError') return;\n\n const payload: DeadLetterPayload = {\n jobId: job.id,\n name: job.name,\n data: job.data,\n failedReason: job.failedReason ?? err?.message,\n attemptsMade: job.attemptsMade,\n };\n this.app?.events.emit('worker:dead-letter', payload);\n this.app?.logger.warn(\n { jobId: job.id, jobName: job.name, attemptsMade: job.attemptsMade },\n 'Job routed to dead-letter',\n );\n }\n\n /**\n * Construye el descriptor de un job, incluyendo el helper `result()` que\n * espera el valor de retorno del handler vía `job.waitUntilFinished`.\n */\n private buildDescriptor<T, R>(job: BullJob, name: string, data: T): JobDescriptor<T, R> {\n return {\n id: job.id!,\n name,\n data,\n // async so a post-stop getQueueEvents() throw surfaces as a rejected\n // promise rather than a synchronous throw.\n result: async (ttlMs?: number): Promise<R> => {\n const queueEvents = this.getQueueEvents();\n return job.waitUntilFinished(queueEvents, ttlMs) as Promise<R>;\n },\n };\n }\n\n /**\n * Devuelve (creando perezosamente) una instancia compartida de QueueEvents\n * usada para esperar resultados de jobs.\n */\n private getQueueEvents(): QueueEvents {\n if (this.stopped) {\n throw new QueueError('WorkerManager is stopped; cannot open QueueEvents', {\n context: { queueName: this.options.queueName || 'iskra-jobs' },\n });\n }\n if (!this.queueEvents) {\n try {\n this.queueEvents = new QueueEvents(this.options.queueName || 'iskra-jobs', {\n connection: this.parseConnection(),\n });\n } catch (err) {\n throw new QueueError('Failed to initialize BullMQ QueueEvents', {\n cause: err instanceof Error ? err : new Error(String(err)),\n context: { queueName: this.options.queueName || 'iskra-jobs' },\n });\n }\n }\n return this.queueEvents;\n }\n}\n\n// A job that failed for good (see WorkerManager's dead-letter handling).\ndeclare module '@iskra-bun/core' {\n interface AppEvents {\n 'worker:dead-letter': DeadLetterPayload;\n }\n}\n","import { IskraError, ErrorCodes } from '@iskra-bun/core';\n\n// ─── Queue Error ─────────────────────────────────────────────────────────────\n\nexport class QueueError extends IskraError {\n constructor(message: string, options?: { cause?: Error; context?: Record<string, unknown> }) {\n super(message, { code: ErrorCodes.QUEUE_ERROR, ...options });\n this.name = 'QueueError';\n }\n}\n\n// ─── Job Error ───────────────────────────────────────────────────────────────\n\nexport class JobError extends IskraError {\n constructor(message: string, options?: { cause?: Error; context?: Record<string, unknown> }) {\n super(message, { code: ErrorCodes.JOB_ERROR, ...options });\n this.name = 'JobError';\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iskra-bun/worker-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Cola de jobs en segundo plano para Iskra con BullMQ.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"iskra",
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
},
|
|
21
21
|
"homepage": "https://github.com/fearful/iskra/tree/main/packages/worker-kit#readme",
|
|
22
22
|
"bugs": "https://github.com/fearful/iskra/issues",
|
|
23
|
+
"engines": {
|
|
24
|
+
"bun": ">=1.3.0"
|
|
25
|
+
},
|
|
23
26
|
"type": "module",
|
|
24
27
|
"main": "./dist/index.js",
|
|
25
28
|
"module": "./dist/index.js",
|
|
@@ -47,7 +50,7 @@
|
|
|
47
50
|
"build": "tsup --config ../../tsup.config.ts"
|
|
48
51
|
},
|
|
49
52
|
"dependencies": {
|
|
50
|
-
"@iskra-bun/core": "0.
|
|
53
|
+
"@iskra-bun/core": "^0.2.0",
|
|
51
54
|
"bullmq": "^5.0.0"
|
|
52
55
|
},
|
|
53
56
|
"devDependencies": {
|