@digilogiclabs/platform-core 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/migrate.js CHANGED
File without changes
@@ -1,5 +1,5 @@
1
- import { I as IPlatform, M as MemoryDatabase, a as MemoryCache, b as MemoryStorage, c as MemoryEmail, d as MemoryQueue, e as IDatabase } from './ConsoleEmail-XeUBAnwc.mjs';
2
- export { C as ConsoleEmail, h as ConsoleLogger, t as EmailMessage, E as EnvSecrets, l as ICache, n as IEmail, p as ILogger, q as IMetrics, k as IQueryBuilder, o as IQueue, r as ISecrets, m as IStorage, J as Job, s as JobOptions, i as MemoryMetrics, f as MemorySecrets, N as NoopLogger, j as NoopMetrics, Q as QueryResult, S as StorageFile, g as createPlatform } from './ConsoleEmail-XeUBAnwc.mjs';
1
+ import { I as IPlatform, M as MemoryDatabase, a as MemoryCache, b as MemoryStorage, c as MemoryEmail, d as MemoryQueue, e as IDatabase } from './ConsoleEmail-Drr30nct.mjs';
2
+ export { C as ConsoleEmail, h as ConsoleLogger, t as EmailMessage, E as EnvSecrets, l as ICache, n as IEmail, p as ILogger, q as IMetrics, k as IQueryBuilder, o as IQueue, r as ISecrets, m as IStorage, J as Job, s as JobOptions, i as MemoryMetrics, f as MemorySecrets, N as NoopLogger, j as NoopMetrics, Q as QueryResult, S as StorageFile, g as createPlatform } from './ConsoleEmail-Drr30nct.mjs';
3
3
  import 'zod';
4
4
 
5
5
  /**
package/dist/testing.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { I as IPlatform, M as MemoryDatabase, a as MemoryCache, b as MemoryStorage, c as MemoryEmail, d as MemoryQueue, e as IDatabase } from './ConsoleEmail-XeUBAnwc.js';
2
- export { C as ConsoleEmail, h as ConsoleLogger, t as EmailMessage, E as EnvSecrets, l as ICache, n as IEmail, p as ILogger, q as IMetrics, k as IQueryBuilder, o as IQueue, r as ISecrets, m as IStorage, J as Job, s as JobOptions, i as MemoryMetrics, f as MemorySecrets, N as NoopLogger, j as NoopMetrics, Q as QueryResult, S as StorageFile, g as createPlatform } from './ConsoleEmail-XeUBAnwc.js';
1
+ import { I as IPlatform, M as MemoryDatabase, a as MemoryCache, b as MemoryStorage, c as MemoryEmail, d as MemoryQueue, e as IDatabase } from './ConsoleEmail-Drr30nct.js';
2
+ export { C as ConsoleEmail, h as ConsoleLogger, t as EmailMessage, E as EnvSecrets, l as ICache, n as IEmail, p as ILogger, q as IMetrics, k as IQueryBuilder, o as IQueue, r as ISecrets, m as IStorage, J as Job, s as JobOptions, i as MemoryMetrics, f as MemorySecrets, N as NoopLogger, j as NoopMetrics, Q as QueryResult, S as StorageFile, g as createPlatform } from './ConsoleEmail-Drr30nct.js';
3
3
  import 'zod';
4
4
 
5
5
  /**
package/dist/testing.js CHANGED
@@ -374,65 +374,631 @@ var MemoryEmail = class {
374
374
  }
375
375
  };
376
376
 
377
+ // src/interfaces/IQueue.ts
378
+ function calculateBackoff(attempt, options) {
379
+ if (options.type === "fixed") {
380
+ return options.delay;
381
+ }
382
+ const delay = options.delay * Math.pow(2, attempt - 1);
383
+ const maxDelay = options.maxDelay ?? options.delay * 32;
384
+ return Math.min(delay, maxDelay);
385
+ }
386
+ function generateJobId() {
387
+ const timestamp = Date.now().toString(36);
388
+ const random = Math.random().toString(36).substring(2, 10);
389
+ return `job_${timestamp}_${random}`;
390
+ }
391
+
392
+ // src/context/CorrelationContext.ts
393
+ var import_async_hooks = require("async_hooks");
394
+ var CorrelationContextManager = class {
395
+ storage = new import_async_hooks.AsyncLocalStorage();
396
+ idGenerator;
397
+ constructor() {
398
+ this.idGenerator = () => {
399
+ const timestamp = Date.now().toString(36);
400
+ const random = Math.random().toString(36).substring(2, 10);
401
+ return `${timestamp}-${random}`;
402
+ };
403
+ }
404
+ /**
405
+ * Set custom ID generator
406
+ */
407
+ setIdGenerator(generator) {
408
+ this.idGenerator = generator;
409
+ }
410
+ /**
411
+ * Generate a new ID
412
+ */
413
+ generateId() {
414
+ return this.idGenerator();
415
+ }
416
+ /**
417
+ * Run a function with correlation context
418
+ */
419
+ run(data, fn) {
420
+ return this.storage.run(
421
+ {
422
+ ...data,
423
+ startTime: data.startTime ?? Date.now()
424
+ },
425
+ fn
426
+ );
427
+ }
428
+ /**
429
+ * Run an async function with correlation context
430
+ */
431
+ async runAsync(data, fn) {
432
+ return this.storage.run(
433
+ {
434
+ ...data,
435
+ startTime: data.startTime ?? Date.now()
436
+ },
437
+ fn
438
+ );
439
+ }
440
+ /**
441
+ * Get current correlation data
442
+ */
443
+ get() {
444
+ return this.storage.getStore();
445
+ }
446
+ /**
447
+ * Get current correlation data or empty object
448
+ */
449
+ getOrEmpty() {
450
+ return this.storage.getStore() ?? {};
451
+ }
452
+ /**
453
+ * Get trace ID from current context
454
+ */
455
+ getTraceId() {
456
+ return this.get()?.traceId;
457
+ }
458
+ /**
459
+ * Get correlation ID from current context
460
+ */
461
+ getCorrelationId() {
462
+ return this.get()?.correlationId ?? this.get()?.traceId;
463
+ }
464
+ /**
465
+ * Get request ID from current context
466
+ */
467
+ getRequestId() {
468
+ return this.get()?.requestId;
469
+ }
470
+ /**
471
+ * Get user ID from current context
472
+ */
473
+ getUserId() {
474
+ return this.get()?.userId;
475
+ }
476
+ /**
477
+ * Get tenant ID from current context
478
+ */
479
+ getTenantId() {
480
+ return this.get()?.tenantId;
481
+ }
482
+ /**
483
+ * Check if we're in a correlation context
484
+ */
485
+ isInContext() {
486
+ return this.storage.getStore() !== void 0;
487
+ }
488
+ /**
489
+ * Update current context (merge data)
490
+ * Note: This doesn't actually update the store, but returns merged data
491
+ * for use in nested contexts
492
+ */
493
+ extend(data) {
494
+ const current = this.get() ?? {};
495
+ return {
496
+ ...current,
497
+ ...data,
498
+ metadata: {
499
+ ...current.metadata,
500
+ ...data.metadata
501
+ }
502
+ };
503
+ }
504
+ /**
505
+ * Run a nested context with extended data
506
+ */
507
+ runNested(data, fn) {
508
+ return this.run(this.extend(data), fn);
509
+ }
510
+ /**
511
+ * Run a nested async context with extended data
512
+ */
513
+ async runNestedAsync(data, fn) {
514
+ return this.runAsync(this.extend(data), fn);
515
+ }
516
+ /**
517
+ * Get context as log metadata (for structured logging)
518
+ */
519
+ getLogMeta() {
520
+ const ctx = this.get();
521
+ if (!ctx) return {};
522
+ const meta = {};
523
+ if (ctx.traceId) meta.traceId = ctx.traceId;
524
+ if (ctx.spanId) meta.spanId = ctx.spanId;
525
+ if (ctx.requestId) meta.requestId = ctx.requestId;
526
+ if (ctx.correlationId) meta.correlationId = ctx.correlationId;
527
+ if (ctx.userId) meta.userId = ctx.userId;
528
+ if (ctx.tenantId) meta.tenantId = ctx.tenantId;
529
+ if (ctx.sessionId) meta.sessionId = ctx.sessionId;
530
+ if (ctx.operation) meta.operation = ctx.operation;
531
+ return meta;
532
+ }
533
+ /**
534
+ * Get context as HTTP headers (for propagation)
535
+ */
536
+ getHeaders() {
537
+ const ctx = this.get();
538
+ if (!ctx) return {};
539
+ const headers = {};
540
+ if (ctx.traceId) {
541
+ const spanId = ctx.spanId ?? this.generateId().substring(0, 16);
542
+ headers["traceparent"] = `00-${ctx.traceId}-${spanId}-01`;
543
+ }
544
+ if (ctx.requestId) {
545
+ headers["x-request-id"] = ctx.requestId;
546
+ }
547
+ if (ctx.correlationId) {
548
+ headers["x-correlation-id"] = ctx.correlationId;
549
+ }
550
+ return headers;
551
+ }
552
+ /**
553
+ * Parse context from HTTP headers
554
+ */
555
+ parseHeaders(headers) {
556
+ const data = {};
557
+ const traceparent = headers["traceparent"];
558
+ if (traceparent && typeof traceparent === "string") {
559
+ const parts = traceparent.split("-");
560
+ if (parts.length >= 3 && parts[0] === "00") {
561
+ data.traceId = parts[1];
562
+ data.spanId = parts[2];
563
+ }
564
+ }
565
+ const requestId = headers["x-request-id"];
566
+ if (requestId) {
567
+ data.requestId = Array.isArray(requestId) ? requestId[0] : requestId;
568
+ }
569
+ const correlationId = headers["x-correlation-id"];
570
+ if (correlationId) {
571
+ data.correlationId = Array.isArray(correlationId) ? correlationId[0] : correlationId;
572
+ }
573
+ return data;
574
+ }
575
+ /**
576
+ * Get elapsed time since context start (ms)
577
+ */
578
+ getElapsed() {
579
+ const ctx = this.get();
580
+ if (!ctx?.startTime) return 0;
581
+ return Date.now() - ctx.startTime;
582
+ }
583
+ /**
584
+ * Create a child span context
585
+ */
586
+ createChildSpan(operation) {
587
+ const current = this.get() ?? {};
588
+ return {
589
+ ...current,
590
+ spanId: this.generateId().substring(0, 16),
591
+ operation,
592
+ startTime: Date.now()
593
+ };
594
+ }
595
+ };
596
+ var correlationContext = new CorrelationContextManager();
597
+ var runWithContext = correlationContext.run.bind(correlationContext);
598
+ var runWithContextAsync = correlationContext.runAsync.bind(correlationContext);
599
+ var getContext = correlationContext.get.bind(correlationContext);
600
+ var getTraceId = correlationContext.getTraceId.bind(correlationContext);
601
+ var getCorrelationId = correlationContext.getCorrelationId.bind(correlationContext);
602
+ var getRequestId = correlationContext.getRequestId.bind(correlationContext);
603
+ var getUserId = correlationContext.getUserId.bind(correlationContext);
604
+ var getTenantId = correlationContext.getTenantId.bind(correlationContext);
605
+ var getLogMeta = correlationContext.getLogMeta.bind(correlationContext);
606
+ var isInContext = correlationContext.isInContext.bind(correlationContext);
607
+ function createJobContext(job) {
608
+ return {
609
+ correlationId: job.correlationId ?? job.id,
610
+ traceId: correlationContext.generateId(),
611
+ operation: `job:${job.name}`,
612
+ metadata: job.metadata,
613
+ startTime: Date.now()
614
+ };
615
+ }
616
+
377
617
  // src/adapters/memory/MemoryQueue.ts
378
618
  var MemoryQueue = class {
379
- jobs = [];
619
+ jobs = /* @__PURE__ */ new Map();
380
620
  handlers = [];
621
+ eventHandlers = /* @__PURE__ */ new Map();
622
+ paused = false;
623
+ processingConcurrency = 1;
624
+ activeCount = 0;
625
+ repeatIntervals = /* @__PURE__ */ new Map();
626
+ queueName;
627
+ processingQueue = [];
628
+ isProcessing = false;
629
+ constructor(name = "default") {
630
+ this.queueName = name;
631
+ }
632
+ // ═══════════════════════════════════════════════════════════════
633
+ // CORE METHODS (IQueue interface)
634
+ // ═══════════════════════════════════════════════════════════════
381
635
  async add(name, data, options) {
382
- const job = { id: "job_" + this.jobs.length, name, data, attemptsMade: 0, progress: 0, timestamp: Date.now() };
383
- this.jobs.push(job);
384
- this.handlers.forEach((h) => h(job));
385
- return job;
636
+ const jobId = options?.jobId || generateJobId();
637
+ const now = Date.now();
638
+ const job = {
639
+ id: jobId,
640
+ name,
641
+ data,
642
+ attemptsMade: 0,
643
+ progress: 0,
644
+ timestamp: now,
645
+ state: options?.delay ? "delayed" : "waiting",
646
+ correlationId: options?.correlationId,
647
+ metadata: options?.metadata,
648
+ options
649
+ };
650
+ this.jobs.set(jobId, job);
651
+ if (options?.delay && options.delay > 0) {
652
+ setTimeout(() => {
653
+ const j = this.jobs.get(jobId);
654
+ if (j && j.state === "delayed") {
655
+ j.state = "waiting";
656
+ this.processNext();
657
+ }
658
+ }, options.delay);
659
+ } else {
660
+ this.processNext();
661
+ }
662
+ return this.toPublicJob(job);
386
663
  }
387
664
  async addBulk(jobs) {
388
665
  return Promise.all(jobs.map((j) => this.add(j.name, j.data, j.options)));
389
666
  }
390
- process(handler) {
667
+ process(handler, options) {
391
668
  this.handlers.push(handler);
669
+ if (options?.concurrency) {
670
+ this.processingConcurrency = options.concurrency;
671
+ }
672
+ this.processNext();
392
673
  }
393
674
  async getJob(id) {
394
- return this.jobs.find((j) => j.id === id) || null;
675
+ const job = this.jobs.get(id);
676
+ return job ? this.toPublicJob(job) : null;
395
677
  }
396
678
  async removeJob(id) {
397
- this.jobs = this.jobs.filter((j) => j.id !== id);
679
+ this.jobs.delete(id);
398
680
  }
399
681
  async pause() {
682
+ this.paused = true;
400
683
  }
401
684
  async resume() {
685
+ this.paused = false;
686
+ this.processNext();
402
687
  }
403
688
  async getStats() {
404
- return { waiting: 0, active: 0, completed: this.jobs.length, failed: 0, delayed: 0 };
689
+ const stats = {
690
+ waiting: 0,
691
+ active: 0,
692
+ completed: 0,
693
+ failed: 0,
694
+ delayed: 0
695
+ };
696
+ for (const job of this.jobs.values()) {
697
+ switch (job.state) {
698
+ case "waiting":
699
+ stats.waiting++;
700
+ break;
701
+ case "active":
702
+ stats.active++;
703
+ break;
704
+ case "completed":
705
+ stats.completed++;
706
+ break;
707
+ case "failed":
708
+ stats.failed++;
709
+ break;
710
+ case "delayed":
711
+ stats.delayed++;
712
+ break;
713
+ }
714
+ }
715
+ return stats;
405
716
  }
406
717
  async healthCheck() {
407
718
  return true;
408
719
  }
409
720
  async close() {
410
- this.jobs = [];
721
+ for (const interval of this.repeatIntervals.values()) {
722
+ clearInterval(interval);
723
+ }
724
+ this.repeatIntervals.clear();
725
+ this.jobs.clear();
726
+ this.handlers = [];
727
+ this.eventHandlers.clear();
728
+ this.paused = true;
729
+ }
730
+ // ═══════════════════════════════════════════════════════════════
731
+ // OPTIONAL METHODS (new in enhanced interface)
732
+ // ═══════════════════════════════════════════════════════════════
733
+ async addRecurring(name, data, repeat, options) {
734
+ const repeatKey = `repeat:${name}:${Date.now()}`;
735
+ const job = await this.add(name, data, { ...options, jobId: repeatKey });
736
+ const internalJob = this.jobs.get(repeatKey);
737
+ if (internalJob) {
738
+ internalJob.repeatKey = repeatKey;
739
+ }
740
+ if (repeat.every) {
741
+ let execCount = 0;
742
+ const interval = setInterval(async () => {
743
+ if (repeat.limit && execCount >= repeat.limit) {
744
+ clearInterval(interval);
745
+ this.repeatIntervals.delete(repeatKey);
746
+ return;
747
+ }
748
+ execCount++;
749
+ await this.add(name, data, options);
750
+ }, repeat.every);
751
+ this.repeatIntervals.set(repeatKey, interval);
752
+ }
753
+ if (repeat.cron) {
754
+ console.warn(
755
+ "MemoryQueue: Cron expressions not supported, use 'every' for intervals"
756
+ );
757
+ }
758
+ return job;
759
+ }
760
+ async getJobs(state, start = 0, end = -1) {
761
+ const states = Array.isArray(state) ? state : [state];
762
+ const filtered = Array.from(this.jobs.values()).filter((j) => states.includes(j.state || "waiting")).sort((a, b) => a.timestamp - b.timestamp);
763
+ const endIndex = end === -1 ? filtered.length : end + 1;
764
+ return filtered.slice(start, endIndex).map((j) => this.toPublicJob(j));
765
+ }
766
+ async getFailedJobs(start = 0, end = -1) {
767
+ return this.getJobs("failed", start, end);
768
+ }
769
+ async retryJob(id) {
770
+ const job = this.jobs.get(id);
771
+ if (!job || job.state !== "failed") {
772
+ return;
773
+ }
774
+ job.state = "waiting";
775
+ job.attemptsMade = 0;
776
+ job.failedReason = void 0;
777
+ this.processNext();
778
+ }
779
+ async replayAllFailed() {
780
+ const failedJobs = Array.from(this.jobs.values()).filter(
781
+ (j) => j.state === "failed"
782
+ );
783
+ for (const job of failedJobs) {
784
+ job.state = "waiting";
785
+ job.attemptsMade = 0;
786
+ job.failedReason = void 0;
787
+ }
788
+ this.processNext();
789
+ return failedJobs.length;
790
+ }
791
+ async updateProgress(id, progress) {
792
+ const job = this.jobs.get(id);
793
+ if (job) {
794
+ job.progress = Math.min(100, Math.max(0, progress));
795
+ this.emitEvent("progress", job, { progress });
796
+ }
797
+ }
798
+ async clean(grace, limit, state) {
799
+ const now = Date.now();
800
+ const removed = [];
801
+ for (const [id, job] of this.jobs.entries()) {
802
+ if (removed.length >= limit) break;
803
+ if (job.state === state) {
804
+ const finishedTime = job.finishedOn || job.timestamp;
805
+ if (now - finishedTime > grace) {
806
+ this.jobs.delete(id);
807
+ removed.push(id);
808
+ }
809
+ }
810
+ }
811
+ return removed;
812
+ }
813
+ async obliterate(options) {
814
+ for (const interval of this.repeatIntervals.values()) {
815
+ clearInterval(interval);
816
+ }
817
+ this.repeatIntervals.clear();
818
+ this.jobs.clear();
819
+ }
820
+ on(event, handler) {
821
+ if (!this.eventHandlers.has(event)) {
822
+ this.eventHandlers.set(event, /* @__PURE__ */ new Set());
823
+ }
824
+ this.eventHandlers.get(event).add(handler);
411
825
  }
826
+ off(event, handler) {
827
+ const handlers = this.eventHandlers.get(event);
828
+ if (handlers) {
829
+ handlers.delete(handler);
830
+ }
831
+ }
832
+ getName() {
833
+ return this.queueName;
834
+ }
835
+ // ═══════════════════════════════════════════════════════════════
836
+ // TESTING UTILITIES (not part of IQueue interface)
837
+ // ═══════════════════════════════════════════════════════════════
412
838
  /**
413
839
  * Clear all jobs (for testing)
414
840
  */
415
841
  clear() {
416
- this.jobs = [];
842
+ for (const interval of this.repeatIntervals.values()) {
843
+ clearInterval(interval);
844
+ }
845
+ this.repeatIntervals.clear();
846
+ this.jobs.clear();
417
847
  this.handlers = [];
848
+ this.paused = false;
849
+ this.activeCount = 0;
418
850
  }
419
851
  /**
420
- * Get all jobs (for testing)
852
+ * Get all jobs regardless of state (for testing)
421
853
  */
422
- getJobs() {
423
- return this.jobs;
854
+ getAllJobs() {
855
+ return Array.from(this.jobs.values()).map((j) => this.toPublicJob(j));
424
856
  }
425
857
  /**
426
- * Get pending jobs (for testing)
858
+ * Get pending (waiting) jobs (for testing)
427
859
  */
428
860
  getPendingJobs() {
429
- return this.jobs.filter((j) => j.progress < 100);
861
+ return Array.from(this.jobs.values()).filter((j) => j.state === "waiting").map((j) => this.toPublicJob(j));
430
862
  }
431
863
  /**
432
864
  * Get number of jobs (for testing)
433
865
  */
434
866
  get size() {
435
- return this.jobs.length;
867
+ return this.jobs.size;
868
+ }
869
+ /**
870
+ * Wait for all jobs to complete (for testing)
871
+ */
872
+ async drain(timeout = 5e3) {
873
+ const start = Date.now();
874
+ while (this.activeCount > 0 || this.hasWaitingJobs()) {
875
+ if (Date.now() - start > timeout) {
876
+ throw new Error("Queue drain timeout");
877
+ }
878
+ await new Promise((resolve) => setTimeout(resolve, 10));
879
+ }
880
+ }
881
+ // ═══════════════════════════════════════════════════════════════
882
+ // PRIVATE METHODS
883
+ // ═══════════════════════════════════════════════════════════════
884
+ hasWaitingJobs() {
885
+ for (const job of this.jobs.values()) {
886
+ if (job.state === "waiting") return true;
887
+ }
888
+ return false;
889
+ }
890
+ async processNext() {
891
+ if (this.paused || this.handlers.length === 0 || this.isProcessing) {
892
+ return;
893
+ }
894
+ if (this.activeCount >= this.processingConcurrency) {
895
+ return;
896
+ }
897
+ let nextJob;
898
+ for (const job of this.jobs.values()) {
899
+ if (job.state === "waiting") {
900
+ nextJob = job;
901
+ break;
902
+ }
903
+ }
904
+ if (!nextJob) {
905
+ return;
906
+ }
907
+ this.activeCount++;
908
+ nextJob.state = "active";
909
+ nextJob.processedOn = Date.now();
910
+ this.emitEvent("active", nextJob);
911
+ try {
912
+ for (const handler of this.handlers) {
913
+ await this.executeWithTimeout(handler, nextJob);
914
+ }
915
+ nextJob.state = "completed";
916
+ nextJob.finishedOn = Date.now();
917
+ nextJob.progress = 100;
918
+ this.emitEvent("completed", nextJob);
919
+ if (nextJob.options?.removeOnComplete === true) {
920
+ this.jobs.delete(nextJob.id);
921
+ }
922
+ } catch (error) {
923
+ await this.handleJobFailure(nextJob, error);
924
+ } finally {
925
+ this.activeCount--;
926
+ setImmediate(() => this.processNext());
927
+ }
928
+ }
929
+ async executeWithTimeout(handler, job) {
930
+ const timeout = job.options?.timeout;
931
+ const publicJob = this.toPublicJob(job);
932
+ const jobContext = createJobContext({
933
+ id: job.id,
934
+ name: job.name,
935
+ correlationId: job.correlationId,
936
+ metadata: job.metadata
937
+ });
938
+ const executeHandler = () => runWithContext(jobContext, () => handler(publicJob));
939
+ if (!timeout) {
940
+ await executeHandler();
941
+ return;
942
+ }
943
+ const timeoutPromise = new Promise((_, reject) => {
944
+ setTimeout(() => reject(new Error("Job timeout")), timeout);
945
+ });
946
+ await Promise.race([executeHandler(), timeoutPromise]);
947
+ }
948
+ async handleJobFailure(job, error) {
949
+ job.attemptsMade++;
950
+ const maxAttempts = job.options?.attempts || 1;
951
+ if (job.attemptsMade < maxAttempts) {
952
+ job.state = "delayed";
953
+ const backoffDelay = job.options?.backoff ? calculateBackoff(job.attemptsMade, job.options.backoff) : 1e3;
954
+ setTimeout(() => {
955
+ const j = this.jobs.get(job.id);
956
+ if (j && j.state === "delayed") {
957
+ j.state = "waiting";
958
+ this.processNext();
959
+ }
960
+ }, backoffDelay);
961
+ } else {
962
+ job.state = "failed";
963
+ job.finishedOn = Date.now();
964
+ job.failedReason = error.message;
965
+ this.emitEvent("failed", job, { error });
966
+ if (job.options?.removeOnFail === true) {
967
+ this.jobs.delete(job.id);
968
+ }
969
+ }
970
+ }
971
+ emitEvent(type, job, extra) {
972
+ const handlers = this.eventHandlers.get(type);
973
+ if (!handlers) return;
974
+ const event = {
975
+ type,
976
+ job: this.toPublicJob(job),
977
+ timestamp: Date.now(),
978
+ ...extra
979
+ };
980
+ for (const handler of handlers) {
981
+ try {
982
+ handler(event);
983
+ } catch {
984
+ }
985
+ }
986
+ }
987
+ toPublicJob(job) {
988
+ return {
989
+ id: job.id,
990
+ name: job.name,
991
+ data: job.data,
992
+ attemptsMade: job.attemptsMade,
993
+ progress: job.progress,
994
+ timestamp: job.timestamp,
995
+ state: job.state,
996
+ processedOn: job.processedOn,
997
+ finishedOn: job.finishedOn,
998
+ failedReason: job.failedReason,
999
+ correlationId: job.correlationId,
1000
+ metadata: job.metadata
1001
+ };
436
1002
  }
437
1003
  };
438
1004