import * as plugins from '../plugins.js'; import { logger } from '../logger.js'; import { CacheDb } from './classes.cachedb.js'; // Import document classes for cleanup import { CachedEmail } from './documents/classes.cached.email.js'; import { CachedIPReputation } from './documents/classes.cached.ip.reputation.js'; import { CachedBounce } from './documents/classes.cached.bounce.js'; import { CachedSuppression } from './documents/classes.cached.suppression.js'; import { CachedDKIMKey } from './documents/classes.cached.dkim.js'; /** * Configuration for the cache cleaner */ export interface ICacheCleanerOptions { /** Cleanup interval in milliseconds (default: 1 hour) */ intervalMs?: number; /** Enable verbose logging */ verbose?: boolean; } /** * CacheCleaner - Periodically removes expired documents from the cache * * Runs on a configurable interval (default: hourly) and queries each * collection for documents where expiresAt < now(), then deletes them. */ export class CacheCleaner { private cleanupInterval: ReturnType | null = null; private isRunning: boolean = false; private options: Required; private cacheDb: CacheDb; constructor(cacheDb: CacheDb, options: ICacheCleanerOptions = {}) { this.cacheDb = cacheDb; this.options = { intervalMs: options.intervalMs || 60 * 60 * 1000, // 1 hour default verbose: options.verbose || false, }; } /** * Start the periodic cleanup process */ public start(): void { if (this.isRunning) { logger.log('warn', 'CacheCleaner already running'); return; } this.isRunning = true; // Run cleanup immediately on start this.runCleanup().catch((error) => { logger.log('error', `Initial cache cleanup failed: ${error.message}`); }); // Schedule periodic cleanup this.cleanupInterval = setInterval(() => { this.runCleanup().catch((error) => { logger.log('error', `Cache cleanup failed: ${error.message}`); }); }, this.options.intervalMs); logger.log( 'info', `CacheCleaner started with interval: ${this.options.intervalMs / 1000 / 60} minutes` ); } /** * Stop the periodic cleanup process */ public stop(): void { if (!this.isRunning) { return; } if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } this.isRunning = false; logger.log('info', 'CacheCleaner stopped'); } /** * Run a single cleanup cycle */ public async runCleanup(): Promise { if (!this.cacheDb.isReady()) { logger.log('warn', 'CacheDb not ready, skipping cleanup'); return; } const now = new Date(); const results: { collection: string; deleted: number }[] = []; try { // Clean CachedEmail documents const emailsDeleted = await this.cleanCollection(CachedEmail, now); results.push({ collection: 'CachedEmail', deleted: emailsDeleted }); // Clean CachedIPReputation documents const ipReputationDeleted = await this.cleanCollection(CachedIPReputation, now); results.push({ collection: 'CachedIPReputation', deleted: ipReputationDeleted }); // Clean CachedBounce documents const bouncesDeleted = await this.cleanCollection(CachedBounce, now); results.push({ collection: 'CachedBounce', deleted: bouncesDeleted }); // Clean CachedSuppression documents (but not permanent ones) const suppressionDeleted = await this.cleanCollection(CachedSuppression, now); results.push({ collection: 'CachedSuppression', deleted: suppressionDeleted }); // Clean CachedDKIMKey documents const dkimDeleted = await this.cleanCollection(CachedDKIMKey, now); results.push({ collection: 'CachedDKIMKey', deleted: dkimDeleted }); // Log results const totalDeleted = results.reduce((sum, r) => sum + r.deleted, 0); if (totalDeleted > 0 || this.options.verbose) { const summary = results .filter((r) => r.deleted > 0) .map((r) => `${r.collection}: ${r.deleted}`) .join(', '); logger.log( 'info', `Cache cleanup completed. Deleted ${totalDeleted} expired documents. ${summary || 'No deletions.'}` ); } } catch (error) { logger.log('error', `Cache cleanup error: ${error.message}`); throw error; } } /** * Clean expired documents from a specific collection */ private async cleanCollection( documentClass: { deleteMany: (filter: any) => Promise }, now: Date ): Promise { try { const result = await documentClass.deleteMany({ expiresAt: { $lt: now }, }); return result?.deletedCount || 0; } catch (error) { logger.log('error', `Error cleaning collection: ${error.message}`); return 0; } } /** * Check if the cleaner is running */ public isActive(): boolean { return this.isRunning; } /** * Get the cleanup interval in milliseconds */ public getIntervalMs(): number { return this.options.intervalMs; } }