This commit is contained in:
impozzible 2026-01-30 09:21:41 +07:00 committed by GitHub
commit 4812d1552e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 29 additions and 6 deletions

View File

@ -1,7 +1,16 @@
import { Cron } from "croner";
import type { CronSchedule } from "./types.js";
export function computeNextRunAtMs(schedule: CronSchedule, nowMs: number): number | undefined {
export type ComputeNextRunOpts = {
/** Default timezone to use when schedule.tz is not specified. */
defaultTimezone?: string;
};
export function computeNextRunAtMs(
schedule: CronSchedule,
nowMs: number,
opts?: ComputeNextRunOpts,
): number | undefined {
if (schedule.kind === "at") {
return schedule.atMs > nowMs ? schedule.atMs : undefined;
}
@ -17,8 +26,10 @@ export function computeNextRunAtMs(schedule: CronSchedule, nowMs: number): numbe
const expr = schedule.expr.trim();
if (!expr) return undefined;
// Use schedule.tz if specified, otherwise fall back to default timezone
const timezone = schedule.tz?.trim() || opts?.defaultTimezone?.trim() || undefined;
const cron = new Cron(expr, {
timezone: schedule.tz?.trim() || undefined,
timezone,
catch: false,
});
const next = cron.nextRun(new Date(nowMs));

View File

@ -33,19 +33,24 @@ export function findJobOrThrow(state: CronServiceState, id: string) {
return job;
}
export function computeJobNextRunAtMs(job: CronJob, nowMs: number): number | undefined {
export function computeJobNextRunAtMs(
job: CronJob,
nowMs: number,
opts?: { defaultTimezone?: string },
): number | undefined {
if (!job.enabled) return undefined;
if (job.schedule.kind === "at") {
// One-shot jobs stay due until they successfully finish.
if (job.state.lastStatus === "ok" && job.state.lastRunAtMs) return undefined;
return job.schedule.atMs;
}
return computeNextRunAtMs(job.schedule, nowMs);
return computeNextRunAtMs(job.schedule, nowMs, { defaultTimezone: opts?.defaultTimezone });
}
export function recomputeNextRuns(state: CronServiceState) {
if (!state.store) return;
const now = state.deps.nowMs();
const defaultTimezone = state.deps.defaultTimezone;
for (const job of state.store.jobs) {
if (!job.state) job.state = {};
if (!job.enabled) {
@ -61,7 +66,7 @@ export function recomputeNextRuns(state: CronServiceState) {
);
job.state.runningAtMs = undefined;
}
job.state.nextRunAtMs = computeJobNextRunAtMs(job, now);
job.state.nextRunAtMs = computeJobNextRunAtMs(job, now, { defaultTimezone });
}
}
@ -77,6 +82,7 @@ export function nextWakeAtMs(state: CronServiceState) {
export function createJob(state: CronServiceState, input: CronJobCreate): CronJob {
const now = state.deps.nowMs();
const defaultTimezone = state.deps.defaultTimezone;
const id = crypto.randomUUID();
const job: CronJob = {
id,
@ -97,7 +103,7 @@ export function createJob(state: CronServiceState, input: CronJobCreate): CronJo
},
};
assertSupportedJobSpec(job);
job.state.nextRunAtMs = computeJobNextRunAtMs(job, now);
job.state.nextRunAtMs = computeJobNextRunAtMs(job, now, { defaultTimezone });
return job;
}

View File

@ -24,6 +24,8 @@ export type CronServiceDeps = {
log: Logger;
storePath: string;
cronEnabled: boolean;
/** Default timezone (IANA) for cron schedules that don't specify their own. */
defaultTimezone?: string;
enqueueSystemEvent: (text: string, opts?: { agentId?: string }) => void;
requestHeartbeatNow: (opts?: { reason?: string }) => void;
runHeartbeatOnce?: (opts?: { reason?: string }) => Promise<HeartbeatRunResult>;

View File

@ -43,9 +43,13 @@ export function buildGatewayCronService(params: {
return { agentId, cfg: runtimeConfig };
};
// Use userTimezone from config as default for cron schedules
const defaultTimezone = params.cfg.agents?.defaults?.userTimezone;
const cron = new CronService({
storePath,
cronEnabled,
defaultTimezone,
enqueueSystemEvent: (text, opts) => {
const { agentId, cfg: runtimeConfig } = resolveCronAgent(opts?.agentId);
const sessionKey = resolveAgentMainSessionKey({