mail-time 4.1.0 → 5.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/package.json CHANGED
@@ -1,10 +1,26 @@
1
1
  {
2
2
  "name": "mail-time",
3
- "version": "4.1.0",
3
+ "version": "5.1.0",
4
4
  "description": "📮 Email queue extending nodemailer with multi-SMTP transports and horizontally scaled apps support",
5
5
  "main": "index.js",
6
6
  "types": "./index.d.ts",
7
7
  "type": "module",
8
+ "files": [
9
+ "adapters/mongo.d.ts",
10
+ "adapters/mongo.js",
11
+ "adapters/postgres.d.ts",
12
+ "adapters/postgres.js",
13
+ "adapters/redis.d.ts",
14
+ "adapters/redis.js",
15
+ "helpers.js",
16
+ "index.cjs",
17
+ "index.d.cts",
18
+ "index.d.ts",
19
+ "index.js",
20
+ "presets.d.ts",
21
+ "presets.js",
22
+ "scripts/migrate-redis-queue-to-cluster.mjs"
23
+ ],
8
24
  "exports": {
9
25
  ".": {
10
26
  "import": {
@@ -48,6 +64,7 @@
48
64
  "test:jest": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --config jest.config.mjs",
49
65
  "test:mocha": "mocha ./test/npm.js",
50
66
  "test:mocha:redis": "mocha ./test/npm-redis-redis.js",
67
+ "test:mocha:redis-cluster": "mocha ./test/npm-redis-cluster.js",
51
68
  "test:mocha:mongo": "mocha ./test/npm-redis-mongo.js ./test/npm-mongo-redis.js ./test/npm-ha-mongo-redis.js",
52
69
  "test:mocha:postgres": "mocha ./test/npm-postgres.js",
53
70
  "test:types": "tsc -p ./tsconfig.types.json",
@@ -125,16 +142,16 @@
125
142
  }
126
143
  },
127
144
  "dependencies": {
128
- "josk": "^6.2.0"
145
+ "josk": "^6.3.0"
129
146
  },
130
147
  "devDependencies": {
131
148
  "@jest/globals": "^30.4.1",
132
149
  "@types/node": "^25.9.0",
133
150
  "chai": "^6.2.2",
134
151
  "jest": "^30.4.2",
135
- "mocha": "^11.7.5",
152
+ "mocha": "^11.8.0",
136
153
  "mongodb": "^7.2.0",
137
- "nodemailer": "^8.0.7",
154
+ "nodemailer": "^9.0.5",
138
155
  "pg": "^8.21.0",
139
156
  "redis": "^5.12.1",
140
157
  "rollup": "^4.60.4",
package/presets.js CHANGED
@@ -73,7 +73,7 @@ const PRESETS = Object.freeze({
73
73
  retries: 5,
74
74
  retryDelay: 2000,
75
75
  revolvingInterval: 1024,
76
- sendingTimeout: 60_000,
76
+ sendingTimeout: 120_000,
77
77
  mode: 'batch',
78
78
  concurrency: 4,
79
79
  onError: defaultPresetOnError,
@@ -126,7 +126,7 @@ const PRESETS = Object.freeze({
126
126
  retries: 20,
127
127
  retryDelay: 5000,
128
128
  revolvingInterval: 1024,
129
- sendingTimeout: 60_000,
129
+ sendingTimeout: 120_000,
130
130
  mode: 'batch',
131
131
  concurrency: 2,
132
132
  onError: defaultPresetOnError,
@@ -0,0 +1,108 @@
1
+ import { createClient, createCluster } from 'redis';
2
+
3
+ const args = process.argv.slice(2);
4
+ const readArg = (name) => {
5
+ const index = args.indexOf(name);
6
+ return index === -1 ? void 0 : args[index + 1];
7
+ };
8
+ const sourceUrl = readArg('--source');
9
+ const targetUrl = readArg('--target');
10
+ const prefix = readArg('--prefix');
11
+ const sendingTimeout = Number(readArg('--sending-timeout') || 300000);
12
+ const overwrite = args.includes('--overwrite');
13
+
14
+ if (!sourceUrl || !targetUrl || !prefix || !Number.isFinite(sendingTimeout) || sendingTimeout <= 0) {
15
+ throw new Error('Usage: node scripts/migrate-redis-queue-to-cluster.mjs --source <redis-url> --target <cluster-url> --prefix <prefix> [--sending-timeout <ms>] [--overwrite]');
16
+ }
17
+
18
+ if (!/^[A-Za-z0-9_\-:.]+$/.test(prefix)) {
19
+ throw new Error(`{prefix} must match /^[A-Za-z0-9_\\-:.]+$/ (received: "${prefix}")`);
20
+ }
21
+
22
+ const legacyName = `mailtime:${prefix}`;
23
+ const taggedName = `mailtime:{${prefix}}`;
24
+ const lettersKey = `${taggedName}:letters`;
25
+ const scheduleKey = `${taggedName}:schedule`;
26
+ const concatKeysKey = `${taggedName}:concatkeys`;
27
+ const source = await createClient({ url: sourceUrl }).connect();
28
+ const target = createCluster({ rootNodes: [{ url: targetUrl }] });
29
+
30
+ const scan = async function * (client, pattern) {
31
+ for await (const batch of client.scanIterator({ TYPE: 'string', MATCH: pattern, COUNT: 9999 })) {
32
+ for (const key of (Array.isArray(batch) ? batch : [batch])) {
33
+ yield key;
34
+ }
35
+ }
36
+ };
37
+
38
+ const nextEligibleAt = (task) => {
39
+ if (task.isSent === true || task.isFailed === true || task.isCancelled === true) {
40
+ return null;
41
+ }
42
+ if (task.isSending === true) {
43
+ return (typeof task.sendingAt === 'number' ? task.sendingAt : 0) + sendingTimeout;
44
+ }
45
+ return +task.sendAt;
46
+ };
47
+
48
+ try {
49
+ await target.connect();
50
+ const trackedConcatKeys = await target.zRange(concatKeysKey, 0, -1);
51
+ for (const key of trackedConcatKeys) {
52
+ if (await target.pTTL(key) === -2) {
53
+ await target.zRem(concatKeysKey, key);
54
+ }
55
+ }
56
+ const targetHasData = (await target.hLen(lettersKey)) > 0
57
+ || (await target.zCard(scheduleKey)) > 0
58
+ || (await target.zCard(concatKeysKey)) > 0;
59
+ if (targetHasData && !overwrite) {
60
+ throw new Error(`Target queue "${taggedName}" is not empty. Pass --overwrite only after verifying its contents can be replaced.`);
61
+ }
62
+ if (targetHasData) {
63
+ const concatKeys = await target.zRange(concatKeysKey, 0, -1);
64
+ if (concatKeys.length) {
65
+ await target.del(concatKeys);
66
+ }
67
+ await target.del([lettersKey, scheduleKey, concatKeysKey]);
68
+ }
69
+
70
+ let migrated = 0;
71
+ for await (const key of scan(source, `${legacyName}:letter:*`)) {
72
+ const payload = await source.get(key);
73
+ if (!payload) {
74
+ continue;
75
+ }
76
+ const task = JSON.parse(payload);
77
+ if (!task || typeof task.uuid !== 'string') {
78
+ continue;
79
+ }
80
+ await target.hSet(lettersKey, task.uuid, payload);
81
+ const eligibleAt = nextEligibleAt(task);
82
+ if (eligibleAt !== null && Number.isFinite(eligibleAt)) {
83
+ await target.zAdd(scheduleKey, { score: eligibleAt, value: task.uuid });
84
+ }
85
+ migrated++;
86
+ }
87
+
88
+ for await (const key of scan(source, `${legacyName}:concatletter:*`)) {
89
+ const uuid = await source.get(key);
90
+ if (!uuid) {
91
+ continue;
92
+ }
93
+ const ttl = await source.pTTL(key);
94
+ const to = key.slice(`${legacyName}:concatletter:`.length);
95
+ const targetKey = `${taggedName}:concatletter:${to}`;
96
+ if (ttl > 0) {
97
+ await target.set(targetKey, uuid, { PX: ttl });
98
+ await target.zAdd(concatKeysKey, { score: Date.now() + ttl, value: targetKey });
99
+ } else if (ttl === -1) {
100
+ await target.set(targetKey, uuid);
101
+ await target.zAdd(concatKeysKey, { score: Number.MAX_SAFE_INTEGER, value: targetKey });
102
+ }
103
+ }
104
+
105
+ console.info(`Migrated ${migrated} RedisQueue tasks from "${legacyName}" to "${taggedName}".`);
106
+ } finally {
107
+ await Promise.all([source.close(), target.close()]);
108
+ }