power-queues 2.0.4 → 2.0.5

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