mail-time 5.1.0 → 5.2.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/README.md +46 -15
- package/adapters/mongo.d.ts +1 -0
- package/adapters/mongo.js +35 -17
- package/adapters/postgres.d.ts +1 -0
- package/adapters/postgres.js +48 -24
- package/adapters/redis.d.ts +6 -2
- package/adapters/redis.js +159 -72
- package/index.cjs +893 -163
- package/index.d.cts +137 -15
- package/index.d.ts +137 -15
- package/index.js +333 -35
- package/package.json +11 -3
- package/presets.js +15 -15
- package/recipient-policy-lease.js +90 -0
- package/recipient-policy.js +166 -0
- package/recipient-rejections.js +62 -0
- package/scripts/migrate-redis-queue-to-cluster.mjs +5 -1
package/adapters/redis.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
debug,
|
|
3
3
|
logError,
|
|
4
|
+
hasOwnProp,
|
|
4
5
|
isSendClaimUpdate,
|
|
5
6
|
isSendLeaseGuardedUpdate,
|
|
6
7
|
isAppendMailOptionUpdate,
|
|
@@ -15,9 +16,12 @@ import { createHash } from 'crypto';
|
|
|
15
16
|
* @property {(key: string) => Promise<string|null>} get
|
|
16
17
|
* @property {(key: string, value: string, options?: object) => Promise<unknown>} set
|
|
17
18
|
* @property {(key: string|string[]) => Promise<number>} del
|
|
18
|
-
* @property {() => Promise<string>} ping
|
|
19
|
+
* @property {() => Promise<string>} [ping]
|
|
20
|
+
* @property {() => unknown} [getRandomNode]
|
|
21
|
+
* @property {(...args: any[]) => any} [nodeClient]
|
|
22
|
+
* @property {(firstKey: string, isReadonly: boolean, args: string[]) => Promise<unknown>} [sendCommand]
|
|
19
23
|
* @property {(options: object) => AsyncIterable<string|string[]>} [scanIterator]
|
|
20
|
-
* @property {(key: string, field: string) => Promise<string|null>} [hGet]
|
|
24
|
+
* @property {(key: string, field: string) => Promise<string|null|undefined>} [hGet]
|
|
21
25
|
* @property {(script: string, options: { keys: string[], arguments: string[] }) => Promise<unknown>} [eval]
|
|
22
26
|
* @property {(sha: string, options: { keys: string[], arguments: string[] }) => Promise<unknown>} [evalSha]
|
|
23
27
|
* @property {(script: string) => Promise<string>} [scriptLoad]
|
|
@@ -37,6 +41,7 @@ const KEY_TYPES = new Set(['letter', 'sendat', 'concatletter']);
|
|
|
37
41
|
const DEFAULT_PREFIX = 'default';
|
|
38
42
|
const VALID_PREFIX = /^[A-Za-z0-9_\-:.]+$/;
|
|
39
43
|
const TAGGED_ITERATE_LIMIT = 100;
|
|
44
|
+
const clientTransactions = new WeakMap();
|
|
40
45
|
|
|
41
46
|
const PUSH_TAGGED_TASK_SCRIPT = `
|
|
42
47
|
redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
|
|
@@ -69,17 +74,19 @@ const ITERATE_TAGGED_TASKS_SCRIPT = `
|
|
|
69
74
|
redis.call('ZREM', KEYS[2], uuid)
|
|
70
75
|
else
|
|
71
76
|
local task = cjson.decode(payload)
|
|
72
|
-
if task.isSent or task.isFailed or task.isCancelled or
|
|
77
|
+
if task.isSent or task.isFailed or task.isCancelled or task.isSettled then
|
|
78
|
+
redis.call('ZREM', KEYS[2], uuid)
|
|
79
|
+
elseif tonumber(task.tries or 0) >= maxTries and not (type(task.recipientResults) == 'table' and task.isSending) then
|
|
73
80
|
redis.call('ZREM', KEYS[2], uuid)
|
|
74
81
|
elseif task.isSending then
|
|
75
82
|
local eligibleAt = tonumber(task.sendingAt or 0) + sendingTimeout
|
|
76
83
|
if eligibleAt > now then
|
|
77
84
|
redis.call('ZADD', KEYS[2], eligibleAt, uuid)
|
|
78
85
|
else
|
|
79
|
-
table.insert(tasks,
|
|
86
|
+
table.insert(tasks, payload)
|
|
80
87
|
end
|
|
81
88
|
elseif tonumber(task.sendAt or 0) <= now then
|
|
82
|
-
table.insert(tasks,
|
|
89
|
+
table.insert(tasks, payload)
|
|
83
90
|
else
|
|
84
91
|
redis.call('ZADD', KEYS[2], tonumber(task.sendAt), uuid)
|
|
85
92
|
end
|
|
@@ -106,28 +113,33 @@ const UPDATE_TAGGED_TASK_SCRIPT = `
|
|
|
106
113
|
local sendingTimeout = tonumber(ARGV[5])
|
|
107
114
|
local expectedTries = tonumber(ARGV[6])
|
|
108
115
|
local leaseSendingAt = tonumber(ARGV[7])
|
|
116
|
+
if payload ~= ARGV[8] then return -1 end
|
|
109
117
|
|
|
110
118
|
if mode == 'claim' then
|
|
111
|
-
if task.isSent or task.isFailed or task.isCancelled or tonumber(task.tries or 0) ~= expectedTries then
|
|
119
|
+
if task.isSent or task.isFailed or task.isCancelled or task.isSettled or tonumber(task.tries or 0) ~= expectedTries then
|
|
112
120
|
return 0
|
|
113
121
|
end
|
|
114
122
|
if task.isSending and tonumber(task.sendingAt or 0) > now - sendingTimeout then
|
|
115
123
|
return 0
|
|
116
124
|
end
|
|
117
125
|
elseif mode == 'lease' then
|
|
118
|
-
if task.isCancelled or task.isFailed or not task.isSending
|
|
126
|
+
if task.isSent or task.isSettled or task.isCancelled or task.isFailed or not task.isSending
|
|
119
127
|
or tonumber(task.tries or 0) ~= expectedTries
|
|
120
128
|
or tonumber(task.sendingAt or 0) ~= leaseSendingAt then
|
|
121
129
|
return 0
|
|
122
130
|
end
|
|
123
131
|
end
|
|
124
132
|
|
|
133
|
+
if mode == 'cancel' and (task.isSent or task.isSettled or task.isCancelled
|
|
134
|
+
or (type(task.recipientResults) == 'table' and task.isFailed)) then
|
|
135
|
+
return 0
|
|
136
|
+
end
|
|
125
137
|
for key, value in pairs(update) do
|
|
126
138
|
task[key] = value
|
|
127
139
|
end
|
|
128
|
-
redis.call('HSET', KEYS[1], ARGV[1],
|
|
140
|
+
redis.call('HSET', KEYS[1], ARGV[1], ARGV[9])
|
|
129
141
|
|
|
130
|
-
if task.isSent or task.isFailed or task.isCancelled then
|
|
142
|
+
if task.isSent or task.isFailed or task.isCancelled or task.isSettled then
|
|
131
143
|
redis.call('ZREM', KEYS[2], ARGV[1])
|
|
132
144
|
if #KEYS > 3 then
|
|
133
145
|
local concatUuid = redis.call('GET', KEYS[3])
|
|
@@ -152,12 +164,11 @@ const APPEND_TAGGED_MAIL_OPTION_SCRIPT = `
|
|
|
152
164
|
return 0
|
|
153
165
|
end
|
|
154
166
|
local task = cjson.decode(payload)
|
|
155
|
-
if task.isSending or task.isSent or task.isFailed or task.isCancelled then
|
|
167
|
+
if task.isSending or task.isSent or task.isFailed or task.isCancelled or task.isSettled or type(task.recipientResults) == 'table' then
|
|
156
168
|
return 0
|
|
157
169
|
end
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
redis.call('HSET', KEYS[1], ARGV[1], cjson.encode(task))
|
|
170
|
+
if payload ~= ARGV[3] then return -1 end
|
|
171
|
+
redis.call('HSET', KEYS[1], ARGV[1], ARGV[4])
|
|
161
172
|
return 1
|
|
162
173
|
`;
|
|
163
174
|
|
|
@@ -168,7 +179,9 @@ const REMOVE_TAGGED_TASK_SCRIPT = `
|
|
|
168
179
|
return 0
|
|
169
180
|
end
|
|
170
181
|
local task = cjson.decode(payload)
|
|
171
|
-
if ARGV[2] == '
|
|
182
|
+
if ARGV[2] == 'cancel' and (task.isSent or task.isSettled or task.isCancelled
|
|
183
|
+
or (type(task.recipientResults) == 'table' and task.isFailed)) then return 0 end
|
|
184
|
+
if ARGV[2] == 'lease' and (task.isSent or task.isSettled or task.isCancelled or task.isFailed or not task.isSending
|
|
172
185
|
or tonumber(task.tries or 0) ~= tonumber(ARGV[3])
|
|
173
186
|
or tonumber(task.sendingAt or 0) ~= tonumber(ARGV[4])) then
|
|
174
187
|
return 0
|
|
@@ -187,6 +200,18 @@ const REMOVE_TAGGED_TASK_SCRIPT = `
|
|
|
187
200
|
return 1
|
|
188
201
|
`;
|
|
189
202
|
|
|
203
|
+
const normalizePolicyArrays = (task) => {
|
|
204
|
+
if (task.recipientResults && !Array.isArray(task.recipientResults) && Object.keys(task.recipientResults).length === 0) task.recipientResults = [];
|
|
205
|
+
if (Array.isArray(task.recipientResults)) {
|
|
206
|
+
for (const result of task.recipientResults) {
|
|
207
|
+
for (const field of ['sources', 'reasons']) {
|
|
208
|
+
if (result[field] && !Array.isArray(result[field]) && Object.keys(result[field]).length === 0) result[field] = [];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return task;
|
|
213
|
+
};
|
|
214
|
+
|
|
190
215
|
const sha1Hex = (string) => createHash('sha1').update(string).digest('hex');
|
|
191
216
|
|
|
192
217
|
const isNoScriptError = (error) => {
|
|
@@ -199,6 +224,8 @@ const canReleaseLease = (currentTask, updateObj) => {
|
|
|
199
224
|
&& currentTask.isSending === true
|
|
200
225
|
&& (typeof currentTask.sendingAt === 'number' ? currentTask.sendingAt : 0) === updateObj.leaseSendingAt
|
|
201
226
|
&& currentTask.isCancelled !== true
|
|
227
|
+
&& currentTask.isSent !== true
|
|
228
|
+
&& currentTask.isSettled !== true
|
|
202
229
|
&& currentTask.isFailed !== true;
|
|
203
230
|
};
|
|
204
231
|
|
|
@@ -206,7 +233,7 @@ const canClaimTask = (currentTask, task, now, sendingTimeout) => {
|
|
|
206
233
|
if (!currentTask) {
|
|
207
234
|
return false;
|
|
208
235
|
}
|
|
209
|
-
if (currentTask.isSent === true || currentTask.isFailed === true || currentTask.isCancelled === true) {
|
|
236
|
+
if (currentTask.isSent === true || currentTask.isFailed === true || currentTask.isCancelled === true || currentTask.isSettled === true) {
|
|
210
237
|
return false;
|
|
211
238
|
}
|
|
212
239
|
if (currentTask.tries !== task.tries) {
|
|
@@ -225,11 +252,11 @@ const isIterateCandidate = (candidate, now, sendingTimeout, maxTries) => {
|
|
|
225
252
|
if (!candidate || typeof candidate !== 'object') {
|
|
226
253
|
return false;
|
|
227
254
|
}
|
|
228
|
-
if (candidate.isSent === true || candidate.isFailed === true || candidate.isCancelled === true) {
|
|
255
|
+
if (candidate.isSent === true || candidate.isFailed === true || candidate.isCancelled === true || candidate.isSettled === true) {
|
|
229
256
|
return false;
|
|
230
257
|
}
|
|
231
258
|
const tries = typeof candidate.tries === 'number' ? candidate.tries : 0;
|
|
232
|
-
if (tries >= maxTries) {
|
|
259
|
+
if (tries >= maxTries && !(Array.isArray(candidate.recipientResults) && candidate.isSending === true)) {
|
|
233
260
|
return false;
|
|
234
261
|
}
|
|
235
262
|
if (candidate.isSending === true) {
|
|
@@ -257,6 +284,7 @@ class RedisQueue {
|
|
|
257
284
|
*/
|
|
258
285
|
constructor (opts) {
|
|
259
286
|
this.name = 'redis-queue';
|
|
287
|
+
this.supportsRecipientPolicies = true;
|
|
260
288
|
if (!opts || typeof opts !== 'object') {
|
|
261
289
|
throw new TypeError('[mail-time] Configuration object must be passed into RedisQueue constructor');
|
|
262
290
|
}
|
|
@@ -319,6 +347,17 @@ class RedisQueue {
|
|
|
319
347
|
throw new Error(`[mail-time] [RedisQueue] unknown script "${scriptKey}"`);
|
|
320
348
|
}
|
|
321
349
|
|
|
350
|
+
if (this.useHashTags && typeof this.client.nodeClient === 'function' && typeof this.client.sendCommand === 'function') {
|
|
351
|
+
const key = options.keys[0];
|
|
352
|
+
const args = [`${options.keys.length}`, ...options.keys, ...options.arguments];
|
|
353
|
+
try {
|
|
354
|
+
return await this.client.sendCommand(key, false, ['EVALSHA', sha, ...args]);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
if (!isNoScriptError(error)) throw error;
|
|
357
|
+
}
|
|
358
|
+
return await this.client.sendCommand(key, false, ['EVAL', source, ...args]);
|
|
359
|
+
}
|
|
360
|
+
|
|
322
361
|
if (this.__loadedShas.has(sha) && typeof this.client.evalSha === 'function') {
|
|
323
362
|
try {
|
|
324
363
|
return await this.client.evalSha(sha, options);
|
|
@@ -386,7 +425,9 @@ class RedisQueue {
|
|
|
386
425
|
}
|
|
387
426
|
|
|
388
427
|
try {
|
|
389
|
-
const
|
|
428
|
+
const pingClient = typeof this.client.ping === 'function' ? this.client
|
|
429
|
+
: await this.client.nodeClient(this.client.getRandomNode());
|
|
430
|
+
const ping = await pingClient.ping();
|
|
390
431
|
if (ping === 'PONG') {
|
|
391
432
|
return {
|
|
392
433
|
status: 'OK',
|
|
@@ -443,7 +484,11 @@ class RedisQueue {
|
|
|
443
484
|
if (!Array.isArray(candidates)) {
|
|
444
485
|
return;
|
|
445
486
|
}
|
|
446
|
-
for (const
|
|
487
|
+
for (const value of candidates) {
|
|
488
|
+
if (this.mailTimeInstance.___isStopped) {
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
const candidate = normalizePolicyArrays(typeof value === 'string' ? JSON.parse(value) : value);
|
|
447
492
|
if (isIterateCandidate(candidate, now, sendingTimeout, maxTries)) {
|
|
448
493
|
await this.mailTimeInstance.___dispatch(candidate);
|
|
449
494
|
}
|
|
@@ -463,6 +508,9 @@ class RedisQueue {
|
|
|
463
508
|
for await (const cursorValue of cursor) {
|
|
464
509
|
const sendatKeys = Array.isArray(cursorValue) ? cursorValue : [cursorValue];
|
|
465
510
|
for (const sendatKey of sendatKeys) {
|
|
511
|
+
if (this.mailTimeInstance.___isStopped) {
|
|
512
|
+
break outer;
|
|
513
|
+
}
|
|
466
514
|
const raw = await this.client.get(sendatKey);
|
|
467
515
|
if (raw === null || parseInt(raw, 10) > now) {
|
|
468
516
|
continue;
|
|
@@ -520,7 +568,7 @@ class RedisQueue {
|
|
|
520
568
|
}
|
|
521
569
|
|
|
522
570
|
const task = JSON.parse(taskJSON);
|
|
523
|
-
if (!task || task.isSent === true || task.isCancelled === true || task.isFailed === true || task.sendAt > sendAt || task.isSending === true || task.tries >= this.mailTimeInstance.maxTries) {
|
|
571
|
+
if (!task || task.isSettled === true || task.recipientResults != null || task.isSent === true || task.isCancelled === true || task.isFailed === true || task.sendAt > sendAt || task.isSending === true || task.tries >= this.mailTimeInstance.maxTries) {
|
|
524
572
|
return null;
|
|
525
573
|
}
|
|
526
574
|
|
|
@@ -593,35 +641,59 @@ class RedisQueue {
|
|
|
593
641
|
* @returns {Promise<boolean>} returns `true` if cancelled or `false` if not found, was sent, or was cancelled previously
|
|
594
642
|
*/
|
|
595
643
|
async cancel(uuid) {
|
|
644
|
+
return await this.__serialize(() => this.__cancel(uuid));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** @internal */
|
|
648
|
+
async __serialize(operation) {
|
|
649
|
+
if (this.useHashTags) return await operation();
|
|
650
|
+
const previous = clientTransactions.get(this.client) || Promise.resolve();
|
|
651
|
+
const pending = previous.then(operation);
|
|
652
|
+
const settled = pending.catch(() => void 0);
|
|
653
|
+
clientTransactions.set(this.client, settled);
|
|
654
|
+
try { return await pending; }
|
|
655
|
+
finally { if (clientTransactions.get(this.client) === settled) clientTransactions.delete(this.client); }
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** @internal */
|
|
659
|
+
async __cancel(uuid) {
|
|
596
660
|
this.__debug('[cancel]', uuid);
|
|
597
661
|
if (typeof uuid !== 'string') {
|
|
598
662
|
return false;
|
|
599
663
|
}
|
|
600
664
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
?
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
665
|
+
this.__ensurePrefix();
|
|
666
|
+
const atomic = typeof this.client.watch === 'function' && typeof this.client.multi === 'function';
|
|
667
|
+
const letterKey = this.__getKey(uuid);
|
|
668
|
+
try {
|
|
669
|
+
if (!this.useHashTags && atomic) await this.client.watch(letterKey);
|
|
670
|
+
const payload = this.useHashTags ? await this.client.hGet(this.lettersKey, uuid) : await this.client.get(letterKey);
|
|
671
|
+
const task = payload ? JSON.parse(payload) : null;
|
|
672
|
+
if (!task || task.isSent || task.isCancelled || task.isSettled || (task.recipientResults != null && task.isFailed)) return false;
|
|
673
|
+
if (this.useHashTags) {
|
|
674
|
+
const keys = [this.lettersKey, this.scheduleKey];
|
|
675
|
+
if (task.to) keys.push(this.__getTaggedConcatKey(task.to), this.concatKeysKey);
|
|
676
|
+
const keep = this.mailTimeInstance.keepHistory;
|
|
677
|
+
const args = keep
|
|
678
|
+
? [uuid, JSON.stringify({ isCancelled: true }), 'cancel', `${Date.now()}`, `${this.mailTimeInstance.sendingTimeout || 300000}`, '0', '0', payload, JSON.stringify({ ...task, isCancelled: true })]
|
|
679
|
+
: [uuid, 'cancel', '0', '0'];
|
|
680
|
+
return Number(await this.__runScript(keep ? 'update' : 'remove', { keys, arguments: args })) >= 1;
|
|
681
|
+
}
|
|
682
|
+
if (!atomic) {
|
|
683
|
+
if (task.recipientResults != null) return false;
|
|
684
|
+
return this.mailTimeInstance.keepHistory ? await this.__update(task, { isCancelled: true }) : await this.__remove(task);
|
|
685
|
+
}
|
|
686
|
+
const multi = this.client.multi();
|
|
687
|
+
if (this.mailTimeInstance.keepHistory) multi.set(letterKey, JSON.stringify({ ...task, isCancelled: true }));
|
|
688
|
+
else multi.del(letterKey);
|
|
689
|
+
multi.del(this.__getKey(uuid, 'sendat'));
|
|
690
|
+
return (await multi.exec()) !== null;
|
|
691
|
+
} catch (error) {
|
|
692
|
+
logError('[cancel] storage error', error);
|
|
615
693
|
return false;
|
|
694
|
+
} finally {
|
|
695
|
+
if (!this.useHashTags && atomic) await this.client.unwatch?.();
|
|
616
696
|
}
|
|
617
|
-
|
|
618
|
-
if (!this.mailTimeInstance.keepHistory) {
|
|
619
|
-
return await this.remove(task);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
return await this.update(task, {
|
|
623
|
-
isCancelled: true,
|
|
624
|
-
});
|
|
625
697
|
}
|
|
626
698
|
|
|
627
699
|
/**
|
|
@@ -634,6 +706,11 @@ class RedisQueue {
|
|
|
634
706
|
* @returns {Promise<boolean>} returns `true` if removed or `false` if not found
|
|
635
707
|
*/
|
|
636
708
|
async remove(task, opts) {
|
|
709
|
+
return await this.__serialize(() => this.__remove(task, opts));
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** @internal */
|
|
713
|
+
async __remove(task, opts) {
|
|
637
714
|
this.__debug('[remove]', task?.uuid);
|
|
638
715
|
if (!task || typeof task !== 'object' || typeof task.uuid !== 'string') {
|
|
639
716
|
return false;
|
|
@@ -680,6 +757,8 @@ class RedisQueue {
|
|
|
680
757
|
|| currentTask.isSending !== true
|
|
681
758
|
|| (typeof currentTask.sendingAt === 'number' ? currentTask.sendingAt : 0) !== opts.leaseSendingAt
|
|
682
759
|
|| currentTask.isCancelled === true
|
|
760
|
+
|| currentTask.isSent === true
|
|
761
|
+
|| currentTask.isSettled === true
|
|
683
762
|
|| currentTask.isFailed === true) {
|
|
684
763
|
await this.client.unwatch?.();
|
|
685
764
|
return false;
|
|
@@ -723,6 +802,11 @@ class RedisQueue {
|
|
|
723
802
|
* @returns {Promise<boolean>} returns `true` if updated or `false` if not found or no changes was made
|
|
724
803
|
*/
|
|
725
804
|
async update(task, updateObj) {
|
|
805
|
+
return await this.__serialize(() => this.__update(task, updateObj));
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** @internal */
|
|
809
|
+
async __update(task, updateObj) {
|
|
726
810
|
this.__debug('[update]', task?.uuid);
|
|
727
811
|
if (!task || typeof task !== 'object' || typeof task.uuid !== 'string' || !updateObj || typeof updateObj !== 'object') {
|
|
728
812
|
return false;
|
|
@@ -733,38 +817,41 @@ class RedisQueue {
|
|
|
733
817
|
const isClaim = isSendClaimUpdate(updateObj);
|
|
734
818
|
const isAppend = isAppendMailOptionUpdate(updateObj);
|
|
735
819
|
const isLeaseRelease = isSendLeaseGuardedUpdate(updateObj);
|
|
820
|
+
if ((hasOwnProp(updateObj, 'recipientResults') || hasOwnProp(updateObj, 'isSettled')) && !isClaim && !isLeaseRelease) return false;
|
|
736
821
|
const now = isClaim && typeof updateObj.sendingAt === 'number' ? updateObj.sendingAt : Date.now();
|
|
737
822
|
const sendingTimeout = this.mailTimeInstance?.sendingTimeout || 300000;
|
|
738
823
|
|
|
739
824
|
try {
|
|
740
825
|
if (this.useHashTags) {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
826
|
+
// JS serialization preserves empty arrays and scalar values that Lua cjson changes.
|
|
827
|
+
// The script compares the exact payload as well as the claim/lease predicates.
|
|
828
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
829
|
+
const payload = await this.client.hGet(this.lettersKey, task.uuid);
|
|
830
|
+
if (!payload) return false;
|
|
831
|
+
const current = normalizePolicyArrays(JSON.parse(payload));
|
|
832
|
+
const persist = stripInternalUpdateMeta(updateObj);
|
|
833
|
+
const next = isAppend
|
|
834
|
+
? { ...current, mailOptions: [...(current.mailOptions || []), updateObj.appendMailOption] }
|
|
835
|
+
: { ...current, ...persist };
|
|
836
|
+
if (isAppend) {
|
|
837
|
+
const result = Number(await this.__runScript('append', {
|
|
838
|
+
keys: [this.lettersKey],
|
|
839
|
+
arguments: [task.uuid, JSON.stringify(updateObj.appendMailOption), payload, JSON.stringify(next)],
|
|
840
|
+
}));
|
|
841
|
+
if (result === -1) continue;
|
|
842
|
+
return result >= 1;
|
|
843
|
+
}
|
|
844
|
+
const keys = [this.lettersKey, this.scheduleKey];
|
|
845
|
+
if (current.to) keys.push(this.__getTaggedConcatKey(current.to), this.concatKeysKey);
|
|
846
|
+
const result = Number(await this.__runScript('update', {
|
|
847
|
+
keys,
|
|
848
|
+
arguments: [task.uuid, JSON.stringify(persist), isClaim ? 'claim' : (isLeaseRelease ? 'lease' : 'plain'),
|
|
849
|
+
`${now}`, `${sendingTimeout}`, `${isClaim ? task.tries : (updateObj.leaseTries || 0)}`,
|
|
850
|
+
`${updateObj.leaseSendingAt || 0}`, payload, JSON.stringify(next)],
|
|
851
|
+
}));
|
|
852
|
+
if (result !== -1) return result >= 1;
|
|
754
853
|
}
|
|
755
|
-
|
|
756
|
-
keys,
|
|
757
|
-
arguments: [
|
|
758
|
-
task.uuid,
|
|
759
|
-
JSON.stringify(stripInternalUpdateMeta(updateObj)),
|
|
760
|
-
mode,
|
|
761
|
-
`${now}`,
|
|
762
|
-
`${sendingTimeout}`,
|
|
763
|
-
`${isClaim ? task.tries : (updateObj.leaseTries || 0)}`,
|
|
764
|
-
`${updateObj.leaseSendingAt || 0}`,
|
|
765
|
-
],
|
|
766
|
-
});
|
|
767
|
-
return Number(result) >= 1;
|
|
854
|
+
return false;
|
|
768
855
|
}
|
|
769
856
|
|
|
770
857
|
if (isAppend) {
|
|
@@ -778,7 +865,7 @@ class RedisQueue {
|
|
|
778
865
|
return false;
|
|
779
866
|
}
|
|
780
867
|
const currentTask = JSON.parse(taskJSON);
|
|
781
|
-
if (currentTask.isSending === true || currentTask.isSent === true || currentTask.isFailed === true || currentTask.isCancelled === true) {
|
|
868
|
+
if (currentTask.isSending === true || currentTask.isSent === true || currentTask.isFailed === true || currentTask.isCancelled === true || currentTask.isSettled === true || Array.isArray(currentTask.recipientResults)) {
|
|
782
869
|
return false;
|
|
783
870
|
}
|
|
784
871
|
currentTask.mailOptions = [...(currentTask.mailOptions || []), updateObj.appendMailOption];
|
|
@@ -793,7 +880,7 @@ class RedisQueue {
|
|
|
793
880
|
return false;
|
|
794
881
|
}
|
|
795
882
|
const currentTask = JSON.parse(taskJSON);
|
|
796
|
-
if (currentTask.isSending === true || currentTask.isSent === true || currentTask.isFailed === true || currentTask.isCancelled === true) {
|
|
883
|
+
if (currentTask.isSending === true || currentTask.isSent === true || currentTask.isFailed === true || currentTask.isCancelled === true || currentTask.isSettled === true || Array.isArray(currentTask.recipientResults)) {
|
|
797
884
|
await this.client.unwatch?.();
|
|
798
885
|
return false;
|
|
799
886
|
}
|
|
@@ -833,7 +920,7 @@ class RedisQueue {
|
|
|
833
920
|
const updatedTask = { ...currentTask, ...stripInternalUpdateMeta(updateObj) };
|
|
834
921
|
const multi = this.client.multi();
|
|
835
922
|
multi.set(letterKey, JSON.stringify(updatedTask));
|
|
836
|
-
if (updatedTask.isSent === true || updatedTask.isFailed === true || updatedTask.isCancelled === true) {
|
|
923
|
+
if (updatedTask.isSent === true || updatedTask.isFailed === true || updatedTask.isCancelled === true || updatedTask.isSettled === true) {
|
|
837
924
|
multi.del(sendatKey);
|
|
838
925
|
} else if (updatedTask.sendAt) {
|
|
839
926
|
multi.set(sendatKey, `${+updatedTask.sendAt}`);
|
|
@@ -852,7 +939,7 @@ class RedisQueue {
|
|
|
852
939
|
const updatedTask = { ...currentTask, ...stripInternalUpdateMeta(updateObj) };
|
|
853
940
|
await this.client.set(letterKey, JSON.stringify(updatedTask));
|
|
854
941
|
|
|
855
|
-
if (updatedTask.isSent === true || updatedTask.isFailed === true || updatedTask.isCancelled === true) {
|
|
942
|
+
if (updatedTask.isSent === true || updatedTask.isFailed === true || updatedTask.isCancelled === true || updatedTask.isSettled === true) {
|
|
856
943
|
await this.client.del(sendatKey);
|
|
857
944
|
} else if (updatedTask.sendAt) {
|
|
858
945
|
await this.client.set(sendatKey, `${+updatedTask.sendAt}`);
|