yiekheng 01eb5752ee feat(scheduler): add fire-reminder handler + job registration
Also fix rrule default-import workaround so the shared package loads
correctly under NodeNext ESM resolution (rrule@2.8.1 has no exports field).
2026-05-09 17:29:21 +08:00

43 lines
1.5 KiB
TypeScript

// rrule@2.8.1 lacks a proper "exports" field, so named ESM imports fail at
// runtime with NodeNext resolution. Use the default import and destructure.
import rrulePkg from "rrule";
import type { RRule as RRuleType } from "rrule";
const { RRule, rrulestr } = rrulePkg as unknown as typeof import("rrule");
import { DateTime } from "luxon";
export const MIN_INTERVAL_MS = 5 * 60 * 1000;
export function parseRRule(rule: string): RRuleType {
const parsed = rrulestr(rule);
if (!(parsed instanceof RRule)) {
throw new Error("Compound RRULE/RRSET not supported");
}
return parsed;
}
export function nextOccurrence(rule: string, timezone: string, after: Date): Date | null {
const parsed = parseRRule(rule);
const afterInZone = DateTime.fromJSDate(after).setZone(timezone).toJSDate();
const next = parsed.after(afterInZone, false);
return next ?? null;
}
export type IntervalCheck = { ok: true } | { ok: false; reason: string };
export function validateMinInterval(rule: string, timezone: string): IntervalCheck {
const parsed = parseRRule(rule);
const now = new Date();
const first = parsed.after(now, false);
if (!first) return { ok: true };
const second = parsed.after(first, false);
if (!second) return { ok: true };
const gap = second.getTime() - first.getTime();
if (gap < MIN_INTERVAL_MS) {
return {
ok: false,
reason: `Recurrence fires every ${Math.round(gap / 1000)}s; minimum interval is ${MIN_INTERVAL_MS / 1000}s.`,
};
}
return { ok: true };
}