power-queues 1.0.8 → 2.0.1

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,10 +1,765 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Queue = exports.QueueProcessor = exports.Processor = void 0;
4
- const Processor_1 = require("./src/Processor");
5
- Object.defineProperty(exports, "Processor", { enumerable: true, get: function () { return Processor_1.Processor; } });
6
- const QueueProcessor_1 = require("./src/QueueProcessor");
7
- Object.defineProperty(exports, "QueueProcessor", { enumerable: true, get: function () { return QueueProcessor_1.QueueProcessor; } });
8
- const Queue_1 = require("./src/Queue");
9
- Object.defineProperty(exports, "Queue", { enumerable: true, get: function () { return Queue_1.Queue; } });
10
- //# sourceMappingURL=index.js.map
1
+ // src/PowerQueue.ts
2
+ import { v4 as uuid } from "uuid";
3
+ import {
4
+ isStrFilled,
5
+ isStr,
6
+ isArrFilled,
7
+ isArr,
8
+ isObjFilled,
9
+ isNumPZ,
10
+ isNumP,
11
+ isFunc,
12
+ wait
13
+ } from "full-utils";
14
+ import { PowerRedis } from "power-redis";
15
+ var PowerQueue = class extends PowerRedis {
16
+ constructor() {
17
+ super(...arguments);
18
+ this.iterationTimeout = 1e3;
19
+ this.portionLength = 1e3;
20
+ this.expireStatusSec = 300;
21
+ this.maxAttempts = 1;
22
+ this.concurrency = 32;
23
+ this.visibilityTimeoutSec = 60;
24
+ this.retryBaseSec = 1;
25
+ this.retryMaxSec = 3600;
26
+ this.runners = /* @__PURE__ */ new Map();
27
+ this.processingRaw = /* @__PURE__ */ new Map();
28
+ this.heartbeatTimers = /* @__PURE__ */ new Map();
29
+ }
30
+ nowSec() {
31
+ return Math.floor(Date.now() / 1e3);
32
+ }
33
+ readyKey(queueName) {
34
+ return this.toKeyString(queueName);
35
+ }
36
+ processingKey(queueName) {
37
+ return this.toKeyString(queueName, "processing");
38
+ }
39
+ processingVtKey(queueName) {
40
+ return this.toKeyString(queueName, "processing", "vt");
41
+ }
42
+ delayedKey(queueName) {
43
+ return this.toKeyString(queueName, "delayed");
44
+ }
45
+ toKeyString(...parts) {
46
+ return super.toKeyString("queue", ...parts);
47
+ }
48
+ getReserveScriptLMOVE() {
49
+ return `
50
+ local source = KEYS[1]
51
+ local processing = KEYS[2]
52
+ local vtkey = KEYS[3]
53
+ local limit = tonumber(ARGV[1])
54
+ local deadline = tonumber(ARGV[2])
55
+ local moved = {}
56
+ for i = 1, limit do
57
+ local v = redis.call('LMOVE', source, processing, 'LEFT', 'RIGHT')
58
+ if not v then break end
59
+ table.insert(moved, v)
60
+ end
61
+ if #moved > 0 then
62
+ for i = 1, #moved do
63
+ redis.call('ZADD', vtkey, deadline, moved[i])
64
+ end
65
+ end
66
+ return moved
67
+ `;
68
+ }
69
+ getReserveScriptRPOPLPUSH() {
70
+ return `
71
+ local source = KEYS[1]
72
+ local processing = KEYS[2]
73
+ local vtkey = KEYS[3]
74
+ local limit = tonumber(ARGV[1])
75
+ local deadline = tonumber(ARGV[2])
76
+ local moved = {}
77
+ for i = 1, limit do
78
+ local v = redis.call('RPOPLPUSH', source, processing)
79
+ if not v then break end
80
+ table.insert(moved, v)
81
+ end
82
+ if #moved > 0 then
83
+ for i = 1, #moved do
84
+ redis.call('ZADD', vtkey, deadline, moved[i])
85
+ end
86
+ end
87
+ return moved
88
+ `;
89
+ }
90
+ getRequeueScript() {
91
+ return `
92
+ -- KEYS: 1=processing, 2=processingVt, 3=ready
93
+ -- ARGV: 1=now, 2=limit
94
+ local processing = KEYS[1]
95
+ local vt = KEYS[2]
96
+ local ready = KEYS[3]
97
+ local now = tonumber(ARGV[1])
98
+ local limit = tonumber(ARGV[2])
99
+
100
+ local members = redis.call('ZRANGEBYSCORE', vt, 0, now, 'LIMIT', 0, limit)
101
+ for i=1,#members do
102
+ local m = members[i]
103
+ redis.call('ZREM', vt, m)
104
+ redis.call('LREM', processing, 1, m)
105
+ redis.call('RPUSH', ready, m)
106
+ end
107
+ return #members
108
+ `;
109
+ }
110
+ getPromoteScript() {
111
+ return `
112
+ -- KEYS: 1=delayed, 2=ready
113
+ -- ARGV: 1=now, 2=limit
114
+ local delayed = KEYS[1]
115
+ local ready = KEYS[2]
116
+ local now = tonumber(ARGV[1])
117
+ local limit = tonumber(ARGV[2])
118
+
119
+ local due = redis.call('ZRANGEBYSCORE', delayed, 0, now, 'LIMIT', 0, limit)
120
+ for i=1,#due do
121
+ local m = due[i]
122
+ redis.call('ZREM', delayed, m)
123
+ redis.call('RPUSH', ready, m)
124
+ end
125
+ return #due
126
+ `;
127
+ }
128
+ async ensureReserveScript(force = false) {
129
+ if (!force && (this.reserveSha || this.reserveShaRpoplpush || !isFunc(this.redis?.script))) {
130
+ return;
131
+ }
132
+ this.reserveSha = void 0;
133
+ this.reserveShaRpoplpush = void 0;
134
+ try {
135
+ this.reserveSha = await this.redis?.script("LOAD", this.getReserveScriptLMOVE());
136
+ } catch {
137
+ this.reserveShaRpoplpush = await this.redis?.script("LOAD", this.getReserveScriptRPOPLPUSH());
138
+ }
139
+ }
140
+ async ensureRequeueScript(force = false) {
141
+ if (!force && this.requeueSha) {
142
+ return;
143
+ }
144
+ const scriptFn = this.redis?.script;
145
+ if (!scriptFn) {
146
+ return;
147
+ }
148
+ this.requeueSha = await scriptFn("LOAD", this.getRequeueScript());
149
+ }
150
+ async ensurePromoteScript(force = false) {
151
+ if (!force && this.promoteSha) {
152
+ return;
153
+ }
154
+ const scriptFn = this.redis?.script;
155
+ if (!scriptFn) {
156
+ return;
157
+ }
158
+ this.promoteSha = await scriptFn("LOAD", this.getPromoteScript());
159
+ }
160
+ async moveOneToProcessing(source, processing) {
161
+ const cli = this.redis;
162
+ try {
163
+ if (isFunc(cli.lmove)) {
164
+ const v = await cli.lmove(source, processing, "LEFT", "RIGHT");
165
+ return isStr(v) ? v : null;
166
+ }
167
+ } catch {
168
+ }
169
+ try {
170
+ if (isFunc(cli.rpoplpush)) {
171
+ const v = await cli.rpoplpush(source, processing);
172
+ return isStr(v) ? v : null;
173
+ }
174
+ } catch {
175
+ }
176
+ return null;
177
+ }
178
+ async evalshaWithReload(shaGetter, ensure, numKeys, keysAndArgs) {
179
+ await ensure(false);
180
+ const sha = shaGetter();
181
+ const evalshaFn = this.redis?.evalsha;
182
+ if (!sha || !evalshaFn) {
183
+ throw new Error("EVALSHA not available or SHA missing");
184
+ }
185
+ try {
186
+ return await evalshaFn(sha, numKeys, ...keysAndArgs.map(String));
187
+ } catch (err) {
188
+ const msg = err?.message;
189
+ if (typeof msg === "string" && msg.includes("NOSCRIPT")) {
190
+ await ensure(true);
191
+ const sha2 = shaGetter();
192
+ if (!sha2) {
193
+ throw new Error("EVALSHA NOSCRIPT and reload failed (no SHA)");
194
+ }
195
+ return await evalshaFn(sha2, numKeys, ...keysAndArgs.map(String));
196
+ }
197
+ throw err;
198
+ }
199
+ }
200
+ async zaddCompatXXCH(key, score, member) {
201
+ const zadd = this.redis?.zadd;
202
+ try {
203
+ if (zadd) {
204
+ await zadd.call(this.redis, key, "XX", "CH", score, member);
205
+ return;
206
+ }
207
+ } catch {
208
+ }
209
+ try {
210
+ if (zadd) {
211
+ await zadd.call(this.redis, key, "CH", "XX", score, member);
212
+ return;
213
+ }
214
+ } catch {
215
+ }
216
+ try {
217
+ await this.redis.zadd(key, score, member);
218
+ } catch {
219
+ }
220
+ }
221
+ startHeartbeat(task) {
222
+ const raw = this.processingRaw.get(task.id);
223
+ if (!raw) {
224
+ return;
225
+ }
226
+ const vtKey = this.processingVtKey(task.queueName);
227
+ const periodMs = Math.max(1e3, Math.floor(this.visibilityTimeoutSec * 1e3 * 0.4));
228
+ const t = setInterval(() => {
229
+ this.extendVisibility(vtKey, raw, this.visibilityTimeoutSec).catch(() => {
230
+ });
231
+ }, periodMs);
232
+ t.unref?.();
233
+ this.heartbeatTimers.set(task.id, t);
234
+ }
235
+ stopHeartbeat(task) {
236
+ const t = this.heartbeatTimers.get(task.id);
237
+ if (t) {
238
+ clearInterval(t);
239
+ }
240
+ this.heartbeatTimers.delete(task.id);
241
+ }
242
+ async reserveMany(source, processing, processingVt, limit = 100, visibilitySec = 60) {
243
+ if (!this.checkConnection()) {
244
+ throw new Error("Redis connection error.");
245
+ }
246
+ if (!isStrFilled(source) || !isStrFilled(processing) || !isStrFilled(processingVt)) {
247
+ throw new Error("Key format error.");
248
+ }
249
+ if (!isNumP(limit) || !isNumP(visibilitySec)) {
250
+ throw new Error("Limit/visibility format error.");
251
+ }
252
+ await this.ensureReserveScript();
253
+ const deadline = this.nowSec() + visibilitySec;
254
+ const tryEval = async () => {
255
+ if (isFunc(this.redis?.evalsha)) {
256
+ if (this.reserveSha) {
257
+ return await this.redis?.evalsha(this.reserveSha, 3, source, processing, processingVt, String(limit), String(deadline));
258
+ }
259
+ if (this.reserveShaRpoplpush) {
260
+ return await this.redis?.evalsha(this.reserveShaRpoplpush, 3, source, processing, processingVt, String(limit), String(deadline));
261
+ }
262
+ }
263
+ return null;
264
+ };
265
+ try {
266
+ const res = await tryEval();
267
+ if (isArr(res)) {
268
+ return Array.from(res).map(String);
269
+ }
270
+ } catch (err) {
271
+ if (isStr(err?.message) && String(err?.message ?? "").includes("NOSCRIPT")) {
272
+ await this.ensureReserveScript(true);
273
+ try {
274
+ const res2 = await tryEval();
275
+ if (isArr(res2)) {
276
+ return Array.from(res2).map(String);
277
+ }
278
+ } catch {
279
+ }
280
+ }
281
+ }
282
+ const moved = [];
283
+ for (let i = 0; i < limit; i++) {
284
+ const v = await this.moveOneToProcessing(source, processing);
285
+ if (!v) {
286
+ break;
287
+ }
288
+ moved.push(v);
289
+ }
290
+ if (moved.length) {
291
+ const tx = this.redis?.multi();
292
+ for (const v of moved) {
293
+ tx.zadd(processingVt, deadline, v);
294
+ }
295
+ await tx.exec();
296
+ }
297
+ return moved;
298
+ }
299
+ async ackProcessing(processing, processingVt, raw) {
300
+ if (!this.checkConnection()) {
301
+ throw new Error("Redis connection error.");
302
+ }
303
+ const tx = this.redis?.multi();
304
+ tx.lrem(processing, 1, raw);
305
+ tx.zrem(processingVt, raw);
306
+ await tx.exec();
307
+ return;
308
+ }
309
+ async requeueExpired(processing, processingVt, ready, nowTs, chunk = 1e3) {
310
+ if (!this.checkConnection()) {
311
+ throw new Error("Redis connection error.");
312
+ }
313
+ const now = isNumP(nowTs) ? nowTs : this.nowSec();
314
+ try {
315
+ const moved = await this.evalshaWithReload(
316
+ () => this.requeueSha,
317
+ (force) => this.ensureRequeueScript(!!force),
318
+ 3,
319
+ [processing, processingVt, ready, String(now), String(chunk)]
320
+ );
321
+ return isNumP(moved) ? moved : 0;
322
+ } catch {
323
+ const expired = await this.redis?.zrangebyscore(processingVt, 0, now, "LIMIT", 0, chunk);
324
+ if (!isArrFilled(expired)) {
325
+ return 0;
326
+ }
327
+ const tx = this.redis?.multi();
328
+ for (const raw of expired) {
329
+ tx.lrem(processing, 1, raw);
330
+ tx.zrem(processingVt, raw);
331
+ tx.rpush(ready, raw);
332
+ }
333
+ await tx.exec();
334
+ return expired.length;
335
+ }
336
+ }
337
+ async promoteDelayed(delayed, ready, nowTs, chunk = 1e3) {
338
+ if (!this.checkConnection()) {
339
+ throw new Error("Redis connection error.");
340
+ }
341
+ const now = isNumP(nowTs) ? nowTs : this.nowSec();
342
+ try {
343
+ const promoted = await this.evalshaWithReload(
344
+ () => this.promoteSha,
345
+ (force) => this.ensurePromoteScript(!!force),
346
+ 2,
347
+ [delayed, ready, String(now), String(chunk)]
348
+ );
349
+ return isNumP(promoted) ? promoted : 0;
350
+ } catch {
351
+ const due = await this.redis?.zrangebyscore(delayed, 0, now, "LIMIT", 0, chunk);
352
+ if (!isArrFilled(due)) {
353
+ return 0;
354
+ }
355
+ const tx = this.redis?.multi();
356
+ for (const raw of due) {
357
+ tx.zrem(delayed, raw);
358
+ tx.rpush(ready, raw);
359
+ }
360
+ await tx.exec();
361
+ return due.length;
362
+ }
363
+ }
364
+ async enqueue(ready, delayed, payload, delaySec) {
365
+ if (!this.checkConnection()) {
366
+ throw new Error("Redis connection error.");
367
+ }
368
+ const raw = this.toPayload(payload);
369
+ if (isNumP(delaySec) && delaySec > 0) {
370
+ const score = this.nowSec() + delaySec;
371
+ return await this.redis?.zadd(delayed, score, raw);
372
+ }
373
+ return await this.redis?.rpush(ready, raw);
374
+ }
375
+ async extendVisibility(processingVt, raw, visibilitySec) {
376
+ const deadline = this.nowSec() + Math.max(1, visibilitySec);
377
+ await this.zaddCompatXXCH(processingVt, deadline, raw);
378
+ }
379
+ run(queueName) {
380
+ if (!isStrFilled(queueName)) {
381
+ throw new Error("Queue name is not valid.");
382
+ }
383
+ const r = this.runners.get(queueName) ?? { running: false };
384
+ if (r.running) {
385
+ throw new Error(`Queue "${queueName}" already started.`);
386
+ }
387
+ r.running = true;
388
+ this.runners.set(queueName, r);
389
+ this.loop(queueName, r).catch(() => {
390
+ r.running = false;
391
+ });
392
+ }
393
+ stop(queueName) {
394
+ const r = this.runners.get(queueName);
395
+ if (r) {
396
+ r.running = false;
397
+ this.runners.delete(queueName);
398
+ }
399
+ }
400
+ buildTask(data) {
401
+ if (!isObjFilled(data)) {
402
+ throw new Error("Data property is not valid.");
403
+ }
404
+ if (!isStrFilled(data.queueName)) {
405
+ throw new Error("Queue name is not valid.");
406
+ }
407
+ return {
408
+ queueName: data.queueName,
409
+ iterationId: isStrFilled(data.iterationId) ? data.iterationId : uuid(),
410
+ iterationLength: Number(data.iterationLength || 0),
411
+ id: isStrFilled(data.id) ? data.id : uuid(),
412
+ maxAttempts: isNumPZ(data.maxAttempts) ? data.maxAttempts : this.maxAttempts,
413
+ currentAttempt: isNumPZ(data.currentAttempt) ? data.currentAttempt : 0,
414
+ chain: isObjFilled(data.chain) && isArrFilled(data.chain.queues) && isNumPZ(data.chain.index) ? data.chain : {
415
+ queues: [],
416
+ index: 0
417
+ },
418
+ payload: isObjFilled(data.payload) ? data.payload : {},
419
+ progress: {
420
+ createdAt: Date.now(),
421
+ successAt: 0,
422
+ errorAt: 0,
423
+ failAt: 0,
424
+ fatalAt: 0,
425
+ retries: [],
426
+ chain: [],
427
+ ...isObjFilled(data.progress) ? data.progress : {}
428
+ },
429
+ result: isObjFilled(data.result) ? data.result : {}
430
+ };
431
+ }
432
+ async addTask(data, delaySec) {
433
+ const ready = this.readyKey(String(data.queueName));
434
+ const delayed = this.delayedKey(String(data.queueName));
435
+ return await this.enqueue(ready, delayed, this.buildTask(data), isNumP(delaySec) ? delaySec : 0);
436
+ }
437
+ async addTasks(data) {
438
+ if (!this.checkConnection()) {
439
+ throw new Error("Redis connection error.");
440
+ }
441
+ if (!isObjFilled(data) || !isStrFilled(data.queueName)) {
442
+ throw new Error("Queue name is not valid.");
443
+ }
444
+ if (!isArrFilled(data.payloads)) {
445
+ return 0;
446
+ }
447
+ const queueName = String(data.queueName);
448
+ const ready = this.readyKey(queueName);
449
+ const delayed = this.delayedKey(queueName);
450
+ const now = this.nowSec();
451
+ const uniformDelay = isNumP(data.delaySec) ? Math.max(0, Number(data.delaySec)) : void 0;
452
+ const perItemDelays = isArr(data.delaySec) ? data.delaySec.map((v) => Math.max(0, Number(v || 0))) : void 0;
453
+ const batchSize = Math.max(1, Math.min(this.portionLength, 1e3));
454
+ let idx = 0, total = 0;
455
+ while (idx < data.payloads.length) {
456
+ const end = Math.min(idx + batchSize, data.payloads.length);
457
+ const tx = this.redis?.multi();
458
+ for (let i = idx; i < end; i++) {
459
+ const item = data.payloads[i];
460
+ let partial;
461
+ if (isObjFilled(item) && Object.prototype.hasOwnProperty.call(item, "payload")) {
462
+ partial = { ...item, queueName };
463
+ } else {
464
+ partial = { queueName, payload: item };
465
+ }
466
+ const task = this.buildTask(partial);
467
+ const raw = this.toPayload(task);
468
+ let d = 0;
469
+ if (isNumP(uniformDelay)) {
470
+ d = uniformDelay;
471
+ } else if (isArr(perItemDelays)) {
472
+ d = Number(perItemDelays[i] || 0);
473
+ }
474
+ if (d > 0) {
475
+ tx.zadd(delayed, now + d, raw);
476
+ } else {
477
+ tx.rpush(ready, raw);
478
+ }
479
+ total++;
480
+ }
481
+ await tx.exec();
482
+ idx = end;
483
+ }
484
+ return total;
485
+ }
486
+ async iteration(tasks) {
487
+ const tasksProcessed = await this.beforeIterationExecution(tasks);
488
+ const limit = Math.max(1, Number(this.concurrency) || 1);
489
+ let i = 0;
490
+ while (i < tasksProcessed.length) {
491
+ const slice = tasksProcessed.slice(i, i + limit);
492
+ await Promise.all(slice.map((task) => this.logic(task)));
493
+ i += limit;
494
+ }
495
+ await this.afterIterationExecution(tasksProcessed, tasksProcessed.map((t) => t.result ?? {}));
496
+ }
497
+ async beforeIterationExecution(data) {
498
+ return data;
499
+ }
500
+ async afterIterationExecution(data, results) {
501
+ }
502
+ async beforeExecution(task) {
503
+ return task;
504
+ }
505
+ async afterExecution(task, result) {
506
+ return result;
507
+ }
508
+ async execute(task) {
509
+ return {};
510
+ }
511
+ async onRetry(task) {
512
+ }
513
+ async onError(err, task) {
514
+ }
515
+ async onFail(err, task) {
516
+ }
517
+ async onFatal(err, task) {
518
+ }
519
+ async onSuccess(task, result) {
520
+ }
521
+ async onChainSuccess(task, result) {
522
+ }
523
+ async onIterationError(err, queueName) {
524
+ }
525
+ async logic(task) {
526
+ let data = task;
527
+ try {
528
+ data = await this.beforeExecution(task);
529
+ const before = data?.result ?? {};
530
+ const after = await this.execute(data);
531
+ data.result = {
532
+ ...isObjFilled(before) ? before : {},
533
+ ...isObjFilled(after) ? after : {}
534
+ };
535
+ await this.success(data, data.result);
536
+ return await this.afterExecution(data, data.result);
537
+ } catch (err) {
538
+ try {
539
+ await this.retry(data);
540
+ } catch (err2) {
541
+ await this.error(err2, data);
542
+ }
543
+ } finally {
544
+ try {
545
+ this.stopHeartbeat(data);
546
+ await this.ack(data).catch(() => {
547
+ });
548
+ } catch {
549
+ }
550
+ }
551
+ return {};
552
+ }
553
+ jitteredBackoffSec(attempt) {
554
+ const base = Math.max(1, Number(this.retryBaseSec) || 1);
555
+ const maxD = Math.max(base, Number(this.retryMaxSec) || 3600);
556
+ const pow = Math.min(maxD, base * Math.pow(2, Math.max(0, attempt - 1)));
557
+ const jitter = Math.floor(Math.random() * base);
558
+ return Math.min(maxD, pow + jitter);
559
+ }
560
+ async retry(task) {
561
+ if (!isObjFilled(task) || !isStrFilled(task.iterationId) || !isStrFilled(task.id) || !isStrFilled(task.queueName) || !isNumPZ(task.currentAttempt) || !isNumPZ(task.maxAttempts)) {
562
+ await this.error(new Error("Task format error."), task);
563
+ return;
564
+ }
565
+ const maxAttempts = task.maxAttempts ?? this.maxAttempts;
566
+ try {
567
+ if (task.currentAttempt < maxAttempts - 1) {
568
+ const taskProcessed = { ...task, currentAttempt: task.currentAttempt + 1 };
569
+ const delaySec = this.jitteredBackoffSec(taskProcessed.currentAttempt);
570
+ await this.addTask(taskProcessed, delaySec);
571
+ await this.onRetry(taskProcessed);
572
+ return;
573
+ }
574
+ } catch (err) {
575
+ await this.fail(err, task);
576
+ return;
577
+ }
578
+ await this.fail(new Error("The attempt limit has been reached."), task);
579
+ }
580
+ async iterationError(err, queueName, data) {
581
+ try {
582
+ await this.onIterationError(err, queueName);
583
+ } catch (err2) {
584
+ }
585
+ for (const t of data || []) {
586
+ if (isStrFilled(t.id)) {
587
+ this.processingRaw.delete(t.id);
588
+ }
589
+ }
590
+ }
591
+ async error(err, task) {
592
+ try {
593
+ await this.addTask({
594
+ ...task,
595
+ queueName: [task.queueName, task.iterationId, "error", "list"].join(":"),
596
+ currentAttempt: 0,
597
+ payload: {
598
+ ...task.payload,
599
+ errorMessage: String(err?.message ?? "")
600
+ }
601
+ });
602
+ await this.onError(err, task);
603
+ } catch (err2) {
604
+ try {
605
+ await this.onFatal(err2, task);
606
+ } catch {
607
+ }
608
+ }
609
+ try {
610
+ await this.status(task, "error");
611
+ } catch {
612
+ }
613
+ }
614
+ async fail(err, task) {
615
+ try {
616
+ await this.addTask({
617
+ ...task,
618
+ queueName: [task.queueName, task.iterationId, "fail", "list"].join(":"),
619
+ currentAttempt: 0,
620
+ payload: {
621
+ ...task.payload,
622
+ errorMessage: String(err?.message ?? "")
623
+ }
624
+ });
625
+ await this.onFail(err, task);
626
+ } catch (err2) {
627
+ try {
628
+ await this.onFatal(err2, task);
629
+ } catch {
630
+ }
631
+ }
632
+ try {
633
+ await this.status(task, "fail");
634
+ } catch {
635
+ }
636
+ }
637
+ async success(task, result) {
638
+ const taskProcessed = {
639
+ ...task,
640
+ progress: {
641
+ ...task.progress,
642
+ successAt: Date.now()
643
+ }
644
+ };
645
+ try {
646
+ if (isObjFilled(taskProcessed.chain) && isArrFilled(taskProcessed.chain.queues) && isNumPZ(taskProcessed.chain.index)) {
647
+ const currentIndex = taskProcessed.chain.index;
648
+ const newIndex = currentIndex + 1;
649
+ taskProcessed.progress.chain.push(Date.now());
650
+ if (currentIndex === taskProcessed.chain.queues.length - 1) {
651
+ await this.status(taskProcessed, "success");
652
+ await this.onChainSuccess(taskProcessed, result);
653
+ } else if (newIndex <= taskProcessed.chain.queues.length - 1) {
654
+ const newQueueName = taskProcessed.chain.queues[newIndex];
655
+ if (isStrFilled(newQueueName)) {
656
+ await this.addTask({
657
+ ...taskProcessed,
658
+ queueName: newQueueName,
659
+ currentAttempt: 0,
660
+ chain: {
661
+ ...taskProcessed.chain,
662
+ index: newIndex
663
+ },
664
+ result
665
+ });
666
+ } else {
667
+ await this.fail(new Error("Next queue format error."), taskProcessed);
668
+ }
669
+ }
670
+ } else {
671
+ await this.status(taskProcessed, "success");
672
+ }
673
+ await this.onSuccess(taskProcessed, result);
674
+ } catch (err) {
675
+ try {
676
+ await this.status(taskProcessed, "fatal");
677
+ } catch {
678
+ }
679
+ try {
680
+ await this.onFatal(err, taskProcessed);
681
+ } catch {
682
+ }
683
+ }
684
+ }
685
+ async status(task, category = "success") {
686
+ if (!this.checkConnection()) {
687
+ throw new Error("Redis connection error.");
688
+ }
689
+ const processedKey = this.toKeyString(task.queueName, task.iterationId, "processed");
690
+ const categoryKey = this.toKeyString(task.queueName, task.iterationId, category);
691
+ await this.redis?.incr(processedKey);
692
+ await this.redis?.incr(categoryKey);
693
+ await this.redis?.expire(processedKey, this.expireStatusSec);
694
+ await this.redis?.expire(categoryKey, this.expireStatusSec);
695
+ }
696
+ async loop(queueName, runner) {
697
+ if (!isStrFilled(queueName)) {
698
+ throw new Error(`Queue name is not valid: "${queueName}"; Type: "${typeof queueName}".`);
699
+ }
700
+ const ready = this.readyKey(queueName);
701
+ const processing = this.processingKey(queueName);
702
+ const processingVt = this.processingVtKey(queueName);
703
+ const delayed = this.delayedKey(queueName);
704
+ while (runner.running) {
705
+ if (!this.checkConnection()) {
706
+ await wait(this.iterationTimeout);
707
+ continue;
708
+ }
709
+ try {
710
+ await this.promoteDelayed(delayed, ready);
711
+ } catch {
712
+ }
713
+ try {
714
+ await this.requeueExpired(processing, processingVt, ready);
715
+ } catch {
716
+ }
717
+ let data = [];
718
+ try {
719
+ data = await this.data(queueName);
720
+ if (!isArrFilled(data)) {
721
+ await wait(this.iterationTimeout);
722
+ continue;
723
+ }
724
+ await this.iteration(data);
725
+ } catch (err) {
726
+ await this.iterationError(err, queueName, data);
727
+ await wait(this.iterationTimeout);
728
+ }
729
+ }
730
+ }
731
+ async data(queueName) {
732
+ const ready = this.readyKey(queueName);
733
+ const processing = this.processingKey(queueName);
734
+ const processingVt = this.processingVtKey(queueName);
735
+ const raws = await this.reserveMany(ready, processing, processingVt, this.portionLength, this.visibilityTimeoutSec);
736
+ if (!isArrFilled(raws)) {
737
+ return [];
738
+ }
739
+ const tasks = [];
740
+ for (const raw of raws) {
741
+ const obj = this.fromPayload(raw);
742
+ if (isObjFilled(obj) && isStrFilled(obj.iterationId) && isStrFilled(obj.id) && isStrFilled(obj.queueName) && isNumPZ(obj.maxAttempts) && isNumPZ(obj.currentAttempt)) {
743
+ const t = obj;
744
+ this.processingRaw.set(t.id, raw);
745
+ this.startHeartbeat(t);
746
+ tasks.push(t);
747
+ }
748
+ }
749
+ return tasks;
750
+ }
751
+ async ack(task) {
752
+ try {
753
+ const raw = this.processingRaw.get(task.id);
754
+ if (!isStrFilled(raw)) {
755
+ return;
756
+ }
757
+ this.processingRaw.delete(task.id);
758
+ await this.ackProcessing(this.processingKey(task.queueName), this.processingVtKey(task.queueName), raw);
759
+ } catch {
760
+ }
761
+ }
762
+ };
763
+ export {
764
+ PowerQueue
765
+ };