import * as cron from "node-cron"; import { logger } from "../server.js"; import { S3Sync, createS3SyncFromEnv } from "./s3Sync.js"; export class DailyS3Scheduler { private s3Sync: S3Sync | null = null; private cronJob: cron.ScheduledTask | null = null; constructor() { this.s3Sync = createS3SyncFromEnv(); } /** * Start the daily S3 sync scheduler * Runs at midnight PST (00:00 PST = 08:00 UTC during standard time, 07:00 UTC during daylight time) */ async start(): Promise { if (!this.s3Sync) { logger.warn("S3 sync not configured. Skipping scheduler setup."); return; } // Test S3 connection before starting scheduler const connectionTest = await this.s3Sync.testConnection(); if (!connectionTest) { logger.error("S3 connection test failed. S3 sync scheduler will not be started."); return; } // Cron expression for midnight PST // Note: This uses PST timezone. During PDT (daylight time), it will still run at midnight local time const cronExpression = "0 0 * * *"; // Every day at midnight const timezone = "America/Los_Angeles"; // PST/PDT timezone this.cronJob = cron.schedule( cronExpression, async () => { await this.performDailySync(); }, { timezone: timezone, } ); logger.info(`Daily S3 sync scheduler started. Will run at midnight PST/PDT.`); logger.info(`Next sync scheduled for: ${this.getNextRunTime()}`); } /** * Stop the scheduler */ stop(): void { if (this.cronJob) { this.cronJob.stop(); this.cronJob = null; logger.info("Daily S3 sync scheduler stopped."); } } /** * Perform the daily sync operation */ private async performDailySync(): Promise { if (!this.s3Sync) { logger.error("S3 sync not available for daily sync"); return; } const startTime = new Date(); logger.info(`Starting daily S3 sync at ${startTime.toISOString()}`); try { await this.s3Sync.syncJobsToS3(); const endTime = new Date(); const duration = endTime.getTime() - startTime.getTime(); logger.info(`Daily S3 sync completed successfully in ${duration}ms`); } catch (error) { logger.error("Daily S3 sync failed:", error); } } /** * Manually trigger a sync (useful for testing) */ async triggerManualSync(): Promise { if (!this.s3Sync) { logger.error("S3 sync not configured"); return; } logger.info("Triggering manual S3 sync..."); await this.performDailySync(); } /** * Get the next scheduled run time */ private getNextRunTime(): string { if (!this.cronJob) { return "Not scheduled"; } // Create a date object for midnight PST today const now = new Date(); const pstNow = new Date(now.toLocaleString("en-US", { timeZone: "America/Los_Angeles" })); // If it's past midnight today, next run is tomorrow at midnight const nextRun = new Date(pstNow); if (pstNow.getHours() > 0 || pstNow.getMinutes() > 0 || pstNow.getSeconds() > 0) { nextRun.setDate(nextRun.getDate() + 1); } nextRun.setHours(0, 0, 0, 0); return nextRun.toLocaleString("en-US", { timeZone: "America/Los_Angeles", weekday: "long", year: "numeric", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit", timeZoneName: "short" }); } /** * Get scheduler status */ async getStatus(): Promise<{ isConfigured: boolean; isRunning: boolean; nextRun: string; syncStats?: { bucketName: string; region: string; keyPrefix: string; available: boolean }; }> { let syncStats; if (this.s3Sync) { try { syncStats = await this.s3Sync.getSyncStats(); } catch (error) { logger.error("Failed to get sync stats:", error); } } return { isConfigured: this.s3Sync !== null, isRunning: this.cronJob !== null, nextRun: this.getNextRunTime(), syncStats, }; } } // Export a singleton instance export const dailyS3Scheduler = new DailyS3Scheduler();