@digilogiclabs/platform-core 1.0.0 → 1.1.1

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/testing.mjs CHANGED
@@ -329,65 +329,631 @@ var MemoryEmail = class {
329
329
  }
330
330
  };
331
331
 
332
+ // src/interfaces/IQueue.ts
333
+ function calculateBackoff(attempt, options) {
334
+ if (options.type === "fixed") {
335
+ return options.delay;
336
+ }
337
+ const delay = options.delay * Math.pow(2, attempt - 1);
338
+ const maxDelay = options.maxDelay ?? options.delay * 32;
339
+ return Math.min(delay, maxDelay);
340
+ }
341
+ function generateJobId() {
342
+ const timestamp = Date.now().toString(36);
343
+ const random = Math.random().toString(36).substring(2, 10);
344
+ return `job_${timestamp}_${random}`;
345
+ }
346
+
347
+ // src/context/CorrelationContext.ts
348
+ import { AsyncLocalStorage } from "async_hooks";
349
+ var CorrelationContextManager = class {
350
+ storage = new AsyncLocalStorage();
351
+ idGenerator;
352
+ constructor() {
353
+ this.idGenerator = () => {
354
+ const timestamp = Date.now().toString(36);
355
+ const random = Math.random().toString(36).substring(2, 10);
356
+ return `${timestamp}-${random}`;
357
+ };
358
+ }
359
+ /**
360
+ * Set custom ID generator
361
+ */
362
+ setIdGenerator(generator) {
363
+ this.idGenerator = generator;
364
+ }
365
+ /**
366
+ * Generate a new ID
367
+ */
368
+ generateId() {
369
+ return this.idGenerator();
370
+ }
371
+ /**
372
+ * Run a function with correlation context
373
+ */
374
+ run(data, fn) {
375
+ return this.storage.run(
376
+ {
377
+ ...data,
378
+ startTime: data.startTime ?? Date.now()
379
+ },
380
+ fn
381
+ );
382
+ }
383
+ /**
384
+ * Run an async function with correlation context
385
+ */
386
+ async runAsync(data, fn) {
387
+ return this.storage.run(
388
+ {
389
+ ...data,
390
+ startTime: data.startTime ?? Date.now()
391
+ },
392
+ fn
393
+ );
394
+ }
395
+ /**
396
+ * Get current correlation data
397
+ */
398
+ get() {
399
+ return this.storage.getStore();
400
+ }
401
+ /**
402
+ * Get current correlation data or empty object
403
+ */
404
+ getOrEmpty() {
405
+ return this.storage.getStore() ?? {};
406
+ }
407
+ /**
408
+ * Get trace ID from current context
409
+ */
410
+ getTraceId() {
411
+ return this.get()?.traceId;
412
+ }
413
+ /**
414
+ * Get correlation ID from current context
415
+ */
416
+ getCorrelationId() {
417
+ return this.get()?.correlationId ?? this.get()?.traceId;
418
+ }
419
+ /**
420
+ * Get request ID from current context
421
+ */
422
+ getRequestId() {
423
+ return this.get()?.requestId;
424
+ }
425
+ /**
426
+ * Get user ID from current context
427
+ */
428
+ getUserId() {
429
+ return this.get()?.userId;
430
+ }
431
+ /**
432
+ * Get tenant ID from current context
433
+ */
434
+ getTenantId() {
435
+ return this.get()?.tenantId;
436
+ }
437
+ /**
438
+ * Check if we're in a correlation context
439
+ */
440
+ isInContext() {
441
+ return this.storage.getStore() !== void 0;
442
+ }
443
+ /**
444
+ * Update current context (merge data)
445
+ * Note: This doesn't actually update the store, but returns merged data
446
+ * for use in nested contexts
447
+ */
448
+ extend(data) {
449
+ const current = this.get() ?? {};
450
+ return {
451
+ ...current,
452
+ ...data,
453
+ metadata: {
454
+ ...current.metadata,
455
+ ...data.metadata
456
+ }
457
+ };
458
+ }
459
+ /**
460
+ * Run a nested context with extended data
461
+ */
462
+ runNested(data, fn) {
463
+ return this.run(this.extend(data), fn);
464
+ }
465
+ /**
466
+ * Run a nested async context with extended data
467
+ */
468
+ async runNestedAsync(data, fn) {
469
+ return this.runAsync(this.extend(data), fn);
470
+ }
471
+ /**
472
+ * Get context as log metadata (for structured logging)
473
+ */
474
+ getLogMeta() {
475
+ const ctx = this.get();
476
+ if (!ctx) return {};
477
+ const meta = {};
478
+ if (ctx.traceId) meta.traceId = ctx.traceId;
479
+ if (ctx.spanId) meta.spanId = ctx.spanId;
480
+ if (ctx.requestId) meta.requestId = ctx.requestId;
481
+ if (ctx.correlationId) meta.correlationId = ctx.correlationId;
482
+ if (ctx.userId) meta.userId = ctx.userId;
483
+ if (ctx.tenantId) meta.tenantId = ctx.tenantId;
484
+ if (ctx.sessionId) meta.sessionId = ctx.sessionId;
485
+ if (ctx.operation) meta.operation = ctx.operation;
486
+ return meta;
487
+ }
488
+ /**
489
+ * Get context as HTTP headers (for propagation)
490
+ */
491
+ getHeaders() {
492
+ const ctx = this.get();
493
+ if (!ctx) return {};
494
+ const headers = {};
495
+ if (ctx.traceId) {
496
+ const spanId = ctx.spanId ?? this.generateId().substring(0, 16);
497
+ headers["traceparent"] = `00-${ctx.traceId}-${spanId}-01`;
498
+ }
499
+ if (ctx.requestId) {
500
+ headers["x-request-id"] = ctx.requestId;
501
+ }
502
+ if (ctx.correlationId) {
503
+ headers["x-correlation-id"] = ctx.correlationId;
504
+ }
505
+ return headers;
506
+ }
507
+ /**
508
+ * Parse context from HTTP headers
509
+ */
510
+ parseHeaders(headers) {
511
+ const data = {};
512
+ const traceparent = headers["traceparent"];
513
+ if (traceparent && typeof traceparent === "string") {
514
+ const parts = traceparent.split("-");
515
+ if (parts.length >= 3 && parts[0] === "00") {
516
+ data.traceId = parts[1];
517
+ data.spanId = parts[2];
518
+ }
519
+ }
520
+ const requestId = headers["x-request-id"];
521
+ if (requestId) {
522
+ data.requestId = Array.isArray(requestId) ? requestId[0] : requestId;
523
+ }
524
+ const correlationId = headers["x-correlation-id"];
525
+ if (correlationId) {
526
+ data.correlationId = Array.isArray(correlationId) ? correlationId[0] : correlationId;
527
+ }
528
+ return data;
529
+ }
530
+ /**
531
+ * Get elapsed time since context start (ms)
532
+ */
533
+ getElapsed() {
534
+ const ctx = this.get();
535
+ if (!ctx?.startTime) return 0;
536
+ return Date.now() - ctx.startTime;
537
+ }
538
+ /**
539
+ * Create a child span context
540
+ */
541
+ createChildSpan(operation) {
542
+ const current = this.get() ?? {};
543
+ return {
544
+ ...current,
545
+ spanId: this.generateId().substring(0, 16),
546
+ operation,
547
+ startTime: Date.now()
548
+ };
549
+ }
550
+ };
551
+ var correlationContext = new CorrelationContextManager();
552
+ var runWithContext = correlationContext.run.bind(correlationContext);
553
+ var runWithContextAsync = correlationContext.runAsync.bind(correlationContext);
554
+ var getContext = correlationContext.get.bind(correlationContext);
555
+ var getTraceId = correlationContext.getTraceId.bind(correlationContext);
556
+ var getCorrelationId = correlationContext.getCorrelationId.bind(correlationContext);
557
+ var getRequestId = correlationContext.getRequestId.bind(correlationContext);
558
+ var getUserId = correlationContext.getUserId.bind(correlationContext);
559
+ var getTenantId = correlationContext.getTenantId.bind(correlationContext);
560
+ var getLogMeta = correlationContext.getLogMeta.bind(correlationContext);
561
+ var isInContext = correlationContext.isInContext.bind(correlationContext);
562
+ function createJobContext(job) {
563
+ return {
564
+ correlationId: job.correlationId ?? job.id,
565
+ traceId: correlationContext.generateId(),
566
+ operation: `job:${job.name}`,
567
+ metadata: job.metadata,
568
+ startTime: Date.now()
569
+ };
570
+ }
571
+
332
572
  // src/adapters/memory/MemoryQueue.ts
333
573
  var MemoryQueue = class {
334
- jobs = [];
574
+ jobs = /* @__PURE__ */ new Map();
335
575
  handlers = [];
576
+ eventHandlers = /* @__PURE__ */ new Map();
577
+ paused = false;
578
+ processingConcurrency = 1;
579
+ activeCount = 0;
580
+ repeatIntervals = /* @__PURE__ */ new Map();
581
+ queueName;
582
+ processingQueue = [];
583
+ isProcessing = false;
584
+ constructor(name = "default") {
585
+ this.queueName = name;
586
+ }
587
+ // ═══════════════════════════════════════════════════════════════
588
+ // CORE METHODS (IQueue interface)
589
+ // ═══════════════════════════════════════════════════════════════
336
590
  async add(name, data, options) {
337
- const job = { id: "job_" + this.jobs.length, name, data, attemptsMade: 0, progress: 0, timestamp: Date.now() };
338
- this.jobs.push(job);
339
- this.handlers.forEach((h) => h(job));
340
- return job;
591
+ const jobId = options?.jobId || generateJobId();
592
+ const now = Date.now();
593
+ const job = {
594
+ id: jobId,
595
+ name,
596
+ data,
597
+ attemptsMade: 0,
598
+ progress: 0,
599
+ timestamp: now,
600
+ state: options?.delay ? "delayed" : "waiting",
601
+ correlationId: options?.correlationId,
602
+ metadata: options?.metadata,
603
+ options
604
+ };
605
+ this.jobs.set(jobId, job);
606
+ if (options?.delay && options.delay > 0) {
607
+ setTimeout(() => {
608
+ const j = this.jobs.get(jobId);
609
+ if (j && j.state === "delayed") {
610
+ j.state = "waiting";
611
+ this.processNext();
612
+ }
613
+ }, options.delay);
614
+ } else {
615
+ this.processNext();
616
+ }
617
+ return this.toPublicJob(job);
341
618
  }
342
619
  async addBulk(jobs) {
343
620
  return Promise.all(jobs.map((j) => this.add(j.name, j.data, j.options)));
344
621
  }
345
- process(handler) {
622
+ process(handler, options) {
346
623
  this.handlers.push(handler);
624
+ if (options?.concurrency) {
625
+ this.processingConcurrency = options.concurrency;
626
+ }
627
+ this.processNext();
347
628
  }
348
629
  async getJob(id) {
349
- return this.jobs.find((j) => j.id === id) || null;
630
+ const job = this.jobs.get(id);
631
+ return job ? this.toPublicJob(job) : null;
350
632
  }
351
633
  async removeJob(id) {
352
- this.jobs = this.jobs.filter((j) => j.id !== id);
634
+ this.jobs.delete(id);
353
635
  }
354
636
  async pause() {
637
+ this.paused = true;
355
638
  }
356
639
  async resume() {
640
+ this.paused = false;
641
+ this.processNext();
357
642
  }
358
643
  async getStats() {
359
- return { waiting: 0, active: 0, completed: this.jobs.length, failed: 0, delayed: 0 };
644
+ const stats = {
645
+ waiting: 0,
646
+ active: 0,
647
+ completed: 0,
648
+ failed: 0,
649
+ delayed: 0
650
+ };
651
+ for (const job of this.jobs.values()) {
652
+ switch (job.state) {
653
+ case "waiting":
654
+ stats.waiting++;
655
+ break;
656
+ case "active":
657
+ stats.active++;
658
+ break;
659
+ case "completed":
660
+ stats.completed++;
661
+ break;
662
+ case "failed":
663
+ stats.failed++;
664
+ break;
665
+ case "delayed":
666
+ stats.delayed++;
667
+ break;
668
+ }
669
+ }
670
+ return stats;
360
671
  }
361
672
  async healthCheck() {
362
673
  return true;
363
674
  }
364
675
  async close() {
365
- this.jobs = [];
676
+ for (const interval of this.repeatIntervals.values()) {
677
+ clearInterval(interval);
678
+ }
679
+ this.repeatIntervals.clear();
680
+ this.jobs.clear();
681
+ this.handlers = [];
682
+ this.eventHandlers.clear();
683
+ this.paused = true;
684
+ }
685
+ // ═══════════════════════════════════════════════════════════════
686
+ // OPTIONAL METHODS (new in enhanced interface)
687
+ // ═══════════════════════════════════════════════════════════════
688
+ async addRecurring(name, data, repeat, options) {
689
+ const repeatKey = `repeat:${name}:${Date.now()}`;
690
+ const job = await this.add(name, data, { ...options, jobId: repeatKey });
691
+ const internalJob = this.jobs.get(repeatKey);
692
+ if (internalJob) {
693
+ internalJob.repeatKey = repeatKey;
694
+ }
695
+ if (repeat.every) {
696
+ let execCount = 0;
697
+ const interval = setInterval(async () => {
698
+ if (repeat.limit && execCount >= repeat.limit) {
699
+ clearInterval(interval);
700
+ this.repeatIntervals.delete(repeatKey);
701
+ return;
702
+ }
703
+ execCount++;
704
+ await this.add(name, data, options);
705
+ }, repeat.every);
706
+ this.repeatIntervals.set(repeatKey, interval);
707
+ }
708
+ if (repeat.cron) {
709
+ console.warn(
710
+ "MemoryQueue: Cron expressions not supported, use 'every' for intervals"
711
+ );
712
+ }
713
+ return job;
714
+ }
715
+ async getJobs(state, start = 0, end = -1) {
716
+ const states = Array.isArray(state) ? state : [state];
717
+ const filtered = Array.from(this.jobs.values()).filter((j) => states.includes(j.state || "waiting")).sort((a, b) => a.timestamp - b.timestamp);
718
+ const endIndex = end === -1 ? filtered.length : end + 1;
719
+ return filtered.slice(start, endIndex).map((j) => this.toPublicJob(j));
720
+ }
721
+ async getFailedJobs(start = 0, end = -1) {
722
+ return this.getJobs("failed", start, end);
723
+ }
724
+ async retryJob(id) {
725
+ const job = this.jobs.get(id);
726
+ if (!job || job.state !== "failed") {
727
+ return;
728
+ }
729
+ job.state = "waiting";
730
+ job.attemptsMade = 0;
731
+ job.failedReason = void 0;
732
+ this.processNext();
733
+ }
734
+ async replayAllFailed() {
735
+ const failedJobs = Array.from(this.jobs.values()).filter(
736
+ (j) => j.state === "failed"
737
+ );
738
+ for (const job of failedJobs) {
739
+ job.state = "waiting";
740
+ job.attemptsMade = 0;
741
+ job.failedReason = void 0;
742
+ }
743
+ this.processNext();
744
+ return failedJobs.length;
745
+ }
746
+ async updateProgress(id, progress) {
747
+ const job = this.jobs.get(id);
748
+ if (job) {
749
+ job.progress = Math.min(100, Math.max(0, progress));
750
+ this.emitEvent("progress", job, { progress });
751
+ }
752
+ }
753
+ async clean(grace, limit, state) {
754
+ const now = Date.now();
755
+ const removed = [];
756
+ for (const [id, job] of this.jobs.entries()) {
757
+ if (removed.length >= limit) break;
758
+ if (job.state === state) {
759
+ const finishedTime = job.finishedOn || job.timestamp;
760
+ if (now - finishedTime > grace) {
761
+ this.jobs.delete(id);
762
+ removed.push(id);
763
+ }
764
+ }
765
+ }
766
+ return removed;
767
+ }
768
+ async obliterate(options) {
769
+ for (const interval of this.repeatIntervals.values()) {
770
+ clearInterval(interval);
771
+ }
772
+ this.repeatIntervals.clear();
773
+ this.jobs.clear();
774
+ }
775
+ on(event, handler) {
776
+ if (!this.eventHandlers.has(event)) {
777
+ this.eventHandlers.set(event, /* @__PURE__ */ new Set());
778
+ }
779
+ this.eventHandlers.get(event).add(handler);
366
780
  }
781
+ off(event, handler) {
782
+ const handlers = this.eventHandlers.get(event);
783
+ if (handlers) {
784
+ handlers.delete(handler);
785
+ }
786
+ }
787
+ getName() {
788
+ return this.queueName;
789
+ }
790
+ // ═══════════════════════════════════════════════════════════════
791
+ // TESTING UTILITIES (not part of IQueue interface)
792
+ // ═══════════════════════════════════════════════════════════════
367
793
  /**
368
794
  * Clear all jobs (for testing)
369
795
  */
370
796
  clear() {
371
- this.jobs = [];
797
+ for (const interval of this.repeatIntervals.values()) {
798
+ clearInterval(interval);
799
+ }
800
+ this.repeatIntervals.clear();
801
+ this.jobs.clear();
372
802
  this.handlers = [];
803
+ this.paused = false;
804
+ this.activeCount = 0;
373
805
  }
374
806
  /**
375
- * Get all jobs (for testing)
807
+ * Get all jobs regardless of state (for testing)
376
808
  */
377
- getJobs() {
378
- return this.jobs;
809
+ getAllJobs() {
810
+ return Array.from(this.jobs.values()).map((j) => this.toPublicJob(j));
379
811
  }
380
812
  /**
381
- * Get pending jobs (for testing)
813
+ * Get pending (waiting) jobs (for testing)
382
814
  */
383
815
  getPendingJobs() {
384
- return this.jobs.filter((j) => j.progress < 100);
816
+ return Array.from(this.jobs.values()).filter((j) => j.state === "waiting").map((j) => this.toPublicJob(j));
385
817
  }
386
818
  /**
387
819
  * Get number of jobs (for testing)
388
820
  */
389
821
  get size() {
390
- return this.jobs.length;
822
+ return this.jobs.size;
823
+ }
824
+ /**
825
+ * Wait for all jobs to complete (for testing)
826
+ */
827
+ async drain(timeout = 5e3) {
828
+ const start = Date.now();
829
+ while (this.activeCount > 0 || this.hasWaitingJobs()) {
830
+ if (Date.now() - start > timeout) {
831
+ throw new Error("Queue drain timeout");
832
+ }
833
+ await new Promise((resolve) => setTimeout(resolve, 10));
834
+ }
835
+ }
836
+ // ═══════════════════════════════════════════════════════════════
837
+ // PRIVATE METHODS
838
+ // ═══════════════════════════════════════════════════════════════
839
+ hasWaitingJobs() {
840
+ for (const job of this.jobs.values()) {
841
+ if (job.state === "waiting") return true;
842
+ }
843
+ return false;
844
+ }
845
+ async processNext() {
846
+ if (this.paused || this.handlers.length === 0 || this.isProcessing) {
847
+ return;
848
+ }
849
+ if (this.activeCount >= this.processingConcurrency) {
850
+ return;
851
+ }
852
+ let nextJob;
853
+ for (const job of this.jobs.values()) {
854
+ if (job.state === "waiting") {
855
+ nextJob = job;
856
+ break;
857
+ }
858
+ }
859
+ if (!nextJob) {
860
+ return;
861
+ }
862
+ this.activeCount++;
863
+ nextJob.state = "active";
864
+ nextJob.processedOn = Date.now();
865
+ this.emitEvent("active", nextJob);
866
+ try {
867
+ for (const handler of this.handlers) {
868
+ await this.executeWithTimeout(handler, nextJob);
869
+ }
870
+ nextJob.state = "completed";
871
+ nextJob.finishedOn = Date.now();
872
+ nextJob.progress = 100;
873
+ this.emitEvent("completed", nextJob);
874
+ if (nextJob.options?.removeOnComplete === true) {
875
+ this.jobs.delete(nextJob.id);
876
+ }
877
+ } catch (error) {
878
+ await this.handleJobFailure(nextJob, error);
879
+ } finally {
880
+ this.activeCount--;
881
+ setImmediate(() => this.processNext());
882
+ }
883
+ }
884
+ async executeWithTimeout(handler, job) {
885
+ const timeout = job.options?.timeout;
886
+ const publicJob = this.toPublicJob(job);
887
+ const jobContext = createJobContext({
888
+ id: job.id,
889
+ name: job.name,
890
+ correlationId: job.correlationId,
891
+ metadata: job.metadata
892
+ });
893
+ const executeHandler = () => runWithContext(jobContext, () => handler(publicJob));
894
+ if (!timeout) {
895
+ await executeHandler();
896
+ return;
897
+ }
898
+ const timeoutPromise = new Promise((_, reject) => {
899
+ setTimeout(() => reject(new Error("Job timeout")), timeout);
900
+ });
901
+ await Promise.race([executeHandler(), timeoutPromise]);
902
+ }
903
+ async handleJobFailure(job, error) {
904
+ job.attemptsMade++;
905
+ const maxAttempts = job.options?.attempts || 1;
906
+ if (job.attemptsMade < maxAttempts) {
907
+ job.state = "delayed";
908
+ const backoffDelay = job.options?.backoff ? calculateBackoff(job.attemptsMade, job.options.backoff) : 1e3;
909
+ setTimeout(() => {
910
+ const j = this.jobs.get(job.id);
911
+ if (j && j.state === "delayed") {
912
+ j.state = "waiting";
913
+ this.processNext();
914
+ }
915
+ }, backoffDelay);
916
+ } else {
917
+ job.state = "failed";
918
+ job.finishedOn = Date.now();
919
+ job.failedReason = error.message;
920
+ this.emitEvent("failed", job, { error });
921
+ if (job.options?.removeOnFail === true) {
922
+ this.jobs.delete(job.id);
923
+ }
924
+ }
925
+ }
926
+ emitEvent(type, job, extra) {
927
+ const handlers = this.eventHandlers.get(type);
928
+ if (!handlers) return;
929
+ const event = {
930
+ type,
931
+ job: this.toPublicJob(job),
932
+ timestamp: Date.now(),
933
+ ...extra
934
+ };
935
+ for (const handler of handlers) {
936
+ try {
937
+ handler(event);
938
+ } catch {
939
+ }
940
+ }
941
+ }
942
+ toPublicJob(job) {
943
+ return {
944
+ id: job.id,
945
+ name: job.name,
946
+ data: job.data,
947
+ attemptsMade: job.attemptsMade,
948
+ progress: job.progress,
949
+ timestamp: job.timestamp,
950
+ state: job.state,
951
+ processedOn: job.processedOn,
952
+ finishedOn: job.finishedOn,
953
+ failedReason: job.failedReason,
954
+ correlationId: job.correlationId,
955
+ metadata: job.metadata
956
+ };
391
957
  }
392
958
  };
393
959
 
@@ -1078,7 +1644,11 @@ var MemoryTracing = class {
1078
1644
  return Math.random().toString(16).substring(2, 34).padStart(32, "0");
1079
1645
  }
1080
1646
  startSpan(name, options) {
1081
- const span = new MemorySpan(name, this.traceId, this.currentSpan?.context.spanId);
1647
+ const span = new MemorySpan(
1648
+ name,
1649
+ this.traceId,
1650
+ this.currentSpan?.context.spanId
1651
+ );
1082
1652
  if (options?.attributes) {
1083
1653
  span.setAttributes(options.attributes);
1084
1654
  }
@@ -1096,7 +1666,9 @@ var MemoryTracing = class {
1096
1666
  span.setStatus({ code: "ok" });
1097
1667
  return result;
1098
1668
  } catch (error) {
1099
- span.recordException(error instanceof Error ? error : new Error(String(error)));
1669
+ span.recordException(
1670
+ error instanceof Error ? error : new Error(String(error))
1671
+ );
1100
1672
  throw error;
1101
1673
  } finally {
1102
1674
  span.end();
@@ -1109,7 +1681,9 @@ var MemoryTracing = class {
1109
1681
  span.setStatus({ code: "ok" });
1110
1682
  return result;
1111
1683
  } catch (error) {
1112
- span.recordException(error instanceof Error ? error : new Error(String(error)));
1684
+ span.recordException(
1685
+ error instanceof Error ? error : new Error(String(error))
1686
+ );
1113
1687
  throw error;
1114
1688
  } finally {
1115
1689
  span.end();