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