import * as plugins from '../../plugins.js'; import { DcRouterDb } from '../classes.dcrouter-db.js'; const DB_OPERATION_TIMEOUT_MS = 5_000; const getDb = () => DcRouterDb.getInstance().getDb(); export interface IEmailTrafficBucketCounts { sent: number; received: number; failed: number; } export interface IEmailTrafficBucketSnapshot extends IEmailTrafficBucketCounts { bucketStart: number; } @plugins.smartdata.Collection(() => getDb()) export class EmailTrafficBucketDoc extends plugins.smartdata.SmartDataDbDoc< EmailTrafficBucketDoc, EmailTrafficBucketDoc > implements IEmailTrafficBucketSnapshot { @plugins.smartdata.unI() @plugins.smartdata.svDb() public id!: string; @plugins.smartdata.svDb() public bucketStart!: number; @plugins.smartdata.svDb() public sent: number = 0; @plugins.smartdata.svDb() public received: number = 0; @plugins.smartdata.svDb() public failed: number = 0; @plugins.smartdata.svDb() public createdAt!: number; @plugins.smartdata.svDb() public updatedAt!: number; constructor() { super(); } private static async getNativeCollection() { const smartdataCollection = (EmailTrafficBucketDoc as typeof EmailTrafficBucketDoc & { collection: plugins.smartdata.SmartdataCollection; }).collection; await smartdataCollection.init(); const probe = new EmailTrafficBucketDoc(); await smartdataCollection.markUniqueIndexes(probe.uniqueIndexes || []); await smartdataCollection.createRegularIndexes(probe.regularIndexes || []); return smartdataCollection.mongoDbCollection; } private static validateSnapshot(snapshotArg: IEmailTrafficBucketSnapshot): void { const counts = [snapshotArg.sent, snapshotArg.received, snapshotArg.failed]; if ( !Number.isSafeInteger(snapshotArg.bucketStart) || snapshotArg.bucketStart < 0 || snapshotArg.bucketStart % 60_000 !== 0 || counts.some((count) => !Number.isSafeInteger(count) || count < 0) ) { throw new Error('Invalid email traffic bucket snapshot'); } } /** Persist retry-safe absolute monotonic counters. */ public static async persistAbsolute(snapshotsArg: IEmailTrafficBucketSnapshot[]): Promise { if (snapshotsArg.length === 0) return; if (snapshotsArg.length > 500) { throw new Error('EmailTrafficBucketDoc.persistAbsolute accepts at most 500 buckets'); } for (const snapshot of snapshotsArg) { EmailTrafficBucketDoc.validateSnapshot(snapshot); } const collection = await EmailTrafficBucketDoc.getNativeCollection(); const now = Date.now(); const updatedAt = new Date(now).toISOString(); await collection.bulkWrite( snapshotsArg.map((snapshot) => ({ updateOne: { filter: { bucketStart: snapshot.bucketStart }, update: { $max: { sent: snapshot.sent, received: snapshot.received, failed: snapshot.failed, }, $set: { updatedAt: now, _updatedAt: updatedAt, }, $setOnInsert: { id: `email-traffic-${snapshot.bucketStart}`, bucketStart: snapshot.bucketStart, createdAt: now, _createdAt: updatedAt, }, }, upsert: true, }, })), { ordered: false, timeoutMS: DB_OPERATION_TIMEOUT_MS }, ); } public static async loadSince(cutoffArg: number): Promise { if (!Number.isSafeInteger(cutoffArg) || cutoffArg < 0) { throw new Error('EmailTrafficBucketDoc.loadSince requires a valid cutoff'); } const collection = await EmailTrafficBucketDoc.getNativeCollection(); const cursor = collection .find( { bucketStart: { $gte: cutoffArg } }, { timeoutMS: DB_OPERATION_TIMEOUT_MS }, ) .sort({ bucketStart: 1 }); try { const rows = await cursor.toArray(); return rows.map((row) => ({ bucketStart: Number(row.bucketStart), sent: Number(row.sent || 0), received: Number(row.received || 0), failed: Number(row.failed || 0), })); } finally { await cursor.close({ timeoutMS: DB_OPERATION_TIMEOUT_MS }); } } public static async pruneBefore(cutoffArg: number): Promise { if (!Number.isSafeInteger(cutoffArg) || cutoffArg < 0) { throw new Error('EmailTrafficBucketDoc.pruneBefore requires a valid cutoff'); } const collection = await EmailTrafficBucketDoc.getNativeCollection(); let deletedCount = 0; while (true) { const rows = await collection .find( { bucketStart: { $lt: cutoffArg } }, { projection: { _id: 1 }, timeoutMS: DB_OPERATION_TIMEOUT_MS }, ) .sort({ bucketStart: 1, _id: 1 }) .limit(500) .toArray(); if (rows.length === 0) break; const result = await collection.deleteMany( { _id: { $in: rows.map((row) => row._id) } }, { timeoutMS: DB_OPERATION_TIMEOUT_MS }, ); deletedCount += result.deletedCount; if (rows.length < 500) break; } return deletedCount; } }