@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/{ConsoleEmail-XeUBAnwc.d.mts → ConsoleEmail-Drr30nct.d.mts} +291 -128
- package/dist/{ConsoleEmail-XeUBAnwc.d.ts → ConsoleEmail-Drr30nct.d.ts} +291 -128
- package/dist/index.d.mts +2434 -5
- package/dist/index.d.ts +2434 -5
- package/dist/index.js +4779 -165
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4730 -165
- package/dist/index.mjs.map +1 -1
- package/dist/migrate.js +0 -0
- package/dist/testing.d.mts +2 -2
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +583 -17
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +583 -17
- package/dist/testing.mjs.map +1 -1
- package/package.json +9 -9
package/dist/migrate.js
CHANGED
|
File without changes
|
package/dist/testing.d.mts
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
675
|
+
const job = this.jobs.get(id);
|
|
676
|
+
return job ? this.toPublicJob(job) : null;
|
|
395
677
|
}
|
|
396
678
|
async removeJob(id) {
|
|
397
|
-
this.jobs
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|