UNPKG

@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
"use strict"; 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); }