import { Cron } from "croner"; import { normalizeOptionalString } from "../shared/string-coerce.js"; import { parseAbsoluteTimeMs } from "./parse.js"; import type { CronSchedule } from "./types.js";
const CRON_EVAL_CACHE_MAX = 512; const cronEvalCache = new Map<string, Cron>();
function resolveCronTimezone(tz?: string) { const trimmed = normalizeOptionalString(tz) ?? ""; if (trimmed) { return trimmed;
} return Intl.DateTimeFormat().resolvedOptions().timeZone;
}
function resolveCachedCron(expr: string, timezone: string): Cron { const key = `${timezone}\u0000${expr}`; const cached = cronEvalCache.get(key); if (cached) { return cached;
} if (cronEvalCache.size >= CRON_EVAL_CACHE_MAX) { const oldest = cronEvalCache.keys().next().value; if (oldest) {
cronEvalCache.delete(oldest);
}
} const next = new Cron(expr, { timezone, catch: false });
cronEvalCache.set(key, next); return next;
}
const cron = resolveCronFromSchedule(schedule as { tz?: string; expr?: unknown; cron?: unknown }); if (!cron) { return undefined;
}
let next = cron.nextRun(new Date(nowMs)); if (!next) { return undefined;
}
let nextMs = next.getTime(); if (!Number.isFinite(nextMs)) { return undefined;
}
// Workaround for croner year-rollback bug: some timezone/date combinations // (e.g. Asia/Shanghai) cause nextRun to return a timestamp in a past year. // Retry from a later reference point when the returned time is not in the // future. if (nextMs <= nowMs) { const nextSecondMs = Math.floor(nowMs / 1000) * 1000 + 1000; const retry = cron.nextRun(new Date(nextSecondMs)); if (retry) { const retryMs = retry.getTime(); if (Number.isFinite(retryMs) && retryMs > nowMs) { return retryMs;
}
} // Still in the past — try from start of tomorrow (UTC) as a broader reset. const tomorrowMs = new Date(nowMs).setUTCHours(24, 0, 0, 0); const retry2 = cron.nextRun(new Date(tomorrowMs)); if (retry2) { const retry2Ms = retry2.getTime(); if (Number.isFinite(retry2Ms) && retry2Ms > nowMs) { return retry2Ms;
}
} return undefined;
}
return nextMs;
}
export function computePreviousRunAtMs(schedule: CronSchedule, nowMs: number): number | undefined { if (schedule.kind !== "cron") { return undefined;
} const cron = resolveCronFromSchedule(schedule as { tz?: string; expr?: unknown; cron?: unknown }); if (!cron) { return undefined;
} const previousRuns = cron.previousRuns(1, new Date(nowMs)); const previous = previousRuns[0]; if (!previous) { return undefined;
} const previousMs = previous.getTime(); if (!Number.isFinite(previousMs)) { return undefined;
} if (previousMs >= nowMs) { return undefined;
} return previousMs;
}
export function clearCronScheduleCacheForTest(): void {
cronEvalCache.clear();
}
export function getCronScheduleCacheSizeForTest(): number { return cronEvalCache.size;
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.