UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

187 lines (186 loc) 9.74 kB
import { o as asDateTimestampMs } from "./number-coercion-CLj0HTDM.js"; import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { t as pruneMapToMaxSize } from "./map-size-CNcWiFKu.js"; import { t as parseAbsoluteTimeMs } from "./parse-tlCSjrTr.js"; import { t as coerceFiniteScheduleNumber } from "./schedule-number-dcwHraee.js"; import { t as parseOffsetlessIsoDateTimeInTimeZone } from "./parse-offsetless-zoned-datetime-DGKymK8c.js"; import { Cron, CronDate } from "croner"; //#region src/cron/schedule.ts /** Computes at/every/cron schedule timestamps with bounded Croner caching. */ const DAY_MS = 864e5; const cronEvalCache = /* @__PURE__ */ new Map(); const cronTimezoneFormatters = /* @__PURE__ */ new WeakMap(); function resolveCronTimezone(tz) { const trimmed = normalizeOptionalString(tz) ?? ""; if (trimmed) return trimmed; return Intl.DateTimeFormat().resolvedOptions().timeZone; } function resolveCachedCron(expr, timezone) { const key = `${timezone}\u0000${expr}`; const cached = cronEvalCache.get(key); if (cached) { cronEvalCache.delete(key); cronEvalCache.set(key, cached); return cached; } pruneMapToMaxSize(cronEvalCache, 511); const next = new Cron(expr, { timezone, catch: false }); cronEvalCache.set(key, next); return next; } function resolveCronFromSchedule(schedule) { if (typeof schedule.expr !== "string") throw new Error("invalid cron schedule: expr is required"); const expr = schedule.expr.trim(); if (!expr) return; const timezone = resolveCronTimezone(schedule.tz); return { cron: resolveCachedCron(expr, timezone), timezone }; } function hasNearbyCronTimezoneTransition(cron, timezone, nowMs, candidateMs) { let formatter = cronTimezoneFormatters.get(cron); if (!formatter) { formatter = new Intl.DateTimeFormat("en-US", { timeZone: timezone, timeZoneName: "longOffset" }); cronTimezoneFormatters.set(cron, formatter); } const resolvedFormatter = formatter; const readOffset = (instantMs) => resolvedFormatter.formatToParts(new Date(instantMs)).find((part) => part.type === "timeZoneName")?.value; const currentOffset = readOffset(nowMs); const candidateOffset = readOffset(candidateMs); return currentOffset !== candidateOffset || currentOffset !== readOffset(nowMs - DAY_MS) || candidateOffset !== readOffset(candidateMs - DAY_MS); } function resolveCronWallTimeMs(instantMs, timezone) { const local = new CronDate(new Date(instantMs), timezone); return Date.UTC(local.year, local.month, local.day, local.hour, local.minute, local.second, local.ms); } function resolveFirstCronOccurrenceMs(instantMs, timezone) { const wallTime = new Date(resolveCronWallTimeMs(instantMs, timezone)).toISOString().slice(0, -1); const resolved = parseOffsetlessIsoDateTimeInTimeZone(wallTime, timezone); return resolved === null ? void 0 : Date.parse(resolved); } function matchesCronOccurrence(cron, instant) { return cron.match.bind(cron)(instant); } function findCronTimezoneTransitionMs(firstMs, lastMs, timezone) { let beforeSeconds = Math.floor(Math.min(firstMs, lastMs) / 1e3); let afterSeconds = Math.floor(Math.max(firstMs, lastMs) / 1e3); const previousOffsetMs = resolveCronWallTimeMs(beforeSeconds * 1e3, timezone) - beforeSeconds * 1e3; if (previousOffsetMs === resolveCronWallTimeMs(afterSeconds * 1e3, timezone) - afterSeconds * 1e3) return; while (afterSeconds - beforeSeconds > 1) { const middleSeconds = Math.floor((beforeSeconds + afterSeconds) / 2); if (resolveCronWallTimeMs(middleSeconds * 1e3, timezone) - middleSeconds * 1e3 === previousOffsetMs) beforeSeconds = middleSeconds; else afterSeconds = middleSeconds; } return afterSeconds * 1e3; } function resolveCronRunAtTransitionMs(cron, transitionMs, timezone) { let transition = new Date(transitionMs); for (;;) { const candidate = matchesCronOccurrence(cron, transition) ? transition : cron.nextRun(transition); if (!candidate) return; if (matchesCronOccurrence(cron, candidate)) return resolveFirstCronOccurrenceMs(candidate.getTime(), timezone); const candidateMs = candidate.getTime(); const nextTransitionMs = findCronTimezoneTransitionMs(candidateMs - DAY_MS, candidateMs, timezone); if (nextTransitionMs === void 0 || nextTransitionMs <= transition.getTime()) return; transition = new Date(nextTransitionMs); } } function resolveNextCronOccurrenceMs(cron, nowMs, nextMs, timezone) { if (!matchesCronOccurrence(cron, new Date(nextMs))) { const transitionMs = findCronTimezoneTransitionMs(nextMs - DAY_MS, nextMs, timezone); return transitionMs === void 0 ? void 0 : resolveCronRunAtTransitionMs(cron, transitionMs, timezone); } const firstOccurrenceMs = resolveFirstCronOccurrenceMs(nextMs, timezone); if (firstOccurrenceMs === void 0 || firstOccurrenceMs > nowMs) return firstOccurrenceMs; const firstCurrentOccurrenceMs = resolveFirstCronOccurrenceMs(nowMs, timezone); const inRepeatedInterval = firstCurrentOccurrenceMs !== void 0 && firstCurrentOccurrenceMs < nowMs; const transitionStartMs = inRepeatedInterval ? firstCurrentOccurrenceMs : nowMs; const transitionEndMs = inRepeatedInterval ? nowMs : nextMs; const overlapMs = inRepeatedInterval ? nowMs - firstCurrentOccurrenceMs : nextMs - firstOccurrenceMs; const transitionMs = findCronTimezoneTransitionMs(transitionStartMs, transitionEndMs, timezone); if (transitionMs === void 0) return; return resolveCronRunAtTransitionMs(cron, transitionMs + overlapMs, timezone); } function resolveValidatedNextCronOccurrenceMs(cron, nowMs, candidateMs, timezone) { if (candidateMs > nowMs && !hasNearbyCronTimezoneTransition(cron, timezone, nowMs, candidateMs)) return candidateMs; const normalizedMs = resolveNextCronOccurrenceMs(cron, nowMs, candidateMs, timezone); return normalizedMs !== void 0 && normalizedMs > nowMs ? normalizedMs : void 0; } /** Computes the next scheduled run timestamp after now for at/every/cron schedules. */ function computeNextRunAtMs(schedule, nowMs) { if (asDateTimestampMs(nowMs) === void 0) return; if (schedule.kind === "at") { const atMs = parseAbsoluteTimeMs(schedule.at); if (atMs === null) return; return atMs > nowMs ? atMs : void 0; } if (schedule.kind === "every") { const everyMsRaw = coerceFiniteScheduleNumber(schedule.everyMs); if (everyMsRaw === void 0) return; const everyMs = Math.floor(everyMsRaw); if (everyMs < 1) return; const anchorRaw = coerceFiniteScheduleNumber(schedule.anchorMs); if (schedule.anchorMs !== void 0 && (anchorRaw === void 0 || anchorRaw < 0)) return; const anchor = Math.max(0, Math.floor(anchorRaw ?? nowMs)); if (nowMs < anchor) return anchor; const elapsed = nowMs - anchor; const steps = Math.floor(elapsed / everyMs) + 1; return asDateTimestampMs(anchor + steps * everyMs); } if (schedule.kind === "on-exit" || schedule.kind === "stream") return; const resolvedCron = resolveCronFromSchedule(schedule); if (!resolvedCron) return; const { cron, timezone } = resolvedCron; const nextMs = cron.nextRun(new Date(nowMs))?.getTime(); if (nextMs === void 0) return; const normalizedNextMs = resolveValidatedNextCronOccurrenceMs(cron, nowMs, nextMs, timezone); if (normalizedNextMs !== void 0) return normalizedNextMs; if (nextMs > nowMs) return; const nextSecondMs = Math.floor(nowMs / 1e3) * 1e3 + 1e3; const retryMs = cron.nextRun(new Date(nextSecondMs))?.getTime(); if (retryMs !== void 0) { const normalizedRetryMs = resolveValidatedNextCronOccurrenceMs(cron, nowMs, retryMs, timezone); if (normalizedRetryMs !== void 0) return normalizedRetryMs; } const tomorrowMs = new Date(nowMs).setUTCHours(24, 0, 0, 0); const retry2Ms = cron.nextRun(new Date(tomorrowMs))?.getTime(); return retry2Ms !== void 0 ? resolveValidatedNextCronOccurrenceMs(cron, nowMs, retry2Ms, timezone) : void 0; } /** Computes the previous cron-expression run timestamp before now. */ function computePreviousRunAtMs(schedule, nowMs) { if (schedule.kind !== "cron" || asDateTimestampMs(nowMs) === void 0) return; const resolvedCron = resolveCronFromSchedule(schedule); if (!resolvedCron) return; const { cron, timezone } = resolvedCron; let previousMs = cron.previousRuns(1, new Date(nowMs))[0]?.getTime(); if (previousMs !== void 0 && previousMs < nowMs && !hasNearbyCronTimezoneTransition(cron, timezone, nowMs, previousMs)) return previousMs; const firstCurrentOccurrenceMs = resolveFirstCronOccurrenceMs(nowMs, timezone); if (firstCurrentOccurrenceMs !== void 0 && firstCurrentOccurrenceMs < nowMs) { const transitionMs = findCronTimezoneTransitionMs(firstCurrentOccurrenceMs, nowMs, timezone); if (transitionMs !== void 0) { const overlapEndMs = transitionMs + nowMs - firstCurrentOccurrenceMs; const candidateMs = cron.previousRuns(1, new Date(overlapEndMs))[0]?.getTime(); if (candidateMs !== void 0) previousMs = candidateMs; } } if (previousMs === void 0) return; while (!matchesCronOccurrence(cron, new Date(previousMs)) || (resolveFirstCronOccurrenceMs(previousMs, timezone) ?? previousMs) >= nowMs) { const transitionMs = findCronTimezoneTransitionMs(previousMs - DAY_MS, previousMs, timezone); if (transitionMs === void 0) return; const beforeTransition = /* @__PURE__ */ new Date(transitionMs - 1e3); const candidateMs = (matchesCronOccurrence(cron, beforeTransition) ? beforeTransition : cron.previousRuns(1, beforeTransition)[0])?.getTime(); if (candidateMs === void 0 || candidateMs >= previousMs) return; previousMs = candidateMs; } const normalizedPreviousMs = resolveFirstCronOccurrenceMs(previousMs, timezone); return normalizedPreviousMs !== void 0 && normalizedPreviousMs < nowMs ? normalizedPreviousMs : void 0; } //#endregion export { computePreviousRunAtMs as n, computeNextRunAtMs as t };