From 2ef0b81743a9840220888020157064a2b76539b6 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Tue, 3 Mar 2026 20:44:36 +0100 Subject: [PATCH] refactor: extract midnight cross rules --- .../src/services/__tests__/timerUtils.test.ts | 22 ++++++++++++++++++- apps/server/src/services/timerUtils.ts | 11 +++++++++- apps/server/src/stores/runtimeState.ts | 5 +++-- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/apps/server/src/services/__tests__/timerUtils.test.ts b/apps/server/src/services/__tests__/timerUtils.test.ts index 7b6d37c04..792cd3021 100644 --- a/apps/server/src/services/__tests__/timerUtils.test.ts +++ b/apps/server/src/services/__tests__/timerUtils.test.ts @@ -1,5 +1,5 @@ import { dayInMs, MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND, millisToString } from 'ontime-utils'; -import { EndAction, Playback, TimeStrategy, TimerPhase, TimerType } from 'ontime-types'; +import { EndAction, Playback, TimeOfDay, TimeStrategy, TimerPhase, TimerType } from 'ontime-types'; import { findDayOffset, @@ -7,6 +7,7 @@ import { getExpectedFinish, getRuntimeOffset, getTimerPhase, + hasCrossedMidnight, normaliseEndTime, skippedOutOfEvent, } from '../timerUtils.js'; @@ -537,6 +538,25 @@ describe('getExpectedFinish() and getCurrentTime() combined', () => { }); }); +describe('hasCrossedMidnight()', () => { + it('returns true when clock wraps from late to early', () => { + const previous = (23 * MILLIS_PER_HOUR + 59 * MILLIS_PER_MINUTE) as TimeOfDay; // 23:59 + const current = (1 * MILLIS_PER_MINUTE) as TimeOfDay; // 00:01 + expect(hasCrossedMidnight(previous, current)).toBe(true); + }); + + it('returns false when clock moves forward on the same day', () => { + const previous = (10 * MILLIS_PER_HOUR) as TimeOfDay; // 10:00 + const current = (10 * MILLIS_PER_HOUR + 5 * MILLIS_PER_MINUTE) as TimeOfDay; // 10:05 + expect(hasCrossedMidnight(previous, current)).toBe(false); + }); + + it('returns false when clock value is unchanged', () => { + const time = (15 * MILLIS_PER_HOUR) as TimeOfDay; // 15:00 + expect(hasCrossedMidnight(time, time)).toBe(false); + }); +}); + describe('skippedOutOfEvent()', () => { const testSkipLimit = 32; it('does not consider an event end as a skip', () => { diff --git a/apps/server/src/services/timerUtils.ts b/apps/server/src/services/timerUtils.ts index cf4ba87e2..aae618d1b 100644 --- a/apps/server/src/services/timerUtils.ts +++ b/apps/server/src/services/timerUtils.ts @@ -1,4 +1,4 @@ -import { Day, MaybeNumber, TimerPhase } from 'ontime-types'; +import { Day, MaybeNumber, TimeOfDay, TimerPhase } from 'ontime-types'; import { checkIsNow, dayInMs, isPlaybackActive, MILLIS_PER_HOUR } from 'ontime-utils'; import type { RuntimeState } from '../stores/runtimeState.js'; @@ -8,6 +8,15 @@ import type { RuntimeState } from '../stores/runtimeState.js'; */ export const normaliseEndTime = (start: number, end: number) => (end < start ? end + dayInMs : end); +/** + * Checks whether the local wall clock wrapped into a new day + * This currently uses a simple wrap heuristic and is centralized + * so day-boundary behavior can be evolved in one place later. + */ +export function hasCrossedMidnight(previous: TimeOfDay, current: TimeOfDay): boolean { + return previous > current; +} + /** * Calculates expected finish time of a running timer * @param {RuntimeState} state runtime state diff --git a/apps/server/src/stores/runtimeState.ts b/apps/server/src/stores/runtimeState.ts index 9775b28d4..26ac65fa1 100644 --- a/apps/server/src/stores/runtimeState.ts +++ b/apps/server/src/stores/runtimeState.ts @@ -35,6 +35,7 @@ import { getExpectedFinish, getRuntimeOffset, getTimerPhase, + hasCrossedMidnight, } from '../services/timerUtils.js'; import { loadRoll, normaliseRollStart } from '../services/rollUtils.js'; import { timerConfig } from '../setup/config.js'; @@ -555,8 +556,8 @@ export function update(): UpdateResult { // 2. are we waiting to roll? if (runtimeState.timer.playback === Playback.Roll && runtimeState.timer.secondaryTimer !== null) { - const hasCrossedMidnight = previousClock > now; - return updateIfWaitingToRoll(hasCrossedMidnight); + const clockHasCrossedMidnight = hasCrossedMidnight(previousClock, now); + return updateIfWaitingToRoll(clockHasCrossedMidnight); } // 3. at this point we know that we are playing an event