import { batchProcess } from "../scripts/fetch_and_generate.js"; import { config } from "./config.js"; import { regenerateStartupFiles } from "./podcast.js"; interface BatchSchedulerState { enabled: boolean; lastRun?: string; nextRun?: string; isRunning: boolean; intervalId?: NodeJS.Timeout; canForceStop: boolean; } class BatchScheduler { private state: BatchSchedulerState = { enabled: true, isRunning: false, canForceStop: false, }; private currentAbortController?: AbortController; private migrationCompleted = false; private readonly SIX_HOURS_MS = 6 * 60 * 60 * 1000; // 6 hours in milliseconds constructor() { // Check if initial run is disabled via environment variable if (config.batch.disableInitialRun) { console.log("⏸ Initial batch run disabled by configuration"); // Still schedule regular runs, just skip the initial one this.scheduleRegularRuns(); } else { // Start with initial delay and then schedule regular runs this.scheduleInitialRun(); } } private scheduleInitialRun() { setTimeout(async () => { if (this.state.enabled) { await this.runBatch(); } if (this.state.enabled) { this.scheduleRegularRuns(); } }, 10000); // Wait 10 seconds after startup } private scheduleRegularRuns() { if (this.state.intervalId) { clearTimeout(this.state.intervalId); } if (!this.state.enabled) { this.state.nextRun = undefined; return; } const nextRunTime = Date.now() + this.SIX_HOURS_MS; this.state.nextRun = new Date(nextRunTime).toISOString(); console.log( `🕕 Next batch process scheduled for: ${new Date(nextRunTime).toLocaleString()}`, ); this.state.intervalId = setTimeout(async () => { if (this.state.enabled) { await this.runBatch(); this.scheduleRegularRuns(); // Schedule next run } }, this.SIX_HOURS_MS); } private async runBatch(): Promise { if (this.state.isRunning) { console.log("! Batch process already running, skipping"); return; } this.state.isRunning = true; this.state.canForceStop = true; this.state.lastRun = new Date().toISOString(); // Create new AbortController for this batch run this.currentAbortController = new AbortController(); try { console.log("🔄 Running scheduled batch process..."); // Run migrations (only once per startup) if (!this.migrationCompleted) { try { // Feed category migration const { migrateFeedsWithCategories, getFeedCategoryMigrationStatus } = await import("./database.js"); const feedMigrationStatus = await getFeedCategoryMigrationStatus(); if (!feedMigrationStatus.migrationComplete) { console.log("🔄 Running feed category migration..."); await migrateFeedsWithCategories(); console.log("✅ Feed category migration completed"); } else { console.log("✅ Feed category migration already complete"); } // Episode category migration const { migrateEpisodesWithCategories, getEpisodeCategoryMigrationStatus, } = await import("./database.js"); const episodeMigrationStatus = await getEpisodeCategoryMigrationStatus(); if (!episodeMigrationStatus.migrationComplete) { console.log("🔄 Running episode category migration..."); await migrateEpisodesWithCategories(); console.log("✅ Episode category migration completed"); } else { console.log("✅ Episode category migration already complete"); } this.migrationCompleted = true; } catch (migrationError) { console.error("❌ Error during category migrations:", migrationError); // Don't fail the entire batch process due to migration error this.migrationCompleted = true; // Mark as completed to avoid retrying every batch } } await batchProcess(this.currentAbortController.signal); console.log("✅ Scheduled batch process completed"); } catch (error) { if (error instanceof Error && error.name === "AbortError") { console.log("🛑 Batch process was forcefully stopped"); } else { console.error("❌ Error during scheduled batch process:", error); } } finally { this.state.isRunning = false; this.state.canForceStop = false; this.currentAbortController = undefined; } } public async triggerManualRun(): Promise { console.log("🚀 Manual batch process triggered"); await this.runBatch(); } public enable(): void { if (this.state.enabled) { console.log("i Batch scheduler already enabled"); return; } this.state.enabled = true; console.log("✅ Batch scheduler enabled"); this.scheduleRegularRuns(); } public disable(): void { if (!this.state.enabled) { console.log("i Batch scheduler already disabled"); return; } this.state.enabled = false; console.log("⏸ Batch scheduler disabled"); if (this.state.intervalId) { clearTimeout(this.state.intervalId); this.state.intervalId = undefined; } this.state.nextRun = undefined; } public getStatus(): BatchSchedulerState { return { ...this.state, intervalId: undefined, // Don't expose the timeout ID }; } public forceStop(): boolean { if (!this.state.isRunning || !this.currentAbortController) { console.log("i No batch process currently running to stop"); return false; } console.log("🛑 Force stopping batch process..."); this.currentAbortController.abort(); return true; } public isEnabled(): boolean { return this.state.enabled; } public isRunning(): boolean { return this.state.isRunning; } } // Export singleton instance export const batchScheduler = new BatchScheduler(); export type { BatchSchedulerState };