power-queues 2.1.6 → 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
@@ -326,7 +326,14 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
326
326
  } catch (err) {
327
327
  await this.batchError(err, queueName, tasks);
328
328
  try {
329
- await this.approve(queueName, tasks.map((task) => task[0]));
329
+ await this.approve(queueName, tasks.map((task) => ({
330
+ id: task[0],
331
+ createdAt: Number(task[2]),
332
+ payload: task[1],
333
+ job: task[3],
334
+ idemKey: task[4],
335
+ attempt: Number(task[5] || 0)
336
+ })));
330
337
  } catch {
331
338
  }
332
339
  await (0, import_full_utils.wait)(300);
@@ -337,17 +344,17 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
337
344
  try {
338
345
  const filtered = {};
339
346
  tasks.forEach((task, index) => {
340
- const key = String(task[5] + ":" + task[2] + ":" + task[3]);
347
+ const key = JSON.stringify([task[5] || "0", task[2], task[3]]);
341
348
  if (!filtered[key]) {
342
349
  filtered[key] = [];
343
350
  }
344
- filtered[key].push(tasks[index][1]);
351
+ filtered[key].push({ ...tasks[index][1], idemKey: tasks[index][4] });
345
352
  });
346
353
  for (let key in filtered) {
347
354
  const filteredTasks = filtered[key];
348
- const keySplit = key.split(":");
349
- const attempt = Number(keySplit[0] || 0);
350
- const job = String(keySplit[2]);
355
+ const keyP = JSON.parse(key);
356
+ const attempt = Number(keyP[0] || 0);
357
+ const job = String(keyP[2]);
351
358
  if (!(attempt >= this.retryCount - 1)) {
352
359
  await this.addTasks(queueName, filteredTasks, {
353
360
  job,
@@ -363,12 +370,10 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
363
370
  }
364
371
  }
365
372
  } catch (err2) {
366
- throw new Error(`Batch error. ${err2.message}`);
367
373
  }
368
374
  try {
369
375
  await this.onBatchError(err, queueName, tasks);
370
376
  } catch (err2) {
371
- throw new Error(`Batch error. ${err2.message}`);
372
377
  }
373
378
  }
374
379
  async approve(queueName, tasks) {
@@ -440,7 +445,7 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
440
445
  const kv = (0, import_full_utils.isArr)(kvRaw) ? kvRaw.map((x) => Buffer.isBuffer(x) ? x.toString() : x) : [];
441
446
  return [id, kv];
442
447
  }).filter(([id, kv]) => (0, import_full_utils.isStrFilled)(id) && (0, import_full_utils.isArr)(kv) && (kv.length & 1) === 0).map(([id, kv]) => {
443
- const { payload, createdAt, job, idemKey = "", attempt } = this.values(kv);
448
+ const { payload, createdAt, job, idemKey, attempt } = this.values(kv);
444
449
  return [id, this.payload(payload), createdAt, job, idemKey, Number(attempt)];
445
450
  });
446
451
  }
@@ -567,7 +572,8 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
567
572
  await this.addTasks(queueName, [{ ...taskP.payload }], {
568
573
  createdAt: taskP.createdAt,
569
574
  job: taskP.job,
570
- attempt: (taskP.attempt || 0) + 1
575
+ attempt: (taskP.attempt || 0) + 1,
576
+ idemKey: taskP.idemKey
571
577
  });
572
578
  } else if (this.logStatus) {
573
579
  const dlqKey = queueName + ":dlq";
@@ -577,7 +583,8 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
577
583
  await this.addTasks(dlqKey, [{ ...taskP.payload }], {
578
584
  createdAt: taskP.createdAt,
579
585
  job: taskP.job,
580
- attempt: taskP.attempt
586
+ attempt: taskP.attempt,
587
+ idemKey: taskP.idemKey
581
588
  });
582
589
  }
583
590
  return await this.onError(err, queueName, { ...taskP, attempt: (taskP.attempt || 0) + 1 });
@@ -758,19 +765,17 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
758
765
  }
759
766
  buildBatches(tasks, opts = {}) {
760
767
  const batches = [];
761
- let batch = [], realKeysLength = 0;
768
+ let batch = [], realKeysLength = 8;
762
769
  for (let task of tasks) {
763
770
  const createdAt = opts?.createdAt || Date.now();
764
- let entry = {};
765
- if ((0, import_full_utils.isObj)(entry)) {
766
- entry = {
767
- payload: JSON.stringify(task),
768
- attempt: Number(opts.attempt || 0),
769
- job: opts.job ?? (0, import_uuid.v4)(),
770
- idemKey: (0, import_uuid.v4)(),
771
- createdAt
772
- };
773
- }
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
+ };
774
779
  const reqKeysLength = this.keysLength(entry);
775
780
  if (batch.length && (batch.length >= this.buildBatchCount || realKeysLength + reqKeysLength > this.buildBatchMaxCount)) {
776
781
  batches.push(batch);
@@ -786,9 +791,6 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
786
791
  return batches;
787
792
  }
788
793
  keysLength(task) {
789
- if ("payload" in task && (0, import_full_utils.isObj)(task.payload)) {
790
- return 2 + Object.keys(task.payload).length * 2;
791
- }
792
794
  return 2 + Object.keys(task).length * 2;
793
795
  }
794
796
  payloadBatch(data, opts) {
@@ -813,7 +815,7 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
813
815
  const entry = item;
814
816
  const id = entry.id ?? "*";
815
817
  let flat = [];
816
- if ("payload" in entry && (0, import_full_utils.isObjFilled)(entry)) {
818
+ if ("payload" in entry) {
817
819
  for (const [k, v] of Object.entries(entry)) {
818
820
  flat.push(k, v);
819
821
  }
@@ -821,7 +823,7 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
821
823
  throw new Error('Task must have "payload" or "flat".');
822
824
  }
823
825
  const pairs = flat.length / 2;
824
- if ((0, import_full_utils.isNumNZ)(pairs)) {
826
+ if (pairs <= 0) {
825
827
  throw new Error('Task "flat" must contain at least one field/value pair.');
826
828
  }
827
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";
@@ -309,7 +306,14 @@ var PowerQueues = class extends PowerRedis {
309
306
  } catch (err) {
310
307
  await this.batchError(err, queueName, tasks);
311
308
  try {
312
- await this.approve(queueName, tasks.map((task) => task[0]));
309
+ await this.approve(queueName, tasks.map((task) => ({
310
+ id: task[0],
311
+ createdAt: Number(task[2]),
312
+ payload: task[1],
313
+ job: task[3],
314
+ idemKey: task[4],
315
+ attempt: Number(task[5] || 0)
316
+ })));
313
317
  } catch {
314
318
  }
315
319
  await wait(300);
@@ -320,17 +324,17 @@ var PowerQueues = class extends PowerRedis {
320
324
  try {
321
325
  const filtered = {};
322
326
  tasks.forEach((task, index) => {
323
- const key = String(task[5] + ":" + task[2] + ":" + task[3]);
327
+ const key = JSON.stringify([task[5] || "0", task[2], task[3]]);
324
328
  if (!filtered[key]) {
325
329
  filtered[key] = [];
326
330
  }
327
- filtered[key].push(tasks[index][1]);
331
+ filtered[key].push({ ...tasks[index][1], idemKey: tasks[index][4] });
328
332
  });
329
333
  for (let key in filtered) {
330
334
  const filteredTasks = filtered[key];
331
- const keySplit = key.split(":");
332
- const attempt = Number(keySplit[0] || 0);
333
- const job = String(keySplit[2]);
335
+ const keyP = JSON.parse(key);
336
+ const attempt = Number(keyP[0] || 0);
337
+ const job = String(keyP[2]);
334
338
  if (!(attempt >= this.retryCount - 1)) {
335
339
  await this.addTasks(queueName, filteredTasks, {
336
340
  job,
@@ -346,12 +350,10 @@ var PowerQueues = class extends PowerRedis {
346
350
  }
347
351
  }
348
352
  } catch (err2) {
349
- throw new Error(`Batch error. ${err2.message}`);
350
353
  }
351
354
  try {
352
355
  await this.onBatchError(err, queueName, tasks);
353
356
  } catch (err2) {
354
- throw new Error(`Batch error. ${err2.message}`);
355
357
  }
356
358
  }
357
359
  async approve(queueName, tasks) {
@@ -423,7 +425,7 @@ var PowerQueues = class extends PowerRedis {
423
425
  const kv = isArr(kvRaw) ? kvRaw.map((x) => Buffer.isBuffer(x) ? x.toString() : x) : [];
424
426
  return [id, kv];
425
427
  }).filter(([id, kv]) => isStrFilled(id) && isArr(kv) && (kv.length & 1) === 0).map(([id, kv]) => {
426
- const { payload, createdAt, job, idemKey = "", attempt } = this.values(kv);
428
+ const { payload, createdAt, job, idemKey, attempt } = this.values(kv);
427
429
  return [id, this.payload(payload), createdAt, job, idemKey, Number(attempt)];
428
430
  });
429
431
  }
@@ -550,7 +552,8 @@ var PowerQueues = class extends PowerRedis {
550
552
  await this.addTasks(queueName, [{ ...taskP.payload }], {
551
553
  createdAt: taskP.createdAt,
552
554
  job: taskP.job,
553
- attempt: (taskP.attempt || 0) + 1
555
+ attempt: (taskP.attempt || 0) + 1,
556
+ idemKey: taskP.idemKey
554
557
  });
555
558
  } else if (this.logStatus) {
556
559
  const dlqKey = queueName + ":dlq";
@@ -560,7 +563,8 @@ var PowerQueues = class extends PowerRedis {
560
563
  await this.addTasks(dlqKey, [{ ...taskP.payload }], {
561
564
  createdAt: taskP.createdAt,
562
565
  job: taskP.job,
563
- attempt: taskP.attempt
566
+ attempt: taskP.attempt,
567
+ idemKey: taskP.idemKey
564
568
  });
565
569
  }
566
570
  return await this.onError(err, queueName, { ...taskP, attempt: (taskP.attempt || 0) + 1 });
@@ -741,19 +745,17 @@ var PowerQueues = class extends PowerRedis {
741
745
  }
742
746
  buildBatches(tasks, opts = {}) {
743
747
  const batches = [];
744
- let batch = [], realKeysLength = 0;
748
+ let batch = [], realKeysLength = 8;
745
749
  for (let task of tasks) {
746
750
  const createdAt = opts?.createdAt || Date.now();
747
- let entry = {};
748
- if (isObj(entry)) {
749
- entry = {
750
- payload: JSON.stringify(task),
751
- attempt: Number(opts.attempt || 0),
752
- job: opts.job ?? uuid(),
753
- idemKey: uuid(),
754
- createdAt
755
- };
756
- }
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
+ };
757
759
  const reqKeysLength = this.keysLength(entry);
758
760
  if (batch.length && (batch.length >= this.buildBatchCount || realKeysLength + reqKeysLength > this.buildBatchMaxCount)) {
759
761
  batches.push(batch);
@@ -769,9 +771,6 @@ var PowerQueues = class extends PowerRedis {
769
771
  return batches;
770
772
  }
771
773
  keysLength(task) {
772
- if ("payload" in task && isObj(task.payload)) {
773
- return 2 + Object.keys(task.payload).length * 2;
774
- }
775
774
  return 2 + Object.keys(task).length * 2;
776
775
  }
777
776
  payloadBatch(data, opts) {
@@ -796,7 +795,7 @@ var PowerQueues = class extends PowerRedis {
796
795
  const entry = item;
797
796
  const id = entry.id ?? "*";
798
797
  let flat = [];
799
- if ("payload" in entry && isObjFilled(entry)) {
798
+ if ("payload" in entry) {
800
799
  for (const [k, v] of Object.entries(entry)) {
801
800
  flat.push(k, v);
802
801
  }
@@ -804,7 +803,7 @@ var PowerQueues = class extends PowerRedis {
804
803
  throw new Error('Task must have "payload" or "flat".');
805
804
  }
806
805
  const pairs = flat.length / 2;
807
- if (isNumNZ(pairs)) {
806
+ if (pairs <= 0) {
808
807
  throw new Error('Task "flat" must contain at least one field/value pair.');
809
808
  }
810
809
  argv.push(String(id));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "power-queues",
3
- "version": "2.1.6",
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",