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 CHANGED
@@ -35,26 +35,99 @@ yarn add power-queues
35
35
 
36
36
  ``` ts
37
37
  const queue = new PowerQueues({
38
- stream: 'email',
38
+ stream: 'mysql',
39
39
  group: 'workers',
40
40
  });
41
41
 
42
42
  await queue.loadScripts(true);
43
43
 
44
- await queue.addTasks('email', [
45
- { payload: { type: 'welcome', userId: 42 } },
46
- { payload: { type: 'hello', userId: 51 } }
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
- Worker:
50
+ Example of a worker for executing a MySQL insert transaction:
51
51
 
52
52
  ``` ts
53
- class EmailWorker extends PowerQueues {
54
- async onExecute(id, payload) {
55
- await sendEmail(payload);
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
- - `onReady`
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 = String((task[5] || "0") + ":" + task[2] + ":" + task[3]);
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 keySplit = key.split(":");
356
- const attempt = Number(keySplit[0] || 0);
357
- const job = String(keySplit[2]);
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 = "", attempt } = this.values(kv);
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 = 0;
766
+ let batch = [], realKeysLength = 8;
767
767
  for (let task of tasks) {
768
768
  const createdAt = opts?.createdAt || Date.now();
769
- let entry = {};
770
- if ((0, import_full_utils.isObj)(entry)) {
771
- entry = {
772
- payload: JSON.stringify(task),
773
- attempt: Number(opts.attempt || 0),
774
- job: opts.job ?? (0, import_uuid.v4)(),
775
- idemKey: (0, import_uuid.v4)(),
776
- createdAt
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 && (0, import_full_utils.isObjFilled)(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 ((0, import_full_utils.isNumNZ)(pairs)) {
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 = String((task[5] || "0") + ":" + task[2] + ":" + task[3]);
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 keySplit = key.split(":");
339
- const attempt = Number(keySplit[0] || 0);
340
- const job = String(keySplit[2]);
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 = "", attempt } = this.values(kv);
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 = 0;
746
+ let batch = [], realKeysLength = 8;
750
747
  for (let task of tasks) {
751
748
  const createdAt = opts?.createdAt || Date.now();
752
- let entry = {};
753
- if (isObj(entry)) {
754
- entry = {
755
- payload: JSON.stringify(task),
756
- attempt: Number(opts.attempt || 0),
757
- job: opts.job ?? uuid(),
758
- idemKey: uuid(),
759
- createdAt
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 && isObjFilled(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 (isNumNZ(pairs)) {
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.7",
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",