diff --git a/apps/server/src/services/__tests__/timerUtils.test.ts b/apps/server/src/services/__tests__/timerUtils.test.ts index bf011ef76..5de0d5da3 100644 --- a/apps/server/src/services/__tests__/timerUtils.test.ts +++ b/apps/server/src/services/__tests__/timerUtils.test.ts @@ -506,6 +506,7 @@ describe('getCurrent()', () => { timeStart: 79200000, // 22:00:00 timeEnd: 600000, // 00:10:00 countToEnd: true, + dayOffset: 0, }, clock: 79500000, // 22:05:00 timer: { @@ -516,6 +517,7 @@ describe('getCurrent()', () => { rundown: { actualStart: 79200000, plannedEnd: 600000, + currentDay: 0, }, _timer: { pausedAt: null, @@ -527,6 +529,35 @@ describe('getCurrent()', () => { expect(current).toBe(dayInMs - 79500000 + 600000); }); + it('handles overnight count-to-end after midnight', () => { + const state = { + eventNow: { + timeStart: 23 * MILLIS_PER_HOUR, // 23:00:00 + timeEnd: 1 * MILLIS_PER_HOUR, // 01:00:00 + countToEnd: true, + dayOffset: 0, + }, + clock: 30 * MILLIS_PER_MINUTE, // 00:30:00 on day 1 + timer: { + addedTime: 0, + duration: Infinity, + startedAt: 23 * MILLIS_PER_HOUR, + }, + rundown: { + actualStart: 23 * MILLIS_PER_HOUR, + plannedEnd: 1 * MILLIS_PER_HOUR, + currentDay: 1, + }, + _timer: { + pausedAt: null, + hasFinished: false, + }, + } as RuntimeState; + + const current = getCurrent(state); + expect(current).toBe(30 * MILLIS_PER_MINUTE); + }); + it('handles events that were started late', () => { const state = { clock: 82000000, // 22:46:40 <--- starting 16 min after the scheduled end diff --git a/apps/server/src/services/timerUtils.ts b/apps/server/src/services/timerUtils.ts index 1ec955db8..7429e2b21 100644 --- a/apps/server/src/services/timerUtils.ts +++ b/apps/server/src/services/timerUtils.ts @@ -77,9 +77,11 @@ export function getCurrent(state: RuntimeState): number { if (countToEnd) { // count to end runs to its fixed end, so added time does not stretch the countdown - const isEventOverMidnight = timeStart > timeEnd; - const correctDay = isEventOverMidnight ? dayInMs : 0; - return correctDay - clock + timeEnd; + const dayOffset = state.eventNow.dayOffset ?? 0; + const currentDay = + state.rundown.currentDay ?? (timeStart > timeEnd && clock <= timeEnd ? dayOffset + 1 : dayOffset); + const endDay = timeStart > timeEnd ? dayOffset + 1 : dayOffset; + return timeEnd + endDay * dayInMs - (clock + currentDay * dayInMs); } if (startedAt === null) { diff --git a/packages/utils/src/date-utils/getExpected.test.ts b/packages/utils/src/date-utils/getExpected.test.ts index c18303c82..4aa0a0b64 100644 --- a/packages/utils/src/date-utils/getExpected.test.ts +++ b/packages/utils/src/date-utils/getExpected.test.ts @@ -99,6 +99,36 @@ describe('getExpectedStart()', () => { expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: false })).toBe(110); // <-- when gap is not enough to compensate for the running behind it absorbs at much as possible // expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(70); This should not be possible }); + + test('negative delay cannot move a day 0 event before the rundown start', () => { + const testState = { + currentDay: 0, + totalGap: 0, + offset: 0, + mode: OffsetMode.Absolute, + actualStart: null, + plannedStart: null, + isLinkedToLoaded: true, + }; + + expect(getExpectedStart({ timeStart: 0, delay: -5, dayOffset: 0 as Day }, testState)).toBe(0); + expect(getExpectedStart({ timeStart: 10, delay: -5, dayOffset: 0 as Day }, testState)).toBe(5); + }); + + test('negative delay can move a later-day event back to the previous day', () => { + const testState = { + currentDay: 0, + totalGap: 0, + offset: 0, + mode: OffsetMode.Absolute, + actualStart: null, + plannedStart: null, + isLinkedToLoaded: true, + }; + + expect(getExpectedStart({ timeStart: 0, delay: -5, dayOffset: 1 as Day }, testState)).toBe(dayInMs - 5); + expect(getExpectedStart({ timeStart: 10, delay: -20, dayOffset: 1 as Day }, testState)).toBe(dayInMs - 10); + }); }); describe('Relative offset mode', () => { @@ -413,6 +443,19 @@ describe('getExpectedEnd()', () => { expect(getExpectedEnd({ ...testEvent, dayOffset: 0 as Day }, { ...baseState, currentDay: 0, offset: 0 })).toBe(150); }); + test('a countToEnd event with negative delay can start on the previous day but keeps its fixed end', () => { + const testEvent = { + timeStart: 0, + duration: 50, + delay: -10, + dayOffset: 1 as Day, + countToEnd: true, + }; + + expect(getExpectedStart(testEvent, { ...baseState, currentDay: 0, offset: 0 })).toBe(dayInMs - 10); + expect(getExpectedEnd(testEvent, { ...baseState, currentDay: 0, offset: 0 })).toBe(dayInMs + 50); + }); + test('a countToEnd event is anchored to its wall-clock end in relative mode', () => { const testEvent = { timeStart: 100, diff --git a/packages/utils/src/date-utils/getExpected.ts b/packages/utils/src/date-utils/getExpected.ts index 9b179b75c..eda73054a 100644 --- a/packages/utils/src/date-utils/getExpected.ts +++ b/packages/utils/src/date-utils/getExpected.ts @@ -23,13 +23,8 @@ export function getExpectedStart( const { timeStart, dayOffset, delay } = event; const { currentDay, totalGap, isLinkedToLoaded, offset, mode, actualStart, plannedStart } = state; - //How many days from the currently running event to this one - const relativeDayOffset = dayOffset - currentDay; - - const delayedStart = Math.max(0, timeStart + delay); - - //The normalised start time of this event relative to the currently running event - const normalisedTimeStart = delayedStart + relativeDayOffset * dayInMs; + const absoluteDelayedStart = Math.max(0, dayOffset * dayInMs + timeStart + delay); + const normalisedTimeStart = absoluteDelayedStart - currentDay * dayInMs; let relativeStartOffset = 0; @@ -69,9 +64,7 @@ export function getExpectedEnd( * - the end time is always the wall clock */ if (event.countToEnd) { - // account for day offset - const relativeDayOffset = event.dayOffset - state.currentDay; - const plannedEnd = event.timeStart + event.duration + relativeDayOffset * dayInMs; + const plannedEnd = event.dayOffset * dayInMs + event.timeStart + event.duration - state.currentDay * dayInMs; // count to end should finish on the planned time or on start return Math.max(expectedStart, plannedEnd);