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