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
package/package.json
CHANGED
|
@@ -1,22 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mail-time",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "📮 Email queue extending
|
|
3
|
+
"version": "4.1.0",
|
|
4
|
+
"description": "📮 Email queue extending nodemailer with multi-SMTP transports and horizontally scaled apps support",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"types": "./index.d.ts",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"exports": {
|
|
8
|
-
"
|
|
9
|
-
|
|
9
|
+
".": {
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./index.d.ts",
|
|
12
|
+
"default": "./index.js"
|
|
13
|
+
},
|
|
14
|
+
"require": {
|
|
15
|
+
"types": "./index.d.cts",
|
|
16
|
+
"default": "./index.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"./presets": {
|
|
20
|
+
"import": {
|
|
21
|
+
"types": "./presets.d.ts",
|
|
22
|
+
"default": "./presets.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"./adapters/mongo": {
|
|
26
|
+
"import": {
|
|
27
|
+
"types": "./adapters/mongo.d.ts",
|
|
28
|
+
"default": "./adapters/mongo.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"./adapters/redis": {
|
|
32
|
+
"import": {
|
|
33
|
+
"types": "./adapters/redis.d.ts",
|
|
34
|
+
"default": "./adapters/redis.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"./adapters/postgres": {
|
|
38
|
+
"import": {
|
|
39
|
+
"types": "./adapters/postgres.d.ts",
|
|
40
|
+
"default": "./adapters/postgres.js"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"./package.json": "./package.json"
|
|
10
44
|
},
|
|
11
45
|
"scripts": {
|
|
12
|
-
"prepublishOnly": "rollup --config rollup.config.js",
|
|
13
|
-
"test": "mocha
|
|
46
|
+
"prepublishOnly": "rollup --config rollup.config.js && rm -rf ./.types-tmp && tsc --emitDeclarationOnly --declaration --outDir ./.types-tmp --rootDir ./ && node ./scripts/strip-internal-dts.mjs ./.types-tmp/index.d.ts ./.types-tmp/presets.d.ts ./.types-tmp/adapters/mongo.d.ts ./.types-tmp/adapters/redis.d.ts ./.types-tmp/adapters/postgres.d.ts && cp ./.types-tmp/index.d.ts ./index.d.ts && cp ./.types-tmp/index.d.ts ./index.d.cts && cp ./.types-tmp/presets.d.ts ./presets.d.ts && cp ./.types-tmp/adapters/*.d.ts ./adapters/ && rm -rf ./.types-tmp",
|
|
47
|
+
"test": "npm run test:jest && npm run test:mocha && npm run test:types",
|
|
48
|
+
"test:jest": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --config jest.config.mjs",
|
|
49
|
+
"test:mocha": "mocha ./test/npm.js",
|
|
50
|
+
"test:mocha:redis": "mocha ./test/npm-redis-redis.js",
|
|
51
|
+
"test:mocha:mongo": "mocha ./test/npm-redis-mongo.js ./test/npm-mongo-redis.js ./test/npm-ha-mongo-redis.js",
|
|
52
|
+
"test:mocha:postgres": "mocha ./test/npm-postgres.js",
|
|
53
|
+
"test:types": "tsc -p ./tsconfig.types.json",
|
|
54
|
+
"test:redis": "mocha ./test/npm-redis-redis.js",
|
|
55
|
+
"test:mongo": "mocha ./test/npm-mongo-mongo.js",
|
|
56
|
+
"test:postgres": "mocha ./test/npm-postgres.js",
|
|
57
|
+
"test:bun": "bun test ./test/jest"
|
|
14
58
|
},
|
|
15
59
|
"keywords": [
|
|
16
60
|
"backup",
|
|
17
61
|
"balanced",
|
|
18
62
|
"balancer",
|
|
19
63
|
"bulletproof",
|
|
64
|
+
"bun",
|
|
20
65
|
"cluster",
|
|
21
66
|
"email",
|
|
22
67
|
"highload",
|
|
@@ -29,7 +74,9 @@
|
|
|
29
74
|
"microservice",
|
|
30
75
|
"micro-service",
|
|
31
76
|
"nodemailer",
|
|
77
|
+
"postgres",
|
|
32
78
|
"queue",
|
|
79
|
+
"scheduler",
|
|
33
80
|
"template"
|
|
34
81
|
],
|
|
35
82
|
"tags": [
|
|
@@ -37,6 +84,7 @@
|
|
|
37
84
|
"balanced",
|
|
38
85
|
"balancer",
|
|
39
86
|
"bulletproof",
|
|
87
|
+
"bun",
|
|
40
88
|
"cluster",
|
|
41
89
|
"email",
|
|
42
90
|
"highload",
|
|
@@ -49,7 +97,9 @@
|
|
|
49
97
|
"microservice",
|
|
50
98
|
"micro-service",
|
|
51
99
|
"nodemailer",
|
|
100
|
+
"postgres",
|
|
52
101
|
"queue",
|
|
102
|
+
"scheduler",
|
|
53
103
|
"template"
|
|
54
104
|
],
|
|
55
105
|
"repository": {
|
|
@@ -63,20 +113,31 @@
|
|
|
63
113
|
"author": "dr.dimitru (https://veliovgroup.com)",
|
|
64
114
|
"license": "BSD-3-Clause",
|
|
65
115
|
"engines": {
|
|
66
|
-
"node": ">=
|
|
116
|
+
"node": ">=20.9.0",
|
|
117
|
+
"bun": ">=1.1.0"
|
|
118
|
+
},
|
|
119
|
+
"meteor": {
|
|
120
|
+
"packageName": "ostrio:mailer",
|
|
121
|
+
"versionsFrom": ["2.14", "3.2"],
|
|
122
|
+
"node": {
|
|
123
|
+
"2.14": ">=14",
|
|
124
|
+
"3.2": ">=18"
|
|
125
|
+
}
|
|
67
126
|
},
|
|
68
127
|
"dependencies": {
|
|
69
|
-
"
|
|
70
|
-
"josk": "^5.0.0"
|
|
128
|
+
"josk": "^6.2.0"
|
|
71
129
|
},
|
|
72
130
|
"devDependencies": {
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"chai": "^
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"nodemailer
|
|
80
|
-
"
|
|
131
|
+
"@jest/globals": "^30.4.1",
|
|
132
|
+
"@types/node": "^25.9.0",
|
|
133
|
+
"chai": "^6.2.2",
|
|
134
|
+
"jest": "^30.4.2",
|
|
135
|
+
"mocha": "^11.7.5",
|
|
136
|
+
"mongodb": "^7.2.0",
|
|
137
|
+
"nodemailer": "^8.0.7",
|
|
138
|
+
"pg": "^8.21.0",
|
|
139
|
+
"redis": "^5.12.1",
|
|
140
|
+
"rollup": "^4.60.4",
|
|
141
|
+
"typescript": "^6.0.3"
|
|
81
142
|
}
|
|
82
143
|
}
|
package/presets.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export type MailTimePresetConfig = {
|
|
2
|
+
concatEmails?: boolean | undefined;
|
|
3
|
+
concatDelay?: number | undefined;
|
|
4
|
+
concatSubject?: string | undefined;
|
|
5
|
+
retries?: number | undefined;
|
|
6
|
+
retryDelay?: number | undefined;
|
|
7
|
+
revolvingInterval?: number | undefined;
|
|
8
|
+
sendingTimeout?: number | undefined;
|
|
9
|
+
mode?: "one" | "batch" | undefined;
|
|
10
|
+
concurrency?: number | undefined;
|
|
11
|
+
onError?: ((error: unknown, email: any, details?: object) => void) | undefined;
|
|
12
|
+
josk?: object | undefined;
|
|
13
|
+
};
|
|
14
|
+
export type MailTimePresetName = keyof typeof PRESETS;
|
|
15
|
+
/**
|
|
16
|
+
* Materialize a MailTime constructor config from a built-in preset.
|
|
17
|
+
* The named preset is deep-cloned (so the result is freely mutable) and
|
|
18
|
+
* `overrides` is deep-merged on top — overrides win for scalar keys, and
|
|
19
|
+
* nested objects like `josk` are merged so the preset's defaults compose
|
|
20
|
+
* with the caller's `adapter`, `lockOwnerId`, `onError`, etc.
|
|
21
|
+
*
|
|
22
|
+
* ```js
|
|
23
|
+
* import { MailTime, RedisQueue, mailTimePreset } from 'mail-time';
|
|
24
|
+
* const mailTime = new MailTime(mailTimePreset('otp', {
|
|
25
|
+
* prefix: 'otp',
|
|
26
|
+
* queue: new RedisQueue({ client }),
|
|
27
|
+
* transports: [otpTransport],
|
|
28
|
+
* josk: { adapter: { type: 'redis', client } },
|
|
29
|
+
* }));
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @param {MailTimePresetName} name - one of `presetNames`
|
|
33
|
+
* @param {object} [overrides] - additional MailTime constructor options
|
|
34
|
+
* @returns {MailTimePresetConfig} fresh, mutable MailTime constructor options (preset deep-cloned + overrides merged)
|
|
35
|
+
* @throws {Error} when `name` is unknown
|
|
36
|
+
* @throws {TypeError} when `overrides` is provided but not a plain object
|
|
37
|
+
*/
|
|
38
|
+
export function mailTimePreset(name: MailTimePresetName, overrides?: object): MailTimePresetConfig;
|
|
39
|
+
/**
|
|
40
|
+
* Read-only map of preset name → partial MailTime config. Use
|
|
41
|
+
* `mailTimePreset(name, overrides)` to materialize a merged copy; use
|
|
42
|
+
* this object directly only when you want to introspect or compose
|
|
43
|
+
* presets manually.
|
|
44
|
+
*/
|
|
45
|
+
export const presets: Readonly<Record<"transactional" | "otp" | "newsletter" | "marketing" | "notifications" | "alerts", Readonly<MailTimePresetConfig>>>;
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {keyof typeof PRESETS} MailTimePresetName
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* Names of every built-in preset.
|
|
51
|
+
* @type {ReadonlyArray<MailTimePresetName>}
|
|
52
|
+
*/
|
|
53
|
+
export const presetNames: ReadonlyArray<MailTimePresetName>;
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {object} MailTimePresetConfig
|
|
56
|
+
* @property {boolean} [concatEmails]
|
|
57
|
+
* @property {number} [concatDelay]
|
|
58
|
+
* @property {string} [concatSubject]
|
|
59
|
+
* @property {number} [retries]
|
|
60
|
+
* @property {number} [retryDelay]
|
|
61
|
+
* @property {number} [revolvingInterval]
|
|
62
|
+
* @property {number} [sendingTimeout]
|
|
63
|
+
* @property {'one' | 'batch'} [mode]
|
|
64
|
+
* @property {number} [concurrency]
|
|
65
|
+
* @property {(error: unknown, email: any, details?: object) => void} [onError]
|
|
66
|
+
* @property {object} [josk]
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
69
|
+
* Built-in MailTime presets keyed by use-case. Each value is a partial
|
|
70
|
+
* MailTime constructor options object — pass it through `mailTimePreset`
|
|
71
|
+
* (or spread directly) and supply your own `queue` / `transports` /
|
|
72
|
+
* `josk.adapter` / `prefix`. Presets only set the knobs that differ from
|
|
73
|
+
* MailTime defaults so the rest of the constructor stays in your hands.
|
|
74
|
+
*
|
|
75
|
+
* | Preset | Shape | Best for |
|
|
76
|
+
* |-----------------|-------|----------|
|
|
77
|
+
* | `transactional` | High retries, single SMTP per instance, no concat | Receipts, password resets, account changes, welcome emails. |
|
|
78
|
+
* | `otp` | Few retries, fast retryDelay, snappy polling, parallel SMTPs | Sign-in codes, 2FA, verification codes — stale OTPs aren't worth resending forever. |
|
|
79
|
+
* | `newsletter` | `concatEmails: true` with 5-min fold window | Scheduled digests / weekly updates / "what's new" emails. |
|
|
80
|
+
* | `marketing` | High concurrency, moderate retries, no concat | Promotional / campaign blasts where each letter is unique. |
|
|
81
|
+
* | `notifications` | `concatEmails: true` with 60-s fold window | App / social activity (likes, mentions) where bursts collapse into one letter. |
|
|
82
|
+
* | `alerts` | Many retries, fast retryDelay, modest concurrency | Ops / admin alerts: monitoring, error reports, escalations. |
|
|
83
|
+
*
|
|
84
|
+
* @type {Readonly<Record<'transactional' | 'otp' | 'newsletter' | 'marketing' | 'notifications' | 'alerts', Readonly<MailTimePresetConfig>>>}
|
|
85
|
+
*/
|
|
86
|
+
declare const PRESETS: Readonly<Record<"transactional" | "otp" | "newsletter" | "marketing" | "notifications" | "alerts", Readonly<MailTimePresetConfig>>>;
|
|
87
|
+
export {};
|
package/presets.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { deepMerge, hasOwnProp, isPlainObject, logError } from './helpers.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default `onError` hook used by every built-in preset. Logs via the
|
|
5
|
+
* shared `logError` helper and tags the line with the MailTime instance
|
|
6
|
+
* `prefix` (or `'default'` when unset) so multi-queue deployments can
|
|
7
|
+
* tell their streams apart. Defined as a regular function so `this`
|
|
8
|
+
* resolves to the MailTime instance at call time — `this.onError(...)`
|
|
9
|
+
* in `index.js` binds the receiver. Users override by passing their own
|
|
10
|
+
* `onError` through `mailTimePreset(name, { onError })` or the
|
|
11
|
+
* constructor.
|
|
12
|
+
*/
|
|
13
|
+
function defaultPresetOnError(error, email, info) {
|
|
14
|
+
logError(`[${this?.prefix || 'default'}] [onError]`, { error, email, info });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {object} MailTimePresetConfig
|
|
19
|
+
* @property {boolean} [concatEmails]
|
|
20
|
+
* @property {number} [concatDelay]
|
|
21
|
+
* @property {string} [concatSubject]
|
|
22
|
+
* @property {number} [retries]
|
|
23
|
+
* @property {number} [retryDelay]
|
|
24
|
+
* @property {number} [revolvingInterval]
|
|
25
|
+
* @property {number} [sendingTimeout]
|
|
26
|
+
* @property {'one' | 'batch'} [mode]
|
|
27
|
+
* @property {number} [concurrency]
|
|
28
|
+
* @property {(error: unknown, email: any, details?: object) => void} [onError]
|
|
29
|
+
* @property {object} [josk]
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Built-in MailTime presets keyed by use-case. Each value is a partial
|
|
34
|
+
* MailTime constructor options object — pass it through `mailTimePreset`
|
|
35
|
+
* (or spread directly) and supply your own `queue` / `transports` /
|
|
36
|
+
* `josk.adapter` / `prefix`. Presets only set the knobs that differ from
|
|
37
|
+
* MailTime defaults so the rest of the constructor stays in your hands.
|
|
38
|
+
*
|
|
39
|
+
* | Preset | Shape | Best for |
|
|
40
|
+
* |-----------------|-------|----------|
|
|
41
|
+
* | `transactional` | High retries, single SMTP per instance, no concat | Receipts, password resets, account changes, welcome emails. |
|
|
42
|
+
* | `otp` | Few retries, fast retryDelay, snappy polling, parallel SMTPs | Sign-in codes, 2FA, verification codes — stale OTPs aren't worth resending forever. |
|
|
43
|
+
* | `newsletter` | `concatEmails: true` with 5-min fold window | Scheduled digests / weekly updates / "what's new" emails. |
|
|
44
|
+
* | `marketing` | High concurrency, moderate retries, no concat | Promotional / campaign blasts where each letter is unique. |
|
|
45
|
+
* | `notifications` | `concatEmails: true` with 60-s fold window | App / social activity (likes, mentions) where bursts collapse into one letter. |
|
|
46
|
+
* | `alerts` | Many retries, fast retryDelay, modest concurrency | Ops / admin alerts: monitoring, error reports, escalations. |
|
|
47
|
+
*
|
|
48
|
+
* @type {Readonly<Record<'transactional' | 'otp' | 'newsletter' | 'marketing' | 'notifications' | 'alerts', Readonly<MailTimePresetConfig>>>}
|
|
49
|
+
*/
|
|
50
|
+
// Every preset pins `mode: 'batch'` explicitly. `'one'` would trade per-tick
|
|
51
|
+
// throughput for cluster-wide fairness across pods on the same `prefix`, but:
|
|
52
|
+
// - urgent classes (`otp`, `alerts`, `transactional`) want all due rows claimed
|
|
53
|
+
// immediately, not spread one-per-tick;
|
|
54
|
+
// - bulk classes (`newsletter`, `marketing`, `notifications`) are bursty and
|
|
55
|
+
// need fast drains during the send window;
|
|
56
|
+
// - multiple `server` pods on the same `prefix` is already an anti-pattern in
|
|
57
|
+
// this library (one JoSk lease per prefix), so the fairness payoff is moot.
|
|
58
|
+
// If you do want `'one'`, pass it via `mailTimePreset(name, { mode: 'one' })`.
|
|
59
|
+
const PRESETS = Object.freeze({
|
|
60
|
+
transactional: Object.freeze({
|
|
61
|
+
concatEmails: false,
|
|
62
|
+
retries: 30,
|
|
63
|
+
retryDelay: 10_000,
|
|
64
|
+
mode: 'batch',
|
|
65
|
+
concurrency: 1,
|
|
66
|
+
onError: defaultPresetOnError,
|
|
67
|
+
josk: Object.freeze({
|
|
68
|
+
zombieTime: 120_000,
|
|
69
|
+
}),
|
|
70
|
+
}),
|
|
71
|
+
otp: Object.freeze({
|
|
72
|
+
concatEmails: false,
|
|
73
|
+
retries: 5,
|
|
74
|
+
retryDelay: 2000,
|
|
75
|
+
revolvingInterval: 1024,
|
|
76
|
+
sendingTimeout: 60_000,
|
|
77
|
+
mode: 'batch',
|
|
78
|
+
concurrency: 4,
|
|
79
|
+
onError: defaultPresetOnError,
|
|
80
|
+
josk: Object.freeze({
|
|
81
|
+
minRevolvingDelay: 256,
|
|
82
|
+
maxRevolvingDelay: 1024,
|
|
83
|
+
zombieTime: 60_000,
|
|
84
|
+
}),
|
|
85
|
+
}),
|
|
86
|
+
newsletter: Object.freeze({
|
|
87
|
+
concatEmails: true,
|
|
88
|
+
concatDelay: 5 * 60_000,
|
|
89
|
+
concatSubject: 'Your updates',
|
|
90
|
+
retries: 5,
|
|
91
|
+
retryDelay: 60_000,
|
|
92
|
+
sendingTimeout: 600_000,
|
|
93
|
+
mode: 'batch',
|
|
94
|
+
concurrency: 2,
|
|
95
|
+
onError: defaultPresetOnError,
|
|
96
|
+
josk: Object.freeze({
|
|
97
|
+
zombieTime: 300_000,
|
|
98
|
+
}),
|
|
99
|
+
}),
|
|
100
|
+
marketing: Object.freeze({
|
|
101
|
+
concatEmails: false,
|
|
102
|
+
retries: 10,
|
|
103
|
+
retryDelay: 30_000,
|
|
104
|
+
mode: 'batch',
|
|
105
|
+
concurrency: 5,
|
|
106
|
+
onError: defaultPresetOnError,
|
|
107
|
+
josk: Object.freeze({
|
|
108
|
+
zombieTime: 180_000,
|
|
109
|
+
}),
|
|
110
|
+
}),
|
|
111
|
+
notifications: Object.freeze({
|
|
112
|
+
concatEmails: true,
|
|
113
|
+
concatDelay: 60_000,
|
|
114
|
+
concatSubject: 'New activity',
|
|
115
|
+
retries: 8,
|
|
116
|
+
retryDelay: 30_000,
|
|
117
|
+
mode: 'batch',
|
|
118
|
+
concurrency: 3,
|
|
119
|
+
onError: defaultPresetOnError,
|
|
120
|
+
josk: Object.freeze({
|
|
121
|
+
zombieTime: 180_000,
|
|
122
|
+
}),
|
|
123
|
+
}),
|
|
124
|
+
alerts: Object.freeze({
|
|
125
|
+
concatEmails: false,
|
|
126
|
+
retries: 20,
|
|
127
|
+
retryDelay: 5000,
|
|
128
|
+
revolvingInterval: 1024,
|
|
129
|
+
sendingTimeout: 60_000,
|
|
130
|
+
mode: 'batch',
|
|
131
|
+
concurrency: 2,
|
|
132
|
+
onError: defaultPresetOnError,
|
|
133
|
+
josk: Object.freeze({
|
|
134
|
+
minRevolvingDelay: 256,
|
|
135
|
+
maxRevolvingDelay: 1024,
|
|
136
|
+
zombieTime: 60_000,
|
|
137
|
+
}),
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Read-only map of preset name → partial MailTime config. Use
|
|
143
|
+
* `mailTimePreset(name, overrides)` to materialize a merged copy; use
|
|
144
|
+
* this object directly only when you want to introspect or compose
|
|
145
|
+
* presets manually.
|
|
146
|
+
*/
|
|
147
|
+
const presets = PRESETS;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @typedef {keyof typeof PRESETS} MailTimePresetName
|
|
151
|
+
*/
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Names of every built-in preset.
|
|
155
|
+
* @type {ReadonlyArray<MailTimePresetName>}
|
|
156
|
+
*/
|
|
157
|
+
const presetNames = Object.freeze(/** @type {MailTimePresetName[]} */ (Object.keys(PRESETS)));
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Materialize a MailTime constructor config from a built-in preset.
|
|
161
|
+
* The named preset is deep-cloned (so the result is freely mutable) and
|
|
162
|
+
* `overrides` is deep-merged on top — overrides win for scalar keys, and
|
|
163
|
+
* nested objects like `josk` are merged so the preset's defaults compose
|
|
164
|
+
* with the caller's `adapter`, `lockOwnerId`, `onError`, etc.
|
|
165
|
+
*
|
|
166
|
+
* ```js
|
|
167
|
+
* import { MailTime, RedisQueue, mailTimePreset } from 'mail-time';
|
|
168
|
+
* const mailTime = new MailTime(mailTimePreset('otp', {
|
|
169
|
+
* prefix: 'otp',
|
|
170
|
+
* queue: new RedisQueue({ client }),
|
|
171
|
+
* transports: [otpTransport],
|
|
172
|
+
* josk: { adapter: { type: 'redis', client } },
|
|
173
|
+
* }));
|
|
174
|
+
* ```
|
|
175
|
+
*
|
|
176
|
+
* @param {MailTimePresetName} name - one of `presetNames`
|
|
177
|
+
* @param {object} [overrides] - additional MailTime constructor options
|
|
178
|
+
* @returns {MailTimePresetConfig} fresh, mutable MailTime constructor options (preset deep-cloned + overrides merged)
|
|
179
|
+
* @throws {Error} when `name` is unknown
|
|
180
|
+
* @throws {TypeError} when `overrides` is provided but not a plain object
|
|
181
|
+
*/
|
|
182
|
+
const mailTimePreset = (name, overrides) => {
|
|
183
|
+
if (typeof name !== 'string' || !hasOwnProp(PRESETS, name)) {
|
|
184
|
+
throw new Error(`[mail-time] [mailTimePreset] unknown preset "${name}". Available: ${presetNames.join(', ')}`);
|
|
185
|
+
}
|
|
186
|
+
if (overrides !== void 0 && !isPlainObject(overrides)) {
|
|
187
|
+
throw new TypeError('[mail-time] [mailTimePreset] {overrides} must be a plain object when provided');
|
|
188
|
+
}
|
|
189
|
+
const cloned = deepMerge({}, PRESETS[name]);
|
|
190
|
+
return overrides ? deepMerge(cloned, overrides) : cloned;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export { mailTimePreset, presets, presetNames };
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import { logError } from '../helpers.js';
|
|
2
|
-
|
|
3
|
-
/** Class representing Example Queue for MailTime */
|
|
4
|
-
class BlankQueue {
|
|
5
|
-
/**
|
|
6
|
-
* Create a BlankQueue instance
|
|
7
|
-
* @param {object} opts - configuration object
|
|
8
|
-
* @param {object} opts.requiredOption - Required option description
|
|
9
|
-
* @param {string} [opts.prefix] - Optional prefix for scope isolation; use when creating multiple MailTime instances within the single application
|
|
10
|
-
*/
|
|
11
|
-
constructor (opts) {
|
|
12
|
-
this.name = 'queue-name';
|
|
13
|
-
if (!opts || typeof opts !== 'object' || opts === null) {
|
|
14
|
-
throw new TypeError('[mail-time] Configuration object must be passed into BlankQueue constructor');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (!opts.requiredOption) {
|
|
18
|
-
throw new Error('[mail-time] [BlankQueue] required {requiredOption} option is missing');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
this.prefix = (typeof opts.prefix === 'string') ? opts.prefix : 'default';
|
|
22
|
-
this.uniqueName = `mailtime:${this.prefix}`;
|
|
23
|
-
this.requiredOption = opts.requiredOption;
|
|
24
|
-
|
|
25
|
-
// Stored JSON structure:
|
|
26
|
-
// uuid {string}
|
|
27
|
-
// to {string|[string]}
|
|
28
|
-
// tries {number} - qty of send attempts
|
|
29
|
-
// sendAt {number} - When letter should be sent
|
|
30
|
-
// isSent {boolean} - Email status
|
|
31
|
-
// isCancelled {boolean} - `true` if email was cancelled before it was sent
|
|
32
|
-
// isFailed {boolean} - `true` if email has failed to send
|
|
33
|
-
// template {string} - Template for this email
|
|
34
|
-
// transport {number} - Last used transport
|
|
35
|
-
// concatSubject {string|boolean} - Email concatenation subject
|
|
36
|
-
// ---
|
|
37
|
-
// mailOptions {[object]} - Array of nodeMailer's `mailOptions`
|
|
38
|
-
// mailOptions.to {string|[string]} - [REQUIRED]
|
|
39
|
-
// mailOptions.from {string}
|
|
40
|
-
// mailOptions.text {string|boolean}
|
|
41
|
-
// mailOptions.html {string}
|
|
42
|
-
// mailOptions.subject {string}
|
|
43
|
-
// mailOptions.Other nodeMailer `sendMail` options...
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* @async
|
|
48
|
-
* @memberOf BlankQueue
|
|
49
|
-
* @name ping
|
|
50
|
-
* @description Check connection to Storage
|
|
51
|
-
* @returns {Promise<object>}
|
|
52
|
-
*/
|
|
53
|
-
async ping() {
|
|
54
|
-
// LEAVE THIS if BLOCK UNCHANGED
|
|
55
|
-
if (!this.mailTimeInstance) {
|
|
56
|
-
return {
|
|
57
|
-
status: 'Service Unavailable',
|
|
58
|
-
code: 503,
|
|
59
|
-
statusCode: 503,
|
|
60
|
-
error: new Error('MailTime instance not yet assigned to {mailTimeInstance} of Queue Adapter context'),
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
const ping = await this.requiredOption.ping();
|
|
66
|
-
if (ping === 'PONG') {
|
|
67
|
-
return {
|
|
68
|
-
status: 'OK',
|
|
69
|
-
code: 200,
|
|
70
|
-
statusCode: 200,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
throw new Error(`Unexpected response from Storage#ping received: ${ping}`);
|
|
75
|
-
} catch (pingError) {
|
|
76
|
-
return {
|
|
77
|
-
status: 'Internal Server Error',
|
|
78
|
-
code: 500,
|
|
79
|
-
statusCode: 500,
|
|
80
|
-
error: pingError
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* @memberOf BlankQueue
|
|
87
|
-
* @name iterate
|
|
88
|
-
* @description iterate over queued emails passing to `mailTimeInstance.___send` method
|
|
89
|
-
* @returns {void 0}
|
|
90
|
-
*/
|
|
91
|
-
async iterate() {
|
|
92
|
-
// GET EMAILS WITHIN this.uniqueName SCOPE!
|
|
93
|
-
// AND ONLY WHERE sendAt <= now
|
|
94
|
-
// RUN ONE BY ONE VIA this.mailTimeInstance.___send() METHOD
|
|
95
|
-
const cursorWithEmails = this.requiredOption.getEmails({
|
|
96
|
-
scope: this.uniqueName,
|
|
97
|
-
isSent: false,
|
|
98
|
-
isFailed: false,
|
|
99
|
-
isCancelled: false,
|
|
100
|
-
sendAt: {
|
|
101
|
-
$lte: Date.now() // Number (timestamp)
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
for await (const emailObj of cursorWithEmails) {
|
|
106
|
-
await this.mailTimeInstance.___send(emailObj);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* @async
|
|
112
|
-
* @memberOf BlankQueue
|
|
113
|
-
* @name getPendingTo
|
|
114
|
-
* @description get queued email by `to` field (addressee)
|
|
115
|
-
* @param to {string} - email address
|
|
116
|
-
* @param sendAt {number} - timestamp
|
|
117
|
-
* @returns {Promise<object|null>}
|
|
118
|
-
*/
|
|
119
|
-
async getPendingTo(to, sendAt) {
|
|
120
|
-
if (typeof to !== 'string' || typeof sendAt !== 'number') {
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const email = await this.requiredOption.get({
|
|
125
|
-
to: to,
|
|
126
|
-
isSent: false,
|
|
127
|
-
isFailed: false,
|
|
128
|
-
isCancelled: false,
|
|
129
|
-
sendAt: {
|
|
130
|
-
$lte: sendAt // Number (timestamp)
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
if (!email || email.isSent === true || email.isCancelled === true || email.isFailed === true) {
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return email;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* @async
|
|
143
|
-
* @memberOf BlankQueue
|
|
144
|
-
* @name push
|
|
145
|
-
* @description push email to the queue/storage
|
|
146
|
-
* @param email {object} - email's object
|
|
147
|
-
* @returns {Promise<void 0>}
|
|
148
|
-
*/
|
|
149
|
-
async push(email) {
|
|
150
|
-
if (typeof email !== 'object') {
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (email.sendAt instanceof Date) {
|
|
155
|
-
email.sendAt = +email.sendAt;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
await this.requiredOption.save(email);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* @async
|
|
163
|
-
* @memberOf BlankQueue
|
|
164
|
-
* @name cancel
|
|
165
|
-
* @description cancel scheduled email
|
|
166
|
-
* @param uuid {string} - email's uuid
|
|
167
|
-
* @returns {Promise<boolean>} returns `true` if cancelled or `false` if not found, was sent, or was cancelled previously
|
|
168
|
-
*/
|
|
169
|
-
async cancel(uuid) {
|
|
170
|
-
if (typeof uuid !== 'string') {
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const email = JSON.parse(await this.requiredOption.get({
|
|
175
|
-
uuid: uuid
|
|
176
|
-
}));
|
|
177
|
-
|
|
178
|
-
if (!email || email.isSent === true || email.isCancelled === true) {
|
|
179
|
-
return false;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (!this.mailTimeInstance.keepHistory) {
|
|
183
|
-
return await this.remove(email);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return await this.update(email, {
|
|
187
|
-
isCancelled: true,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* @async
|
|
193
|
-
* @memberOf BlankQueue
|
|
194
|
-
* @name remove
|
|
195
|
-
* @description remove email from queue
|
|
196
|
-
* @param email {object} - email's object
|
|
197
|
-
* @returns {Promise<boolean>} returns `true` if removed or `false` if not found
|
|
198
|
-
*/
|
|
199
|
-
async remove(email) {
|
|
200
|
-
if (typeof email !== 'object' || typeof email.uuid !== 'string') {
|
|
201
|
-
return false;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return await this.requiredOption.remove({
|
|
205
|
-
uuid: email.uuid
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* @async
|
|
211
|
-
* @memberOf BlankQueue
|
|
212
|
-
* @name update
|
|
213
|
-
* @description remove email from queue
|
|
214
|
-
* @param email {object} - email's object
|
|
215
|
-
* @param updateObj {object} - fields with new values to update
|
|
216
|
-
* @returns {Promise<boolean>} returns `true` if updated or `false` if not found or no changes was made
|
|
217
|
-
*/
|
|
218
|
-
async update(email, updateObj) {
|
|
219
|
-
if (typeof email !== 'object' || typeof email.uuid !== 'string' || typeof updateObj !== 'object') {
|
|
220
|
-
return false;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const updatedEmail = { ...email, ...updateObj };
|
|
224
|
-
return await this.requiredOption.update({
|
|
225
|
-
uuid: email.uuid
|
|
226
|
-
}, updatedEmail);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export { BlankQueue };
|