@email-service/email-service
Version:
email-service is a versatile npm package designed to simplify the integration and standardization of email communications across multiple Email Service Providers (ESPs).
111 lines (110 loc) • 3.98 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CompositeBucket = exports.TokenBucket = exports.RATE_LIMIT_DEFAULTS = void 0;
exports.createRateLimiter = createRateLimiter;
/**
* Défauts embarqués par ESP, exprimés en envois par seconde.
*
* - resend : 10/s (limite officielle Resend)
* - brevo : 100/s (conservative, plan business)
* - postmark : 10/s (safe default Postmark)
* - nodemailer : 5/s (prudent SMTP, dépend fortement du fournisseur)
* - emailserviceviewer / local : 1000/s (viewer de test — pas de vraie limite)
*
* Ces défauts s'appliquent UNIQUEMENT si `config.rateLimit` est absent. Dès
* qu'une valeur `rateLimit` est fournie par le consommateur, elle remplace
* complètement le défaut (pas de merge partiel).
*/
exports.RATE_LIMIT_DEFAULTS = {
resend: { perSecond: 10 },
brevo: { perSecond: 100 },
postmark: { perSecond: 10 },
nodemailer: { perSecond: 5 },
emailserviceviewer: { perSecond: 1000 },
emailserviceviewerlocal: { perSecond: 1000 },
};
/**
* Token bucket minimal, in-memory, **par instance**. Chaque instance
* `EmailServiceSelector` créée par le consommateur a son propre bucket :
* pas de synchronisation cross-process, pas de persistance. Au redémarrage,
* le bucket repart plein.
*
* Algorithme :
* - capacity tokens au démarrage (autorise un petit burst initial)
* - refill continu à `rate` tokens/s (calcul paresseux à chaque acquire)
* - acquire() attend le temps nécessaire si aucun token dispo, via setTimeout
* (zéro CPU spin)
*
* Retourne le nombre de ms attendues — utile pour le logger.
*/
class TokenBucket {
constructor(rate, capacity) {
this.rate = rate;
this.capacity = capacity;
if (rate <= 0)
throw new Error('TokenBucket: rate must be > 0');
if (capacity <= 0)
throw new Error('TokenBucket: capacity must be > 0');
this.tokens = capacity;
this.lastRefill = Date.now();
}
refill() {
const now = Date.now();
const elapsedSec = (now - this.lastRefill) / 1000;
if (elapsedSec <= 0)
return;
this.tokens = Math.min(this.capacity, this.tokens + elapsedSec * this.rate);
this.lastRefill = now;
}
async acquire() {
this.refill();
if (this.tokens >= 1) {
this.tokens -= 1;
return 0;
}
const waitSec = (1 - this.tokens) / this.rate;
const waitMs = Math.ceil(waitSec * 1000);
await new Promise((resolve) => setTimeout(resolve, waitMs));
this.refill();
this.tokens -= 1;
return waitMs;
}
}
exports.TokenBucket = TokenBucket;
/**
* Compose plusieurs token buckets — utile quand on veut cumuler
* `perSecond` et `perMinute`. `acquire()` attend sur le bucket le plus
* restrictif.
*/
class CompositeBucket {
constructor(buckets) {
this.buckets = buckets;
}
async acquire() {
const waits = await Promise.all(this.buckets.map((b) => b.acquire()));
return Math.max(...waits);
}
}
exports.CompositeBucket = CompositeBucket;
/**
* Construit un rate limiter à partir d'un `Config`. Retourne `null` si
* aucun rate limit n'est applicable (ne devrait pas arriver avec les
* défauts embarqués, sauf ESP inconnu).
*/
function createRateLimiter(esp, override) {
const config = override ?? exports.RATE_LIMIT_DEFAULTS[esp];
if (!config)
return null;
const buckets = [];
if (typeof config.perSecond === 'number' && config.perSecond > 0) {
buckets.push(new TokenBucket(config.perSecond, config.perSecond));
}
if (typeof config.perMinute === 'number' && config.perMinute > 0) {
buckets.push(new TokenBucket(config.perMinute / 60, config.perMinute));
}
if (buckets.length === 0)
return null;
if (buckets.length === 1)
return buckets[0];
return new CompositeBucket(buckets);
}