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/adapters/redis.js CHANGED
@@ -1,16 +1,100 @@
1
- import { logError } from '../helpers.js';
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 {object} opts - configuration object
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' || opts === null) {
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
- // 3 types of keys are stored in Redis:
26
- // 'letter' - JSON with email details
27
- // 'sendat' - Timestamp used to scan/find/iterate over scheduled emails
28
- // 'concatletter' — uuid of "pending" email for concatenation used when {concatEmails: true}
29
-
30
- // Stored JSON structure:
31
- // to {string|[string]}
32
- // uuid {string}
33
- // tries {number} - qty of send attempts
34
- // sendAt {number} - When letter should be sent
35
- // isSent {boolean} - Email status
36
- // isCancelled {boolean} - `true` if email was cancelled before it was sent
37
- // isFailed {boolean} - `true` if email has failed to send
38
- // template {string} - Template for this email
39
- // transport {number} - Last used transport
40
- // concatSubject {string|boolean} - Email concatenation subject
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.___send` method
98
- * @returns {void 0}
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: this.__getKey('*', 'sendat'),
211
+ MATCH: matchPattern,
106
212
  COUNT: 9999,
107
213
  });
108
214
 
109
- for await (const sendatKey of cursor) {
110
- if (parseInt(await this.client.get(sendatKey)) <= now) {
111
- await this.mailTimeInstance.___send(JSON.parse(await this.client.get(this.__getKey(sendatKey.split(':')[3]))));
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
- let exists = await this.client.exists(concatKey);
135
- if (!exists) {
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
- exists = await this.client.exists(letterKey);
142
- if (!exists) {
269
+ const taskJSON = await this.client.get(letterKey);
270
+ if (!taskJSON) {
143
271
  return null;
144
272
  }
145
273
 
146
- const task = JSON.parse(await this.client.get(letterKey));
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
- if (typeof task !== 'object') {
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
- await this.client.set(this.__getKey(task.uuid, 'letter'), JSON.stringify(task));
172
- await this.client.set(this.__getKey(task.uuid, 'sendat'), `${task.sendAt}`);
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 exists = await this.client.exists(letterKey);
196
- if (!exists) {
342
+ const taskJSON = await this.client.get(letterKey);
343
+ if (!taskJSON) {
197
344
  return false;
198
345
  }
199
346
 
200
- const task = JSON.parse(await this.client.get(letterKey));
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
- if (typeof task !== 'object' || typeof task.uuid !== 'string') {
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
- await this.client.del([
234
- letterKey,
235
- this.__getKey(task.uuid, 'sendat'),
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 remove task from queue
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
- if (typeof task !== 'object' || typeof task.uuid !== 'string' || typeof updateObj !== 'object') {
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
- const exists = await this.client.exists(letterKey);
258
- if (!exists) {
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
- const updatedTask = { ...task, ...updateObj };
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 (task.sendAt) {
268
- await this.client.set(sendatKey, `${+task.sendAt}`);
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 (!this.__keyTypes.includes(type)) {
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 };