power-queues 2.1.7 → 2.1.8
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 +21 -24
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +21 -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
|
}
|
|
@@ -572,7 +572,8 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
|
|
|
572
572
|
await this.addTasks(queueName, [{ ...taskP.payload }], {
|
|
573
573
|
createdAt: taskP.createdAt,
|
|
574
574
|
job: taskP.job,
|
|
575
|
-
attempt: (taskP.attempt || 0) + 1
|
|
575
|
+
attempt: (taskP.attempt || 0) + 1,
|
|
576
|
+
idemKey: taskP.idemKey
|
|
576
577
|
});
|
|
577
578
|
} else if (this.logStatus) {
|
|
578
579
|
const dlqKey = queueName + ":dlq";
|
|
@@ -582,7 +583,8 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
|
|
|
582
583
|
await this.addTasks(dlqKey, [{ ...taskP.payload }], {
|
|
583
584
|
createdAt: taskP.createdAt,
|
|
584
585
|
job: taskP.job,
|
|
585
|
-
attempt: taskP.attempt
|
|
586
|
+
attempt: taskP.attempt,
|
|
587
|
+
idemKey: taskP.idemKey
|
|
586
588
|
});
|
|
587
589
|
}
|
|
588
590
|
return await this.onError(err, queueName, { ...taskP, attempt: (taskP.attempt || 0) + 1 });
|
|
@@ -763,19 +765,17 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
|
|
|
763
765
|
}
|
|
764
766
|
buildBatches(tasks, opts = {}) {
|
|
765
767
|
const batches = [];
|
|
766
|
-
let batch = [], realKeysLength =
|
|
768
|
+
let batch = [], realKeysLength = 8;
|
|
767
769
|
for (let task of tasks) {
|
|
768
770
|
const createdAt = opts?.createdAt || Date.now();
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
};
|
|
778
|
-
}
|
|
771
|
+
const { idemKey, ...taskP } = task;
|
|
772
|
+
const entry = {
|
|
773
|
+
payload: JSON.stringify(taskP),
|
|
774
|
+
attempt: Number(opts.attempt || 0),
|
|
775
|
+
job: opts.job ?? (0, import_uuid.v4)(),
|
|
776
|
+
idemKey: String((idemKey ?? opts?.idemKey) || (0, import_uuid.v4)()),
|
|
777
|
+
createdAt
|
|
778
|
+
};
|
|
779
779
|
const reqKeysLength = this.keysLength(entry);
|
|
780
780
|
if (batch.length && (batch.length >= this.buildBatchCount || realKeysLength + reqKeysLength > this.buildBatchMaxCount)) {
|
|
781
781
|
batches.push(batch);
|
|
@@ -791,9 +791,6 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
|
|
|
791
791
|
return batches;
|
|
792
792
|
}
|
|
793
793
|
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
794
|
return 2 + Object.keys(task).length * 2;
|
|
798
795
|
}
|
|
799
796
|
payloadBatch(data, opts) {
|
|
@@ -818,7 +815,7 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
|
|
|
818
815
|
const entry = item;
|
|
819
816
|
const id = entry.id ?? "*";
|
|
820
817
|
let flat = [];
|
|
821
|
-
if ("payload" in entry
|
|
818
|
+
if ("payload" in entry) {
|
|
822
819
|
for (const [k, v] of Object.entries(entry)) {
|
|
823
820
|
flat.push(k, v);
|
|
824
821
|
}
|
|
@@ -826,7 +823,7 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
|
|
|
826
823
|
throw new Error('Task must have "payload" or "flat".');
|
|
827
824
|
}
|
|
828
825
|
const pairs = flat.length / 2;
|
|
829
|
-
if (
|
|
826
|
+
if (pairs <= 0) {
|
|
830
827
|
throw new Error('Task "flat" must contain at least one field/value pair.');
|
|
831
828
|
}
|
|
832
829
|
argv.push(String(id));
|
package/dist/index.d.cts
CHANGED
package/dist/index.d.ts
CHANGED
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
|
}
|
|
@@ -555,7 +552,8 @@ var PowerQueues = class extends PowerRedis {
|
|
|
555
552
|
await this.addTasks(queueName, [{ ...taskP.payload }], {
|
|
556
553
|
createdAt: taskP.createdAt,
|
|
557
554
|
job: taskP.job,
|
|
558
|
-
attempt: (taskP.attempt || 0) + 1
|
|
555
|
+
attempt: (taskP.attempt || 0) + 1,
|
|
556
|
+
idemKey: taskP.idemKey
|
|
559
557
|
});
|
|
560
558
|
} else if (this.logStatus) {
|
|
561
559
|
const dlqKey = queueName + ":dlq";
|
|
@@ -565,7 +563,8 @@ var PowerQueues = class extends PowerRedis {
|
|
|
565
563
|
await this.addTasks(dlqKey, [{ ...taskP.payload }], {
|
|
566
564
|
createdAt: taskP.createdAt,
|
|
567
565
|
job: taskP.job,
|
|
568
|
-
attempt: taskP.attempt
|
|
566
|
+
attempt: taskP.attempt,
|
|
567
|
+
idemKey: taskP.idemKey
|
|
569
568
|
});
|
|
570
569
|
}
|
|
571
570
|
return await this.onError(err, queueName, { ...taskP, attempt: (taskP.attempt || 0) + 1 });
|
|
@@ -746,19 +745,17 @@ var PowerQueues = class extends PowerRedis {
|
|
|
746
745
|
}
|
|
747
746
|
buildBatches(tasks, opts = {}) {
|
|
748
747
|
const batches = [];
|
|
749
|
-
let batch = [], realKeysLength =
|
|
748
|
+
let batch = [], realKeysLength = 8;
|
|
750
749
|
for (let task of tasks) {
|
|
751
750
|
const createdAt = opts?.createdAt || Date.now();
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
};
|
|
761
|
-
}
|
|
751
|
+
const { idemKey, ...taskP } = task;
|
|
752
|
+
const entry = {
|
|
753
|
+
payload: JSON.stringify(taskP),
|
|
754
|
+
attempt: Number(opts.attempt || 0),
|
|
755
|
+
job: opts.job ?? uuid(),
|
|
756
|
+
idemKey: String((idemKey ?? opts?.idemKey) || uuid()),
|
|
757
|
+
createdAt
|
|
758
|
+
};
|
|
762
759
|
const reqKeysLength = this.keysLength(entry);
|
|
763
760
|
if (batch.length && (batch.length >= this.buildBatchCount || realKeysLength + reqKeysLength > this.buildBatchMaxCount)) {
|
|
764
761
|
batches.push(batch);
|
|
@@ -774,9 +771,6 @@ var PowerQueues = class extends PowerRedis {
|
|
|
774
771
|
return batches;
|
|
775
772
|
}
|
|
776
773
|
keysLength(task) {
|
|
777
|
-
if ("payload" in task && isObj(task.payload)) {
|
|
778
|
-
return 2 + Object.keys(task.payload).length * 2;
|
|
779
|
-
}
|
|
780
774
|
return 2 + Object.keys(task).length * 2;
|
|
781
775
|
}
|
|
782
776
|
payloadBatch(data, opts) {
|
|
@@ -801,7 +795,7 @@ var PowerQueues = class extends PowerRedis {
|
|
|
801
795
|
const entry = item;
|
|
802
796
|
const id = entry.id ?? "*";
|
|
803
797
|
let flat = [];
|
|
804
|
-
if ("payload" in entry
|
|
798
|
+
if ("payload" in entry) {
|
|
805
799
|
for (const [k, v] of Object.entries(entry)) {
|
|
806
800
|
flat.push(k, v);
|
|
807
801
|
}
|
|
@@ -809,7 +803,7 @@ var PowerQueues = class extends PowerRedis {
|
|
|
809
803
|
throw new Error('Task must have "payload" or "flat".');
|
|
810
804
|
}
|
|
811
805
|
const pairs = flat.length / 2;
|
|
812
|
-
if (
|
|
806
|
+
if (pairs <= 0) {
|
|
813
807
|
throw new Error('Task "flat" must contain at least one field/value pair.');
|
|
814
808
|
}
|
|
815
809
|
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.8",
|
|
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",
|