mail-time 3.0.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 +325 -609
- package/adapters/mongo.d.ts +118 -0
- package/adapters/mongo.js +199 -78
- package/adapters/postgres.d.ts +100 -0
- package/adapters/postgres.js +520 -0
- package/adapters/redis.d.ts +105 -0
- package/adapters/redis.js +342 -79
- package/helpers.js +209 -3
- package/index.cjs +2219 -446
- package/index.d.cts +300 -0
- package/index.d.ts +300 -0
- package/index.js +817 -305
- package/package.json +66 -17
- package/presets.d.ts +87 -0
- package/presets.js +193 -0
- package/adapters/blank-example.js +0 -230
package/adapters/redis.js
CHANGED
|
@@ -1,16 +1,100 @@
|
|
|
1
|
-
import {
|
|
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
|
+
};
|
|
2
88
|
|
|
3
89
|
/** Class representing Redis Queue for MailTime */
|
|
4
90
|
class RedisQueue {
|
|
5
91
|
/**
|
|
6
92
|
* Create a RedisQueue instance
|
|
7
|
-
* @param {
|
|
8
|
-
* @param {RedisClient} opts.client - Required, Redis'es `RedisClient` instance, like one returned from `await redis.createClient().connect()` method
|
|
9
|
-
* @param {string} [opts.prefix] - Optional prefix for scope isolation; use when creating multiple MailTime instances within the single application
|
|
93
|
+
* @param {RedisQueueOption} opts - configuration object
|
|
10
94
|
*/
|
|
11
95
|
constructor (opts) {
|
|
12
96
|
this.name = 'redis-queue';
|
|
13
|
-
if (!opts || typeof opts !== 'object'
|
|
97
|
+
if (!opts || typeof opts !== 'object') {
|
|
14
98
|
throw new TypeError('[mail-time] Configuration object must be passed into RedisQueue constructor');
|
|
15
99
|
}
|
|
16
100
|
|
|
@@ -18,34 +102,41 @@ class RedisQueue {
|
|
|
18
102
|
throw new Error('[mail-time] [RedisQueue] required {client} option is missing, e.g. returned from `redis.createClient()` or `redis.createCluster()` method');
|
|
19
103
|
}
|
|
20
104
|
|
|
21
|
-
this.prefix = (typeof opts.prefix === 'string') ? opts.prefix : 'default';
|
|
22
|
-
this.uniqueName = `mailtime:${this.prefix}`;
|
|
23
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
|
+
}
|
|
24
123
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// ---
|
|
42
|
-
// mailOptions {[object]} - Array of nodeMailer's `mailOptions`
|
|
43
|
-
// mailOptions.to {string|[string]} - [REQUIRED]
|
|
44
|
-
// mailOptions.from {string}
|
|
45
|
-
// mailOptions.text {string|boolean}
|
|
46
|
-
// mailOptions.html {string}
|
|
47
|
-
// mailOptions.subject {string}
|
|
48
|
-
// mailOptions.Other nodeMailer `sendMail` options...
|
|
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;
|
|
49
140
|
}
|
|
50
141
|
|
|
51
142
|
/**
|
|
@@ -56,6 +147,7 @@ class RedisQueue {
|
|
|
56
147
|
* @returns {Promise<object>}
|
|
57
148
|
*/
|
|
58
149
|
async ping() {
|
|
150
|
+
this.__debug('[ping]');
|
|
59
151
|
if (!this.mailTimeInstance) {
|
|
60
152
|
return {
|
|
61
153
|
status: 'Service Unavailable',
|
|
@@ -79,7 +171,7 @@ class RedisQueue {
|
|
|
79
171
|
status: 'Internal Server Error',
|
|
80
172
|
code: 500,
|
|
81
173
|
statusCode: 500,
|
|
82
|
-
error: pingError
|
|
174
|
+
error: pingError,
|
|
83
175
|
};
|
|
84
176
|
}
|
|
85
177
|
|
|
@@ -87,28 +179,64 @@ class RedisQueue {
|
|
|
87
179
|
status: 'Service Unavailable',
|
|
88
180
|
code: 503,
|
|
89
181
|
statusCode: 503,
|
|
90
|
-
error: new Error('Service Unavailable')
|
|
182
|
+
error: new Error('Service Unavailable'),
|
|
91
183
|
};
|
|
92
184
|
}
|
|
93
185
|
|
|
94
186
|
/**
|
|
95
187
|
* @memberOf RedisQueue
|
|
96
188
|
* @name iterate
|
|
97
|
-
* @description iterate over queued tasks passing to `mailTimeInstance.
|
|
98
|
-
* @
|
|
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>}
|
|
99
192
|
*/
|
|
100
|
-
async iterate() {
|
|
193
|
+
async iterate(opts) {
|
|
194
|
+
this.__debug('[iterate]', opts);
|
|
101
195
|
try {
|
|
102
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');
|
|
103
209
|
const cursor = this.client.scanIterator({
|
|
104
210
|
TYPE: 'string',
|
|
105
|
-
MATCH:
|
|
211
|
+
MATCH: matchPattern,
|
|
106
212
|
COUNT: 9999,
|
|
107
213
|
});
|
|
108
214
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
}
|
|
112
240
|
}
|
|
113
241
|
}
|
|
114
242
|
} catch (iterateError) {
|
|
@@ -126,25 +254,25 @@ class RedisQueue {
|
|
|
126
254
|
* @returns {Promise<object|null>}
|
|
127
255
|
*/
|
|
128
256
|
async getPendingTo(to, sendAt) {
|
|
257
|
+
this.__debug('[getPendingTo]', to, sendAt);
|
|
129
258
|
if (typeof to !== 'string' || typeof sendAt !== 'number') {
|
|
130
259
|
return null;
|
|
131
260
|
}
|
|
132
261
|
|
|
133
262
|
const concatKey = this.__getKey(to, 'concatletter');
|
|
134
|
-
|
|
135
|
-
if (!
|
|
263
|
+
const uuid = await this.client.get(concatKey);
|
|
264
|
+
if (!uuid) {
|
|
136
265
|
return null;
|
|
137
266
|
}
|
|
138
267
|
|
|
139
|
-
const uuid = await this.client.get(concatKey);
|
|
140
268
|
const letterKey = this.__getKey(uuid, 'letter');
|
|
141
|
-
|
|
142
|
-
if (!
|
|
269
|
+
const taskJSON = await this.client.get(letterKey);
|
|
270
|
+
if (!taskJSON) {
|
|
143
271
|
return null;
|
|
144
272
|
}
|
|
145
273
|
|
|
146
|
-
const task = JSON.parse(
|
|
147
|
-
if (!task || task.isSent === true || task.isCancelled === true || task.isFailed === true) {
|
|
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) {
|
|
148
276
|
return null;
|
|
149
277
|
}
|
|
150
278
|
|
|
@@ -160,7 +288,8 @@ class RedisQueue {
|
|
|
160
288
|
* @returns {Promise<void 0>}
|
|
161
289
|
*/
|
|
162
290
|
async push(task) {
|
|
163
|
-
|
|
291
|
+
this.__debug('[push]', task?.uuid);
|
|
292
|
+
if (!task || typeof task !== 'object') {
|
|
164
293
|
return;
|
|
165
294
|
}
|
|
166
295
|
|
|
@@ -168,11 +297,28 @@ class RedisQueue {
|
|
|
168
297
|
task.sendAt = +task.sendAt;
|
|
169
298
|
}
|
|
170
299
|
|
|
171
|
-
|
|
172
|
-
|
|
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}`);
|
|
173
319
|
if (task.to) {
|
|
174
320
|
await this.client.set(this.__getKey(task.to, 'concatletter'), task.uuid, {
|
|
175
|
-
PXAT: task.sendAt - 128
|
|
321
|
+
PXAT: task.sendAt - 128,
|
|
176
322
|
});
|
|
177
323
|
}
|
|
178
324
|
}
|
|
@@ -186,18 +332,19 @@ class RedisQueue {
|
|
|
186
332
|
* @returns {Promise<boolean>} returns `true` if cancelled or `false` if not found, was sent, or was cancelled previously
|
|
187
333
|
*/
|
|
188
334
|
async cancel(uuid) {
|
|
335
|
+
this.__debug('[cancel]', uuid);
|
|
189
336
|
if (typeof uuid !== 'string') {
|
|
190
337
|
return false;
|
|
191
338
|
}
|
|
192
339
|
|
|
193
340
|
await this.client.del(this.__getKey(uuid, 'sendat'));
|
|
194
341
|
const letterKey = this.__getKey(uuid, 'letter');
|
|
195
|
-
const
|
|
196
|
-
if (!
|
|
342
|
+
const taskJSON = await this.client.get(letterKey);
|
|
343
|
+
if (!taskJSON) {
|
|
197
344
|
return false;
|
|
198
345
|
}
|
|
199
346
|
|
|
200
|
-
const task = JSON.parse(
|
|
347
|
+
const task = JSON.parse(taskJSON);
|
|
201
348
|
if (!task || task.isSent === true || task.isCancelled === true) {
|
|
202
349
|
return false;
|
|
203
350
|
}
|
|
@@ -217,23 +364,62 @@ class RedisQueue {
|
|
|
217
364
|
* @name remove
|
|
218
365
|
* @description remove task from queue
|
|
219
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)
|
|
220
368
|
* @returns {Promise<boolean>} returns `true` if removed or `false` if not found
|
|
221
369
|
*/
|
|
222
|
-
async remove(task) {
|
|
223
|
-
|
|
370
|
+
async remove(task, opts) {
|
|
371
|
+
this.__debug('[remove]', task?.uuid);
|
|
372
|
+
if (!task || typeof task !== 'object' || typeof task.uuid !== 'string') {
|
|
224
373
|
return false;
|
|
225
374
|
}
|
|
226
375
|
|
|
227
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
|
+
|
|
228
413
|
const exists = await this.client.exists(letterKey);
|
|
229
414
|
if (!exists) {
|
|
230
415
|
return false;
|
|
231
416
|
}
|
|
232
417
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
this.__getKey(task.
|
|
236
|
-
|
|
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);
|
|
237
423
|
return true;
|
|
238
424
|
}
|
|
239
425
|
|
|
@@ -241,31 +427,115 @@ class RedisQueue {
|
|
|
241
427
|
* @async
|
|
242
428
|
* @memberOf RedisQueue
|
|
243
429
|
* @name update
|
|
244
|
-
* @description
|
|
430
|
+
* @description update task in queue
|
|
245
431
|
* @param task {object} - task's object
|
|
246
432
|
* @param updateObj {object} - fields with new values to update
|
|
247
433
|
* @returns {Promise<boolean>} returns `true` if updated or `false` if not found or no changes was made
|
|
248
434
|
*/
|
|
249
435
|
async update(task, updateObj) {
|
|
250
|
-
|
|
436
|
+
this.__debug('[update]', task?.uuid);
|
|
437
|
+
if (!task || typeof task !== 'object' || typeof task.uuid !== 'string' || !updateObj || typeof updateObj !== 'object') {
|
|
251
438
|
return false;
|
|
252
439
|
}
|
|
253
440
|
|
|
254
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;
|
|
255
448
|
|
|
256
449
|
try {
|
|
257
|
-
|
|
258
|
-
|
|
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
|
+
}
|
|
259
492
|
return false;
|
|
260
493
|
}
|
|
261
|
-
|
|
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) };
|
|
262
533
|
await this.client.set(letterKey, JSON.stringify(updatedTask));
|
|
263
534
|
|
|
264
|
-
const sendatKey = this.__getKey(task.uuid, 'sendat');
|
|
265
535
|
if (updatedTask.isSent === true || updatedTask.isFailed === true || updatedTask.isCancelled === true) {
|
|
266
536
|
await this.client.del(sendatKey);
|
|
267
|
-
} else if (
|
|
268
|
-
await this.client.set(sendatKey, `${+
|
|
537
|
+
} else if (updatedTask.sendAt) {
|
|
538
|
+
await this.client.set(sendatKey, `${+updatedTask.sendAt}`);
|
|
269
539
|
}
|
|
270
540
|
return true;
|
|
271
541
|
} catch (opError) {
|
|
@@ -274,29 +544,22 @@ class RedisQueue {
|
|
|
274
544
|
}
|
|
275
545
|
}
|
|
276
546
|
|
|
277
|
-
|
|
278
547
|
/**
|
|
548
|
+
* @internal
|
|
279
549
|
* @memberOf RedisQueue
|
|
280
550
|
* @name __getKey
|
|
281
551
|
* @description helper to generate scoped key
|
|
282
|
-
* @param uuid {string} - letter's uuid
|
|
552
|
+
* @param uuid {string} - letter's uuid (or "to" address for `concatletter` keys)
|
|
283
553
|
* @param type {string} - "letter" or "sendat" or "concatletter"
|
|
284
554
|
* @returns {string} returns key used by Redis
|
|
285
555
|
*/
|
|
286
556
|
__getKey(uuid, type = 'letter') {
|
|
287
|
-
if (!
|
|
557
|
+
if (!KEY_TYPES.has(type)) {
|
|
288
558
|
throw new Error(`[mail-time] [RedisQueue] [__getKey] unsupported key "${type}" passed into the second argument`);
|
|
289
559
|
}
|
|
560
|
+
this.__ensurePrefix();
|
|
290
561
|
return `${this.uniqueName}:${type}:${uuid}`;
|
|
291
562
|
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* @memberOf RedisQueue
|
|
295
|
-
* @name __keyTypes
|
|
296
|
-
* @description list of supported key type
|
|
297
|
-
* @returns {string} returns key used by Redis
|
|
298
|
-
*/
|
|
299
|
-
__keyTypes = ['letter', 'sendat', 'concatletter'];
|
|
300
563
|
}
|
|
301
564
|
|
|
302
565
|
export { RedisQueue };
|