@igniter-js/jobs 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/AGENTS.md +557 -0
- package/README.md +410 -0
- package/dist/adapter-CcQCatSa.d.mts +1411 -0
- package/dist/adapter-CcQCatSa.d.ts +1411 -0
- package/dist/adapters/bullmq.adapter.d.mts +131 -0
- package/dist/adapters/bullmq.adapter.d.ts +131 -0
- package/dist/adapters/bullmq.adapter.js +598 -0
- package/dist/adapters/bullmq.adapter.js.map +1 -0
- package/dist/adapters/bullmq.adapter.mjs +596 -0
- package/dist/adapters/bullmq.adapter.mjs.map +1 -0
- package/dist/adapters/index.d.mts +5 -0
- package/dist/adapters/index.d.ts +5 -0
- package/dist/adapters/index.js +1129 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/index.mjs +1126 -0
- package/dist/adapters/index.mjs.map +1 -0
- package/dist/adapters/memory.adapter.d.mts +118 -0
- package/dist/adapters/memory.adapter.d.ts +118 -0
- package/dist/adapters/memory.adapter.js +571 -0
- package/dist/adapters/memory.adapter.js.map +1 -0
- package/dist/adapters/memory.adapter.mjs +569 -0
- package/dist/adapters/memory.adapter.mjs.map +1 -0
- package/dist/index.d.mts +1107 -0
- package/dist/index.d.ts +1107 -0
- package/dist/index.js +1137 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1128 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +93 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@igniter-js/core');
|
|
4
|
+
|
|
5
|
+
// src/errors/igniter-jobs.error.ts
|
|
6
|
+
var IGNITER_JOBS_ERROR_CODES = {
|
|
7
|
+
// Configuration errors
|
|
8
|
+
JOBS_ADAPTER_REQUIRED: "JOBS_ADAPTER_REQUIRED",
|
|
9
|
+
JOBS_CONTEXT_REQUIRED: "JOBS_CONTEXT_REQUIRED",
|
|
10
|
+
JOBS_QUEUE_REQUIRED: "JOBS_QUEUE_REQUIRED",
|
|
11
|
+
JOBS_CONFIGURATION_INVALID: "JOBS_CONFIGURATION_INVALID",
|
|
12
|
+
// Queue errors
|
|
13
|
+
JOBS_QUEUE_NOT_FOUND: "JOBS_QUEUE_NOT_FOUND",
|
|
14
|
+
JOBS_QUEUE_ALREADY_EXISTS: "JOBS_QUEUE_ALREADY_EXISTS",
|
|
15
|
+
JOBS_QUEUE_NAME_INVALID: "JOBS_QUEUE_NAME_INVALID",
|
|
16
|
+
JOBS_QUEUE_PAUSE_FAILED: "JOBS_QUEUE_PAUSE_FAILED",
|
|
17
|
+
JOBS_QUEUE_RESUME_FAILED: "JOBS_QUEUE_RESUME_FAILED",
|
|
18
|
+
JOBS_QUEUE_DRAIN_FAILED: "JOBS_QUEUE_DRAIN_FAILED",
|
|
19
|
+
JOBS_QUEUE_CLEAN_FAILED: "JOBS_QUEUE_CLEAN_FAILED",
|
|
20
|
+
JOBS_QUEUE_OBLITERATE_FAILED: "JOBS_QUEUE_OBLITERATE_FAILED",
|
|
21
|
+
// Job definition errors
|
|
22
|
+
JOBS_JOB_NOT_FOUND: "JOBS_JOB_NOT_FOUND",
|
|
23
|
+
JOBS_JOB_ALREADY_EXISTS: "JOBS_JOB_ALREADY_EXISTS",
|
|
24
|
+
JOBS_JOB_NAME_INVALID: "JOBS_JOB_NAME_INVALID",
|
|
25
|
+
JOBS_JOB_HANDLER_REQUIRED: "JOBS_JOB_HANDLER_REQUIRED",
|
|
26
|
+
// Cron errors
|
|
27
|
+
JOBS_CRON_NOT_FOUND: "JOBS_CRON_NOT_FOUND",
|
|
28
|
+
JOBS_CRON_ALREADY_EXISTS: "JOBS_CRON_ALREADY_EXISTS",
|
|
29
|
+
JOBS_CRON_EXPRESSION_INVALID: "JOBS_CRON_EXPRESSION_INVALID",
|
|
30
|
+
JOBS_CRON_HANDLER_REQUIRED: "JOBS_CRON_HANDLER_REQUIRED",
|
|
31
|
+
// Dispatch errors
|
|
32
|
+
JOBS_DISPATCH_FAILED: "JOBS_DISPATCH_FAILED",
|
|
33
|
+
JOBS_SCHEDULE_FAILED: "JOBS_SCHEDULE_FAILED",
|
|
34
|
+
JOBS_INPUT_REQUIRED: "JOBS_INPUT_REQUIRED",
|
|
35
|
+
JOBS_INPUT_VALIDATION_FAILED: "JOBS_INPUT_VALIDATION_FAILED",
|
|
36
|
+
// Scope errors
|
|
37
|
+
JOBS_SCOPE_REQUIRED: "JOBS_SCOPE_REQUIRED",
|
|
38
|
+
JOBS_SCOPE_ALREADY_DEFINED: "JOBS_SCOPE_ALREADY_DEFINED",
|
|
39
|
+
JOBS_SCOPE_INVALID: "JOBS_SCOPE_INVALID",
|
|
40
|
+
// Actor errors
|
|
41
|
+
JOBS_ACTOR_ALREADY_DEFINED: "JOBS_ACTOR_ALREADY_DEFINED",
|
|
42
|
+
JOBS_ACTOR_INVALID: "JOBS_ACTOR_INVALID",
|
|
43
|
+
// Job management errors
|
|
44
|
+
JOBS_GET_FAILED: "JOBS_GET_FAILED",
|
|
45
|
+
JOBS_RETRY_FAILED: "JOBS_RETRY_FAILED",
|
|
46
|
+
JOBS_REMOVE_FAILED: "JOBS_REMOVE_FAILED",
|
|
47
|
+
JOBS_PROMOTE_FAILED: "JOBS_PROMOTE_FAILED",
|
|
48
|
+
JOBS_MOVE_FAILED: "JOBS_MOVE_FAILED",
|
|
49
|
+
JOBS_STATE_FAILED: "JOBS_STATE_FAILED",
|
|
50
|
+
JOBS_PROGRESS_FAILED: "JOBS_PROGRESS_FAILED",
|
|
51
|
+
JOBS_LOGS_FAILED: "JOBS_LOGS_FAILED",
|
|
52
|
+
// Worker errors
|
|
53
|
+
JOBS_WORKER_CREATE_FAILED: "JOBS_WORKER_CREATE_FAILED",
|
|
54
|
+
JOBS_WORKER_START_FAILED: "JOBS_WORKER_START_FAILED",
|
|
55
|
+
JOBS_WORKER_STOP_FAILED: "JOBS_WORKER_STOP_FAILED",
|
|
56
|
+
JOBS_WORKER_NOT_FOUND: "JOBS_WORKER_NOT_FOUND",
|
|
57
|
+
JOBS_WORKER_ALREADY_RUNNING: "JOBS_WORKER_ALREADY_RUNNING",
|
|
58
|
+
// Event/Subscribe errors
|
|
59
|
+
JOBS_SUBSCRIBE_FAILED: "JOBS_SUBSCRIBE_FAILED",
|
|
60
|
+
JOBS_UNSUBSCRIBE_FAILED: "JOBS_UNSUBSCRIBE_FAILED",
|
|
61
|
+
JOBS_EVENT_EMIT_FAILED: "JOBS_EVENT_EMIT_FAILED",
|
|
62
|
+
// Search errors
|
|
63
|
+
JOBS_SEARCH_FAILED: "JOBS_SEARCH_FAILED",
|
|
64
|
+
JOBS_SEARCH_INVALID_TARGET: "JOBS_SEARCH_INVALID_TARGET",
|
|
65
|
+
// Shutdown errors
|
|
66
|
+
JOBS_SHUTDOWN_FAILED: "JOBS_SHUTDOWN_FAILED",
|
|
67
|
+
// Handler errors
|
|
68
|
+
JOBS_HANDLER_FAILED: "JOBS_HANDLER_FAILED",
|
|
69
|
+
JOBS_HANDLER_TIMEOUT: "JOBS_HANDLER_TIMEOUT"
|
|
70
|
+
};
|
|
71
|
+
var IgniterJobsError = class _IgniterJobsError extends core.IgniterError {
|
|
72
|
+
constructor(options) {
|
|
73
|
+
super({
|
|
74
|
+
code: options.code,
|
|
75
|
+
message: options.message,
|
|
76
|
+
statusCode: options.statusCode ?? 500,
|
|
77
|
+
causer: "@igniter-js/jobs",
|
|
78
|
+
cause: options.cause,
|
|
79
|
+
details: options.details,
|
|
80
|
+
logger: options.logger
|
|
81
|
+
});
|
|
82
|
+
this.code = options.code;
|
|
83
|
+
this.details = options.details;
|
|
84
|
+
this.name = "IgniterJobsError";
|
|
85
|
+
if (Error.captureStackTrace) {
|
|
86
|
+
Error.captureStackTrace(this, _IgniterJobsError);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Convert error to a plain object for serialization.
|
|
91
|
+
*/
|
|
92
|
+
toJSON() {
|
|
93
|
+
return {
|
|
94
|
+
name: this.name,
|
|
95
|
+
code: this.code,
|
|
96
|
+
message: this.message,
|
|
97
|
+
statusCode: this.statusCode,
|
|
98
|
+
details: this.details,
|
|
99
|
+
stack: this.stack
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// src/builders/igniter-worker.builder.ts
|
|
105
|
+
var IgniterWorkerBuilder = class {
|
|
106
|
+
constructor(adapter, jobHandler, availableQueues) {
|
|
107
|
+
this.adapter = adapter;
|
|
108
|
+
this.jobHandler = jobHandler;
|
|
109
|
+
this.availableQueues = availableQueues;
|
|
110
|
+
this.state = {
|
|
111
|
+
queues: [],
|
|
112
|
+
concurrency: 1
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Specify which queues this worker should process.
|
|
117
|
+
* If not called, worker processes all queues.
|
|
118
|
+
*
|
|
119
|
+
* @param queues - Queue names to process
|
|
120
|
+
* @returns The builder for chaining
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```typescript
|
|
124
|
+
* .forQueues('email', 'payment')
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
forQueues(...queues) {
|
|
128
|
+
for (const queue of queues) {
|
|
129
|
+
if (!this.availableQueues.includes(queue)) {
|
|
130
|
+
throw new IgniterJobsError({
|
|
131
|
+
code: "JOBS_QUEUE_NOT_FOUND",
|
|
132
|
+
message: `Queue "${queue}" is not registered. Available queues: ${this.availableQueues.join(", ")}`,
|
|
133
|
+
statusCode: 400,
|
|
134
|
+
details: { queue, availableQueues: this.availableQueues }
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
this.state.queues = queues;
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Set the concurrency level (jobs processed in parallel per queue).
|
|
143
|
+
*
|
|
144
|
+
* @param concurrency - Number of parallel jobs
|
|
145
|
+
* @returns The builder for chaining
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```typescript
|
|
149
|
+
* .withConcurrency(10)
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
withConcurrency(concurrency) {
|
|
153
|
+
if (concurrency < 1) {
|
|
154
|
+
throw new IgniterJobsError({
|
|
155
|
+
code: "JOBS_CONFIGURATION_INVALID",
|
|
156
|
+
message: "Concurrency must be at least 1",
|
|
157
|
+
statusCode: 400,
|
|
158
|
+
details: { concurrency }
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
this.state.concurrency = concurrency;
|
|
162
|
+
return this;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Set the lock duration in milliseconds.
|
|
166
|
+
* Jobs are locked for this duration while being processed.
|
|
167
|
+
*
|
|
168
|
+
* @param duration - Lock duration in milliseconds
|
|
169
|
+
* @returns The builder for chaining
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```typescript
|
|
173
|
+
* .withLockDuration(30000) // 30 seconds
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
withLockDuration(duration) {
|
|
177
|
+
if (duration < 1e3) {
|
|
178
|
+
throw new IgniterJobsError({
|
|
179
|
+
code: "JOBS_CONFIGURATION_INVALID",
|
|
180
|
+
message: "Lock duration must be at least 1000ms",
|
|
181
|
+
statusCode: 400,
|
|
182
|
+
details: { duration }
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
this.state.lockDuration = duration;
|
|
186
|
+
return this;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Configure rate limiting for the worker.
|
|
190
|
+
*
|
|
191
|
+
* @param config - Rate limiter configuration
|
|
192
|
+
* @returns The builder for chaining
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* .withLimiter({ max: 100, duration: 60000 }) // 100 jobs per minute
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
withLimiter(config) {
|
|
200
|
+
if (config.max < 1) {
|
|
201
|
+
throw new IgniterJobsError({
|
|
202
|
+
code: "JOBS_CONFIGURATION_INVALID",
|
|
203
|
+
message: "Limiter max must be at least 1",
|
|
204
|
+
statusCode: 400,
|
|
205
|
+
details: { max: config.max }
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (config.duration < 1) {
|
|
209
|
+
throw new IgniterJobsError({
|
|
210
|
+
code: "JOBS_CONFIGURATION_INVALID",
|
|
211
|
+
message: "Limiter duration must be at least 1ms",
|
|
212
|
+
statusCode: 400,
|
|
213
|
+
details: { duration: config.duration }
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
this.state.limiter = config;
|
|
217
|
+
return this;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Set a callback to be called when the worker becomes idle.
|
|
221
|
+
*
|
|
222
|
+
* @param callback - Idle callback
|
|
223
|
+
* @returns The builder for chaining
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```typescript
|
|
227
|
+
* .onIdle(() => console.log('Worker is idle'))
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
onIdle(callback) {
|
|
231
|
+
this.state.onIdle = callback;
|
|
232
|
+
return this;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Start the worker.
|
|
236
|
+
*
|
|
237
|
+
* @returns Worker handle for management
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* ```typescript
|
|
241
|
+
* const worker = await jobs.worker
|
|
242
|
+
* .create()
|
|
243
|
+
* .forQueues('email')
|
|
244
|
+
* .start()
|
|
245
|
+
*
|
|
246
|
+
* // Later
|
|
247
|
+
* await worker.pause()
|
|
248
|
+
* await worker.close()
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
async start() {
|
|
252
|
+
const queuesToProcess = this.state.queues.length > 0 ? this.state.queues : this.availableQueues;
|
|
253
|
+
try {
|
|
254
|
+
const handle = await this.adapter.createWorker(
|
|
255
|
+
{
|
|
256
|
+
queues: queuesToProcess,
|
|
257
|
+
concurrency: this.state.concurrency,
|
|
258
|
+
lockDuration: this.state.lockDuration,
|
|
259
|
+
limiter: this.state.limiter,
|
|
260
|
+
onIdle: this.state.onIdle
|
|
261
|
+
},
|
|
262
|
+
this.jobHandler
|
|
263
|
+
);
|
|
264
|
+
return {
|
|
265
|
+
id: handle.id,
|
|
266
|
+
pause: () => handle.pause(),
|
|
267
|
+
resume: () => handle.resume(),
|
|
268
|
+
close: () => handle.close(),
|
|
269
|
+
isRunning: () => handle.isRunning(),
|
|
270
|
+
isPaused: () => handle.isPaused(),
|
|
271
|
+
getMetrics: () => handle.getMetrics()
|
|
272
|
+
};
|
|
273
|
+
} catch (error) {
|
|
274
|
+
throw new IgniterJobsError({
|
|
275
|
+
code: "JOBS_WORKER_START_FAILED",
|
|
276
|
+
message: "Failed to start worker",
|
|
277
|
+
statusCode: 500,
|
|
278
|
+
cause: error instanceof Error ? error : void 0,
|
|
279
|
+
details: { queues: queuesToProcess }
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// src/core/igniter-jobs.ts
|
|
286
|
+
var IGNITER_JOBS_PREFIX = "igniter:jobs";
|
|
287
|
+
function getFullQueueName(queueName) {
|
|
288
|
+
return `${IGNITER_JOBS_PREFIX}:${queueName}`;
|
|
289
|
+
}
|
|
290
|
+
var IgniterJobsRuntime = class {
|
|
291
|
+
constructor(config) {
|
|
292
|
+
this.config = config;
|
|
293
|
+
this.queueNames = Object.keys(config.queues);
|
|
294
|
+
return this.createProxy();
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Create the main proxy that provides queue access and global methods.
|
|
298
|
+
*/
|
|
299
|
+
createProxy() {
|
|
300
|
+
const self = this;
|
|
301
|
+
return new Proxy({}, {
|
|
302
|
+
get(_, prop) {
|
|
303
|
+
if (prop === "subscribe") {
|
|
304
|
+
return self.createGlobalSubscribe();
|
|
305
|
+
}
|
|
306
|
+
if (prop === "search") {
|
|
307
|
+
return self.createSearch();
|
|
308
|
+
}
|
|
309
|
+
if (prop === "worker") {
|
|
310
|
+
return self.createWorkerBuilder();
|
|
311
|
+
}
|
|
312
|
+
if (prop === "shutdown") {
|
|
313
|
+
return () => self.shutdown();
|
|
314
|
+
}
|
|
315
|
+
if (self.queueNames.includes(prop)) {
|
|
316
|
+
return self.createQueueProxy(prop);
|
|
317
|
+
}
|
|
318
|
+
return void 0;
|
|
319
|
+
},
|
|
320
|
+
has(_, prop) {
|
|
321
|
+
return ["subscribe", "search", "worker", "shutdown", ...self.queueNames].includes(prop);
|
|
322
|
+
},
|
|
323
|
+
ownKeys() {
|
|
324
|
+
return ["subscribe", "search", "worker", "shutdown", ...self.queueNames];
|
|
325
|
+
},
|
|
326
|
+
getOwnPropertyDescriptor(_, prop) {
|
|
327
|
+
if (["subscribe", "search", "worker", "shutdown", ...self.queueNames].includes(prop)) {
|
|
328
|
+
return { configurable: true, enumerable: true };
|
|
329
|
+
}
|
|
330
|
+
return void 0;
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Create a proxy for a specific queue.
|
|
336
|
+
*/
|
|
337
|
+
createQueueProxy(queueName) {
|
|
338
|
+
const self = this;
|
|
339
|
+
const queueConfig = this.config.queues[queueName];
|
|
340
|
+
const jobNames = Object.keys(queueConfig.jobs);
|
|
341
|
+
return new Proxy({}, {
|
|
342
|
+
get(_, prop) {
|
|
343
|
+
if (prop === "subscribe") {
|
|
344
|
+
return self.createQueueSubscribe(queueName);
|
|
345
|
+
}
|
|
346
|
+
if (prop === "list") {
|
|
347
|
+
return (options) => self.listQueueJobs(queueName, options);
|
|
348
|
+
}
|
|
349
|
+
if (prop === "get") {
|
|
350
|
+
return () => self.createQueueManagement(queueName);
|
|
351
|
+
}
|
|
352
|
+
if (jobNames.includes(prop)) {
|
|
353
|
+
return self.createJobProxy(queueName, prop);
|
|
354
|
+
}
|
|
355
|
+
return void 0;
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Create a proxy for a specific job.
|
|
361
|
+
*/
|
|
362
|
+
createJobProxy(queueName, jobName) {
|
|
363
|
+
const self = this;
|
|
364
|
+
return {
|
|
365
|
+
dispatch: (input, options) => self.dispatchJob(queueName, jobName, input, options),
|
|
366
|
+
schedule: (params) => self.scheduleJob(queueName, jobName, params),
|
|
367
|
+
get: (jobId) => self.createJobManagement(queueName, jobId),
|
|
368
|
+
many: (jobIds) => self.createJobBatchManagement(queueName, jobIds),
|
|
369
|
+
pause: () => self.pauseJobType(queueName, jobName),
|
|
370
|
+
resume: () => self.resumeJobType(queueName, jobName),
|
|
371
|
+
subscribe: (handler) => self.subscribeToJob(queueName, jobName, handler)
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Dispatch a job.
|
|
376
|
+
*/
|
|
377
|
+
async dispatchJob(queueName, jobName, input, options) {
|
|
378
|
+
const queueConfig = this.config.queues[queueName];
|
|
379
|
+
const jobDef = queueConfig.jobs[jobName];
|
|
380
|
+
if (jobDef.input) {
|
|
381
|
+
try {
|
|
382
|
+
const result = await jobDef.input["~standard"].validate(input);
|
|
383
|
+
if (result.issues) {
|
|
384
|
+
throw new IgniterJobsError({
|
|
385
|
+
code: "JOBS_INPUT_VALIDATION_FAILED",
|
|
386
|
+
message: `Input validation failed for job "${jobName}"`,
|
|
387
|
+
statusCode: 400,
|
|
388
|
+
details: { issues: result.issues }
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
input = result.value;
|
|
392
|
+
} catch (error) {
|
|
393
|
+
if (error instanceof IgniterJobsError) throw error;
|
|
394
|
+
if (typeof jobDef.input.parse === "function") {
|
|
395
|
+
input = jobDef.input.parse(input);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (this.config.scope?.options?.required && !options?.scope) {
|
|
400
|
+
throw new IgniterJobsError({
|
|
401
|
+
code: "JOBS_SCOPE_REQUIRED",
|
|
402
|
+
message: `Scope "${this.config.scope.key}" is required for job dispatch`,
|
|
403
|
+
statusCode: 400,
|
|
404
|
+
details: { queue: queueName, job: jobName }
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
const params = {
|
|
408
|
+
queue: getFullQueueName(queueName),
|
|
409
|
+
name: jobName,
|
|
410
|
+
data: input,
|
|
411
|
+
jobId: options?.jobId,
|
|
412
|
+
delay: options?.delay,
|
|
413
|
+
priority: options?.priority ?? jobDef.priority,
|
|
414
|
+
attempts: jobDef.retry?.attempts ?? this.config.defaults?.retry?.attempts ?? 3,
|
|
415
|
+
backoff: jobDef.retry?.backoff ?? this.config.defaults?.retry?.backoff,
|
|
416
|
+
removeOnComplete: jobDef.removeOnComplete ?? this.config.defaults?.removeOnComplete,
|
|
417
|
+
removeOnFail: jobDef.removeOnFail ?? this.config.defaults?.removeOnFail,
|
|
418
|
+
scope: options?.scope,
|
|
419
|
+
actor: options?.actor
|
|
420
|
+
};
|
|
421
|
+
try {
|
|
422
|
+
const jobId = await this.config.adapter.dispatch(params);
|
|
423
|
+
if (this.config.telemetry) {
|
|
424
|
+
}
|
|
425
|
+
return jobId;
|
|
426
|
+
} catch (error) {
|
|
427
|
+
throw new IgniterJobsError({
|
|
428
|
+
code: "JOBS_DISPATCH_FAILED",
|
|
429
|
+
message: `Failed to dispatch job "${jobName}" to queue "${queueName}"`,
|
|
430
|
+
statusCode: 500,
|
|
431
|
+
cause: error instanceof Error ? error : void 0,
|
|
432
|
+
details: { queue: queueName, job: jobName }
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Schedule a job for future execution.
|
|
438
|
+
*/
|
|
439
|
+
async scheduleJob(queueName, jobName, params) {
|
|
440
|
+
const queueConfig = this.config.queues[queueName];
|
|
441
|
+
const jobDef = queueConfig.jobs[jobName];
|
|
442
|
+
let validatedInput = params.input;
|
|
443
|
+
if (jobDef.input) {
|
|
444
|
+
try {
|
|
445
|
+
const result = await jobDef.input["~standard"].validate(params.input);
|
|
446
|
+
if (result.issues) {
|
|
447
|
+
throw new IgniterJobsError({
|
|
448
|
+
code: "JOBS_INPUT_VALIDATION_FAILED",
|
|
449
|
+
message: `Input validation failed for job "${jobName}"`,
|
|
450
|
+
statusCode: 400,
|
|
451
|
+
details: { issues: result.issues }
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
validatedInput = result.value;
|
|
455
|
+
} catch (error) {
|
|
456
|
+
if (error instanceof IgniterJobsError) throw error;
|
|
457
|
+
if (typeof jobDef.input.parse === "function") {
|
|
458
|
+
validatedInput = jobDef.input.parse(params.input);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const scheduleParams = {
|
|
463
|
+
queue: getFullQueueName(queueName),
|
|
464
|
+
name: jobName,
|
|
465
|
+
data: validatedInput,
|
|
466
|
+
jobId: params.jobId,
|
|
467
|
+
at: params.at,
|
|
468
|
+
cron: params.cron,
|
|
469
|
+
every: params.every,
|
|
470
|
+
timezone: params.timezone,
|
|
471
|
+
attempts: jobDef.retry?.attempts ?? this.config.defaults?.retry?.attempts ?? 3,
|
|
472
|
+
backoff: jobDef.retry?.backoff ?? this.config.defaults?.retry?.backoff,
|
|
473
|
+
removeOnComplete: jobDef.removeOnComplete ?? this.config.defaults?.removeOnComplete,
|
|
474
|
+
removeOnFail: jobDef.removeOnFail ?? this.config.defaults?.removeOnFail,
|
|
475
|
+
scope: params.scope,
|
|
476
|
+
actor: params.actor
|
|
477
|
+
};
|
|
478
|
+
try {
|
|
479
|
+
return await this.config.adapter.schedule(scheduleParams);
|
|
480
|
+
} catch (error) {
|
|
481
|
+
throw new IgniterJobsError({
|
|
482
|
+
code: "JOBS_SCHEDULE_FAILED",
|
|
483
|
+
message: `Failed to schedule job "${jobName}" in queue "${queueName}"`,
|
|
484
|
+
statusCode: 500,
|
|
485
|
+
cause: error instanceof Error ? error : void 0,
|
|
486
|
+
details: { queue: queueName, job: jobName }
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Create job management methods.
|
|
492
|
+
*/
|
|
493
|
+
createJobManagement(queueName, jobId) {
|
|
494
|
+
const adapter = this.config.adapter;
|
|
495
|
+
const fullQueueName = getFullQueueName(queueName);
|
|
496
|
+
return {
|
|
497
|
+
retrieve: () => adapter.getJob(fullQueueName, jobId),
|
|
498
|
+
retry: () => adapter.retryJob(fullQueueName, jobId),
|
|
499
|
+
remove: () => adapter.removeJob(fullQueueName, jobId),
|
|
500
|
+
state: () => adapter.getJobState(fullQueueName, jobId),
|
|
501
|
+
progress: () => adapter.getJobProgress(fullQueueName, jobId),
|
|
502
|
+
logs: () => adapter.getJobLogs(fullQueueName, jobId),
|
|
503
|
+
promote: () => adapter.promoteJob(fullQueueName, jobId),
|
|
504
|
+
move: (state, reason) => adapter.moveJob(fullQueueName, jobId, state, reason)
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Create batch job management methods.
|
|
509
|
+
*/
|
|
510
|
+
createJobBatchManagement(queueName, jobIds) {
|
|
511
|
+
const adapter = this.config.adapter;
|
|
512
|
+
const fullQueueName = getFullQueueName(queueName);
|
|
513
|
+
return {
|
|
514
|
+
retry: () => adapter.retryJobs(fullQueueName, jobIds),
|
|
515
|
+
remove: () => adapter.removeJobs(fullQueueName, jobIds)
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Create queue management methods.
|
|
520
|
+
*/
|
|
521
|
+
createQueueManagement(queueName) {
|
|
522
|
+
const adapter = this.config.adapter;
|
|
523
|
+
const fullQueueName = getFullQueueName(queueName);
|
|
524
|
+
return {
|
|
525
|
+
retrieve: () => adapter.getQueue(fullQueueName),
|
|
526
|
+
pause: () => adapter.pauseQueue(fullQueueName),
|
|
527
|
+
resume: () => adapter.resumeQueue(fullQueueName),
|
|
528
|
+
drain: () => adapter.drainQueue(fullQueueName),
|
|
529
|
+
clean: (options) => adapter.cleanQueue(fullQueueName, {
|
|
530
|
+
status: options.status,
|
|
531
|
+
olderThan: options.olderThan,
|
|
532
|
+
limit: options.limit
|
|
533
|
+
}),
|
|
534
|
+
obliterate: (options) => adapter.obliterateQueue(fullQueueName, options),
|
|
535
|
+
retryAll: () => adapter.retryAllFailed(fullQueueName)
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Pause a specific job type.
|
|
540
|
+
*/
|
|
541
|
+
async pauseJobType(queueName, jobName) {
|
|
542
|
+
const fullQueueName = getFullQueueName(queueName);
|
|
543
|
+
await this.config.adapter.pauseJobType(fullQueueName, jobName);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Resume a specific job type.
|
|
547
|
+
*/
|
|
548
|
+
async resumeJobType(queueName, jobName) {
|
|
549
|
+
const fullQueueName = getFullQueueName(queueName);
|
|
550
|
+
await this.config.adapter.resumeJobType(fullQueueName, jobName);
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Subscribe to events for a specific job.
|
|
554
|
+
*/
|
|
555
|
+
async subscribeToJob(queueName, jobName, handler) {
|
|
556
|
+
const pattern = `${queueName}:${jobName}:*`;
|
|
557
|
+
return this.config.adapter.subscribe(pattern, handler);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Create queue-level subscribe.
|
|
561
|
+
*/
|
|
562
|
+
createQueueSubscribe(queueName) {
|
|
563
|
+
return async (handler) => {
|
|
564
|
+
const pattern = `${queueName}:*`;
|
|
565
|
+
return this.config.adapter.subscribe(pattern, handler);
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* List jobs in a queue.
|
|
570
|
+
*/
|
|
571
|
+
async listQueueJobs(queueName, options) {
|
|
572
|
+
const fullQueueName = getFullQueueName(queueName);
|
|
573
|
+
return this.config.adapter.listJobs(fullQueueName, {
|
|
574
|
+
status: options?.status,
|
|
575
|
+
end: options?.limit
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Create global subscribe.
|
|
580
|
+
*/
|
|
581
|
+
createGlobalSubscribe() {
|
|
582
|
+
return async (handler) => {
|
|
583
|
+
return this.config.adapter.subscribe("*", handler);
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Create search function.
|
|
588
|
+
*/
|
|
589
|
+
createSearch() {
|
|
590
|
+
const adapter = this.config.adapter;
|
|
591
|
+
return async (target, filter) => {
|
|
592
|
+
switch (target) {
|
|
593
|
+
case "queues":
|
|
594
|
+
return adapter.searchQueues(filter || {});
|
|
595
|
+
case "jobs":
|
|
596
|
+
return adapter.searchJobs(filter || {});
|
|
597
|
+
case "workers":
|
|
598
|
+
return [];
|
|
599
|
+
default:
|
|
600
|
+
throw new IgniterJobsError({
|
|
601
|
+
code: "JOBS_SEARCH_INVALID_TARGET",
|
|
602
|
+
message: `Invalid search target "${target}". Use "queues", "jobs", or "workers".`,
|
|
603
|
+
statusCode: 400
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Create worker builder.
|
|
610
|
+
*/
|
|
611
|
+
createWorkerBuilder() {
|
|
612
|
+
return {
|
|
613
|
+
create: () => new IgniterWorkerBuilder(
|
|
614
|
+
this.config.adapter,
|
|
615
|
+
this.createJobHandler(),
|
|
616
|
+
this.queueNames.map((q) => getFullQueueName(q))
|
|
617
|
+
)
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Create the job handler function for workers.
|
|
622
|
+
*/
|
|
623
|
+
createJobHandler() {
|
|
624
|
+
const self = this;
|
|
625
|
+
return async (job) => {
|
|
626
|
+
const queueName = job.queue.replace(`${IGNITER_JOBS_PREFIX}:`, "");
|
|
627
|
+
const queueConfig = self.config.queues[queueName];
|
|
628
|
+
if (!queueConfig) {
|
|
629
|
+
throw new IgniterJobsError({
|
|
630
|
+
code: "JOBS_QUEUE_NOT_FOUND",
|
|
631
|
+
message: `Queue "${queueName}" not found`,
|
|
632
|
+
statusCode: 404
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
const jobDef = queueConfig.jobs[job.name];
|
|
636
|
+
if (!jobDef) {
|
|
637
|
+
throw new IgniterJobsError({
|
|
638
|
+
code: "JOBS_JOB_NOT_FOUND",
|
|
639
|
+
message: `Job "${job.name}" not found in queue "${queueName}"`,
|
|
640
|
+
statusCode: 404
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
const context = await self.config.context();
|
|
644
|
+
const ctx = {
|
|
645
|
+
input: job.data,
|
|
646
|
+
context,
|
|
647
|
+
job: {
|
|
648
|
+
id: job.id,
|
|
649
|
+
name: job.name,
|
|
650
|
+
queue: queueName,
|
|
651
|
+
timestamp: job.timestamp
|
|
652
|
+
},
|
|
653
|
+
attempt: job.attempt,
|
|
654
|
+
scope: job.scope,
|
|
655
|
+
actor: job.actor,
|
|
656
|
+
log: job.log,
|
|
657
|
+
updateProgress: job.updateProgress
|
|
658
|
+
};
|
|
659
|
+
try {
|
|
660
|
+
const result = await jobDef.handler(ctx);
|
|
661
|
+
if (jobDef.onComplete) {
|
|
662
|
+
await jobDef.onComplete(ctx, result);
|
|
663
|
+
}
|
|
664
|
+
return result;
|
|
665
|
+
} catch (error) {
|
|
666
|
+
if (jobDef.onFailure) {
|
|
667
|
+
await jobDef.onFailure(ctx, error instanceof Error ? error : new Error(String(error)));
|
|
668
|
+
}
|
|
669
|
+
throw error;
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Shutdown the jobs instance.
|
|
675
|
+
*/
|
|
676
|
+
async shutdown() {
|
|
677
|
+
try {
|
|
678
|
+
await this.config.adapter.shutdown();
|
|
679
|
+
} catch (error) {
|
|
680
|
+
throw new IgniterJobsError({
|
|
681
|
+
code: "JOBS_SHUTDOWN_FAILED",
|
|
682
|
+
message: "Failed to shutdown jobs instance",
|
|
683
|
+
statusCode: 500,
|
|
684
|
+
cause: error instanceof Error ? error : void 0
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
// src/builders/igniter-jobs.builder.ts
|
|
691
|
+
var IgniterJobsBuilder = class _IgniterJobsBuilder {
|
|
692
|
+
constructor(state) {
|
|
693
|
+
this.state = state;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Create a new IgniterJobs builder.
|
|
697
|
+
*
|
|
698
|
+
* @returns A new IgniterJobsBuilder instance
|
|
699
|
+
*
|
|
700
|
+
* @example
|
|
701
|
+
* ```typescript
|
|
702
|
+
* const jobs = IgniterJobs.create<AppContext>()
|
|
703
|
+
* .withAdapter(...)
|
|
704
|
+
* .build()
|
|
705
|
+
* ```
|
|
706
|
+
*/
|
|
707
|
+
static create() {
|
|
708
|
+
return new _IgniterJobsBuilder({
|
|
709
|
+
queues: {}
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Configure the queue adapter (required).
|
|
714
|
+
*
|
|
715
|
+
* @param adapter - The queue adapter (e.g., BullMQAdapter)
|
|
716
|
+
* @returns The builder with adapter configured
|
|
717
|
+
*
|
|
718
|
+
* @example
|
|
719
|
+
* ```typescript
|
|
720
|
+
* .withAdapter(BullMQAdapter.create({ redis }))
|
|
721
|
+
* ```
|
|
722
|
+
*/
|
|
723
|
+
withAdapter(adapter) {
|
|
724
|
+
return new _IgniterJobsBuilder({
|
|
725
|
+
...this.state,
|
|
726
|
+
adapter
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Configure the application context provider (required).
|
|
731
|
+
* This function is called for each job to provide the application context.
|
|
732
|
+
*
|
|
733
|
+
* @param contextFn - Function that returns the application context
|
|
734
|
+
* @returns The builder with context configured
|
|
735
|
+
*
|
|
736
|
+
* @example
|
|
737
|
+
* ```typescript
|
|
738
|
+
* .withContext(() => ({
|
|
739
|
+
* db: prisma,
|
|
740
|
+
* mailer: mailerService,
|
|
741
|
+
* cache: redis,
|
|
742
|
+
* }))
|
|
743
|
+
* ```
|
|
744
|
+
*/
|
|
745
|
+
withContext(contextFn) {
|
|
746
|
+
return new _IgniterJobsBuilder({
|
|
747
|
+
...this.state,
|
|
748
|
+
context: contextFn
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Add a queue to the jobs instance.
|
|
753
|
+
*
|
|
754
|
+
* @param queue - Queue configuration from IgniterQueue.create().build()
|
|
755
|
+
* @returns The builder with the queue added
|
|
756
|
+
*
|
|
757
|
+
* @example
|
|
758
|
+
* ```typescript
|
|
759
|
+
* const emailQueue = IgniterQueue.create<AppContext>('email')
|
|
760
|
+
* .addJob('sendWelcome', { ... })
|
|
761
|
+
* .build()
|
|
762
|
+
*
|
|
763
|
+
* const jobs = IgniterJobs.create<AppContext>()
|
|
764
|
+
* .addQueue(emailQueue)
|
|
765
|
+
* .build()
|
|
766
|
+
* ```
|
|
767
|
+
*/
|
|
768
|
+
addQueue(queue) {
|
|
769
|
+
const queueName = queue.name;
|
|
770
|
+
if (queueName in this.state.queues) {
|
|
771
|
+
throw new IgniterJobsError({
|
|
772
|
+
code: "JOBS_QUEUE_ALREADY_EXISTS",
|
|
773
|
+
message: `Queue "${queueName}" has already been added`,
|
|
774
|
+
statusCode: 400,
|
|
775
|
+
details: { queue: queueName }
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
return new _IgniterJobsBuilder({
|
|
779
|
+
...this.state,
|
|
780
|
+
queues: {
|
|
781
|
+
...this.state.queues,
|
|
782
|
+
[queueName]: queue
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Add a scope for multi-tenancy (only one scope allowed).
|
|
788
|
+
* Scopes are used to isolate jobs by organization, tenant, etc.
|
|
789
|
+
*
|
|
790
|
+
* @param key - Scope key (e.g., 'organization', 'tenant')
|
|
791
|
+
* @param options - Optional scope configuration
|
|
792
|
+
* @returns The builder with scope configured
|
|
793
|
+
*
|
|
794
|
+
* @example
|
|
795
|
+
* ```typescript
|
|
796
|
+
* .addScope('organization', { required: true })
|
|
797
|
+
* ```
|
|
798
|
+
*/
|
|
799
|
+
addScope(key, options) {
|
|
800
|
+
if (this.state.scope) {
|
|
801
|
+
throw new IgniterJobsError({
|
|
802
|
+
code: "JOBS_SCOPE_ALREADY_DEFINED",
|
|
803
|
+
message: `Scope "${this.state.scope.key}" is already defined. Only one scope is allowed.`,
|
|
804
|
+
statusCode: 400,
|
|
805
|
+
details: { existingScope: this.state.scope.key, newScope: key }
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
return new _IgniterJobsBuilder({
|
|
809
|
+
...this.state,
|
|
810
|
+
scope: { key, options }
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Add an actor for auditing (only one actor type allowed).
|
|
815
|
+
* Actors are used to track who initiated jobs.
|
|
816
|
+
*
|
|
817
|
+
* @param key - Actor key (e.g., 'user', 'system')
|
|
818
|
+
* @param options - Optional actor configuration
|
|
819
|
+
* @returns The builder with actor configured
|
|
820
|
+
*
|
|
821
|
+
* @example
|
|
822
|
+
* ```typescript
|
|
823
|
+
* .addActor('user', { description: 'The user who initiated the job' })
|
|
824
|
+
* ```
|
|
825
|
+
*/
|
|
826
|
+
addActor(key, options) {
|
|
827
|
+
if (this.state.actor) {
|
|
828
|
+
throw new IgniterJobsError({
|
|
829
|
+
code: "JOBS_ACTOR_ALREADY_DEFINED",
|
|
830
|
+
message: `Actor "${this.state.actor.key}" is already defined. Only one actor is allowed.`,
|
|
831
|
+
statusCode: 400,
|
|
832
|
+
details: { existingActor: this.state.actor.key, newActor: key }
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
return new _IgniterJobsBuilder({
|
|
836
|
+
...this.state,
|
|
837
|
+
actor: { key, options }
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Configure telemetry for observability (optional).
|
|
842
|
+
*
|
|
843
|
+
* @param telemetry - IgniterTelemetry instance
|
|
844
|
+
* @returns The builder with telemetry configured
|
|
845
|
+
*
|
|
846
|
+
* @example
|
|
847
|
+
* ```typescript
|
|
848
|
+
* .withTelemetry(telemetry)
|
|
849
|
+
* ```
|
|
850
|
+
*/
|
|
851
|
+
withTelemetry(telemetry) {
|
|
852
|
+
return new _IgniterJobsBuilder({
|
|
853
|
+
...this.state,
|
|
854
|
+
telemetry
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Configure a custom logger (optional).
|
|
859
|
+
*
|
|
860
|
+
* @param logger - Logger instance
|
|
861
|
+
* @returns The builder with logger configured
|
|
862
|
+
*
|
|
863
|
+
* @example
|
|
864
|
+
* ```typescript
|
|
865
|
+
* .withLogger(customLogger)
|
|
866
|
+
* ```
|
|
867
|
+
*/
|
|
868
|
+
withLogger(logger) {
|
|
869
|
+
return new _IgniterJobsBuilder({
|
|
870
|
+
...this.state,
|
|
871
|
+
logger
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Configure default job options applied to all jobs.
|
|
876
|
+
*
|
|
877
|
+
* @param defaults - Default job configuration
|
|
878
|
+
* @returns The builder with defaults configured
|
|
879
|
+
*
|
|
880
|
+
* @example
|
|
881
|
+
* ```typescript
|
|
882
|
+
* .withDefaults({
|
|
883
|
+
* retry: { attempts: 3, backoff: { type: 'exponential', delay: 1000 } },
|
|
884
|
+
* timeout: 30000,
|
|
885
|
+
* removeOnComplete: { count: 1000 },
|
|
886
|
+
* })
|
|
887
|
+
* ```
|
|
888
|
+
*/
|
|
889
|
+
withDefaults(defaults) {
|
|
890
|
+
return new _IgniterJobsBuilder({
|
|
891
|
+
...this.state,
|
|
892
|
+
defaults
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Build the IgniterJobs instance.
|
|
897
|
+
* Validates configuration and returns a fully typed jobs instance.
|
|
898
|
+
*
|
|
899
|
+
* @returns The IgniterJobs instance with proxy for typed access
|
|
900
|
+
*
|
|
901
|
+
* @throws {IgniterJobsError} If adapter is not configured
|
|
902
|
+
* @throws {IgniterJobsError} If context is not configured
|
|
903
|
+
* @throws {IgniterJobsError} If no queues are defined
|
|
904
|
+
*
|
|
905
|
+
* @example
|
|
906
|
+
* ```typescript
|
|
907
|
+
* const jobs = IgniterJobs.create<AppContext>()
|
|
908
|
+
* .withAdapter(adapter)
|
|
909
|
+
* .withContext(() => context)
|
|
910
|
+
* .addQueue(emailQueue)
|
|
911
|
+
* .build()
|
|
912
|
+
*
|
|
913
|
+
* // Now use with full type safety
|
|
914
|
+
* await jobs.email.sendWelcome.dispatch({ userId: '123' })
|
|
915
|
+
* ```
|
|
916
|
+
*/
|
|
917
|
+
build() {
|
|
918
|
+
if (!this.state.adapter) {
|
|
919
|
+
throw new IgniterJobsError({
|
|
920
|
+
code: "JOBS_ADAPTER_REQUIRED",
|
|
921
|
+
message: "Adapter is required. Use .withAdapter() to configure.",
|
|
922
|
+
statusCode: 400
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
if (!this.state.context) {
|
|
926
|
+
throw new IgniterJobsError({
|
|
927
|
+
code: "JOBS_CONTEXT_REQUIRED",
|
|
928
|
+
message: "Context provider is required. Use .withContext() to configure.",
|
|
929
|
+
statusCode: 400
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
if (Object.keys(this.state.queues).length === 0) {
|
|
933
|
+
throw new IgniterJobsError({
|
|
934
|
+
code: "JOBS_QUEUE_REQUIRED",
|
|
935
|
+
message: "At least one queue is required. Use .addQueue() to add queues.",
|
|
936
|
+
statusCode: 400
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
return new IgniterJobsRuntime(
|
|
940
|
+
this.state
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
var IgniterJobs = {
|
|
945
|
+
create: IgniterJobsBuilder.create
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
// src/builders/igniter-queue.builder.ts
|
|
949
|
+
function validateQueueName(name) {
|
|
950
|
+
if (!name || typeof name !== "string") {
|
|
951
|
+
throw new IgniterJobsError({
|
|
952
|
+
code: "JOBS_QUEUE_NAME_INVALID",
|
|
953
|
+
message: "Queue name must be a non-empty string",
|
|
954
|
+
statusCode: 400
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
958
|
+
throw new IgniterJobsError({
|
|
959
|
+
code: "JOBS_QUEUE_NAME_INVALID",
|
|
960
|
+
message: `Queue name "${name}" must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens`,
|
|
961
|
+
statusCode: 400,
|
|
962
|
+
details: { name }
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
function validateJobName(name) {
|
|
967
|
+
if (!name || typeof name !== "string") {
|
|
968
|
+
throw new IgniterJobsError({
|
|
969
|
+
code: "JOBS_JOB_NAME_INVALID",
|
|
970
|
+
message: "Job name must be a non-empty string",
|
|
971
|
+
statusCode: 400
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(name)) {
|
|
975
|
+
throw new IgniterJobsError({
|
|
976
|
+
code: "JOBS_JOB_NAME_INVALID",
|
|
977
|
+
message: `Job name "${name}" must start with a letter and contain only letters and numbers (camelCase recommended)`,
|
|
978
|
+
statusCode: 400,
|
|
979
|
+
details: { name }
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
var IgniterQueueBuilder = class _IgniterQueueBuilder {
|
|
984
|
+
constructor(state) {
|
|
985
|
+
this.state = state;
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Create a new queue builder.
|
|
989
|
+
*
|
|
990
|
+
* @param name - Queue name (kebab-case, e.g., 'email', 'payment-processing')
|
|
991
|
+
* @returns A new IgniterQueueBuilder instance
|
|
992
|
+
*
|
|
993
|
+
* @example
|
|
994
|
+
* ```typescript
|
|
995
|
+
* const emailQueue = IgniterQueue.create<AppContext>('email')
|
|
996
|
+
* ```
|
|
997
|
+
*/
|
|
998
|
+
static create(name) {
|
|
999
|
+
validateQueueName(name);
|
|
1000
|
+
return new _IgniterQueueBuilder({
|
|
1001
|
+
name,
|
|
1002
|
+
jobs: {},
|
|
1003
|
+
crons: {}
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Add a job definition to the queue.
|
|
1008
|
+
*
|
|
1009
|
+
* @param name - Job name (camelCase, e.g., 'sendWelcome', 'processPayment')
|
|
1010
|
+
* @param definition - Job definition with input schema, handler, retry config, etc.
|
|
1011
|
+
* @returns The builder with the new job added
|
|
1012
|
+
*
|
|
1013
|
+
* @example
|
|
1014
|
+
* ```typescript
|
|
1015
|
+
* .addJob('sendWelcome', {
|
|
1016
|
+
* input: z.object({
|
|
1017
|
+
* userId: z.string(),
|
|
1018
|
+
* email: z.string().email(),
|
|
1019
|
+
* }),
|
|
1020
|
+
* retry: { attempts: 3, backoff: { type: 'exponential', delay: 1000 } },
|
|
1021
|
+
* handler: async (ctx) => {
|
|
1022
|
+
* await ctx.context.mailer.send({
|
|
1023
|
+
* to: ctx.input.email,
|
|
1024
|
+
* template: 'welcome',
|
|
1025
|
+
* })
|
|
1026
|
+
* },
|
|
1027
|
+
* })
|
|
1028
|
+
* ```
|
|
1029
|
+
*/
|
|
1030
|
+
addJob(name, definition) {
|
|
1031
|
+
validateJobName(name);
|
|
1032
|
+
if (name in this.state.jobs) {
|
|
1033
|
+
throw new IgniterJobsError({
|
|
1034
|
+
code: "JOBS_JOB_ALREADY_EXISTS",
|
|
1035
|
+
message: `Job "${name}" already exists in queue "${this.state.name}"`,
|
|
1036
|
+
statusCode: 400,
|
|
1037
|
+
details: { queue: this.state.name, job: name }
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
if (!definition.handler || typeof definition.handler !== "function") {
|
|
1041
|
+
throw new IgniterJobsError({
|
|
1042
|
+
code: "JOBS_JOB_HANDLER_REQUIRED",
|
|
1043
|
+
message: `Job "${name}" must have a handler function`,
|
|
1044
|
+
statusCode: 400,
|
|
1045
|
+
details: { queue: this.state.name, job: name }
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
return new _IgniterQueueBuilder({
|
|
1049
|
+
...this.state,
|
|
1050
|
+
jobs: {
|
|
1051
|
+
...this.state.jobs,
|
|
1052
|
+
[name]: definition
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Add a cron job definition to the queue.
|
|
1058
|
+
*
|
|
1059
|
+
* @param name - Cron job name (camelCase)
|
|
1060
|
+
* @param definition - Cron definition with expression, handler, etc.
|
|
1061
|
+
* @returns The builder with the new cron added
|
|
1062
|
+
*
|
|
1063
|
+
* @example
|
|
1064
|
+
* ```typescript
|
|
1065
|
+
* .addCron('cleanupExpired', {
|
|
1066
|
+
* expression: '0 0 * * *', // Every day at midnight
|
|
1067
|
+
* timezone: 'America/New_York',
|
|
1068
|
+
* handler: async (ctx) => {
|
|
1069
|
+
* await ctx.context.db.cleanup.expiredSessions()
|
|
1070
|
+
* },
|
|
1071
|
+
* })
|
|
1072
|
+
* ```
|
|
1073
|
+
*/
|
|
1074
|
+
addCron(name, definition) {
|
|
1075
|
+
validateJobName(name);
|
|
1076
|
+
if (name in this.state.crons) {
|
|
1077
|
+
throw new IgniterJobsError({
|
|
1078
|
+
code: "JOBS_CRON_ALREADY_EXISTS",
|
|
1079
|
+
message: `Cron "${name}" already exists in queue "${this.state.name}"`,
|
|
1080
|
+
statusCode: 400,
|
|
1081
|
+
details: { queue: this.state.name, cron: name }
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
if (!definition.expression || typeof definition.expression !== "string") {
|
|
1085
|
+
throw new IgniterJobsError({
|
|
1086
|
+
code: "JOBS_CRON_EXPRESSION_INVALID",
|
|
1087
|
+
message: `Cron "${name}" must have a valid expression`,
|
|
1088
|
+
statusCode: 400,
|
|
1089
|
+
details: { queue: this.state.name, cron: name }
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
if (!definition.handler || typeof definition.handler !== "function") {
|
|
1093
|
+
throw new IgniterJobsError({
|
|
1094
|
+
code: "JOBS_CRON_HANDLER_REQUIRED",
|
|
1095
|
+
message: `Cron "${name}" must have a handler function`,
|
|
1096
|
+
statusCode: 400,
|
|
1097
|
+
details: { queue: this.state.name, cron: name }
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
return new _IgniterQueueBuilder({
|
|
1101
|
+
...this.state,
|
|
1102
|
+
crons: {
|
|
1103
|
+
...this.state.crons,
|
|
1104
|
+
[name]: definition
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Build the queue configuration.
|
|
1110
|
+
*
|
|
1111
|
+
* @returns The queue configuration ready to be registered with IgniterJobs
|
|
1112
|
+
*
|
|
1113
|
+
* @example
|
|
1114
|
+
* ```typescript
|
|
1115
|
+
* const emailQueue = IgniterQueue.create<AppContext>('email')
|
|
1116
|
+
* .addJob('sendWelcome', { ... })
|
|
1117
|
+
* .build()
|
|
1118
|
+
* ```
|
|
1119
|
+
*/
|
|
1120
|
+
build() {
|
|
1121
|
+
return { ...this.state };
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
var IgniterQueue = {
|
|
1125
|
+
create: IgniterQueueBuilder.create
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
exports.IGNITER_JOBS_ERROR_CODES = IGNITER_JOBS_ERROR_CODES;
|
|
1129
|
+
exports.IgniterJobs = IgniterJobs;
|
|
1130
|
+
exports.IgniterJobsBuilder = IgniterJobsBuilder;
|
|
1131
|
+
exports.IgniterJobsError = IgniterJobsError;
|
|
1132
|
+
exports.IgniterJobsRuntime = IgniterJobsRuntime;
|
|
1133
|
+
exports.IgniterQueue = IgniterQueue;
|
|
1134
|
+
exports.IgniterQueueBuilder = IgniterQueueBuilder;
|
|
1135
|
+
exports.IgniterWorkerBuilder = IgniterWorkerBuilder;
|
|
1136
|
+
//# sourceMappingURL=index.js.map
|
|
1137
|
+
//# sourceMappingURL=index.js.map
|