Also fix rrule default-import workaround so the shared package loads correctly under NodeNext ESM resolution (rrule@2.8.1 has no exports field).
43 lines
1.5 KiB
TypeScript
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 };
|
|
}
|