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