power-queues 2.0.4 → 2.0.6

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