import { parseExpression } from "cron-parser"; export function calculateNextScheduledTimestampFromNow(schedule: string, timezone: string | null) { return calculateNextScheduledTimestamp(schedule, timezone, new Date()); } export function calculateNextScheduledTimestamp( schedule: string, timezone: string | null, lastScheduledTimestamp: Date = new Date() ) { const nextStep = calculateNextStep(schedule, timezone, lastScheduledTimestamp); if (nextStep.getTime() < Date.now()) { // If the next step is in the past, we just need to calculate the next step from now return calculateNextStep(schedule, timezone, new Date()); } return nextStep; } function calculateNextStep(schedule: string, timezone: string | null, currentDate: Date) { return parseExpression(schedule, { currentDate, utc: timezone === null, tz: timezone ?? undefined, }) .next() .toDate(); } /** * Cron's previous slot relative to `fromTimestamp`. For a continuously- * running schedule this equals the actual last fire time; for paused or * DST-edge cases it's an approximation. Used only on the recovery path * where the actual last fire isn't recoverable from in-flight worker state. */ export function previousScheduledTimestamp( cron: string, timezone: string | null, fromTimestamp: Date = new Date() ) { return parseExpression(cron, { currentDate: fromTimestamp, utc: timezone === null, tz: timezone ?? undefined, }) .prev() .toDate(); } export function nextScheduledTimestamps( cron: string, timezone: string | null, lastScheduledTimestamp: Date, count: number = 1 ) { const result: Array = []; let nextScheduledTimestamp = lastScheduledTimestamp; for (let i = 0; i < count; i++) { nextScheduledTimestamp = calculateNextScheduledTimestamp( cron, timezone, nextScheduledTimestamp ); result.push(nextScheduledTimestamp); } return result; }