@igniter-js/jobs 0.1.1 → 0.1.12

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.
Files changed (42) hide show
  1. package/AGENTS.md +1118 -96
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +2146 -93
  4. package/dist/{adapter-PiDCQWQd.d.mts → adapter-CXZxomI9.d.mts} +2 -2
  5. package/dist/{adapter-PiDCQWQd.d.ts → adapter-CXZxomI9.d.ts} +2 -2
  6. package/dist/adapters/bullmq.adapter.d.mts +2 -2
  7. package/dist/adapters/bullmq.adapter.d.ts +2 -2
  8. package/dist/adapters/bullmq.adapter.js +2 -2
  9. package/dist/adapters/bullmq.adapter.js.map +1 -1
  10. package/dist/adapters/bullmq.adapter.mjs +1 -1
  11. package/dist/adapters/bullmq.adapter.mjs.map +1 -1
  12. package/dist/adapters/index.d.mts +140 -2
  13. package/dist/adapters/index.d.ts +140 -2
  14. package/dist/adapters/index.js +864 -31
  15. package/dist/adapters/index.js.map +1 -1
  16. package/dist/adapters/index.mjs +863 -31
  17. package/dist/adapters/index.mjs.map +1 -1
  18. package/dist/adapters/memory.adapter.d.mts +2 -2
  19. package/dist/adapters/memory.adapter.d.ts +2 -2
  20. package/dist/adapters/memory.adapter.js +122 -30
  21. package/dist/adapters/memory.adapter.js.map +1 -1
  22. package/dist/adapters/memory.adapter.mjs +121 -29
  23. package/dist/adapters/memory.adapter.mjs.map +1 -1
  24. package/dist/index.d.mts +452 -342
  25. package/dist/index.d.ts +452 -342
  26. package/dist/index.js +1923 -1002
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.mjs +1921 -1001
  29. package/dist/index.mjs.map +1 -1
  30. package/dist/shim.d.mts +36 -0
  31. package/dist/shim.d.ts +36 -0
  32. package/dist/shim.js +75 -0
  33. package/dist/shim.js.map +1 -0
  34. package/dist/shim.mjs +67 -0
  35. package/dist/shim.mjs.map +1 -0
  36. package/dist/telemetry/index.d.mts +281 -0
  37. package/dist/telemetry/index.d.ts +281 -0
  38. package/dist/telemetry/index.js +97 -0
  39. package/dist/telemetry/index.js.map +1 -0
  40. package/dist/telemetry/index.mjs +95 -0
  41. package/dist/telemetry/index.mjs.map +1 -0
  42. package/package.json +44 -11
package/dist/index.mjs CHANGED
@@ -1,7 +1,12 @@
1
- import { IgniterError } from '@igniter-js/core';
1
+ import { IgniterError } from '@igniter-js/common';
2
2
  import { createBullMQAdapter } from '@igniter-js/adapter-bullmq';
3
3
 
4
- // src/errors/igniter-jobs.error.ts
4
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
6
+ }) : x)(function(x) {
7
+ if (typeof require !== "undefined") return require.apply(this, arguments);
8
+ throw Error('Dynamic require of "' + x + '" is not supported');
9
+ });
5
10
  var IGNITER_JOBS_ERROR_CODES = {
6
11
  JOBS_ADAPTER_REQUIRED: "JOBS_ADAPTER_REQUIRED",
7
12
  JOBS_SERVICE_REQUIRED: "JOBS_SERVICE_REQUIRED",
@@ -34,7 +39,7 @@ var IgniterJobsError = class extends IgniterError {
34
39
  }
35
40
  };
36
41
 
37
- // src/builders/igniter-worker.builder.ts
42
+ // src/builders/worker.builder.ts
38
43
  var IgniterWorkerBuilder = class _IgniterWorkerBuilder {
39
44
  constructor(params) {
40
45
  this.adapter = params.adapter;
@@ -220,27 +225,6 @@ var IgniterJobsScopeUtils = class {
220
225
  */
221
226
  IgniterJobsScopeUtils.SCOPE_METADATA_KEY = "__igniter_jobs_scope";
222
227
 
223
- // src/utils/telemetry.ts
224
- var IgniterJobsTelemetryUtils = class {
225
- /**
226
- * Emits a telemetry event if telemetry is configured.
227
- * This is a fire-and-forget operation - telemetry errors are silently ignored
228
- * to avoid affecting job processing.
229
- *
230
- * @param telemetry - The telemetry instance (optional).
231
- * @param eventName - The name of the event to emit.
232
- * @param attributes - Attributes to attach to the event.
233
- * @param level - The log level for the event (default: 'info').
234
- */
235
- static emitTelemetry(telemetry, eventName, attributes, level = "info") {
236
- if (!telemetry) return;
237
- try {
238
- telemetry.emit(eventName, { attributes, level });
239
- } catch {
240
- }
241
- }
242
- };
243
-
244
228
  // src/utils/validation.ts
245
229
  var IgniterJobsValidationUtils = class {
246
230
  /**
@@ -315,428 +299,637 @@ var IgniterJobsValidationUtils = class {
315
299
  }
316
300
  };
317
301
 
318
- // src/core/igniter-jobs.ts
319
- var IgniterJobs = class {
302
+ // src/core/manager.ts
303
+ var registeredAdapters = /* @__PURE__ */ new WeakSet();
304
+ var IgniterJobsManager = class _IgniterJobsManager {
305
+ /**
306
+ * @internal
307
+ * Constructor is internal. Use `IgniterJobs.create()` instead.
308
+ */
309
+ constructor(config, scopeEntry) {
310
+ this.config = config;
311
+ this.adapter = config.adapter;
312
+ this.telemetry = config.telemetry;
313
+ this.scopeEntry = scopeEntry;
314
+ }
315
+ // ---------------------------------------------------------------------------
316
+ // Public Methods
317
+ // ---------------------------------------------------------------------------
320
318
  /**
321
- * Starts the fluent builder API for jobs.
319
+ * Creates a scoped jobs instance.
320
+ *
321
+ * Scopes provide multi-tenant isolation by adding scope metadata to all jobs.
322
+ * Jobs dispatched from a scoped instance will include scope information in their metadata.
323
+ *
324
+ * @param type - The scope type (e.g., 'organization', 'workspace')
325
+ * @param id - The scope identifier (e.g., 'org_123')
326
+ * @param tags - Optional additional tags for the scope
327
+ * @returns A new scoped IgniterJobsManager instance
322
328
  *
323
329
  * @example
324
330
  * ```typescript
325
- * const jobs = IgniterJobs.create<AppContext>()
326
- * .withAdapter(IgniterJobsMemoryAdapter.create())
327
- * .withService('my-api')
328
- * .withEnvironment('test')
329
- * .withContext(async () => ({ db }))
330
- * .addQueue(emailQueue)
331
- * .build()
331
+ * const orgJobs = jobs.scope('organization', 'org_123')
332
+ * await orgJobs.email.sendWelcome.dispatch({ input: { to: 'user@example.com' } })
332
333
  * ```
333
334
  */
334
- static create() {
335
- return IgniterJobsBuilder.create();
335
+ scope(type, id, tags) {
336
+ const entry = { type, id: String(id), tags };
337
+ return new _IgniterJobsManager(this.config, entry);
336
338
  }
337
339
  /**
338
- * Creates a runtime instance from a validated configuration.
340
+ * Subscribes to all job events from this jobs instance.
341
+ *
342
+ * @param handler - The event handler function
343
+ * @returns A function to unsubscribe
344
+ *
345
+ * @example
346
+ * ```typescript
347
+ * const unsubscribe = await jobs.subscribe((event) => {
348
+ * console.log('Job event:', event.type, event.data)
349
+ * })
350
+ * // Later: await unsubscribe()
351
+ * ```
339
352
  */
340
- static fromConfig(config) {
341
- const internal = createInternalState(config);
342
- return createRuntime(internal, void 0);
353
+ async subscribe(handler) {
354
+ const channel = this.buildEventsChannel();
355
+ return this.adapter.subscribeEvent(channel, async (payload) => {
356
+ await handler(payload);
357
+ });
343
358
  }
344
- };
345
- function createInternalState(config) {
346
- return {
347
- config,
348
- adapter: config.adapter,
349
- registered: false
350
- };
351
- }
352
- function createRuntime(internal, boundScope) {
353
- if (!internal.registered) {
354
- registerAll(internal.config, internal.adapter);
355
- internal.registered = true;
356
- }
357
- const baseChannel = IgniterJobsPrefix.buildEventsChannel({
358
- service: internal.config.service,
359
- environment: internal.config.environment
360
- });
361
- const scopeChannel = boundScope ? IgniterJobsPrefix.buildEventsChannel({
362
- service: internal.config.service,
363
- environment: internal.config.environment,
364
- scope: { type: boundScope.type, id: boundScope.id }
365
- }) : void 0;
366
- const runtime = {
367
- config: internal.config,
368
- async subscribe(handler) {
369
- const channel = boundScope ? scopeChannel : baseChannel;
370
- return internal.adapter.subscribeEvent(channel, async (payload) => {
371
- await handler(payload);
372
- });
373
- },
374
- async search(target, filter) {
375
- switch (target) {
376
- case "jobs":
377
- return internal.adapter.searchJobs(filter);
378
- case "queues":
379
- return internal.adapter.searchQueues(filter);
380
- case "workers":
381
- return internal.adapter.searchWorkers(filter);
382
- default:
383
- return [];
384
- }
385
- },
386
- async shutdown() {
387
- await internal.adapter.shutdown();
388
- },
389
- worker: {
359
+ /**
360
+ * Searches for jobs, queues, or workers based on filters.
361
+ *
362
+ * @param target - The target to search ('jobs', 'queues', or 'workers')
363
+ * @param filter - The filter criteria
364
+ * @returns The search results
365
+ *
366
+ * @example
367
+ * ```typescript
368
+ * const failedJobs = await jobs.search('jobs', { status: 'failed', queue: 'email' })
369
+ * const queues = await jobs.search('queues', {})
370
+ * ```
371
+ */
372
+ async search(target, filter) {
373
+ switch (target) {
374
+ case "jobs":
375
+ return this.adapter.searchJobs(filter);
376
+ case "queues":
377
+ return this.adapter.searchQueues(filter);
378
+ case "workers":
379
+ return this.adapter.searchWorkers(filter);
380
+ default:
381
+ return [];
382
+ }
383
+ }
384
+ /**
385
+ * Gracefully shuts down the jobs instance.
386
+ *
387
+ * This will close all connections and stop all workers.
388
+ *
389
+ * @example
390
+ * ```typescript
391
+ * await jobs.shutdown()
392
+ * ```
393
+ */
394
+ async shutdown() {
395
+ await this.adapter.shutdown();
396
+ }
397
+ /**
398
+ * Creates a new worker builder for processing jobs.
399
+ *
400
+ * @returns A new IgniterWorkerBuilder instance
401
+ *
402
+ * @example
403
+ * ```typescript
404
+ * const worker = await jobs.worker
405
+ * .create()
406
+ * .addQueue('email')
407
+ * .withConcurrency(5)
408
+ * .start()
409
+ * ```
410
+ */
411
+ get worker() {
412
+ return {
390
413
  create: () => new IgniterWorkerBuilder({
391
- adapter: internal.adapter,
392
- allowedQueues: Object.keys(internal.config.queues)
414
+ adapter: this.adapter,
415
+ allowedQueues: Object.keys(this.config.queues)
393
416
  })
394
- }
395
- };
396
- if (internal.config.scopeDefinition) {
397
- runtime.scope = (type, id, tags) => {
398
- const scope = { type, id, tags };
399
- return createRuntime(internal, scope);
400
417
  };
401
418
  }
402
- for (const [queueName, queueConfig] of Object.entries(
403
- internal.config.queues
404
- )) {
405
- runtime[queueName] = createQueueAccessor({
406
- internal,
407
- boundScope,
408
- queueName,
409
- queueConfig
410
- });
419
+ /**
420
+ * Converts the manager to a runtime proxy with typed queue accessors.
421
+ *
422
+ * @internal
423
+ * @returns The runtime proxy
424
+ */
425
+ toRuntime() {
426
+ this.ensureRegistered();
427
+ const self = this;
428
+ const queueNames = Object.keys(this.config.queues);
429
+ const handler = {
430
+ get(target, prop) {
431
+ if (prop === "config") return target.config;
432
+ if (prop === "subscribe") return target.subscribe.bind(target);
433
+ if (prop === "search") return target.search.bind(target);
434
+ if (prop === "shutdown") return target.shutdown.bind(target);
435
+ if (prop === "worker") return target.worker;
436
+ if (prop === "scope" && target.config.scopeDefinition) {
437
+ return (type, id, tags) => target.scope(type, id, tags).toRuntime();
438
+ }
439
+ if (queueNames.includes(prop)) {
440
+ return self.createQueueAccessor(prop);
441
+ }
442
+ return void 0;
443
+ }
444
+ };
445
+ return new Proxy(this, handler);
411
446
  }
412
- return runtime;
413
- }
414
- function registerAll(config, adapter) {
415
- for (const [queueName, queue] of Object.entries(
416
- config.queues
417
- )) {
418
- for (const [jobName, def] of Object.entries(
419
- queue.jobs
447
+ // ---------------------------------------------------------------------------
448
+ // Private Methods - Registration
449
+ // ---------------------------------------------------------------------------
450
+ /**
451
+ * Ensures all jobs and crons are registered with the adapter.
452
+ * Uses a shared WeakSet to prevent duplicate registration across scoped instances.
453
+ * @internal
454
+ */
455
+ ensureRegistered() {
456
+ if (registeredAdapters.has(this.adapter)) return;
457
+ for (const [queueName, queue] of Object.entries(
458
+ this.config.queues
420
459
  )) {
421
- adapter.registerJob(
422
- queueName,
423
- jobName,
424
- wrapJobDefinition({
425
- config,
426
- adapter,
460
+ for (const [jobName, def] of Object.entries(
461
+ queue.jobs
462
+ )) {
463
+ this.adapter.registerJob(
427
464
  queueName,
428
465
  jobName,
429
- definition: def
430
- })
431
- );
432
- }
433
- for (const [cronName, cron] of Object.entries(
434
- queue.crons
435
- )) {
436
- adapter.registerCron(
437
- queueName,
438
- cronName,
439
- wrapCronDefinition({
440
- config,
441
- adapter,
466
+ this.wrapJobDefinition(queueName, jobName, def)
467
+ );
468
+ }
469
+ for (const [cronName, cron] of Object.entries(
470
+ queue.crons
471
+ )) {
472
+ this.adapter.registerCron(
442
473
  queueName,
443
474
  cronName,
444
- definition: cron
445
- })
446
- );
475
+ this.wrapCronDefinition(queueName, cronName, cron)
476
+ );
477
+ }
447
478
  }
479
+ registeredAdapters.add(this.adapter);
448
480
  }
449
- }
450
- function wrapCronDefinition(params) {
451
- const { config, adapter, queueName, cronName, definition } = params;
452
- const buildExecutionContext = async (ctx) => {
453
- const realContext = await config.contextFactory();
454
- const scope = ctx.scope ?? IgniterJobsScopeUtils.extractScopeFromMetadata(
455
- ctx.job?.metadata
456
- );
481
+ // ---------------------------------------------------------------------------
482
+ // Private Methods - Accessors
483
+ // ---------------------------------------------------------------------------
484
+ /**
485
+ * Creates a queue accessor with job accessors and management methods.
486
+ * @internal
487
+ */
488
+ createQueueAccessor(queueName) {
489
+ const queueConfig = this.config.queues[queueName];
490
+ const self = this;
491
+ const queueAccessor = {
492
+ async list(filter) {
493
+ return self.adapter.queues.getJobs(queueName, filter);
494
+ },
495
+ get() {
496
+ return {
497
+ retrieve: () => self.adapter.getQueueInfo(queueName),
498
+ pause: () => self.adapter.pauseQueue(queueName),
499
+ resume: () => self.adapter.resumeQueue(queueName),
500
+ drain: () => self.adapter.drainQueue(queueName),
501
+ clean: (options) => self.adapter.cleanQueue(queueName, options),
502
+ obliterate: (options) => self.adapter.obliterateQueue(queueName, options),
503
+ retryAll: () => self.adapter.retryAllInQueue(queueName)
504
+ };
505
+ },
506
+ async subscribe(handler) {
507
+ const channel = self.buildEventsChannel();
508
+ return self.adapter.subscribeEvent(channel, async (event) => {
509
+ const typed = event;
510
+ if (typeof typed?.type === "string" && typed.type.startsWith(`${queueName}:`)) {
511
+ await handler(typed);
512
+ }
513
+ });
514
+ },
515
+ jobs: {}
516
+ };
517
+ for (const jobName of Object.keys(queueConfig.jobs)) {
518
+ const jobAccessor = this.createJobAccessor(queueName, jobName);
519
+ queueAccessor.jobs[jobName] = jobAccessor;
520
+ queueAccessor[jobName] = jobAccessor;
521
+ }
522
+ return queueAccessor;
523
+ }
524
+ /**
525
+ * Creates a job accessor with dispatch, schedule, and management methods.
526
+ * @internal
527
+ */
528
+ createJobAccessor(queueName, jobName) {
529
+ const self = this;
457
530
  return {
458
- ...ctx,
459
- context: realContext,
460
- job: { ...ctx.job, name: cronName, queue: queueName },
461
- scope
531
+ async dispatch(params) {
532
+ return self.dispatchJob(queueName, jobName, params);
533
+ },
534
+ async schedule(params) {
535
+ return self.scheduleJob(queueName, jobName, params);
536
+ },
537
+ get(id) {
538
+ return {
539
+ retrieve: () => self.adapter.getJob(id, queueName),
540
+ retry: () => self.adapter.retryJob(id, queueName),
541
+ remove: () => self.adapter.removeJob(id, queueName),
542
+ promote: () => self.adapter.promoteJob(id, queueName),
543
+ move: (state, reason) => {
544
+ if (state !== "failed") return Promise.resolve();
545
+ return self.adapter.moveJobToFailed(id, reason, queueName);
546
+ },
547
+ state: () => self.adapter.getJobState(id, queueName),
548
+ progress: () => self.adapter.getJobProgress(id, queueName),
549
+ logs: () => self.adapter.getJobLogs(id, queueName)
550
+ };
551
+ },
552
+ many(ids) {
553
+ return {
554
+ retry: () => self.adapter.retryManyJobs(ids, queueName),
555
+ remove: () => self.adapter.removeManyJobs(ids, queueName)
556
+ };
557
+ },
558
+ pause: () => self.adapter.pauseJobType(queueName, jobName),
559
+ resume: () => self.adapter.resumeJobType(queueName, jobName),
560
+ async subscribe(handler) {
561
+ const channel = self.buildEventsChannel();
562
+ return self.adapter.subscribeEvent(channel, async (event) => {
563
+ const typed = event;
564
+ if (typeof typed?.type === "string" && typed.type.startsWith(`${queueName}:${jobName}:`)) {
565
+ await handler(typed);
566
+ }
567
+ });
568
+ }
462
569
  };
463
- };
464
- const publishLifecycle = async (event, ctx, data) => {
465
- const scope = ctx.scope ?? IgniterJobsScopeUtils.extractScopeFromMetadata(
466
- ctx.job?.metadata
570
+ }
571
+ // ---------------------------------------------------------------------------
572
+ // Private Methods - Job Operations
573
+ // ---------------------------------------------------------------------------
574
+ /**
575
+ * Dispatches a job for immediate or delayed processing.
576
+ * @internal
577
+ */
578
+ async dispatchJob(queueName, jobName, params) {
579
+ const definition = this.getJobDefinition(queueName, jobName);
580
+ if (definition?.input) {
581
+ await IgniterJobsValidationUtils.validateInput(
582
+ definition.input,
583
+ params.input
584
+ );
585
+ }
586
+ const scope = this.resolveScope(params.scope);
587
+ const metadata = IgniterJobsScopeUtils.mergeMetadataWithScope(
588
+ params.metadata,
589
+ scope
467
590
  );
468
- await IgniterJobsEventsUtils.publishJobsEvent({
469
- adapter,
470
- service: config.service,
471
- environment: config.environment,
591
+ const jobId = await this.adapter.dispatch({
592
+ queue: queueName,
593
+ jobName,
594
+ ...params,
472
595
  scope,
473
- event: {
474
- type: IgniterJobsEventsUtils.buildJobEventType(
475
- queueName,
476
- cronName,
477
- event
478
- ),
479
- data,
480
- timestamp: /* @__PURE__ */ new Date(),
481
- scope
482
- }
596
+ metadata
483
597
  });
484
- };
485
- return {
486
- ...definition,
487
- handler: async (ctx) => {
488
- const enhanced = await buildExecutionContext(ctx);
489
- await publishLifecycle("started", enhanced, {
490
- jobId: enhanced.job?.id,
491
- jobName: cronName,
492
- queue: queueName,
493
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
494
- });
495
- try {
496
- const result = await definition.handler(enhanced);
497
- await publishLifecycle("completed", enhanced, {
498
- jobId: enhanced.job?.id,
499
- jobName: cronName,
500
- queue: queueName,
501
- result,
502
- completedAt: (/* @__PURE__ */ new Date()).toISOString()
503
- });
504
- return result;
505
- } catch (error) {
506
- await publishLifecycle("failed", enhanced, {
507
- jobId: enhanced.job?.id,
508
- jobName: cronName,
509
- queue: queueName,
510
- error: { message: error?.message ?? String(error) },
511
- failedAt: (/* @__PURE__ */ new Date()).toISOString()
512
- });
513
- throw error;
514
- }
598
+ await this.publishJobEvent(
599
+ queueName,
600
+ jobName,
601
+ "enqueued",
602
+ { jobId, queue: queueName, jobName },
603
+ scope
604
+ );
605
+ this.emitTelemetry("igniter.jobs.job.enqueued", {
606
+ "ctx.job.id": jobId,
607
+ "ctx.job.name": jobName,
608
+ "ctx.job.queue": queueName,
609
+ "ctx.job.priority": params.priority ?? null,
610
+ "ctx.job.delay": params.delay ?? null
611
+ });
612
+ return jobId;
613
+ }
614
+ /**
615
+ * Schedules a job for future processing.
616
+ * @internal
617
+ */
618
+ async scheduleJob(queueName, jobName, params) {
619
+ const definition = this.getJobDefinition(queueName, jobName);
620
+ if (definition?.input) {
621
+ await IgniterJobsValidationUtils.validateInput(
622
+ definition.input,
623
+ params.input
624
+ );
515
625
  }
516
- };
517
- }
518
- function wrapJobDefinition(params) {
519
- const { config, queueName, jobName, definition } = params;
520
- const buildExecutionContext = async (ctx) => {
521
- const realContext = await config.contextFactory();
522
- const scope = ctx.scope ?? IgniterJobsScopeUtils.extractScopeFromMetadata(ctx.job.metadata);
523
- return {
524
- ...ctx,
525
- context: realContext,
526
- job: { ...ctx.job, name: jobName, queue: queueName },
626
+ const scope = this.resolveScope(params.scope);
627
+ const metadata = IgniterJobsScopeUtils.mergeMetadataWithScope(
628
+ params.metadata,
527
629
  scope
528
- };
529
- };
530
- const publishLifecycle = async (event, ctx, data) => {
531
- const scope = ctx.scope ?? IgniterJobsScopeUtils.extractScopeFromMetadata(
532
- ctx.job?.metadata
533
630
  );
534
- await IgniterJobsEventsUtils.publishJobsEvent({
535
- adapter: params.adapter,
536
- service: config.service,
537
- environment: config.environment,
631
+ const jobId = await this.adapter.schedule({
632
+ queue: queueName,
633
+ jobName,
634
+ ...params,
538
635
  scope,
539
- event: {
540
- type: IgniterJobsEventsUtils.buildJobEventType(
636
+ metadata
637
+ });
638
+ await this.publishJobEvent(
639
+ queueName,
640
+ jobName,
641
+ "scheduled",
642
+ { jobId, queue: queueName, jobName },
643
+ scope
644
+ );
645
+ this.emitTelemetry("igniter.jobs.job.scheduled", {
646
+ "ctx.job.id": jobId,
647
+ "ctx.job.name": jobName,
648
+ "ctx.job.queue": queueName,
649
+ "ctx.job.scheduledAt": params.runAt?.toISOString?.() ?? null,
650
+ "ctx.job.cron": params.cron ?? null
651
+ });
652
+ return jobId;
653
+ }
654
+ // ---------------------------------------------------------------------------
655
+ // Private Methods - Handler Wrapping
656
+ // ---------------------------------------------------------------------------
657
+ /**
658
+ * Wraps a job definition with context building, validation, and telemetry.
659
+ * @internal
660
+ */
661
+ wrapJobDefinition(queueName, jobName, definition) {
662
+ const self = this;
663
+ return {
664
+ ...definition,
665
+ handler: async (ctx) => {
666
+ const enhanced = await self.buildExecutionContext(
667
+ ctx,
668
+ queueName,
669
+ jobName
670
+ );
671
+ if (definition.input) {
672
+ const validated = await IgniterJobsValidationUtils.validateInput(
673
+ definition.input,
674
+ enhanced.input
675
+ );
676
+ enhanced.input = validated;
677
+ }
678
+ return definition.handler(enhanced);
679
+ },
680
+ onStart: async (ctx) => {
681
+ const enhanced = await self.buildExecutionContext(
682
+ ctx,
683
+ queueName,
684
+ jobName
685
+ );
686
+ await self.publishJobEvent(
541
687
  queueName,
542
688
  jobName,
543
- event
544
- ),
545
- data,
546
- timestamp: /* @__PURE__ */ new Date(),
547
- scope
548
- }
549
- });
550
- };
551
- return {
552
- ...definition,
553
- handler: async (ctx) => {
554
- const enhanced = await buildExecutionContext(ctx);
555
- if (definition.input) {
556
- const validated = await IgniterJobsValidationUtils.validateInput(
557
- definition.input,
558
- enhanced.input
689
+ "started",
690
+ {
691
+ jobId: enhanced.job.id,
692
+ jobName,
693
+ queue: queueName,
694
+ attemptsMade: enhanced.job.attemptsMade,
695
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
696
+ },
697
+ enhanced.scope
559
698
  );
560
- enhanced.input = validated;
561
- }
562
- return definition.handler(enhanced);
563
- },
564
- onStart: async (ctx) => {
565
- const enhanced = await buildExecutionContext(ctx);
566
- await publishLifecycle("started", enhanced, {
567
- jobId: enhanced.job.id,
568
- jobName,
569
- queue: queueName,
570
- attemptsMade: enhanced.job.attemptsMade,
571
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
572
- });
573
- IgniterJobsTelemetryUtils.emitTelemetry(
574
- config.telemetry,
575
- "igniter.jobs.job.started",
576
- {
699
+ self.emitTelemetry("igniter.jobs.job.started", {
577
700
  "ctx.job.id": enhanced.job.id,
578
701
  "ctx.job.name": jobName,
579
702
  "ctx.job.queue": queueName,
580
703
  "ctx.job.attempt": enhanced.job.attemptsMade,
581
704
  "ctx.job.maxAttempts": definition.attempts ?? 3
582
- }
583
- );
584
- await definition.onStart?.(enhanced);
585
- },
586
- onSuccess: async (ctx) => {
587
- const enhanced = await buildExecutionContext(ctx);
588
- const duration = ctx.duration ?? ctx.executionTime ?? 0;
589
- await publishLifecycle(
590
- "completed",
591
- { ...enhanced},
592
- {
593
- jobId: enhanced.job.id,
705
+ });
706
+ await definition.onStart?.(enhanced);
707
+ },
708
+ onSuccess: async (ctx) => {
709
+ const enhanced = await self.buildExecutionContext(
710
+ ctx,
711
+ queueName,
712
+ jobName
713
+ );
714
+ const duration = ctx.duration ?? ctx.executionTime ?? 0;
715
+ await self.publishJobEvent(
716
+ queueName,
594
717
  jobName,
595
- queue: queueName,
596
- result: ctx.result,
597
- duration,
598
- completedAt: (/* @__PURE__ */ new Date()).toISOString()
599
- }
600
- );
601
- IgniterJobsTelemetryUtils.emitTelemetry(
602
- config.telemetry,
603
- "igniter.jobs.job.completed",
604
- {
718
+ "completed",
719
+ {
720
+ jobId: enhanced.job.id,
721
+ jobName,
722
+ queue: queueName,
723
+ result: ctx.result,
724
+ duration,
725
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
726
+ },
727
+ enhanced.scope
728
+ );
729
+ self.emitTelemetry("igniter.jobs.job.completed", {
605
730
  "ctx.job.id": enhanced.job.id,
606
731
  "ctx.job.name": jobName,
607
732
  "ctx.job.queue": queueName,
608
733
  "ctx.job.duration": typeof duration === "number" ? duration : 0
609
- }
610
- );
611
- await definition.onSuccess?.(enhanced);
612
- },
613
- onFailure: async (ctx) => {
614
- const enhanced = await buildExecutionContext(ctx);
615
- const duration = ctx.duration ?? ctx.executionTime ?? 0;
616
- const isFinalAttempt = Boolean(ctx.isFinalAttempt);
617
- const errorMessage = ctx.error?.message ?? String(ctx.error);
618
- const errorCode = ctx.error?.code;
619
- const maxAttempts = definition.attempts ?? 3;
620
- await publishLifecycle(
621
- "failed",
622
- { ...enhanced},
623
- {
624
- jobId: enhanced.job.id,
734
+ });
735
+ await definition.onSuccess?.(enhanced);
736
+ },
737
+ onFailure: async (ctx) => {
738
+ const enhanced = await self.buildExecutionContext(
739
+ ctx,
740
+ queueName,
741
+ jobName
742
+ );
743
+ const duration = ctx.duration ?? ctx.executionTime ?? 0;
744
+ const isFinalAttempt = Boolean(ctx.isFinalAttempt);
745
+ const errorMessage = ctx.error?.message ?? String(ctx.error);
746
+ const errorCode = ctx.error?.code;
747
+ const maxAttempts = definition.attempts ?? 3;
748
+ await self.publishJobEvent(
749
+ queueName,
625
750
  jobName,
626
- queue: queueName,
627
- error: { message: errorMessage },
628
- attemptsMade: enhanced.job.attemptsMade,
629
- isFinalAttempt,
630
- duration,
631
- failedAt: (/* @__PURE__ */ new Date()).toISOString()
632
- }
633
- );
634
- IgniterJobsTelemetryUtils.emitTelemetry(
635
- config.telemetry,
636
- "igniter.jobs.job.failed",
637
- {
638
- "ctx.job.id": enhanced.job.id,
639
- "ctx.job.name": jobName,
640
- "ctx.job.queue": queueName,
641
- "ctx.job.error.message": errorMessage,
642
- "ctx.job.error.code": errorCode ?? null,
643
- "ctx.job.attempt": enhanced.job.attemptsMade,
644
- "ctx.job.maxAttempts": maxAttempts,
645
- "ctx.job.isFinalAttempt": isFinalAttempt
646
- },
647
- "error"
648
- );
649
- await definition.onFailure?.(enhanced);
650
- },
651
- onProgress: definition.onProgress ? async (ctx) => {
652
- const enhanced = await buildExecutionContext(ctx);
653
- const progress = ctx.progress ?? 0;
654
- const message = ctx.message;
655
- await publishLifecycle("progress", enhanced, {
656
- jobId: enhanced.job.id,
657
- jobName,
658
- queue: queueName,
659
- progress,
660
- message,
661
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
662
- });
663
- IgniterJobsTelemetryUtils.emitTelemetry(
664
- config.telemetry,
665
- "igniter.jobs.job.progress",
666
- {
751
+ "failed",
752
+ {
753
+ jobId: enhanced.job.id,
754
+ jobName,
755
+ queue: queueName,
756
+ error: { message: errorMessage },
757
+ attemptsMade: enhanced.job.attemptsMade,
758
+ isFinalAttempt,
759
+ duration,
760
+ failedAt: (/* @__PURE__ */ new Date()).toISOString()
761
+ },
762
+ enhanced.scope
763
+ );
764
+ self.emitTelemetry(
765
+ "igniter.jobs.job.failed",
766
+ {
767
+ "ctx.job.id": enhanced.job.id,
768
+ "ctx.job.name": jobName,
769
+ "ctx.job.queue": queueName,
770
+ "ctx.job.error.message": errorMessage,
771
+ "ctx.job.error.code": errorCode ?? null,
772
+ "ctx.job.attempt": enhanced.job.attemptsMade,
773
+ "ctx.job.maxAttempts": maxAttempts,
774
+ "ctx.job.isFinalAttempt": isFinalAttempt
775
+ },
776
+ "error"
777
+ );
778
+ await definition.onFailure?.(enhanced);
779
+ },
780
+ onProgress: definition.onProgress ? async (ctx) => {
781
+ const enhanced = await self.buildExecutionContext(
782
+ ctx,
783
+ queueName,
784
+ jobName
785
+ );
786
+ const progress = ctx.progress ?? 0;
787
+ const message = ctx.message;
788
+ await self.publishJobEvent(
789
+ queueName,
790
+ jobName,
791
+ "progress",
792
+ {
793
+ jobId: enhanced.job.id,
794
+ jobName,
795
+ queue: queueName,
796
+ progress,
797
+ message,
798
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
799
+ },
800
+ enhanced.scope
801
+ );
802
+ self.emitTelemetry("igniter.jobs.job.progress", {
667
803
  "ctx.job.id": enhanced.job.id,
668
804
  "ctx.job.name": jobName,
669
805
  "ctx.job.queue": queueName,
670
806
  "ctx.job.progress": typeof progress === "number" ? progress : 0,
671
807
  "ctx.job.progress.message": message ?? null
808
+ });
809
+ await definition.onProgress?.(enhanced);
810
+ } : void 0
811
+ };
812
+ }
813
+ /**
814
+ * Wraps a cron definition with context building and lifecycle events.
815
+ * @internal
816
+ */
817
+ wrapCronDefinition(queueName, cronName, definition) {
818
+ const self = this;
819
+ return {
820
+ ...definition,
821
+ handler: async (ctx) => {
822
+ const enhanced = await self.buildCronExecutionContext(
823
+ ctx,
824
+ queueName,
825
+ cronName
826
+ );
827
+ await self.publishJobEvent(
828
+ queueName,
829
+ cronName,
830
+ "started",
831
+ {
832
+ jobId: enhanced.job?.id,
833
+ jobName: cronName,
834
+ queue: queueName,
835
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
836
+ },
837
+ enhanced.scope
838
+ );
839
+ try {
840
+ const result = await definition.handler(enhanced);
841
+ await self.publishJobEvent(
842
+ queueName,
843
+ cronName,
844
+ "completed",
845
+ {
846
+ jobId: enhanced.job?.id,
847
+ jobName: cronName,
848
+ queue: queueName,
849
+ result,
850
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
851
+ },
852
+ enhanced.scope
853
+ );
854
+ return result;
855
+ } catch (error) {
856
+ await self.publishJobEvent(
857
+ queueName,
858
+ cronName,
859
+ "failed",
860
+ {
861
+ jobId: enhanced.job?.id,
862
+ jobName: cronName,
863
+ queue: queueName,
864
+ error: { message: error?.message ?? String(error) },
865
+ failedAt: (/* @__PURE__ */ new Date()).toISOString()
866
+ },
867
+ enhanced.scope
868
+ );
869
+ throw error;
672
870
  }
673
- );
674
- await definition.onProgress?.(enhanced);
675
- } : void 0
676
- };
677
- }
678
- function createQueueAccessor(params) {
679
- const { internal, boundScope, queueName, queueConfig } = params;
680
- const queueAccessor = {
681
- async list(filter) {
682
- return internal.adapter.queues.getJobs(queueName, filter);
683
- },
684
- get() {
685
- return {
686
- retrieve: () => internal.adapter.getQueueInfo(queueName),
687
- pause: () => internal.adapter.pauseQueue(queueName),
688
- resume: () => internal.adapter.resumeQueue(queueName),
689
- drain: () => internal.adapter.drainQueue(queueName),
690
- clean: (options) => internal.adapter.cleanQueue(queueName, options),
691
- obliterate: (options) => internal.adapter.obliterateQueue(queueName, options),
692
- retryAll: () => internal.adapter.retryAllInQueue(queueName)
693
- };
694
- },
695
- async subscribe(handler) {
696
- const channel = boundScope ? IgniterJobsPrefix.buildEventsChannel({
697
- service: internal.config.service,
698
- environment: internal.config.environment,
699
- scope: { type: boundScope.type, id: boundScope.id }
700
- }) : IgniterJobsPrefix.buildEventsChannel({
701
- service: internal.config.service,
702
- environment: internal.config.environment
703
- });
704
- return internal.adapter.subscribeEvent(channel, async (event) => {
705
- const typed = event;
706
- if (typeof typed?.type === "string" && typed.type.startsWith(`${queueName}:`)) {
707
- await handler(typed);
708
- }
709
- });
710
- },
711
- jobs: {}
712
- };
713
- for (const jobName of Object.keys(queueConfig.jobs)) {
714
- queueAccessor.jobs[jobName] = createJobAccessor({
715
- internal,
716
- boundScope,
717
- queueName,
718
- jobName
719
- });
720
- queueAccessor[jobName] = queueAccessor.jobs[jobName];
871
+ }
872
+ };
721
873
  }
722
- return queueAccessor;
723
- }
724
- function createJobAccessor(params) {
725
- const { internal, boundScope, queueName, jobName } = params;
726
- const resolveScope = (paramsScope) => {
727
- if (!internal.config.scopeDefinition) return void 0;
874
+ // ---------------------------------------------------------------------------
875
+ // Private Methods - Helpers
876
+ // ---------------------------------------------------------------------------
877
+ /**
878
+ * Builds the execution context for a job.
879
+ * @internal
880
+ */
881
+ async buildExecutionContext(ctx, queueName, jobName) {
882
+ const realContext = await this.config.contextFactory();
883
+ const scope = ctx.scope ?? IgniterJobsScopeUtils.extractScopeFromMetadata(ctx.job.metadata);
884
+ return {
885
+ ...ctx,
886
+ context: realContext,
887
+ job: { ...ctx.job, name: jobName, queue: queueName },
888
+ scope
889
+ };
890
+ }
891
+ /**
892
+ * Builds the execution context for a cron job.
893
+ * @internal
894
+ */
895
+ async buildCronExecutionContext(ctx, queueName, cronName) {
896
+ const realContext = await this.config.contextFactory();
897
+ const scope = ctx.scope ?? IgniterJobsScopeUtils.extractScopeFromMetadata(
898
+ ctx.job?.metadata
899
+ );
900
+ return {
901
+ ...ctx,
902
+ context: realContext,
903
+ job: { ...ctx.job, name: cronName, queue: queueName },
904
+ scope
905
+ };
906
+ }
907
+ /**
908
+ * Gets a job definition by queue and job name.
909
+ * @internal
910
+ */
911
+ getJobDefinition(queueName, jobName) {
912
+ const queue = this.config.queues[queueName];
913
+ return queue?.jobs?.[jobName];
914
+ }
915
+ /**
916
+ * Resolves the effective scope for a job operation.
917
+ * @internal
918
+ */
919
+ resolveScope(paramsScope) {
920
+ if (!this.config.scopeDefinition) return void 0;
728
921
  const required = Object.values(
729
- internal.config.scopeDefinition
922
+ this.config.scopeDefinition
730
923
  )[0]?.required ?? false;
731
- const effective = boundScope ?? paramsScope;
924
+ const effective = this.scopeEntry ?? paramsScope;
732
925
  if (required && !effective) {
733
926
  throw new IgniterJobsError({
734
927
  code: "JOBS_CONFIGURATION_INVALID",
735
928
  message: "Scope is required for this jobs instance."
736
929
  });
737
930
  }
738
- if (boundScope && paramsScope) {
739
- if (boundScope.type !== paramsScope.type || boundScope.id !== paramsScope.id) {
931
+ if (this.scopeEntry && paramsScope) {
932
+ if (this.scopeEntry.type !== paramsScope.type || this.scopeEntry.id !== paramsScope.id) {
740
933
  throw new IgniterJobsError({
741
934
  code: "JOBS_CONFIGURATION_INVALID",
742
935
  message: "Cannot override scope on a scoped jobs instance."
@@ -744,308 +937,57 @@ function createJobAccessor(params) {
744
937
  }
745
938
  }
746
939
  return effective;
747
- };
748
- const publish = async (event) => {
749
- await IgniterJobsEventsUtils.publishJobsEvent({
750
- adapter: internal.adapter,
751
- service: internal.config.service,
752
- environment: internal.config.environment,
753
- scope: event.scope,
754
- event
940
+ }
941
+ /**
942
+ * Builds the events channel string for subscriptions.
943
+ * @internal
944
+ */
945
+ buildEventsChannel() {
946
+ return this.scopeEntry ? IgniterJobsPrefix.buildEventsChannel({
947
+ service: this.config.service,
948
+ environment: this.config.environment,
949
+ scope: { type: this.scopeEntry.type, id: this.scopeEntry.id }
950
+ }) : IgniterJobsPrefix.buildEventsChannel({
951
+ service: this.config.service,
952
+ environment: this.config.environment
755
953
  });
756
- };
757
- const getDefinition = () => {
758
- const q = internal.config.queues[queueName];
759
- return q?.jobs?.[jobName];
760
- };
761
- return {
762
- async dispatch(params2) {
763
- const definition = getDefinition();
764
- if (definition?.input) {
765
- await IgniterJobsValidationUtils.validateInput(
766
- definition.input,
767
- params2.input
768
- );
769
- }
770
- const scope = resolveScope(params2.scope);
771
- const metadata = IgniterJobsScopeUtils.mergeMetadataWithScope(
772
- params2.metadata,
773
- scope
774
- );
775
- const jobId = await internal.adapter.dispatch({
776
- queue: queueName,
777
- jobName,
778
- ...params2,
779
- scope,
780
- metadata
781
- });
782
- await publish({
954
+ }
955
+ /**
956
+ * Publishes a job lifecycle event.
957
+ * @internal
958
+ */
959
+ async publishJobEvent(queueName, jobName, eventType, data, scope) {
960
+ await IgniterJobsEventsUtils.publishJobsEvent({
961
+ adapter: this.adapter,
962
+ service: this.config.service,
963
+ environment: this.config.environment,
964
+ scope,
965
+ event: {
783
966
  type: IgniterJobsEventsUtils.buildJobEventType(
784
967
  queueName,
785
968
  jobName,
786
- "enqueued"
969
+ eventType
787
970
  ),
788
- data: { jobId, queue: queueName, jobName },
971
+ data,
789
972
  timestamp: /* @__PURE__ */ new Date(),
790
973
  scope
791
- });
792
- IgniterJobsTelemetryUtils.emitTelemetry(
793
- internal.config.telemetry,
794
- "igniter.jobs.job.enqueued",
795
- {
796
- "ctx.job.id": jobId,
797
- "ctx.job.name": jobName,
798
- "ctx.job.queue": queueName,
799
- "ctx.job.priority": params2.priority ?? null,
800
- "ctx.job.delay": params2.delay ?? null
801
- }
802
- );
803
- return jobId;
804
- },
805
- async schedule(params2) {
806
- const definition = getDefinition();
807
- if (definition?.input) {
808
- await IgniterJobsValidationUtils.validateInput(
809
- definition.input,
810
- params2.input
811
- );
812
974
  }
813
- const scope = resolveScope(params2.scope);
814
- const metadata = IgniterJobsScopeUtils.mergeMetadataWithScope(
815
- params2.metadata,
816
- scope
817
- );
818
- const jobId = await internal.adapter.schedule({
819
- queue: queueName,
820
- jobName,
821
- ...params2,
822
- scope,
823
- metadata
824
- });
825
- await publish({
826
- type: IgniterJobsEventsUtils.buildJobEventType(
827
- queueName,
828
- jobName,
829
- "scheduled"
830
- ),
831
- data: { jobId, queue: queueName, jobName },
832
- timestamp: /* @__PURE__ */ new Date(),
833
- scope
834
- });
835
- IgniterJobsTelemetryUtils.emitTelemetry(
836
- internal.config.telemetry,
837
- "igniter.jobs.job.scheduled",
838
- {
839
- "ctx.job.id": jobId,
840
- "ctx.job.name": jobName,
841
- "ctx.job.queue": queueName,
842
- "ctx.job.scheduledAt": params2.runAt?.toISOString?.() ?? null,
843
- "ctx.job.cron": params2.cron ?? null
844
- }
845
- );
846
- return jobId;
847
- },
848
- get(id) {
849
- return {
850
- retrieve: () => internal.adapter.getJob(id, queueName),
851
- retry: () => internal.adapter.retryJob(id, queueName),
852
- remove: () => internal.adapter.removeJob(id, queueName),
853
- promote: () => internal.adapter.promoteJob(id, queueName),
854
- move: (state, reason) => {
855
- if (state !== "failed") return Promise.resolve();
856
- return internal.adapter.moveJobToFailed(id, reason, queueName);
857
- },
858
- state: () => internal.adapter.getJobState(id, queueName),
859
- progress: () => internal.adapter.getJobProgress(id, queueName),
860
- logs: () => internal.adapter.getJobLogs(id, queueName)
861
- };
862
- },
863
- many(ids) {
864
- return {
865
- retry: () => internal.adapter.retryManyJobs(ids, queueName),
866
- remove: () => internal.adapter.removeManyJobs(ids, queueName)
867
- };
868
- },
869
- pause: () => internal.adapter.pauseJobType(queueName, jobName),
870
- resume: () => internal.adapter.resumeJobType(queueName, jobName),
871
- async subscribe(handler) {
872
- const channel = boundScope ? IgniterJobsPrefix.buildEventsChannel({
873
- service: internal.config.service,
874
- environment: internal.config.environment,
875
- scope: { type: boundScope.type, id: boundScope.id }
876
- }) : IgniterJobsPrefix.buildEventsChannel({
877
- service: internal.config.service,
878
- environment: internal.config.environment
879
- });
880
- return internal.adapter.subscribeEvent(channel, async (event) => {
881
- const typed = event;
882
- if (typeof typed?.type === "string" && typed.type.startsWith(`${queueName}:${jobName}:`)) {
883
- await handler(typed);
884
- }
885
- });
886
- }
887
- };
888
- }
889
-
890
- // src/builders/igniter-jobs.builder.ts
891
- var IgniterJobsBuilder = class _IgniterJobsBuilder {
892
- constructor(state) {
893
- this.state = {
894
- queues: state?.queues ?? {},
895
- ...state
896
- };
897
- }
898
- /**
899
- * Creates the initial builder with no configuration.
900
- */
901
- static create() {
902
- return new _IgniterJobsBuilder({ queues: {} });
903
- }
904
- /**
905
- * Returns a new builder with updated state while preserving generics.
906
- */
907
- clone(patch) {
908
- return new _IgniterJobsBuilder({
909
- ...this.state,
910
- ...patch,
911
- queues: patch.queues ?? this.state.queues,
912
- scope: patch.scope ?? this.state.scope
913
975
  });
914
976
  }
915
977
  /**
916
- * Attaches the jobs adapter.
917
- *
918
- * @param adapter - Backend adapter implementation (BullMQ, memory, etc.).
919
- */
920
- withAdapter(adapter) {
921
- return this.clone({ adapter });
922
- }
923
- /**
924
- * Sets the service identifier for telemetry and metrics.
925
- *
926
- * @param service - Service name (e.g., "my-api").
927
- */
928
- withService(service) {
929
- return this.clone({ service });
930
- }
931
- /**
932
- * Sets the environment name (e.g., development, staging, production).
933
- */
934
- withEnvironment(environment) {
935
- return this.clone({ environment });
936
- }
937
- /**
938
- * Provides a context factory used when executing jobs.
939
- */
940
- withContext(factory) {
941
- return this.clone({ contextFactory: factory });
942
- }
943
- /**
944
- * Adds a scope definition (single scope supported).
945
- */
946
- addScope(name, options) {
947
- if (this.state.scope) {
948
- throw new IgniterJobsError({
949
- code: "JOBS_SCOPE_ALREADY_DEFINED",
950
- message: "Only one scope can be defined for IgniterJobs."
951
- });
952
- }
953
- return this.clone({ scope: { name, options } });
954
- }
955
- /**
956
- * Registers a queue definition on the builder.
957
- */
958
- addQueue(queue) {
959
- if (this.state.queues[queue.name]) {
960
- throw new IgniterJobsError({
961
- code: "JOBS_QUEUE_DUPLICATE",
962
- message: `Queue "${queue.name}" is already registered.`
963
- });
964
- }
965
- const nextQueues = {
966
- ...this.state.queues,
967
- [queue.name]: queue
968
- };
969
- return this.clone({ queues: nextQueues });
970
- }
971
- /**
972
- * Applies default job options to all queues.
973
- */
974
- withQueueDefaults(defaults) {
975
- return this.clone({ queueDefaults: defaults });
976
- }
977
- /**
978
- * Applies default worker options.
979
- */
980
- withWorkerDefaults(defaults) {
981
- return this.clone({ workerDefaults: defaults });
982
- }
983
- /**
984
- * Configures automatic worker startup.
985
- */
986
- withAutoStartWorker(config) {
987
- return this.clone({ autoStartWorker: config });
988
- }
989
- /**
990
- * Attaches telemetry support.
991
- */
992
- withTelemetry(telemetry) {
993
- return this.clone({ telemetry });
994
- }
995
- /**
996
- * Attaches a custom logger.
997
- */
998
- withLogger(logger) {
999
- return this.clone({ logger });
1000
- }
1001
- /**
1002
- * Finalizes the configuration and returns the runtime instance.
978
+ * Emits a telemetry event if telemetry is configured.
979
+ * @internal
1003
980
  */
1004
- build() {
1005
- if (!this.state.adapter) {
1006
- throw new IgniterJobsError({
1007
- code: "JOBS_ADAPTER_REQUIRED",
1008
- message: "Jobs adapter is required. Call withAdapter() before build()."
1009
- });
1010
- }
1011
- if (!this.state.service) {
1012
- throw new IgniterJobsError({
1013
- code: "JOBS_SERVICE_REQUIRED",
1014
- message: "Service name is required. Call withService() before build()."
1015
- });
1016
- }
1017
- if (!this.state.environment) {
1018
- throw new IgniterJobsError({
1019
- code: "JOBS_CONFIGURATION_INVALID",
1020
- message: "Environment is required. Call withEnvironment() before build()."
1021
- });
1022
- }
1023
- if (!this.state.contextFactory) {
1024
- throw new IgniterJobsError({
1025
- code: "JOBS_CONTEXT_REQUIRED",
1026
- message: "Context factory is required. Call withContext() before build()."
1027
- });
1028
- }
1029
- const config = {
1030
- adapter: this.state.adapter,
1031
- service: this.state.service,
1032
- environment: this.state.environment,
1033
- contextFactory: this.state.contextFactory,
1034
- queues: this.state.queues,
1035
- scopeDefinition: this.state.scope ? {
1036
- [this.state.scope.name]: this.state.scope.options ?? {}
1037
- } : void 0,
1038
- queueDefaults: this.state.queueDefaults,
1039
- workerDefaults: this.state.workerDefaults,
1040
- autoStartWorker: this.state.autoStartWorker,
1041
- logger: this.state.logger,
1042
- telemetry: this.state.telemetry
1043
- };
1044
- return IgniterJobs.fromConfig(config);
981
+ emitTelemetry(eventName, attributes, level = "info") {
982
+ if (!this.telemetry) return;
983
+ this.telemetry.emit(eventName, {
984
+ attributes,
985
+ level
986
+ });
1045
987
  }
1046
988
  };
1047
989
 
1048
- // src/builders/igniter-queue.builder.ts
990
+ // src/builders/queue.builder.ts
1049
991
  var IgniterQueueBuilder = class _IgniterQueueBuilder {
1050
992
  constructor(state) {
1051
993
  this.state = state;
@@ -1066,21 +1008,6 @@ var IgniterQueueBuilder = class _IgniterQueueBuilder {
1066
1008
  crons: {}
1067
1009
  });
1068
1010
  }
1069
- /**
1070
- * Re-types this builder with the application context type.
1071
- *
1072
- * This is a type-level helper; it does not mutate runtime state.
1073
- *
1074
- * @example
1075
- * ```typescript
1076
- * const queue = IgniterQueue.create('email')
1077
- * .withContext<AppContext>()
1078
- * .addJob('send', { handler: async ({ context }) => context.mailer.send() })
1079
- * ```
1080
- */
1081
- withContext() {
1082
- return this;
1083
- }
1084
1011
  clone(patch) {
1085
1012
  return new _IgniterQueueBuilder({
1086
1013
  ...this.state,
@@ -1195,7 +1122,7 @@ var IgniterQueueBuilder = class _IgniterQueueBuilder {
1195
1122
  }
1196
1123
  };
1197
1124
 
1198
- // src/core/igniter-queue.ts
1125
+ // src/core/queue.ts
1199
1126
  var IgniterQueue = class {
1200
1127
  /**
1201
1128
  * Creates a new queue builder for the given name.
@@ -1212,54 +1139,253 @@ var IgniterQueue = class {
1212
1139
  return IgniterQueueBuilder.create(name);
1213
1140
  }
1214
1141
  };
1215
- function toDateArray(values) {
1216
- if (!values) return void 0;
1217
- return values.map((v) => v instanceof Date ? v : new Date(v));
1218
- }
1219
- var IgniterJobsBullMQAdapter = class _IgniterJobsBullMQAdapter {
1220
- constructor(options) {
1221
- this.subscribers = /* @__PURE__ */ new Map();
1222
- this.coreAdapter = null;
1223
- this.coreExecutor = null;
1224
- this.executorDirty = true;
1225
- this.jobsByQueue = /* @__PURE__ */ new Map();
1226
- this.cronsByQueue = /* @__PURE__ */ new Map();
1227
- this.redis = options.redis;
1228
- this.publisher = this.redis;
1229
- this.subscriber = this.redis.duplicate();
1230
- this.client = { redis: this.redis };
1231
- this.subscriber.on("message", (channel, message) => {
1232
- const set = this.subscribers.get(channel);
1233
- if (!set || set.size === 0) return;
1234
- let payload = message;
1235
- try {
1236
- payload = JSON.parse(message);
1237
- } catch {
1238
- }
1239
- for (const handler of set) handler(payload);
1240
- });
1241
- this.queues = {
1242
- list: async () => this.listQueues(),
1243
- get: async (name) => this.getQueueInfo(name),
1244
- getJobCounts: async (name) => this.getQueueJobCounts(name),
1245
- getJobs: async (name, filter) => {
1246
- const full = this.toCoreQueueName(name);
1247
- return this.core().queues.getJobs(full, filter);
1248
- },
1249
- pause: async (name) => this.pauseQueue(name),
1250
- resume: async (name) => this.resumeQueue(name),
1251
- isPaused: async (name) => {
1252
- const full = this.toCoreQueueName(name);
1253
- return this.core().queues.isPaused(full);
1254
- },
1255
- drain: async (name) => this.drainQueue(name),
1256
- clean: async (name, options2) => this.cleanQueue(name, options2),
1257
- obliterate: async (name, options2) => this.obliterateQueue(name, options2)
1142
+
1143
+ // src/builders/main.builder.ts
1144
+ var IgniterJobsBuilder = class _IgniterJobsBuilder {
1145
+ constructor(state) {
1146
+ this.state = {
1147
+ queues: state?.queues ?? {},
1148
+ ...state
1258
1149
  };
1259
1150
  }
1260
- static create(options) {
1261
- return new _IgniterJobsBullMQAdapter(options);
1262
- }
1151
+ /**
1152
+ * Creates the initial builder with no configuration.
1153
+ *
1154
+ * Context type is inferred from `withContext()` - no explicit generic needed.
1155
+ *
1156
+ * @example
1157
+ * ```typescript
1158
+ * // Context is inferred from the factory return type
1159
+ * const jobs = IgniterJobs.create()
1160
+ * .withContext(async () => ({ db: prisma, cache: redis }))
1161
+ * // TContext is now { db: PrismaClient, cache: Redis }
1162
+ * .build()
1163
+ * ```
1164
+ */
1165
+ static create() {
1166
+ return new _IgniterJobsBuilder({
1167
+ queues: {}
1168
+ });
1169
+ }
1170
+ /**
1171
+ * Returns a new builder with updated state while preserving generics.
1172
+ */
1173
+ clone(patch) {
1174
+ return new _IgniterJobsBuilder({
1175
+ ...this.state,
1176
+ ...patch,
1177
+ queues: patch.queues ?? this.state.queues,
1178
+ scope: patch.scope ?? this.state.scope
1179
+ });
1180
+ }
1181
+ /**
1182
+ * Attaches the jobs adapter.
1183
+ *
1184
+ * @param adapter - Backend adapter implementation (BullMQ, memory, etc.).
1185
+ */
1186
+ withAdapter(adapter) {
1187
+ return this.clone({ adapter });
1188
+ }
1189
+ /**
1190
+ * Sets the service identifier for telemetry and metrics.
1191
+ *
1192
+ * @param service - Service name (e.g., "my-api").
1193
+ */
1194
+ withService(service) {
1195
+ return this.clone({ service });
1196
+ }
1197
+ /**
1198
+ * Sets the environment name (e.g., development, staging, production).
1199
+ */
1200
+ withEnvironment(environment) {
1201
+ return this.clone({ environment });
1202
+ }
1203
+ /**
1204
+ * Provides a context factory used when executing jobs.
1205
+ *
1206
+ * The context type is inferred from the factory return type, eliminating
1207
+ * the need for explicit generics on `IgniterJobs.create()`.
1208
+ *
1209
+ * **Note:** This must be called before adding any queues, as the context
1210
+ * type affects queue type compatibility.
1211
+ *
1212
+ * @param factory - Function that returns the context (sync or async)
1213
+ * @returns A new builder with the inferred context type
1214
+ *
1215
+ * @example
1216
+ * ```typescript
1217
+ * // Context type is inferred as { db: PrismaClient, cache: Redis }
1218
+ * const jobs = IgniterJobs.create()
1219
+ * .withContext(async () => ({
1220
+ * db: new PrismaClient(),
1221
+ * cache: new Redis(),
1222
+ * }))
1223
+ * .addQueue(emailQueue)
1224
+ * .build()
1225
+ * ```
1226
+ */
1227
+ withContext(factory) {
1228
+ return new _IgniterJobsBuilder({
1229
+ ...this.state,
1230
+ contextFactory: factory,
1231
+ queues: {}
1232
+ });
1233
+ }
1234
+ /**
1235
+ * Adds a scope definition (single scope supported).
1236
+ */
1237
+ addScope(name, options) {
1238
+ if (this.state.scope) {
1239
+ throw new IgniterJobsError({
1240
+ code: "JOBS_SCOPE_ALREADY_DEFINED",
1241
+ message: "Only one scope can be defined for IgniterJobs."
1242
+ });
1243
+ }
1244
+ return this.clone({ scope: { name, options } });
1245
+ }
1246
+ /**
1247
+ * Registers a queue definition on the builder.
1248
+ */
1249
+ addQueue(queue) {
1250
+ if (this.state.queues[queue.name]) {
1251
+ throw new IgniterJobsError({
1252
+ code: "JOBS_QUEUE_DUPLICATE",
1253
+ message: `Queue "${queue.name}" is already registered.`
1254
+ });
1255
+ }
1256
+ const nextQueues = {
1257
+ ...this.state.queues,
1258
+ [queue.name]: queue
1259
+ };
1260
+ return this.clone({ queues: nextQueues });
1261
+ }
1262
+ /**
1263
+ * Applies default job options to all queues.
1264
+ */
1265
+ withQueueDefaults(defaults) {
1266
+ return this.clone({ queueDefaults: defaults });
1267
+ }
1268
+ /**
1269
+ * Applies default worker options.
1270
+ */
1271
+ withWorkerDefaults(defaults) {
1272
+ return this.clone({ workerDefaults: defaults });
1273
+ }
1274
+ /**
1275
+ * Configures automatic worker startup.
1276
+ */
1277
+ withAutoStartWorker(config) {
1278
+ return this.clone({ autoStartWorker: config });
1279
+ }
1280
+ /**
1281
+ * Attaches telemetry support.
1282
+ */
1283
+ withTelemetry(telemetry) {
1284
+ return this.clone({ telemetry });
1285
+ }
1286
+ /**
1287
+ * Attaches a custom logger.
1288
+ */
1289
+ withLogger(logger) {
1290
+ return this.clone({ logger });
1291
+ }
1292
+ /**
1293
+ * Finalizes the configuration and returns the runtime instance.
1294
+ */
1295
+ build() {
1296
+ if (!this.state.adapter) {
1297
+ throw new IgniterJobsError({
1298
+ code: "JOBS_ADAPTER_REQUIRED",
1299
+ message: "Jobs adapter is required. Call withAdapter() before build()."
1300
+ });
1301
+ }
1302
+ if (!this.state.service) {
1303
+ throw new IgniterJobsError({
1304
+ code: "JOBS_SERVICE_REQUIRED",
1305
+ message: "Service name is required. Call withService() before build()."
1306
+ });
1307
+ }
1308
+ if (!this.state.environment) {
1309
+ throw new IgniterJobsError({
1310
+ code: "JOBS_CONFIGURATION_INVALID",
1311
+ message: "Environment is required. Call withEnvironment() before build()."
1312
+ });
1313
+ }
1314
+ if (!this.state.contextFactory) {
1315
+ throw new IgniterJobsError({
1316
+ code: "JOBS_CONTEXT_REQUIRED",
1317
+ message: "Context factory is required. Call withContext() before build()."
1318
+ });
1319
+ }
1320
+ const config = {
1321
+ adapter: this.state.adapter,
1322
+ service: this.state.service,
1323
+ environment: this.state.environment,
1324
+ contextFactory: this.state.contextFactory,
1325
+ queues: this.state.queues,
1326
+ scopeDefinition: this.state.scope ? {
1327
+ [this.state.scope.name]: this.state.scope.options ?? {}
1328
+ } : void 0,
1329
+ queueDefaults: this.state.queueDefaults,
1330
+ workerDefaults: this.state.workerDefaults,
1331
+ autoStartWorker: this.state.autoStartWorker,
1332
+ logger: this.state.logger,
1333
+ telemetry: this.state.telemetry
1334
+ };
1335
+ return new IgniterJobsManager(config).toRuntime();
1336
+ }
1337
+ };
1338
+ var IgniterJobs = {
1339
+ create: IgniterJobsBuilder.create
1340
+ };
1341
+ function toDateArray(values) {
1342
+ if (!values) return void 0;
1343
+ return values.map((v) => v instanceof Date ? v : new Date(v));
1344
+ }
1345
+ var IgniterJobsBullMQAdapter = class _IgniterJobsBullMQAdapter {
1346
+ constructor(options) {
1347
+ this.subscribers = /* @__PURE__ */ new Map();
1348
+ this.coreAdapter = null;
1349
+ this.coreExecutor = null;
1350
+ this.executorDirty = true;
1351
+ this.jobsByQueue = /* @__PURE__ */ new Map();
1352
+ this.cronsByQueue = /* @__PURE__ */ new Map();
1353
+ this.redis = options.redis;
1354
+ this.publisher = this.redis;
1355
+ this.subscriber = this.redis.duplicate();
1356
+ this.client = { redis: this.redis };
1357
+ this.subscriber.on("message", (channel, message) => {
1358
+ const set = this.subscribers.get(channel);
1359
+ if (!set || set.size === 0) return;
1360
+ let payload = message;
1361
+ try {
1362
+ payload = JSON.parse(message);
1363
+ } catch {
1364
+ }
1365
+ for (const handler of set) handler(payload);
1366
+ });
1367
+ this.queues = {
1368
+ list: async () => this.listQueues(),
1369
+ get: async (name) => this.getQueueInfo(name),
1370
+ getJobCounts: async (name) => this.getQueueJobCounts(name),
1371
+ getJobs: async (name, filter) => {
1372
+ const full = this.toCoreQueueName(name);
1373
+ return this.core().queues.getJobs(full, filter);
1374
+ },
1375
+ pause: async (name) => this.pauseQueue(name),
1376
+ resume: async (name) => this.resumeQueue(name),
1377
+ isPaused: async (name) => {
1378
+ const full = this.toCoreQueueName(name);
1379
+ return this.core().queues.isPaused(full);
1380
+ },
1381
+ drain: async (name) => this.drainQueue(name),
1382
+ clean: async (name, options2) => this.cleanQueue(name, options2),
1383
+ obliterate: async (name, options2) => this.obliterateQueue(name, options2)
1384
+ };
1385
+ }
1386
+ static create(options) {
1387
+ return new _IgniterJobsBullMQAdapter(options);
1388
+ }
1263
1389
  registerJob(queueName, jobName, definition) {
1264
1390
  const map = this.jobsByQueue.get(queueName) ?? /* @__PURE__ */ new Map();
1265
1391
  if (map.has(jobName)) {
@@ -1752,38 +1878,761 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
1752
1878
  async dispatch(params) {
1753
1879
  const jobId = params.jobId ?? IgniterJobsIdGenerator.generate("job");
1754
1880
  const maxAttempts = params.attempts ?? 1;
1755
- const metadata = params.metadata ?? {};
1756
- const job = {
1757
- id: jobId,
1758
- name: params.jobName,
1759
- queue: params.queue,
1760
- input: params.input,
1761
- status: this.pausedQueues.has(params.queue) ? "paused" : params.delay && params.delay > 0 ? "delayed" : "waiting",
1762
- progress: 0,
1763
- attemptsMade: 0,
1881
+ const metadata = params.metadata ?? {};
1882
+ const job = {
1883
+ id: jobId,
1884
+ name: params.jobName,
1885
+ queue: params.queue,
1886
+ input: params.input,
1887
+ status: this.pausedQueues.has(params.queue) ? "paused" : params.delay && params.delay > 0 ? "delayed" : "waiting",
1888
+ progress: 0,
1889
+ attemptsMade: 0,
1890
+ maxAttempts,
1891
+ priority: params.priority ?? 0,
1892
+ createdAt: /* @__PURE__ */ new Date(),
1893
+ metadata,
1894
+ scope: params.scope,
1895
+ logs: []
1896
+ };
1897
+ this.jobsById.set(jobId, job);
1898
+ const queueList = this.jobsByQueue.get(params.queue) ?? [];
1899
+ queueList.push(jobId);
1900
+ this.jobsByQueue.set(params.queue, queueList);
1901
+ if (params.delay && params.delay > 0) {
1902
+ setTimeout(() => {
1903
+ const stored = this.jobsById.get(jobId);
1904
+ if (!stored) return;
1905
+ if (!this.pausedQueues.has(params.queue)) stored.status = "waiting";
1906
+ void this.kickWorkers(params.queue);
1907
+ }, params.delay);
1908
+ return jobId;
1909
+ }
1910
+ void this.kickWorkers(params.queue);
1911
+ return jobId;
1912
+ }
1913
+ async schedule(params) {
1914
+ if (params.at) {
1915
+ const delay = params.at.getTime() - Date.now();
1916
+ if (delay <= 0) {
1917
+ throw new IgniterJobsError({
1918
+ code: "JOBS_INVALID_SCHEDULE",
1919
+ message: "Scheduled time must be in the future."
1920
+ });
1921
+ }
1922
+ return this.dispatch({ ...params, delay });
1923
+ }
1924
+ if (params.cron || params.every) {
1925
+ return this.dispatch({ ...params, delay: params.delay ?? 0 });
1926
+ }
1927
+ return this.dispatch(params);
1928
+ }
1929
+ async getJob(jobId, queue) {
1930
+ const job = this.jobsById.get(jobId);
1931
+ if (!job) return null;
1932
+ if (queue && job.queue !== queue) return null;
1933
+ return this.toSearchResult(job);
1934
+ }
1935
+ async getJobState(jobId, queue) {
1936
+ const job = this.jobsById.get(jobId);
1937
+ if (!job) return null;
1938
+ if (queue && job.queue !== queue) return null;
1939
+ return job.status;
1940
+ }
1941
+ async getJobLogs(jobId, queue) {
1942
+ const job = this.jobsById.get(jobId);
1943
+ if (!job) return [];
1944
+ if (queue && job.queue !== queue) return [];
1945
+ return job.logs;
1946
+ }
1947
+ async getJobProgress(jobId, queue) {
1948
+ const job = this.jobsById.get(jobId);
1949
+ if (!job) return 0;
1950
+ if (queue && job.queue !== queue) return 0;
1951
+ return job.progress;
1952
+ }
1953
+ async retryJob(jobId, queue) {
1954
+ const job = this.jobsById.get(jobId);
1955
+ if (!job) {
1956
+ throw new IgniterJobsError({
1957
+ code: "JOBS_NOT_FOUND",
1958
+ message: `Job "${jobId}" not found.`
1959
+ });
1960
+ }
1961
+ if (queue && job.queue !== queue) {
1962
+ throw new IgniterJobsError({
1963
+ code: "JOBS_NOT_FOUND",
1964
+ message: `Job "${jobId}" not found in queue "${queue}".`
1965
+ });
1966
+ }
1967
+ job.status = "waiting";
1968
+ job.error = void 0;
1969
+ job.completedAt = void 0;
1970
+ job.progress = 0;
1971
+ void this.kickWorkers(job.queue);
1972
+ }
1973
+ async removeJob(jobId, queue) {
1974
+ const job = this.jobsById.get(jobId);
1975
+ if (!job) return;
1976
+ if (queue && job.queue !== queue) return;
1977
+ this.jobsById.delete(jobId);
1978
+ const list = this.jobsByQueue.get(job.queue);
1979
+ if (list)
1980
+ this.jobsByQueue.set(
1981
+ job.queue,
1982
+ list.filter((id) => id !== jobId)
1983
+ );
1984
+ }
1985
+ async promoteJob(jobId, queue) {
1986
+ const job = this.jobsById.get(jobId);
1987
+ if (!job) {
1988
+ throw new IgniterJobsError({
1989
+ code: "JOBS_NOT_FOUND",
1990
+ message: `Job "${jobId}" not found.`
1991
+ });
1992
+ }
1993
+ if (queue && job.queue !== queue) {
1994
+ throw new IgniterJobsError({
1995
+ code: "JOBS_NOT_FOUND",
1996
+ message: `Job "${jobId}" not found in queue "${queue}".`
1997
+ });
1998
+ }
1999
+ if (job.status === "delayed" || job.status === "paused") {
2000
+ job.status = this.pausedQueues.has(job.queue) ? "paused" : "waiting";
2001
+ void this.kickWorkers(job.queue);
2002
+ }
2003
+ }
2004
+ async moveJobToFailed(jobId, reason, queue) {
2005
+ const job = this.jobsById.get(jobId);
2006
+ if (!job) {
2007
+ throw new IgniterJobsError({
2008
+ code: "JOBS_NOT_FOUND",
2009
+ message: `Job "${jobId}" not found.`
2010
+ });
2011
+ }
2012
+ if (queue && job.queue !== queue) {
2013
+ throw new IgniterJobsError({
2014
+ code: "JOBS_NOT_FOUND",
2015
+ message: `Job "${jobId}" not found in queue "${queue}".`
2016
+ });
2017
+ }
2018
+ job.status = "failed";
2019
+ job.error = reason;
2020
+ job.completedAt = /* @__PURE__ */ new Date();
2021
+ }
2022
+ async retryManyJobs(jobIds, queue) {
2023
+ await Promise.all(jobIds.map((id) => this.retryJob(id, queue)));
2024
+ }
2025
+ async removeManyJobs(jobIds, queue) {
2026
+ await Promise.all(jobIds.map((id) => this.removeJob(id, queue)));
2027
+ }
2028
+ async getQueueInfo(queue) {
2029
+ const counts = await this.getQueueJobCounts(queue);
2030
+ return {
2031
+ name: queue,
2032
+ isPaused: this.pausedQueues.has(queue),
2033
+ jobCounts: counts
2034
+ };
2035
+ }
2036
+ async getQueueJobCounts(queue) {
2037
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
2038
+ const counts = {
2039
+ waiting: 0,
2040
+ active: 0,
2041
+ completed: 0,
2042
+ failed: 0,
2043
+ delayed: 0,
2044
+ paused: 0
2045
+ };
2046
+ for (const id of jobIds) {
2047
+ const job = this.jobsById.get(id);
2048
+ if (!job) continue;
2049
+ if (job.status in counts) {
2050
+ counts[job.status]++;
2051
+ }
2052
+ }
2053
+ return counts;
2054
+ }
2055
+ async listQueues() {
2056
+ const queues = Array.from(
2057
+ /* @__PURE__ */ new Set([
2058
+ ...this.jobsByQueue.keys(),
2059
+ ...this.registeredJobs.keys(),
2060
+ ...this.registeredCrons.keys()
2061
+ ])
2062
+ );
2063
+ const result = [];
2064
+ for (const q of queues) {
2065
+ result.push(await this.getQueueInfo(q));
2066
+ }
2067
+ return result;
2068
+ }
2069
+ async pauseQueue(queue) {
2070
+ this.pausedQueues.add(queue);
2071
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
2072
+ for (const id of jobIds) {
2073
+ const job = this.jobsById.get(id);
2074
+ if (!job) continue;
2075
+ if (job.status === "waiting") job.status = "paused";
2076
+ }
2077
+ }
2078
+ async resumeQueue(queue) {
2079
+ this.pausedQueues.delete(queue);
2080
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
2081
+ for (const id of jobIds) {
2082
+ const job = this.jobsById.get(id);
2083
+ if (!job) continue;
2084
+ if (job.status === "paused") job.status = "waiting";
2085
+ }
2086
+ void this.kickWorkers(queue);
2087
+ }
2088
+ async drainQueue(queue) {
2089
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
2090
+ let removed = 0;
2091
+ for (const id of jobIds) {
2092
+ const job = this.jobsById.get(id);
2093
+ if (!job) continue;
2094
+ if (job.status === "waiting" || job.status === "paused") {
2095
+ this.jobsById.delete(id);
2096
+ removed++;
2097
+ }
2098
+ }
2099
+ this.jobsByQueue.set(
2100
+ queue,
2101
+ jobIds.filter((id) => this.jobsById.has(id))
2102
+ );
2103
+ return removed;
2104
+ }
2105
+ async cleanQueue(queue, options) {
2106
+ const statuses = Array.isArray(options.status) ? options.status : [options.status];
2107
+ const olderThan = options.olderThan ?? 0;
2108
+ const limit = options.limit ?? Number.POSITIVE_INFINITY;
2109
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
2110
+ const now = Date.now();
2111
+ let cleaned = 0;
2112
+ for (const id of [...jobIds]) {
2113
+ if (cleaned >= limit) break;
2114
+ const job = this.jobsById.get(id);
2115
+ if (!job) continue;
2116
+ if (!statuses.includes(job.status)) continue;
2117
+ const ageMs = now - job.createdAt.getTime();
2118
+ if (ageMs < olderThan) continue;
2119
+ this.jobsById.delete(id);
2120
+ cleaned++;
2121
+ }
2122
+ this.jobsByQueue.set(
2123
+ queue,
2124
+ jobIds.filter((id) => this.jobsById.has(id))
2125
+ );
2126
+ return cleaned;
2127
+ }
2128
+ async obliterateQueue(queue, options) {
2129
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
2130
+ for (const id of jobIds) this.jobsById.delete(id);
2131
+ this.jobsByQueue.delete(queue);
2132
+ this.registeredJobs.delete(queue);
2133
+ this.registeredCrons.delete(queue);
2134
+ this.pausedQueues.delete(queue);
2135
+ }
2136
+ async retryAllInQueue(queue) {
2137
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
2138
+ let retried = 0;
2139
+ for (const id of jobIds) {
2140
+ const job = this.jobsById.get(id);
2141
+ if (!job) continue;
2142
+ if (job.status === "failed") {
2143
+ await this.retryJob(id, queue);
2144
+ retried++;
2145
+ }
2146
+ }
2147
+ return retried;
2148
+ }
2149
+ async pauseJobType(queue, jobName) {
2150
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
2151
+ for (const id of jobIds) {
2152
+ const job = this.jobsById.get(id);
2153
+ if (!job) continue;
2154
+ if (job.name === jobName && job.status === "waiting")
2155
+ job.status = "paused";
2156
+ }
2157
+ }
2158
+ async resumeJobType(queue, jobName) {
2159
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
2160
+ for (const id of jobIds) {
2161
+ const job = this.jobsById.get(id);
2162
+ if (!job) continue;
2163
+ if (job.name === jobName && job.status === "paused")
2164
+ job.status = "waiting";
2165
+ }
2166
+ void this.kickWorkers(queue);
2167
+ }
2168
+ async searchJobs(filter) {
2169
+ const queue = filter?.queue;
2170
+ const statuses = filter?.status;
2171
+ const limit = filter?.limit ?? 100;
2172
+ const offset = filter?.offset ?? 0;
2173
+ const all = Array.from(this.jobsById.values()).filter((j) => queue ? j.queue === queue : true).filter((j) => statuses ? statuses.includes(j.status) : true).sort(
2174
+ (a, b) => b.priority - a.priority || a.createdAt.getTime() - b.createdAt.getTime()
2175
+ );
2176
+ return all.slice(offset, offset + limit).map((j) => this.toSearchResult(j));
2177
+ }
2178
+ async searchQueues(filter) {
2179
+ const name = filter?.name;
2180
+ const isPaused = filter?.isPaused;
2181
+ const all = await this.listQueues();
2182
+ return all.filter((q) => name ? q.name.includes(name) : true).filter(
2183
+ (q) => typeof isPaused === "boolean" ? q.isPaused === isPaused : true
2184
+ );
2185
+ }
2186
+ async searchWorkers(filter) {
2187
+ const queue = filter?.queue;
2188
+ const isRunning = filter?.isRunning;
2189
+ return Array.from(this.workers.values()).filter((w) => queue ? w.queues.includes(queue) : true).filter(
2190
+ (w) => typeof isRunning === "boolean" ? isRunning ? !w.closed : w.closed : true
2191
+ ).map((w) => this.toWorkerHandle(w));
2192
+ }
2193
+ async createWorker(config) {
2194
+ const workerId = IgniterJobsIdGenerator.generate("worker");
2195
+ const state = {
2196
+ id: workerId,
2197
+ queues: config.queues ?? [],
2198
+ concurrency: config.concurrency ?? 1,
2199
+ paused: false,
2200
+ closed: false,
2201
+ startedAt: /* @__PURE__ */ new Date(),
2202
+ metrics: { processed: 0, failed: 0, totalDuration: 0 },
2203
+ handlers: config.handlers
2204
+ };
2205
+ this.workers.set(workerId, state);
2206
+ for (const q of state.queues) void this.kickWorkers(q);
2207
+ return this.toWorkerHandle(state);
2208
+ }
2209
+ getWorkers() {
2210
+ const out = /* @__PURE__ */ new Map();
2211
+ for (const [id, state] of this.workers)
2212
+ out.set(id, this.toWorkerHandle(state));
2213
+ return out;
2214
+ }
2215
+ async publishEvent(channel, payload) {
2216
+ const handlers = this.subscribers.get(channel);
2217
+ if (!handlers) return;
2218
+ await Promise.all(Array.from(handlers).map(async (h) => h(payload)));
2219
+ }
2220
+ async subscribeEvent(channel, handler) {
2221
+ const set = this.subscribers.get(channel) ?? /* @__PURE__ */ new Set();
2222
+ set.add(handler);
2223
+ this.subscribers.set(channel, set);
2224
+ return async () => {
2225
+ const current = this.subscribers.get(channel);
2226
+ if (!current) return;
2227
+ current.delete(handler);
2228
+ if (current.size === 0) this.subscribers.delete(channel);
2229
+ };
2230
+ }
2231
+ async shutdown() {
2232
+ this.workers.clear();
2233
+ this.subscribers.clear();
2234
+ }
2235
+ toSearchResult(job) {
2236
+ return {
2237
+ id: job.id,
2238
+ name: job.name,
2239
+ queue: job.queue,
2240
+ status: job.status,
2241
+ input: job.input,
2242
+ result: job.result,
2243
+ error: job.error,
2244
+ progress: job.progress,
2245
+ attemptsMade: job.attemptsMade,
2246
+ priority: job.priority,
2247
+ createdAt: job.createdAt,
2248
+ startedAt: job.startedAt,
2249
+ completedAt: job.completedAt,
2250
+ metadata: job.metadata,
2251
+ scope: job.scope
2252
+ };
2253
+ }
2254
+ toWorkerHandle(worker) {
2255
+ return {
2256
+ id: worker.id,
2257
+ queues: worker.queues,
2258
+ pause: async () => {
2259
+ worker.paused = true;
2260
+ },
2261
+ resume: async () => {
2262
+ worker.paused = false;
2263
+ for (const q of worker.queues) void this.kickWorkers(q);
2264
+ },
2265
+ close: async () => {
2266
+ worker.closed = true;
2267
+ },
2268
+ isRunning: () => !worker.closed && !worker.paused,
2269
+ isPaused: () => worker.paused,
2270
+ isClosed: () => worker.closed,
2271
+ getMetrics: async () => this.toWorkerMetrics(worker)
2272
+ };
2273
+ }
2274
+ toWorkerMetrics(worker) {
2275
+ const uptime = Date.now() - worker.startedAt.getTime();
2276
+ const processed = worker.metrics.processed;
2277
+ return {
2278
+ processed,
2279
+ failed: worker.metrics.failed,
2280
+ avgDuration: processed > 0 ? worker.metrics.totalDuration / processed : 0,
2281
+ concurrency: worker.concurrency,
2282
+ uptime
2283
+ };
2284
+ }
2285
+ async kickWorkers(queue) {
2286
+ if (this.pausedQueues.has(queue)) return;
2287
+ const relevant = Array.from(this.workers.values()).filter(
2288
+ (w) => !w.closed && !w.paused && (w.queues.length === 0 || w.queues.includes(queue))
2289
+ );
2290
+ if (relevant.length === 0) return;
2291
+ for (const w of relevant) {
2292
+ void this.processLoop(w, queue);
2293
+ }
2294
+ }
2295
+ async processLoop(worker, queue) {
2296
+ if (worker.closed || worker.paused) return;
2297
+ const concurrency = Math.max(1, worker.concurrency);
2298
+ const running = worker.__running;
2299
+ const currentRunning = running ?? 0;
2300
+ if (currentRunning >= concurrency) return;
2301
+ worker.__running = currentRunning + 1;
2302
+ try {
2303
+ const next = this.nextJob(queue);
2304
+ if (!next) return;
2305
+ await this.processJob(worker, next);
2306
+ } finally {
2307
+ worker.__running = worker.__running - 1;
2308
+ if (this.nextJob(queue)) void this.processLoop(worker, queue);
2309
+ else if (worker.handlers?.onIdle) await worker.handlers.onIdle();
2310
+ }
2311
+ }
2312
+ nextJob(queue) {
2313
+ const ids = this.jobsByQueue.get(queue) ?? [];
2314
+ const candidates = ids.map((id) => this.jobsById.get(id)).filter((j) => Boolean(j)).filter((j) => j.status === "waiting").sort(
2315
+ (a, b) => b.priority - a.priority || a.createdAt.getTime() - b.createdAt.getTime()
2316
+ );
2317
+ return candidates[0] ?? null;
2318
+ }
2319
+ async processJob(worker, job) {
2320
+ if (this.pausedQueues.has(job.queue)) {
2321
+ job.status = "paused";
2322
+ return;
2323
+ }
2324
+ job.status = "active";
2325
+ job.startedAt = /* @__PURE__ */ new Date();
2326
+ job.attemptsMade += 1;
2327
+ job.logs.push({
2328
+ timestamp: /* @__PURE__ */ new Date(),
2329
+ level: "info",
2330
+ message: "Job started"
2331
+ });
2332
+ if (worker.handlers?.onActive)
2333
+ await worker.handlers.onActive({ job: this.toSearchResult(job) });
2334
+ const start = Date.now();
2335
+ try {
2336
+ const definition = this.registeredJobs.get(job.queue)?.get(job.name);
2337
+ if (!definition) {
2338
+ throw new IgniterJobsError({
2339
+ code: "JOBS_NOT_REGISTERED",
2340
+ message: `Job "${job.name}" is not registered for queue "${job.queue}".`
2341
+ });
2342
+ }
2343
+ if (definition.onStart) {
2344
+ await definition.onStart({
2345
+ input: job.input,
2346
+ context: {},
2347
+ job: {
2348
+ id: job.id,
2349
+ name: job.name,
2350
+ queue: job.queue,
2351
+ attemptsMade: job.attemptsMade,
2352
+ metadata: job.metadata
2353
+ },
2354
+ scope: job.scope,
2355
+ startedAt: job.startedAt
2356
+ });
2357
+ }
2358
+ const result = await definition.handler({
2359
+ input: job.input,
2360
+ context: {},
2361
+ job: {
2362
+ id: job.id,
2363
+ name: job.name,
2364
+ queue: job.queue,
2365
+ attemptsMade: job.attemptsMade,
2366
+ metadata: job.metadata
2367
+ },
2368
+ scope: job.scope
2369
+ });
2370
+ const duration = Date.now() - start;
2371
+ job.status = "completed";
2372
+ job.completedAt = /* @__PURE__ */ new Date();
2373
+ job.result = result;
2374
+ job.progress = 100;
2375
+ job.logs.push({
2376
+ timestamp: /* @__PURE__ */ new Date(),
2377
+ level: "info",
2378
+ message: `Job completed in ${duration}ms`
2379
+ });
2380
+ worker.metrics.processed += 1;
2381
+ worker.metrics.totalDuration += duration;
2382
+ if (definition.onSuccess) {
2383
+ await definition.onSuccess({
2384
+ input: job.input,
2385
+ context: {},
2386
+ job: {
2387
+ id: job.id,
2388
+ name: job.name,
2389
+ queue: job.queue,
2390
+ attemptsMade: job.attemptsMade,
2391
+ metadata: job.metadata
2392
+ },
2393
+ scope: job.scope,
2394
+ result,
2395
+ duration
2396
+ });
2397
+ }
2398
+ if (worker.handlers?.onSuccess)
2399
+ await worker.handlers.onSuccess({
2400
+ job: this.toSearchResult(job),
2401
+ result
2402
+ });
2403
+ } catch (error) {
2404
+ job.error = error?.message ?? String(error);
2405
+ job.logs.push({
2406
+ timestamp: /* @__PURE__ */ new Date(),
2407
+ level: "error",
2408
+ message: job.error ?? "Unknown error"
2409
+ });
2410
+ const isFinalAttempt = job.attemptsMade >= job.maxAttempts;
2411
+ if (isFinalAttempt) {
2412
+ job.status = "failed";
2413
+ job.completedAt = /* @__PURE__ */ new Date();
2414
+ worker.metrics.failed += 1;
2415
+ const definition = this.registeredJobs.get(job.queue)?.get(job.name);
2416
+ if (definition?.onFailure) {
2417
+ await definition.onFailure({
2418
+ input: job.input,
2419
+ context: {},
2420
+ job: {
2421
+ id: job.id,
2422
+ name: job.name,
2423
+ queue: job.queue,
2424
+ attemptsMade: job.attemptsMade,
2425
+ metadata: job.metadata
2426
+ },
2427
+ scope: job.scope,
2428
+ error,
2429
+ isFinalAttempt: true
2430
+ });
2431
+ }
2432
+ if (worker.handlers?.onFailure)
2433
+ await worker.handlers.onFailure({
2434
+ job: this.toSearchResult(job),
2435
+ error
2436
+ });
2437
+ } else {
2438
+ job.status = "waiting";
2439
+ void this.kickWorkers(job.queue);
2440
+ }
2441
+ }
2442
+ }
2443
+ };
2444
+
2445
+ // src/adapters/sqlite.adapter.ts
2446
+ var IgniterJobsSQLiteAdapter = class _IgniterJobsSQLiteAdapter {
2447
+ constructor(options) {
2448
+ this.registeredJobs = /* @__PURE__ */ new Map();
2449
+ this.registeredCrons = /* @__PURE__ */ new Map();
2450
+ this.workers = /* @__PURE__ */ new Map();
2451
+ this.subscribers = /* @__PURE__ */ new Map();
2452
+ this.pausedQueues = /* @__PURE__ */ new Set();
2453
+ this.queues = {
2454
+ list: async () => this.listQueues(),
2455
+ get: async (name) => this.getQueueInfo(name),
2456
+ getJobCounts: async (name) => this.getQueueJobCounts(name),
2457
+ getJobs: async (name, filter) => {
2458
+ const statuses = filter?.status;
2459
+ const limit = filter?.limit ?? 100;
2460
+ const offset = filter?.offset ?? 0;
2461
+ const results = await this.searchJobs({
2462
+ queue: name,
2463
+ status: statuses,
2464
+ limit,
2465
+ offset
2466
+ });
2467
+ return results;
2468
+ },
2469
+ pause: async (name) => this.pauseQueue(name),
2470
+ resume: async (name) => this.resumeQueue(name),
2471
+ isPaused: async (name) => {
2472
+ const info = await this.getQueueInfo(name);
2473
+ return info?.isPaused ?? false;
2474
+ },
2475
+ drain: async (name) => this.drainQueue(name),
2476
+ clean: async (name, options) => this.cleanQueue(name, options),
2477
+ obliterate: async (name, options) => this.obliterateQueue(name, options)
2478
+ };
2479
+ this.options = {
2480
+ path: options.path,
2481
+ pollingInterval: options.pollingInterval ?? 500,
2482
+ enableWAL: options.enableWAL ?? true
2483
+ };
2484
+ this.client = {
2485
+ type: "sqlite",
2486
+ path: this.options.path
2487
+ };
2488
+ const Database = __require("better-sqlite3");
2489
+ this.db = new Database(this.options.path);
2490
+ this.initializeSchema();
2491
+ }
2492
+ /**
2493
+ * Creates a new SQLite adapter instance.
2494
+ *
2495
+ * @param options - Configuration options
2496
+ * @returns A new adapter instance
2497
+ *
2498
+ * @example
2499
+ * ```ts
2500
+ * // File-based database (persistent)
2501
+ * const adapter = IgniterJobsSQLiteAdapter.create({
2502
+ * path: './data/jobs.sqlite'
2503
+ * });
2504
+ *
2505
+ * // In-memory database (for testing)
2506
+ * const testAdapter = IgniterJobsSQLiteAdapter.create({
2507
+ * path: ':memory:'
2508
+ * });
2509
+ * ```
2510
+ */
2511
+ static create(options) {
2512
+ return new _IgniterJobsSQLiteAdapter(options);
2513
+ }
2514
+ initializeSchema() {
2515
+ if (this.options.enableWAL) {
2516
+ this.db.exec("PRAGMA journal_mode = WAL;");
2517
+ }
2518
+ this.db.exec(`
2519
+ CREATE TABLE IF NOT EXISTS jobs (
2520
+ id TEXT PRIMARY KEY,
2521
+ name TEXT NOT NULL,
2522
+ queue TEXT NOT NULL,
2523
+ input TEXT NOT NULL,
2524
+ status TEXT NOT NULL DEFAULT 'waiting',
2525
+ progress REAL NOT NULL DEFAULT 0,
2526
+ attempts_made INTEGER NOT NULL DEFAULT 0,
2527
+ max_attempts INTEGER NOT NULL DEFAULT 1,
2528
+ priority INTEGER NOT NULL DEFAULT 0,
2529
+ created_at TEXT NOT NULL,
2530
+ started_at TEXT,
2531
+ completed_at TEXT,
2532
+ scheduled_at TEXT,
2533
+ result TEXT,
2534
+ error TEXT,
2535
+ metadata TEXT,
2536
+ scope TEXT
2537
+ );
2538
+
2539
+ CREATE INDEX IF NOT EXISTS idx_jobs_queue_status ON jobs(queue, status);
2540
+ CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
2541
+ CREATE INDEX IF NOT EXISTS idx_jobs_scheduled_at ON jobs(scheduled_at);
2542
+ CREATE INDEX IF NOT EXISTS idx_jobs_priority ON jobs(priority DESC, created_at ASC);
2543
+ `);
2544
+ this.db.exec(`
2545
+ CREATE TABLE IF NOT EXISTS job_logs (
2546
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2547
+ job_id TEXT NOT NULL,
2548
+ timestamp TEXT NOT NULL,
2549
+ level TEXT NOT NULL,
2550
+ message TEXT NOT NULL,
2551
+ FOREIGN KEY (job_id) REFERENCES jobs(id) ON DELETE CASCADE
2552
+ );
2553
+
2554
+ CREATE INDEX IF NOT EXISTS idx_job_logs_job_id ON job_logs(job_id);
2555
+ `);
2556
+ this.db.exec(`
2557
+ CREATE TABLE IF NOT EXISTS paused_queues (
2558
+ name TEXT PRIMARY KEY
2559
+ );
2560
+ `);
2561
+ const pausedRows = this.db.prepare(
2562
+ "SELECT name FROM paused_queues"
2563
+ ).all();
2564
+ for (const row of pausedRows) {
2565
+ this.pausedQueues.add(row.name);
2566
+ }
2567
+ }
2568
+ registerJob(queueName, jobName, definition) {
2569
+ const queueJobs = this.registeredJobs.get(queueName) ?? /* @__PURE__ */ new Map();
2570
+ if (queueJobs.has(jobName)) {
2571
+ throw new IgniterJobsError({
2572
+ code: "JOBS_DUPLICATE_JOB",
2573
+ message: `Job "${jobName}" already registered for queue "${queueName}".`
2574
+ });
2575
+ }
2576
+ queueJobs.set(jobName, definition);
2577
+ this.registeredJobs.set(queueName, queueJobs);
2578
+ }
2579
+ registerCron(queueName, cronName, definition) {
2580
+ const queueCrons = this.registeredCrons.get(queueName) ?? /* @__PURE__ */ new Map();
2581
+ if (queueCrons.has(cronName)) {
2582
+ throw new IgniterJobsError({
2583
+ code: "JOBS_INVALID_CRON",
2584
+ message: `Cron "${cronName}" already registered for queue "${queueName}".`
2585
+ });
2586
+ }
2587
+ queueCrons.set(cronName, definition);
2588
+ this.registeredCrons.set(queueName, queueCrons);
2589
+ }
2590
+ async dispatch(params) {
2591
+ const jobId = params.jobId ?? IgniterJobsIdGenerator.generate("job");
2592
+ const maxAttempts = params.attempts ?? 1;
2593
+ const now = /* @__PURE__ */ new Date();
2594
+ let status = "waiting";
2595
+ let scheduledAt = null;
2596
+ if (this.pausedQueues.has(params.queue)) {
2597
+ status = "paused";
2598
+ } else if (params.delay && params.delay > 0) {
2599
+ status = "delayed";
2600
+ scheduledAt = new Date(now.getTime() + params.delay);
2601
+ }
2602
+ const stmt = this.db.prepare(`
2603
+ INSERT INTO jobs (
2604
+ id, name, queue, input, status, progress, attempts_made, max_attempts,
2605
+ priority, created_at, scheduled_at, metadata, scope
2606
+ ) VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?)
2607
+ `);
2608
+ stmt.run(
2609
+ jobId,
2610
+ params.jobName,
2611
+ params.queue,
2612
+ JSON.stringify(params.input ?? {}),
2613
+ status,
1764
2614
  maxAttempts,
1765
- priority: params.priority ?? 0,
1766
- createdAt: /* @__PURE__ */ new Date(),
1767
- metadata,
1768
- scope: params.scope,
1769
- logs: []
1770
- };
1771
- this.jobsById.set(jobId, job);
1772
- const queueList = this.jobsByQueue.get(params.queue) ?? [];
1773
- queueList.push(jobId);
1774
- this.jobsByQueue.set(params.queue, queueList);
2615
+ params.priority ?? 0,
2616
+ now.toISOString(),
2617
+ scheduledAt?.toISOString() ?? null,
2618
+ params.metadata ? JSON.stringify(params.metadata) : null,
2619
+ params.scope ? JSON.stringify(params.scope) : null
2620
+ );
1775
2621
  if (params.delay && params.delay > 0) {
1776
2622
  setTimeout(() => {
1777
- const stored = this.jobsById.get(jobId);
1778
- if (!stored) return;
1779
- if (!this.pausedQueues.has(params.queue)) stored.status = "waiting";
1780
- void this.kickWorkers(params.queue);
2623
+ this.promoteDelayedJob(jobId, params.queue);
1781
2624
  }, params.delay);
1782
- return jobId;
1783
2625
  }
1784
- void this.kickWorkers(params.queue);
1785
2626
  return jobId;
1786
2627
  }
2628
+ promoteDelayedJob(jobId, queue) {
2629
+ if (this.pausedQueues.has(queue)) return;
2630
+ const stmt = this.db.prepare(`
2631
+ UPDATE jobs SET status = 'waiting', scheduled_at = NULL
2632
+ WHERE id = ? AND status = 'delayed'
2633
+ `);
2634
+ stmt.run(jobId);
2635
+ }
1787
2636
  async schedule(params) {
1788
2637
  if (params.at) {
1789
2638
  const delay = params.at.getTime() - Date.now();
@@ -1801,75 +2650,116 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
1801
2650
  return this.dispatch(params);
1802
2651
  }
1803
2652
  async getJob(jobId, queue) {
1804
- const job = this.jobsById.get(jobId);
1805
- if (!job) return null;
1806
- if (queue && job.queue !== queue) return null;
1807
- return this.toSearchResult(job);
2653
+ let sql = "SELECT * FROM jobs WHERE id = ?";
2654
+ const params = [jobId];
2655
+ if (queue) {
2656
+ sql += " AND queue = ?";
2657
+ params.push(queue);
2658
+ }
2659
+ const row = this.db.prepare(sql).get(...params);
2660
+ if (!row) return null;
2661
+ return this.rowToSearchResult(row);
1808
2662
  }
1809
2663
  async getJobState(jobId, queue) {
1810
- const job = this.jobsById.get(jobId);
1811
- if (!job) return null;
1812
- if (queue && job.queue !== queue) return null;
1813
- return job.status;
2664
+ let sql = "SELECT status FROM jobs WHERE id = ?";
2665
+ const params = [jobId];
2666
+ if (queue) {
2667
+ sql += " AND queue = ?";
2668
+ params.push(queue);
2669
+ }
2670
+ const row = this.db.prepare(sql).get(...params);
2671
+ return row?.status ?? null;
1814
2672
  }
1815
2673
  async getJobLogs(jobId, queue) {
1816
- const job = this.jobsById.get(jobId);
1817
- if (!job) return [];
1818
- if (queue && job.queue !== queue) return [];
1819
- return job.logs;
2674
+ if (queue) {
2675
+ const job = await this.getJob(jobId, queue);
2676
+ if (!job) return [];
2677
+ }
2678
+ const rows = this.db.prepare("SELECT * FROM job_logs WHERE job_id = ? ORDER BY timestamp ASC").all(jobId);
2679
+ return rows.map((row) => ({
2680
+ timestamp: new Date(row.timestamp),
2681
+ level: row.level,
2682
+ message: row.message
2683
+ }));
1820
2684
  }
1821
2685
  async getJobProgress(jobId, queue) {
1822
- const job = this.jobsById.get(jobId);
1823
- if (!job) return 0;
1824
- if (queue && job.queue !== queue) return 0;
1825
- return job.progress;
2686
+ let sql = "SELECT progress FROM jobs WHERE id = ?";
2687
+ const params = [jobId];
2688
+ if (queue) {
2689
+ sql += " AND queue = ?";
2690
+ params.push(queue);
2691
+ }
2692
+ const row = this.db.prepare(sql).get(...params);
2693
+ return row?.progress ?? 0;
1826
2694
  }
1827
2695
  async retryJob(jobId, queue) {
1828
- const job = this.jobsById.get(jobId);
1829
- if (!job) {
1830
- throw new IgniterJobsError({ code: "JOBS_NOT_FOUND", message: `Job "${jobId}" not found.` });
2696
+ let sql = "SELECT id FROM jobs WHERE id = ?";
2697
+ const checkParams = [jobId];
2698
+ if (queue) {
2699
+ sql += " AND queue = ?";
2700
+ checkParams.push(queue);
1831
2701
  }
1832
- if (queue && job.queue !== queue) {
1833
- throw new IgniterJobsError({ code: "JOBS_NOT_FOUND", message: `Job "${jobId}" not found in queue "${queue}".` });
2702
+ const exists = this.db.prepare(sql).get(...checkParams);
2703
+ if (!exists) {
2704
+ throw new IgniterJobsError({
2705
+ code: "JOBS_NOT_FOUND",
2706
+ message: `Job "${jobId}" not found${queue ? ` in queue "${queue}"` : ""}.`
2707
+ });
1834
2708
  }
1835
- job.status = "waiting";
1836
- job.error = void 0;
1837
- job.completedAt = void 0;
1838
- job.progress = 0;
1839
- void this.kickWorkers(job.queue);
2709
+ let updateSql = "UPDATE jobs SET status = 'waiting', error = NULL, completed_at = NULL, progress = 0 WHERE id = ?";
2710
+ const updateParams = [jobId];
2711
+ if (queue) {
2712
+ updateSql = updateSql.replace("WHERE id = ?", "WHERE id = ? AND queue = ?");
2713
+ updateParams.push(queue);
2714
+ }
2715
+ this.db.prepare(updateSql).run(...updateParams);
1840
2716
  }
1841
2717
  async removeJob(jobId, queue) {
1842
- const job = this.jobsById.get(jobId);
1843
- if (!job) return;
1844
- if (queue && job.queue !== queue) return;
1845
- this.jobsById.delete(jobId);
1846
- const list = this.jobsByQueue.get(job.queue);
1847
- if (list) this.jobsByQueue.set(job.queue, list.filter((id) => id !== jobId));
2718
+ let sql = "DELETE FROM jobs WHERE id = ?";
2719
+ const params = [jobId];
2720
+ if (queue) {
2721
+ sql += " AND queue = ?";
2722
+ params.push(queue);
2723
+ }
2724
+ this.db.prepare(sql).run(...params);
1848
2725
  }
1849
2726
  async promoteJob(jobId, queue) {
1850
- const job = this.jobsById.get(jobId);
1851
- if (!job) {
1852
- throw new IgniterJobsError({ code: "JOBS_NOT_FOUND", message: `Job "${jobId}" not found.` });
2727
+ let sql = "SELECT id, status, queue FROM jobs WHERE id = ?";
2728
+ const checkParams = [jobId];
2729
+ if (queue) {
2730
+ sql += " AND queue = ?";
2731
+ checkParams.push(queue);
1853
2732
  }
1854
- if (queue && job.queue !== queue) {
1855
- throw new IgniterJobsError({ code: "JOBS_NOT_FOUND", message: `Job "${jobId}" not found in queue "${queue}".` });
2733
+ const row = this.db.prepare(sql).get(...checkParams);
2734
+ if (!row) {
2735
+ throw new IgniterJobsError({
2736
+ code: "JOBS_NOT_FOUND",
2737
+ message: `Job "${jobId}" not found${queue ? ` in queue "${queue}"` : ""}.`
2738
+ });
1856
2739
  }
1857
- if (job.status === "delayed" || job.status === "paused") {
1858
- job.status = this.pausedQueues.has(job.queue) ? "paused" : "waiting";
1859
- void this.kickWorkers(job.queue);
2740
+ if (row.status === "delayed" || row.status === "paused") {
2741
+ const newStatus = this.pausedQueues.has(row.queue) ? "paused" : "waiting";
2742
+ this.db.prepare("UPDATE jobs SET status = ?, scheduled_at = NULL WHERE id = ?").run(newStatus, jobId);
1860
2743
  }
1861
2744
  }
1862
2745
  async moveJobToFailed(jobId, reason, queue) {
1863
- const job = this.jobsById.get(jobId);
1864
- if (!job) {
1865
- throw new IgniterJobsError({ code: "JOBS_NOT_FOUND", message: `Job "${jobId}" not found.` });
2746
+ let sql = "SELECT id FROM jobs WHERE id = ?";
2747
+ const checkParams = [jobId];
2748
+ if (queue) {
2749
+ sql += " AND queue = ?";
2750
+ checkParams.push(queue);
1866
2751
  }
1867
- if (queue && job.queue !== queue) {
1868
- throw new IgniterJobsError({ code: "JOBS_NOT_FOUND", message: `Job "${jobId}" not found in queue "${queue}".` });
2752
+ const exists = this.db.prepare(sql).get(...checkParams);
2753
+ if (!exists) {
2754
+ throw new IgniterJobsError({
2755
+ code: "JOBS_NOT_FOUND",
2756
+ message: `Job "${jobId}" not found${queue ? ` in queue "${queue}"` : ""}.`
2757
+ });
1869
2758
  }
1870
- job.status = "failed";
1871
- job.error = reason;
1872
- job.completedAt = /* @__PURE__ */ new Date();
2759
+ this.db.prepare(`
2760
+ UPDATE jobs SET status = 'failed', error = ?, completed_at = ?
2761
+ WHERE id = ?
2762
+ `).run(reason, (/* @__PURE__ */ new Date()).toISOString(), jobId);
1873
2763
  }
1874
2764
  async retryManyJobs(jobIds, queue) {
1875
2765
  await Promise.all(jobIds.map((id) => this.retryJob(id, queue)));
@@ -1886,7 +2776,6 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
1886
2776
  };
1887
2777
  }
1888
2778
  async getQueueJobCounts(queue) {
1889
- const jobIds = this.jobsByQueue.get(queue) ?? [];
1890
2779
  const counts = {
1891
2780
  waiting: 0,
1892
2781
  active: 0,
@@ -1895,132 +2784,131 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
1895
2784
  delayed: 0,
1896
2785
  paused: 0
1897
2786
  };
1898
- for (const id of jobIds) {
1899
- const job = this.jobsById.get(id);
1900
- if (!job) continue;
1901
- if (job.status in counts) {
1902
- counts[job.status]++;
2787
+ const rows = this.db.prepare(
2788
+ "SELECT status, COUNT(*) as count FROM jobs WHERE queue = ? GROUP BY status"
2789
+ ).all(queue);
2790
+ for (const row of rows) {
2791
+ if (row.status in counts) {
2792
+ counts[row.status] = row.count;
1903
2793
  }
1904
2794
  }
1905
2795
  return counts;
1906
2796
  }
1907
2797
  async listQueues() {
1908
- const queues = Array.from(/* @__PURE__ */ new Set([...this.jobsByQueue.keys(), ...this.registeredJobs.keys(), ...this.registeredCrons.keys()]));
2798
+ const jobQueues = this.db.prepare("SELECT DISTINCT queue FROM jobs").all().map((r) => r.queue);
2799
+ const allQueues = /* @__PURE__ */ new Set([
2800
+ ...jobQueues,
2801
+ ...this.registeredJobs.keys(),
2802
+ ...this.registeredCrons.keys()
2803
+ ]);
1909
2804
  const result = [];
1910
- for (const q of queues) {
1911
- result.push(await this.getQueueInfo(q));
2805
+ for (const q of allQueues) {
2806
+ const info = await this.getQueueInfo(q);
2807
+ if (info) result.push(info);
1912
2808
  }
1913
2809
  return result;
1914
2810
  }
1915
2811
  async pauseQueue(queue) {
1916
2812
  this.pausedQueues.add(queue);
1917
- const jobIds = this.jobsByQueue.get(queue) ?? [];
1918
- for (const id of jobIds) {
1919
- const job = this.jobsById.get(id);
1920
- if (!job) continue;
1921
- if (job.status === "waiting") job.status = "paused";
1922
- }
2813
+ this.db.prepare("INSERT OR IGNORE INTO paused_queues (name) VALUES (?)").run(queue);
2814
+ this.db.prepare(
2815
+ "UPDATE jobs SET status = 'paused' WHERE queue = ? AND status = 'waiting'"
2816
+ ).run(queue);
1923
2817
  }
1924
2818
  async resumeQueue(queue) {
1925
2819
  this.pausedQueues.delete(queue);
1926
- const jobIds = this.jobsByQueue.get(queue) ?? [];
1927
- for (const id of jobIds) {
1928
- const job = this.jobsById.get(id);
1929
- if (!job) continue;
1930
- if (job.status === "paused") job.status = "waiting";
1931
- }
1932
- void this.kickWorkers(queue);
2820
+ this.db.prepare("DELETE FROM paused_queues WHERE name = ?").run(queue);
2821
+ this.db.prepare(
2822
+ "UPDATE jobs SET status = 'waiting' WHERE queue = ? AND status = 'paused'"
2823
+ ).run(queue);
1933
2824
  }
1934
2825
  async drainQueue(queue) {
1935
- const jobIds = this.jobsByQueue.get(queue) ?? [];
1936
- let removed = 0;
1937
- for (const id of jobIds) {
1938
- const job = this.jobsById.get(id);
1939
- if (!job) continue;
1940
- if (job.status === "waiting" || job.status === "paused") {
1941
- this.jobsById.delete(id);
1942
- removed++;
1943
- }
1944
- }
1945
- this.jobsByQueue.set(queue, jobIds.filter((id) => this.jobsById.has(id)));
1946
- return removed;
2826
+ const result = this.db.prepare(
2827
+ "DELETE FROM jobs WHERE queue = ? AND status IN ('waiting', 'paused')"
2828
+ ).run(queue);
2829
+ return result.changes;
1947
2830
  }
1948
2831
  async cleanQueue(queue, options) {
1949
2832
  const statuses = Array.isArray(options.status) ? options.status : [options.status];
1950
2833
  const olderThan = options.olderThan ?? 0;
1951
2834
  const limit = options.limit ?? Number.POSITIVE_INFINITY;
1952
- const jobIds = this.jobsByQueue.get(queue) ?? [];
1953
- const now = Date.now();
1954
- let cleaned = 0;
1955
- for (const id of [...jobIds]) {
1956
- if (cleaned >= limit) break;
1957
- const job = this.jobsById.get(id);
1958
- if (!job) continue;
1959
- if (!statuses.includes(job.status)) continue;
1960
- const ageMs = now - job.createdAt.getTime();
1961
- if (ageMs < olderThan) continue;
1962
- this.jobsById.delete(id);
1963
- cleaned++;
1964
- }
1965
- this.jobsByQueue.set(queue, jobIds.filter((id) => this.jobsById.has(id)));
1966
- return cleaned;
2835
+ const cutoffTime = new Date(Date.now() - olderThan).toISOString();
2836
+ const statusPlaceholders = statuses.map(() => "?").join(", ");
2837
+ let sql = `
2838
+ DELETE FROM jobs WHERE id IN (
2839
+ SELECT id FROM jobs
2840
+ WHERE queue = ? AND status IN (${statusPlaceholders}) AND created_at < ?
2841
+ ORDER BY created_at ASC
2842
+ LIMIT ?
2843
+ )
2844
+ `;
2845
+ const result = this.db.prepare(sql).run(
2846
+ queue,
2847
+ ...statuses,
2848
+ cutoffTime,
2849
+ limit === Number.POSITIVE_INFINITY ? -1 : limit
2850
+ );
2851
+ return result.changes;
1967
2852
  }
1968
- async obliterateQueue(queue, options) {
1969
- const jobIds = this.jobsByQueue.get(queue) ?? [];
1970
- for (const id of jobIds) this.jobsById.delete(id);
1971
- this.jobsByQueue.delete(queue);
2853
+ async obliterateQueue(queue, _options) {
2854
+ this.db.prepare("DELETE FROM jobs WHERE queue = ?").run(queue);
1972
2855
  this.registeredJobs.delete(queue);
1973
2856
  this.registeredCrons.delete(queue);
1974
2857
  this.pausedQueues.delete(queue);
2858
+ this.db.prepare("DELETE FROM paused_queues WHERE name = ?").run(queue);
1975
2859
  }
1976
2860
  async retryAllInQueue(queue) {
1977
- const jobIds = this.jobsByQueue.get(queue) ?? [];
1978
- let retried = 0;
1979
- for (const id of jobIds) {
1980
- const job = this.jobsById.get(id);
1981
- if (!job) continue;
1982
- if (job.status === "failed") {
1983
- await this.retryJob(id, queue);
1984
- retried++;
1985
- }
1986
- }
1987
- return retried;
2861
+ const result = this.db.prepare(`
2862
+ UPDATE jobs SET status = 'waiting', error = NULL, completed_at = NULL, progress = 0
2863
+ WHERE queue = ? AND status = 'failed'
2864
+ `).run(queue);
2865
+ return result.changes;
1988
2866
  }
1989
2867
  async pauseJobType(queue, jobName) {
1990
- const jobIds = this.jobsByQueue.get(queue) ?? [];
1991
- for (const id of jobIds) {
1992
- const job = this.jobsById.get(id);
1993
- if (!job) continue;
1994
- if (job.name === jobName && job.status === "waiting") job.status = "paused";
1995
- }
2868
+ this.db.prepare(
2869
+ "UPDATE jobs SET status = 'paused' WHERE queue = ? AND name = ? AND status = 'waiting'"
2870
+ ).run(queue, jobName);
1996
2871
  }
1997
2872
  async resumeJobType(queue, jobName) {
1998
- const jobIds = this.jobsByQueue.get(queue) ?? [];
1999
- for (const id of jobIds) {
2000
- const job = this.jobsById.get(id);
2001
- if (!job) continue;
2002
- if (job.name === jobName && job.status === "paused") job.status = "waiting";
2003
- }
2004
- void this.kickWorkers(queue);
2873
+ this.db.prepare(
2874
+ "UPDATE jobs SET status = 'waiting' WHERE queue = ? AND name = ? AND status = 'paused'"
2875
+ ).run(queue, jobName);
2005
2876
  }
2006
2877
  async searchJobs(filter) {
2007
2878
  const queue = filter?.queue;
2008
2879
  const statuses = filter?.status;
2009
2880
  const limit = filter?.limit ?? 100;
2010
2881
  const offset = filter?.offset ?? 0;
2011
- const all = Array.from(this.jobsById.values()).filter((j) => queue ? j.queue === queue : true).filter((j) => statuses ? statuses.includes(j.status) : true).sort((a, b) => b.priority - a.priority || a.createdAt.getTime() - b.createdAt.getTime());
2012
- return all.slice(offset, offset + limit).map((j) => this.toSearchResult(j));
2882
+ let sql = "SELECT * FROM jobs WHERE 1=1";
2883
+ const params = [];
2884
+ if (queue) {
2885
+ sql += " AND queue = ?";
2886
+ params.push(queue);
2887
+ }
2888
+ if (statuses && statuses.length > 0) {
2889
+ const placeholders = statuses.map(() => "?").join(", ");
2890
+ sql += ` AND status IN (${placeholders})`;
2891
+ params.push(...statuses);
2892
+ }
2893
+ sql += " ORDER BY priority DESC, created_at ASC LIMIT ? OFFSET ?";
2894
+ params.push(limit, offset);
2895
+ const rows = this.db.prepare(sql).all(...params);
2896
+ return rows.map((row) => this.rowToSearchResult(row));
2013
2897
  }
2014
2898
  async searchQueues(filter) {
2015
2899
  const name = filter?.name;
2016
2900
  const isPaused = filter?.isPaused;
2017
2901
  const all = await this.listQueues();
2018
- return all.filter((q) => name ? q.name.includes(name) : true).filter((q) => typeof isPaused === "boolean" ? q.isPaused === isPaused : true);
2902
+ return all.filter((q) => name ? q.name.includes(name) : true).filter(
2903
+ (q) => typeof isPaused === "boolean" ? q.isPaused === isPaused : true
2904
+ );
2019
2905
  }
2020
2906
  async searchWorkers(filter) {
2021
2907
  const queue = filter?.queue;
2022
2908
  const isRunning = filter?.isRunning;
2023
- return Array.from(this.workers.values()).filter((w) => queue ? w.queues.includes(queue) : true).filter((w) => typeof isRunning === "boolean" ? isRunning ? !w.closed : w.closed : true).map((w) => this.toWorkerHandle(w));
2909
+ return Array.from(this.workers.values()).filter((w) => queue ? w.queues.includes(queue) : true).filter(
2910
+ (w) => typeof isRunning === "boolean" ? isRunning ? !w.closed : w.closed : true
2911
+ ).map((w) => this.toWorkerHandle(w));
2024
2912
  }
2025
2913
  async createWorker(config) {
2026
2914
  const workerId = IgniterJobsIdGenerator.generate("worker");
@@ -2032,15 +2920,18 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
2032
2920
  closed: false,
2033
2921
  startedAt: /* @__PURE__ */ new Date(),
2034
2922
  metrics: { processed: 0, failed: 0, totalDuration: 0 },
2035
- handlers: config.handlers
2923
+ handlers: config.handlers,
2924
+ activeJobs: 0
2036
2925
  };
2037
2926
  this.workers.set(workerId, state);
2038
- for (const q of state.queues) void this.kickWorkers(q);
2927
+ this.startPollingLoop(state);
2039
2928
  return this.toWorkerHandle(state);
2040
2929
  }
2041
2930
  getWorkers() {
2042
2931
  const out = /* @__PURE__ */ new Map();
2043
- for (const [id, state] of this.workers) out.set(id, this.toWorkerHandle(state));
2932
+ for (const [id, state] of this.workers) {
2933
+ out.set(id, this.toWorkerHandle(state));
2934
+ }
2044
2935
  return out;
2045
2936
  }
2046
2937
  async publishEvent(channel, payload) {
@@ -2060,26 +2951,36 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
2060
2951
  };
2061
2952
  }
2062
2953
  async shutdown() {
2954
+ for (const worker of this.workers.values()) {
2955
+ worker.closed = true;
2956
+ if (worker.pollingTimer) {
2957
+ clearInterval(worker.pollingTimer);
2958
+ }
2959
+ }
2063
2960
  this.workers.clear();
2064
2961
  this.subscribers.clear();
2962
+ this.db.close();
2065
2963
  }
2066
- toSearchResult(job) {
2964
+ // ─────────────────────────────────────────────────────────────────────────────
2965
+ // Private helpers
2966
+ // ─────────────────────────────────────────────────────────────────────────────
2967
+ rowToSearchResult(row) {
2067
2968
  return {
2068
- id: job.id,
2069
- name: job.name,
2070
- queue: job.queue,
2071
- status: job.status,
2072
- input: job.input,
2073
- result: job.result,
2074
- error: job.error,
2075
- progress: job.progress,
2076
- attemptsMade: job.attemptsMade,
2077
- priority: job.priority,
2078
- createdAt: job.createdAt,
2079
- startedAt: job.startedAt,
2080
- completedAt: job.completedAt,
2081
- metadata: job.metadata,
2082
- scope: job.scope
2969
+ id: row.id,
2970
+ name: row.name,
2971
+ queue: row.queue,
2972
+ status: row.status,
2973
+ input: JSON.parse(row.input),
2974
+ result: row.result ? JSON.parse(row.result) : void 0,
2975
+ error: row.error ?? void 0,
2976
+ progress: row.progress,
2977
+ attemptsMade: row.attempts_made,
2978
+ priority: row.priority,
2979
+ createdAt: new Date(row.created_at),
2980
+ startedAt: row.started_at ? new Date(row.started_at) : void 0,
2981
+ completedAt: row.completed_at ? new Date(row.completed_at) : void 0,
2982
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
2983
+ scope: row.scope ? JSON.parse(row.scope) : void 0
2083
2984
  };
2084
2985
  }
2085
2986
  toWorkerHandle(worker) {
@@ -2091,10 +2992,12 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
2091
2992
  },
2092
2993
  resume: async () => {
2093
2994
  worker.paused = false;
2094
- for (const q of worker.queues) void this.kickWorkers(q);
2095
2995
  },
2096
2996
  close: async () => {
2097
2997
  worker.closed = true;
2998
+ if (worker.pollingTimer) {
2999
+ clearInterval(worker.pollingTimer);
3000
+ }
2098
3001
  },
2099
3002
  isRunning: () => !worker.closed && !worker.paused,
2100
3003
  isPaused: () => worker.paused,
@@ -2113,150 +3016,167 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
2113
3016
  uptime
2114
3017
  };
2115
3018
  }
2116
- async kickWorkers(queue) {
2117
- if (this.pausedQueues.has(queue)) return;
2118
- const relevant = Array.from(this.workers.values()).filter((w) => !w.closed && !w.paused && (w.queues.length === 0 || w.queues.includes(queue)));
2119
- if (relevant.length === 0) return;
2120
- for (const w of relevant) {
2121
- void this.processLoop(w, queue);
2122
- }
3019
+ startPollingLoop(worker) {
3020
+ const poll = () => {
3021
+ if (worker.closed || worker.paused) return;
3022
+ void this.processNextJobs(worker);
3023
+ };
3024
+ poll();
3025
+ worker.pollingTimer = setInterval(poll, this.options.pollingInterval);
2123
3026
  }
2124
- async processLoop(worker, queue) {
3027
+ async processNextJobs(worker) {
2125
3028
  if (worker.closed || worker.paused) return;
2126
- const concurrency = Math.max(1, worker.concurrency);
2127
- const running = worker.__running;
2128
- const currentRunning = running ?? 0;
2129
- if (currentRunning >= concurrency) return;
2130
- worker.__running = currentRunning + 1;
2131
- try {
2132
- const next = this.nextJob(queue);
2133
- if (!next) return;
2134
- await this.processJob(worker, next);
2135
- } finally {
2136
- worker.__running = worker.__running - 1;
2137
- if (this.nextJob(queue)) void this.processLoop(worker, queue);
2138
- else if (worker.handlers?.onIdle) await worker.handlers.onIdle();
3029
+ const availableSlots = worker.concurrency - worker.activeJobs;
3030
+ if (availableSlots <= 0) return;
3031
+ const queueFilter = worker.queues.length > 0 ? `queue IN (${worker.queues.map(() => "?").join(", ")})` : "1=1";
3032
+ const params = worker.queues.length > 0 ? [...worker.queues, availableSlots] : [availableSlots];
3033
+ const rows = this.db.prepare(`
3034
+ SELECT * FROM jobs
3035
+ WHERE status = 'waiting' AND ${queueFilter}
3036
+ ORDER BY priority DESC, created_at ASC
3037
+ LIMIT ?
3038
+ `).all(...params);
3039
+ for (const row of rows) {
3040
+ if (worker.closed || worker.paused) break;
3041
+ if (worker.activeJobs >= worker.concurrency) break;
3042
+ const claimed = this.db.prepare(`
3043
+ UPDATE jobs SET status = 'active', started_at = ?
3044
+ WHERE id = ? AND status = 'waiting'
3045
+ `).run((/* @__PURE__ */ new Date()).toISOString(), row.id);
3046
+ if (claimed.changes === 0) continue;
3047
+ worker.activeJobs++;
3048
+ void this.processJob(worker, row.id).finally(() => {
3049
+ worker.activeJobs--;
3050
+ if (worker.handlers?.onIdle && worker.activeJobs === 0) {
3051
+ void worker.handlers.onIdle();
3052
+ }
3053
+ });
2139
3054
  }
2140
3055
  }
2141
- nextJob(queue) {
2142
- const ids = this.jobsByQueue.get(queue) ?? [];
2143
- const candidates = ids.map((id) => this.jobsById.get(id)).filter((j) => Boolean(j)).filter((j) => j.status === "waiting").sort((a, b) => b.priority - a.priority || a.createdAt.getTime() - b.createdAt.getTime());
2144
- return candidates[0] ?? null;
2145
- }
2146
- async processJob(worker, job) {
2147
- if (this.pausedQueues.has(job.queue)) {
2148
- job.status = "paused";
2149
- return;
3056
+ async processJob(worker, jobId) {
3057
+ const row = this.db.prepare("SELECT * FROM jobs WHERE id = ?").get(jobId);
3058
+ if (!row) return;
3059
+ const job = this.rowToSearchResult(row);
3060
+ this.addJobLog(jobId, "info", "Job started");
3061
+ if (worker.handlers?.onActive) {
3062
+ await worker.handlers.onActive({ job });
2150
3063
  }
2151
- job.status = "active";
2152
- job.startedAt = /* @__PURE__ */ new Date();
2153
- job.attemptsMade += 1;
2154
- job.logs.push({ timestamp: /* @__PURE__ */ new Date(), level: "info", message: "Job started" });
2155
- if (worker.handlers?.onActive) await worker.handlers.onActive({ job: this.toSearchResult(job) });
2156
3064
  const start = Date.now();
2157
3065
  try {
2158
- const definition = this.registeredJobs.get(job.queue)?.get(job.name);
3066
+ const definition = this.registeredJobs.get(row.queue)?.get(row.name);
2159
3067
  if (!definition) {
2160
3068
  throw new IgniterJobsError({
2161
3069
  code: "JOBS_NOT_REGISTERED",
2162
- message: `Job "${job.name}" is not registered for queue "${job.queue}".`
3070
+ message: `Job "${row.name}" is not registered for queue "${row.queue}".`
2163
3071
  });
2164
3072
  }
3073
+ this.db.prepare("UPDATE jobs SET attempts_made = attempts_made + 1 WHERE id = ?").run(jobId);
2165
3074
  if (definition.onStart) {
2166
3075
  await definition.onStart({
2167
3076
  input: job.input,
2168
3077
  context: {},
2169
- job: { id: job.id, name: job.name, queue: job.queue, attemptsMade: job.attemptsMade, metadata: job.metadata },
3078
+ job: {
3079
+ id: job.id,
3080
+ name: job.name,
3081
+ queue: job.queue,
3082
+ attemptsMade: job.attemptsMade + 1,
3083
+ metadata: job.metadata
3084
+ },
2170
3085
  scope: job.scope,
2171
- startedAt: job.startedAt
3086
+ startedAt: /* @__PURE__ */ new Date()
2172
3087
  });
2173
3088
  }
2174
3089
  const result = await definition.handler({
2175
3090
  input: job.input,
2176
3091
  context: {},
2177
- job: { id: job.id, name: job.name, queue: job.queue, attemptsMade: job.attemptsMade, metadata: job.metadata },
3092
+ job: {
3093
+ id: job.id,
3094
+ name: job.name,
3095
+ queue: job.queue,
3096
+ attemptsMade: job.attemptsMade + 1,
3097
+ metadata: job.metadata
3098
+ },
2178
3099
  scope: job.scope
2179
3100
  });
2180
3101
  const duration = Date.now() - start;
2181
- job.status = "completed";
2182
- job.completedAt = /* @__PURE__ */ new Date();
2183
- job.result = result;
2184
- job.progress = 100;
2185
- job.logs.push({ timestamp: /* @__PURE__ */ new Date(), level: "info", message: `Job completed in ${duration}ms` });
2186
- worker.metrics.processed += 1;
3102
+ this.db.prepare(`
3103
+ UPDATE jobs SET status = 'completed', completed_at = ?, result = ?, progress = 100
3104
+ WHERE id = ?
3105
+ `).run((/* @__PURE__ */ new Date()).toISOString(), JSON.stringify(result), jobId);
3106
+ this.addJobLog(jobId, "info", `Job completed in ${duration}ms`);
3107
+ worker.metrics.processed++;
2187
3108
  worker.metrics.totalDuration += duration;
2188
3109
  if (definition.onSuccess) {
2189
3110
  await definition.onSuccess({
2190
3111
  input: job.input,
2191
3112
  context: {},
2192
- job: { id: job.id, name: job.name, queue: job.queue, attemptsMade: job.attemptsMade, metadata: job.metadata },
3113
+ job: {
3114
+ id: job.id,
3115
+ name: job.name,
3116
+ queue: job.queue,
3117
+ attemptsMade: job.attemptsMade + 1,
3118
+ metadata: job.metadata
3119
+ },
2193
3120
  scope: job.scope,
2194
3121
  result,
2195
3122
  duration
2196
3123
  });
2197
3124
  }
2198
- if (worker.handlers?.onSuccess) await worker.handlers.onSuccess({ job: this.toSearchResult(job), result });
3125
+ if (worker.handlers?.onSuccess) {
3126
+ const updatedJob = await this.getJob(jobId);
3127
+ if (updatedJob) {
3128
+ await worker.handlers.onSuccess({ job: updatedJob, result });
3129
+ }
3130
+ }
2199
3131
  } catch (error) {
2200
- job.error = error?.message ?? String(error);
2201
- job.logs.push({ timestamp: /* @__PURE__ */ new Date(), level: "error", message: job.error ?? "Unknown error" });
2202
- const isFinalAttempt = job.attemptsMade >= job.maxAttempts;
3132
+ const errorMessage = error?.message ?? String(error);
3133
+ this.addJobLog(jobId, "error", errorMessage);
3134
+ const current = this.db.prepare(
3135
+ "SELECT attempts_made, max_attempts FROM jobs WHERE id = ?"
3136
+ ).get(jobId);
3137
+ const isFinalAttempt = (current?.attempts_made ?? 0) >= (current?.max_attempts ?? 1);
2203
3138
  if (isFinalAttempt) {
2204
- job.status = "failed";
2205
- job.completedAt = /* @__PURE__ */ new Date();
2206
- worker.metrics.failed += 1;
2207
- const definition = this.registeredJobs.get(job.queue)?.get(job.name);
3139
+ this.db.prepare(`
3140
+ UPDATE jobs SET status = 'failed', error = ?, completed_at = ?
3141
+ WHERE id = ?
3142
+ `).run(errorMessage, (/* @__PURE__ */ new Date()).toISOString(), jobId);
3143
+ worker.metrics.failed++;
3144
+ const definition = this.registeredJobs.get(row.queue)?.get(row.name);
2208
3145
  if (definition?.onFailure) {
2209
3146
  await definition.onFailure({
2210
3147
  input: job.input,
2211
3148
  context: {},
2212
- job: { id: job.id, name: job.name, queue: job.queue, attemptsMade: job.attemptsMade, metadata: job.metadata },
3149
+ job: {
3150
+ id: job.id,
3151
+ name: job.name,
3152
+ queue: job.queue,
3153
+ attemptsMade: current?.attempts_made ?? 1,
3154
+ metadata: job.metadata
3155
+ },
2213
3156
  scope: job.scope,
2214
3157
  error,
2215
3158
  isFinalAttempt: true
2216
3159
  });
2217
3160
  }
2218
- if (worker.handlers?.onFailure) await worker.handlers.onFailure({ job: this.toSearchResult(job), error });
3161
+ if (worker.handlers?.onFailure) {
3162
+ const updatedJob = await this.getJob(jobId);
3163
+ if (updatedJob) {
3164
+ await worker.handlers.onFailure({ job: updatedJob, error });
3165
+ }
3166
+ }
2219
3167
  } else {
2220
- job.status = "waiting";
2221
- void this.kickWorkers(job.queue);
3168
+ this.db.prepare("UPDATE jobs SET status = 'waiting' WHERE id = ?").run(jobId);
2222
3169
  }
2223
3170
  }
2224
3171
  }
2225
- };
2226
-
2227
- // src/telemetry/jobs.telemetry.ts
2228
- var IgniterJobsTelemetryEvents = {
2229
- namespace: "igniter.jobs",
2230
- events: {
2231
- // Job lifecycle events
2232
- job: {
2233
- enqueued: {},
2234
- started: {},
2235
- completed: {},
2236
- failed: {},
2237
- progress: {},
2238
- retrying: {},
2239
- scheduled: {}
2240
- },
2241
- // Worker lifecycle events
2242
- worker: {
2243
- started: {},
2244
- stopped: {},
2245
- idle: {},
2246
- paused: {},
2247
- resumed: {}
2248
- },
2249
- // Queue management events
2250
- queue: {
2251
- paused: {},
2252
- resumed: {},
2253
- drained: {},
2254
- cleaned: {},
2255
- obliterated: {}
2256
- }
3172
+ addJobLog(jobId, level, message) {
3173
+ this.db.prepare(`
3174
+ INSERT INTO job_logs (job_id, timestamp, level, message)
3175
+ VALUES (?, ?, ?, ?)
3176
+ `).run(jobId, (/* @__PURE__ */ new Date()).toISOString(), level, message);
2257
3177
  }
2258
3178
  };
2259
3179
 
2260
- export { IGNITER_JOBS_ERROR_CODES, IgniterJobs, IgniterJobsBuilder, IgniterJobsBullMQAdapter, IgniterJobsError, IgniterJobsMemoryAdapter, IgniterJobsTelemetryEvents, IgniterQueue, IgniterQueueBuilder, IgniterWorkerBuilder };
3180
+ export { IGNITER_JOBS_ERROR_CODES, IgniterJobs, IgniterJobsBuilder, IgniterJobsBullMQAdapter, IgniterJobsError, IgniterJobsManager, IgniterJobsMemoryAdapter, IgniterJobsSQLiteAdapter, IgniterQueue, IgniterQueueBuilder, IgniterWorkerBuilder };
2261
3181
  //# sourceMappingURL=index.mjs.map
2262
3182
  //# sourceMappingURL=index.mjs.map