@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/README.md +86 -2
- package/dist/index.cjs +1359 -73
- package/dist/index.d.cts +689 -7
- package/dist/index.d.ts +689 -7
- package/dist/index.js +1361 -73
- package/package.json +10 -6
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,
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
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
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const
|
|
385
|
-
if (
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
(
|
|
697
|
-
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
|
|
768
|
-
|
|
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
|
-
|
|
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
|
};
|