mail-time 1.4.0 → 4.0.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/LICENSE +1 -1
- package/README.md +367 -301
- package/adapters/mongo.d.ts +118 -0
- package/adapters/mongo.js +431 -0
- package/adapters/postgres.d.ts +100 -0
- package/adapters/postgres.js +520 -0
- package/adapters/redis.d.ts +105 -0
- package/adapters/redis.js +565 -0
- package/helpers.js +217 -0
- package/index.cjs +2862 -462
- package/index.d.cts +300 -0
- package/index.d.ts +300 -0
- package/index.js +1002 -475
- package/package.json +66 -16
- package/presets.d.ts +87 -0
- package/presets.js +193 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
import {
|
|
2
|
+
debug,
|
|
3
|
+
logError,
|
|
4
|
+
isSendClaimUpdate,
|
|
5
|
+
isSendLeaseGuardedUpdate,
|
|
6
|
+
isAppendMailOptionUpdate,
|
|
7
|
+
stripInternalUpdateMeta,
|
|
8
|
+
isSendLeaseRemove,
|
|
9
|
+
} from '../helpers.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} RedisClient
|
|
13
|
+
* @property {(key: string) => Promise<number>} exists
|
|
14
|
+
* @property {(key: string) => Promise<string|null>} get
|
|
15
|
+
* @property {(key: string, value: string, options?: object) => Promise<unknown>} set
|
|
16
|
+
* @property {(key: string|string[]) => Promise<number>} del
|
|
17
|
+
* @property {() => Promise<string>} ping
|
|
18
|
+
* @property {(options: object) => AsyncIterable<string|string[]>} scanIterator
|
|
19
|
+
* @property {(key: string) => Promise<unknown>} [watch]
|
|
20
|
+
* @property {() => Promise<unknown>} [unwatch]
|
|
21
|
+
* @property {() => object} [multi]
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {object} RedisQueueOption
|
|
26
|
+
* @property {RedisClient} client
|
|
27
|
+
* @property {string} [prefix]
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const KEY_TYPES = new Set(['letter', 'sendat', 'concatletter']);
|
|
31
|
+
const DEFAULT_PREFIX = 'default';
|
|
32
|
+
|
|
33
|
+
const canReleaseLease = (currentTask, updateObj) => {
|
|
34
|
+
return currentTask
|
|
35
|
+
&& currentTask.tries === updateObj.leaseTries
|
|
36
|
+
&& currentTask.isSending === true
|
|
37
|
+
&& (typeof currentTask.sendingAt === 'number' ? currentTask.sendingAt : 0) === updateObj.leaseSendingAt
|
|
38
|
+
&& currentTask.isCancelled !== true
|
|
39
|
+
&& currentTask.isFailed !== true;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const canClaimTask = (currentTask, task, now, sendingTimeout) => {
|
|
43
|
+
if (!currentTask) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (currentTask.isSent === true || currentTask.isFailed === true || currentTask.isCancelled === true) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (currentTask.tries !== task.tries) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (currentTask.isSending === true) {
|
|
53
|
+
const sendingAt = typeof currentTask.sendingAt === 'number' ? currentTask.sendingAt : 0;
|
|
54
|
+
if (sendingAt > now - sendingTimeout) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const isIterateCandidate = (candidate, now, sendingTimeout, maxTries) => {
|
|
62
|
+
if (!candidate || typeof candidate !== 'object') {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
if (candidate.isSent === true || candidate.isFailed === true || candidate.isCancelled === true) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const tries = typeof candidate.tries === 'number' ? candidate.tries : 0;
|
|
69
|
+
if (tries >= maxTries) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (candidate.isSending === true) {
|
|
73
|
+
const sendingAt = typeof candidate.sendingAt === 'number' ? candidate.sendingAt : 0;
|
|
74
|
+
if (sendingAt > now - sendingTimeout) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const parseUuidFromKey = (key, uniqueName) => {
|
|
82
|
+
const prefix = `${uniqueName}:sendat:`;
|
|
83
|
+
if (!key.startsWith(prefix)) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return key.slice(prefix.length);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Class representing Redis Queue for MailTime */
|
|
90
|
+
class RedisQueue {
|
|
91
|
+
/**
|
|
92
|
+
* Create a RedisQueue instance
|
|
93
|
+
* @param {RedisQueueOption} opts - configuration object
|
|
94
|
+
*/
|
|
95
|
+
constructor (opts) {
|
|
96
|
+
this.name = 'redis-queue';
|
|
97
|
+
if (!opts || typeof opts !== 'object') {
|
|
98
|
+
throw new TypeError('[mail-time] Configuration object must be passed into RedisQueue constructor');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!opts.client) {
|
|
102
|
+
throw new Error('[mail-time] [RedisQueue] required {client} option is missing, e.g. returned from `redis.createClient()` or `redis.createCluster()` method');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.client = opts.client;
|
|
106
|
+
if (typeof opts.prefix === 'string') {
|
|
107
|
+
this.__applyPrefix(opts.prefix);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** @internal */
|
|
112
|
+
__applyPrefix(prefix) {
|
|
113
|
+
this.prefix = prefix;
|
|
114
|
+
this.uniqueName = `mailtime:${prefix}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** @internal */
|
|
118
|
+
__ensurePrefix() {
|
|
119
|
+
if (typeof this.prefix !== 'string') {
|
|
120
|
+
this.__applyPrefix(this.mailTimeInstance?.prefix || DEFAULT_PREFIX);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** @internal */
|
|
125
|
+
__debug(...args) {
|
|
126
|
+
debug(this.mailTimeInstance?.debug === true, `[${this.name}]`, ...args);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @async
|
|
131
|
+
* @memberOf RedisQueue
|
|
132
|
+
* @name ready
|
|
133
|
+
* @description Storage adapter has no async setup
|
|
134
|
+
* @returns {Promise<void 0>}
|
|
135
|
+
*/
|
|
136
|
+
async ready() {
|
|
137
|
+
this.__ensurePrefix();
|
|
138
|
+
this.__debug('[ready]');
|
|
139
|
+
return void 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @async
|
|
144
|
+
* @memberOf RedisQueue
|
|
145
|
+
* @name ping
|
|
146
|
+
* @description Check connection to Storage
|
|
147
|
+
* @returns {Promise<object>}
|
|
148
|
+
*/
|
|
149
|
+
async ping() {
|
|
150
|
+
this.__debug('[ping]');
|
|
151
|
+
if (!this.mailTimeInstance) {
|
|
152
|
+
return {
|
|
153
|
+
status: 'Service Unavailable',
|
|
154
|
+
code: 503,
|
|
155
|
+
statusCode: 503,
|
|
156
|
+
error: new Error('MailTime instance not yet assigned to {mailTimeInstance} of Queue Adapter context'),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const ping = await this.client.ping();
|
|
162
|
+
if (ping === 'PONG') {
|
|
163
|
+
return {
|
|
164
|
+
status: 'OK',
|
|
165
|
+
code: 200,
|
|
166
|
+
statusCode: 200,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
} catch (pingError) {
|
|
170
|
+
return {
|
|
171
|
+
status: 'Internal Server Error',
|
|
172
|
+
code: 500,
|
|
173
|
+
statusCode: 500,
|
|
174
|
+
error: pingError,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
status: 'Service Unavailable',
|
|
180
|
+
code: 503,
|
|
181
|
+
statusCode: 503,
|
|
182
|
+
error: new Error('Service Unavailable'),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* @memberOf RedisQueue
|
|
188
|
+
* @name iterate
|
|
189
|
+
* @description iterate over queued tasks passing each to `mailTimeInstance.___dispatch` (the bounded send pool)
|
|
190
|
+
* @param {{ limit?: number, sendingTimeout?: number }} [opts] - iteration options
|
|
191
|
+
* @returns {Promise<void>}
|
|
192
|
+
*/
|
|
193
|
+
async iterate(opts) {
|
|
194
|
+
this.__debug('[iterate]', opts);
|
|
195
|
+
try {
|
|
196
|
+
const now = Date.now();
|
|
197
|
+
const sendingTimeout = (opts && typeof opts.sendingTimeout === 'number' && opts.sendingTimeout > 0)
|
|
198
|
+
? opts.sendingTimeout
|
|
199
|
+
: 300000;
|
|
200
|
+
const limit = (opts && typeof opts.limit === 'number' && Number.isFinite(opts.limit) && opts.limit > 0)
|
|
201
|
+
? Math.floor(opts.limit)
|
|
202
|
+
: 0;
|
|
203
|
+
const maxTries = (this.mailTimeInstance && typeof this.mailTimeInstance.maxTries === 'number')
|
|
204
|
+
? this.mailTimeInstance.maxTries
|
|
205
|
+
: 60;
|
|
206
|
+
let dispatched = 0;
|
|
207
|
+
|
|
208
|
+
const matchPattern = this.__getKey('*', 'sendat');
|
|
209
|
+
const cursor = this.client.scanIterator({
|
|
210
|
+
TYPE: 'string',
|
|
211
|
+
MATCH: matchPattern,
|
|
212
|
+
COUNT: 9999,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
outer:
|
|
216
|
+
for await (const cursorValue of cursor) {
|
|
217
|
+
const sendatKeys = Array.isArray(cursorValue) ? cursorValue : [cursorValue];
|
|
218
|
+
for (const sendatKey of sendatKeys) {
|
|
219
|
+
const raw = await this.client.get(sendatKey);
|
|
220
|
+
if (raw === null || parseInt(raw, 10) > now) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const uuid = parseUuidFromKey(sendatKey, this.uniqueName);
|
|
224
|
+
if (!uuid) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const taskJSON = await this.client.get(this.__getKey(uuid));
|
|
228
|
+
if (!taskJSON) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const candidate = JSON.parse(taskJSON);
|
|
232
|
+
if (!isIterateCandidate(candidate, now, sendingTimeout, maxTries)) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
await this.mailTimeInstance.___dispatch(candidate);
|
|
236
|
+
dispatched++;
|
|
237
|
+
if (limit > 0 && dispatched >= limit) {
|
|
238
|
+
break outer;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} catch (iterateError) {
|
|
243
|
+
logError('[iterate] [for/await] [iterateError]', iterateError);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @async
|
|
249
|
+
* @memberOf RedisQueue
|
|
250
|
+
* @name getPendingTo
|
|
251
|
+
* @description get queued task by `to` field (addressee)
|
|
252
|
+
* @param to {string} - email address
|
|
253
|
+
* @param sendAt {number} - timestamp
|
|
254
|
+
* @returns {Promise<object|null>}
|
|
255
|
+
*/
|
|
256
|
+
async getPendingTo(to, sendAt) {
|
|
257
|
+
this.__debug('[getPendingTo]', to, sendAt);
|
|
258
|
+
if (typeof to !== 'string' || typeof sendAt !== 'number') {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const concatKey = this.__getKey(to, 'concatletter');
|
|
263
|
+
const uuid = await this.client.get(concatKey);
|
|
264
|
+
if (!uuid) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const letterKey = this.__getKey(uuid, 'letter');
|
|
269
|
+
const taskJSON = await this.client.get(letterKey);
|
|
270
|
+
if (!taskJSON) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const task = JSON.parse(taskJSON);
|
|
275
|
+
if (!task || task.isSent === true || task.isCancelled === true || task.isFailed === true || task.sendAt > sendAt || task.isSending === true || task.tries >= this.mailTimeInstance.maxTries) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return task;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* @async
|
|
284
|
+
* @memberOf RedisQueue
|
|
285
|
+
* @name push
|
|
286
|
+
* @description push task to the queue/storage
|
|
287
|
+
* @param task {object} - task's object
|
|
288
|
+
* @returns {Promise<void 0>}
|
|
289
|
+
*/
|
|
290
|
+
async push(task) {
|
|
291
|
+
this.__debug('[push]', task?.uuid);
|
|
292
|
+
if (!task || typeof task !== 'object') {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (task.sendAt instanceof Date) {
|
|
297
|
+
task.sendAt = +task.sendAt;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const letterKey = this.__getKey(task.uuid, 'letter');
|
|
301
|
+
const sendatKey = this.__getKey(task.uuid, 'sendat');
|
|
302
|
+
const taskJSON = JSON.stringify(task);
|
|
303
|
+
|
|
304
|
+
if (typeof this.client.multi === 'function') {
|
|
305
|
+
const multi = this.client.multi();
|
|
306
|
+
multi.set(letterKey, taskJSON);
|
|
307
|
+
multi.set(sendatKey, `${task.sendAt}`);
|
|
308
|
+
if (task.to) {
|
|
309
|
+
multi.set(this.__getKey(task.to, 'concatletter'), task.uuid, {
|
|
310
|
+
PXAT: task.sendAt - 128,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
await multi.exec();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
await this.client.set(letterKey, taskJSON);
|
|
318
|
+
await this.client.set(sendatKey, `${task.sendAt}`);
|
|
319
|
+
if (task.to) {
|
|
320
|
+
await this.client.set(this.__getKey(task.to, 'concatletter'), task.uuid, {
|
|
321
|
+
PXAT: task.sendAt - 128,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* @async
|
|
328
|
+
* @memberOf RedisQueue
|
|
329
|
+
* @name cancel
|
|
330
|
+
* @description cancel scheduled email
|
|
331
|
+
* @param uuid {string} - email's uuid
|
|
332
|
+
* @returns {Promise<boolean>} returns `true` if cancelled or `false` if not found, was sent, or was cancelled previously
|
|
333
|
+
*/
|
|
334
|
+
async cancel(uuid) {
|
|
335
|
+
this.__debug('[cancel]', uuid);
|
|
336
|
+
if (typeof uuid !== 'string') {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
await this.client.del(this.__getKey(uuid, 'sendat'));
|
|
341
|
+
const letterKey = this.__getKey(uuid, 'letter');
|
|
342
|
+
const taskJSON = await this.client.get(letterKey);
|
|
343
|
+
if (!taskJSON) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const task = JSON.parse(taskJSON);
|
|
348
|
+
if (!task || task.isSent === true || task.isCancelled === true) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (!this.mailTimeInstance.keepHistory) {
|
|
353
|
+
return await this.remove(task);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return await this.update(task, {
|
|
357
|
+
isCancelled: true,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* @async
|
|
363
|
+
* @memberOf RedisQueue
|
|
364
|
+
* @name remove
|
|
365
|
+
* @description remove task from queue
|
|
366
|
+
* @param task {object} - task's object
|
|
367
|
+
* @param {{ leaseTries: number, leaseSendingAt: number }} [opts] - lease guard: only remove if this worker still holds the lease (tries + sendingAt match, row not cancelled/failed)
|
|
368
|
+
* @returns {Promise<boolean>} returns `true` if removed or `false` if not found
|
|
369
|
+
*/
|
|
370
|
+
async remove(task, opts) {
|
|
371
|
+
this.__debug('[remove]', task?.uuid);
|
|
372
|
+
if (!task || typeof task !== 'object' || typeof task.uuid !== 'string') {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const letterKey = this.__getKey(task.uuid, 'letter');
|
|
377
|
+
if (isSendLeaseRemove(opts)) {
|
|
378
|
+
if (typeof this.client.watch !== 'function' || typeof this.client.multi !== 'function') {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
await this.client.watch(letterKey);
|
|
383
|
+
const taskJSON = await this.client.get(letterKey);
|
|
384
|
+
if (!taskJSON) {
|
|
385
|
+
await this.client.unwatch?.();
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
const currentTask = JSON.parse(taskJSON);
|
|
389
|
+
if (currentTask.tries !== opts.leaseTries
|
|
390
|
+
|| currentTask.isSending !== true
|
|
391
|
+
|| (typeof currentTask.sendingAt === 'number' ? currentTask.sendingAt : 0) !== opts.leaseSendingAt
|
|
392
|
+
|| currentTask.isCancelled === true
|
|
393
|
+
|| currentTask.isFailed === true) {
|
|
394
|
+
await this.client.unwatch?.();
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
const keysToDelete = [letterKey, this.__getKey(task.uuid, 'sendat')];
|
|
398
|
+
if (task.to) {
|
|
399
|
+
keysToDelete.push(this.__getKey(task.to, 'concatletter'));
|
|
400
|
+
}
|
|
401
|
+
const multi = this.client.multi();
|
|
402
|
+
for (const key of keysToDelete) {
|
|
403
|
+
multi.del(key);
|
|
404
|
+
}
|
|
405
|
+
const result = await multi.exec();
|
|
406
|
+
return result !== null;
|
|
407
|
+
} catch (opError) {
|
|
408
|
+
logError('[remove] [lease] [opError]', opError);
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const exists = await this.client.exists(letterKey);
|
|
414
|
+
if (!exists) {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const keysToDelete = [letterKey, this.__getKey(task.uuid, 'sendat')];
|
|
419
|
+
if (task.to) {
|
|
420
|
+
keysToDelete.push(this.__getKey(task.to, 'concatletter'));
|
|
421
|
+
}
|
|
422
|
+
await this.client.del(keysToDelete);
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* @async
|
|
428
|
+
* @memberOf RedisQueue
|
|
429
|
+
* @name update
|
|
430
|
+
* @description update task in queue
|
|
431
|
+
* @param task {object} - task's object
|
|
432
|
+
* @param updateObj {object} - fields with new values to update
|
|
433
|
+
* @returns {Promise<boolean>} returns `true` if updated or `false` if not found or no changes was made
|
|
434
|
+
*/
|
|
435
|
+
async update(task, updateObj) {
|
|
436
|
+
this.__debug('[update]', task?.uuid);
|
|
437
|
+
if (!task || typeof task !== 'object' || typeof task.uuid !== 'string' || !updateObj || typeof updateObj !== 'object') {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const letterKey = this.__getKey(task.uuid, 'letter');
|
|
442
|
+
const sendatKey = this.__getKey(task.uuid, 'sendat');
|
|
443
|
+
const isClaim = isSendClaimUpdate(updateObj);
|
|
444
|
+
const isAppend = isAppendMailOptionUpdate(updateObj);
|
|
445
|
+
const isLeaseRelease = isSendLeaseGuardedUpdate(updateObj);
|
|
446
|
+
const now = isClaim && typeof updateObj.sendingAt === 'number' ? updateObj.sendingAt : Date.now();
|
|
447
|
+
const sendingTimeout = this.mailTimeInstance?.sendingTimeout || 300000;
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
if (isAppend) {
|
|
451
|
+
if (typeof this.client.watch !== 'function' || typeof this.client.multi !== 'function') {
|
|
452
|
+
if (!RedisQueue.__atomicAppendWarned) {
|
|
453
|
+
RedisQueue.__atomicAppendWarned = true;
|
|
454
|
+
logError('[update] Redis client without watch()/multi() — concat appendMailOption falls back to non-atomic read-modify-write; concurrent folds into the same row may lose letters');
|
|
455
|
+
}
|
|
456
|
+
const taskJSON = await this.client.get(letterKey);
|
|
457
|
+
if (!taskJSON) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
const currentTask = JSON.parse(taskJSON);
|
|
461
|
+
if (currentTask.isSending === true || currentTask.isSent === true || currentTask.isFailed === true || currentTask.isCancelled === true) {
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
currentTask.mailOptions = [...(currentTask.mailOptions || []), updateObj.appendMailOption];
|
|
465
|
+
await this.client.set(letterKey, JSON.stringify(currentTask));
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
await this.client.watch(letterKey);
|
|
470
|
+
const taskJSON = await this.client.get(letterKey);
|
|
471
|
+
if (!taskJSON) {
|
|
472
|
+
await this.client.unwatch?.();
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
const currentTask = JSON.parse(taskJSON);
|
|
476
|
+
if (currentTask.isSending === true || currentTask.isSent === true || currentTask.isFailed === true || currentTask.isCancelled === true) {
|
|
477
|
+
await this.client.unwatch?.();
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
currentTask.mailOptions = [...(currentTask.mailOptions || []), updateObj.appendMailOption];
|
|
481
|
+
const multi = this.client.multi();
|
|
482
|
+
multi.set(letterKey, JSON.stringify(currentTask));
|
|
483
|
+
const result = await multi.exec();
|
|
484
|
+
return result !== null;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if ((isClaim || isLeaseRelease) && (typeof this.client.watch !== 'function' || typeof this.client.multi !== 'function')) {
|
|
488
|
+
if (isClaim && !RedisQueue.__atomicClaimWarned) {
|
|
489
|
+
RedisQueue.__atomicClaimWarned = true;
|
|
490
|
+
logError('[update] Redis client must support watch() and multi() for atomic send claims');
|
|
491
|
+
}
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (isClaim || isLeaseRelease) {
|
|
496
|
+
await this.client.watch(letterKey);
|
|
497
|
+
const taskJSON = await this.client.get(letterKey);
|
|
498
|
+
if (!taskJSON) {
|
|
499
|
+
await this.client.unwatch?.();
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const currentTask = JSON.parse(taskJSON);
|
|
504
|
+
if (isClaim && !canClaimTask(currentTask, task, now, sendingTimeout)) {
|
|
505
|
+
await this.client.unwatch?.();
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
if (isLeaseRelease && !canReleaseLease(currentTask, updateObj)) {
|
|
509
|
+
await this.client.unwatch?.();
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const updatedTask = { ...currentTask, ...stripInternalUpdateMeta(updateObj) };
|
|
514
|
+
const multi = this.client.multi();
|
|
515
|
+
multi.set(letterKey, JSON.stringify(updatedTask));
|
|
516
|
+
if (updatedTask.isSent === true || updatedTask.isFailed === true || updatedTask.isCancelled === true) {
|
|
517
|
+
multi.del(sendatKey);
|
|
518
|
+
} else if (updatedTask.sendAt) {
|
|
519
|
+
multi.set(sendatKey, `${+updatedTask.sendAt}`);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const result = await multi.exec();
|
|
523
|
+
return result !== null;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const taskJSON = await this.client.get(letterKey);
|
|
527
|
+
if (!taskJSON) {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const currentTask = JSON.parse(taskJSON);
|
|
532
|
+
const updatedTask = { ...currentTask, ...stripInternalUpdateMeta(updateObj) };
|
|
533
|
+
await this.client.set(letterKey, JSON.stringify(updatedTask));
|
|
534
|
+
|
|
535
|
+
if (updatedTask.isSent === true || updatedTask.isFailed === true || updatedTask.isCancelled === true) {
|
|
536
|
+
await this.client.del(sendatKey);
|
|
537
|
+
} else if (updatedTask.sendAt) {
|
|
538
|
+
await this.client.set(sendatKey, `${+updatedTask.sendAt}`);
|
|
539
|
+
}
|
|
540
|
+
return true;
|
|
541
|
+
} catch (opError) {
|
|
542
|
+
logError('[update] [try/catch] [opError]', opError);
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* @internal
|
|
549
|
+
* @memberOf RedisQueue
|
|
550
|
+
* @name __getKey
|
|
551
|
+
* @description helper to generate scoped key
|
|
552
|
+
* @param uuid {string} - letter's uuid (or "to" address for `concatletter` keys)
|
|
553
|
+
* @param type {string} - "letter" or "sendat" or "concatletter"
|
|
554
|
+
* @returns {string} returns key used by Redis
|
|
555
|
+
*/
|
|
556
|
+
__getKey(uuid, type = 'letter') {
|
|
557
|
+
if (!KEY_TYPES.has(type)) {
|
|
558
|
+
throw new Error(`[mail-time] [RedisQueue] [__getKey] unsupported key "${type}" passed into the second argument`);
|
|
559
|
+
}
|
|
560
|
+
this.__ensurePrefix();
|
|
561
|
+
return `${this.uniqueName}:${type}:${uuid}`;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export { RedisQueue };
|