@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 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
  }