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/package.json CHANGED
@@ -1,22 +1,64 @@
1
1
  {
2
2
  "name": "mail-time",
3
- "version": "1.4.0",
4
- "description": "Bulletproof email queue on top of NodeMailer for a single and multi-server setups",
3
+ "version": "4.0.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
- "import": "./index.js",
9
- "require": "./index.cjs"
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 ./test/npm.js"
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:types": "tsc -p ./tsconfig.types.json",
51
+ "test:redis": "mocha ./test/npm-redis-redis.js",
52
+ "test:mongo": "mocha ./test/npm-mongo-mongo.js",
53
+ "test:postgres": "mocha ./test/npm-postgres.js",
54
+ "test:bun": "bun test ./test/jest"
14
55
  },
15
56
  "keywords": [
16
57
  "backup",
17
58
  "balanced",
18
59
  "balancer",
19
60
  "bulletproof",
61
+ "bun",
20
62
  "cluster",
21
63
  "email",
22
64
  "highload",
@@ -29,7 +71,9 @@
29
71
  "microservice",
30
72
  "micro-service",
31
73
  "nodemailer",
74
+ "postgres",
32
75
  "queue",
76
+ "scheduler",
33
77
  "template"
34
78
  ],
35
79
  "tags": [
@@ -37,6 +81,7 @@
37
81
  "balanced",
38
82
  "balancer",
39
83
  "bulletproof",
84
+ "bun",
40
85
  "cluster",
41
86
  "email",
42
87
  "highload",
@@ -49,7 +94,9 @@
49
94
  "microservice",
50
95
  "micro-service",
51
96
  "nodemailer",
97
+ "postgres",
52
98
  "queue",
99
+ "scheduler",
53
100
  "template"
54
101
  ],
55
102
  "repository": {
@@ -63,19 +110,22 @@
63
110
  "author": "dr.dimitru (https://veliovgroup.com)",
64
111
  "license": "BSD-3-Clause",
65
112
  "engines": {
66
- "node": ">=14.1.0"
113
+ "node": ">=20.9.0"
67
114
  },
68
115
  "dependencies": {
69
- "deepmerge": "^4.3.1",
70
- "josk": "^3.1.1"
116
+ "josk": "^6.1.0"
71
117
  },
72
118
  "devDependencies": {
73
- "bson": "^6.6.0",
74
- "bson-ext": "^4.0.3",
75
- "chai": "^4.4.1",
76
- "mocha": "^10.3.0",
77
- "mongodb": "^6.5.0",
78
- "nodemailer": "^6.9.12",
79
- "nodemailer-direct-transport": "^3.3.2"
119
+ "@jest/globals": "^30.4.1",
120
+ "@types/node": "^25.9.0",
121
+ "chai": "^6.2.2",
122
+ "jest": "^30.4.2",
123
+ "mocha": "^11.7.5",
124
+ "mongodb": "^7.2.0",
125
+ "nodemailer": "^8.0.7",
126
+ "pg": "^8.21.0",
127
+ "redis": "^5.12.1",
128
+ "rollup": "^4.60.4",
129
+ "typescript": "^6.0.3"
80
130
  }
81
131
  }
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: object, 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 {object} fresh, mutable MailTime constructor options
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): object;
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: object, 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, 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: object, 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 {object} fresh, mutable MailTime constructor options
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' || !Object.hasOwn(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 };