fix(schedule): keep low-year LMT handoff safe
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user