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