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