export class CronScheduler { private timers: Map> = new Map() parse(cron: string): { next: () => number } { const parts = cron.trim().split(/\s+/) if (parts.length !== 5) throw new Error(`Invalid cron: ${cron}`) const [min, hour, day, month, weekday] = parts const mins = this.parseField(min, 0, 59) const hours = this.parseField(hour, 0, 23) const days = this.parseField(day, 1, 31) const months = this.parseField(month, 1, 12) const weekdays = this.parseField(weekday, 0, 6) return { next: () => { const now = new Date() const candidates: number[] = [] for (let m = 0; m < 60; m++) { for (let h = 0; h < 24; h++) { for (let d = 1; d <= 31; d++) { for (let mo = 1; mo <= 12; mo++) { if (mins.has(m) && hours.has(h) && days.has(d) && months.has(mo)) { const candidate = new Date(now.getFullYear(), mo - 1, d, h, m, 0, 0) if (candidate > now && weekdays.has(candidate.getDay())) { candidates.push(candidate.getTime()) } } } } } } candidates.sort((a, b) => a - b) return candidates[0] || (now.getTime() + 60000) } } } private parseField(field: string, min: number, max: number): Set { const result = new Set() if (field === "*") { for (let i = min; i <= max; i++) result.add(i) return result } for (const part of field.split(",")) { if (part.includes("/")) { const [range, step] = part.split("/") const s = parseInt(step) const [rMin, rMax] = range === "*" ? [min, max] : range.split("-").map(Number) for (let i = rMin!; i <= (rMax ?? rMin!); i += s) { if (i >= min && i <= max) result.add(i) } } else if (part.includes("-")) { const [lo, hi] = part.split("-").map(Number) for (let i = lo!; i <= hi!; i++) { if (i >= min && i <= max) result.add(i) } } else { const v = parseInt(part) if (v >= min && v <= max) result.add(v) } } return result } start(name: string, intervalMs: number, fn: () => void): void { this.stop(name) const t = setInterval(fn, intervalMs) this.timers.set(name, t) } stop(name: string): void { const t = this.timers.get(name) if (t) { clearInterval(t) this.timers.delete(name) } } stopAll(): void { for (const t of this.timers.values()) clearInterval(t) this.timers.clear() } }