Initial Commit
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user