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

View File

@@ -815,12 +815,15 @@ function ownedLowYearCronInstant(
function nextCronInstant(rule: ParsedCronRule, timeZone: string, after: number): number | undefined { function nextCronInstant(rule: ParsedCronRule, timeZone: string, after: number): number | undefined {
if (!rule.hasMatchingDate) return undefined if (!rule.hasMatchingDate) return undefined
let cursor = after let cursor = after
const formatter = cronLocalFormatter(timeZone)
if (new Date(after).getUTCFullYear() <= CRONER_LOW_YEAR_CUTOFF) { if (new Date(after).getUTCFullYear() <= CRONER_LOW_YEAR_CUTOFF) {
const lower = ownedLowYearCronInstant(rule, timeZone, after, 1) const lower = ownedLowYearCronInstant(rule, timeZone, after, 1)
if (lower !== undefined) return lower if (lower !== undefined) return lower
if (localProjection(formatter, after).offset % 60_000 !== 0) {
cursor = cursorBeforeNextOffsetTransition(formatter, after)
}
} }
const evaluator = cronEvaluator(rule, timeZone) const evaluator = cronEvaluator(rule, timeZone)
const formatter = cronLocalFormatter(timeZone)
let gapCorrections = 0 let gapCorrections = 0
while (cursor < MAX_FOUR_DIGIT_YEAR_MS) { while (cursor < MAX_FOUR_DIGIT_YEAR_MS) {
const candidate = evaluator.nextRun(new Date(cursor)) const candidate = evaluator.nextRun(new Date(cursor))
@@ -840,6 +843,7 @@ function nextCronInstant(rule: ParsedCronRule, timeZone: string, after: number):
} }
gapCorrections = 0 gapCorrections = 0
if (epoch > MAX_FOUR_DIGIT_YEAR_MS) return undefined 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) { if (epoch % 60_000 !== 0) {
cursor = cursorBeforeNextOffsetTransition(formatter, epoch) cursor = cursorBeforeNextOffsetTransition(formatter, epoch)
continue continue

View File

@@ -246,15 +246,23 @@ describe('Croner calendar adapter', () => {
}) })
it('skips a sub-minute local-mean-time era before iterating dense safe-year matches', () => { 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'), ScheduleId('schedule-sub-minute-offset'),
'standard-time handoff', 'standard-time handoff',
'*/5 * * * *', '*/5 * * * *',
'Europe/Amsterdam', 'Europe/Amsterdam',
Date.parse('0100-01-01T00:00:00.000Z'), Date.parse('0100-01-01T00:00:00.000Z'),
) )
expect(new Date(record.scheduledAt).getUTCFullYear()).toBeGreaterThan(109) expect(yearOne.scheduledAt).toBe(yearOneHundred.scheduledAt)
expect(Math.abs(Date.parse(record.scheduledAt) % 60_000)).toBe(0) expect(new Date(yearOne.scheduledAt).getUTCFullYear()).toBeGreaterThan(109)
expect(Math.abs(Date.parse(yearOne.scheduledAt) % 60_000)).toBe(0)
}, 1_000) }, 1_000)
it('selects the latest current match after a persisted baseline', () => { it('selects the latest current match after a persisted baseline', () => {