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.
- package/LICENSE +1 -1
- package/README.md +381 -607
- package/adapters/mongo.d.ts +118 -0
- package/adapters/mongo.js +204 -80
- package/adapters/postgres.d.ts +100 -0
- package/adapters/postgres.js +536 -0
- package/adapters/redis.d.ts +105 -0
- package/adapters/redis.js +342 -79
- package/helpers.js +286 -3
- package/index.cjs +2395 -457
- package/index.d.cts +322 -0
- package/index.d.ts +322 -0
- package/index.js +886 -303
- package/package.json +78 -17
- package/presets.d.ts +87 -0
- package/presets.js +193 -0
- package/adapters/blank-example.js +0 -230
|
@@ -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
|
+
}
|
package/adapters/mongo.js
CHANGED
|
@@ -1,37 +1,62 @@
|
|
|
1
|
-
import {
|
|
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
|
+
*/
|
|
2
31
|
|
|
3
32
|
/**
|
|
4
|
-
*
|
|
5
|
-
* @
|
|
6
|
-
* @
|
|
7
|
-
* @param {object} keys - Field and value pairs where the field is the index key and the value describes the type of index for that field
|
|
8
|
-
* @param {object} opts - Set of options that controls the creation of the index
|
|
9
|
-
* @returns {void 0}
|
|
33
|
+
* @typedef {object} MongoQueueOption
|
|
34
|
+
* @property {Db} db
|
|
35
|
+
* @property {string} [prefix]
|
|
10
36
|
*/
|
|
37
|
+
|
|
38
|
+
/** @internal */
|
|
11
39
|
const ensureIndex = async (collection, keys, opts) => {
|
|
12
40
|
try {
|
|
13
41
|
await collection.createIndex(keys, opts);
|
|
14
42
|
} catch (e) {
|
|
15
|
-
if (e
|
|
43
|
+
if (e?.code === 85) {
|
|
16
44
|
let indexName;
|
|
17
45
|
const indexes = await collection.indexes();
|
|
46
|
+
const keyNames = Object.keys(keys);
|
|
18
47
|
for (const index of indexes) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
drop = false;
|
|
23
|
-
break;
|
|
24
|
-
}
|
|
48
|
+
const indexKeys = Object.keys(index.key);
|
|
49
|
+
if (indexKeys.length !== keyNames.length) {
|
|
50
|
+
continue;
|
|
25
51
|
}
|
|
26
|
-
|
|
27
|
-
for (const
|
|
28
|
-
if (typeof
|
|
29
|
-
|
|
52
|
+
let match = true;
|
|
53
|
+
for (const k of keyNames) {
|
|
54
|
+
if (typeof index.key[k] === 'undefined') {
|
|
55
|
+
match = false;
|
|
30
56
|
break;
|
|
31
57
|
}
|
|
32
58
|
}
|
|
33
|
-
|
|
34
|
-
if (drop) {
|
|
59
|
+
if (match) {
|
|
35
60
|
indexName = index.name;
|
|
36
61
|
break;
|
|
37
62
|
}
|
|
@@ -42,7 +67,7 @@ const ensureIndex = async (collection, keys, opts) => {
|
|
|
42
67
|
await collection.createIndex(keys, opts);
|
|
43
68
|
}
|
|
44
69
|
} else {
|
|
45
|
-
logError(`[ensureIndex] Can not set ${Object.keys(keys).join(' + ')} index on "${collection
|
|
70
|
+
logError(`[ensureIndex] Can not set ${Object.keys(keys).join(' + ')} index on "${collection?.collectionName || 'MongoDB'}" collection`, { keys, opts, details: e });
|
|
46
71
|
}
|
|
47
72
|
}
|
|
48
73
|
};
|
|
@@ -51,47 +76,58 @@ const ensureIndex = async (collection, keys, opts) => {
|
|
|
51
76
|
class MongoQueue {
|
|
52
77
|
/**
|
|
53
78
|
* Create a MongoQueue instance
|
|
54
|
-
* @param {
|
|
55
|
-
* @param {Db} opts.db - Required, Mongo's `Db` instance, like one returned from `MongoClient#db()` method
|
|
56
|
-
* @param {string} [opts.prefix] - Optional prefix for scope isolation; use when creating multiple MailTime instances within the single application
|
|
79
|
+
* @param {MongoQueueOption} opts - configuration object
|
|
57
80
|
*/
|
|
58
81
|
constructor (opts) {
|
|
59
82
|
this.name = 'mongo-queue';
|
|
60
|
-
if (!opts || typeof opts !== 'object'
|
|
83
|
+
if (!opts || typeof opts !== 'object') {
|
|
61
84
|
throw new TypeError('[mail-time] Configuration object must be passed into MongoQueue constructor');
|
|
62
85
|
}
|
|
63
86
|
|
|
64
87
|
if (!opts.db) {
|
|
65
|
-
throw new Error('[mail-time] [MongoQueue] requires MongoDB database {db} option, like returned from `MongoClient
|
|
88
|
+
throw new Error('[mail-time] [MongoQueue] requires MongoDB database {db} option, like returned from `MongoClient#db()`');
|
|
66
89
|
}
|
|
67
90
|
|
|
68
|
-
this.prefix = (typeof opts.prefix === 'string') ? opts.prefix : '';
|
|
69
91
|
this.db = opts.db;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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;
|
|
95
131
|
}
|
|
96
132
|
|
|
97
133
|
/**
|
|
@@ -102,6 +138,7 @@ class MongoQueue {
|
|
|
102
138
|
* @returns {Promise<object>}
|
|
103
139
|
*/
|
|
104
140
|
async ping() {
|
|
141
|
+
this.__debug('[ping]');
|
|
105
142
|
if (!this.mailTimeInstance) {
|
|
106
143
|
return {
|
|
107
144
|
status: 'Service Unavailable',
|
|
@@ -125,7 +162,7 @@ class MongoQueue {
|
|
|
125
162
|
status: 'Internal Server Error',
|
|
126
163
|
code: 500,
|
|
127
164
|
statusCode: 500,
|
|
128
|
-
error: pingError
|
|
165
|
+
error: pingError,
|
|
129
166
|
};
|
|
130
167
|
}
|
|
131
168
|
|
|
@@ -133,28 +170,43 @@ class MongoQueue {
|
|
|
133
170
|
status: 'Service Unavailable',
|
|
134
171
|
code: 503,
|
|
135
172
|
statusCode: 503,
|
|
136
|
-
error: new Error('Service Unavailable')
|
|
173
|
+
error: new Error('Service Unavailable'),
|
|
137
174
|
};
|
|
138
175
|
}
|
|
139
176
|
|
|
140
177
|
/**
|
|
141
178
|
* @memberOf MongoQueue
|
|
142
179
|
* @name iterate
|
|
143
|
-
* @description iterate over queued tasks passing to `mailTimeInstance.
|
|
144
|
-
* @
|
|
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>}
|
|
145
183
|
*/
|
|
146
|
-
async iterate() {
|
|
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
|
+
|
|
147
195
|
try {
|
|
148
196
|
const cursor = this.collection.find({
|
|
149
197
|
isSent: false,
|
|
150
198
|
isFailed: false,
|
|
151
199
|
isCancelled: false,
|
|
152
200
|
sendAt: {
|
|
153
|
-
$lte:
|
|
201
|
+
$lte: now,
|
|
154
202
|
},
|
|
155
203
|
tries: {
|
|
156
|
-
$lt: this.mailTimeInstance.maxTries
|
|
157
|
-
}
|
|
204
|
+
$lt: this.mailTimeInstance.maxTries,
|
|
205
|
+
},
|
|
206
|
+
$or: [
|
|
207
|
+
{ isSending: { $ne: true } },
|
|
208
|
+
{ sendingAt: { $lte: now - sendingTimeout } },
|
|
209
|
+
],
|
|
158
210
|
}, {
|
|
159
211
|
projection: {
|
|
160
212
|
_id: 1,
|
|
@@ -165,15 +217,24 @@ class MongoQueue {
|
|
|
165
217
|
isSent: 1,
|
|
166
218
|
isFailed: 1,
|
|
167
219
|
isCancelled: 1,
|
|
220
|
+
isSending: 1,
|
|
221
|
+
sendingAt: 1,
|
|
168
222
|
mailOptions: 1,
|
|
169
223
|
concatSubject: 1,
|
|
170
|
-
}
|
|
224
|
+
},
|
|
171
225
|
});
|
|
172
226
|
|
|
173
|
-
|
|
174
|
-
|
|
227
|
+
if (limit > 0) {
|
|
228
|
+
cursor.limit(limit);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
while (await cursor.hasNext()) {
|
|
233
|
+
await this.mailTimeInstance.___dispatch(await cursor.next());
|
|
234
|
+
}
|
|
235
|
+
} finally {
|
|
236
|
+
await cursor.close().catch(() => {});
|
|
175
237
|
}
|
|
176
|
-
await cursor.close();
|
|
177
238
|
} catch (iterateError) {
|
|
178
239
|
logError('[iterate] [while/await] [iterateError]', iterateError);
|
|
179
240
|
}
|
|
@@ -189,18 +250,22 @@ class MongoQueue {
|
|
|
189
250
|
* @returns {Promise<object|null>}
|
|
190
251
|
*/
|
|
191
252
|
async getPendingTo(to, sendAt) {
|
|
253
|
+
this.__debug('[getPendingTo]', to, sendAt);
|
|
192
254
|
if (typeof to !== 'string' || typeof sendAt !== 'number') {
|
|
193
255
|
return null;
|
|
194
256
|
}
|
|
257
|
+
this.__ensurePrefix();
|
|
195
258
|
|
|
196
259
|
return await this.collection.findOne({
|
|
197
|
-
to
|
|
260
|
+
to,
|
|
198
261
|
isSent: false,
|
|
199
262
|
isFailed: false,
|
|
200
263
|
isCancelled: false,
|
|
264
|
+
isSending: { $ne: true },
|
|
265
|
+
tries: { $lt: this.mailTimeInstance.maxTries },
|
|
201
266
|
sendAt: {
|
|
202
|
-
$lte: sendAt
|
|
203
|
-
}
|
|
267
|
+
$lte: sendAt,
|
|
268
|
+
},
|
|
204
269
|
}, {
|
|
205
270
|
projection: {
|
|
206
271
|
_id: 1,
|
|
@@ -211,7 +276,7 @@ class MongoQueue {
|
|
|
211
276
|
isFailed: 1,
|
|
212
277
|
isCancelled: 1,
|
|
213
278
|
mailOptions: 1,
|
|
214
|
-
}
|
|
279
|
+
},
|
|
215
280
|
});
|
|
216
281
|
}
|
|
217
282
|
|
|
@@ -224,9 +289,11 @@ class MongoQueue {
|
|
|
224
289
|
* @returns {Promise<void 0>}
|
|
225
290
|
*/
|
|
226
291
|
async push(task) {
|
|
227
|
-
|
|
292
|
+
this.__debug('[push]', task?.uuid);
|
|
293
|
+
if (!task || typeof task !== 'object') {
|
|
228
294
|
return;
|
|
229
295
|
}
|
|
296
|
+
this.__ensurePrefix();
|
|
230
297
|
|
|
231
298
|
if (task.sendAt instanceof Date) {
|
|
232
299
|
task.sendAt = +task.sendAt;
|
|
@@ -243,9 +310,11 @@ class MongoQueue {
|
|
|
243
310
|
* @returns {Promise<boolean>} returns `true` if cancelled or `false` if not found, was sent, or was cancelled previously
|
|
244
311
|
*/
|
|
245
312
|
async cancel(uuid) {
|
|
313
|
+
this.__debug('[cancel]', uuid);
|
|
246
314
|
if (typeof uuid !== 'string') {
|
|
247
315
|
return false;
|
|
248
316
|
}
|
|
317
|
+
this.__ensurePrefix();
|
|
249
318
|
|
|
250
319
|
const task = await this.collection.findOne({ uuid }, {
|
|
251
320
|
projection: {
|
|
@@ -253,7 +322,7 @@ class MongoQueue {
|
|
|
253
322
|
uuid: 1,
|
|
254
323
|
isSent: 1,
|
|
255
324
|
isCancelled: 1,
|
|
256
|
-
}
|
|
325
|
+
},
|
|
257
326
|
});
|
|
258
327
|
|
|
259
328
|
if (!task || task.isSent === true || task.isCancelled === true) {
|
|
@@ -275,35 +344,90 @@ class MongoQueue {
|
|
|
275
344
|
* @name remove
|
|
276
345
|
* @description remove task from queue
|
|
277
346
|
* @param task {object} - task's object
|
|
347
|
+
* @param {{ leaseTries: number, leaseSendingAt: number }} [opts] - lease guard: only remove if this worker still holds the lease (tries + sendingAt match, row not cancelled/failed)
|
|
278
348
|
* @returns {Promise<boolean>} returns `true` if removed or `false` if not found
|
|
279
349
|
*/
|
|
280
|
-
async remove(task) {
|
|
281
|
-
|
|
350
|
+
async remove(task, opts) {
|
|
351
|
+
this.__debug('[remove]', task?.uuid);
|
|
352
|
+
if (!task || typeof task !== 'object') {
|
|
282
353
|
return false;
|
|
283
354
|
}
|
|
355
|
+
this.__ensurePrefix();
|
|
284
356
|
|
|
285
|
-
|
|
357
|
+
const query = { _id: task._id };
|
|
358
|
+
if (isSendLeaseRemove(opts)) {
|
|
359
|
+
query.tries = opts.leaseTries;
|
|
360
|
+
query.isSending = true;
|
|
361
|
+
query.sendingAt = opts.leaseSendingAt;
|
|
362
|
+
query.isCancelled = false;
|
|
363
|
+
query.isFailed = false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const res = await this.collection.deleteOne(query);
|
|
367
|
+
return (res?.deletedCount || 0) >= 1;
|
|
286
368
|
}
|
|
287
369
|
|
|
288
370
|
/**
|
|
289
371
|
* @async
|
|
290
372
|
* @memberOf MongoQueue
|
|
291
373
|
* @name update
|
|
292
|
-
* @description
|
|
374
|
+
* @description update task in queue
|
|
293
375
|
* @param task {object} - task's object
|
|
294
376
|
* @param updateObj {object} - fields with new values to update
|
|
295
377
|
* @returns {Promise<boolean>} returns `true` if updated or `false` if not found or no changes was made
|
|
296
378
|
*/
|
|
297
379
|
async update(task, updateObj) {
|
|
298
|
-
|
|
380
|
+
this.__debug('[update]', task?.uuid);
|
|
381
|
+
if (!task || typeof task !== 'object' || !updateObj || typeof updateObj !== 'object') {
|
|
299
382
|
return false;
|
|
300
383
|
}
|
|
384
|
+
this.__ensurePrefix();
|
|
301
385
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
386
|
+
if (isAppendMailOptionUpdate(updateObj)) {
|
|
387
|
+
const res = await this.collection.updateOne({
|
|
388
|
+
_id: task._id,
|
|
389
|
+
isSent: false,
|
|
390
|
+
isFailed: false,
|
|
391
|
+
isCancelled: false,
|
|
392
|
+
isSending: { $ne: true },
|
|
393
|
+
}, {
|
|
394
|
+
$push: {
|
|
395
|
+
mailOptions: updateObj.appendMailOption,
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
return (res?.modifiedCount || 0) >= 1;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const query = {
|
|
402
|
+
_id: task._id,
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
if (isSendClaimUpdate(updateObj)) {
|
|
406
|
+
const now = typeof updateObj.sendingAt === 'number' ? updateObj.sendingAt : Date.now();
|
|
407
|
+
const sendingTimeout = this.mailTimeInstance?.sendingTimeout || 300000;
|
|
408
|
+
query.isSent = false;
|
|
409
|
+
query.isFailed = false;
|
|
410
|
+
query.isCancelled = false;
|
|
411
|
+
query.tries = task.tries;
|
|
412
|
+
query.$or = [
|
|
413
|
+
{ isSending: { $ne: true } },
|
|
414
|
+
{ sendingAt: { $lte: now - sendingTimeout } },
|
|
415
|
+
];
|
|
416
|
+
} else if (isSendLeaseGuardedUpdate(updateObj)) {
|
|
417
|
+
query.tries = updateObj.leaseTries;
|
|
418
|
+
query.isSending = true;
|
|
419
|
+
query.sendingAt = updateObj.leaseSendingAt;
|
|
420
|
+
query.isCancelled = false;
|
|
421
|
+
query.isFailed = false;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const res = await this.collection.updateOne(query, {
|
|
425
|
+
$set: stripInternalUpdateMeta(updateObj),
|
|
426
|
+
});
|
|
427
|
+
if (isSendClaimUpdate(updateObj)) {
|
|
428
|
+
return (res?.modifiedCount || res?.matchedCount || 0) >= 1;
|
|
429
|
+
}
|
|
430
|
+
return (res?.modifiedCount || 0) >= 1;
|
|
307
431
|
}
|
|
308
432
|
}
|
|
309
433
|
|
|
@@ -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
|
+
}
|