power-queues 2.0.21 → 2.1.0

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/dist/index.cjs CHANGED
@@ -269,315 +269,191 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
269
269
  super(...arguments);
270
270
  this.abort = new AbortController();
271
271
  this.scripts = {};
272
- this.addingBatchTasksCount = 800;
273
- this.addingBatchKeysLimit = 1e4;
274
- this.workerExecuteLockTimeoutMs = 18e4;
275
- this.workerCacheTaskTimeoutMs = 6e4;
276
- this.approveBatchTasksCount = 2e3;
272
+ this.host = "host";
273
+ this.group = "gr1";
274
+ this.selectStuckCount = 200;
275
+ this.selectStuckTimeout = 6e4;
276
+ this.selectStuckMaxTimeout = 80;
277
+ this.selectCount = 200;
278
+ this.selectTimeout = 5e3;
279
+ this.buildBatchCount = 800;
280
+ this.buildBatchMaxCount = 1e4;
281
+ this.retryCount = 1;
282
+ this.executeSync = false;
283
+ this.idemLockTimeout = 18e4;
284
+ this.idemDoneTimeout = 6e4;
285
+ this.logStatus = false;
286
+ this.logStatusTimeout = 3e5;
287
+ this.approveCount = 2e3;
277
288
  this.removeOnExecuted = false;
278
- this.executeBatchAtOnce = false;
279
- this.executeJobStatus = false;
280
- this.executeJobStatusTtlMs = 3e5;
281
- this.consumerHost = "host";
282
- this.stream = "stream";
283
- this.group = "group";
284
- this.workerBatchTasksCount = 200;
285
- this.recoveryStuckTasksTimeoutMs = 6e4;
286
- this.workerLoopIntervalMs = 5e3;
287
- this.workerSelectionTimeoutMs = 80;
288
- this.workerMaxRetries = 1;
289
- this.workerClearAttemptsTimeoutMs = 864e5;
290
- }
291
- async onSelected(data) {
292
- return data;
293
- }
294
- async onExecute(id, payload, createdAt, job, key, attempt) {
295
- }
296
- async onReady(data) {
297
- }
298
- async onSuccess(id, payload, createdAt, job, key) {
299
289
  }
300
- async onBatchError(err, tasks) {
290
+ signal() {
291
+ return this.abort.signal;
301
292
  }
302
- async onError(err, id, payload, createdAt, job, key) {
293
+ consumer() {
294
+ return this.host + ":" + process.pid;
303
295
  }
304
- async onRetry(err, id, payload, createdAt, job, key, attempts) {
296
+ async runQueue(queueName, from = "0-0") {
297
+ await this.createGroup(queueName, from);
298
+ await this.consumerLoop(queueName, from);
305
299
  }
306
- async runQueue() {
307
- await this.createGroup("0-0");
308
- await this.consumerLoop();
300
+ async createGroup(queueName, from = "0-0") {
301
+ try {
302
+ await this.redis.xgroup("CREATE", queueName, this.group, from, "MKSTREAM");
303
+ } catch (err) {
304
+ const msg = String(err?.message || "");
305
+ if (!msg.includes("BUSYGROUP")) {
306
+ throw err;
307
+ }
308
+ }
309
309
  }
310
- async consumerLoop() {
310
+ async consumerLoop(queueName, from = "0-0") {
311
311
  const signal = this.signal();
312
312
  while (!signal?.aborted) {
313
+ let tasks = [];
313
314
  try {
314
- const tasks = await this.select();
315
- if (!(0, import_full_utils.isArrFilled)(tasks)) {
316
- await (0, import_full_utils.wait)(600);
317
- continue;
318
- }
319
- const tasksP = await this.onSelected(tasks);
320
- const ids = await this.execute((0, import_full_utils.isArrFilled)(tasksP) ? tasksP : tasks);
321
- if ((0, import_full_utils.isArrFilled)(ids)) {
322
- await this.approve(ids);
323
- }
315
+ tasks = await this.select(queueName, from);
324
316
  } catch (err) {
325
- await this.batchError(err);
326
- await (0, import_full_utils.wait)(600);
317
+ }
318
+ if (!(0, import_full_utils.isArrFilled)(tasks)) {
319
+ await (0, import_full_utils.wait)(400);
320
+ continue;
321
+ }
322
+ try {
323
+ await this.approve(queueName, await this.execute(queueName, await this.beforeExecute(queueName, tasks)));
324
+ } catch (err) {
325
+ await this.batchError(err, queueName, tasks);
326
+ try {
327
+ await this.approve(queueName, tasks.map((task) => task[0]));
328
+ } catch {
329
+ }
330
+ await (0, import_full_utils.wait)(400);
327
331
  }
328
332
  }
329
333
  }
330
- async addTasks(queueName, data, opts = {}) {
331
- if (!(0, import_full_utils.isArrFilled)(data)) {
332
- throw new Error("Tasks is not filled.");
333
- }
334
- if (!(0, import_full_utils.isStrFilled)(queueName)) {
335
- throw new Error("Queue name is required.");
336
- }
337
- const job = (0, import_uuid.v4)();
338
- const batches = this.buildBatches(data, job, opts.idem);
339
- const result = new Array(data.length);
340
- const promises = [];
341
- let cursor = 0;
342
- for (const batch of batches) {
343
- const start = cursor;
344
- const end = start + batch.length;
345
- cursor = end;
346
- promises.push(async () => {
347
- const partIds = await this.xaddBatch(queueName, ...this.payloadBatch(batch, opts));
348
- for (let k = 0; k < partIds.length; k++) {
349
- result[start + k] = partIds[k];
334
+ async batchError(err, queueName, tasks) {
335
+ try {
336
+ const filtered = {};
337
+ tasks.forEach((task, index) => {
338
+ const key = String(task[5] + ":" + task[2] + ":" + task[3]);
339
+ if (!filtered[key]) {
340
+ filtered[key] = [];
350
341
  }
342
+ filtered[key].push(tasks[index]);
351
343
  });
352
- }
353
- const runners = Array.from({ length: promises.length }, async () => {
354
- while (promises.length) {
355
- const promise = promises.shift();
356
- if (promise) {
357
- await promise();
358
- }
344
+ for (let key in filtered) {
345
+ const filteredTasks = filtered[key];
346
+ const keySplit = key.split(":");
347
+ const attempt = Number(keySplit[0]);
348
+ await this.addTasks(queueName + (attempt + 1 >= this.retryCount ? ":dlq" : ""), filteredTasks.map((task) => ({ ...task[1] })), {
349
+ createdAt: Number(keySplit[1]),
350
+ job: keySplit[2],
351
+ attempt: attempt + 1
352
+ });
359
353
  }
360
- });
361
- if (opts.status) {
362
- await this.redis.set(`${queueName}:${job}:total`, data.length);
363
- await this.redis.pexpire(`${queueName}:${job}:total`, opts.statusTimeoutMs || 3e5);
354
+ } catch (err2) {
355
+ throw new Error(`Batch error. ${err2.message}`);
364
356
  }
365
- await Promise.all(runners);
366
- return result;
367
- }
368
- async loadScripts(full = false) {
369
- const scripts = full ? [
370
- ["XAddBulk", XAddBulk],
371
- ["Approve", Approve],
372
- ["IdempotencyAllow", IdempotencyAllow],
373
- ["IdempotencyStart", IdempotencyStart],
374
- ["IdempotencyDone", IdempotencyDone],
375
- ["IdempotencyFree", IdempotencyFree],
376
- ["SelectStuck", SelectStuck]
377
- ] : [
378
- ["XAddBulk", XAddBulk]
379
- ];
380
- for (const [name, code] of scripts) {
381
- await this.loadScript(this.saveScript(name, code));
357
+ try {
358
+ await this.onBatchError(err, queueName, tasks);
359
+ } catch (err2) {
360
+ throw new Error(`Batch error. ${err2.message}`);
382
361
  }
383
362
  }
384
- async loadScript(code) {
385
- for (let i = 0; i < 3; i++) {
386
- try {
387
- return await this.redis.script("LOAD", code);
388
- } catch (e) {
389
- if (i === 2) {
390
- throw e;
391
- }
392
- await new Promise((r) => setTimeout(r, 10 + Math.floor(Math.random() * 40)));
393
- }
363
+ async approve(queueName, ids) {
364
+ if (!(0, import_full_utils.isArrFilled)(ids)) {
365
+ return 0;
394
366
  }
395
- throw new Error("Load lua script failed.");
396
- }
397
- saveScript(name, codeBody) {
398
- if (!(0, import_full_utils.isStrFilled)(codeBody)) {
399
- throw new Error("Script body is empty.");
367
+ const approveCount = Math.max(500, Math.min(4e3, this.approveCount));
368
+ let total = 0, i = 0;
369
+ while (i < ids.length) {
370
+ const room = Math.min(approveCount, ids.length - i);
371
+ const part = ids.slice(i, i + room);
372
+ const approved = await this.runScript("Approve", [queueName], [this.group, this.removeOnExecuted ? "1" : "0", ...part], Approve);
373
+ total += Number(approved || 0);
374
+ i += room;
400
375
  }
401
- this.scripts[name] = { codeBody };
402
- return codeBody;
376
+ return total;
403
377
  }
404
- async runScript(name, keys, args, defaultCode) {
405
- if (!this.scripts[name]) {
406
- if (!(0, import_full_utils.isStrFilled)(defaultCode)) {
407
- throw new Error(`Undefined script "${name}". Save it before executing.`);
408
- }
409
- this.saveScript(name, defaultCode);
410
- }
411
- if (!this.scripts[name].codeReady) {
412
- this.scripts[name].codeReady = await this.loadScript(this.scripts[name].codeBody);
378
+ async select(queueName, from = "0-0") {
379
+ let selected = await this.selectS(queueName, from);
380
+ if (!(0, import_full_utils.isArrFilled)(selected)) {
381
+ selected = await this.selectF(queueName, from);
413
382
  }
383
+ return this.selectP(selected);
384
+ }
385
+ async selectS(queueName, from = "0-0") {
414
386
  try {
415
- return await this.redis.evalsha(this.scripts[name].codeReady, keys.length, ...keys, ...args);
387
+ const res = await this.runScript("SelectStuck", [queueName], [this.group, this.consumer(), String(this.selectStuckTimeout), String(this.selectStuckCount), String(this.selectStuckMaxTimeout)], SelectStuck);
388
+ return (0, import_full_utils.isArr)(res) ? res : [];
416
389
  } catch (err) {
417
- if (String(err?.message || "").includes("NOSCRIPT")) {
418
- this.scripts[name].codeReady = await this.loadScript(this.scripts[name].codeBody);
419
- return await this.redis.evalsha(this.scripts[name].codeReady, keys.length, ...keys, ...args);
390
+ if (String(err?.message || "").includes("NOGROUP")) {
391
+ await this.createGroup(queueName, from);
420
392
  }
421
- throw err;
422
393
  }
394
+ return [];
423
395
  }
424
- async xaddBatch(queueName, ...batches) {
425
- return await this.runScript("XAddBulk", [queueName], batches, XAddBulk);
426
- }
427
- payloadBatch(data, opts) {
428
- const maxlen = Math.max(0, Math.floor(opts?.maxlen ?? 0));
429
- const approx = opts?.exact ? 0 : opts?.approx !== false ? 1 : 0;
430
- const exact = opts?.exact ? 1 : 0;
431
- const nomkstream = opts?.nomkstream ? 1 : 0;
432
- const trimLimit = Math.max(0, Math.floor(opts?.trimLimit ?? 0));
433
- const minidWindowMs = Math.max(0, Math.floor(opts?.minidWindowMs ?? 0));
434
- const minidExact = opts?.minidExact ? 1 : 0;
435
- const argv = [
436
- String(maxlen),
437
- String(approx),
438
- String(data.length),
439
- String(exact),
440
- String(nomkstream),
441
- String(trimLimit),
442
- String(minidWindowMs),
443
- String(minidExact)
444
- ];
445
- for (const item of data) {
446
- const entry = item;
447
- const id = entry.id ?? "*";
448
- let flat;
449
- if ("flat" in entry && (0, import_full_utils.isArrFilled)(entry.flat)) {
450
- flat = entry.flat;
451
- if (flat.length % 2 !== 0) {
452
- throw new Error('Property "flat" must contain an even number of realKeysLength (field/value pairs).');
453
- }
454
- } else if ("payload" in entry && (0, import_full_utils.isObjFilled)(entry.payload)) {
455
- flat = [];
456
- for (const [k, v] of Object.entries(entry.payload)) {
457
- flat.push(k, v);
458
- }
459
- } else {
460
- throw new Error('Task must have "payload" or "flat".');
461
- }
462
- const pairs = flat.length / 2;
463
- if ((0, import_full_utils.isNumNZ)(pairs)) {
464
- throw new Error('Task "flat" must contain at least one field/value pair.');
396
+ async selectF(queueName, from = "0-0") {
397
+ let rows = [];
398
+ try {
399
+ const res = await this.redis.xreadgroup(
400
+ "GROUP",
401
+ this.group,
402
+ this.consumer(),
403
+ "BLOCK",
404
+ Math.max(2, this.selectTimeout | 0),
405
+ "COUNT",
406
+ this.selectCount,
407
+ "STREAMS",
408
+ queueName,
409
+ ">"
410
+ );
411
+ rows = res?.[0]?.[1] ?? [];
412
+ if (!(0, import_full_utils.isArrFilled)(rows)) {
413
+ return [];
465
414
  }
466
- argv.push(String(id));
467
- argv.push(String(pairs));
468
- for (const token of flat) {
469
- argv.push(!token ? "" : (0, import_full_utils.isStrFilled)(token) ? token : String(token));
415
+ } catch (err) {
416
+ if (String(err?.message || "").includes("NOGROUP")) {
417
+ await this.createGroup(queueName, from);
470
418
  }
471
419
  }
472
- return argv;
420
+ return rows;
473
421
  }
474
- buildBatches(tasks, job, idem) {
475
- const batches = [];
476
- let batch = [], realKeysLength = 0;
477
- for (let task of tasks) {
478
- const createdAt = task?.createdAt || Date.now();
479
- let entry = task;
480
- if ((0, import_full_utils.isObj)(entry.payload)) {
481
- entry = {
482
- ...entry,
483
- payload: {
484
- payload: JSON.stringify(entry.payload),
485
- createdAt,
486
- job
487
- }
488
- };
489
- if (idem) {
490
- entry.payload["idemKey"] = entry?.idemKey || (0, import_uuid.v4)();
491
- }
492
- } else if (Array.isArray(entry.flat)) {
493
- entry.flat.push("createdAt");
494
- entry.flat.push(String(createdAt));
495
- entry.flat.push("job");
496
- entry.flat.push(job);
497
- if (idem) {
498
- entry.flat.push("idemKey");
499
- entry.flat.push(entry?.idemKey || (0, import_uuid.v4)());
500
- }
501
- }
502
- const reqKeysLength = this.keysLength(entry);
503
- if (batch.length && (batch.length >= this.addingBatchTasksCount || realKeysLength + reqKeysLength > this.addingBatchKeysLimit)) {
504
- batches.push(batch);
505
- batch = [];
506
- realKeysLength = 0;
507
- }
508
- batch.push(entry);
509
- realKeysLength += reqKeysLength;
510
- }
511
- if (batch.length) {
512
- batches.push(batch);
422
+ selectP(raw) {
423
+ if (!Array.isArray(raw)) {
424
+ return [];
513
425
  }
514
- return batches;
426
+ return Array.from(raw || []).map((e) => {
427
+ const id = Buffer.isBuffer(e?.[0]) ? e[0].toString() : e?.[0];
428
+ const kvRaw = e?.[1] ?? [];
429
+ const kv = (0, import_full_utils.isArr)(kvRaw) ? kvRaw.map((x) => Buffer.isBuffer(x) ? x.toString() : x) : [];
430
+ return [id, kv];
431
+ }).filter(([id, kv]) => (0, import_full_utils.isStrFilled)(id) && (0, import_full_utils.isArr)(kv) && (kv.length & 1) === 0).map(([id, kv]) => {
432
+ const { payload, createdAt, job, idemKey = "", attempt } = this.values(kv);
433
+ return [id, this.payload(payload), createdAt, job, idemKey, Number(attempt)];
434
+ });
515
435
  }
516
- keysLength(task) {
517
- if ("flat" in task && Array.isArray(task.flat) && task.flat.length) {
518
- return 2 + task.flat.length;
519
- }
520
- if ("payload" in task && (0, import_full_utils.isObj)(task.payload)) {
521
- return 2 + Object.keys(task.payload).length * 2;
436
+ values(value) {
437
+ const result = {};
438
+ for (let i = 0; i < value.length; i += 2) {
439
+ result[value[i]] = value[i + 1];
522
440
  }
523
- return 2 + Object.keys(task).length * 2;
524
- }
525
- attemptsKey(id) {
526
- const safeStream = this.stream.replace(/[^\w:\-]/g, "_");
527
- const safeId = id.replace(/[^\w:\-]/g, "_");
528
- return `q:${safeStream}:attempts:${safeId}`;
441
+ return result;
529
442
  }
530
- async incrAttempts(id) {
443
+ payload(data) {
531
444
  try {
532
- const key = this.attemptsKey(id);
533
- const attempts = await this.redis.incr(key);
534
- await this.redis.pexpire(key, this.workerClearAttemptsTimeoutMs);
535
- return attempts;
445
+ return JSON.parse(data);
536
446
  } catch (err) {
537
447
  }
538
- return 0;
539
- }
540
- async getAttempts(id) {
541
- const key = this.attemptsKey(id);
542
- const v = await this.redis.get(key);
543
- return Number(v || 0);
544
- }
545
- async clearAttempts(id) {
546
- const key = this.attemptsKey(id);
547
- try {
548
- await this.redis.del(key);
549
- } catch (e) {
550
- }
551
- }
552
- async success(id, payload, createdAt, job, key) {
553
- if (this.executeJobStatus) {
554
- const prefix = `${this.stream}:${job}:`;
555
- await this.incr(`${prefix}ok`, this.executeJobStatusTtlMs);
556
- await this.incr(`${prefix}ready`, this.executeJobStatusTtlMs);
557
- }
558
- await this.onSuccess(id, payload, createdAt, job, key);
559
- }
560
- async batchError(err, tasks) {
561
- await this.onBatchError(err, tasks);
562
- }
563
- async error(err, id, payload, createdAt, job, key, attempt) {
564
- if (this.executeJobStatus && attempt >= this.workerMaxRetries) {
565
- const prefix = `${this.stream}:${job}:`;
566
- await this.incr(`${prefix}err`, this.executeJobStatusTtlMs);
567
- await this.incr(`${prefix}ready`, this.executeJobStatusTtlMs);
568
- }
569
- await this.onError(err, id, payload, createdAt, job, key);
570
- }
571
- async attempt(err, id, payload, createdAt, job, key, attempt) {
572
- await this.onRetry(err, id, payload, createdAt, job, key, attempt);
448
+ return data;
573
449
  }
574
- async execute(tasks) {
450
+ async execute(queueName, tasks) {
575
451
  const result = [];
576
452
  let contended = 0, promises = [];
577
- for (const [id, payload, createdAt, job, idemKey] of tasks) {
578
- if (this.executeBatchAtOnce) {
453
+ for (const [id, payload, createdAt, job, idemKey, attempt] of tasks) {
454
+ if (!this.executeSync) {
579
455
  promises.push((async () => {
580
- const r = await this.executeProcess(id, payload, createdAt, job, idemKey);
456
+ const r = await this.executeProcess(queueName, { id, payload, createdAt, job, idemKey, attempt });
581
457
  if (r.id) {
582
458
  result.push(id);
583
459
  } else if (r.contended) {
@@ -585,7 +461,7 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
585
461
  }
586
462
  })());
587
463
  } else {
588
- const r = await this.executeProcess(id, payload, createdAt, job, idemKey);
464
+ const r = await this.executeProcess(queueName, { id, payload, createdAt, job, idemKey, attempt });
589
465
  if (r.id) {
590
466
  result.push(id);
591
467
  } else if (r.contended) {
@@ -593,69 +469,20 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
593
469
  }
594
470
  }
595
471
  }
596
- try {
597
- if (this.executeBatchAtOnce && promises.length > 0) {
598
- await Promise.all(promises);
599
- }
600
- await this.onReady(tasks);
601
- if (!(0, import_full_utils.isArrFilled)(result) && contended > tasks.length >> 1) {
602
- await this.waitAbortable(15 + Math.floor(Math.random() * 35) + Math.min(250, 15 * contended + Math.floor(Math.random() * 40)));
603
- }
604
- } catch (err) {
605
- await this.batchError(err, tasks);
606
- }
607
- return result;
608
- }
609
- async executeProcess(id, payload, createdAt, job, key) {
610
- if (key) {
611
- return await this.idempotency(id, payload, createdAt, job, key);
612
- } else {
613
- try {
614
- await this.onExecute(id, payload, createdAt, job, key, await this.getAttempts(id));
615
- await this.success(id, payload, createdAt, job, key);
616
- return { id };
617
- } catch (err) {
618
- const attempt = await this.incrAttempts(id);
619
- await this.attempt(err, id, payload, createdAt, job, key, attempt);
620
- await this.error(err, id, payload, createdAt, job, key, attempt);
621
- if (attempt >= this.workerMaxRetries) {
622
- await this.addTasks(`${this.stream}:dlq`, [{
623
- payload: {
624
- ...payload,
625
- error: String(err?.message || err),
626
- createdAt,
627
- job,
628
- id,
629
- attempt
630
- }
631
- }]);
632
- await this.clearAttempts(id);
633
- return { id };
634
- }
635
- }
636
- }
637
- return {};
638
- }
639
- async approve(ids) {
640
- if (!Array.isArray(ids) || !(ids.length > 0)) {
641
- return 0;
472
+ if (!this.executeSync && promises.length > 0) {
473
+ await Promise.all(promises);
642
474
  }
643
- const approveBatchTasksCount = Math.max(500, Math.min(4e3, this.approveBatchTasksCount));
644
- let total = 0, i = 0;
645
- while (i < ids.length) {
646
- const room = Math.min(approveBatchTasksCount, ids.length - i);
647
- const part = ids.slice(i, i + room);
648
- const approved = await this.runScript("Approve", [this.stream], [this.group, this.removeOnExecuted ? "1" : "0", ...part], Approve);
649
- total += Number(approved || 0);
650
- i += room;
475
+ await this.onBatchSuccess(queueName, tasks);
476
+ if (!(0, import_full_utils.isArrFilled)(result) && contended > tasks.length >> 1) {
477
+ await this.waitAbortable(15 + Math.floor(Math.random() * 35) + Math.min(250, 15 * contended + Math.floor(Math.random() * 40)));
651
478
  }
652
- return total;
479
+ return result;
653
480
  }
654
- async idempotency(id, payload, createdAt, job, key) {
655
- const keys = this.idempotencyKeys(key);
481
+ async executeProcess(queueName, task) {
482
+ const keys = this.idempotencyKeys(queueName, String(task.idemKey));
656
483
  const allow = await this.idempotencyAllow(keys);
657
484
  if (allow === 1) {
658
- return { id };
485
+ return { id: task.id };
659
486
  } else if (allow === 0) {
660
487
  let ttl = -2;
661
488
  try {
@@ -671,29 +498,17 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
671
498
  const heartbeat = this.heartbeat(keys) || (() => {
672
499
  });
673
500
  try {
674
- await this.onExecute(id, payload, createdAt, job, key, await this.getAttempts(id));
501
+ await this.onExecute(queueName, task);
675
502
  await this.idempotencyDone(keys);
676
- await this.success(id, payload, createdAt, job, key);
677
- return { id };
503
+ await this.success(queueName, task);
504
+ return { id: task.id };
678
505
  } catch (err) {
679
- const attempt = await this.incrAttempts(id);
680
506
  try {
681
- await this.attempt(err, id, payload, createdAt, job, key, attempt);
682
- await this.error(err, id, payload, createdAt, job, key, attempt);
683
- if (attempt >= this.workerMaxRetries) {
684
- await this.addTasks(`${this.stream}:dlq`, [{
685
- payload: {
686
- ...payload,
687
- error: String(err?.message || err),
688
- createdAt,
689
- job,
690
- id,
691
- attempt: 0
692
- }
693
- }]);
694
- await this.clearAttempts(id);
507
+ task.attempt = task.attempt + 1;
508
+ await this.error(err, queueName, task);
509
+ if (task.attempt >= this.retryCount) {
695
510
  await this.idempotencyFree(keys);
696
- return { id };
511
+ return { id: task.id };
697
512
  }
698
513
  await this.idempotencyFree(keys);
699
514
  } catch (err2) {
@@ -702,8 +517,8 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
702
517
  heartbeat();
703
518
  }
704
519
  }
705
- idempotencyKeys(key) {
706
- const prefix = `q:${this.stream.replace(/[^\w:\-]/g, "_")}:`;
520
+ idempotencyKeys(queueName, key) {
521
+ const prefix = `q:${queueName.replace(/[^\w:\-]/g, "_")}:`;
707
522
  const keyP = key.replace(/[^\w:\-]/g, "_");
708
523
  const doneKey = `${prefix}done:${keyP}`;
709
524
  const lockKey = `${prefix}lock:${keyP}`;
@@ -718,72 +533,50 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
718
533
  };
719
534
  }
720
535
  async idempotencyAllow(keys) {
721
- const res = await this.runScript("IdempotencyAllow", [keys.doneKey, keys.lockKey, keys.startKey], [String(this.workerExecuteLockTimeoutMs), keys.token], IdempotencyAllow);
536
+ const res = await this.runScript("IdempotencyAllow", [keys.doneKey, keys.lockKey, keys.startKey], [String(this.idemLockTimeout), keys.token], IdempotencyAllow);
722
537
  return Number(res || 0);
723
538
  }
724
539
  async idempotencyStart(keys) {
725
- const res = await this.runScript("IdempotencyStart", [keys.lockKey, keys.startKey], [keys.token, String(this.workerExecuteLockTimeoutMs)], IdempotencyStart);
540
+ const res = await this.runScript("IdempotencyStart", [keys.lockKey, keys.startKey], [keys.token, String(this.idemLockTimeout)], IdempotencyStart);
726
541
  return Number(res || 0) === 1;
727
542
  }
728
543
  async idempotencyDone(keys) {
729
- await this.runScript("IdempotencyDone", [keys.doneKey, keys.lockKey, keys.startKey], [String(this.workerCacheTaskTimeoutMs), keys.token], IdempotencyDone);
544
+ await this.runScript("IdempotencyDone", [keys.doneKey, keys.lockKey, keys.startKey], [String(this.idemDoneTimeout), keys.token], IdempotencyDone);
730
545
  }
731
546
  async idempotencyFree(keys) {
732
547
  await this.runScript("IdempotencyFree", [keys.lockKey, keys.startKey], [keys.token], IdempotencyFree);
733
548
  }
734
- async createGroup(from = "$") {
735
- try {
736
- await this.redis.xgroup("CREATE", this.stream, this.group, from, "MKSTREAM");
737
- } catch (err) {
738
- const msg = String(err?.message || "");
739
- if (!msg.includes("BUSYGROUP")) {
740
- throw err;
741
- }
742
- }
743
- }
744
- async select() {
745
- let entries = await this.selectStuck();
746
- if (!(0, import_full_utils.isArrFilled)(entries)) {
747
- entries = await this.selectFresh();
748
- }
749
- return this.normalizeEntries(entries);
750
- }
751
- async selectStuck() {
752
- try {
753
- const res = await this.runScript("SelectStuck", [this.stream], [this.group, this.consumer(), String(this.recoveryStuckTasksTimeoutMs), String(this.workerBatchTasksCount), String(this.workerSelectionTimeoutMs)], SelectStuck);
754
- return (0, import_full_utils.isArr)(res) ? res : [];
755
- } catch (err) {
756
- if (String(err?.message || "").includes("NOGROUP")) {
757
- await this.createGroup();
758
- }
759
- }
760
- return [];
761
- }
762
- async selectFresh() {
763
- let entries = [];
764
- try {
765
- const res = await this.redis.xreadgroup(
766
- "GROUP",
767
- this.group,
768
- this.consumer(),
769
- "BLOCK",
770
- Math.max(2, this.workerLoopIntervalMs | 0),
771
- "COUNT",
772
- this.workerBatchTasksCount,
773
- "STREAMS",
774
- this.stream,
775
- ">"
776
- );
777
- entries = res?.[0]?.[1] ?? [];
778
- if (!(0, import_full_utils.isArrFilled)(entries)) {
779
- return [];
780
- }
781
- } catch (err) {
782
- if (String(err?.message || "").includes("NOGROUP")) {
783
- await this.createGroup();
549
+ async success(queueName, task) {
550
+ if (this.logStatus) {
551
+ const statusKey = `${queueName}:${task.job}:`;
552
+ await this.incr(statusKey + "ok", this.logStatusTimeout);
553
+ await this.incr(statusKey + "ready", this.logStatusTimeout);
554
+ }
555
+ await this.onSuccess(queueName, task);
556
+ }
557
+ async error(err, queueName, task) {
558
+ const dlqKey = queueName + ":dlq";
559
+ const taskP = { ...task };
560
+ if (task.attempt >= this.retryCount) {
561
+ const statusKey = `${queueName}:${task.job}:`;
562
+ await this.addTasks(dlqKey, [{ ...taskP.payload }], {
563
+ createdAt: task.createdAt,
564
+ job: task.job,
565
+ attempt: task.attempt
566
+ });
567
+ if (this.logStatus) {
568
+ await this.incr(statusKey + "err", this.logStatusTimeout);
569
+ await this.incr(statusKey + "ready", this.logStatusTimeout);
784
570
  }
571
+ } else {
572
+ await this.onRetry(err, queueName, task);
573
+ await this.addTasks(queueName, [{ ...taskP.payload }], {
574
+ createdAt: task.createdAt,
575
+ job: task.job,
576
+ attempt: task.attempt
577
+ });
785
578
  }
786
- return entries;
579
+ await this.onError(err, queueName, task);
787
580
  }
788
581
  async waitAbortable(ttl) {
789
582
  return new Promise((resolve) => {
@@ -815,8 +608,8 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
815
608
  }
816
609
  async sendHeartbeat(keys) {
817
610
  try {
818
- const r1 = await this.redis.pexpire(keys.lockKey, this.workerExecuteLockTimeoutMs);
819
- const r2 = await this.redis.pexpire(keys.startKey, this.workerExecuteLockTimeoutMs);
611
+ const r1 = await this.redis.pexpire(keys.lockKey, this.idemLockTimeout);
612
+ const r2 = await this.redis.pexpire(keys.startKey, this.idemLockTimeout);
820
613
  const ok1 = Number(r1 || 0) === 1;
821
614
  const ok2 = Number(r2 || 0) === 1;
822
615
  return ok1 || ok2;
@@ -825,10 +618,10 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
825
618
  }
826
619
  }
827
620
  heartbeat(keys) {
828
- if (this.workerExecuteLockTimeoutMs <= 0) {
621
+ if (this.idemLockTimeout <= 0) {
829
622
  return;
830
623
  }
831
- const workerHeartbeatTimeoutMs = Math.max(1e3, Math.floor(Math.max(5e3, this.workerExecuteLockTimeoutMs | 0) / 4));
624
+ const workerHeartbeatTimeoutMs = Math.max(1e3, Math.floor(Math.max(5e3, this.idemLockTimeout | 0) / 4));
832
625
  let timer;
833
626
  let alive = true;
834
627
  let hbFails = 0;
@@ -868,39 +661,190 @@ var PowerQueues = class extends import_power_redis.PowerRedis {
868
661
  stop();
869
662
  };
870
663
  }
871
- normalizeEntries(raw) {
872
- if (!Array.isArray(raw)) {
873
- return [];
664
+ async runScript(name, keys, args, defaultCode) {
665
+ if (!this.scripts[name]) {
666
+ if (!(0, import_full_utils.isStrFilled)(defaultCode)) {
667
+ throw new Error(`Undefined script "${name}". Save it before executing.`);
668
+ }
669
+ this.saveScript(name, defaultCode);
670
+ }
671
+ if (!this.scripts[name].codeReady) {
672
+ this.scripts[name].codeReady = await this.loadScript(this.scripts[name].codeBody);
673
+ }
674
+ try {
675
+ return await this.redis.evalsha(this.scripts[name].codeReady, keys.length, ...keys, ...args);
676
+ } catch (err) {
677
+ if (String(err?.message || "").includes("NOSCRIPT")) {
678
+ this.scripts[name].codeReady = await this.loadScript(this.scripts[name].codeBody);
679
+ return await this.redis.evalsha(this.scripts[name].codeReady, keys.length, ...keys, ...args);
680
+ }
681
+ throw err;
874
682
  }
875
- return Array.from(raw || []).map((e) => {
876
- const id = Buffer.isBuffer(e?.[0]) ? e[0].toString() : e?.[0];
877
- const kvRaw = e?.[1] ?? [];
878
- const kv = (0, import_full_utils.isArr)(kvRaw) ? kvRaw.map((x) => Buffer.isBuffer(x) ? x.toString() : x) : [];
879
- return [id, kv];
880
- }).filter(([id, kv]) => (0, import_full_utils.isStrFilled)(id) && (0, import_full_utils.isArr)(kv) && (kv.length & 1) === 0).map(([id, kv]) => {
881
- const { idemKey = "", job, createdAt, payload } = this.values(kv);
882
- return [id, this.payload(payload), createdAt, job, idemKey];
883
- });
884
683
  }
885
- values(value) {
886
- const result = {};
887
- for (let i = 0; i < value.length; i += 2) {
888
- result[value[i]] = value[i + 1];
684
+ async loadScripts(full = false) {
685
+ const scripts = full ? [
686
+ ["XAddBulk", XAddBulk],
687
+ ["Approve", Approve],
688
+ ["IdempotencyAllow", IdempotencyAllow],
689
+ ["IdempotencyStart", IdempotencyStart],
690
+ ["IdempotencyDone", IdempotencyDone],
691
+ ["IdempotencyFree", IdempotencyFree],
692
+ ["SelectStuck", SelectStuck]
693
+ ] : [
694
+ ["XAddBulk", XAddBulk]
695
+ ];
696
+ for (const [name, code] of scripts) {
697
+ await this.loadScript(this.saveScript(name, code));
889
698
  }
699
+ }
700
+ async loadScript(code) {
701
+ for (let i = 0; i < 3; i++) {
702
+ try {
703
+ return await this.redis.script("LOAD", code);
704
+ } catch (e) {
705
+ if (i === 2) {
706
+ throw e;
707
+ }
708
+ await new Promise((r) => setTimeout(r, 10 + Math.floor(Math.random() * 40)));
709
+ }
710
+ }
711
+ throw new Error("Load lua script failed.");
712
+ }
713
+ saveScript(name, codeBody) {
714
+ if (!(0, import_full_utils.isStrFilled)(codeBody)) {
715
+ throw new Error("Script body is empty.");
716
+ }
717
+ this.scripts[name] = { codeBody };
718
+ return codeBody;
719
+ }
720
+ async addTasks(queueName, data, opts = {}) {
721
+ if (!(0, import_full_utils.isArrFilled)(data)) {
722
+ throw new Error("Tasks is not filled.");
723
+ }
724
+ if (!(0, import_full_utils.isStrFilled)(queueName)) {
725
+ throw new Error("Queue name is required.");
726
+ }
727
+ const batches = this.buildBatches(data, opts);
728
+ const result = new Array(data.length);
729
+ const promises = [];
730
+ let cursor = 0;
731
+ for (const batch of batches) {
732
+ const start = cursor;
733
+ const end = start + batch.length;
734
+ cursor = end;
735
+ promises.push(async () => {
736
+ const payload = this.payloadBatch(batch, opts);
737
+ const partIds = await this.runScript("XAddBulk", [queueName], payload, XAddBulk);
738
+ for (let k = 0; k < partIds.length; k++) {
739
+ result[start + k] = partIds[k];
740
+ }
741
+ });
742
+ }
743
+ const runners = Array.from({ length: promises.length }, async () => {
744
+ while (promises.length) {
745
+ const promise = promises.shift();
746
+ if (promise) {
747
+ await promise();
748
+ }
749
+ }
750
+ });
751
+ if (opts.status) {
752
+ await this.redis.set(`${queueName}:${opts.job}:total`, data.length);
753
+ await this.redis.pexpire(`${queueName}:${opts.job}:total`, opts.statusTimeoutMs || 3e5);
754
+ }
755
+ await Promise.all(runners);
890
756
  return result;
891
757
  }
892
- payload(data) {
893
- try {
894
- return (0, import_full_utils.jsonDecode)(data);
895
- } catch (err) {
758
+ buildBatches(tasks, opts = {}) {
759
+ const batches = [];
760
+ let batch = [], realKeysLength = 0;
761
+ for (let task of tasks) {
762
+ const createdAt = opts?.createdAt || Date.now();
763
+ let entry = {};
764
+ if ((0, import_full_utils.isObj)(entry)) {
765
+ entry = {
766
+ payload: JSON.stringify(task),
767
+ attempt: Number(opts.attempt || 0),
768
+ job: opts.job ?? (0, import_uuid.v4)(),
769
+ idemKey: (0, import_uuid.v4)(),
770
+ createdAt
771
+ };
772
+ }
773
+ const reqKeysLength = this.keysLength(entry);
774
+ if (batch.length && (batch.length >= this.buildBatchCount || realKeysLength + reqKeysLength > this.buildBatchMaxCount)) {
775
+ batches.push(batch);
776
+ batch = [];
777
+ realKeysLength = 0;
778
+ }
779
+ batch.push(entry);
780
+ realKeysLength += reqKeysLength;
896
781
  }
897
- return data;
782
+ if (batch.length) {
783
+ batches.push(batch);
784
+ }
785
+ return batches;
898
786
  }
899
- signal() {
900
- return this.abort.signal;
787
+ keysLength(task) {
788
+ if ("payload" in task && (0, import_full_utils.isObj)(task.payload)) {
789
+ return 2 + Object.keys(task.payload).length * 2;
790
+ }
791
+ return 2 + Object.keys(task).length * 2;
901
792
  }
902
- consumer() {
903
- return `${String(this.consumerHost || "host")}:${process.pid}`;
793
+ payloadBatch(data, opts) {
794
+ const maxlen = Math.max(0, Math.floor(opts?.maxlen ?? 0));
795
+ const approx = opts?.exact ? 0 : opts?.approx !== false ? 1 : 0;
796
+ const exact = opts?.exact ? 1 : 0;
797
+ const nomkstream = opts?.nomkstream ? 1 : 0;
798
+ const trimLimit = Math.max(0, Math.floor(opts?.trimLimit ?? 0));
799
+ const minidWindowMs = Math.max(0, Math.floor(opts?.minidWindowMs ?? 0));
800
+ const minidExact = opts?.minidExact ? 1 : 0;
801
+ const argv = [
802
+ String(maxlen),
803
+ String(approx),
804
+ String(data.length),
805
+ String(exact),
806
+ String(nomkstream),
807
+ String(trimLimit),
808
+ String(minidWindowMs),
809
+ String(minidExact)
810
+ ];
811
+ for (const item of data) {
812
+ const entry = item;
813
+ const id = entry.id ?? "*";
814
+ let flat = [];
815
+ if ("payload" in entry && (0, import_full_utils.isObjFilled)(entry)) {
816
+ for (const [k, v] of Object.entries(entry)) {
817
+ flat.push(k, v);
818
+ }
819
+ } else {
820
+ throw new Error('Task must have "payload" or "flat".');
821
+ }
822
+ const pairs = flat.length / 2;
823
+ if ((0, import_full_utils.isNumNZ)(pairs)) {
824
+ throw new Error('Task "flat" must contain at least one field/value pair.');
825
+ }
826
+ argv.push(String(id));
827
+ argv.push(String(pairs));
828
+ for (const token of flat) {
829
+ argv.push(!(0, import_full_utils.isExists)(token) ? "" : (0, import_full_utils.isStrFilled)(token) ? token : String(token));
830
+ }
831
+ }
832
+ return argv;
833
+ }
834
+ async beforeExecute(queueName, tasks) {
835
+ return tasks;
836
+ }
837
+ async onExecute(queueName, task) {
838
+ }
839
+ async onBatchSuccess(queueName, tasks) {
840
+ }
841
+ async onSuccess(queueName, task) {
842
+ }
843
+ async onError(err, queueName, task) {
844
+ }
845
+ async onBatchError(err, queueName, tasks) {
846
+ }
847
+ async onRetry(err, queueName, task) {
904
848
  }
905
849
  };
906
850
  // Annotate the CommonJS export names for ESM import in node: