power-queues 2.1.7 → 2.1.9
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 +84 -13
- package/dist/index.cjs +19 -24
- package/dist/index.js +19 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,26 +35,99 @@ yarn add power-queues
|
|
|
35
35
|
|
|
36
36
|
``` ts
|
|
37
37
|
const queue = new PowerQueues({
|
|
38
|
-
stream: '
|
|
38
|
+
stream: 'mysql',
|
|
39
39
|
group: 'workers',
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
await queue.loadScripts(true);
|
|
43
43
|
|
|
44
|
-
await queue.addTasks('
|
|
45
|
-
{
|
|
46
|
-
{
|
|
44
|
+
await queue.addTasks('mysql_create:example:table_name', [
|
|
45
|
+
{ type: 'welcome', userId: 42 },
|
|
46
|
+
{ type: 'hello', userId: 51 }
|
|
47
47
|
]);
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
Example of a worker for executing a MySQL insert transaction:
|
|
51
51
|
|
|
52
52
|
``` ts
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
import mysql from 'mysql2/promise';
|
|
54
|
+
import Redis from 'ioredis';
|
|
55
|
+
import type { IORedisLike } from 'power-redis';
|
|
56
|
+
import { type Task, PowerQueues, } from 'power-queues';
|
|
57
|
+
import {
|
|
58
|
+
isArrFilled,
|
|
59
|
+
isObjFilled,
|
|
60
|
+
} from 'full-utils';
|
|
61
|
+
|
|
62
|
+
const pool = mysql.createPool({
|
|
63
|
+
host: '127.0.0.1',
|
|
64
|
+
port: 3306,
|
|
65
|
+
user: 'user',
|
|
66
|
+
password: 'password',
|
|
67
|
+
database: 'example',
|
|
68
|
+
waitForConnections: true,
|
|
69
|
+
connectionLimit: 10,
|
|
70
|
+
});
|
|
71
|
+
const redis = new Redis('redis://127.0.0.1:6379');
|
|
72
|
+
|
|
73
|
+
export class ExampleQueue extends PowerQueues {
|
|
74
|
+
public readonly selectStuckCount: number = 256;
|
|
75
|
+
public readonly selectCount: number = 256;
|
|
76
|
+
public readonly retryCount: number = 3;
|
|
77
|
+
public readonly executeSync: boolean = true;
|
|
78
|
+
public readonly removeOnExecuted: boolean = true;
|
|
79
|
+
public redis!: IORedisLike;
|
|
80
|
+
|
|
81
|
+
constructor() {
|
|
82
|
+
super();
|
|
83
|
+
|
|
84
|
+
this.redis = redis;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async onBatchReady(queueName: string, tasks: Task[]) {
|
|
88
|
+
const values = tasks
|
|
89
|
+
.filter((task) => isObjFilled(task.payload))
|
|
90
|
+
.map((task) => task.payload);
|
|
91
|
+
|
|
92
|
+
if (isArrFilled(values)) {
|
|
93
|
+
const conn = await pool.getConnection();
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await conn.beginTransaction();
|
|
97
|
+
|
|
98
|
+
const cols = Object.keys(values[0]);
|
|
99
|
+
const placeholder = `(${cols.map(() => '?').join(',')})`;
|
|
100
|
+
const sql = `INSERT INTO \`alerts\` (${cols.map((c) => `\`${c}\``).join(',')}) VALUES ${values.map(() => placeholder).join(',')}`;
|
|
101
|
+
const params = [];
|
|
102
|
+
|
|
103
|
+
for (const row of values) {
|
|
104
|
+
for (const c of cols) {
|
|
105
|
+
params.push(row[c]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
await conn.execute(sql, params);
|
|
109
|
+
await conn.commit();
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
await conn.rollback();
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
conn.release();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async onBatchError(err: any, queueName: string, tasks: Array<[ string, any, number, string, string, number ]>) {
|
|
122
|
+
this.logger.error('Transaction error', queueName, tasks.length, (process.env.NODE_ENV === 'production')
|
|
123
|
+
? err.message
|
|
124
|
+
: err, tasks.map((task) => task[1]));
|
|
56
125
|
}
|
|
57
126
|
}
|
|
127
|
+
|
|
128
|
+
const exampleQueue = new ExampleQueue();
|
|
129
|
+
|
|
130
|
+
exampleQueue.runQueue('mysql_create:example:table_name');
|
|
58
131
|
```
|
|
59
132
|
|
|
60
133
|
## ⚖️ power-queues vs Existing Solutions
|
|
@@ -124,27 +197,24 @@ and efficiently.
|
|
|
124
197
|
When retries reach the configured limit:
|
|
125
198
|
- the task is moved into `${stream}:dlq`
|
|
126
199
|
- includes: payload, attempt count, job, timestamp, error text
|
|
127
|
-
- fully JSON‑safe
|
|
128
200
|
|
|
129
201
|
Perfect for monitoring or later re‑processing.
|
|
130
202
|
|
|
131
203
|
### ✔ Zero‑Overhead Serialization
|
|
132
204
|
**power-queues** uses:
|
|
133
205
|
- safe JSON encoding
|
|
134
|
-
- optional "flat" key/value task format
|
|
135
206
|
- predictable and optimized payload transformation
|
|
136
207
|
|
|
137
208
|
This keeps Redis memory layout clean and eliminates overhead.
|
|
138
209
|
|
|
139
210
|
### ✔ Complete Set of Lifecycle Hooks
|
|
140
211
|
You can extend any part of the execution flow:
|
|
141
|
-
- `onSelected`
|
|
142
212
|
- `onExecute`
|
|
213
|
+
- `onBatchReady`
|
|
143
214
|
- `onSuccess`
|
|
144
215
|
- `onError`
|
|
145
|
-
- `onRetry`
|
|
146
216
|
- `onBatchError`
|
|
147
|
-
- `
|
|
217
|
+
- `onRetry`
|
|
148
218
|
|
|
149
219
|
This allows full integration with:
|
|
150
220
|
- monitoring systems
|
|
@@ -163,6 +233,7 @@ Ensures resilience in failover scenarios.
|
|
|
163
233
|
|
|
164
234
|
### ✔ Job Progress Tracking
|
|
165
235
|
Optional per‑job counters:
|
|
236
|
+
- `job:total`
|
|
166
237
|
- `job:ok`
|
|
167
238
|
- `job:err`
|
|
168
239
|
- `job:ready`
|
package/dist/index.cjs
CHANGED
|
@@ -344,17 +344,17 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
|
|
|
344
344
|
try {
|
|
345
345
|
const filtered = {};
|
|
346
346
|
tasks.forEach((task, index) => {
|
|
347
|
-
const key =
|
|
347
|
+
const key = JSON.stringify([task[5] || "0", task[2], task[3]]);
|
|
348
348
|
if (!filtered[key]) {
|
|
349
349
|
filtered[key] = [];
|
|
350
350
|
}
|
|
351
|
-
filtered[key].push(tasks[index][1]);
|
|
351
|
+
filtered[key].push({ ...tasks[index][1], idemKey: tasks[index][4] });
|
|
352
352
|
});
|
|
353
353
|
for (let key in filtered) {
|
|
354
354
|
const filteredTasks = filtered[key];
|
|
355
|
-
const
|
|
356
|
-
const attempt = Number(
|
|
357
|
-
const job = String(
|
|
355
|
+
const keyP = JSON.parse(key);
|
|
356
|
+
const attempt = Number(keyP[0] || 0);
|
|
357
|
+
const job = String(keyP[2]);
|
|
358
358
|
if (!(attempt >= this.retryCount - 1)) {
|
|
359
359
|
await this.addTasks(queueName, filteredTasks, {
|
|
360
360
|
job,
|
|
@@ -445,7 +445,7 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
|
|
|
445
445
|
const kv = (0, import_full_utils.isArr)(kvRaw) ? kvRaw.map((x) => Buffer.isBuffer(x) ? x.toString() : x) : [];
|
|
446
446
|
return [id, kv];
|
|
447
447
|
}).filter(([id, kv]) => (0, import_full_utils.isStrFilled)(id) && (0, import_full_utils.isArr)(kv) && (kv.length & 1) === 0).map(([id, kv]) => {
|
|
448
|
-
const { payload, createdAt, job, idemKey
|
|
448
|
+
const { payload, createdAt, job, idemKey, attempt } = this.values(kv);
|
|
449
449
|
return [id, this.payload(payload), createdAt, job, idemKey, Number(attempt)];
|
|
450
450
|
});
|
|
451
451
|
}
|
|
@@ -569,7 +569,7 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
|
|
|
569
569
|
const taskP = { ...task };
|
|
570
570
|
if (!(taskP.attempt >= this.retryCount - 1)) {
|
|
571
571
|
await this.onRetry(err, queueName, taskP);
|
|
572
|
-
await this.addTasks(queueName, [{ ...taskP.payload }], {
|
|
572
|
+
await this.addTasks(queueName, [{ ...taskP.payload, idemKey: taskP.idemKey }], {
|
|
573
573
|
createdAt: taskP.createdAt,
|
|
574
574
|
job: taskP.job,
|
|
575
575
|
attempt: (taskP.attempt || 0) + 1
|
|
@@ -579,7 +579,7 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
|
|
|
579
579
|
const statusKey = `${queueName}:${taskP.job}:`;
|
|
580
580
|
await this.incr(statusKey + "err", this.logStatusTimeout);
|
|
581
581
|
await this.incr(statusKey + "ready", this.logStatusTimeout);
|
|
582
|
-
await this.addTasks(dlqKey, [{ ...taskP.payload }], {
|
|
582
|
+
await this.addTasks(dlqKey, [{ ...taskP.payload, idemKey: taskP.idemKey }], {
|
|
583
583
|
createdAt: taskP.createdAt,
|
|
584
584
|
job: taskP.job,
|
|
585
585
|
attempt: taskP.attempt
|
|
@@ -763,19 +763,17 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
|
|
|
763
763
|
}
|
|
764
764
|
buildBatches(tasks, opts = {}) {
|
|
765
765
|
const batches = [];
|
|
766
|
-
let batch = [], realKeysLength =
|
|
766
|
+
let batch = [], realKeysLength = 8;
|
|
767
767
|
for (let task of tasks) {
|
|
768
768
|
const createdAt = opts?.createdAt || Date.now();
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
};
|
|
778
|
-
}
|
|
769
|
+
const { idemKey, ...taskP } = task;
|
|
770
|
+
const entry = {
|
|
771
|
+
payload: JSON.stringify(taskP),
|
|
772
|
+
attempt: Number(opts.attempt || 0),
|
|
773
|
+
job: opts.job ?? (0, import_uuid.v4)(),
|
|
774
|
+
idemKey: String(idemKey || (0, import_uuid.v4)()),
|
|
775
|
+
createdAt
|
|
776
|
+
};
|
|
779
777
|
const reqKeysLength = this.keysLength(entry);
|
|
780
778
|
if (batch.length && (batch.length >= this.buildBatchCount || realKeysLength + reqKeysLength > this.buildBatchMaxCount)) {
|
|
781
779
|
batches.push(batch);
|
|
@@ -791,9 +789,6 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
|
|
|
791
789
|
return batches;
|
|
792
790
|
}
|
|
793
791
|
keysLength(task) {
|
|
794
|
-
if ("payload" in task && (0, import_full_utils.isObj)(task.payload)) {
|
|
795
|
-
return 2 + Object.keys(task.payload).length * 2;
|
|
796
|
-
}
|
|
797
792
|
return 2 + Object.keys(task).length * 2;
|
|
798
793
|
}
|
|
799
794
|
payloadBatch(data, opts) {
|
|
@@ -818,7 +813,7 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
|
|
|
818
813
|
const entry = item;
|
|
819
814
|
const id = entry.id ?? "*";
|
|
820
815
|
let flat = [];
|
|
821
|
-
if ("payload" in entry
|
|
816
|
+
if ("payload" in entry) {
|
|
822
817
|
for (const [k, v] of Object.entries(entry)) {
|
|
823
818
|
flat.push(k, v);
|
|
824
819
|
}
|
|
@@ -826,7 +821,7 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
|
|
|
826
821
|
throw new Error('Task must have "payload" or "flat".');
|
|
827
822
|
}
|
|
828
823
|
const pairs = flat.length / 2;
|
|
829
|
-
if (
|
|
824
|
+
if (pairs <= 0) {
|
|
830
825
|
throw new Error('Task "flat" must contain at least one field/value pair.');
|
|
831
826
|
}
|
|
832
827
|
argv.push(String(id));
|
package/dist/index.js
CHANGED
|
@@ -2,13 +2,10 @@
|
|
|
2
2
|
import { setMaxListeners } from "events";
|
|
3
3
|
import { PowerRedis } from "power-redis";
|
|
4
4
|
import {
|
|
5
|
-
isObjFilled,
|
|
6
|
-
isObj,
|
|
7
5
|
isArrFilled,
|
|
8
6
|
isArr,
|
|
9
7
|
isStrFilled,
|
|
10
8
|
isExists,
|
|
11
|
-
isNumNZ,
|
|
12
9
|
wait
|
|
13
10
|
} from "full-utils";
|
|
14
11
|
import { v4 as uuid } from "uuid";
|
|
@@ -327,17 +324,17 @@ var PowerQueues = class extends PowerRedis {
|
|
|
327
324
|
try {
|
|
328
325
|
const filtered = {};
|
|
329
326
|
tasks.forEach((task, index) => {
|
|
330
|
-
const key =
|
|
327
|
+
const key = JSON.stringify([task[5] || "0", task[2], task[3]]);
|
|
331
328
|
if (!filtered[key]) {
|
|
332
329
|
filtered[key] = [];
|
|
333
330
|
}
|
|
334
|
-
filtered[key].push(tasks[index][1]);
|
|
331
|
+
filtered[key].push({ ...tasks[index][1], idemKey: tasks[index][4] });
|
|
335
332
|
});
|
|
336
333
|
for (let key in filtered) {
|
|
337
334
|
const filteredTasks = filtered[key];
|
|
338
|
-
const
|
|
339
|
-
const attempt = Number(
|
|
340
|
-
const job = String(
|
|
335
|
+
const keyP = JSON.parse(key);
|
|
336
|
+
const attempt = Number(keyP[0] || 0);
|
|
337
|
+
const job = String(keyP[2]);
|
|
341
338
|
if (!(attempt >= this.retryCount - 1)) {
|
|
342
339
|
await this.addTasks(queueName, filteredTasks, {
|
|
343
340
|
job,
|
|
@@ -428,7 +425,7 @@ var PowerQueues = class extends PowerRedis {
|
|
|
428
425
|
const kv = isArr(kvRaw) ? kvRaw.map((x) => Buffer.isBuffer(x) ? x.toString() : x) : [];
|
|
429
426
|
return [id, kv];
|
|
430
427
|
}).filter(([id, kv]) => isStrFilled(id) && isArr(kv) && (kv.length & 1) === 0).map(([id, kv]) => {
|
|
431
|
-
const { payload, createdAt, job, idemKey
|
|
428
|
+
const { payload, createdAt, job, idemKey, attempt } = this.values(kv);
|
|
432
429
|
return [id, this.payload(payload), createdAt, job, idemKey, Number(attempt)];
|
|
433
430
|
});
|
|
434
431
|
}
|
|
@@ -552,7 +549,7 @@ var PowerQueues = class extends PowerRedis {
|
|
|
552
549
|
const taskP = { ...task };
|
|
553
550
|
if (!(taskP.attempt >= this.retryCount - 1)) {
|
|
554
551
|
await this.onRetry(err, queueName, taskP);
|
|
555
|
-
await this.addTasks(queueName, [{ ...taskP.payload }], {
|
|
552
|
+
await this.addTasks(queueName, [{ ...taskP.payload, idemKey: taskP.idemKey }], {
|
|
556
553
|
createdAt: taskP.createdAt,
|
|
557
554
|
job: taskP.job,
|
|
558
555
|
attempt: (taskP.attempt || 0) + 1
|
|
@@ -562,7 +559,7 @@ var PowerQueues = class extends PowerRedis {
|
|
|
562
559
|
const statusKey = `${queueName}:${taskP.job}:`;
|
|
563
560
|
await this.incr(statusKey + "err", this.logStatusTimeout);
|
|
564
561
|
await this.incr(statusKey + "ready", this.logStatusTimeout);
|
|
565
|
-
await this.addTasks(dlqKey, [{ ...taskP.payload }], {
|
|
562
|
+
await this.addTasks(dlqKey, [{ ...taskP.payload, idemKey: taskP.idemKey }], {
|
|
566
563
|
createdAt: taskP.createdAt,
|
|
567
564
|
job: taskP.job,
|
|
568
565
|
attempt: taskP.attempt
|
|
@@ -746,19 +743,17 @@ var PowerQueues = class extends PowerRedis {
|
|
|
746
743
|
}
|
|
747
744
|
buildBatches(tasks, opts = {}) {
|
|
748
745
|
const batches = [];
|
|
749
|
-
let batch = [], realKeysLength =
|
|
746
|
+
let batch = [], realKeysLength = 8;
|
|
750
747
|
for (let task of tasks) {
|
|
751
748
|
const createdAt = opts?.createdAt || Date.now();
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
};
|
|
761
|
-
}
|
|
749
|
+
const { idemKey, ...taskP } = task;
|
|
750
|
+
const entry = {
|
|
751
|
+
payload: JSON.stringify(taskP),
|
|
752
|
+
attempt: Number(opts.attempt || 0),
|
|
753
|
+
job: opts.job ?? uuid(),
|
|
754
|
+
idemKey: String(idemKey || uuid()),
|
|
755
|
+
createdAt
|
|
756
|
+
};
|
|
762
757
|
const reqKeysLength = this.keysLength(entry);
|
|
763
758
|
if (batch.length && (batch.length >= this.buildBatchCount || realKeysLength + reqKeysLength > this.buildBatchMaxCount)) {
|
|
764
759
|
batches.push(batch);
|
|
@@ -774,9 +769,6 @@ var PowerQueues = class extends PowerRedis {
|
|
|
774
769
|
return batches;
|
|
775
770
|
}
|
|
776
771
|
keysLength(task) {
|
|
777
|
-
if ("payload" in task && isObj(task.payload)) {
|
|
778
|
-
return 2 + Object.keys(task.payload).length * 2;
|
|
779
|
-
}
|
|
780
772
|
return 2 + Object.keys(task).length * 2;
|
|
781
773
|
}
|
|
782
774
|
payloadBatch(data, opts) {
|
|
@@ -801,7 +793,7 @@ var PowerQueues = class extends PowerRedis {
|
|
|
801
793
|
const entry = item;
|
|
802
794
|
const id = entry.id ?? "*";
|
|
803
795
|
let flat = [];
|
|
804
|
-
if ("payload" in entry
|
|
796
|
+
if ("payload" in entry) {
|
|
805
797
|
for (const [k, v] of Object.entries(entry)) {
|
|
806
798
|
flat.push(k, v);
|
|
807
799
|
}
|
|
@@ -809,7 +801,7 @@ var PowerQueues = class extends PowerRedis {
|
|
|
809
801
|
throw new Error('Task must have "payload" or "flat".');
|
|
810
802
|
}
|
|
811
803
|
const pairs = flat.length / 2;
|
|
812
|
-
if (
|
|
804
|
+
if (pairs <= 0) {
|
|
813
805
|
throw new Error('Task "flat" must contain at least one field/value pair.');
|
|
814
806
|
}
|
|
815
807
|
argv.push(String(id));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "power-queues",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.9",
|
|
4
4
|
"description": "High-performance Redis Streams queue for Node.js with Lua-powered bulk XADD, idempotent workers, heartbeat locks, stuck-task recovery, retries, DLQ, and distributed processing.",
|
|
5
5
|
"author": "ihor-bielchenko",
|
|
6
6
|
"license": "MIT",
|