// src/services/scheduler.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 import env from "../config/env.ts"; import { CronJob } from "cron"; import type { IProjectTask, IProject } from "@gadget/api"; import { GadgetService } from "../lib/service.ts"; import PlatformService from "./platform.ts"; class SchedulerService extends GadgetService { private jobs: Map = new Map(); private _activeCount: number = 0; get name(): string { return "SchedulerService"; } get slug(): string { return "svc:scheduler"; } get activeCount(): number { return this._activeCount; } get scheduledCount(): number { return this.jobs.size; } async start(): Promise { this.log.info("started"); } async stop(): Promise { for (const [taskId, job] of this.jobs) { job.stop(); this.log.info("stopped cron job", { taskId }); } this.jobs.clear(); this.log.info("stopped"); } /** * Schedules a task by creating a CronJob. * Key: task._id (string) */ scheduleTask(task: IProjectTask, project: IProject): void { // If already scheduled, unschedule first const taskId = String(task._id); if (this.jobs.has(taskId)) { this.unscheduleTask(taskId); } try { const job = new CronJob( task.crontab, // cron expression async () => { await this.executeTask(task, project); }, null, // onComplete true, // start env.timezone, // timeZone ); this.jobs.set(taskId, job); this.log.info("task scheduled", { taskId, taskName: task.name, crontab: task.crontab, projectSlug: project.slug, }); } catch (error) { const err = error as Error; this.log.error("failed to schedule task", { taskId, crontab: task.crontab, error: err.message, }); } } /** * Removes a scheduled task. */ unscheduleTask(taskId: string): void { const job = this.jobs.get(taskId); if (job) { job.stop(); this.jobs.delete(taskId); this.log.info("task unscheduled", { taskId }); } } /** * Execute a task with concurrency control. * Delegates to PlatformService.executeTask() which handles * the full headless IDE pipeline. */ private async executeTask(task: IProjectTask, project: IProject): Promise { // Check concurrency if (this._activeCount >= env.concurrency) { this.log.warn("task skipped: concurrency limit reached", { taskId: task._id, taskName: task.name, activeCount: this._activeCount, maxConcurrency: env.concurrency, }); return; } this._activeCount++; try { this.log.info("executing task", { taskId: task._id, taskName: task.name, projectSlug: project.slug, activeCount: this._activeCount, }); await PlatformService.executeTask(task, project); } catch (error) { const err = error as Error; this.log.error("task execution failed", { taskId: task._id, error: err.message, }); } finally { this._activeCount--; } } } export default new SchedulerService();