mail-time 4.1.0 → 5.1.0
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 +76 -19
- package/adapters/redis.d.ts +19 -1
- package/adapters/redis.js +337 -8
- package/helpers.js +19 -0
- package/index.cjs +778 -127
- package/index.d.cts +50 -7
- package/index.d.ts +50 -7
- package/index.js +423 -118
- package/package.json +21 -4
- package/presets.js +2 -2
- package/scripts/migrate-redis-queue-to-cluster.mjs +108 -0
package/adapters/redis.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
stripInternalUpdateMeta,
|
|
8
8
|
isSendLeaseRemove,
|
|
9
9
|
} from '../helpers.js';
|
|
10
|
+
import { createHash } from 'crypto';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* @typedef {object} RedisClient
|
|
@@ -15,7 +16,11 @@ import {
|
|
|
15
16
|
* @property {(key: string, value: string, options?: object) => Promise<unknown>} set
|
|
16
17
|
* @property {(key: string|string[]) => Promise<number>} del
|
|
17
18
|
* @property {() => Promise<string>} ping
|
|
18
|
-
* @property {(options: object) => AsyncIterable<string|string[]>} scanIterator
|
|
19
|
+
* @property {(options: object) => AsyncIterable<string|string[]>} [scanIterator]
|
|
20
|
+
* @property {(key: string, field: string) => Promise<string|null>} [hGet]
|
|
21
|
+
* @property {(script: string, options: { keys: string[], arguments: string[] }) => Promise<unknown>} [eval]
|
|
22
|
+
* @property {(sha: string, options: { keys: string[], arguments: string[] }) => Promise<unknown>} [evalSha]
|
|
23
|
+
* @property {(script: string) => Promise<string>} [scriptLoad]
|
|
19
24
|
* @property {(key: string) => Promise<unknown>} [watch]
|
|
20
25
|
* @property {() => Promise<unknown>} [unwatch]
|
|
21
26
|
* @property {() => object} [multi]
|
|
@@ -25,10 +30,168 @@ import {
|
|
|
25
30
|
* @typedef {object} RedisQueueOption
|
|
26
31
|
* @property {RedisClient} client
|
|
27
32
|
* @property {string} [prefix]
|
|
33
|
+
* @property {boolean} [useHashTags] - Use Redis Cluster hash-tag keys (`mailtime:{prefix}:*`). Default keeps existing standalone keys.
|
|
28
34
|
*/
|
|
29
35
|
|
|
30
36
|
const KEY_TYPES = new Set(['letter', 'sendat', 'concatletter']);
|
|
31
37
|
const DEFAULT_PREFIX = 'default';
|
|
38
|
+
const VALID_PREFIX = /^[A-Za-z0-9_\-:.]+$/;
|
|
39
|
+
const TAGGED_ITERATE_LIMIT = 100;
|
|
40
|
+
|
|
41
|
+
const PUSH_TAGGED_TASK_SCRIPT = `
|
|
42
|
+
redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
|
|
43
|
+
redis.call('ZADD', KEYS[2], tonumber(ARGV[3]), ARGV[1])
|
|
44
|
+
if #KEYS > 3 then
|
|
45
|
+
redis.call('SET', KEYS[3], ARGV[1], 'PXAT', tonumber(ARGV[4]))
|
|
46
|
+
redis.call('ZADD', KEYS[4], tonumber(ARGV[4]), KEYS[3])
|
|
47
|
+
end
|
|
48
|
+
return 1
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const ITERATE_TAGGED_TASKS_SCRIPT = `
|
|
52
|
+
local now = tonumber(ARGV[1])
|
|
53
|
+
local maxTries = tonumber(ARGV[2])
|
|
54
|
+
local sendingTimeout = tonumber(ARGV[3])
|
|
55
|
+
local limit = tonumber(ARGV[4])
|
|
56
|
+
local scanLimit = tonumber(ARGV[5])
|
|
57
|
+
local tasks = {}
|
|
58
|
+
if #KEYS > 2 then
|
|
59
|
+
local expiredPointers = redis.call('ZRANGEBYSCORE', KEYS[3], '-inf', now, 'LIMIT', 0, 100)
|
|
60
|
+
for _, pointerKey in ipairs(expiredPointers) do
|
|
61
|
+
redis.call('DEL', pointerKey)
|
|
62
|
+
redis.call('ZREM', KEYS[3], pointerKey)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
local due = redis.call('ZRANGEBYSCORE', KEYS[2], '-inf', now, 'LIMIT', 0, scanLimit)
|
|
66
|
+
for _, uuid in ipairs(due) do
|
|
67
|
+
local payload = redis.call('HGET', KEYS[1], uuid)
|
|
68
|
+
if not payload then
|
|
69
|
+
redis.call('ZREM', KEYS[2], uuid)
|
|
70
|
+
else
|
|
71
|
+
local task = cjson.decode(payload)
|
|
72
|
+
if task.isSent or task.isFailed or task.isCancelled or tonumber(task.tries or 0) >= maxTries then
|
|
73
|
+
redis.call('ZREM', KEYS[2], uuid)
|
|
74
|
+
elseif task.isSending then
|
|
75
|
+
local eligibleAt = tonumber(task.sendingAt or 0) + sendingTimeout
|
|
76
|
+
if eligibleAt > now then
|
|
77
|
+
redis.call('ZADD', KEYS[2], eligibleAt, uuid)
|
|
78
|
+
else
|
|
79
|
+
table.insert(tasks, task)
|
|
80
|
+
end
|
|
81
|
+
elseif tonumber(task.sendAt or 0) <= now then
|
|
82
|
+
table.insert(tasks, task)
|
|
83
|
+
else
|
|
84
|
+
redis.call('ZADD', KEYS[2], tonumber(task.sendAt), uuid)
|
|
85
|
+
end
|
|
86
|
+
if #tasks >= limit then
|
|
87
|
+
break
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
return cjson.encode(tasks)
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
const UPDATE_TAGGED_TASK_SCRIPT = `
|
|
96
|
+
local payload = redis.call('HGET', KEYS[1], ARGV[1])
|
|
97
|
+
if not payload then
|
|
98
|
+
redis.call('ZREM', KEYS[2], ARGV[1])
|
|
99
|
+
return 0
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
local task = cjson.decode(payload)
|
|
103
|
+
local update = cjson.decode(ARGV[2])
|
|
104
|
+
local mode = ARGV[3]
|
|
105
|
+
local now = tonumber(ARGV[4])
|
|
106
|
+
local sendingTimeout = tonumber(ARGV[5])
|
|
107
|
+
local expectedTries = tonumber(ARGV[6])
|
|
108
|
+
local leaseSendingAt = tonumber(ARGV[7])
|
|
109
|
+
|
|
110
|
+
if mode == 'claim' then
|
|
111
|
+
if task.isSent or task.isFailed or task.isCancelled or tonumber(task.tries or 0) ~= expectedTries then
|
|
112
|
+
return 0
|
|
113
|
+
end
|
|
114
|
+
if task.isSending and tonumber(task.sendingAt or 0) > now - sendingTimeout then
|
|
115
|
+
return 0
|
|
116
|
+
end
|
|
117
|
+
elseif mode == 'lease' then
|
|
118
|
+
if task.isCancelled or task.isFailed or not task.isSending
|
|
119
|
+
or tonumber(task.tries or 0) ~= expectedTries
|
|
120
|
+
or tonumber(task.sendingAt or 0) ~= leaseSendingAt then
|
|
121
|
+
return 0
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
for key, value in pairs(update) do
|
|
126
|
+
task[key] = value
|
|
127
|
+
end
|
|
128
|
+
redis.call('HSET', KEYS[1], ARGV[1], cjson.encode(task))
|
|
129
|
+
|
|
130
|
+
if task.isSent or task.isFailed or task.isCancelled then
|
|
131
|
+
redis.call('ZREM', KEYS[2], ARGV[1])
|
|
132
|
+
if #KEYS > 3 then
|
|
133
|
+
local concatUuid = redis.call('GET', KEYS[3])
|
|
134
|
+
if concatUuid == ARGV[1] then
|
|
135
|
+
redis.call('DEL', KEYS[3])
|
|
136
|
+
redis.call('ZREM', KEYS[4], KEYS[3])
|
|
137
|
+
elseif not concatUuid then
|
|
138
|
+
redis.call('ZREM', KEYS[4], KEYS[3])
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
elseif task.isSending then
|
|
142
|
+
redis.call('ZADD', KEYS[2], tonumber(task.sendingAt or now) + sendingTimeout, ARGV[1])
|
|
143
|
+
else
|
|
144
|
+
redis.call('ZADD', KEYS[2], tonumber(task.sendAt), ARGV[1])
|
|
145
|
+
end
|
|
146
|
+
return 1
|
|
147
|
+
`;
|
|
148
|
+
|
|
149
|
+
const APPEND_TAGGED_MAIL_OPTION_SCRIPT = `
|
|
150
|
+
local payload = redis.call('HGET', KEYS[1], ARGV[1])
|
|
151
|
+
if not payload then
|
|
152
|
+
return 0
|
|
153
|
+
end
|
|
154
|
+
local task = cjson.decode(payload)
|
|
155
|
+
if task.isSending or task.isSent or task.isFailed or task.isCancelled then
|
|
156
|
+
return 0
|
|
157
|
+
end
|
|
158
|
+
task.mailOptions = task.mailOptions or {}
|
|
159
|
+
table.insert(task.mailOptions, cjson.decode(ARGV[2]))
|
|
160
|
+
redis.call('HSET', KEYS[1], ARGV[1], cjson.encode(task))
|
|
161
|
+
return 1
|
|
162
|
+
`;
|
|
163
|
+
|
|
164
|
+
const REMOVE_TAGGED_TASK_SCRIPT = `
|
|
165
|
+
local payload = redis.call('HGET', KEYS[1], ARGV[1])
|
|
166
|
+
if not payload then
|
|
167
|
+
redis.call('ZREM', KEYS[2], ARGV[1])
|
|
168
|
+
return 0
|
|
169
|
+
end
|
|
170
|
+
local task = cjson.decode(payload)
|
|
171
|
+
if ARGV[2] == 'lease' and (task.isCancelled or task.isFailed or not task.isSending
|
|
172
|
+
or tonumber(task.tries or 0) ~= tonumber(ARGV[3])
|
|
173
|
+
or tonumber(task.sendingAt or 0) ~= tonumber(ARGV[4])) then
|
|
174
|
+
return 0
|
|
175
|
+
end
|
|
176
|
+
redis.call('HDEL', KEYS[1], ARGV[1])
|
|
177
|
+
redis.call('ZREM', KEYS[2], ARGV[1])
|
|
178
|
+
if #KEYS > 3 then
|
|
179
|
+
local concatUuid = redis.call('GET', KEYS[3])
|
|
180
|
+
if concatUuid == ARGV[1] then
|
|
181
|
+
redis.call('DEL', KEYS[3])
|
|
182
|
+
redis.call('ZREM', KEYS[4], KEYS[3])
|
|
183
|
+
elseif not concatUuid then
|
|
184
|
+
redis.call('ZREM', KEYS[4], KEYS[3])
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
return 1
|
|
188
|
+
`;
|
|
189
|
+
|
|
190
|
+
const sha1Hex = (string) => createHash('sha1').update(string).digest('hex');
|
|
191
|
+
|
|
192
|
+
const isNoScriptError = (error) => {
|
|
193
|
+
return !!error && (error.code === 'NOSCRIPT' || (typeof error.message === 'string' && error.message.includes('NOSCRIPT')));
|
|
194
|
+
};
|
|
32
195
|
|
|
33
196
|
const canReleaseLease = (currentTask, updateObj) => {
|
|
34
197
|
return currentTask
|
|
@@ -102,7 +265,21 @@ class RedisQueue {
|
|
|
102
265
|
throw new Error('[mail-time] [RedisQueue] required {client} option is missing, e.g. returned from `redis.createClient()` or `redis.createCluster()` method');
|
|
103
266
|
}
|
|
104
267
|
|
|
268
|
+
if (opts.useHashTags !== undefined && typeof opts.useHashTags !== 'boolean') {
|
|
269
|
+
throw new TypeError(`[mail-time] [RedisQueue] {useHashTags} option must be a boolean (received: ${typeof opts.useHashTags})`);
|
|
270
|
+
}
|
|
271
|
+
|
|
105
272
|
this.client = opts.client;
|
|
273
|
+
this.useHashTags = opts.useHashTags === true;
|
|
274
|
+
this.__scriptSources = {
|
|
275
|
+
push: PUSH_TAGGED_TASK_SCRIPT,
|
|
276
|
+
iterate: ITERATE_TAGGED_TASKS_SCRIPT,
|
|
277
|
+
update: UPDATE_TAGGED_TASK_SCRIPT,
|
|
278
|
+
append: APPEND_TAGGED_MAIL_OPTION_SCRIPT,
|
|
279
|
+
remove: REMOVE_TAGGED_TASK_SCRIPT,
|
|
280
|
+
};
|
|
281
|
+
this.__scriptShas = Object.fromEntries(Object.entries(this.__scriptSources).map(([name, source]) => [name, sha1Hex(source)]));
|
|
282
|
+
this.__loadedShas = new Set();
|
|
106
283
|
if (typeof opts.prefix === 'string') {
|
|
107
284
|
this.__applyPrefix(opts.prefix);
|
|
108
285
|
}
|
|
@@ -110,8 +287,16 @@ class RedisQueue {
|
|
|
110
287
|
|
|
111
288
|
/** @internal */
|
|
112
289
|
__applyPrefix(prefix) {
|
|
290
|
+
if (this.useHashTags && !VALID_PREFIX.test(prefix)) {
|
|
291
|
+
throw new Error(`[mail-time] [RedisQueue] {prefix} option must match ${VALID_PREFIX} when {useHashTags} is true (received: "${prefix}")`);
|
|
292
|
+
}
|
|
113
293
|
this.prefix = prefix;
|
|
114
|
-
this.uniqueName = `mailtime:${prefix}`;
|
|
294
|
+
this.uniqueName = this.useHashTags ? `mailtime:{${prefix}}` : `mailtime:${prefix}`;
|
|
295
|
+
if (this.useHashTags) {
|
|
296
|
+
this.lettersKey = `${this.uniqueName}:letters`;
|
|
297
|
+
this.scheduleKey = `${this.uniqueName}:schedule`;
|
|
298
|
+
this.concatKeysKey = `${this.uniqueName}:concatkeys`;
|
|
299
|
+
}
|
|
115
300
|
}
|
|
116
301
|
|
|
117
302
|
/** @internal */
|
|
@@ -126,6 +311,49 @@ class RedisQueue {
|
|
|
126
311
|
debug(this.mailTimeInstance?.debug === true, `[${this.name}]`, ...args);
|
|
127
312
|
}
|
|
128
313
|
|
|
314
|
+
/** @internal */
|
|
315
|
+
async __runScript(scriptKey, options) {
|
|
316
|
+
const source = this.__scriptSources[scriptKey];
|
|
317
|
+
const sha = this.__scriptShas[scriptKey];
|
|
318
|
+
if (!source || !sha) {
|
|
319
|
+
throw new Error(`[mail-time] [RedisQueue] unknown script "${scriptKey}"`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (this.__loadedShas.has(sha) && typeof this.client.evalSha === 'function') {
|
|
323
|
+
try {
|
|
324
|
+
return await this.client.evalSha(sha, options);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (!isNoScriptError(error)) {
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
this.__loadedShas.delete(sha);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (typeof this.client.scriptLoad === 'function' && typeof this.client.evalSha === 'function') {
|
|
334
|
+
try {
|
|
335
|
+
await this.client.scriptLoad(source);
|
|
336
|
+
this.__loadedShas.add(sha);
|
|
337
|
+
return await this.client.evalSha(sha, options);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (!isNoScriptError(error)) {
|
|
340
|
+
this.__debug(`[script:${scriptKey}] scriptLoad failed; falling back to EVAL`, error);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (typeof this.client.eval !== 'function') {
|
|
346
|
+
throw new Error('[mail-time] [RedisQueue] Redis Cluster client must support EVAL');
|
|
347
|
+
}
|
|
348
|
+
return await this.client.eval(source, options);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** @internal */
|
|
352
|
+
__getTaggedConcatKey(to) {
|
|
353
|
+
this.__ensurePrefix();
|
|
354
|
+
return `${this.uniqueName}:concatletter:${to}`;
|
|
355
|
+
}
|
|
356
|
+
|
|
129
357
|
/**
|
|
130
358
|
* @async
|
|
131
359
|
* @memberOf RedisQueue
|
|
@@ -203,6 +431,25 @@ class RedisQueue {
|
|
|
203
431
|
const maxTries = (this.mailTimeInstance && typeof this.mailTimeInstance.maxTries === 'number')
|
|
204
432
|
? this.mailTimeInstance.maxTries
|
|
205
433
|
: 60;
|
|
434
|
+
|
|
435
|
+
if (this.useHashTags) {
|
|
436
|
+
this.__ensurePrefix();
|
|
437
|
+
const dispatchLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : TAGGED_ITERATE_LIMIT;
|
|
438
|
+
const payload = await this.__runScript('iterate', {
|
|
439
|
+
keys: [this.lettersKey, this.scheduleKey, this.concatKeysKey],
|
|
440
|
+
arguments: [`${now}`, `${maxTries}`, `${sendingTimeout}`, `${dispatchLimit}`, `${Math.max(dispatchLimit * 20, 100)}`],
|
|
441
|
+
});
|
|
442
|
+
const candidates = payload ? JSON.parse(String(payload)) : [];
|
|
443
|
+
if (!Array.isArray(candidates)) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
for (const candidate of candidates) {
|
|
447
|
+
if (isIterateCandidate(candidate, now, sendingTimeout, maxTries)) {
|
|
448
|
+
await this.mailTimeInstance.___dispatch(candidate);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
206
453
|
let dispatched = 0;
|
|
207
454
|
|
|
208
455
|
const matchPattern = this.__getKey('*', 'sendat');
|
|
@@ -259,14 +506,15 @@ class RedisQueue {
|
|
|
259
506
|
return null;
|
|
260
507
|
}
|
|
261
508
|
|
|
262
|
-
const concatKey = this.__getKey(to, 'concatletter');
|
|
509
|
+
const concatKey = this.useHashTags ? this.__getTaggedConcatKey(to) : this.__getKey(to, 'concatletter');
|
|
263
510
|
const uuid = await this.client.get(concatKey);
|
|
264
511
|
if (!uuid) {
|
|
265
512
|
return null;
|
|
266
513
|
}
|
|
267
514
|
|
|
268
|
-
const
|
|
269
|
-
|
|
515
|
+
const taskJSON = this.useHashTags
|
|
516
|
+
? await this.client.hGet(this.lettersKey, uuid)
|
|
517
|
+
: await this.client.get(this.__getKey(uuid, 'letter'));
|
|
270
518
|
if (!taskJSON) {
|
|
271
519
|
return null;
|
|
272
520
|
}
|
|
@@ -297,6 +545,19 @@ class RedisQueue {
|
|
|
297
545
|
task.sendAt = +task.sendAt;
|
|
298
546
|
}
|
|
299
547
|
|
|
548
|
+
if (this.useHashTags) {
|
|
549
|
+
this.__ensurePrefix();
|
|
550
|
+
const keys = [this.lettersKey, this.scheduleKey];
|
|
551
|
+
const args = [task.uuid, JSON.stringify(task), `${+task.sendAt}`];
|
|
552
|
+
if (task.to) {
|
|
553
|
+
keys.push(this.__getTaggedConcatKey(task.to));
|
|
554
|
+
keys.push(this.concatKeysKey);
|
|
555
|
+
args.push(`${task.sendAt - 128}`);
|
|
556
|
+
}
|
|
557
|
+
await this.__runScript('push', { keys, arguments: args });
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
300
561
|
const letterKey = this.__getKey(task.uuid, 'letter');
|
|
301
562
|
const sendatKey = this.__getKey(task.uuid, 'sendat');
|
|
302
563
|
const taskJSON = JSON.stringify(task);
|
|
@@ -337,9 +598,14 @@ class RedisQueue {
|
|
|
337
598
|
return false;
|
|
338
599
|
}
|
|
339
600
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
601
|
+
if (this.useHashTags) {
|
|
602
|
+
this.__ensurePrefix();
|
|
603
|
+
} else {
|
|
604
|
+
await this.client.del(this.__getKey(uuid, 'sendat'));
|
|
605
|
+
}
|
|
606
|
+
const taskJSON = this.useHashTags
|
|
607
|
+
? await this.client.hGet(this.lettersKey, uuid)
|
|
608
|
+
: await this.client.get(this.__getKey(uuid, 'letter'));
|
|
343
609
|
if (!taskJSON) {
|
|
344
610
|
return false;
|
|
345
611
|
}
|
|
@@ -373,6 +639,30 @@ class RedisQueue {
|
|
|
373
639
|
return false;
|
|
374
640
|
}
|
|
375
641
|
|
|
642
|
+
if (this.useHashTags) {
|
|
643
|
+
this.__ensurePrefix();
|
|
644
|
+
try {
|
|
645
|
+
const keys = [this.lettersKey, this.scheduleKey];
|
|
646
|
+
if (task.to) {
|
|
647
|
+
keys.push(this.__getTaggedConcatKey(task.to));
|
|
648
|
+
keys.push(this.concatKeysKey);
|
|
649
|
+
}
|
|
650
|
+
const result = await this.__runScript('remove', {
|
|
651
|
+
keys,
|
|
652
|
+
arguments: [
|
|
653
|
+
task.uuid,
|
|
654
|
+
isSendLeaseRemove(opts) ? 'lease' : 'plain',
|
|
655
|
+
`${opts?.leaseTries || 0}`,
|
|
656
|
+
`${opts?.leaseSendingAt || 0}`,
|
|
657
|
+
],
|
|
658
|
+
});
|
|
659
|
+
return Number(result) >= 1;
|
|
660
|
+
} catch (opError) {
|
|
661
|
+
logError('[remove] [tagged] [opError]', opError);
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
376
666
|
const letterKey = this.__getKey(task.uuid, 'letter');
|
|
377
667
|
if (isSendLeaseRemove(opts)) {
|
|
378
668
|
if (typeof this.client.watch !== 'function' || typeof this.client.multi !== 'function') {
|
|
@@ -447,6 +737,36 @@ class RedisQueue {
|
|
|
447
737
|
const sendingTimeout = this.mailTimeInstance?.sendingTimeout || 300000;
|
|
448
738
|
|
|
449
739
|
try {
|
|
740
|
+
if (this.useHashTags) {
|
|
741
|
+
if (isAppend) {
|
|
742
|
+
const result = await this.__runScript('append', {
|
|
743
|
+
keys: [this.lettersKey],
|
|
744
|
+
arguments: [task.uuid, JSON.stringify(updateObj.appendMailOption)],
|
|
745
|
+
});
|
|
746
|
+
return Number(result) >= 1;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const mode = isClaim ? 'claim' : (isLeaseRelease ? 'lease' : 'plain');
|
|
750
|
+
const keys = [this.lettersKey, this.scheduleKey];
|
|
751
|
+
if (task.to) {
|
|
752
|
+
keys.push(this.__getTaggedConcatKey(task.to));
|
|
753
|
+
keys.push(this.concatKeysKey);
|
|
754
|
+
}
|
|
755
|
+
const result = await this.__runScript('update', {
|
|
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;
|
|
768
|
+
}
|
|
769
|
+
|
|
450
770
|
if (isAppend) {
|
|
451
771
|
if (typeof this.client.watch !== 'function' || typeof this.client.multi !== 'function') {
|
|
452
772
|
if (!RedisQueue.__atomicAppendWarned) {
|
|
@@ -558,6 +878,15 @@ class RedisQueue {
|
|
|
558
878
|
throw new Error(`[mail-time] [RedisQueue] [__getKey] unsupported key "${type}" passed into the second argument`);
|
|
559
879
|
}
|
|
560
880
|
this.__ensurePrefix();
|
|
881
|
+
if (this.useHashTags) {
|
|
882
|
+
if (type === 'letter') {
|
|
883
|
+
return this.lettersKey;
|
|
884
|
+
}
|
|
885
|
+
if (type === 'sendat') {
|
|
886
|
+
return this.scheduleKey;
|
|
887
|
+
}
|
|
888
|
+
return this.__getTaggedConcatKey(uuid);
|
|
889
|
+
}
|
|
561
890
|
return `${this.uniqueName}:${type}:${uuid}`;
|
|
562
891
|
}
|
|
563
892
|
}
|
package/helpers.js
CHANGED
|
@@ -220,6 +220,24 @@ const filterAddressField = (field, acceptedSet) => {
|
|
|
220
220
|
return field;
|
|
221
221
|
};
|
|
222
222
|
|
|
223
|
+
const HTML_ESCAPES = {
|
|
224
|
+
'&': '&',
|
|
225
|
+
'<': '<',
|
|
226
|
+
'>': '>',
|
|
227
|
+
'"': '"',
|
|
228
|
+
"'": ''',
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @name escapeHtml - Escape the five HTML-significant characters.
|
|
233
|
+
* @function
|
|
234
|
+
* @param {string} value
|
|
235
|
+
* @returns {string} `value` safe to interpolate into HTML text or an attribute value.
|
|
236
|
+
*/
|
|
237
|
+
const escapeHtml = (value) => {
|
|
238
|
+
return `${value}`.replace(/[&<>"']/g, (char) => HTML_ESCAPES[char]);
|
|
239
|
+
};
|
|
240
|
+
|
|
223
241
|
/**
|
|
224
242
|
* @name isSendClaimUpdate - Detect an atomic send-claim update (`{ isSending: true, tries: N }`).
|
|
225
243
|
* @function
|
|
@@ -278,6 +296,7 @@ const isSendLeaseRemove = (opts) => {
|
|
|
278
296
|
|
|
279
297
|
export {
|
|
280
298
|
debug,
|
|
299
|
+
escapeHtml,
|
|
281
300
|
logError,
|
|
282
301
|
hasOwnProp,
|
|
283
302
|
isPlainObject,
|