@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/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