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 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
  }
@@ -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 = 0;
768
+ let batch = [], realKeysLength = 8;
767
769
  for (let task of tasks) {
768
770
  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
- }
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 && (0, import_full_utils.isObjFilled)(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 ((0, import_full_utils.isNumNZ)(pairs)) {
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
@@ -12,6 +12,7 @@ type AddTasksOptions = {
12
12
  approx?: boolean;
13
13
  exact?: boolean;
14
14
  trimLimit?: number;
15
+ idemKey?: string;
15
16
  job?: string;
16
17
  status?: boolean;
17
18
  statusTimeoutMs?: number;
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ type AddTasksOptions = {
12
12
  approx?: boolean;
13
13
  exact?: boolean;
14
14
  trimLimit?: number;
15
+ idemKey?: string;
15
16
  job?: string;
16
17
  status?: boolean;
17
18
  statusTimeoutMs?: number;
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
  }
@@ -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 = 0;
748
+ let batch = [], realKeysLength = 8;
750
749
  for (let task of tasks) {
751
750
  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
- }
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 && isObjFilled(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 (isNumNZ(pairs)) {
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.7",
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",