@astralibx/email-rule-engine 12.1.0 → 12.3.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/README.md +20 -0
- package/dist/index.cjs +29 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.mjs +29 -29
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -113,6 +113,26 @@ See [docs/configuration.md](https://github.com/Hariprakash1997/astralib/blob/mai
|
|
|
113
113
|
|
|
114
114
|
Reference: [API Routes](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/api-routes.md) | [Programmatic API](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/programmatic-api.md) | [Types](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/types.md) | [Constants](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/constants.md) | [Error Handling](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/error-handling.md)
|
|
115
115
|
|
|
116
|
+
### Redis Key Prefix (Required for Multi-Project Deployments)
|
|
117
|
+
|
|
118
|
+
> **WARNING:** If multiple projects share the same Redis server, you MUST set a unique `keyPrefix` per project. Without this, run locks, cancel flags, and progress keys will collide between projects.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
const engine = createEmailRuleEngine({
|
|
122
|
+
redis: {
|
|
123
|
+
connection: redis,
|
|
124
|
+
keyPrefix: 'myproject:', // REQUIRED if sharing Redis
|
|
125
|
+
},
|
|
126
|
+
// ...
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
| Default | Risk |
|
|
131
|
+
|---------|------|
|
|
132
|
+
| `''` (empty) | Two projects share global keys like `email-rule-runner:lock` and `run:{id}:cancel` — Project A can cancel Project B's runs |
|
|
133
|
+
|
|
134
|
+
**Always set a unique prefix** like `projectname:` when sharing Redis.
|
|
135
|
+
|
|
116
136
|
> **Important:** Configure throttle settings before running rules. Default limits (1/day, 2/week) may be too restrictive. See [Throttling](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/throttling.md).
|
|
117
137
|
|
|
118
138
|
## License
|
package/dist/index.cjs
CHANGED
|
@@ -1022,7 +1022,7 @@ var RuleRunnerService = class {
|
|
|
1022
1022
|
}
|
|
1023
1023
|
return true;
|
|
1024
1024
|
});
|
|
1025
|
-
this.config.hooks?.onRunStart?.({ rulesCount: activeRules.length, triggeredBy });
|
|
1025
|
+
this.config.hooks?.onRunStart?.({ rulesCount: activeRules.length, triggeredBy, runId });
|
|
1026
1026
|
await this.updateRunProgress(runId, {
|
|
1027
1027
|
progress: { rulesTotal: activeRules.length, rulesCompleted: 0, sent: 0, failed: 0, skipped: 0, invalid: 0 }
|
|
1028
1028
|
});
|
|
@@ -1111,7 +1111,7 @@ var RuleRunnerService = class {
|
|
|
1111
1111
|
status: runStatus
|
|
1112
1112
|
});
|
|
1113
1113
|
await this.updateRunProgress(runId, { status: runStatus, currentRule: "", elapsed: Date.now() - runStartTime });
|
|
1114
|
-
this.config.hooks?.onRunComplete?.({ duration: Date.now() - runStartTime, totalStats, perRuleStats });
|
|
1114
|
+
this.config.hooks?.onRunComplete?.({ duration: Date.now() - runStartTime, totalStats, perRuleStats, runId });
|
|
1115
1115
|
this.logger.info("Rule run completed", {
|
|
1116
1116
|
triggeredBy,
|
|
1117
1117
|
rulesProcessed: activeRules.length,
|
|
@@ -1150,7 +1150,7 @@ var RuleRunnerService = class {
|
|
|
1150
1150
|
stats.matched = emailsToProcess.length;
|
|
1151
1151
|
const ruleId = rule._id.toString();
|
|
1152
1152
|
const templateId = rule.templateId.toString();
|
|
1153
|
-
this.config.hooks?.onRuleStart?.({ ruleId, ruleName: rule.name, matchedCount: emailsToProcess.length });
|
|
1153
|
+
this.config.hooks?.onRuleStart?.({ ruleId, ruleName: rule.name, matchedCount: emailsToProcess.length, templateId, runId: runId || "" });
|
|
1154
1154
|
if (emailsToProcess.length === 0) return stats;
|
|
1155
1155
|
const identifierResults = await processInChunks(
|
|
1156
1156
|
emailsToProcess,
|
|
@@ -1195,7 +1195,7 @@ var RuleRunnerService = class {
|
|
|
1195
1195
|
const identifier = identifierMap.get(email);
|
|
1196
1196
|
if (!identifier) {
|
|
1197
1197
|
stats.skipped++;
|
|
1198
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "invalid" });
|
|
1198
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "invalid", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "invalid email" });
|
|
1199
1199
|
continue;
|
|
1200
1200
|
}
|
|
1201
1201
|
const dedupKey = identifier.id;
|
|
@@ -1203,27 +1203,27 @@ var RuleRunnerService = class {
|
|
|
1203
1203
|
if (lastSend) {
|
|
1204
1204
|
if (rule.sendOnce && !rule.resendAfterDays) {
|
|
1205
1205
|
stats.skipped++;
|
|
1206
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped" });
|
|
1206
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "send once" });
|
|
1207
1207
|
continue;
|
|
1208
1208
|
}
|
|
1209
1209
|
if (rule.resendAfterDays) {
|
|
1210
1210
|
const daysSince = (Date.now() - new Date(lastSend.sentAt).getTime()) / MS_PER_DAY;
|
|
1211
1211
|
if (daysSince < rule.resendAfterDays) {
|
|
1212
1212
|
stats.skipped++;
|
|
1213
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped" });
|
|
1213
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "resend too soon" });
|
|
1214
1214
|
continue;
|
|
1215
1215
|
}
|
|
1216
1216
|
} else {
|
|
1217
1217
|
stats.skipped++;
|
|
1218
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped" });
|
|
1218
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "send once" });
|
|
1219
1219
|
continue;
|
|
1220
1220
|
}
|
|
1221
1221
|
}
|
|
1222
|
-
if (!this.checkThrottle(rule, dedupKey, email, throttleMap, throttleConfig, stats)) continue;
|
|
1222
|
+
if (!this.checkThrottle(rule, dedupKey, email, throttleMap, throttleConfig, stats, templateId, runId)) continue;
|
|
1223
1223
|
const agentSelection = await this.config.adapters.selectAgent(identifier.id, { ruleId, templateId });
|
|
1224
1224
|
if (!agentSelection) {
|
|
1225
1225
|
stats.skipped++;
|
|
1226
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped" });
|
|
1226
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "no account available" });
|
|
1227
1227
|
continue;
|
|
1228
1228
|
}
|
|
1229
1229
|
const user = { _id: identifier.id, email };
|
|
@@ -1274,7 +1274,7 @@ var RuleRunnerService = class {
|
|
|
1274
1274
|
} catch (hookErr) {
|
|
1275
1275
|
this.logger.error(`beforeSend hook failed for email ${email}: ${hookErr.message}`);
|
|
1276
1276
|
stats.errorCount++;
|
|
1277
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "error" });
|
|
1277
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "error", accountId: agentSelection.accountId, templateId, runId: runId || "", subjectIndex: si, bodyIndex: bi, failureReason: hookErr.message });
|
|
1278
1278
|
continue;
|
|
1279
1279
|
}
|
|
1280
1280
|
}
|
|
@@ -1302,7 +1302,7 @@ var RuleRunnerService = class {
|
|
|
1302
1302
|
lastSentDate: /* @__PURE__ */ new Date()
|
|
1303
1303
|
});
|
|
1304
1304
|
stats.sent++;
|
|
1305
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "sent" });
|
|
1305
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "sent", accountId: agentSelection.accountId, templateId, runId: runId || "", subjectIndex: si, bodyIndex: bi });
|
|
1306
1306
|
totalProcessed++;
|
|
1307
1307
|
if (runId && totalProcessed % 10 === 0) {
|
|
1308
1308
|
await this.updateRunSendProgress(runId, stats);
|
|
@@ -1317,7 +1317,7 @@ var RuleRunnerService = class {
|
|
|
1317
1317
|
}
|
|
1318
1318
|
} catch (err) {
|
|
1319
1319
|
stats.errorCount++;
|
|
1320
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "error" });
|
|
1320
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "error", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: err.message || "unknown error" });
|
|
1321
1321
|
this.logger.error(`Rule "${rule.name}" failed for identifier ${email}`, { error: err });
|
|
1322
1322
|
}
|
|
1323
1323
|
}
|
|
@@ -1340,7 +1340,7 @@ var RuleRunnerService = class {
|
|
|
1340
1340
|
this.logger.info(`Rule '${rule.name}' auto-disabled \u2014 all identifiers processed`);
|
|
1341
1341
|
}
|
|
1342
1342
|
}
|
|
1343
|
-
this.config.hooks?.onRuleComplete?.({ ruleId, ruleName: rule.name, stats });
|
|
1343
|
+
this.config.hooks?.onRuleComplete?.({ ruleId, ruleName: rule.name, stats, templateId, runId: runId || "" });
|
|
1344
1344
|
return stats;
|
|
1345
1345
|
}
|
|
1346
1346
|
async executeQueryMode(rule, template, throttleMap, throttleConfig, stats, runId) {
|
|
@@ -1353,7 +1353,7 @@ var RuleRunnerService = class {
|
|
|
1353
1353
|
return stats;
|
|
1354
1354
|
}
|
|
1355
1355
|
stats.matched = users.length;
|
|
1356
|
-
this.config.hooks?.onRuleStart?.({ ruleId: rule._id.toString(), ruleName: rule.name, matchedCount: users.length });
|
|
1356
|
+
this.config.hooks?.onRuleStart?.({ ruleId: rule._id.toString(), ruleName: rule.name, matchedCount: users.length, templateId: rule.templateId.toString(), runId: runId || "" });
|
|
1357
1357
|
if (users.length === 0) return stats;
|
|
1358
1358
|
const userIds = users.map((u) => u._id?.toString()).filter(Boolean);
|
|
1359
1359
|
const emails = users.map((u) => u.email).filter(Boolean);
|
|
@@ -1402,40 +1402,40 @@ var RuleRunnerService = class {
|
|
|
1402
1402
|
const email = user.email;
|
|
1403
1403
|
if (!userId || !email) {
|
|
1404
1404
|
stats.skipped++;
|
|
1405
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email: email || "unknown", status: "invalid" });
|
|
1405
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email: email || "unknown", status: "invalid", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "invalid email" });
|
|
1406
1406
|
continue;
|
|
1407
1407
|
}
|
|
1408
1408
|
const lastSend = sendMap.get(userId);
|
|
1409
1409
|
if (lastSend) {
|
|
1410
1410
|
if (rule.sendOnce && !rule.resendAfterDays) {
|
|
1411
1411
|
stats.skipped++;
|
|
1412
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped" });
|
|
1412
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "send once" });
|
|
1413
1413
|
continue;
|
|
1414
1414
|
}
|
|
1415
1415
|
if (rule.resendAfterDays) {
|
|
1416
1416
|
const daysSince = (Date.now() - new Date(lastSend.sentAt).getTime()) / MS_PER_DAY;
|
|
1417
1417
|
if (daysSince < rule.resendAfterDays) {
|
|
1418
1418
|
stats.skipped++;
|
|
1419
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped" });
|
|
1419
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "resend too soon" });
|
|
1420
1420
|
continue;
|
|
1421
1421
|
}
|
|
1422
1422
|
} else {
|
|
1423
1423
|
stats.skipped++;
|
|
1424
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped" });
|
|
1424
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "send once" });
|
|
1425
1425
|
continue;
|
|
1426
1426
|
}
|
|
1427
1427
|
}
|
|
1428
1428
|
const identifier = identifierMap.get(email.toLowerCase().trim());
|
|
1429
1429
|
if (!identifier) {
|
|
1430
1430
|
stats.skipped++;
|
|
1431
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "invalid" });
|
|
1431
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "invalid", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "invalid email" });
|
|
1432
1432
|
continue;
|
|
1433
1433
|
}
|
|
1434
|
-
if (!this.checkThrottle(rule, userId, email, throttleMap, throttleConfig, stats)) continue;
|
|
1434
|
+
if (!this.checkThrottle(rule, userId, email, throttleMap, throttleConfig, stats, templateId, runId)) continue;
|
|
1435
1435
|
const agentSelection = await this.config.adapters.selectAgent(identifier.id, { ruleId, templateId });
|
|
1436
1436
|
if (!agentSelection) {
|
|
1437
1437
|
stats.skipped++;
|
|
1438
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped" });
|
|
1438
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "no account available" });
|
|
1439
1439
|
continue;
|
|
1440
1440
|
}
|
|
1441
1441
|
const resolvedData = this.config.adapters.resolveData(user);
|
|
@@ -1485,7 +1485,7 @@ var RuleRunnerService = class {
|
|
|
1485
1485
|
} catch (hookErr) {
|
|
1486
1486
|
this.logger.error(`beforeSend hook failed for email ${email}: ${hookErr.message}`);
|
|
1487
1487
|
stats.errorCount++;
|
|
1488
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "error" });
|
|
1488
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "error", accountId: agentSelection.accountId, templateId, runId: runId || "", subjectIndex: si, bodyIndex: bi, failureReason: hookErr.message });
|
|
1489
1489
|
continue;
|
|
1490
1490
|
}
|
|
1491
1491
|
}
|
|
@@ -1513,7 +1513,7 @@ var RuleRunnerService = class {
|
|
|
1513
1513
|
lastSentDate: /* @__PURE__ */ new Date()
|
|
1514
1514
|
});
|
|
1515
1515
|
stats.sent++;
|
|
1516
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "sent" });
|
|
1516
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "sent", accountId: agentSelection.accountId, templateId, runId: runId || "", subjectIndex: si, bodyIndex: bi });
|
|
1517
1517
|
totalProcessed++;
|
|
1518
1518
|
if (runId && totalProcessed % 10 === 0) {
|
|
1519
1519
|
await this.updateRunSendProgress(runId, stats);
|
|
@@ -1528,7 +1528,7 @@ var RuleRunnerService = class {
|
|
|
1528
1528
|
}
|
|
1529
1529
|
} catch (err) {
|
|
1530
1530
|
stats.errorCount++;
|
|
1531
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email: user.email || "unknown", status: "error" });
|
|
1531
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email: user.email || "unknown", status: "error", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: err.message || "unknown error" });
|
|
1532
1532
|
this.logger.error(`Rule "${rule.name}" failed for user ${user._id?.toString()}`, { error: err });
|
|
1533
1533
|
}
|
|
1534
1534
|
}
|
|
@@ -1536,27 +1536,27 @@ var RuleRunnerService = class {
|
|
|
1536
1536
|
$set: { lastRunAt: /* @__PURE__ */ new Date(), lastRunStats: stats },
|
|
1537
1537
|
$inc: { totalSent: stats.sent, totalSkipped: stats.skipped }
|
|
1538
1538
|
});
|
|
1539
|
-
this.config.hooks?.onRuleComplete?.({ ruleId, ruleName: rule.name, stats });
|
|
1539
|
+
this.config.hooks?.onRuleComplete?.({ ruleId, ruleName: rule.name, stats, templateId, runId: runId || "" });
|
|
1540
1540
|
return stats;
|
|
1541
1541
|
}
|
|
1542
|
-
checkThrottle(rule, userId, email, throttleMap, config, stats) {
|
|
1542
|
+
checkThrottle(rule, userId, email, throttleMap, config, stats, templateId, runId) {
|
|
1543
1543
|
if (rule.emailType === EMAIL_TYPE.Transactional || rule.bypassThrottle) return true;
|
|
1544
1544
|
const userThrottle = throttleMap.get(userId) || { today: 0, thisWeek: 0, lastSentDate: null };
|
|
1545
1545
|
if (userThrottle.today >= config.maxPerUserPerDay) {
|
|
1546
1546
|
stats.skippedByThrottle++;
|
|
1547
|
-
this.config.hooks?.onSend?.({ ruleId: rule._id.toString(), ruleName: rule.name, email, status: "throttled" });
|
|
1547
|
+
this.config.hooks?.onSend?.({ ruleId: rule._id.toString(), ruleName: rule.name, email, status: "throttled", accountId: "", templateId: templateId || "", runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "daily throttle limit" });
|
|
1548
1548
|
return false;
|
|
1549
1549
|
}
|
|
1550
1550
|
if (userThrottle.thisWeek >= config.maxPerUserPerWeek) {
|
|
1551
1551
|
stats.skippedByThrottle++;
|
|
1552
|
-
this.config.hooks?.onSend?.({ ruleId: rule._id.toString(), ruleName: rule.name, email, status: "throttled" });
|
|
1552
|
+
this.config.hooks?.onSend?.({ ruleId: rule._id.toString(), ruleName: rule.name, email, status: "throttled", accountId: "", templateId: templateId || "", runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "weekly throttle limit" });
|
|
1553
1553
|
return false;
|
|
1554
1554
|
}
|
|
1555
1555
|
if (userThrottle.lastSentDate) {
|
|
1556
1556
|
const daysSinceLastSend = (Date.now() - userThrottle.lastSentDate.getTime()) / MS_PER_DAY;
|
|
1557
1557
|
if (daysSinceLastSend < config.minGapDays) {
|
|
1558
1558
|
stats.skippedByThrottle++;
|
|
1559
|
-
this.config.hooks?.onSend?.({ ruleId: rule._id.toString(), ruleName: rule.name, email, status: "throttled" });
|
|
1559
|
+
this.config.hooks?.onSend?.({ ruleId: rule._id.toString(), ruleName: rule.name, email, status: "throttled", accountId: "", templateId: templateId || "", runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "min gap days" });
|
|
1560
1560
|
return false;
|
|
1561
1561
|
}
|
|
1562
1562
|
}
|