Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
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<string, SmsDeliveryReport>();
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<SmsDeliveryReport> {
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();
}
}