mail-time 1.4.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +367 -301
- package/adapters/mongo.d.ts +118 -0
- package/adapters/mongo.js +431 -0
- package/adapters/postgres.d.ts +100 -0
- package/adapters/postgres.js +520 -0
- package/adapters/redis.d.ts +105 -0
- package/adapters/redis.js +565 -0
- package/helpers.js +217 -0
- package/index.cjs +2862 -462
- package/index.d.cts +300 -0
- package/index.d.ts +300 -0
- package/index.js +1002 -475
- package/package.json +66 -16
- package/presets.d.ts +87 -0
- package/presets.js +193 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export type MongoCollection = {
|
|
2
|
+
collectionName?: string | undefined;
|
|
3
|
+
createIndex: (keys: object, opts?: object) => Promise<unknown>;
|
|
4
|
+
indexes: () => Promise<{
|
|
5
|
+
name: string;
|
|
6
|
+
key: Record<string, unknown>;
|
|
7
|
+
}[]>;
|
|
8
|
+
dropIndex: (name: string) => Promise<unknown>;
|
|
9
|
+
find: (query: object, opts?: object) => unknown;
|
|
10
|
+
findOne: (query: object, opts?: object) => Promise<object | null>;
|
|
11
|
+
insertOne: (doc: object) => Promise<unknown>;
|
|
12
|
+
deleteOne: (query: object) => Promise<{
|
|
13
|
+
deletedCount?: number;
|
|
14
|
+
}>;
|
|
15
|
+
updateOne: (query: object, update: object) => Promise<{
|
|
16
|
+
modifiedCount?: number;
|
|
17
|
+
}>;
|
|
18
|
+
};
|
|
19
|
+
export type Db = {
|
|
20
|
+
collection: (name: string) => MongoCollection;
|
|
21
|
+
command: (cmd: object) => Promise<{
|
|
22
|
+
ok?: number;
|
|
23
|
+
}>;
|
|
24
|
+
};
|
|
25
|
+
export type MongoQueueOption = {
|
|
26
|
+
db: Db;
|
|
27
|
+
prefix?: string | undefined;
|
|
28
|
+
};
|
|
29
|
+
/** Class representing MongoDB Queue for MailTime */
|
|
30
|
+
export class MongoQueue {
|
|
31
|
+
/**
|
|
32
|
+
* Create a MongoQueue instance
|
|
33
|
+
* @param {MongoQueueOption} opts - configuration object
|
|
34
|
+
*/
|
|
35
|
+
constructor(opts: MongoQueueOption);
|
|
36
|
+
name: string;
|
|
37
|
+
db: Db;
|
|
38
|
+
prefix: any;
|
|
39
|
+
collection: MongoCollection | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* @async
|
|
42
|
+
* @memberOf MongoQueue
|
|
43
|
+
* @name ready
|
|
44
|
+
* @description Wait until indexes are created
|
|
45
|
+
* @returns {Promise<void 0>}
|
|
46
|
+
*/
|
|
47
|
+
ready(): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* @async
|
|
50
|
+
* @memberOf MongoQueue
|
|
51
|
+
* @name ping
|
|
52
|
+
* @description Check connection to Storage
|
|
53
|
+
* @returns {Promise<object>}
|
|
54
|
+
*/
|
|
55
|
+
ping(): Promise<object>;
|
|
56
|
+
/**
|
|
57
|
+
* @memberOf MongoQueue
|
|
58
|
+
* @name iterate
|
|
59
|
+
* @description iterate over queued tasks passing each to `mailTimeInstance.___dispatch` (the bounded send pool)
|
|
60
|
+
* @param {{ limit?: number, sendingTimeout?: number }} [opts] - iteration options
|
|
61
|
+
* @returns {Promise<void>}
|
|
62
|
+
*/
|
|
63
|
+
iterate(opts?: {
|
|
64
|
+
limit?: number;
|
|
65
|
+
sendingTimeout?: number;
|
|
66
|
+
}): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* @async
|
|
69
|
+
* @memberOf MongoQueue
|
|
70
|
+
* @name getPendingTo
|
|
71
|
+
* @description get queued task by `to` field (addressee)
|
|
72
|
+
* @param to {string} - email address
|
|
73
|
+
* @param sendAt {number} - timestamp
|
|
74
|
+
* @returns {Promise<object|null>}
|
|
75
|
+
*/
|
|
76
|
+
getPendingTo(to: string, sendAt: number): Promise<object | null>;
|
|
77
|
+
/**
|
|
78
|
+
* @async
|
|
79
|
+
* @memberOf MongoQueue
|
|
80
|
+
* @name push
|
|
81
|
+
* @description push task to the queue/storage
|
|
82
|
+
* @param task {object} - task's object
|
|
83
|
+
* @returns {Promise<void 0>}
|
|
84
|
+
*/
|
|
85
|
+
push(task: object): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* @async
|
|
88
|
+
* @memberOf MongoQueue
|
|
89
|
+
* @name cancel
|
|
90
|
+
* @description cancel scheduled email
|
|
91
|
+
* @param uuid {string} - email's uuid
|
|
92
|
+
* @returns {Promise<boolean>} returns `true` if cancelled or `false` if not found, was sent, or was cancelled previously
|
|
93
|
+
*/
|
|
94
|
+
cancel(uuid: string): Promise<boolean>;
|
|
95
|
+
/**
|
|
96
|
+
* @async
|
|
97
|
+
* @memberOf MongoQueue
|
|
98
|
+
* @name remove
|
|
99
|
+
* @description remove task from queue
|
|
100
|
+
* @param task {object} - task's object
|
|
101
|
+
* @param {{ leaseTries: number, leaseSendingAt: number }} [opts] - lease guard: only remove if this worker still holds the lease (tries + sendingAt match, row not cancelled/failed)
|
|
102
|
+
* @returns {Promise<boolean>} returns `true` if removed or `false` if not found
|
|
103
|
+
*/
|
|
104
|
+
remove(task: object, opts?: {
|
|
105
|
+
leaseTries: number;
|
|
106
|
+
leaseSendingAt: number;
|
|
107
|
+
}): Promise<boolean>;
|
|
108
|
+
/**
|
|
109
|
+
* @async
|
|
110
|
+
* @memberOf MongoQueue
|
|
111
|
+
* @name update
|
|
112
|
+
* @description update task in queue
|
|
113
|
+
* @param task {object} - task's object
|
|
114
|
+
* @param updateObj {object} - fields with new values to update
|
|
115
|
+
* @returns {Promise<boolean>} returns `true` if updated or `false` if not found or no changes was made
|
|
116
|
+
*/
|
|
117
|
+
update(task: object, updateObj: object): Promise<boolean>;
|
|
118
|
+
}
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import {
|
|
2
|
+
debug,
|
|
3
|
+
logError,
|
|
4
|
+
isSendClaimUpdate,
|
|
5
|
+
isSendLeaseGuardedUpdate,
|
|
6
|
+
isAppendMailOptionUpdate,
|
|
7
|
+
stripInternalUpdateMeta,
|
|
8
|
+
isSendLeaseRemove,
|
|
9
|
+
} from '../helpers.js';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_PREFIX = '';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} MongoCollection
|
|
15
|
+
* @property {string} [collectionName]
|
|
16
|
+
* @property {(keys: object, opts?: object) => Promise<unknown>} createIndex
|
|
17
|
+
* @property {() => Promise<{ name: string, key: Record<string, unknown> }[]>} indexes
|
|
18
|
+
* @property {(name: string) => Promise<unknown>} dropIndex
|
|
19
|
+
* @property {(query: object, opts?: object) => unknown} find
|
|
20
|
+
* @property {(query: object, opts?: object) => Promise<object|null>} findOne
|
|
21
|
+
* @property {(doc: object) => Promise<unknown>} insertOne
|
|
22
|
+
* @property {(query: object) => Promise<{ deletedCount?: number }>} deleteOne
|
|
23
|
+
* @property {(query: object, update: object) => Promise<{ modifiedCount?: number }>} updateOne
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {object} Db
|
|
28
|
+
* @property {(name: string) => MongoCollection} collection
|
|
29
|
+
* @property {(cmd: object) => Promise<{ ok?: number }>} command
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {object} MongoQueueOption
|
|
34
|
+
* @property {Db} db
|
|
35
|
+
* @property {string} [prefix]
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/** @internal */
|
|
39
|
+
const ensureIndex = async (collection, keys, opts) => {
|
|
40
|
+
try {
|
|
41
|
+
await collection.createIndex(keys, opts);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
if (e?.code === 85) {
|
|
44
|
+
let indexName;
|
|
45
|
+
const indexes = await collection.indexes();
|
|
46
|
+
const keyNames = Object.keys(keys);
|
|
47
|
+
for (const index of indexes) {
|
|
48
|
+
const indexKeys = Object.keys(index.key);
|
|
49
|
+
if (indexKeys.length !== keyNames.length) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
let match = true;
|
|
53
|
+
for (const k of keyNames) {
|
|
54
|
+
if (typeof index.key[k] === 'undefined') {
|
|
55
|
+
match = false;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (match) {
|
|
60
|
+
indexName = index.name;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (indexName) {
|
|
66
|
+
await collection.dropIndex(indexName);
|
|
67
|
+
await collection.createIndex(keys, opts);
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
logError(`[ensureIndex] Can not set ${Object.keys(keys).join(' + ')} index on "${collection?.collectionName || 'MongoDB'}" collection`, { keys, opts, details: e });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Class representing MongoDB Queue for MailTime */
|
|
76
|
+
class MongoQueue {
|
|
77
|
+
/**
|
|
78
|
+
* Create a MongoQueue instance
|
|
79
|
+
* @param {MongoQueueOption} opts - configuration object
|
|
80
|
+
*/
|
|
81
|
+
constructor (opts) {
|
|
82
|
+
this.name = 'mongo-queue';
|
|
83
|
+
if (!opts || typeof opts !== 'object') {
|
|
84
|
+
throw new TypeError('[mail-time] Configuration object must be passed into MongoQueue constructor');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!opts.db) {
|
|
88
|
+
throw new Error('[mail-time] [MongoQueue] requires MongoDB database {db} option, like returned from `MongoClient#db()`');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.db = opts.db;
|
|
92
|
+
if (typeof opts.prefix === 'string') {
|
|
93
|
+
this.__applyPrefix(opts.prefix);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** @internal */
|
|
98
|
+
__applyPrefix(prefix) {
|
|
99
|
+
this.prefix = prefix;
|
|
100
|
+
this.collection = this.db.collection(`__mailTimeQueue__${prefix}`);
|
|
101
|
+
this.__readyPromise = Promise.all([
|
|
102
|
+
ensureIndex(this.collection, { uuid: 1 }, { background: false }),
|
|
103
|
+
ensureIndex(this.collection, { isSent: 1, isFailed: 1, isCancelled: 1, to: 1, sendAt: 1 }, { background: false }),
|
|
104
|
+
ensureIndex(this.collection, { isSent: 1, isFailed: 1, isCancelled: 1, isSending: 1, sendingAt: 1, sendAt: 1, tries: 1 }, { background: false }),
|
|
105
|
+
]).then(() => void 0);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @internal */
|
|
109
|
+
__ensurePrefix() {
|
|
110
|
+
if (typeof this.prefix !== 'string') {
|
|
111
|
+
this.__applyPrefix(this.mailTimeInstance?.prefix || DEFAULT_PREFIX);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** @internal */
|
|
116
|
+
__debug(...args) {
|
|
117
|
+
debug(this.mailTimeInstance?.debug === true, `[${this.name}]`, ...args);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @async
|
|
122
|
+
* @memberOf MongoQueue
|
|
123
|
+
* @name ready
|
|
124
|
+
* @description Wait until indexes are created
|
|
125
|
+
* @returns {Promise<void 0>}
|
|
126
|
+
*/
|
|
127
|
+
async ready() {
|
|
128
|
+
this.__ensurePrefix();
|
|
129
|
+
this.__debug('[ready]');
|
|
130
|
+
await this.__readyPromise;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @async
|
|
135
|
+
* @memberOf MongoQueue
|
|
136
|
+
* @name ping
|
|
137
|
+
* @description Check connection to Storage
|
|
138
|
+
* @returns {Promise<object>}
|
|
139
|
+
*/
|
|
140
|
+
async ping() {
|
|
141
|
+
this.__debug('[ping]');
|
|
142
|
+
if (!this.mailTimeInstance) {
|
|
143
|
+
return {
|
|
144
|
+
status: 'Service Unavailable',
|
|
145
|
+
code: 503,
|
|
146
|
+
statusCode: 503,
|
|
147
|
+
error: new Error('MailTime instance not yet assigned to {mailTimeInstance} of Queue Adapter context'),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const ping = await this.db.command({ ping: 1 });
|
|
153
|
+
if (ping?.ok === 1) {
|
|
154
|
+
return {
|
|
155
|
+
status: 'OK',
|
|
156
|
+
code: 200,
|
|
157
|
+
statusCode: 200,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
} catch (pingError) {
|
|
161
|
+
return {
|
|
162
|
+
status: 'Internal Server Error',
|
|
163
|
+
code: 500,
|
|
164
|
+
statusCode: 500,
|
|
165
|
+
error: pingError,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
status: 'Service Unavailable',
|
|
171
|
+
code: 503,
|
|
172
|
+
statusCode: 503,
|
|
173
|
+
error: new Error('Service Unavailable'),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @memberOf MongoQueue
|
|
179
|
+
* @name iterate
|
|
180
|
+
* @description iterate over queued tasks passing each to `mailTimeInstance.___dispatch` (the bounded send pool)
|
|
181
|
+
* @param {{ limit?: number, sendingTimeout?: number }} [opts] - iteration options
|
|
182
|
+
* @returns {Promise<void>}
|
|
183
|
+
*/
|
|
184
|
+
async iterate(opts) {
|
|
185
|
+
this.__debug('[iterate]', opts);
|
|
186
|
+
this.__ensurePrefix();
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
const sendingTimeout = (opts && typeof opts.sendingTimeout === 'number' && opts.sendingTimeout > 0)
|
|
189
|
+
? opts.sendingTimeout
|
|
190
|
+
: 300000;
|
|
191
|
+
const limit = (opts && typeof opts.limit === 'number' && Number.isFinite(opts.limit) && opts.limit > 0)
|
|
192
|
+
? Math.floor(opts.limit)
|
|
193
|
+
: 0;
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const cursor = this.collection.find({
|
|
197
|
+
isSent: false,
|
|
198
|
+
isFailed: false,
|
|
199
|
+
isCancelled: false,
|
|
200
|
+
sendAt: {
|
|
201
|
+
$lte: now,
|
|
202
|
+
},
|
|
203
|
+
tries: {
|
|
204
|
+
$lt: this.mailTimeInstance.maxTries,
|
|
205
|
+
},
|
|
206
|
+
$or: [
|
|
207
|
+
{ isSending: { $ne: true } },
|
|
208
|
+
{ sendingAt: { $lte: now - sendingTimeout } },
|
|
209
|
+
],
|
|
210
|
+
}, {
|
|
211
|
+
projection: {
|
|
212
|
+
_id: 1,
|
|
213
|
+
uuid: 1,
|
|
214
|
+
tries: 1,
|
|
215
|
+
template: 1,
|
|
216
|
+
transport: 1,
|
|
217
|
+
isSent: 1,
|
|
218
|
+
isFailed: 1,
|
|
219
|
+
isCancelled: 1,
|
|
220
|
+
isSending: 1,
|
|
221
|
+
sendingAt: 1,
|
|
222
|
+
mailOptions: 1,
|
|
223
|
+
concatSubject: 1,
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (limit > 0) {
|
|
228
|
+
cursor.limit(limit);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
while (await cursor.hasNext()) {
|
|
232
|
+
await this.mailTimeInstance.___dispatch(await cursor.next());
|
|
233
|
+
}
|
|
234
|
+
await cursor.close();
|
|
235
|
+
} catch (iterateError) {
|
|
236
|
+
logError('[iterate] [while/await] [iterateError]', iterateError);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @async
|
|
242
|
+
* @memberOf MongoQueue
|
|
243
|
+
* @name getPendingTo
|
|
244
|
+
* @description get queued task by `to` field (addressee)
|
|
245
|
+
* @param to {string} - email address
|
|
246
|
+
* @param sendAt {number} - timestamp
|
|
247
|
+
* @returns {Promise<object|null>}
|
|
248
|
+
*/
|
|
249
|
+
async getPendingTo(to, sendAt) {
|
|
250
|
+
this.__debug('[getPendingTo]', to, sendAt);
|
|
251
|
+
if (typeof to !== 'string' || typeof sendAt !== 'number') {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
this.__ensurePrefix();
|
|
255
|
+
|
|
256
|
+
return await this.collection.findOne({
|
|
257
|
+
to,
|
|
258
|
+
isSent: false,
|
|
259
|
+
isFailed: false,
|
|
260
|
+
isCancelled: false,
|
|
261
|
+
isSending: { $ne: true },
|
|
262
|
+
tries: { $lt: this.mailTimeInstance.maxTries },
|
|
263
|
+
sendAt: {
|
|
264
|
+
$lte: sendAt,
|
|
265
|
+
},
|
|
266
|
+
}, {
|
|
267
|
+
projection: {
|
|
268
|
+
_id: 1,
|
|
269
|
+
to: 1,
|
|
270
|
+
uuid: 1,
|
|
271
|
+
tries: 1,
|
|
272
|
+
isSent: 1,
|
|
273
|
+
isFailed: 1,
|
|
274
|
+
isCancelled: 1,
|
|
275
|
+
mailOptions: 1,
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* @async
|
|
282
|
+
* @memberOf MongoQueue
|
|
283
|
+
* @name push
|
|
284
|
+
* @description push task to the queue/storage
|
|
285
|
+
* @param task {object} - task's object
|
|
286
|
+
* @returns {Promise<void 0>}
|
|
287
|
+
*/
|
|
288
|
+
async push(task) {
|
|
289
|
+
this.__debug('[push]', task?.uuid);
|
|
290
|
+
if (!task || typeof task !== 'object') {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
this.__ensurePrefix();
|
|
294
|
+
|
|
295
|
+
if (task.sendAt instanceof Date) {
|
|
296
|
+
task.sendAt = +task.sendAt;
|
|
297
|
+
}
|
|
298
|
+
await this.collection.insertOne(task);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @async
|
|
303
|
+
* @memberOf MongoQueue
|
|
304
|
+
* @name cancel
|
|
305
|
+
* @description cancel scheduled email
|
|
306
|
+
* @param uuid {string} - email's uuid
|
|
307
|
+
* @returns {Promise<boolean>} returns `true` if cancelled or `false` if not found, was sent, or was cancelled previously
|
|
308
|
+
*/
|
|
309
|
+
async cancel(uuid) {
|
|
310
|
+
this.__debug('[cancel]', uuid);
|
|
311
|
+
if (typeof uuid !== 'string') {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
this.__ensurePrefix();
|
|
315
|
+
|
|
316
|
+
const task = await this.collection.findOne({ uuid }, {
|
|
317
|
+
projection: {
|
|
318
|
+
_id: 1,
|
|
319
|
+
uuid: 1,
|
|
320
|
+
isSent: 1,
|
|
321
|
+
isCancelled: 1,
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
if (!task || task.isSent === true || task.isCancelled === true) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!this.mailTimeInstance.keepHistory) {
|
|
330
|
+
return await this.remove(task);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return await this.update(task, {
|
|
334
|
+
isCancelled: true,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* @async
|
|
340
|
+
* @memberOf MongoQueue
|
|
341
|
+
* @name remove
|
|
342
|
+
* @description remove task from queue
|
|
343
|
+
* @param task {object} - task's object
|
|
344
|
+
* @param {{ leaseTries: number, leaseSendingAt: number }} [opts] - lease guard: only remove if this worker still holds the lease (tries + sendingAt match, row not cancelled/failed)
|
|
345
|
+
* @returns {Promise<boolean>} returns `true` if removed or `false` if not found
|
|
346
|
+
*/
|
|
347
|
+
async remove(task, opts) {
|
|
348
|
+
this.__debug('[remove]', task?.uuid);
|
|
349
|
+
if (!task || typeof task !== 'object') {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
this.__ensurePrefix();
|
|
353
|
+
|
|
354
|
+
const query = { _id: task._id };
|
|
355
|
+
if (isSendLeaseRemove(opts)) {
|
|
356
|
+
query.tries = opts.leaseTries;
|
|
357
|
+
query.isSending = true;
|
|
358
|
+
query.sendingAt = opts.leaseSendingAt;
|
|
359
|
+
query.isCancelled = false;
|
|
360
|
+
query.isFailed = false;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const res = await this.collection.deleteOne(query);
|
|
364
|
+
return (res?.deletedCount || 0) >= 1;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* @async
|
|
369
|
+
* @memberOf MongoQueue
|
|
370
|
+
* @name update
|
|
371
|
+
* @description update task in queue
|
|
372
|
+
* @param task {object} - task's object
|
|
373
|
+
* @param updateObj {object} - fields with new values to update
|
|
374
|
+
* @returns {Promise<boolean>} returns `true` if updated or `false` if not found or no changes was made
|
|
375
|
+
*/
|
|
376
|
+
async update(task, updateObj) {
|
|
377
|
+
this.__debug('[update]', task?.uuid);
|
|
378
|
+
if (!task || typeof task !== 'object' || !updateObj || typeof updateObj !== 'object') {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
this.__ensurePrefix();
|
|
382
|
+
|
|
383
|
+
if (isAppendMailOptionUpdate(updateObj)) {
|
|
384
|
+
const res = await this.collection.updateOne({
|
|
385
|
+
_id: task._id,
|
|
386
|
+
isSent: false,
|
|
387
|
+
isFailed: false,
|
|
388
|
+
isCancelled: false,
|
|
389
|
+
isSending: { $ne: true },
|
|
390
|
+
}, {
|
|
391
|
+
$push: {
|
|
392
|
+
mailOptions: updateObj.appendMailOption,
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
return (res?.modifiedCount || 0) >= 1;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const query = {
|
|
399
|
+
_id: task._id,
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
if (isSendClaimUpdate(updateObj)) {
|
|
403
|
+
const now = typeof updateObj.sendingAt === 'number' ? updateObj.sendingAt : Date.now();
|
|
404
|
+
const sendingTimeout = this.mailTimeInstance?.sendingTimeout || 300000;
|
|
405
|
+
query.isSent = false;
|
|
406
|
+
query.isFailed = false;
|
|
407
|
+
query.isCancelled = false;
|
|
408
|
+
query.tries = task.tries;
|
|
409
|
+
query.$or = [
|
|
410
|
+
{ isSending: { $ne: true } },
|
|
411
|
+
{ sendingAt: { $lte: now - sendingTimeout } },
|
|
412
|
+
];
|
|
413
|
+
} else if (isSendLeaseGuardedUpdate(updateObj)) {
|
|
414
|
+
query.tries = updateObj.leaseTries;
|
|
415
|
+
query.isSending = true;
|
|
416
|
+
query.sendingAt = updateObj.leaseSendingAt;
|
|
417
|
+
query.isCancelled = false;
|
|
418
|
+
query.isFailed = false;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const res = await this.collection.updateOne(query, {
|
|
422
|
+
$set: stripInternalUpdateMeta(updateObj),
|
|
423
|
+
});
|
|
424
|
+
if (isSendClaimUpdate(updateObj)) {
|
|
425
|
+
return (res?.modifiedCount || res?.matchedCount || 0) >= 1;
|
|
426
|
+
}
|
|
427
|
+
return (res?.modifiedCount || 0) >= 1;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export { MongoQueue };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export type PostgresQueryResult = {
|
|
2
|
+
rowCount?: number | null | undefined;
|
|
3
|
+
rows?: unknown[] | undefined;
|
|
4
|
+
};
|
|
5
|
+
export type PostgresClient = {
|
|
6
|
+
query: (queryText: string, values?: unknown[]) => Promise<PostgresQueryResult>;
|
|
7
|
+
};
|
|
8
|
+
export type PostgresQueueOption = {
|
|
9
|
+
client: PostgresClient;
|
|
10
|
+
prefix?: string | undefined;
|
|
11
|
+
};
|
|
12
|
+
/** Class representing PostgreSQL Queue for MailTime */
|
|
13
|
+
export class PostgresQueue {
|
|
14
|
+
/**
|
|
15
|
+
* Create a PostgresQueue instance
|
|
16
|
+
* @param {PostgresQueueOption} opts - configuration object
|
|
17
|
+
*/
|
|
18
|
+
constructor(opts: PostgresQueueOption);
|
|
19
|
+
name: string;
|
|
20
|
+
client: PostgresClient;
|
|
21
|
+
prefix: any;
|
|
22
|
+
/**
|
|
23
|
+
* @async
|
|
24
|
+
* @memberOf PostgresQueue
|
|
25
|
+
* @name ready
|
|
26
|
+
* @description Wait until PostgreSQL schema is ready
|
|
27
|
+
* @returns {Promise<void 0>}
|
|
28
|
+
*/
|
|
29
|
+
ready(): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* @async
|
|
32
|
+
* @memberOf PostgresQueue
|
|
33
|
+
* @name ping
|
|
34
|
+
* @description Check connection to Storage
|
|
35
|
+
* @returns {Promise<object>}
|
|
36
|
+
*/
|
|
37
|
+
ping(): Promise<object>;
|
|
38
|
+
/**
|
|
39
|
+
* @memberOf PostgresQueue
|
|
40
|
+
* @name iterate
|
|
41
|
+
* @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.
|
|
42
|
+
* @param {{ limit?: number, sendingTimeout?: number }} [opts] - iteration options
|
|
43
|
+
* @returns {Promise<void>}
|
|
44
|
+
*/
|
|
45
|
+
iterate(opts?: {
|
|
46
|
+
limit?: number;
|
|
47
|
+
sendingTimeout?: number;
|
|
48
|
+
}): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* @async
|
|
51
|
+
* @memberOf PostgresQueue
|
|
52
|
+
* @name getPendingTo
|
|
53
|
+
* @description get queued task by `to` field (addressee)
|
|
54
|
+
* @param to {string} - email address
|
|
55
|
+
* @param sendAt {number} - timestamp
|
|
56
|
+
* @returns {Promise<object|null>}
|
|
57
|
+
*/
|
|
58
|
+
getPendingTo(to: string, sendAt: number): Promise<object | null>;
|
|
59
|
+
/**
|
|
60
|
+
* @async
|
|
61
|
+
* @memberOf PostgresQueue
|
|
62
|
+
* @name push
|
|
63
|
+
* @description push task to the queue/storage
|
|
64
|
+
* @param task {object} - task's object
|
|
65
|
+
* @returns {Promise<void 0>}
|
|
66
|
+
*/
|
|
67
|
+
push(task: object): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* @async
|
|
70
|
+
* @memberOf PostgresQueue
|
|
71
|
+
* @name cancel
|
|
72
|
+
* @description cancel scheduled email
|
|
73
|
+
* @param uuid {string} - email's uuid
|
|
74
|
+
* @returns {Promise<boolean>} returns `true` if cancelled or `false` if not found, was sent, or was cancelled previously
|
|
75
|
+
*/
|
|
76
|
+
cancel(uuid: string): Promise<boolean>;
|
|
77
|
+
/**
|
|
78
|
+
* @async
|
|
79
|
+
* @memberOf PostgresQueue
|
|
80
|
+
* @name remove
|
|
81
|
+
* @description remove task from queue
|
|
82
|
+
* @param task {object} - task's object
|
|
83
|
+
* @param {{ leaseTries: number, leaseSendingAt: number }} [opts] - lease guard: only remove if this worker still holds the lease (tries + sendingAt match, row not cancelled/failed)
|
|
84
|
+
* @returns {Promise<boolean>} returns `true` if removed or `false` if not found
|
|
85
|
+
*/
|
|
86
|
+
remove(task: object, opts?: {
|
|
87
|
+
leaseTries: number;
|
|
88
|
+
leaseSendingAt: number;
|
|
89
|
+
}): Promise<boolean>;
|
|
90
|
+
/**
|
|
91
|
+
* @async
|
|
92
|
+
* @memberOf PostgresQueue
|
|
93
|
+
* @name update
|
|
94
|
+
* @description update task in queue
|
|
95
|
+
* @param task {object} - task's object
|
|
96
|
+
* @param updateObj {object} - fields with new values to update
|
|
97
|
+
* @returns {Promise<boolean>} returns `true` if updated or `false` if not found or no changes was made
|
|
98
|
+
*/
|
|
99
|
+
update(task: object, updateObj: object): Promise<boolean>;
|
|
100
|
+
}
|