mail-time 3.0.0 → 4.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.
@@ -0,0 +1,536 @@
1
+ import { createHash } from 'crypto';
2
+ import {
3
+ debug,
4
+ logError,
5
+ isSendClaimUpdate,
6
+ isSendLeaseGuardedUpdate,
7
+ isAppendMailOptionUpdate,
8
+ stripInternalUpdateMeta,
9
+ isSendLeaseRemove,
10
+ } from '../helpers.js';
11
+
12
+ /**
13
+ * @typedef {object} PostgresQueryResult
14
+ * @property {number | null | undefined} [rowCount]
15
+ * @property {unknown[]} [rows]
16
+ */
17
+
18
+ /**
19
+ * @typedef {object} PostgresClient
20
+ * @property {(queryText: string, values?: unknown[]) => Promise<PostgresQueryResult>} query
21
+ */
22
+
23
+ /**
24
+ * @typedef {object} PostgresQueueOption
25
+ * @property {PostgresClient} client
26
+ * @property {string} [prefix]
27
+ */
28
+
29
+ const DEFAULT_PREFIX = 'default';
30
+
31
+ // Two-key advisory lock, mirroring josk's PostgresAdapter: a stable MailTime namespace
32
+ // plus a per-prefix hash. `pg_advisory_lock(int4, int4)` lives in its own keyspace, isolated
33
+ // from single-int callers in the same database; distinct prefixes get distinct lock IDs, so
34
+ // co-tenant MailTime queues no longer serialize each other's schema setup.
35
+ const ADVISORY_LOCK_NAMESPACE = 0x4D61696C; // 'Mail' in ASCII as int32
36
+
37
+ /**
38
+ * @internal
39
+ * @param {string} prefix
40
+ * @returns {number} signed int32 hash of the prefix string
41
+ */
42
+ const advisoryLockKeyFor = (prefix) => {
43
+ return createHash('sha256').update(prefix).digest().readInt32BE(0);
44
+ };
45
+
46
+ const fieldMap = {
47
+ to: 'to_address',
48
+ tries: 'tries',
49
+ sendAt: 'send_at',
50
+ isSent: 'is_sent',
51
+ isCancelled: 'is_cancelled',
52
+ isFailed: 'is_failed',
53
+ isSending: 'is_sending',
54
+ sendingAt: 'sending_at',
55
+ template: 'template',
56
+ transport: 'transport',
57
+ concatSubject: 'concat_subject',
58
+ mailOptions: 'mail_options',
59
+ };
60
+
61
+ const parseMailOptions = (mailOptions) => {
62
+ if (typeof mailOptions === 'string') {
63
+ return JSON.parse(mailOptions);
64
+ }
65
+ return mailOptions;
66
+ };
67
+
68
+ const normalizeRow = (row) => {
69
+ if (!row) {
70
+ return null;
71
+ }
72
+
73
+ return {
74
+ id: row.id,
75
+ uuid: row.uuid,
76
+ to: row.to_address,
77
+ tries: parseInt(row.tries, 10),
78
+ sendAt: parseInt(row.send_at, 10),
79
+ isSent: row.is_sent,
80
+ isCancelled: row.is_cancelled,
81
+ isFailed: row.is_failed,
82
+ isSending: row.is_sending === true,
83
+ sendingAt: row.sending_at !== null && row.sending_at !== undefined ? parseInt(row.sending_at, 10) : 0,
84
+ template: row.template || false,
85
+ transport: parseInt(row.transport, 10),
86
+ concatSubject: row.concat_subject || false,
87
+ mailOptions: parseMailOptions(row.mail_options),
88
+ };
89
+ };
90
+
91
+ /** Class representing PostgreSQL Queue for MailTime */
92
+ class PostgresQueue {
93
+ /**
94
+ * Create a PostgresQueue instance
95
+ * @param {PostgresQueueOption} opts - configuration object
96
+ */
97
+ constructor(opts) {
98
+ this.name = 'postgres-queue';
99
+ if (!opts || typeof opts !== 'object') {
100
+ throw new TypeError('[mail-time] Configuration object must be passed into PostgresQueue constructor');
101
+ }
102
+
103
+ if (!opts.client || typeof opts.client.query !== 'function') {
104
+ throw new Error('[mail-time] [PostgresQueue] required {client} option is missing or does not expose a `query` method');
105
+ }
106
+
107
+ this.client = opts.client;
108
+ if (typeof opts.prefix === 'string' && opts.prefix.length > 0) {
109
+ this.__applyPrefix(opts.prefix);
110
+ }
111
+ }
112
+
113
+ /** @internal */
114
+ __applyPrefix(prefix) {
115
+ this.prefix = prefix;
116
+ this.__readyPromise = this.__setup();
117
+ this.__readyPromise.catch(() => void 0);
118
+ }
119
+
120
+ /** @internal */
121
+ __ensurePrefix() {
122
+ if (typeof this.prefix !== 'string') {
123
+ this.__applyPrefix(this.mailTimeInstance?.prefix || DEFAULT_PREFIX);
124
+ }
125
+ }
126
+
127
+ /** @internal */
128
+ __debug(...args) {
129
+ debug(this.mailTimeInstance?.debug === true, `[${this.name}]`, ...args);
130
+ }
131
+
132
+ /**
133
+ * @async
134
+ * @memberOf PostgresQueue
135
+ * @name ready
136
+ * @description Wait until PostgreSQL schema is ready
137
+ * @returns {Promise<void 0>}
138
+ */
139
+ async ready() {
140
+ this.__ensurePrefix();
141
+ this.__debug('[ready]');
142
+ await this.__readyPromise;
143
+ }
144
+
145
+ /** @internal */
146
+ async __setup() {
147
+ const advisoryLockKey = advisoryLockKeyFor(this.prefix);
148
+ await this.client.query('SELECT pg_advisory_lock($1, $2)', [ADVISORY_LOCK_NAMESPACE, advisoryLockKey]);
149
+
150
+ try {
151
+ await this.client.query(`CREATE TABLE IF NOT EXISTS mail_time_queue (
152
+ id BIGSERIAL PRIMARY KEY,
153
+ prefix TEXT NOT NULL DEFAULT 'default',
154
+ uuid TEXT NOT NULL,
155
+ to_address TEXT,
156
+ tries INTEGER NOT NULL DEFAULT 0,
157
+ send_at BIGINT NOT NULL,
158
+ is_sent BOOLEAN NOT NULL DEFAULT false,
159
+ is_cancelled BOOLEAN NOT NULL DEFAULT false,
160
+ is_failed BOOLEAN NOT NULL DEFAULT false,
161
+ is_sending BOOLEAN NOT NULL DEFAULT false,
162
+ sending_at BIGINT NOT NULL DEFAULT 0,
163
+ template TEXT,
164
+ transport INTEGER NOT NULL DEFAULT 0,
165
+ concat_subject TEXT,
166
+ mail_options JSONB NOT NULL,
167
+ created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
168
+ updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
169
+ )`);
170
+ await this.client.query(`CREATE UNIQUE INDEX IF NOT EXISTS idx_mail_time_queue_prefix_uuid
171
+ ON mail_time_queue (prefix, uuid)`);
172
+
173
+ await this.client.query(`CREATE INDEX IF NOT EXISTS idx_mail_time_queue_due
174
+ ON mail_time_queue (prefix, is_sent, is_failed, is_cancelled, is_sending, sending_at, send_at, tries)`);
175
+
176
+ await this.client.query(`CREATE INDEX IF NOT EXISTS idx_mail_time_queue_pending_to
177
+ ON mail_time_queue (prefix, to_address, is_sent, is_failed, is_cancelled, send_at)`);
178
+ } finally {
179
+ await this.client.query('SELECT pg_advisory_unlock($1, $2)', [ADVISORY_LOCK_NAMESPACE, advisoryLockKey]);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * @async
185
+ * @memberOf PostgresQueue
186
+ * @name ping
187
+ * @description Check connection to Storage
188
+ * @returns {Promise<object>}
189
+ */
190
+ async ping() {
191
+ this.__debug('[ping]');
192
+ if (!this.mailTimeInstance) {
193
+ return {
194
+ status: 'Service Unavailable',
195
+ code: 503,
196
+ statusCode: 503,
197
+ error: new Error('MailTime instance not yet assigned to {mailTimeInstance} of Queue Adapter context'),
198
+ };
199
+ }
200
+
201
+ try {
202
+ await this.ready();
203
+ const ping = await this.client.query('SELECT 1 as ping');
204
+ if (ping?.rows?.[0]?.ping === 1) {
205
+ return {
206
+ status: 'OK',
207
+ code: 200,
208
+ statusCode: 200,
209
+ };
210
+ }
211
+ } catch (pingError) {
212
+ return {
213
+ status: 'Internal Server Error',
214
+ code: 500,
215
+ statusCode: 500,
216
+ error: pingError,
217
+ };
218
+ }
219
+
220
+ return {
221
+ status: 'Service Unavailable',
222
+ code: 503,
223
+ statusCode: 503,
224
+ error: new Error('Service Unavailable'),
225
+ };
226
+ }
227
+
228
+ /**
229
+ * @memberOf PostgresQueue
230
+ * @name iterate
231
+ * @description iterate over queued tasks passing each to `mailTimeInstance.___dispatch` (the bounded send pool). Postgres reads buffer the full result, so each tick is bounded by `opts.limit` (or 1000 when caller passes `Infinity` / no limit) to keep memory predictable; high-throughput deployments should shard prefixes.
232
+ * @param {{ limit?: number, sendingTimeout?: number }} [opts] - iteration options
233
+ * @returns {Promise<void>}
234
+ */
235
+ async iterate(opts) {
236
+ this.__debug('[iterate]', opts);
237
+ if (!this.mailTimeInstance) {
238
+ return;
239
+ }
240
+ await this.ready();
241
+
242
+ const now = Date.now();
243
+ const sendingTimeout = (opts && typeof opts.sendingTimeout === 'number' && opts.sendingTimeout > 0)
244
+ ? opts.sendingTimeout
245
+ : 300000;
246
+ const limit = (opts && typeof opts.limit === 'number' && Number.isFinite(opts.limit) && opts.limit > 0)
247
+ ? Math.max(1, Math.floor(opts.limit))
248
+ : 1000;
249
+
250
+ try {
251
+ const res = await this.client.query(`SELECT id, uuid, to_address, tries, send_at, is_sent, is_cancelled, is_failed,
252
+ is_sending, sending_at, template, transport, concat_subject, mail_options
253
+ FROM mail_time_queue
254
+ WHERE prefix = $1
255
+ AND is_sent = false
256
+ AND is_failed = false
257
+ AND is_cancelled = false
258
+ AND send_at <= $2
259
+ AND tries < $3
260
+ AND (is_sending = false OR sending_at <= $4)
261
+ ORDER BY send_at ASC
262
+ LIMIT $5`, [this.prefix, now, this.mailTimeInstance.maxTries, now - sendingTimeout, limit]);
263
+
264
+ for (const row of res.rows || []) {
265
+ await this.mailTimeInstance.___dispatch(normalizeRow(row));
266
+ }
267
+ } catch (iterateError) {
268
+ logError('[PostgresQueue] [iterate] [iterateError]', iterateError);
269
+ }
270
+ }
271
+
272
+ /**
273
+ * @async
274
+ * @memberOf PostgresQueue
275
+ * @name getPendingTo
276
+ * @description get queued task by `to` field (addressee)
277
+ * @param to {string} - email address
278
+ * @param sendAt {number} - timestamp
279
+ * @returns {Promise<object|null>}
280
+ */
281
+ async getPendingTo(to, sendAt) {
282
+ this.__debug('[getPendingTo]', to, sendAt);
283
+ if (typeof to !== 'string' || typeof sendAt !== 'number') {
284
+ return null;
285
+ }
286
+
287
+ await this.ready();
288
+
289
+ const res = await this.client.query(`SELECT id, uuid, to_address, tries, send_at, is_sent, is_cancelled, is_failed,
290
+ is_sending, sending_at, template, transport, concat_subject, mail_options
291
+ FROM mail_time_queue
292
+ WHERE prefix = $1
293
+ AND to_address = $2
294
+ AND is_sent = false
295
+ AND is_failed = false
296
+ AND is_cancelled = false
297
+ AND is_sending = false
298
+ AND tries < $4
299
+ AND send_at <= $3
300
+ ORDER BY send_at DESC
301
+ LIMIT 1`, [this.prefix, to, sendAt, this.mailTimeInstance.maxTries]);
302
+
303
+ return normalizeRow(res.rows?.[0]);
304
+ }
305
+
306
+ /**
307
+ * @async
308
+ * @memberOf PostgresQueue
309
+ * @name push
310
+ * @description push task to the queue/storage
311
+ * @param task {object} - task's object
312
+ * @returns {Promise<void 0>}
313
+ */
314
+ async push(task) {
315
+ this.__debug('[push]', task?.uuid);
316
+ if (!task || typeof task !== 'object') {
317
+ return;
318
+ }
319
+
320
+ await this.ready();
321
+
322
+ if (task.sendAt instanceof Date) {
323
+ task.sendAt = +task.sendAt;
324
+ }
325
+
326
+ await this.client.query(`INSERT INTO mail_time_queue (
327
+ prefix, uuid, to_address, tries, send_at, is_sent, is_cancelled, is_failed,
328
+ is_sending, sending_at, template, transport, concat_subject, mail_options, created_at, updated_at
329
+ )
330
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
331
+ ON CONFLICT (prefix, uuid) DO UPDATE SET
332
+ to_address = EXCLUDED.to_address,
333
+ tries = EXCLUDED.tries,
334
+ send_at = EXCLUDED.send_at,
335
+ is_sent = EXCLUDED.is_sent,
336
+ is_cancelled = EXCLUDED.is_cancelled,
337
+ is_failed = EXCLUDED.is_failed,
338
+ is_sending = EXCLUDED.is_sending,
339
+ sending_at = EXCLUDED.sending_at,
340
+ template = EXCLUDED.template,
341
+ transport = EXCLUDED.transport,
342
+ concat_subject = EXCLUDED.concat_subject,
343
+ mail_options = EXCLUDED.mail_options,
344
+ updated_at = CURRENT_TIMESTAMP`, [
345
+ this.prefix,
346
+ task.uuid,
347
+ typeof task.to === 'string' ? task.to : null,
348
+ task.tries,
349
+ task.sendAt,
350
+ task.isSent,
351
+ task.isCancelled,
352
+ task.isFailed,
353
+ task.isSending === true,
354
+ typeof task.sendingAt === 'number' ? task.sendingAt : 0,
355
+ task.template || null,
356
+ task.transport,
357
+ task.concatSubject || null,
358
+ JSON.stringify(task.mailOptions || []),
359
+ ]);
360
+ }
361
+
362
+ /**
363
+ * @async
364
+ * @memberOf PostgresQueue
365
+ * @name cancel
366
+ * @description cancel scheduled email
367
+ * @param uuid {string} - email's uuid
368
+ * @returns {Promise<boolean>} returns `true` if cancelled or `false` if not found, was sent, or was cancelled previously
369
+ */
370
+ async cancel(uuid) {
371
+ this.__debug('[cancel]', uuid);
372
+ if (typeof uuid !== 'string') {
373
+ return false;
374
+ }
375
+
376
+ await this.ready();
377
+
378
+ const task = normalizeRow((await this.client.query(`SELECT id, uuid, to_address, tries, send_at, is_sent, is_cancelled, is_failed,
379
+ template, transport, concat_subject, mail_options
380
+ FROM mail_time_queue
381
+ WHERE prefix = $1
382
+ AND uuid = $2
383
+ LIMIT 1`, [this.prefix, uuid])).rows?.[0]);
384
+
385
+ if (!task || task.isSent === true || task.isCancelled === true) {
386
+ return false;
387
+ }
388
+
389
+ if (!this.mailTimeInstance.keepHistory) {
390
+ return await this.remove(task);
391
+ }
392
+
393
+ return await this.update(task, {
394
+ isCancelled: true,
395
+ });
396
+ }
397
+
398
+ /**
399
+ * @async
400
+ * @memberOf PostgresQueue
401
+ * @name remove
402
+ * @description remove task from queue
403
+ * @param task {object} - task's object
404
+ * @param {{ leaseTries: number, leaseSendingAt: number }} [opts] - lease guard: only remove if this worker still holds the lease (tries + sendingAt match, row not cancelled/failed)
405
+ * @returns {Promise<boolean>} returns `true` if removed or `false` if not found
406
+ */
407
+ async remove(task, opts) {
408
+ this.__debug('[remove]', task?.uuid);
409
+ if (!task || typeof task !== 'object') {
410
+ return false;
411
+ }
412
+
413
+ await this.ready();
414
+
415
+ const where = task.id ? 'id = $2' : 'uuid = $2';
416
+ const value = task.id || task.uuid;
417
+ let leaseWhere = '';
418
+ const params = [this.prefix, value];
419
+ if (isSendLeaseRemove(opts)) {
420
+ params.push(opts.leaseTries, opts.leaseSendingAt);
421
+ leaseWhere = ` AND tries = $3 AND is_sending = true AND sending_at = $4 AND is_cancelled = false AND is_failed = false`;
422
+ }
423
+
424
+ const res = await this.client.query(`DELETE FROM mail_time_queue
425
+ WHERE prefix = $1
426
+ AND ${where}${leaseWhere}`, params);
427
+ return (res.rowCount || 0) >= 1;
428
+ }
429
+
430
+ /**
431
+ * @async
432
+ * @memberOf PostgresQueue
433
+ * @name update
434
+ * @description update task in queue
435
+ * @param task {object} - task's object
436
+ * @param updateObj {object} - fields with new values to update
437
+ * @returns {Promise<boolean>} returns `true` if updated or `false` if not found or no changes was made
438
+ */
439
+ async update(task, updateObj) {
440
+ this.__debug('[update]', task?.uuid);
441
+ if (!task || typeof task !== 'object' || !updateObj || typeof updateObj !== 'object') {
442
+ return false;
443
+ }
444
+
445
+ await this.ready();
446
+
447
+ if (isAppendMailOptionUpdate(updateObj)) {
448
+ const where = task.id ? 'id = $3' : 'uuid = $3';
449
+ const value = task.id || task.uuid;
450
+ const res = await this.client.query(`UPDATE mail_time_queue
451
+ SET mail_options = mail_options || $1::jsonb,
452
+ updated_at = CURRENT_TIMESTAMP
453
+ WHERE prefix = $2
454
+ AND ${where}
455
+ AND is_sent = false
456
+ AND is_failed = false
457
+ AND is_cancelled = false
458
+ AND is_sending = false`, [
459
+ JSON.stringify([updateObj.appendMailOption]),
460
+ this.prefix,
461
+ value,
462
+ ]);
463
+ return (res.rowCount || 0) >= 1;
464
+ }
465
+
466
+ const persistObj = stripInternalUpdateMeta(updateObj);
467
+ const sets = [];
468
+ const values = [];
469
+ for (const key of Object.keys(persistObj)) {
470
+ if (!fieldMap[key]) {
471
+ continue;
472
+ }
473
+
474
+ let value = persistObj[key];
475
+ if (key === 'sendAt' && value instanceof Date) {
476
+ value = +value;
477
+ }
478
+ if (key === 'mailOptions') {
479
+ value = JSON.stringify(value);
480
+ }
481
+
482
+ values.push(value);
483
+ sets.push(`${fieldMap[key]} = $${values.length}`);
484
+ }
485
+
486
+ if (!sets.length) {
487
+ return false;
488
+ }
489
+
490
+ let claimWhere = '';
491
+ if (isSendClaimUpdate(updateObj)) {
492
+ const now = typeof updateObj.sendingAt === 'number' ? updateObj.sendingAt : Date.now();
493
+ const sendingTimeout = this.mailTimeInstance?.sendingTimeout || 300000;
494
+ values.push(task.tries);
495
+ const triesIndex = values.length;
496
+ values.push(now - sendingTimeout);
497
+ const staleIndex = values.length;
498
+ claimWhere = `
499
+ AND is_sent = false
500
+ AND is_failed = false
501
+ AND is_cancelled = false
502
+ AND tries = $${triesIndex}
503
+ AND (is_sending = false OR sending_at <= $${staleIndex})
504
+ `;
505
+ } else if (isSendLeaseGuardedUpdate(updateObj)) {
506
+ values.push(updateObj.leaseTries);
507
+ const triesIndex = values.length;
508
+ values.push(updateObj.leaseSendingAt);
509
+ const sendingAtIndex = values.length;
510
+ claimWhere = `
511
+ AND tries = $${triesIndex}
512
+ AND is_sending = true
513
+ AND sending_at = $${sendingAtIndex}
514
+ AND is_cancelled = false
515
+ AND is_failed = false
516
+ `;
517
+ }
518
+
519
+ values.push(this.prefix);
520
+ const prefixIndex = values.length;
521
+ values.push(task.id || task.uuid);
522
+ const taskIndex = values.length;
523
+ const where = task.id ? 'id' : 'uuid';
524
+
525
+ const res = await this.client.query(`UPDATE mail_time_queue
526
+ SET ${sets.join(', ')},
527
+ updated_at = CURRENT_TIMESTAMP
528
+ WHERE prefix = $${prefixIndex}
529
+ AND ${where} = $${taskIndex}
530
+ ${claimWhere}`, values);
531
+
532
+ return (res.rowCount || 0) >= 1;
533
+ }
534
+ }
535
+
536
+ export { PostgresQueue };
@@ -0,0 +1,105 @@
1
+ export type RedisClient = {
2
+ exists: (key: string) => Promise<number>;
3
+ get: (key: string) => Promise<string | null>;
4
+ set: (key: string, value: string, options?: object) => Promise<unknown>;
5
+ del: (key: string | string[]) => Promise<number>;
6
+ ping: () => Promise<string>;
7
+ scanIterator: (options: object) => AsyncIterable<string | string[]>;
8
+ watch?: ((key: string) => Promise<unknown>) | undefined;
9
+ unwatch?: (() => Promise<unknown>) | undefined;
10
+ multi?: (() => object) | undefined;
11
+ };
12
+ export type RedisQueueOption = {
13
+ client: RedisClient;
14
+ prefix?: string | undefined;
15
+ };
16
+ /** Class representing Redis Queue for MailTime */
17
+ export class RedisQueue {
18
+ /**
19
+ * Create a RedisQueue instance
20
+ * @param {RedisQueueOption} opts - configuration object
21
+ */
22
+ constructor(opts: RedisQueueOption);
23
+ name: string;
24
+ client: RedisClient;
25
+ prefix: any;
26
+ uniqueName: string | undefined;
27
+ /**
28
+ * @async
29
+ * @memberOf RedisQueue
30
+ * @name ready
31
+ * @description Storage adapter has no async setup
32
+ * @returns {Promise<void 0>}
33
+ */
34
+ ready(): Promise<void>;
35
+ /**
36
+ * @async
37
+ * @memberOf RedisQueue
38
+ * @name ping
39
+ * @description Check connection to Storage
40
+ * @returns {Promise<object>}
41
+ */
42
+ ping(): Promise<object>;
43
+ /**
44
+ * @memberOf RedisQueue
45
+ * @name iterate
46
+ * @description iterate over queued tasks passing each to `mailTimeInstance.___dispatch` (the bounded send pool)
47
+ * @param {{ limit?: number, sendingTimeout?: number }} [opts] - iteration options
48
+ * @returns {Promise<void>}
49
+ */
50
+ iterate(opts?: {
51
+ limit?: number;
52
+ sendingTimeout?: number;
53
+ }): Promise<void>;
54
+ /**
55
+ * @async
56
+ * @memberOf RedisQueue
57
+ * @name getPendingTo
58
+ * @description get queued task by `to` field (addressee)
59
+ * @param to {string} - email address
60
+ * @param sendAt {number} - timestamp
61
+ * @returns {Promise<object|null>}
62
+ */
63
+ getPendingTo(to: string, sendAt: number): Promise<object | null>;
64
+ /**
65
+ * @async
66
+ * @memberOf RedisQueue
67
+ * @name push
68
+ * @description push task to the queue/storage
69
+ * @param task {object} - task's object
70
+ * @returns {Promise<void 0>}
71
+ */
72
+ push(task: object): Promise<void>;
73
+ /**
74
+ * @async
75
+ * @memberOf RedisQueue
76
+ * @name cancel
77
+ * @description cancel scheduled email
78
+ * @param uuid {string} - email's uuid
79
+ * @returns {Promise<boolean>} returns `true` if cancelled or `false` if not found, was sent, or was cancelled previously
80
+ */
81
+ cancel(uuid: string): Promise<boolean>;
82
+ /**
83
+ * @async
84
+ * @memberOf RedisQueue
85
+ * @name remove
86
+ * @description remove task from queue
87
+ * @param task {object} - task's object
88
+ * @param {{ leaseTries: number, leaseSendingAt: number }} [opts] - lease guard: only remove if this worker still holds the lease (tries + sendingAt match, row not cancelled/failed)
89
+ * @returns {Promise<boolean>} returns `true` if removed or `false` if not found
90
+ */
91
+ remove(task: object, opts?: {
92
+ leaseTries: number;
93
+ leaseSendingAt: number;
94
+ }): Promise<boolean>;
95
+ /**
96
+ * @async
97
+ * @memberOf RedisQueue
98
+ * @name update
99
+ * @description update task in queue
100
+ * @param task {object} - task's object
101
+ * @param updateObj {object} - fields with new values to update
102
+ * @returns {Promise<boolean>} returns `true` if updated or `false` if not found or no changes was made
103
+ */
104
+ update(task: object, updateObj: object): Promise<boolean>;
105
+ }