Skip to main content
Glama
cron-engine.ts5.64 kB
/** * Cron Engine - Scheduling engine using node-schedule * Manages in-memory timers for scheduled Jules tasks */ import schedule, { Job } from 'node-schedule'; import type { ScheduledTask } from '../types/schedule.js'; import type { JulesClient } from '../api/jules-client.js'; import type { ScheduleStorage } from '../storage/schedule-store.js'; import { retryWithBackoff } from '../utils/security.js'; /** * Manages the scheduling and execution of cron jobs for Jules tasks. */ export class CronEngine { private jobs: Map<string, Job> = new Map(); private readonly storage: ScheduleStorage; private readonly julesClient: JulesClient; private readonly logger: (message: string) => void; /** * Creates an instance of CronEngine. * @param storage - The storage instance for scheduled tasks. * @param julesClient - The client for interacting with the Jules API. * @param logger - The logger function to use (defaults to console.log). */ constructor( storage: ScheduleStorage, julesClient: JulesClient, logger: (message: string) => void = console.log ) { this.storage = storage; this.julesClient = julesClient; this.logger = logger; } /** * Hydrates all schedules from storage on startup. * Loads tasks from storage and schedules them if enabled. */ async initialize(): Promise<void> { const tasks = await this.storage.listTasks(); this.logger(`Loading ${tasks.length} scheduled tasks from storage...`); for (const task of tasks) { if (task.enabled) { try { this.scheduleTask(task); this.logger(`✓ Scheduled: ${task.name} (${task.cron})`); } catch (error) { this.logger( `✗ Failed to schedule ${task.name}: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } } this.logger('Scheduler initialized.'); } /** * Validates a cron expression. * @param expression - The cron expression to validate. * @returns True if the expression is valid, false otherwise. */ static validateCronExpression(expression: string): boolean { try { // Create job to test validity const testJob = schedule.scheduleJob(expression, () => {}); if (!testJob) { return false; } // CRITICAL: Cancel immediately to prevent memory leak testJob.cancel(); return true; } catch { return false; } } /** * Schedules a task in memory. * Cancels any existing job for the task ID before scheduling. * @param task - The task to schedule. * @throws Error if the schedule creation fails. */ scheduleTask(task: ScheduledTask): void { // Cancel existing job if present this.cancelTask(task.id); // Create the job callback const jobCallback = async () => { const timestamp = new Date().toISOString(); this.logger(`[${timestamp}] Executing scheduled task: ${task.name}`); try { // Create Jules session with retry logic (3 attempts with exponential backoff) const session = await retryWithBackoff( () => this.julesClient.createSession({ prompt: task.taskPayload.prompt, sourceContext: { source: task.taskPayload.source, githubRepoContext: { startingBranch: task.taskPayload.branch || 'main', }, }, automationMode: task.taskPayload.automationMode, requirePlanApproval: task.taskPayload.requirePlanApproval, title: task.taskPayload.title, }), 3, // maxRetries 2000 // 2 second base delay ); this.logger( `✓ Task "${task.name}" created session: ${session.id}` ); // Update last run metadata await this.storage.updateLastRun(task.id, timestamp, session.id); } catch (error) { this.logger( `✗ Task "${task.name}" failed after 3 retries: ${error instanceof Error ? error.message : 'Unknown error'}` ); // Update last run even on failure for audit trail await this.storage.updateLastRun(task.id, timestamp, undefined); } }; // Schedule the job const job = schedule.scheduleJob(task.cron, jobCallback); if (!job) { throw new Error(`Failed to create schedule for cron: ${task.cron}`); } this.jobs.set(task.id, job); } /** * Cancels a scheduled task. * @param taskId - The ID of the task to cancel. */ cancelTask(taskId: string): void { const job = this.jobs.get(taskId); if (job) { job.cancel(); this.jobs.delete(taskId); } } /** * Gets the next scheduled execution time for a task. * @param taskId - The ID of the task. * @returns The next invocation date, or null if the task is not scheduled. */ getNextInvocation(taskId: string): Date | null { const job = this.jobs.get(taskId); if (!job) { return null; } return job.nextInvocation(); } /** * Reschedules a task (useful when cron expression changes). * @param task - The task to reschedule. */ async rescheduleTask(task: ScheduledTask): Promise<void> { this.cancelTask(task.id); this.scheduleTask(task); } /** * Cancels all jobs and shuts down scheduler. */ shutdown(): void { this.logger('Shutting down scheduler...'); for (const [taskId, job] of this.jobs.entries()) { job.cancel(); this.logger(`Canceled job: ${taskId}`); } this.jobs.clear(); schedule.gracefulShutdown(); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/savethepolarbears/jules-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server