fix(schedule): keep low-year LMT handoff safe

This commit is contained in:
pku-xht
2026-08-08 06:03:14 +08:00
committed by Tianyi Cui
parent 7f5b301d5f
commit b37877d80b
2 changed files with 16 additions and 4 deletions
@@ -815,12 +815,15 @@ function ownedLowYearCronInstant(
function nextCronInstant(rule: ParsedCronRule, timeZone: string, after: number): number | undefined {
if (!rule.hasMatchingDate) return undefined
let cursor = after
const formatter = cronLocalFormatter(timeZone)
if (new Date(after).getUTCFullYear() <= CRONER_LOW_YEAR_CUTOFF) {
const lower = ownedLowYearCronInstant(rule, timeZone, after, 1)
if (lower !== undefined) return lower
if (localProjection(formatter, after).offset % 60_000 !== 0) {
cursor = cursorBeforeNextOffsetTransition(formatter, after)
}
}
const evaluator = cronEvaluator(rule, timeZone)
const formatter = cronLocalFormatter(timeZone)
let gapCorrections = 0
while (cursor < MAX_FOUR_DIGIT_YEAR_MS) {
const candidate = evaluator.nextRun(new Date(cursor))
@@ -840,6 +843,7 @@ function nextCronInstant(rule: ParsedCronRule, timeZone: string, after: number):
}
gapCorrections = 0
if (epoch > MAX_FOUR_DIGIT_YEAR_MS) return undefined
/* v8 ignore next 3 -- current IANA data leaves sub-minute LMT at its first transition. */
if (epoch % 60_000 !== 0) {
cursor = cursorBeforeNextOffsetTransition(formatter, epoch)
continue
@@ -246,15 +246,23 @@ describe('Croner calendar adapter', () => {
})
it('skips a sub-minute local-mean-time era before iterating dense safe-year matches', () => {
const record = createCronScheduleRecord(
const yearOne = createCronScheduleRecord(
ScheduleId('schedule-sub-minute-offset-year-one'),
'standard-time handoff',
'*/5 * * * *',
'Europe/Amsterdam',
Date.parse('0001-01-01T00:00:00.000Z'),
)
const yearOneHundred = createCronScheduleRecord(
ScheduleId('schedule-sub-minute-offset'),
'standard-time handoff',
'*/5 * * * *',
'Europe/Amsterdam',
Date.parse('0100-01-01T00:00:00.000Z'),
)
expect(new Date(record.scheduledAt).getUTCFullYear()).toBeGreaterThan(109)
expect(Math.abs(Date.parse(record.scheduledAt) % 60_000)).toBe(0)
expect(yearOne.scheduledAt).toBe(yearOneHundred.scheduledAt)
expect(new Date(yearOne.scheduledAt).getUTCFullYear()).toBeGreaterThan(109)
expect(Math.abs(Date.parse(yearOne.scheduledAt) % 60_000)).toBe(0)
}, 1_000)
it('selects the latest current match after a persisted baseline', () => {