import { SmsReceiptNotFoundError } from "../error-codes/error-codes.js"; import type { DeliveryTrackerOptions, SmsDeliveryReport, SmsProvider, } from "../types/sms.types.js"; /** * Bounded LRU cache of `SmsDeliveryReport` keyed by `messageId`. Receipts * may arrive via webhook ingest (`ingest`) or be lazily fetched from a * provider (`lookupOrFetch`). The cache is bounded so a long-running * service does not retain every receipt indefinitely. */ export class DeliveryTracker { private readonly maxEntries: number; private readonly cache = new Map(); public constructor(options: DeliveryTrackerOptions = {}) { this.maxEntries = options.maxEntries ?? 1024; if (this.maxEntries < 1) { throw new Error("DeliveryTracker.maxEntries must be ≥ 1"); } } /** Record a delivery report; evicts the oldest entry past `maxEntries`. */ public ingest(report: SmsDeliveryReport): void { if (this.cache.has(report.messageId)) { this.cache.delete(report.messageId); } this.cache.set(report.messageId, report); if (this.cache.size > this.maxEntries) { const oldest = this.cache.keys().next().value as string; this.cache.delete(oldest); } } /** Read the cached status; returns `undefined` on miss. */ public getStatus(messageId: string): SmsDeliveryReport | undefined { const entry = this.cache.get(messageId); if (entry === undefined) return undefined; // Re-insert to refresh LRU order. this.cache.delete(messageId); this.cache.set(messageId, entry); return entry; } /** Cache miss → consult provider; throws `SmsReceiptNotFoundError` if no provider. */ public async lookupOrFetch( messageId: string, provider?: SmsProvider ): Promise { const cached = this.getStatus(messageId); if (cached !== undefined) return cached; if (provider === undefined) { throw new SmsReceiptNotFoundError({ context: { messageId } }); } const report = await provider.getStatus(messageId); this.ingest(report); return report; } /** Number of cached entries. */ public size(): number { return this.cache.size; } /** Drop every cached entry. */ public clear(): void { this.cache.clear(); } }