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,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,13 +217,19 @@ 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
|
|
|
227
|
+
if (limit > 0) {
|
|
228
|
+
cursor.limit(limit);
|
|
229
|
+
}
|
|
230
|
+
|
|
173
231
|
while (await cursor.hasNext()) {
|
|
174
|
-
await this.mailTimeInstance.
|
|
232
|
+
await this.mailTimeInstance.___dispatch(await cursor.next());
|
|
175
233
|
}
|
|
176
234
|
await cursor.close();
|
|
177
235
|
} catch (iterateError) {
|
|
@@ -189,18 +247,22 @@ class MongoQueue {
|
|
|
189
247
|
* @returns {Promise<object|null>}
|
|
190
248
|
*/
|
|
191
249
|
async getPendingTo(to, sendAt) {
|
|
250
|
+
this.__debug('[getPendingTo]', to, sendAt);
|
|
192
251
|
if (typeof to !== 'string' || typeof sendAt !== 'number') {
|
|
193
252
|
return null;
|
|
194
253
|
}
|
|
254
|
+
this.__ensurePrefix();
|
|
195
255
|
|
|
196
256
|
return await this.collection.findOne({
|
|
197
|
-
to
|
|
257
|
+
to,
|
|
198
258
|
isSent: false,
|
|
199
259
|
isFailed: false,
|
|
200
260
|
isCancelled: false,
|
|
261
|
+
isSending: { $ne: true },
|
|
262
|
+
tries: { $lt: this.mailTimeInstance.maxTries },
|
|
201
263
|
sendAt: {
|
|
202
|
-
$lte: sendAt
|
|
203
|
-
}
|
|
264
|
+
$lte: sendAt,
|
|
265
|
+
},
|
|
204
266
|
}, {
|
|
205
267
|
projection: {
|
|
206
268
|
_id: 1,
|
|
@@ -211,7 +273,7 @@ class MongoQueue {
|
|
|
211
273
|
isFailed: 1,
|
|
212
274
|
isCancelled: 1,
|
|
213
275
|
mailOptions: 1,
|
|
214
|
-
}
|
|
276
|
+
},
|
|
215
277
|
});
|
|
216
278
|
}
|
|
217
279
|
|
|
@@ -224,9 +286,11 @@ class MongoQueue {
|
|
|
224
286
|
* @returns {Promise<void 0>}
|
|
225
287
|
*/
|
|
226
288
|
async push(task) {
|
|
227
|
-
|
|
289
|
+
this.__debug('[push]', task?.uuid);
|
|
290
|
+
if (!task || typeof task !== 'object') {
|
|
228
291
|
return;
|
|
229
292
|
}
|
|
293
|
+
this.__ensurePrefix();
|
|
230
294
|
|
|
231
295
|
if (task.sendAt instanceof Date) {
|
|
232
296
|
task.sendAt = +task.sendAt;
|
|
@@ -243,9 +307,11 @@ class MongoQueue {
|
|
|
243
307
|
* @returns {Promise<boolean>} returns `true` if cancelled or `false` if not found, was sent, or was cancelled previously
|
|
244
308
|
*/
|
|
245
309
|
async cancel(uuid) {
|
|
310
|
+
this.__debug('[cancel]', uuid);
|
|
246
311
|
if (typeof uuid !== 'string') {
|
|
247
312
|
return false;
|
|
248
313
|
}
|
|
314
|
+
this.__ensurePrefix();
|
|
249
315
|
|
|
250
316
|
const task = await this.collection.findOne({ uuid }, {
|
|
251
317
|
projection: {
|
|
@@ -253,7 +319,7 @@ class MongoQueue {
|
|
|
253
319
|
uuid: 1,
|
|
254
320
|
isSent: 1,
|
|
255
321
|
isCancelled: 1,
|
|
256
|
-
}
|
|
322
|
+
},
|
|
257
323
|
});
|
|
258
324
|
|
|
259
325
|
if (!task || task.isSent === true || task.isCancelled === true) {
|
|
@@ -275,35 +341,90 @@ class MongoQueue {
|
|
|
275
341
|
* @name remove
|
|
276
342
|
* @description remove task from queue
|
|
277
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)
|
|
278
345
|
* @returns {Promise<boolean>} returns `true` if removed or `false` if not found
|
|
279
346
|
*/
|
|
280
|
-
async remove(task) {
|
|
281
|
-
|
|
347
|
+
async remove(task, opts) {
|
|
348
|
+
this.__debug('[remove]', task?.uuid);
|
|
349
|
+
if (!task || typeof task !== 'object') {
|
|
282
350
|
return false;
|
|
283
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
|
+
}
|
|
284
362
|
|
|
285
|
-
|
|
363
|
+
const res = await this.collection.deleteOne(query);
|
|
364
|
+
return (res?.deletedCount || 0) >= 1;
|
|
286
365
|
}
|
|
287
366
|
|
|
288
367
|
/**
|
|
289
368
|
* @async
|
|
290
369
|
* @memberOf MongoQueue
|
|
291
370
|
* @name update
|
|
292
|
-
* @description
|
|
371
|
+
* @description update task in queue
|
|
293
372
|
* @param task {object} - task's object
|
|
294
373
|
* @param updateObj {object} - fields with new values to update
|
|
295
374
|
* @returns {Promise<boolean>} returns `true` if updated or `false` if not found or no changes was made
|
|
296
375
|
*/
|
|
297
376
|
async update(task, updateObj) {
|
|
298
|
-
|
|
377
|
+
this.__debug('[update]', task?.uuid);
|
|
378
|
+
if (!task || typeof task !== 'object' || !updateObj || typeof updateObj !== 'object') {
|
|
299
379
|
return false;
|
|
300
380
|
}
|
|
381
|
+
this.__ensurePrefix();
|
|
301
382
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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;
|
|
307
428
|
}
|
|
308
429
|
}
|
|
309
430
|
|
|
@@ -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
|
+
}
|