209 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			209 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
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<void> {
 | 
						|
    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<void> {
 | 
						|
    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 };
 |