@gravito/stream 1.0.0-beta.1 → 1.0.0-beta.2

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/index.js CHANGED
@@ -2,6 +2,12 @@ var __defProp = Object.defineProperty;
2
2
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
4
  var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
6
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
7
+ }) : x)(function(x) {
8
+ if (typeof require !== "undefined") return require.apply(this, arguments);
9
+ throw Error('Dynamic require of "' + x + '" is not supported');
10
+ });
5
11
  var __esm = (fn, res) => function __init() {
6
12
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
13
  };
@@ -45,10 +51,11 @@ var init_DatabaseDriver = __esm({
45
51
  */
46
52
  async push(queue, job) {
47
53
  const availableAt = job.delaySeconds ? new Date(Date.now() + job.delaySeconds * 1e3) : /* @__PURE__ */ new Date();
54
+ const payload = JSON.stringify(job);
48
55
  await this.dbService.execute(
49
56
  `INSERT INTO ${this.tableName} (queue, payload, attempts, available_at, created_at)
50
57
  VALUES ($1, $2, $3, $4, $5)`,
51
- [queue, job.data, job.attempts ?? 0, availableAt.toISOString(), (/* @__PURE__ */ new Date()).toISOString()]
58
+ [queue, payload, job.attempts ?? 0, availableAt.toISOString(), (/* @__PURE__ */ new Date()).toISOString()]
52
59
  );
53
60
  }
54
61
  /**
@@ -91,15 +98,32 @@ var init_DatabaseDriver = __esm({
91
98
  );
92
99
  const createdAt = new Date(row.created_at).getTime();
93
100
  const delaySeconds = row.available_at ? Math.max(0, Math.floor((new Date(row.available_at).getTime() - createdAt) / 1e3)) : void 0;
94
- return {
95
- id: row.id,
96
- type: "class",
97
- // Default; should be inferred from payload in a full implementation
98
- data: row.payload,
99
- createdAt,
100
- attempts: row.attempts,
101
- ...delaySeconds !== void 0 ? { delaySeconds } : {}
102
- };
101
+ let job;
102
+ try {
103
+ const parsed = JSON.parse(row.payload);
104
+ if (parsed && typeof parsed === "object" && parsed.type && parsed.data) {
105
+ job = {
106
+ ...parsed,
107
+ id: row.id,
108
+ // DB ID is the source of truth for deletion
109
+ attempts: row.attempts
110
+ };
111
+ } else {
112
+ throw new Error("Fallback");
113
+ }
114
+ } catch (_e) {
115
+ job = {
116
+ id: row.id,
117
+ type: "class",
118
+ data: row.payload,
119
+ createdAt,
120
+ attempts: row.attempts
121
+ };
122
+ }
123
+ if (delaySeconds !== void 0) {
124
+ job.delaySeconds = delaySeconds;
125
+ }
126
+ return job;
103
127
  }
104
128
  /**
105
129
  * Get queue size.
@@ -139,6 +163,27 @@ var init_DatabaseDriver = __esm({
139
163
  }
140
164
  });
141
165
  }
166
+ /**
167
+ * Mark a job as failed (DLQ).
168
+ */
169
+ async fail(queue, job) {
170
+ const failedQueue = `failed:${queue}`;
171
+ const payload = JSON.stringify(job);
172
+ await this.dbService.execute(
173
+ `INSERT INTO ${this.tableName} (queue, payload, attempts, available_at, created_at)
174
+ VALUES ($1, $2, $3, $4, $5)`,
175
+ [failedQueue, payload, job.attempts, (/* @__PURE__ */ new Date()).toISOString(), (/* @__PURE__ */ new Date()).toISOString()]
176
+ );
177
+ }
178
+ /**
179
+ * Acknowledge/Complete a job.
180
+ */
181
+ async complete(_queue, job) {
182
+ if (!job.id) {
183
+ return;
184
+ }
185
+ await this.dbService.execute(`DELETE FROM ${this.tableName} WHERE id = $1`, [job.id]);
186
+ }
142
187
  };
143
188
  }
144
189
  });
@@ -317,6 +362,150 @@ var init_KafkaDriver = __esm({
317
362
  }
318
363
  });
319
364
 
365
+ // src/drivers/RabbitMQDriver.ts
366
+ var RabbitMQDriver_exports = {};
367
+ __export(RabbitMQDriver_exports, {
368
+ RabbitMQDriver: () => RabbitMQDriver
369
+ });
370
+ var RabbitMQDriver;
371
+ var init_RabbitMQDriver = __esm({
372
+ "src/drivers/RabbitMQDriver.ts"() {
373
+ "use strict";
374
+ RabbitMQDriver = class {
375
+ connection;
376
+ channel;
377
+ exchange;
378
+ exchangeType;
379
+ constructor(config) {
380
+ this.connection = config.client;
381
+ this.exchange = config.exchange;
382
+ this.exchangeType = config.exchangeType ?? "fanout";
383
+ if (!this.connection) {
384
+ throw new Error(
385
+ "[RabbitMQDriver] RabbitMQ connection is required. Please provide a connection from amqplib."
386
+ );
387
+ }
388
+ }
389
+ /**
390
+ * Ensure channel is created.
391
+ */
392
+ async ensureChannel() {
393
+ if (this.channel) {
394
+ return this.channel;
395
+ }
396
+ if (typeof this.connection.createChannel === "function") {
397
+ this.channel = await this.connection.createChannel();
398
+ } else {
399
+ this.channel = this.connection;
400
+ }
401
+ if (this.exchange) {
402
+ await this.channel.assertExchange(this.exchange, this.exchangeType, { durable: true });
403
+ }
404
+ return this.channel;
405
+ }
406
+ /**
407
+ * Get the underlying connection.
408
+ */
409
+ getRawConnection() {
410
+ return this.connection;
411
+ }
412
+ /**
413
+ * Push a job (sendToQueue / publish).
414
+ */
415
+ async push(queue, job) {
416
+ const channel = await this.ensureChannel();
417
+ const payload = Buffer.from(JSON.stringify(job));
418
+ if (this.exchange) {
419
+ await channel.assertQueue(queue, { durable: true });
420
+ await channel.bindQueue(queue, this.exchange, "");
421
+ channel.publish(this.exchange, "", payload, { persistent: true });
422
+ } else {
423
+ await channel.assertQueue(queue, { durable: true });
424
+ channel.sendToQueue(queue, payload, { persistent: true });
425
+ }
426
+ }
427
+ /**
428
+ * Pop a job (get).
429
+ */
430
+ async pop(queue) {
431
+ const channel = await this.ensureChannel();
432
+ await channel.assertQueue(queue, { durable: true });
433
+ const msg = await channel.get(queue, { noAck: false });
434
+ if (!msg) {
435
+ return null;
436
+ }
437
+ const job = JSON.parse(msg.content.toString());
438
+ job._raw = msg;
439
+ return job;
440
+ }
441
+ /**
442
+ * Acknowledge a message.
443
+ */
444
+ async acknowledge(messageId) {
445
+ const channel = await this.ensureChannel();
446
+ if (typeof messageId === "object") {
447
+ channel.ack(messageId);
448
+ }
449
+ }
450
+ /**
451
+ * Negative acknowledge a message.
452
+ */
453
+ async nack(message, requeue = true) {
454
+ const channel = await this.ensureChannel();
455
+ channel.nack(message, false, requeue);
456
+ }
457
+ /**
458
+ * Reject a message.
459
+ */
460
+ async reject(message, requeue = true) {
461
+ const channel = await this.ensureChannel();
462
+ channel.reject(message, requeue);
463
+ }
464
+ /**
465
+ * Subscribe to a queue.
466
+ */
467
+ async subscribe(queue, callback, options = {}) {
468
+ const channel = await this.ensureChannel();
469
+ await channel.assertQueue(queue, { durable: true });
470
+ if (this.exchange) {
471
+ await channel.bindQueue(queue, this.exchange, "");
472
+ }
473
+ const { autoAck = true } = options;
474
+ await channel.consume(
475
+ queue,
476
+ async (msg) => {
477
+ if (!msg) {
478
+ return;
479
+ }
480
+ const job = JSON.parse(msg.content.toString());
481
+ job._raw = msg;
482
+ await callback(job);
483
+ if (autoAck) {
484
+ channel.ack(msg);
485
+ }
486
+ },
487
+ { noAck: false }
488
+ );
489
+ }
490
+ /**
491
+ * Get queue size.
492
+ */
493
+ async size(queue) {
494
+ const channel = await this.ensureChannel();
495
+ const ok = await channel.checkQueue(queue);
496
+ return ok.messageCount;
497
+ }
498
+ /**
499
+ * Clear a queue.
500
+ */
501
+ async clear(queue) {
502
+ const channel = await this.ensureChannel();
503
+ await channel.purgeQueue(queue);
504
+ }
505
+ };
506
+ }
507
+ });
508
+
320
509
  // src/drivers/RedisDriver.ts
321
510
  var RedisDriver_exports = {};
322
511
  __export(RedisDriver_exports, {
@@ -326,9 +515,43 @@ var RedisDriver;
326
515
  var init_RedisDriver = __esm({
327
516
  "src/drivers/RedisDriver.ts"() {
328
517
  "use strict";
329
- RedisDriver = class {
518
+ RedisDriver = class _RedisDriver {
330
519
  prefix;
331
520
  client;
521
+ // Lua Logic:
522
+ // IF (IS_MEMBER(activeSet, groupId)) -> PUSH(pendingList, job)
523
+ // ELSE -> SADD(activeSet, groupId) & LPUSH(waitList, job)
524
+ static PUSH_SCRIPT = `
525
+ local waitList = KEYS[1]
526
+ local activeSet = KEYS[2]
527
+ local pendingList = KEYS[3]
528
+ local groupId = ARGV[1]
529
+ local payload = ARGV[2]
530
+
531
+ if redis.call('SISMEMBER', activeSet, groupId) == 1 then
532
+ return redis.call('RPUSH', pendingList, payload)
533
+ else
534
+ redis.call('SADD', activeSet, groupId)
535
+ return redis.call('LPUSH', waitList, payload)
536
+ end
537
+ `;
538
+ // Lua Logic:
539
+ // local next = LPOP(pendingList)
540
+ // IF (next) -> LPUSH(waitList, next)
541
+ // ELSE -> SREM(activeSet, groupId)
542
+ static COMPLETE_SCRIPT = `
543
+ local waitList = KEYS[1]
544
+ local activeSet = KEYS[2]
545
+ local pendingList = KEYS[3]
546
+ local groupId = ARGV[1]
547
+
548
+ local nextJob = redis.call('LPOP', pendingList)
549
+ if nextJob then
550
+ return redis.call('LPUSH', waitList, nextJob)
551
+ else
552
+ return redis.call('SREM', activeSet, groupId)
553
+ end
554
+ `;
332
555
  constructor(config) {
333
556
  this.client = config.client;
334
557
  this.prefix = config.prefix ?? "queue:";
@@ -337,19 +560,36 @@ var init_RedisDriver = __esm({
337
560
  "[RedisDriver] Redis client is required. Please install ioredis or redis package."
338
561
  );
339
562
  }
563
+ if (typeof this.client.defineCommand === "function") {
564
+ ;
565
+ this.client.defineCommand("pushGroupJob", {
566
+ numberOfKeys: 3,
567
+ lua: _RedisDriver.PUSH_SCRIPT
568
+ });
569
+ this.client.defineCommand("completeGroupJob", {
570
+ numberOfKeys: 3,
571
+ lua: _RedisDriver.COMPLETE_SCRIPT
572
+ });
573
+ }
340
574
  }
341
575
  /**
342
576
  * Get full Redis key for a queue.
343
577
  */
344
- getKey(queue) {
578
+ getKey(queue, priority) {
579
+ if (priority) {
580
+ return `${this.prefix}${queue}:${priority}`;
581
+ }
345
582
  return `${this.prefix}${queue}`;
346
583
  }
347
584
  /**
348
585
  * Push a job (LPUSH).
349
586
  */
350
- async push(queue, job) {
351
- const key = this.getKey(queue);
352
- const payload = JSON.stringify({
587
+ async push(queue, job, options) {
588
+ const key = this.getKey(queue, options?.priority);
589
+ const groupId = options?.groupId;
590
+ if (groupId && options?.priority) {
591
+ }
592
+ const payloadObj = {
353
593
  id: job.id,
354
594
  type: job.type,
355
595
  data: job.data,
@@ -357,8 +597,18 @@ var init_RedisDriver = __esm({
357
597
  createdAt: job.createdAt,
358
598
  delaySeconds: job.delaySeconds,
359
599
  attempts: job.attempts,
360
- maxAttempts: job.maxAttempts
361
- });
600
+ maxAttempts: job.maxAttempts,
601
+ groupId,
602
+ error: job.error,
603
+ failedAt: job.failedAt
604
+ };
605
+ const payload = JSON.stringify(payloadObj);
606
+ if (groupId && typeof this.client.pushGroupJob === "function") {
607
+ const activeSetKey = `${this.prefix}active`;
608
+ const pendingListKey = `${this.prefix}pending:${groupId}`;
609
+ await this.client.pushGroupJob(key, activeSetKey, pendingListKey, groupId, payload);
610
+ return;
611
+ }
362
612
  if (job.delaySeconds && job.delaySeconds > 0) {
363
613
  const delayKey = `${key}:delayed`;
364
614
  const score = Date.now() + job.delaySeconds * 1e3;
@@ -371,29 +621,53 @@ var init_RedisDriver = __esm({
371
621
  await this.client.lpush(key, payload);
372
622
  }
373
623
  }
624
+ /**
625
+ * Complete a job (handle Group FIFO).
626
+ */
627
+ async complete(queue, job) {
628
+ if (!job.groupId) {
629
+ return;
630
+ }
631
+ const key = this.getKey(queue);
632
+ const activeSetKey = `${this.prefix}active`;
633
+ const pendingListKey = `${this.prefix}pending:${job.groupId}`;
634
+ if (typeof this.client.completeGroupJob === "function") {
635
+ await this.client.completeGroupJob(key, activeSetKey, pendingListKey, job.groupId);
636
+ }
637
+ }
374
638
  /**
375
639
  * Pop a job (RPOP, FIFO).
640
+ * Supports implicit priority polling (critical -> high -> default -> low).
376
641
  */
377
642
  async pop(queue) {
378
- const key = this.getKey(queue);
379
- const delayKey = `${key}:delayed`;
380
- if (typeof this.client.zrange === "function") {
381
- const now = Date.now();
382
- const delayedJobs = await this.client.zrange(delayKey, 0, 0, true);
383
- if (delayedJobs && delayedJobs.length >= 2) {
384
- const score = parseFloat(delayedJobs[1]);
385
- if (score <= now) {
386
- const payload2 = delayedJobs[0];
387
- await this.client.zrem(delayKey, payload2);
388
- return this.parsePayload(payload2);
643
+ const priorities = ["critical", "high", void 0, "low"];
644
+ for (const priority of priorities) {
645
+ const key = this.getKey(queue, priority);
646
+ const delayKey = `${key}:delayed`;
647
+ if (typeof this.client.zrange === "function") {
648
+ const now = Date.now();
649
+ const delayedJobs = await this.client.zrange(delayKey, 0, 0, "WITHSCORES");
650
+ if (delayedJobs && delayedJobs.length >= 2) {
651
+ const score = parseFloat(delayedJobs[1]);
652
+ if (score <= now) {
653
+ const payload2 = delayedJobs[0];
654
+ await this.client.zrem(delayKey, payload2);
655
+ return this.parsePayload(payload2);
656
+ }
389
657
  }
390
658
  }
659
+ if (typeof this.client.get === "function") {
660
+ const isPaused = await this.client.get(`${key}:paused`);
661
+ if (isPaused === "1") {
662
+ continue;
663
+ }
664
+ }
665
+ const payload = await this.client.rpop(key);
666
+ if (payload) {
667
+ return this.parsePayload(payload);
668
+ }
391
669
  }
392
- const payload = await this.client.rpop(key);
393
- if (!payload) {
394
- return null;
395
- }
396
- return this.parsePayload(payload);
670
+ return null;
397
671
  }
398
672
  /**
399
673
  * Parse Redis payload.
@@ -408,7 +682,11 @@ var init_RedisDriver = __esm({
408
682
  createdAt: parsed.createdAt,
409
683
  delaySeconds: parsed.delaySeconds,
410
684
  attempts: parsed.attempts,
411
- maxAttempts: parsed.maxAttempts
685
+ maxAttempts: parsed.maxAttempts,
686
+ groupId: parsed.groupId,
687
+ error: parsed.error,
688
+ failedAt: parsed.failedAt,
689
+ priority: parsed.priority
412
690
  };
413
691
  }
414
692
  /**
@@ -418,15 +696,31 @@ var init_RedisDriver = __esm({
418
696
  const key = this.getKey(queue);
419
697
  return this.client.llen(key);
420
698
  }
699
+ /**
700
+ * Mark a job as permanently failed (DLQ).
701
+ */
702
+ async fail(queue, job) {
703
+ const key = `${this.getKey(queue)}:failed`;
704
+ const payload = JSON.stringify({
705
+ ...job,
706
+ failedAt: Date.now()
707
+ });
708
+ await this.client.lpush(key, payload);
709
+ if (typeof this.client.ltrim === "function") {
710
+ await this.client.ltrim(key, 0, 999);
711
+ }
712
+ }
421
713
  /**
422
714
  * Clear a queue.
423
715
  */
424
716
  async clear(queue) {
425
717
  const key = this.getKey(queue);
426
718
  const delayKey = `${key}:delayed`;
719
+ const activeSetKey = `${this.prefix}active`;
427
720
  await this.client.del(key);
428
721
  if (typeof this.client.del === "function") {
429
722
  await this.client.del(delayKey);
723
+ await this.client.del(activeSetKey);
430
724
  }
431
725
  }
432
726
  /**
@@ -436,6 +730,17 @@ var init_RedisDriver = __esm({
436
730
  if (jobs.length === 0) {
437
731
  return;
438
732
  }
733
+ const hasGroup = jobs.some((j) => j.groupId);
734
+ const hasPriority = jobs.some((j) => j.priority);
735
+ if (hasGroup || hasPriority) {
736
+ for (const job of jobs) {
737
+ await this.push(queue, job, {
738
+ groupId: job.groupId,
739
+ priority: job.priority
740
+ });
741
+ }
742
+ return;
743
+ }
439
744
  const key = this.getKey(queue);
440
745
  const payloads = jobs.map(
441
746
  (job) => JSON.stringify({
@@ -446,7 +751,9 @@ var init_RedisDriver = __esm({
446
751
  createdAt: job.createdAt,
447
752
  delaySeconds: job.delaySeconds,
448
753
  attempts: job.attempts,
449
- maxAttempts: job.maxAttempts
754
+ maxAttempts: job.maxAttempts,
755
+ groupId: job.groupId,
756
+ priority: job.priority
450
757
  })
451
758
  );
452
759
  await this.client.lpush(key, ...payloads);
@@ -467,6 +774,90 @@ var init_RedisDriver = __esm({
467
774
  }
468
775
  return results;
469
776
  }
777
+ /**
778
+ * Report worker heartbeat for monitoring.
779
+ */
780
+ async reportHeartbeat(workerInfo, prefix) {
781
+ const key = `${prefix ?? this.prefix}worker:${workerInfo.id}`;
782
+ if (typeof this.client.set === "function") {
783
+ await this.client.set(key, JSON.stringify(workerInfo), "EX", 10);
784
+ }
785
+ }
786
+ /**
787
+ * Publish a log message for monitoring.
788
+ */
789
+ async publishLog(logPayload, prefix) {
790
+ const payload = JSON.stringify(logPayload);
791
+ const monitorPrefix = prefix ?? this.prefix;
792
+ if (typeof this.client.publish === "function") {
793
+ await this.client.publish(`${monitorPrefix}logs`, payload);
794
+ }
795
+ const historyKey = `${monitorPrefix}logs:history`;
796
+ if (typeof this.client.pipeline === "function") {
797
+ const pipe = this.client.pipeline();
798
+ pipe.lpush(historyKey, payload);
799
+ pipe.ltrim(historyKey, 0, 99);
800
+ await pipe.exec();
801
+ } else {
802
+ await this.client.lpush(historyKey, payload);
803
+ }
804
+ }
805
+ /**
806
+ * Check if a queue is rate limited.
807
+ * Uses a fixed window counter.
808
+ */
809
+ async checkRateLimit(queue, config) {
810
+ const key = `${this.prefix}${queue}:ratelimit`;
811
+ const now = Date.now();
812
+ const windowStart = Math.floor(now / config.duration);
813
+ const windowKey = `${key}:${windowStart}`;
814
+ const client = this.client;
815
+ if (typeof client.incr === "function") {
816
+ const current = await client.incr(windowKey);
817
+ if (current === 1) {
818
+ await client.expire(windowKey, Math.ceil(config.duration / 1e3) + 1);
819
+ }
820
+ return current <= config.max;
821
+ }
822
+ return true;
823
+ }
824
+ /**
825
+ * Get failed jobs from DLQ.
826
+ */
827
+ async getFailed(queue, start = 0, end = -1) {
828
+ const key = `${this.getKey(queue)}:failed`;
829
+ const payloads = await this.client.lrange(key, start, end);
830
+ return payloads.map((p) => this.parsePayload(p));
831
+ }
832
+ /**
833
+ * Retry failed jobs from DLQ.
834
+ * Moves jobs from failed list back to the main queue.
835
+ */
836
+ async retryFailed(queue, count = 1) {
837
+ const failedKey = `${this.getKey(queue)}:failed`;
838
+ const queueKey = this.getKey(queue);
839
+ let retried = 0;
840
+ for (let i = 0; i < count; i++) {
841
+ const payload = await this.client.rpop(failedKey);
842
+ if (!payload) {
843
+ break;
844
+ }
845
+ const job = this.parsePayload(payload);
846
+ job.attempts = 0;
847
+ delete job.error;
848
+ delete job.failedAt;
849
+ await this.push(queue, job, { priority: job.priority, groupId: job.groupId });
850
+ retried++;
851
+ }
852
+ return retried;
853
+ }
854
+ /**
855
+ * Clear failed jobs from DLQ.
856
+ */
857
+ async clearFailed(queue) {
858
+ const key = `${this.getKey(queue)}:failed`;
859
+ await this.client.del(key);
860
+ }
470
861
  };
471
862
  }
472
863
  });
@@ -672,6 +1063,126 @@ var init_SQSDriver = __esm({
672
1063
  }
673
1064
  });
674
1065
 
1066
+ // src/Scheduler.ts
1067
+ var Scheduler_exports = {};
1068
+ __export(Scheduler_exports, {
1069
+ Scheduler: () => Scheduler
1070
+ });
1071
+ import parser from "cron-parser";
1072
+ var Scheduler;
1073
+ var init_Scheduler = __esm({
1074
+ "src/Scheduler.ts"() {
1075
+ "use strict";
1076
+ Scheduler = class {
1077
+ constructor(manager, options = {}) {
1078
+ this.manager = manager;
1079
+ this.prefix = options.prefix ?? "queue:";
1080
+ }
1081
+ prefix;
1082
+ get client() {
1083
+ const driver = this.manager.getDriver(this.manager.getDefaultConnection());
1084
+ return driver.client;
1085
+ }
1086
+ /**
1087
+ * Register a scheduled job.
1088
+ */
1089
+ async register(config) {
1090
+ const nextRun = parser.parse(config.cron).next().getTime();
1091
+ const fullConfig = {
1092
+ ...config,
1093
+ nextRun,
1094
+ enabled: true
1095
+ };
1096
+ const pipe = this.client.pipeline();
1097
+ pipe.hset(`${this.prefix}schedule:${config.id}`, {
1098
+ ...fullConfig,
1099
+ job: JSON.stringify(fullConfig.job)
1100
+ });
1101
+ pipe.zadd(`${this.prefix}schedules`, nextRun, config.id);
1102
+ await pipe.exec();
1103
+ }
1104
+ /**
1105
+ * Remove a scheduled job.
1106
+ */
1107
+ async remove(id) {
1108
+ const pipe = this.client.pipeline();
1109
+ pipe.del(`${this.prefix}schedule:${id}`);
1110
+ pipe.zrem(`${this.prefix}schedules`, id);
1111
+ await pipe.exec();
1112
+ }
1113
+ /**
1114
+ * List all scheduled jobs.
1115
+ */
1116
+ async list() {
1117
+ const ids = await this.client.zrange(`${this.prefix}schedules`, 0, -1);
1118
+ const configs = [];
1119
+ for (const id of ids) {
1120
+ const data = await this.client.hgetall(`${this.prefix}schedule:${id}`);
1121
+ if (data?.id) {
1122
+ configs.push({
1123
+ ...data,
1124
+ lastRun: data.lastRun ? parseInt(data.lastRun, 10) : void 0,
1125
+ nextRun: data.nextRun ? parseInt(data.nextRun, 10) : void 0,
1126
+ enabled: data.enabled === "true",
1127
+ job: JSON.parse(data.job)
1128
+ });
1129
+ }
1130
+ }
1131
+ return configs;
1132
+ }
1133
+ /**
1134
+ * Run a scheduled job immediately (out of schedule).
1135
+ */
1136
+ async runNow(id) {
1137
+ const data = await this.client.hgetall(`${this.prefix}schedule:${id}`);
1138
+ if (data?.id) {
1139
+ const serialized = JSON.parse(data.job);
1140
+ const serializer = this.manager.getSerializer();
1141
+ const job = serializer.deserialize(serialized);
1142
+ await this.manager.push(job);
1143
+ }
1144
+ }
1145
+ /**
1146
+ * Process due tasks (TICK).
1147
+ * This should be called periodically (e.g. every minute).
1148
+ */
1149
+ async tick() {
1150
+ const now = Date.now();
1151
+ const dueIds = await this.client.zrangebyscore(`${this.prefix}schedules`, 0, now);
1152
+ let fired = 0;
1153
+ const serializer = this.manager.getSerializer();
1154
+ for (const id of dueIds) {
1155
+ const lockKey = `${this.prefix}lock:schedule:${id}:${Math.floor(now / 1e3)}`;
1156
+ const lock = await this.client.set(lockKey, "1", "EX", 10, "NX");
1157
+ if (lock === "OK") {
1158
+ const data = await this.client.hgetall(`${this.prefix}schedule:${id}`);
1159
+ if (data?.id && data.enabled === "true") {
1160
+ try {
1161
+ const serializedJob = JSON.parse(data.job);
1162
+ const connection = data.connection || this.manager.getDefaultConnection();
1163
+ const driver = this.manager.getDriver(connection);
1164
+ await driver.push(data.queue, serializedJob);
1165
+ const nextRun = parser.parse(data.cron).next().getTime();
1166
+ const pipe = this.client.pipeline();
1167
+ pipe.hset(`${this.prefix}schedule:${id}`, {
1168
+ lastRun: now,
1169
+ nextRun
1170
+ });
1171
+ pipe.zadd(`${this.prefix}schedules`, nextRun, id);
1172
+ await pipe.exec();
1173
+ fired++;
1174
+ } catch (err) {
1175
+ console.error(`[Scheduler] Failed to process schedule ${id}:`, err);
1176
+ }
1177
+ }
1178
+ }
1179
+ }
1180
+ return fired;
1181
+ }
1182
+ };
1183
+ }
1184
+ });
1185
+
675
1186
  // src/Worker.ts
676
1187
  var Worker = class {
677
1188
  constructor(options = {}) {
@@ -682,36 +1193,31 @@ var Worker = class {
682
1193
  * @param job - Job instance
683
1194
  */
684
1195
  async process(job) {
685
- const maxAttempts = this.options.maxAttempts ?? 3;
1196
+ const maxAttempts = job.maxAttempts ?? this.options.maxAttempts ?? 3;
686
1197
  const timeout = this.options.timeout;
687
- let lastError = null;
688
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
689
- try {
690
- job.attempts = attempt;
691
- job.maxAttempts = maxAttempts;
692
- if (timeout) {
693
- await Promise.race([
694
- job.handle(),
695
- new Promise(
696
- (_, reject) => setTimeout(
697
- () => reject(new Error(`Job timeout after ${timeout} seconds`)),
698
- timeout * 1e3
699
- )
1198
+ if (!job.attempts) {
1199
+ job.attempts = 1;
1200
+ }
1201
+ try {
1202
+ if (timeout) {
1203
+ await Promise.race([
1204
+ job.handle(),
1205
+ new Promise(
1206
+ (_, reject) => setTimeout(
1207
+ () => reject(new Error(`Job timeout after ${timeout} seconds`)),
1208
+ timeout * 1e3
700
1209
  )
701
- ]);
702
- } else {
703
- await job.handle();
704
- }
705
- return;
706
- } catch (error) {
707
- lastError = error instanceof Error ? error : new Error(String(error));
708
- if (attempt === maxAttempts) {
709
- await this.handleFailure(job, lastError);
710
- throw lastError;
711
- }
712
- const delay = Math.min(1e3 * 2 ** (attempt - 1), 3e4);
713
- await new Promise((resolve) => setTimeout(resolve, delay));
1210
+ )
1211
+ ]);
1212
+ } else {
1213
+ await job.handle();
1214
+ }
1215
+ } catch (error) {
1216
+ const err = error instanceof Error ? error : new Error(String(error));
1217
+ if (job.attempts >= maxAttempts) {
1218
+ await this.handleFailure(job, err);
714
1219
  }
1220
+ throw err;
715
1221
  }
716
1222
  }
717
1223
  /**
@@ -741,6 +1247,11 @@ var Consumer = class {
741
1247
  }
742
1248
  running = false;
743
1249
  stopRequested = false;
1250
+ workerId = `worker-${Math.random().toString(36).substring(2, 8)}`;
1251
+ heartbeatTimer = null;
1252
+ get connectionName() {
1253
+ return this.options.connection ?? this.queueManager.getDefaultConnection();
1254
+ }
744
1255
  /**
745
1256
  * Start the consumer loop.
746
1257
  */
@@ -755,18 +1266,72 @@ var Consumer = class {
755
1266
  const keepAlive = this.options.keepAlive ?? true;
756
1267
  console.log("[Consumer] Started", {
757
1268
  queues: this.options.queues,
758
- connection: this.options.connection
1269
+ connection: this.options.connection,
1270
+ workerId: this.workerId
759
1271
  });
1272
+ if (this.options.monitor) {
1273
+ this.startHeartbeat();
1274
+ await this.publishLog("info", `Consumer started on [${this.options.queues.join(", ")}]`);
1275
+ }
760
1276
  while (this.running && !this.stopRequested) {
761
1277
  let processed = false;
762
1278
  for (const queue of this.options.queues) {
1279
+ if (this.options.rateLimits?.[queue]) {
1280
+ const limit = this.options.rateLimits[queue];
1281
+ try {
1282
+ const driver = this.queueManager.getDriver(this.connectionName);
1283
+ if (driver.checkRateLimit) {
1284
+ const allowed = await driver.checkRateLimit(queue, limit);
1285
+ if (!allowed) {
1286
+ continue;
1287
+ }
1288
+ }
1289
+ } catch (err) {
1290
+ console.error(`[Consumer] Error checking rate limit for "${queue}":`, err);
1291
+ }
1292
+ }
763
1293
  try {
764
1294
  const job = await this.queueManager.pop(queue, this.options.connection);
765
1295
  if (job) {
766
1296
  processed = true;
767
- await worker.process(job).catch((error) => {
768
- console.error(`[Consumer] Error processing job in queue "${queue}":`, error);
769
- });
1297
+ if (this.options.monitor) {
1298
+ await this.publishLog("info", `Processing job: ${job.id}`, job.id);
1299
+ }
1300
+ try {
1301
+ await worker.process(job);
1302
+ if (this.options.monitor) {
1303
+ await this.publishLog("success", `Completed job: ${job.id}`, job.id);
1304
+ }
1305
+ } catch (err) {
1306
+ console.error(`[Consumer] Error processing job in queue "${queue}":`, err);
1307
+ if (this.options.monitor) {
1308
+ await this.publishLog("error", `Job failed: ${job.id} - ${err.message}`, job.id);
1309
+ }
1310
+ const attempts = job.attempts ?? 1;
1311
+ const maxAttempts = job.maxAttempts ?? this.options.workerOptions?.maxAttempts ?? 3;
1312
+ if (attempts < maxAttempts) {
1313
+ job.attempts = attempts + 1;
1314
+ const delayMs = job.getRetryDelay(job.attempts);
1315
+ const delaySec = Math.ceil(delayMs / 1e3);
1316
+ job.delay(delaySec);
1317
+ await this.queueManager.push(job);
1318
+ if (this.options.monitor) {
1319
+ await this.publishLog(
1320
+ "warning",
1321
+ `Job retrying in ${delaySec}s (Attempt ${job.attempts}/${maxAttempts})`,
1322
+ job.id
1323
+ );
1324
+ }
1325
+ } else {
1326
+ await this.queueManager.fail(job, err).catch((dlqErr) => {
1327
+ console.error(`[Consumer] Error moving job to DLQ:`, dlqErr);
1328
+ });
1329
+ }
1330
+ } finally {
1331
+ await this.queueManager.complete(job).catch((err) => {
1332
+ console.error(`[Consumer] Error completing job in queue "${queue}":`, err);
1333
+ });
1334
+ }
770
1335
  }
771
1336
  } catch (error) {
772
1337
  console.error(`[Consumer] Error polling queue "${queue}":`, error);
@@ -775,13 +1340,83 @@ var Consumer = class {
775
1340
  if (!processed && !keepAlive) {
776
1341
  break;
777
1342
  }
778
- if (!this.stopRequested) {
1343
+ if (!this.stopRequested && !processed) {
779
1344
  await new Promise((resolve) => setTimeout(resolve, pollInterval));
1345
+ } else if (!this.stopRequested && processed) {
1346
+ await new Promise((resolve) => setTimeout(resolve, 0));
780
1347
  }
781
1348
  }
782
1349
  this.running = false;
1350
+ this.stopHeartbeat();
1351
+ if (this.options.monitor) {
1352
+ await this.publishLog("info", "Consumer stopped");
1353
+ }
783
1354
  console.log("[Consumer] Stopped");
784
1355
  }
1356
+ startHeartbeat() {
1357
+ const interval = typeof this.options.monitor === "object" ? this.options.monitor.interval ?? 5e3 : 5e3;
1358
+ const monitorOptions = typeof this.options.monitor === "object" ? this.options.monitor : {};
1359
+ this.heartbeatTimer = setInterval(async () => {
1360
+ try {
1361
+ const driver = this.queueManager.getDriver(this.connectionName);
1362
+ if (driver.reportHeartbeat) {
1363
+ const monitorPrefix = typeof this.options.monitor === "object" ? this.options.monitor.prefix : void 0;
1364
+ const os = __require("os");
1365
+ const mem = process.memoryUsage();
1366
+ const metrics = {
1367
+ cpu: os.loadavg()[0],
1368
+ // 1m load avg
1369
+ cores: os.cpus().length,
1370
+ ram: {
1371
+ rss: Math.floor(mem.rss / 1024 / 1024),
1372
+ heapUsed: Math.floor(mem.heapUsed / 1024 / 1024),
1373
+ total: Math.floor(os.totalmem() / 1024 / 1024)
1374
+ }
1375
+ };
1376
+ await driver.reportHeartbeat(
1377
+ {
1378
+ id: this.workerId,
1379
+ status: "online",
1380
+ hostname: os.hostname(),
1381
+ pid: process.pid,
1382
+ uptime: Math.floor(process.uptime()),
1383
+ last_ping: (/* @__PURE__ */ new Date()).toISOString(),
1384
+ queues: this.options.queues,
1385
+ metrics,
1386
+ ...monitorOptions.extraInfo || {}
1387
+ },
1388
+ monitorPrefix
1389
+ );
1390
+ }
1391
+ } catch (_e) {
1392
+ }
1393
+ }, interval);
1394
+ }
1395
+ stopHeartbeat() {
1396
+ if (this.heartbeatTimer) {
1397
+ clearInterval(this.heartbeatTimer);
1398
+ this.heartbeatTimer = null;
1399
+ }
1400
+ }
1401
+ async publishLog(level, message, jobId) {
1402
+ try {
1403
+ const driver = this.queueManager.getDriver(this.connectionName);
1404
+ if (driver.publishLog) {
1405
+ const monitorPrefix = typeof this.options.monitor === "object" ? this.options.monitor.prefix : void 0;
1406
+ await driver.publishLog(
1407
+ {
1408
+ level,
1409
+ message,
1410
+ workerId: this.workerId,
1411
+ jobId,
1412
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1413
+ },
1414
+ monitorPrefix
1415
+ );
1416
+ }
1417
+ } catch (_e) {
1418
+ }
1419
+ }
785
1420
  /**
786
1421
  * Stop the consumer loop (graceful shutdown).
787
1422
  */
@@ -872,11 +1507,16 @@ var MemoryDriver = class {
872
1507
  };
873
1508
 
874
1509
  // src/index.ts
1510
+ init_RabbitMQDriver();
875
1511
  init_RedisDriver();
876
1512
  init_SQSDriver();
877
1513
 
878
1514
  // src/Job.ts
879
1515
  var Job = class {
1516
+ /**
1517
+ * Unique job identifier.
1518
+ */
1519
+ id;
880
1520
  /**
881
1521
  * Queue name.
882
1522
  */
@@ -897,6 +1537,22 @@ var Job = class {
897
1537
  * Maximum attempts.
898
1538
  */
899
1539
  maxAttempts;
1540
+ /**
1541
+ * Group ID for FIFO.
1542
+ */
1543
+ groupId;
1544
+ /**
1545
+ * Job priority.
1546
+ */
1547
+ priority;
1548
+ /**
1549
+ * Initial retry delay (seconds).
1550
+ */
1551
+ retryAfterSeconds;
1552
+ /**
1553
+ * Retry delay multiplier.
1554
+ */
1555
+ retryMultiplier;
900
1556
  /**
901
1557
  * Set target queue.
902
1558
  */
@@ -911,6 +1567,14 @@ var Job = class {
911
1567
  this.connectionName = connection;
912
1568
  return this;
913
1569
  }
1570
+ /**
1571
+ * Set job priority.
1572
+ * @param priority - 'high', 'low', or number
1573
+ */
1574
+ withPriority(priority) {
1575
+ this.priority = priority;
1576
+ return this;
1577
+ }
914
1578
  /**
915
1579
  * Set delay (seconds).
916
1580
  */
@@ -918,6 +1582,26 @@ var Job = class {
918
1582
  this.delaySeconds = delay;
919
1583
  return this;
920
1584
  }
1585
+ /**
1586
+ * Set retry backoff strategy.
1587
+ * @param seconds - Initial delay in seconds
1588
+ * @param multiplier - Multiplier for each subsequent attempt (default: 2)
1589
+ */
1590
+ backoff(seconds, multiplier = 2) {
1591
+ this.retryAfterSeconds = seconds;
1592
+ this.retryMultiplier = multiplier;
1593
+ return this;
1594
+ }
1595
+ /**
1596
+ * Calculate retry delay for the next attempt.
1597
+ * @param attempt - Current attempt number (1-based)
1598
+ * @returns Delay in milliseconds
1599
+ */
1600
+ getRetryDelay(attempt) {
1601
+ const initialDelay = (this.retryAfterSeconds ?? 1) * 1e3;
1602
+ const multiplier = this.retryMultiplier ?? 2;
1603
+ return Math.min(initialDelay * multiplier ** (attempt - 1), 36e5);
1604
+ }
921
1605
  /**
922
1606
  * Failure handler (optional).
923
1607
  *
@@ -956,7 +1640,7 @@ var ClassNameSerializer = class {
956
1640
  * Serialize a Job.
957
1641
  */
958
1642
  serialize(job) {
959
- const id = `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
1643
+ const id = job.id || `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
960
1644
  const className = job.constructor.name;
961
1645
  const properties = {};
962
1646
  for (const key in job) {
@@ -975,7 +1659,11 @@ var ClassNameSerializer = class {
975
1659
  createdAt: Date.now(),
976
1660
  ...job.delaySeconds !== void 0 ? { delaySeconds: job.delaySeconds } : {},
977
1661
  attempts: job.attempts ?? 0,
978
- ...job.maxAttempts !== void 0 ? { maxAttempts: job.maxAttempts } : {}
1662
+ ...job.maxAttempts !== void 0 ? { maxAttempts: job.maxAttempts } : {},
1663
+ ...job.groupId ? { groupId: job.groupId } : {},
1664
+ ...job.retryAfterSeconds !== void 0 ? { retryAfterSeconds: job.retryAfterSeconds } : {},
1665
+ ...job.retryMultiplier !== void 0 ? { retryMultiplier: job.retryMultiplier } : {},
1666
+ ...job.priority !== void 0 ? { priority: job.priority } : {}
979
1667
  };
980
1668
  }
981
1669
  /**
@@ -999,6 +1687,7 @@ var ClassNameSerializer = class {
999
1687
  if (parsed.properties) {
1000
1688
  Object.assign(job, parsed.properties);
1001
1689
  }
1690
+ job.id = serialized.id;
1002
1691
  if (serialized.delaySeconds !== void 0) {
1003
1692
  job.delaySeconds = serialized.delaySeconds;
1004
1693
  }
@@ -1008,6 +1697,18 @@ var ClassNameSerializer = class {
1008
1697
  if (serialized.maxAttempts !== void 0) {
1009
1698
  job.maxAttempts = serialized.maxAttempts;
1010
1699
  }
1700
+ if (serialized.groupId !== void 0) {
1701
+ job.groupId = serialized.groupId;
1702
+ }
1703
+ if (serialized.retryAfterSeconds !== void 0) {
1704
+ job.retryAfterSeconds = serialized.retryAfterSeconds;
1705
+ }
1706
+ if (serialized.retryMultiplier !== void 0) {
1707
+ job.retryMultiplier = serialized.retryMultiplier;
1708
+ }
1709
+ if (serialized.priority !== void 0) {
1710
+ job.priority = serialized.priority;
1711
+ }
1011
1712
  return job;
1012
1713
  }
1013
1714
  };
@@ -1029,7 +1730,9 @@ var JsonSerializer = class {
1029
1730
  createdAt: Date.now(),
1030
1731
  ...job.delaySeconds !== void 0 ? { delaySeconds: job.delaySeconds } : {},
1031
1732
  attempts: job.attempts ?? 0,
1032
- ...job.maxAttempts !== void 0 ? { maxAttempts: job.maxAttempts } : {}
1733
+ ...job.maxAttempts !== void 0 ? { maxAttempts: job.maxAttempts } : {},
1734
+ ...job.groupId ? { groupId: job.groupId } : {},
1735
+ ...job.priority ? { priority: job.priority } : {}
1033
1736
  };
1034
1737
  }
1035
1738
  /**
@@ -1045,6 +1748,12 @@ var JsonSerializer = class {
1045
1748
  const parsed = JSON.parse(serialized.data);
1046
1749
  const job = /* @__PURE__ */ Object.create({});
1047
1750
  Object.assign(job, parsed.properties);
1751
+ if (serialized.groupId) {
1752
+ job.groupId = serialized.groupId;
1753
+ }
1754
+ if (serialized.priority) {
1755
+ job.priority = serialized.priority;
1756
+ }
1048
1757
  return job;
1049
1758
  }
1050
1759
  };
@@ -1055,7 +1764,11 @@ var QueueManager = class {
1055
1764
  serializers = /* @__PURE__ */ new Map();
1056
1765
  defaultConnection;
1057
1766
  defaultSerializer;
1767
+ persistence;
1768
+ scheduler;
1769
+ // Using any to avoid circular dependency or import issues for now
1058
1770
  constructor(config = {}) {
1771
+ this.persistence = config.persistence;
1059
1772
  this.defaultConnection = config.default ?? "default";
1060
1773
  const serializerType = config.defaultSerializer ?? "class";
1061
1774
  if (serializerType === "class") {
@@ -1163,9 +1876,30 @@ var QueueManager = class {
1163
1876
  );
1164
1877
  break;
1165
1878
  }
1879
+ case "rabbitmq": {
1880
+ const { RabbitMQDriver: RabbitMQDriver2 } = (init_RabbitMQDriver(), __toCommonJS(RabbitMQDriver_exports));
1881
+ const client = config.client;
1882
+ if (!client) {
1883
+ throw new Error(
1884
+ "[QueueManager] RabbitMQDriver requires client. Please provide RabbitMQ connection/channel in connection config."
1885
+ );
1886
+ }
1887
+ this.drivers.set(
1888
+ name,
1889
+ new RabbitMQDriver2({
1890
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic driver loading requires type assertion
1891
+ client,
1892
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic driver config type
1893
+ exchange: config.exchange,
1894
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic driver config type
1895
+ exchangeType: config.exchangeType
1896
+ })
1897
+ );
1898
+ break;
1899
+ }
1166
1900
  default:
1167
1901
  throw new Error(
1168
- `Driver "${driverType}" is not supported. Supported drivers: memory, database, redis, kafka, sqs`
1902
+ `Driver "${driverType}" is not supported. Supported drivers: memory, database, redis, kafka, sqs, rabbitmq`
1169
1903
  );
1170
1904
  }
1171
1905
  }
@@ -1181,6 +1915,13 @@ var QueueManager = class {
1181
1915
  }
1182
1916
  return driver;
1183
1917
  }
1918
+ /**
1919
+ * Get the default connection name.
1920
+ * @returns Default connection name
1921
+ */
1922
+ getDefaultConnection() {
1923
+ return this.defaultConnection;
1924
+ }
1184
1925
  /**
1185
1926
  * Get a serializer.
1186
1927
  * @param type - Serializer type
@@ -1210,6 +1951,7 @@ var QueueManager = class {
1210
1951
  *
1211
1952
  * @template T - The type of the job.
1212
1953
  * @param job - Job instance to push.
1954
+ * @param options - Push options.
1213
1955
  * @returns The same job instance (for fluent chaining).
1214
1956
  *
1215
1957
  * @example
@@ -1217,13 +1959,22 @@ var QueueManager = class {
1217
1959
  * await manager.push(new SendEmailJob('user@example.com'));
1218
1960
  * ```
1219
1961
  */
1220
- async push(job) {
1962
+ async push(job, options) {
1221
1963
  const connection = job.connectionName ?? this.defaultConnection;
1222
1964
  const queue = job.queueName ?? "default";
1223
1965
  const driver = this.getDriver(connection);
1224
1966
  const serializer = this.getSerializer();
1225
1967
  const serialized = serializer.serialize(job);
1226
- await driver.push(queue, serialized);
1968
+ const pushOptions = { ...options };
1969
+ if (job.priority) {
1970
+ pushOptions.priority = job.priority;
1971
+ }
1972
+ await driver.push(queue, serialized, pushOptions);
1973
+ if (this.persistence?.archiveEnqueued) {
1974
+ this.persistence.adapter.archive(queue, serialized, "waiting").catch((err) => {
1975
+ console.error("[QueueManager] Persistence archive failed (waiting):", err);
1976
+ });
1977
+ }
1227
1978
  return job;
1228
1979
  }
1229
1980
  /**
@@ -1316,6 +2067,92 @@ var QueueManager = class {
1316
2067
  const driver = this.getDriver(connection);
1317
2068
  await driver.clear(queue);
1318
2069
  }
2070
+ /**
2071
+ * Mark a job as completed.
2072
+ * @param job - Job instance
2073
+ */
2074
+ async complete(job) {
2075
+ const connection = job.connectionName ?? this.defaultConnection;
2076
+ const queue = job.queueName ?? "default";
2077
+ const driver = this.getDriver(connection);
2078
+ const serializer = this.getSerializer();
2079
+ if (driver.complete) {
2080
+ const serialized = serializer.serialize(job);
2081
+ await driver.complete(queue, serialized);
2082
+ if (this.persistence?.archiveCompleted) {
2083
+ await this.persistence.adapter.archive(queue, serialized, "completed").catch((err) => {
2084
+ console.error("[QueueManager] Persistence archive failed (completed):", err);
2085
+ });
2086
+ }
2087
+ }
2088
+ }
2089
+ /**
2090
+ * Mark a job as permanently failed.
2091
+ * @param job - Job instance
2092
+ * @param error - Error object
2093
+ */
2094
+ async fail(job, error) {
2095
+ const connection = job.connectionName ?? this.defaultConnection;
2096
+ const queue = job.queueName ?? "default";
2097
+ const driver = this.getDriver(connection);
2098
+ const serializer = this.getSerializer();
2099
+ if (driver.fail) {
2100
+ const serialized = serializer.serialize(job);
2101
+ serialized.error = error.message;
2102
+ serialized.failedAt = Date.now();
2103
+ await driver.fail(queue, serialized);
2104
+ if (this.persistence?.archiveFailed) {
2105
+ await this.persistence.adapter.archive(queue, serialized, "failed").catch((err) => {
2106
+ console.error("[QueueManager] Persistence archive failed (failed):", err);
2107
+ });
2108
+ }
2109
+ }
2110
+ }
2111
+ /**
2112
+ * Get the persistence adapter if configured.
2113
+ */
2114
+ getPersistence() {
2115
+ return this.persistence?.adapter;
2116
+ }
2117
+ /**
2118
+ * Get the scheduler if configured.
2119
+ */
2120
+ getScheduler() {
2121
+ if (!this.scheduler) {
2122
+ const { Scheduler: Scheduler2 } = (init_Scheduler(), __toCommonJS(Scheduler_exports));
2123
+ this.scheduler = new Scheduler2(this);
2124
+ }
2125
+ return this.scheduler;
2126
+ }
2127
+ /**
2128
+ * Get failed jobs from DLQ (if driver supports it).
2129
+ */
2130
+ async getFailed(queue, start = 0, end = -1, connection = this.defaultConnection) {
2131
+ const driver = this.getDriver(connection);
2132
+ if (driver.getFailed) {
2133
+ return driver.getFailed(queue, start, end);
2134
+ }
2135
+ return [];
2136
+ }
2137
+ /**
2138
+ * Retry failed jobs from DLQ (if driver supports it).
2139
+ */
2140
+ async retryFailed(queue, count = 1, connection = this.defaultConnection) {
2141
+ const driver = this.getDriver(connection);
2142
+ if (driver.retryFailed) {
2143
+ return driver.retryFailed(queue, count);
2144
+ }
2145
+ return 0;
2146
+ }
2147
+ /**
2148
+ * Clear failed jobs from DLQ (if driver supports it).
2149
+ */
2150
+ async clearFailed(queue, connection = this.defaultConnection) {
2151
+ const driver = this.getDriver(connection);
2152
+ if (driver.clearFailed) {
2153
+ await driver.clearFailed(queue);
2154
+ }
2155
+ }
1319
2156
  };
1320
2157
 
1321
2158
  // src/OrbitStream.ts
@@ -1396,6 +2233,453 @@ var OrbitStream = class _OrbitStream {
1396
2233
  return this.queueManager;
1397
2234
  }
1398
2235
  };
2236
+
2237
+ // src/persistence/MySQLPersistence.ts
2238
+ import { DB, Schema } from "@gravito/atlas";
2239
+ var MySQLPersistence = class {
2240
+ /**
2241
+ * @param db - An Atlas DB instance or compatible QueryBuilder.
2242
+ * @param table - The name of the table to store archived jobs.
2243
+ */
2244
+ constructor(db, table = "flux_job_archive", logsTable = "flux_system_logs") {
2245
+ this.db = db;
2246
+ this.table = table;
2247
+ this.logsTable = logsTable;
2248
+ }
2249
+ /**
2250
+ * Archive a job.
2251
+ */
2252
+ async archive(queue, job, status) {
2253
+ try {
2254
+ await this.db.table(this.table).insert({
2255
+ job_id: job.id,
2256
+ queue,
2257
+ status,
2258
+ payload: JSON.stringify(job),
2259
+ error: job.error || null,
2260
+ created_at: new Date(job.createdAt),
2261
+ archived_at: /* @__PURE__ */ new Date()
2262
+ });
2263
+ } catch (err) {
2264
+ console.error(`[MySQLPersistence] Failed to archive job ${job.id}:`, err);
2265
+ }
2266
+ }
2267
+ /**
2268
+ * Find a specific job in the archive.
2269
+ */
2270
+ async find(queue, id) {
2271
+ const row = await this.db.table(this.table).where("queue", queue).where("job_id", id).first();
2272
+ if (!row) {
2273
+ return null;
2274
+ }
2275
+ try {
2276
+ const job = typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload;
2277
+ return job;
2278
+ } catch (_e) {
2279
+ return null;
2280
+ }
2281
+ }
2282
+ /**
2283
+ * List jobs from the archive.
2284
+ */
2285
+ async list(queue, options = {}) {
2286
+ let query = this.db.table(this.table).where("queue", queue);
2287
+ if (options.status) {
2288
+ query = query.where("status", options.status);
2289
+ }
2290
+ if (options.jobId) {
2291
+ query = query.where("job_id", options.jobId);
2292
+ }
2293
+ if (options.startTime) {
2294
+ query = query.where("archived_at", ">=", options.startTime);
2295
+ }
2296
+ if (options.endTime) {
2297
+ query = query.where("archived_at", "<=", options.endTime);
2298
+ }
2299
+ const rows = await query.orderBy("archived_at", "desc").limit(options.limit ?? 50).offset(options.offset ?? 0).get();
2300
+ return rows.map((r) => {
2301
+ try {
2302
+ const job = typeof r.payload === "string" ? JSON.parse(r.payload) : r.payload;
2303
+ return { ...job, _status: r.status, _archivedAt: r.archived_at };
2304
+ } catch (_e) {
2305
+ return null;
2306
+ }
2307
+ }).filter(Boolean);
2308
+ }
2309
+ /**
2310
+ * Search jobs from the archive.
2311
+ */
2312
+ async search(query, options = {}) {
2313
+ let q = this.db.table(this.table);
2314
+ if (options.queue) {
2315
+ q = q.where("queue", options.queue);
2316
+ }
2317
+ const rows = await q.where((sub) => {
2318
+ sub.where("job_id", "like", `%${query}%`).orWhere("payload", "like", `%${query}%`).orWhere("error", "like", `%${query}%`);
2319
+ }).orderBy("archived_at", "desc").limit(options.limit ?? 50).offset(options.offset ?? 0).get();
2320
+ return rows.map((r) => {
2321
+ try {
2322
+ const job = typeof r.payload === "string" ? JSON.parse(r.payload) : r.payload;
2323
+ return { ...job, _status: r.status, _archivedAt: r.archived_at };
2324
+ } catch (_e) {
2325
+ return null;
2326
+ }
2327
+ }).filter(Boolean);
2328
+ }
2329
+ /**
2330
+ * Archive a system log message.
2331
+ */
2332
+ async archiveLog(log) {
2333
+ try {
2334
+ await this.db.table(this.logsTable).insert({
2335
+ level: log.level,
2336
+ message: log.message,
2337
+ worker_id: log.workerId,
2338
+ queue: log.queue || null,
2339
+ timestamp: log.timestamp
2340
+ });
2341
+ } catch (err) {
2342
+ console.error(`[MySQLPersistence] Failed to archive log:`, err.message);
2343
+ }
2344
+ }
2345
+ /**
2346
+ * List system logs from the archive.
2347
+ */
2348
+ async listLogs(options = {}) {
2349
+ let query = this.db.table(this.logsTable);
2350
+ if (options.level) query = query.where("level", options.level);
2351
+ if (options.workerId) query = query.where("worker_id", options.workerId);
2352
+ if (options.queue) query = query.where("queue", options.queue);
2353
+ if (options.search) {
2354
+ query = query.where("message", "like", `%${options.search}%`);
2355
+ }
2356
+ if (options.startTime) {
2357
+ query = query.where("timestamp", ">=", options.startTime);
2358
+ }
2359
+ if (options.endTime) {
2360
+ query = query.where("timestamp", "<=", options.endTime);
2361
+ }
2362
+ return await query.orderBy("timestamp", "desc").limit(options.limit ?? 50).offset(options.offset ?? 0).get();
2363
+ }
2364
+ /**
2365
+ * Count system logs in the archive.
2366
+ */
2367
+ async countLogs(options = {}) {
2368
+ let query = this.db.table(this.logsTable);
2369
+ if (options.level) query = query.where("level", options.level);
2370
+ if (options.workerId) query = query.where("worker_id", options.workerId);
2371
+ if (options.queue) query = query.where("queue", options.queue);
2372
+ if (options.search) {
2373
+ query = query.where("message", "like", `%${options.search}%`);
2374
+ }
2375
+ if (options.startTime) {
2376
+ query = query.where("timestamp", ">=", options.startTime);
2377
+ }
2378
+ if (options.endTime) {
2379
+ query = query.where("timestamp", "<=", options.endTime);
2380
+ }
2381
+ const result = await query.count("id as total").first();
2382
+ return result?.total || 0;
2383
+ }
2384
+ /**
2385
+ * Remove old records from the archive.
2386
+ */
2387
+ async cleanup(days) {
2388
+ const threshold = /* @__PURE__ */ new Date();
2389
+ threshold.setDate(threshold.getDate() - days);
2390
+ const [jobsDeleted, logsDeleted] = await Promise.all([
2391
+ this.db.table(this.table).where("archived_at", "<", threshold).delete(),
2392
+ this.db.table(this.logsTable).where("timestamp", "<", threshold).delete()
2393
+ ]);
2394
+ return (jobsDeleted || 0) + (logsDeleted || 0);
2395
+ }
2396
+ /**
2397
+ * Count jobs in the archive.
2398
+ */
2399
+ async count(queue, options = {}) {
2400
+ let query = this.db.table(this.table).where("queue", queue);
2401
+ if (options.status) {
2402
+ query = query.where("status", options.status);
2403
+ }
2404
+ if (options.jobId) {
2405
+ query = query.where("job_id", options.jobId);
2406
+ }
2407
+ if (options.startTime) {
2408
+ query = query.where("archived_at", ">=", options.startTime);
2409
+ }
2410
+ if (options.endTime) {
2411
+ query = query.where("archived_at", "<=", options.endTime);
2412
+ }
2413
+ const result = await query.count("id as total").first();
2414
+ return result?.total || 0;
2415
+ }
2416
+ /**
2417
+ * Help script to create the necessary table.
2418
+ */
2419
+ async setupTable() {
2420
+ await Promise.all([this.setupJobsTable(), this.setupLogsTable()]);
2421
+ }
2422
+ async setupJobsTable() {
2423
+ const exists = await Schema.hasTable(this.table);
2424
+ if (exists) return;
2425
+ await Schema.create(this.table, (table) => {
2426
+ table.id();
2427
+ table.string("job_id", 64);
2428
+ table.string("queue", 128);
2429
+ table.string("status", 20);
2430
+ table.json("payload");
2431
+ table.text("error").nullable();
2432
+ table.timestamp("created_at").nullable();
2433
+ table.timestamp("archived_at").default(DB.raw("CURRENT_TIMESTAMP"));
2434
+ table.index(["queue", "archived_at"]);
2435
+ table.index(["queue", "job_id"]);
2436
+ table.index(["status", "archived_at"]);
2437
+ table.index(["archived_at"]);
2438
+ });
2439
+ console.log(`[MySQLPersistence] Created jobs archive table: ${this.table}`);
2440
+ }
2441
+ async setupLogsTable() {
2442
+ const exists = await Schema.hasTable(this.logsTable);
2443
+ if (exists) return;
2444
+ await Schema.create(this.logsTable, (table) => {
2445
+ table.id();
2446
+ table.string("level", 20);
2447
+ table.text("message");
2448
+ table.string("worker_id", 128);
2449
+ table.string("queue", 128).nullable();
2450
+ table.timestamp("timestamp").default(DB.raw("CURRENT_TIMESTAMP"));
2451
+ table.index(["worker_id"]);
2452
+ table.index(["queue"]);
2453
+ table.index(["level"]);
2454
+ table.index(["timestamp"]);
2455
+ });
2456
+ console.log(`[MySQLPersistence] Created logs archive table: ${this.logsTable}`);
2457
+ }
2458
+ };
2459
+
2460
+ // src/persistence/SQLitePersistence.ts
2461
+ import { Schema as Schema2 } from "@gravito/atlas";
2462
+ var SQLitePersistence = class {
2463
+ /**
2464
+ * @param db - An Atlas DB instance (SQLite driver).
2465
+ * @param table - The name of the table to store archived jobs.
2466
+ */
2467
+ constructor(db, table = "flux_job_archive", logsTable = "flux_system_logs") {
2468
+ this.db = db;
2469
+ this.table = table;
2470
+ this.logsTable = logsTable;
2471
+ }
2472
+ /**
2473
+ * Archive a job.
2474
+ */
2475
+ async archive(queue, job, status) {
2476
+ try {
2477
+ await this.db.table(this.table).insert({
2478
+ job_id: job.id,
2479
+ queue,
2480
+ status,
2481
+ payload: JSON.stringify(job),
2482
+ error: job.error || null,
2483
+ created_at: new Date(job.createdAt),
2484
+ archived_at: /* @__PURE__ */ new Date()
2485
+ });
2486
+ } catch (err) {
2487
+ console.error(`[SQLitePersistence] Failed to archive job ${job.id}:`, err.message);
2488
+ }
2489
+ }
2490
+ /**
2491
+ * Find a specific job in the archive.
2492
+ */
2493
+ async find(queue, id) {
2494
+ const row = await this.db.table(this.table).where("queue", queue).where("job_id", id).first();
2495
+ if (!row) {
2496
+ return null;
2497
+ }
2498
+ try {
2499
+ const job = typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload;
2500
+ return job;
2501
+ } catch (_e) {
2502
+ return null;
2503
+ }
2504
+ }
2505
+ /**
2506
+ * List jobs from the archive.
2507
+ */
2508
+ async list(queue, options = {}) {
2509
+ let query = this.db.table(this.table).where("queue", queue);
2510
+ if (options.status) {
2511
+ query = query.where("status", options.status);
2512
+ }
2513
+ if (options.jobId) {
2514
+ query = query.where("job_id", options.jobId);
2515
+ }
2516
+ if (options.startTime) {
2517
+ query = query.where("archived_at", ">=", options.startTime);
2518
+ }
2519
+ if (options.endTime) {
2520
+ query = query.where("archived_at", "<=", options.endTime);
2521
+ }
2522
+ const rows = await query.orderBy("archived_at", "desc").limit(options.limit ?? 50).offset(options.offset ?? 0).get();
2523
+ return rows.map((r) => {
2524
+ try {
2525
+ const job = typeof r.payload === "string" ? JSON.parse(r.payload) : r.payload;
2526
+ return { ...job, _status: r.status, _archivedAt: r.archived_at };
2527
+ } catch (_e) {
2528
+ return null;
2529
+ }
2530
+ }).filter(Boolean);
2531
+ }
2532
+ /**
2533
+ * Search jobs from the archive.
2534
+ */
2535
+ async search(query, options = {}) {
2536
+ let q = this.db.table(this.table);
2537
+ if (options.queue) {
2538
+ q = q.where("queue", options.queue);
2539
+ }
2540
+ const rows = await q.where((sub) => {
2541
+ sub.where("job_id", "like", `%${query}%`).orWhere("payload", "like", `%${query}%`).orWhere("error", "like", `%${query}%`);
2542
+ }).orderBy("archived_at", "desc").limit(options.limit ?? 50).offset(options.offset ?? 0).get();
2543
+ return rows.map((r) => {
2544
+ try {
2545
+ const job = typeof r.payload === "string" ? JSON.parse(r.payload) : r.payload;
2546
+ return { ...job, _status: r.status, _archivedAt: r.archived_at };
2547
+ } catch (_e) {
2548
+ return null;
2549
+ }
2550
+ }).filter(Boolean);
2551
+ }
2552
+ /**
2553
+ * Archive a system log message.
2554
+ */
2555
+ async archiveLog(log) {
2556
+ try {
2557
+ await this.db.table(this.logsTable).insert({
2558
+ level: log.level,
2559
+ message: log.message,
2560
+ worker_id: log.workerId,
2561
+ queue: log.queue || null,
2562
+ timestamp: log.timestamp
2563
+ });
2564
+ } catch (err) {
2565
+ console.error(`[SQLitePersistence] Failed to archive log:`, err.message);
2566
+ }
2567
+ }
2568
+ /**
2569
+ * List system logs from the archive.
2570
+ */
2571
+ async listLogs(options = {}) {
2572
+ let query = this.db.table(this.logsTable);
2573
+ if (options.level) query = query.where("level", options.level);
2574
+ if (options.workerId) query = query.where("worker_id", options.workerId);
2575
+ if (options.queue) query = query.where("queue", options.queue);
2576
+ if (options.search) {
2577
+ query = query.where("message", "like", `%${options.search}%`);
2578
+ }
2579
+ if (options.startTime) {
2580
+ query = query.where("timestamp", ">=", options.startTime);
2581
+ }
2582
+ if (options.endTime) {
2583
+ query = query.where("timestamp", "<=", options.endTime);
2584
+ }
2585
+ return await query.orderBy("timestamp", "desc").limit(options.limit ?? 50).offset(options.offset ?? 0).get();
2586
+ }
2587
+ /**
2588
+ * Count system logs in the archive.
2589
+ */
2590
+ async countLogs(options = {}) {
2591
+ let query = this.db.table(this.logsTable);
2592
+ if (options.level) query = query.where("level", options.level);
2593
+ if (options.workerId) query = query.where("worker_id", options.workerId);
2594
+ if (options.queue) query = query.where("queue", options.queue);
2595
+ if (options.search) {
2596
+ query = query.where("message", "like", `%${options.search}%`);
2597
+ }
2598
+ if (options.startTime) {
2599
+ query = query.where("timestamp", ">=", options.startTime);
2600
+ }
2601
+ if (options.endTime) {
2602
+ query = query.where("timestamp", "<=", options.endTime);
2603
+ }
2604
+ const result = await query.count("id as total").first();
2605
+ return result?.total || 0;
2606
+ }
2607
+ /**
2608
+ * Remove old records from the archive.
2609
+ */
2610
+ async cleanup(days) {
2611
+ const threshold = /* @__PURE__ */ new Date();
2612
+ threshold.setDate(threshold.getDate() - days);
2613
+ const [jobsDeleted, logsDeleted] = await Promise.all([
2614
+ this.db.table(this.table).where("archived_at", "<", threshold).delete(),
2615
+ this.db.table(this.logsTable).where("timestamp", "<", threshold).delete()
2616
+ ]);
2617
+ return (jobsDeleted || 0) + (logsDeleted || 0);
2618
+ }
2619
+ /**
2620
+ * Count jobs in the archive.
2621
+ */
2622
+ async count(queue, options = {}) {
2623
+ let query = this.db.table(this.table).where("queue", queue);
2624
+ if (options.status) {
2625
+ query = query.where("status", options.status);
2626
+ }
2627
+ if (options.jobId) {
2628
+ query = query.where("job_id", options.jobId);
2629
+ }
2630
+ if (options.startTime) {
2631
+ query = query.where("archived_at", ">=", options.startTime);
2632
+ }
2633
+ if (options.endTime) {
2634
+ query = query.where("archived_at", "<=", options.endTime);
2635
+ }
2636
+ const result = await query.count("id as total").first();
2637
+ return result?.total || 0;
2638
+ }
2639
+ /**
2640
+ * Setup table for SQLite.
2641
+ */
2642
+ async setupTable() {
2643
+ await Promise.all([this.setupJobsTable(), this.setupLogsTable()]);
2644
+ }
2645
+ async setupJobsTable() {
2646
+ const exists = await Schema2.hasTable(this.table);
2647
+ if (exists) return;
2648
+ await Schema2.create(this.table, (table) => {
2649
+ table.id();
2650
+ table.string("job_id", 64);
2651
+ table.string("queue", 128);
2652
+ table.string("status", 20);
2653
+ table.text("payload");
2654
+ table.text("error").nullable();
2655
+ table.timestamp("created_at").nullable();
2656
+ table.timestamp("archived_at").nullable();
2657
+ table.index(["queue", "archived_at"]);
2658
+ table.index(["archived_at"]);
2659
+ });
2660
+ console.log(`[SQLitePersistence] Created jobs archive table: ${this.table}`);
2661
+ }
2662
+ async setupLogsTable() {
2663
+ const exists = await Schema2.hasTable(this.logsTable);
2664
+ if (exists) return;
2665
+ await Schema2.create(this.logsTable, (table) => {
2666
+ table.id();
2667
+ table.string("level", 20);
2668
+ table.text("message");
2669
+ table.string("worker_id", 128);
2670
+ table.string("queue", 128).nullable();
2671
+ table.timestamp("timestamp");
2672
+ table.index(["worker_id"]);
2673
+ table.index(["queue"]);
2674
+ table.index(["level"]);
2675
+ table.index(["timestamp"]);
2676
+ });
2677
+ console.log(`[SQLitePersistence] Created logs archive table: ${this.logsTable}`);
2678
+ }
2679
+ };
2680
+
2681
+ // src/index.ts
2682
+ init_Scheduler();
1399
2683
  export {
1400
2684
  ClassNameSerializer,
1401
2685
  Consumer,
@@ -1404,9 +2688,13 @@ export {
1404
2688
  JsonSerializer,
1405
2689
  KafkaDriver,
1406
2690
  MemoryDriver,
2691
+ MySQLPersistence,
1407
2692
  OrbitStream,
1408
2693
  QueueManager,
2694
+ RabbitMQDriver,
1409
2695
  RedisDriver,
2696
+ SQLitePersistence,
1410
2697
  SQSDriver,
2698
+ Scheduler,
1411
2699
  Worker
1412
2700
  };