@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/{ConsoleEmail-XeUBAnwc.d.mts → ConsoleEmail-CYPpn2sR.d.mts} +370 -207
- package/dist/{ConsoleEmail-XeUBAnwc.d.ts → ConsoleEmail-CYPpn2sR.d.ts} +370 -207
- package/dist/{index-C_2W7Byw.d.mts → index-CepDdu7h.d.mts} +68 -1
- package/dist/{index-C_2W7Byw.d.ts → index-CepDdu7h.d.ts} +68 -1
- package/dist/index.d.mts +10744 -8
- package/dist/index.d.ts +10744 -8
- package/dist/index.js +16692 -2746
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +16604 -2746
- package/dist/index.mjs.map +1 -1
- package/dist/migrations/index.d.mts +1 -1
- package/dist/migrations/index.d.ts +1 -1
- package/dist/migrations/index.js +234 -0
- package/dist/migrations/index.js.map +1 -1
- package/dist/migrations/index.mjs +224 -0
- package/dist/migrations/index.mjs.map +1 -1
- package/dist/testing.d.mts +2 -2
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +594 -20
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +594 -20
- package/dist/testing.mjs.map +1 -1
- package/package.json +14 -9
- package/dist/migrate.js +0 -1101
- package/dist/migrate.js.map +0 -1
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
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
630
|
+
const job = this.jobs.get(id);
|
|
631
|
+
return job ? this.toPublicJob(job) : null;
|
|
350
632
|
}
|
|
351
633
|
async removeJob(id) {
|
|
352
|
-
this.jobs
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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(
|
|
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(
|
|
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(
|
|
1684
|
+
span.recordException(
|
|
1685
|
+
error instanceof Error ? error : new Error(String(error))
|
|
1686
|
+
);
|
|
1113
1687
|
throw error;
|
|
1114
1688
|
} finally {
|
|
1115
1689
|
span.end();
|