From 3fa793bef6de92cdabef87baf2b60c8fabfd8116 Mon Sep 17 00:00:00 2001 From: Vutsal Date: Sat, 7 Mar 2026 21:51:28 -0800 Subject: [PATCH] Refactor scheduler to use CronExpressionParser --- app/server/core/scheduler.ts | 71 +++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/app/server/core/scheduler.ts b/app/server/core/scheduler.ts index d115fbeb..5a45b526 100644 --- a/app/server/core/scheduler.ts +++ b/app/server/core/scheduler.ts @@ -1,4 +1,4 @@ -import cron, { type ScheduledTask } from "node-cron"; +import CronExpressionParser from "cron-parser"; import { logger } from "../utils/logger"; export abstract class Job { @@ -7,6 +7,73 @@ export abstract class Job { type JobConstructor = new () => Job; +class ScheduledTask { + private timer: ReturnType | null = null; + private active = true; + private running = false; + + constructor( + private readonly jobName: string, + private readonly cronExpression: string, + private readonly run: () => Promise, + ) { + this.scheduleNext(); + } + + private getDelay(fromDate: Date) { + try { + const interval = CronExpressionParser.parse(this.cronExpression, { + currentDate: fromDate, + tz: Intl.DateTimeFormat().resolvedOptions().timeZone, + }); + const nextRun = interval.next().toDate(); + return Math.max(0, nextRun.getTime() - Date.now()); + } catch (error) { + logger.error(`Failed to parse cron expression for ${this.jobName}:`, error); + return 60_000; + } + } + + private scheduleNext(fromDate = new Date()) { + if (!this.active) return; + + const delay = this.getDelay(fromDate); + this.timer = setTimeout(() => { + void this.tick(); + }, delay); + } + + private async tick() { + if (!this.active) return; + + if (this.running) { + logger.warn(`Skipping overlapping run for job ${this.jobName}`); + this.scheduleNext(new Date()); + return; + } + + this.running = true; + try { + await this.run(); + } finally { + this.running = false; + this.scheduleNext(new Date()); + } + } + + async stop() { + this.active = false; + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + } + + async destroy() { + await this.stop(); + } +} + class SchedulerClass { private tasks: ScheduledTask[] = []; @@ -18,7 +85,7 @@ class SchedulerClass { const job = new JobClass(); return { schedule: (cronExpression: string) => { - const task = cron.schedule(cronExpression, async () => { + const task = new ScheduledTask(JobClass.name, cronExpression, async () => { try { await job.run(); } catch (error) {