diff --git a/apps/server/src/lib/time-core/timeCore.ts b/apps/server/src/lib/time-core/timeCore.ts index 269dd2d76..55b39ed45 100644 --- a/apps/server/src/lib/time-core/timeCore.ts +++ b/apps/server/src/lib/time-core/timeCore.ts @@ -1,5 +1,5 @@ import { Day, Duration, Instant, TimeOfDay } from 'ontime-types'; -import { MILLIS_PER_MINUTE, dayInMs } from 'ontime-utils'; +import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND, dayInMs } from 'ontime-utils'; /** Returns the current instant */ export function now(): Instant { @@ -59,6 +59,10 @@ export function addDuration(instant: Instant, duration: Duration | Duration[]): return (instant + totalDuration) as Instant; } +export function combineValues(...duration: T[]): T { + return Array.isArray(duration) ? (duration.reduce((total, current) => total + current, 0) as T) : duration; +} + /** * Calculates elapsed time on the clock from a starting time to the current time * Handles overnight crossing (when current < start, assumes we've crossed midnight) @@ -80,3 +84,15 @@ export function daysSinceStart(startEpoch: Instant, currentEpoch: Instant): Day return (currentDaySerial - startDaySerial) as Day; } + +export const ZERO_DURATION = 0 as Duration; + +/** + * utility function to generate Instant times + */ +export function generateInstanceFromClock(hour: number, minutes: number, second: number, ms = 0) { + return toInstant( + (hour * MILLIS_PER_HOUR + minutes * MILLIS_PER_MINUTE + second * MILLIS_PER_SECOND + ms) as TimeOfDay, + now(), + ); +} diff --git a/apps/server/src/services/__tests__/timerUtils.test.ts b/apps/server/src/services/__tests__/timerUtils.test.ts index eecc5893a..660aa00dd 100644 --- a/apps/server/src/services/__tests__/timerUtils.test.ts +++ b/apps/server/src/services/__tests__/timerUtils.test.ts @@ -1,7 +1,8 @@ -import { EndAction, Playback, TimeOfDay, TimeStrategy, TimerPhase, TimerType } from 'ontime-types'; +import { Duration, EndAction, Playback, TimeOfDay, TimeStrategy, TimerPhase, TimerType } from 'ontime-types'; import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND, dayInMs, millisToString } from 'ontime-utils'; -import type { RuntimeState } from '../../stores/runtimeState.js'; +import * as timeCore from '../../lib/time-core/timeCore.js'; +import type { InternalRuntimeState, RuntimeState } from '../../stores/runtimeState.js'; import { findDayOffset, getCurrent, @@ -19,7 +20,7 @@ const asTimeOfDay = (value: number): RuntimeState['clock'] => value as RuntimeSt describe('getElapsed()', () => { it('returns active elapsed time from startedAt without add-time adjustments', () => { const state = { - clock: 5 * MILLIS_PER_MINUTE, + _now: timeCore.generateInstanceFromClock(0, 5, 0), timer: { addedTime: -10 * MILLIS_PER_MINUTE, current: 15 * MILLIS_PER_MINUTE, @@ -30,14 +31,14 @@ describe('getElapsed()', () => { pausedAt: null, pausedDuration: 0, }, - } as RuntimeState; + } as InternalRuntimeState; expect(getElapsed(state)).toBe(3 * MILLIS_PER_MINUTE); }); it('subtracts accumulated pause time', () => { const state = { - clock: 10 * MILLIS_PER_MINUTE, + _now: timeCore.generateInstanceFromClock(0, 10, 0), timer: { startedAt: 2 * MILLIS_PER_MINUTE, }, @@ -45,14 +46,14 @@ describe('getElapsed()', () => { pausedAt: null, pausedDuration: 5 * MILLIS_PER_MINUTE, }, - } as RuntimeState; + } as InternalRuntimeState; expect(getElapsed(state)).toBe(3 * MILLIS_PER_MINUTE); }); it('uses the current pause start while paused', () => { const state = { - clock: 10 * MILLIS_PER_MINUTE, + _now: timeCore.generateInstanceFromClock(0, 10, 0), timer: { startedAt: 2 * MILLIS_PER_MINUTE, }, @@ -60,7 +61,7 @@ describe('getElapsed()', () => { pausedAt: 7 * MILLIS_PER_MINUTE, pausedDuration: 1 * MILLIS_PER_MINUTE, }, - } as RuntimeState; + } as InternalRuntimeState; expect(getElapsed(state)).toBe(4 * MILLIS_PER_MINUTE); }); @@ -82,7 +83,7 @@ describe('getExpectedFinish()', () => { pausedAt: null, hasFinished: false, }, - } as RuntimeState; + } as InternalRuntimeState; const calculatedFinish = getExpectedFinish(state); expect(calculatedFinish).toBe(null); }); @@ -101,7 +102,7 @@ describe('getExpectedFinish()', () => { pausedAt: null, hasFinished: true, }, - } as RuntimeState; + } as InternalRuntimeState; const calculatedFinish = getExpectedFinish(state); expect(calculatedFinish).toBe(20); }); @@ -120,7 +121,7 @@ describe('getExpectedFinish()', () => { pausedAt: null, hasFinished: false, }, - } as RuntimeState; + } as InternalRuntimeState; const calculatedFinish = getExpectedFinish(state); expect(calculatedFinish).toBe(11); }); @@ -139,7 +140,7 @@ describe('getExpectedFinish()', () => { pausedAt: null, hasFinished: false, }, - } as RuntimeState; + } as InternalRuntimeState; const calculatedFinish = getExpectedFinish(state); expect(calculatedFinish).toBe(31); @@ -159,7 +160,7 @@ describe('getExpectedFinish()', () => { pausedAt: null, hasFinished: false, }, - } as RuntimeState; + } as InternalRuntimeState; const calculatedFinish = getExpectedFinish(state); expect(calculatedFinish).toBe(1); @@ -179,7 +180,7 @@ describe('getExpectedFinish()', () => { pausedAt: null, hasFinished: false, }, - } as RuntimeState; + } as InternalRuntimeState; const calculatedFinish = getExpectedFinish(state); expect(calculatedFinish).toBe(1); @@ -199,7 +200,7 @@ describe('getExpectedFinish()', () => { pausedAt: null, hasFinished: false, }, - } as RuntimeState; + } as InternalRuntimeState; const calculatedFinish = getExpectedFinish(state); expect(calculatedFinish).toBe(1); @@ -219,7 +220,7 @@ describe('getExpectedFinish()', () => { pausedAt: null, hasFinished: false, }, - } as RuntimeState; + } as InternalRuntimeState; const calculatedFinish = getExpectedFinish(state); expect(calculatedFinish).toBe(10); @@ -240,7 +241,7 @@ describe('getExpectedFinish()', () => { pausedAt: null, hasFinished: false, }, - } as RuntimeState; + } as InternalRuntimeState; const calculatedFinish = getExpectedFinish(state); expect(calculatedFinish).toBe(40); @@ -263,7 +264,7 @@ describe('getExpectedFinish()', () => { actualStart: 79200000, plannedEnd: 600000, }, - } as RuntimeState; + } as InternalRuntimeState; const calculatedFinish = getExpectedFinish(state); // expected finish is not a duration but a point in time @@ -279,7 +280,7 @@ describe('getCurrent()', () => { timeEnd: 30, timerType: TimerType.CountDown, }, - clock: 0, + _now: timeCore.generateInstanceFromClock(0, 0, 0), timer: { addedTime: 10, duration: 111, // <-- we take the duration value @@ -289,7 +290,7 @@ describe('getCurrent()', () => { pausedAt: null, hasFinished: false, }, - } as RuntimeState; + } as InternalRuntimeState; const current = getCurrent(state); expect(current).toBe(111); @@ -300,7 +301,7 @@ describe('getCurrent()', () => { timeEnd: 10, timerType: TimerType.CountDown, }, - clock: 1, + _now: timeCore.generateInstanceFromClock(0, 0, 0, 1), timer: { addedTime: 0, duration: 10, @@ -310,7 +311,7 @@ describe('getCurrent()', () => { pausedAt: null, hasFinished: false, }, - } as RuntimeState; + } as InternalRuntimeState; const current = getCurrent(state); expect(current).toBe(9); @@ -321,7 +322,7 @@ describe('getCurrent()', () => { timeEnd: 10, timerType: TimerType.CountDown, }, - clock: 1, + _now: timeCore.generateInstanceFromClock(0, 0, 0, 1), timer: { addedTime: 10, duration: 10, @@ -331,7 +332,7 @@ describe('getCurrent()', () => { pausedAt: null, hasFinished: false, }, - } as RuntimeState; + } as InternalRuntimeState; const current = getCurrent(state); expect(current).toBe(19); @@ -342,7 +343,7 @@ describe('getCurrent()', () => { timeEnd: 20, timerType: TimerType.CountDown, }, - clock: 10, + _now: timeCore.generateInstanceFromClock(0, 0, 0, 10), timer: { addedTime: 0, duration: dayInMs + 10, @@ -352,7 +353,7 @@ describe('getCurrent()', () => { pausedAt: null, hasFinished: false, }, - } as RuntimeState; + } as InternalRuntimeState; const current = getCurrent(state); expect(current).toBe(dayInMs + 10); @@ -363,7 +364,7 @@ describe('getCurrent()', () => { timeEnd: 20, timerType: TimerType.CountDown, }, - clock: 5, + _now: timeCore.generateInstanceFromClock(0, 0, 0, 5), timer: { addedTime: 0, duration: dayInMs + 10, @@ -373,7 +374,7 @@ describe('getCurrent()', () => { pausedAt: null, hasFinished: false, }, - } as RuntimeState; + } as InternalRuntimeState; const current = getCurrent(state); expect(current).toBe(15); @@ -384,7 +385,7 @@ describe('getCurrent()', () => { timeEnd: 20, timerType: TimerType.CountDown, }, - clock: 5, + _now: timeCore.generateInstanceFromClock(0, 0, 0, 5), timer: { addedTime: 20, duration: dayInMs + 10, @@ -394,7 +395,7 @@ describe('getCurrent()', () => { pausedAt: null, hasFinished: false, }, - } as RuntimeState; + } as InternalRuntimeState; const current = getCurrent(state); expect(current).toBe(35); @@ -407,7 +408,7 @@ describe('getCurrent()', () => { timeEnd: 100, countToEnd: true, }, - clock: 30, + _now: timeCore.generateInstanceFromClock(0, 0, 0, 30), timer: { addedTime: 0, duration: 100, @@ -420,7 +421,7 @@ describe('getCurrent()', () => { pausedAt: null, hasFinished: false, }, - } as RuntimeState; + } as InternalRuntimeState; const current = getCurrent(state); expect(current).toBe(70); @@ -432,7 +433,7 @@ describe('getCurrent()', () => { timeEnd: 100, countToEnd: true, }, - clock: 30, + _now: timeCore.generateInstanceFromClock(0, 0, 0, 30), timer: { addedTime: 0, duration: 100, @@ -445,7 +446,7 @@ describe('getCurrent()', () => { pausedAt: null, hasFinished: false, }, - } as RuntimeState; + } as InternalRuntimeState; const current = getCurrent(state); expect(current).toBe(70); @@ -457,7 +458,7 @@ describe('getCurrent()', () => { timeEnd: 100, countToEnd: true, }, - clock: 30, + _now: timeCore.generateInstanceFromClock(0, 0, 0, 30), timer: { addedTime: 7, duration: 100, @@ -470,7 +471,7 @@ describe('getCurrent()', () => { pausedAt: null, hasFinished: false, }, - } as RuntimeState; + } as InternalRuntimeState; const current = getCurrent(state); expect(current).toBe(77); @@ -483,7 +484,7 @@ describe('getCurrent()', () => { timeEnd: 600000, // 00:10:00 countToEnd: true, }, - clock: 79500000, // 22:05:00 + _now: timeCore.generateInstanceFromClock(22, 5, 0), timer: { addedTime: 0, duration: Infinity, // not relevant, @@ -497,7 +498,7 @@ describe('getCurrent()', () => { pausedAt: null, hasFinished: false, }, - } as RuntimeState; + } as InternalRuntimeState; const current = getCurrent(state); expect(current).toBe(dayInMs - 79500000 + 600000); @@ -505,7 +506,7 @@ describe('getCurrent()', () => { it('handles events that were started late', () => { const state = { - clock: 82000000, // 22:46:40 <--- starting 16 min after the scheduled end + _now: timeCore.generateInstanceFromClock(22, 46, 40), // 22:46:40 <--- starting 16 min after the scheduled end eventNow: { timeStart: 77400000, // 21:30:00 timeEnd: 81000000, // 22:30:00 @@ -525,7 +526,7 @@ describe('getCurrent()', () => { pausedAt: null, hasFinished: false, }, - } as RuntimeState; + } as InternalRuntimeState; const current = getCurrent(state); expect(current).toBe(81000000 - 82000000); // <-- planned end - now @@ -541,7 +542,7 @@ describe('getExpectedFinish() and getCurrentTime() combined', () => { timeEnd: 10, timerType: TimerType.CountDown, }, - clock: 0, + _now: timeCore.generateInstanceFromClock(0, 0, 0), timer: { addedTime: 0, duration, @@ -551,7 +552,7 @@ describe('getExpectedFinish() and getCurrentTime() combined', () => { pausedAt: null, hasFinished: false, }, - } as RuntimeState; + } as InternalRuntimeState; const expectedFinish = getExpectedFinish(state); const current = getCurrent(state); @@ -569,7 +570,7 @@ describe('getExpectedFinish() and getCurrentTime() combined', () => { timeEnd: 10, timerType: TimerType.CountDown, }, - clock: 5, + _now: timeCore.generateInstanceFromClock(0, 0, 0, 5), timer: { addedTime: 3, duration, @@ -579,7 +580,7 @@ describe('getExpectedFinish() and getCurrentTime() combined', () => { pausedAt: null, hasFinished: false, }, - } as RuntimeState; + } as InternalRuntimeState; const expectedFinish = getExpectedFinish(state); const current = getCurrent(state); @@ -883,9 +884,9 @@ describe('getRuntimeOffset()', () => { plannedStart: 100, currentDay: 0, }, - clock: 150, + _now: timeCore.generateInstanceFromClock(0, 0, 0, 150), _startDayOffset: 0, - } as RuntimeState; + } as InternalRuntimeState; const { absolute } = getRuntimeOffset(state); expect(absolute).toBe(50); @@ -912,7 +913,7 @@ describe('getRuntimeOffset()', () => { currentDay: 0, }, _startDayOffset: 0, - } as RuntimeState; + } as InternalRuntimeState; const { absolute } = getRuntimeOffset(state); expect(absolute).toBe(60); @@ -940,7 +941,7 @@ describe('getRuntimeOffset()', () => { currentDay: 0, }, _startDayOffset: 0, - } as RuntimeState; + } as InternalRuntimeState; const { absolute } = getRuntimeOffset(state); expect(absolute).toBe(10); @@ -954,7 +955,7 @@ describe('getRuntimeOffset()', () => { timeEnd: 150, dayOffset: 0, }, - clock: 150, + _now: timeCore.generateInstanceFromClock(0, 0, 0, 150), timer: { startedAt: 100, // started on time current: 25, // are 25ms into it @@ -969,7 +970,7 @@ describe('getRuntimeOffset()', () => { currentDay: 0, }, _startDayOffset: 0, - } as RuntimeState; + } as InternalRuntimeState; const { absolute } = getRuntimeOffset(state); expect(absolute).toBe(25); @@ -977,7 +978,7 @@ describe('getRuntimeOffset()', () => { it('offset doesnt exist if we havent started', () => { const state = { - clock: 78480789, + _now: timeCore.generateInstanceFromClock(22, 48, 0), // 78480789 eventNow: { id: 'd6a2ce', timeStart: 77400000, @@ -1011,7 +1012,7 @@ describe('getRuntimeOffset()', () => { }, _startDayOffset: 0, _timer: { pausedAt: null }, - } as RuntimeState; + } as InternalRuntimeState; const { absolute } = getRuntimeOffset(state); expect(absolute).toBe(0); @@ -1019,7 +1020,7 @@ describe('getRuntimeOffset()', () => { it('with time-to-end, offsets dont exist if we are not in overtime', () => { const state = { - clock: 80000000, // 22:13:20 + _now: timeCore.generateInstanceFromClock(22, 13, 20), eventNow: { id: 'd6a2ce', type: 'event', @@ -1067,7 +1068,7 @@ describe('getRuntimeOffset()', () => { }, _startDayOffset: 0, _timer: { pausedAt: null }, - } as RuntimeState; + } as InternalRuntimeState; const { absolute } = getRuntimeOffset(state); expect(absolute).toBe(0); @@ -1075,7 +1076,7 @@ describe('getRuntimeOffset()', () => { it('with time-to-end, offset is the overtime', () => { const state = { - clock: 82000000, // 22:46:40 + _now: timeCore.generateInstanceFromClock(22, 46, 40), eventNow: { id: 'd6a2ce', type: 'event', @@ -1123,7 +1124,7 @@ describe('getRuntimeOffset()', () => { }, _startDayOffset: 0, _timer: { pausedAt: null }, - } as RuntimeState; + } as InternalRuntimeState; const { absolute } = getRuntimeOffset(state); expect(absolute).toBe(400000 - 200000); // <--- offset is always the overtime + added time @@ -1131,7 +1132,7 @@ describe('getRuntimeOffset()', () => { it('handles time-to-end started after the end time', () => { const state = { - clock: 82000000, // 22:46:40 <--- starting 16m 40s after the scheduled end + _now: timeCore.generateInstanceFromClock(22, 46, 40), // 22:46:40 <--- starting 16m 40s after the scheduled end eventNow: { id: 'd6a2ce', timeStart: 77400000, // 21:30:00 @@ -1168,10 +1169,10 @@ describe('getRuntimeOffset()', () => { }, _startDayOffset: 0, _timer: { pausedAt: null }, - } as RuntimeState; + } as InternalRuntimeState; const updateCurrent = getCurrent(state); - state.timer.current = updateCurrent; + state.timer.current = updateCurrent as Duration; const { absolute } = getRuntimeOffset(state); expect(millisToString(absolute)).toBe('00:16:40'); expect(absolute).toBe(82000000 - 81000000); // <-- now - planned end @@ -1200,7 +1201,7 @@ describe('getRuntimeOffset() relative', () => { currentDay: 0, }, _startDayOffset: 0, - } as RuntimeState; + } as InternalRuntimeState; const { absolute, relative } = getRuntimeOffset(state); expect(absolute).toBe(0); @@ -1227,7 +1228,7 @@ describe('getRuntimeOffset() relative', () => { currentDay: 0, }, _startDayOffset: 0, - } as RuntimeState; + } as InternalRuntimeState; const { absolute, relative } = getRuntimeOffset(state); expect(absolute).toBe(50); @@ -1254,7 +1255,7 @@ describe('getRuntimeOffset() relative', () => { currentDay: 0, }, _startDayOffset: 0, - } as RuntimeState; + } as InternalRuntimeState; const { absolute, relative } = getRuntimeOffset(state); expect(absolute).toBe(-50); @@ -1276,7 +1277,7 @@ describe('getTimerPhase()', () => { secondaryTimer: null, startedAt: null, }, - } as RuntimeState; + } as InternalRuntimeState; const phase = getTimerPhase(state); expect(phase).toBe(TimerPhase.None); @@ -1294,7 +1295,7 @@ describe('getTimerPhase()', () => { timeDanger: 100, timeWarning: 200, }, - } as RuntimeState; + } as InternalRuntimeState; const phase = getTimerPhase(state); expect(phase).toBe(TimerPhase.Overtime); @@ -1312,7 +1313,7 @@ describe('getTimerPhase()', () => { timeDanger: 100, timeWarning: 200, }, - } as RuntimeState; + } as InternalRuntimeState; const phase = getTimerPhase(state); expect(phase).toBe(TimerPhase.Danger); @@ -1330,7 +1331,7 @@ describe('getTimerPhase()', () => { timeDanger: 100, timeWarning: 200, }, - } as RuntimeState; + } as InternalRuntimeState; const phase = getTimerPhase(state); expect(phase).toBe(TimerPhase.Warning); @@ -1348,7 +1349,7 @@ describe('getTimerPhase()', () => { timeDanger: 100, timeWarning: 200, }, - } as RuntimeState; + } as InternalRuntimeState; const phase = getTimerPhase(state); expect(phase).toBe(TimerPhase.Default); @@ -1356,7 +1357,7 @@ describe('getTimerPhase()', () => { it('#1042 identifies waiting to roll', () => { const state = { - clock: 55691050, + _now: timeCore.generateInstanceFromClock(16, 28, 11), //55691050 eventNow: null, eventNext: null, rundown: { @@ -1382,13 +1383,13 @@ describe('getTimerPhase()', () => { startedAt: null, }, _timer: { - forceFinish: null, + forceFinish: false, pausedAt: null, }, _rundown: { totalDelay: 0, }, - } as RuntimeState; + } as InternalRuntimeState; const phase = getTimerPhase(state); expect(phase).toBe(TimerPhase.Pending); @@ -1396,7 +1397,7 @@ describe('getTimerPhase()', () => { it('#1042 identifies waiting to roll', () => { const state = { - clock: 55691050, + _now: timeCore.generateInstanceFromClock(16, 28, 11), //55691050 eventNow: null, eventNext: null, rundown: { @@ -1422,13 +1423,13 @@ describe('getTimerPhase()', () => { startedAt: null, }, _timer: { - forceFinish: null, + forceFinish: false, pausedAt: null, }, _rundown: { totalDelay: 0, }, - } as RuntimeState; + } as InternalRuntimeState; const phase = getTimerPhase(state); expect(phase).toBe(TimerPhase.Pending); diff --git a/apps/server/src/services/rollUtils.ts b/apps/server/src/services/rollUtils.ts index eae586b3a..eec631214 100644 --- a/apps/server/src/services/rollUtils.ts +++ b/apps/server/src/services/rollUtils.ts @@ -5,6 +5,8 @@ import { RundownMetadata } from '../api-data/rundown/rundown.types.js'; import { getTimedIndexFromPlayableIndex } from '../api-data/rundown/rundown.utils.js'; import { normaliseEndTime } from './timerUtils.js'; +// import * as timeCore from '../lib/time-core/timeCore.js'; + /** * Finds current event in a rolling rundown */ diff --git a/apps/server/src/services/runtime-service/runtime.service.ts b/apps/server/src/services/runtime-service/runtime.service.ts index ee6d0de58..ca12f1888 100644 --- a/apps/server/src/services/runtime-service/runtime.service.ts +++ b/apps/server/src/services/runtime-service/runtime.service.ts @@ -760,16 +760,17 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert // save the restore state if (hasImmediateChanges) { + const internalState = runtimeState.getInternalState(); restoreService .save({ playback: state.timer.playback, selectedEventId: state.eventNow?.id ?? null, startedAt: state.timer.startedAt, addedTime: state.timer.addedTime, - pausedAt: state._timer.pausedAt, - pausedDuration: state._timer.pausedDuration, + pausedAt: internalState._timer.pausedAt, + pausedDuration: internalState._timer.pausedDuration, firstStart: state.rundown.actualStart, - startEpoch: state._startEpoch, + startEpoch: internalState._startEpoch, currentDay: state.rundown.currentDay, }) .catch((_e) => { diff --git a/apps/server/src/services/timerUtils.ts b/apps/server/src/services/timerUtils.ts index 8c28a5e8d..d1d8e9bec 100644 --- a/apps/server/src/services/timerUtils.ts +++ b/apps/server/src/services/timerUtils.ts @@ -1,7 +1,9 @@ -import { Day, MaybeNumber, TimeOfDay, TimerPhase } from 'ontime-types'; +import { Day, Duration, Maybe, TimeOfDay, TimerPhase } from 'ontime-types'; import { MILLIS_PER_HOUR, checkIsNow, dayInMs, isPlaybackActive } from 'ontime-utils'; -import type { RuntimeState } from '../stores/runtimeState.js'; +import * as timeCore from '../lib/time-core/timeCore.js'; +import { ZERO_DURATION } from '../lib/time-core/timeCore.js'; +import type { InternalRuntimeState, RuntimeState } from '../stores/runtimeState.js'; /** * handle events that span over midnight @@ -20,10 +22,10 @@ export function hasCrossedMidnight(previous: TimeOfDay, current: TimeOfDay): boo /** * Calculates expected finish time of a running timer - * @param {RuntimeState} state runtime state + * @param {InternalRuntimeState} state runtime state * @returns {number | null} new current time or null if nothing is running */ -export function getExpectedFinish(state: RuntimeState): MaybeNumber { +export function getExpectedFinish(state: InternalRuntimeState): Maybe { const { startedAt, duration, addedTime } = state.timer; if (state.eventNow === null) { @@ -32,36 +34,35 @@ export function getExpectedFinish(state: RuntimeState): MaybeNumber { const { countToEnd, timeEnd } = state.eventNow; const { pausedAt } = state._timer; - const { clock } = state; + const { _now } = state; if (startedAt === null) { return null; } - const pausedTime = pausedAt != null ? clock - pausedAt : 0; + const pausedTime = pausedAt != null ? timeCore.toTimeOfDay(_now) - pausedAt : 0; if (countToEnd) { - return timeEnd + addedTime + pausedTime; + return (timeEnd + addedTime + pausedTime) as TimeOfDay; } // handle events that finish the day after - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- duration exists if ther eis a timer + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- duration exists if there is a timer const expectedFinish = startedAt + duration! + addedTime + pausedTime; if (expectedFinish > dayInMs) { - return expectedFinish - dayInMs; + return (expectedFinish - dayInMs) as TimeOfDay; } // an event cannot finish before it started (user added too much negative time) - return Math.max(expectedFinish, startedAt); + return Math.max(expectedFinish, startedAt) as TimeOfDay; } /** * Calculates running countdown - * @param {RuntimeState} state runtime state + * @param {InternalRuntimeState} state runtime state * @returns {number} current time for timer */ - -export function getCurrent(state: RuntimeState): number { +export function getCurrent(state: InternalRuntimeState): Duration { // eslint-disable-next-line no-unused-labels -- dev code path DEV: { if (state.eventNow === null || state.timer.duration === null) { @@ -71,12 +72,12 @@ export function getCurrent(state: RuntimeState): number { const { startedAt, duration, addedTime } = state.timer; const { countToEnd, timeStart, timeEnd } = state.eventNow; const { pausedAt } = state._timer; - const { clock } = state; + const { _now } = state; if (countToEnd) { const isEventOverMidnight = timeStart > timeEnd; const correctDay = isEventOverMidnight ? dayInMs : 0; - return correctDay - clock + timeEnd + addedTime; + return (correctDay - timeCore.toTimeOfDay(_now) + timeEnd + addedTime) as Duration; } if (startedAt === null) { @@ -84,19 +85,19 @@ export function getCurrent(state: RuntimeState): number { } if (pausedAt != null) { - return startedAt + duration + addedTime - pausedAt; + return (startedAt + duration + addedTime - pausedAt) as Duration; } - const hasPassedMidnight = startedAt > clock; + const hasPassedMidnight = startedAt > timeCore.toTimeOfDay(_now); const correctDay = hasPassedMidnight ? dayInMs : 0; - return startedAt + duration + addedTime - clock - correctDay; + return (startedAt + duration + addedTime - timeCore.toTimeOfDay(_now) - correctDay) as Duration; } /** * Calculates active time elapsed since the timer started. */ -export function getElapsed(state: RuntimeState): MaybeNumber { - const { clock } = state; +export function getElapsed(state: InternalRuntimeState): Maybe { + const { _now } = state; const { startedAt } = state.timer; const { pausedAt, pausedDuration } = state._timer; @@ -104,11 +105,11 @@ export function getElapsed(state: RuntimeState): MaybeNumber { return null; } - const referenceClock = pausedAt ?? clock; + const referenceClock = pausedAt ?? timeCore.toTimeOfDay(_now); const elapsedSinceStart = getTimeSinceStart(referenceClock, startedAt); const activeElapsed = elapsedSinceStart - pausedDuration; - return Math.max(0, activeElapsed); + return Math.max(0, activeElapsed) as Duration; } function getTimeSinceStart(clock: TimeOfDay, startedAt: number): number { @@ -127,7 +128,7 @@ function getTimeSinceStart(clock: TimeOfDay, startedAt: number): number { * @returns {boolean} */ export function skippedOutOfEvent(state: RuntimeState, previousTime: number, skipLimit: number): boolean { - // we cant have skipped if we havent started + // we cant have skipped if we haven't started if (state.timer.expectedFinish === null || state.timer.startedAt === null) { return false; } @@ -151,12 +152,12 @@ export function skippedOutOfEvent(state: RuntimeState, previousTime: number, ski * Positive offset is over time / behind schedule * Negative offset is under time / ahead of schedule */ -export function getRuntimeOffset(state: RuntimeState): { absolute: number; relative: number } { - const { eventNow, clock, _startDayOffset } = state; +export function getRuntimeOffset(state: InternalRuntimeState): { absolute: Duration; relative: Duration } { + const { eventNow, _now, _startDayOffset } = state; const { addedTime, current, startedAt } = state.timer; // nothing to calculate if there are no loaded events or if we havent started if (eventNow === null || startedAt === null || _startDayOffset === null) { - return { absolute: 0, relative: 0 }; + return { absolute: ZERO_DURATION, relative: ZERO_DURATION }; } const { countToEnd, timeStart, dayOffset } = eventNow; @@ -177,14 +178,16 @@ export function getRuntimeOffset(state: RuntimeState): { absolute: number; relat const overtime = Math.abs(Math.min(current, 0)); // time the playback was paused, the different from now to when we paused is added to the offset TODO: brakes when crossing midnight - const pausedTime = state._timer.pausedAt === null ? 0 : clock - state._timer.pausedAt; + const pausedTime = state._timer.pausedAt === null ? 0 : timeCore.toTimeOfDay(_now) - state._timer.pausedAt; // absolute offset is difference between schedule and playback time // in case of count to end, the absolute offset is overtime and added time - const absolute = countToEnd ? overtime + addedTime : eventStartOffset + overtime + pausedTime + addedTime; + const absolute = ( + countToEnd ? overtime + addedTime : eventStartOffset + overtime + pausedTime + addedTime + ) as Duration; // the relative offset is the same as the absolute but adjusted relative to the actual start time - const relative = absolute + plannedStart - actualStart - _startDayOffset * dayInMs; + const relative = (absolute + plannedStart - actualStart - _startDayOffset * dayInMs) as Duration; return { absolute, relative }; } @@ -193,7 +196,7 @@ export function getRuntimeOffset(state: RuntimeState): { absolute: number; relat * Checks running timer to see which phase it currently is in * @param state */ -export function getTimerPhase(state: RuntimeState): TimerPhase { +export function getTimerPhase(state: InternalRuntimeState): TimerPhase { if (!isPlaybackActive(state.timer.playback)) { return TimerPhase.None; } @@ -223,7 +226,7 @@ export function getTimerPhase(state: RuntimeState): TimerPhase { /** * Finds the day offset relative to an event start - * used byt the runtimeState on first start to get correct offsets + * used by the RuntimeState on first start to get correct offsets */ export function findDayOffset(plannedStart: number, clock: number): Day { const distance = clock - plannedStart; diff --git a/apps/server/src/stores/__mocks__/runtimeState.mocks.ts b/apps/server/src/stores/__mocks__/runtimeState.mocks.ts index eb9905f2b..e1257cef2 100644 --- a/apps/server/src/stores/__mocks__/runtimeState.mocks.ts +++ b/apps/server/src/stores/__mocks__/runtimeState.mocks.ts @@ -1,33 +1,12 @@ -import { OffsetMode, Playback, type TimeOfDay, TimerPhase } from 'ontime-types'; +import { Day, type Duration, type Instant, OffsetMode, Playback, TimeOfDay, TimerPhase } from 'ontime-types'; import { deepmerge } from 'ontime-utils'; -import type { RuntimeState } from '../runtimeState.js'; +import { ZERO_DURATION } from '../../lib/time-core/timeCore.js'; +import type { InternalRuntimeState, RuntimeState } from '../runtimeState.js'; -const baseState: RuntimeState = { - clock: 0 as TimeOfDay, - eventNow: null, - eventNext: null, - eventFlag: null, - groupNow: null, - rundown: { - selectedEventIndex: null, - numEvents: 0, - plannedStart: 0, - plannedEnd: 0, - actualStart: null, - actualGroupStart: null, - currentDay: 0, - }, - offset: { - absolute: 0, - relative: 0, - mode: OffsetMode.Absolute, - expectedRundownEnd: null, - expectedGroupEnd: null, - expectedFlagStart: null, - }, +export const runtimePlaceholder = { timer: { - addedTime: 0, + addedTime: ZERO_DURATION, current: null, duration: null, elapsed: null, @@ -37,10 +16,43 @@ const baseState: RuntimeState = { secondaryTimer: null, startedAt: null, }, + offset: { + absolute: ZERO_DURATION, + relative: ZERO_DURATION, + mode: OffsetMode.Absolute, + expectedGroupEnd: null, + expectedRundownEnd: null, + expectedFlagStart: null, + }, + rundown: { + selectedEventIndex: null, + numEvents: 0, + plannedStart: 0 as TimeOfDay, + plannedEnd: 0 as TimeOfDay, + actualStart: null, + actualGroupStart: null, + + currentDay: 0 as Day, + }, +} as Readonly; + +const baseState: InternalRuntimeState = { + _now: 0 as Instant, + eventNow: null, + eventNext: null, + eventFlag: null, + groupNow: null, + rundown: { + ...runtimePlaceholder.rundown, + }, + offset: { + ...runtimePlaceholder.offset, + }, + timer: { ...runtimePlaceholder.timer }, _timer: { - forceFinish: null, + forceFinish: false, pausedAt: null, - pausedDuration: 0, + pausedDuration: 0 as Duration, secondaryTarget: null, hasFinished: false, }, diff --git a/apps/server/src/stores/__tests__/runtimeState.test.ts b/apps/server/src/stores/__tests__/runtimeState.test.ts index 3031f32db..dad032702 100644 --- a/apps/server/src/stores/__tests__/runtimeState.test.ts +++ b/apps/server/src/stores/__tests__/runtimeState.test.ts @@ -6,9 +6,10 @@ import { rundownCache } from '../../api-data/rundown/rundown.dao.js'; import { initRundown } from '../../api-data/rundown/rundown.service.js'; import { RundownMetadata } from '../../api-data/rundown/rundown.types.js'; import { - type RuntimeState, + type InternalRuntimeState, addTime, clearState, + getInternalState, getState, load, loadGroupFlagAndEnd, @@ -20,6 +21,8 @@ import { update, } from '../runtimeState.js'; +// import * as timeCore from '../../lib/time-core/timeCore.js' + const mockEvent = { type: 'event', id: 'mock', @@ -32,7 +35,7 @@ const mockEvent = { } as PlayableEvent; const mockState = { - clock: 666, + _now: 666, eventNow: null, eventNext: null, rundown: { @@ -53,7 +56,7 @@ const mockState = { pausedAt: null, hasFinished: false, }, -} as RuntimeState; +} as InternalRuntimeState; vi.mock('../../classes/data-provider/DataProvider.js', () => { return { @@ -108,34 +111,34 @@ describe('mutation on runtimeState', () => { const { metadata, rundown } = rundownCache.get(); load(mockEvent, rundown, metadata); - let newState = getState(); + let newState = getInternalState(); expect(newState.eventNow?.id).toBe(mockEvent.id); expect(newState.eventNext?.id).toBe('event2'); expect(newState.timer.playback).toBe(Playback.Armed); - expect(newState.clock).not.toBe(666); + expect(getState().clock).not.toBe(666); expect(newState.groupNow).toBeNull(); // 2. Start event vi.setSystemTime('jan 1 00:02'); let success = start(); - newState = getState(); + newState = getInternalState(); expect(success).toBe(true); expect(newState.timer).toMatchObject({ playback: Playback.Play, }); - expect(newState.rundown.actualStart).toBe(newState.clock); + expect(newState.rundown.actualStart).toBe(getState().clock); // 3. Pause event vi.setSystemTime('jan 1 00:03'); success = pause(); - newState = getState(); + newState = getInternalState(); expect(success).toBe(true); - expect(newState.clock).not.toBe(666); + expect(getState().clock).not.toBe(666); expect(newState.timer).toMatchObject({ playback: Playback.Pause, addedTime: 0, }); - expect(newState._timer.pausedAt).toEqual(newState.clock); + expect(newState._timer.pausedAt).toEqual(getState().clock); success = pause(); expect(success).toBe(false); @@ -143,7 +146,7 @@ describe('mutation on runtimeState', () => { // 4. Restart event vi.setSystemTime('jan 1 00:04'); success = start(); - newState = getState(); + newState = getInternalState(); expect(success).toBe(true); expect(newState.timer).toMatchObject({ playback: Playback.Play, @@ -163,7 +166,7 @@ describe('mutation on runtimeState', () => { // 5. Stop event vi.setSystemTime('jan 1 00:05'); success = stop(); - newState = getState(); + newState = getInternalState(); expect(success).toBe(true); expect(newState.eventNow).toBe(null); expect(newState.timer).toMatchObject({ @@ -825,7 +828,7 @@ describe('roll mode', () => { test('pending event', () => { const { rundown, metadata } = rundownCache.get(); const { eventId, didStart } = roll(rundown, metadata); - const state = getState(); + const state = getInternalState(); expect(eventId).toBe('1'); expect(didStart).toBe(false); @@ -833,7 +836,7 @@ describe('roll mode', () => { expect(state.timer.secondaryTimer).toBe(1000); }); - test('roll events', () => { + test.only('roll events', () => { vi.setSystemTime('jan 1 00:00:01'); const { rundown, metadata } = rundownCache.get(); let result = roll(rundown, metadata); @@ -944,7 +947,7 @@ describe('loadGroupFlagAndEnd()', () => { groupNow: null, eventNow: rundown.entries[11], rundown: { actualGroupStart: null }, - } as RuntimeState; + } as InternalRuntimeState; const metadata = { playableEventOrder: ['0', '11', '3'], flags: ['1'] } as RundownMetadata; @@ -972,7 +975,7 @@ describe('loadGroupFlagAndEnd()', () => { groupNow: rundown.entries[1], eventNow: rundown.entries[22], rundown: { actualGroupStart: null }, - } as RuntimeState; + } as InternalRuntimeState; const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata; @@ -1000,7 +1003,7 @@ describe('loadGroupFlagAndEnd()', () => { groupNow: rundown.entries[1], eventNow: rundown.entries[0], rundown: { actualGroupStart: null }, - } as RuntimeState; + } as InternalRuntimeState; const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata; @@ -1025,7 +1028,7 @@ describe('loadGroupFlagAndEnd()', () => { groupNow: null, eventNow: rundown.entries[0], rundown: { actualGroupStart: null }, - } as RuntimeState; + } as InternalRuntimeState; const metadata = { playableEventOrder: ['0', '1'], flags: ['1'] } as RundownMetadata; @@ -1087,7 +1090,7 @@ describe('loadGroupFlagAndEnd()', () => { groupNow: null, eventNow: rundown.entries[0], rundown: { actualGroupStart: null }, - } as RuntimeState; + } as InternalRuntimeState; const metadata = { playableEventOrder: ['0', '1', '2', '3'], flags: ['3'] } as RundownMetadata; @@ -1169,7 +1172,7 @@ describe('loadGroupFlagAndEnd()', () => { groupNow: null, eventNow: rundown.entries[0], rundown: { actualGroupStart: null }, - } as RuntimeState; + } as InternalRuntimeState; const metadata = { playableEventOrder: ['0', '1', '2', '3'], flags: ['3'] } as RundownMetadata; diff --git a/apps/server/src/stores/runtimeState.ts b/apps/server/src/stores/runtimeState.ts index f306b154e..4797bf118 100644 --- a/apps/server/src/stores/runtimeState.ts +++ b/apps/server/src/stores/runtimeState.ts @@ -17,7 +17,6 @@ import { TimerPhase, TimerState, isOntimeEvent, - runtimeStorePlaceholder, } from 'ontime-types'; import { calculateDuration, @@ -44,14 +43,18 @@ import { hasCrossedMidnight, } from '../services/timerUtils.js'; import { timerConfig } from '../setup/config.js'; +import { runtimePlaceholder } from './__mocks__/runtimeState.mocks.js'; type ExpectedMetadata = { event: OntimeEvent; - accumulatedGap: number; + accumulatedGap: Duration; isLinkedToLoaded: boolean; } | null; export type RuntimeState = { + /** + * @deprecated we want to phase out the 24h clock + */ clock: TimeOfDay; groupNow: OntimeGroup | null; eventNow: PlayableEvent | null; @@ -61,36 +64,72 @@ export type RuntimeState = { timer: TimerState; rundown: RundownState; // private properties of the timer calculations +}; + +export type InternalRuntimeState = Omit & { + timer: { + addedTime: Duration; + current: Maybe; + duration: Maybe; + elapsed: Maybe; + expectedFinish: Maybe; + phase: TimerPhase; + playback: Playback; + secondaryTimer: Maybe; + startedAt: Maybe; + }; + offset: { + absolute: Duration; + relative: Duration; + mode: OffsetMode; + expectedGroupEnd: Maybe; + expectedRundownEnd: Maybe; + expectedFlagStart: Maybe; + }; + rundown: { + selectedEventIndex: Maybe; + numEvents: number; + plannedStart: Maybe; + plannedEnd: Maybe; + actualStart: Maybe; + currentDay: Maybe; + actualGroupStart: Maybe; + }; + _now: Instant; _timer: { - forceFinish: Maybe; // whether we should declare an event as finished, will contain the finish time + forceFinish: boolean; // whether we should declare an event as finished, will contain the finish time pausedAt: Maybe; - pausedDuration: number; + pausedDuration: Duration; secondaryTarget: Maybe; hasFinished: boolean; }; _rundown: { totalDelay: number; // this value comes from rundown service }; + _startDayOffset: Maybe; + _startEpoch: Maybe; _group: ExpectedMetadata; _flag: ExpectedMetadata; _end: ExpectedMetadata; - _startEpoch: Maybe; - _startDayOffset: Maybe; }; -const runtimeState: RuntimeState = { - clock: timeCore.timeOfDayNow(), +const runtimeState: InternalRuntimeState = { + _now: timeCore.now(), groupNow: null, eventNow: null, eventNext: null, eventFlag: null, - offset: { ...runtimeStorePlaceholder.offset }, - timer: { ...runtimeStorePlaceholder.timer }, - rundown: { ...runtimeStorePlaceholder.rundown }, + offset: { ...runtimePlaceholder.offset }, + timer: { + ...runtimePlaceholder.timer, + }, + rundown: { + ...runtimePlaceholder.rundown, + }, _timer: { - forceFinish: null, + forceFinish: false, pausedAt: null, - pausedDuration: 0, + pausedDuration: 0 as Duration, secondaryTarget: null, hasFinished: false, }, @@ -104,10 +143,15 @@ const runtimeState: RuntimeState = { _startDayOffset: null, }; +export function getInternalState(): Readonly { + return runtimeState; +} + export function getState(): Readonly { // create a shallow copy of the state return { ...runtimeState, + clock: timeCore.toTimeOfDay(runtimeState._now), eventNow: runtimeState.eventNow ? { ...runtimeState.eventNow } : null, eventNext: runtimeState.eventNext ? { ...runtimeState.eventNext } : null, eventFlag: runtimeState.eventFlag ? { ...runtimeState.eventFlag } : null, @@ -115,8 +159,6 @@ export function getState(): Readonly { offset: { ...runtimeState.offset }, rundown: { ...runtimeState.rundown }, timer: { ...runtimeState.timer }, - _timer: { ...runtimeState._timer }, - _rundown: { ...runtimeState._rundown }, }; } @@ -127,22 +169,18 @@ export function clearEventData() { runtimeState.eventNow = null; runtimeState.eventNext = null; - runtimeState.offset.absolute = 0; - runtimeState.offset.relative = 0; - runtimeState.offset.expectedFlagStart = null; - runtimeState.offset.expectedGroupEnd = null; - runtimeState.offset.expectedRundownEnd = null; + runtimeState.offset = { ...runtimePlaceholder.offset }; runtimeState.rundown.selectedEventIndex = null; runtimeState.timer.playback = Playback.Stop; - runtimeState.clock = timeCore.timeOfDayNow(); - runtimeState.timer = { ...runtimeStorePlaceholder.timer }; + runtimeState._now = timeCore.now(); + runtimeState.timer = { ...runtimePlaceholder.timer }; // when clearing, we maintain the total delay from the rundown - runtimeState._timer.forceFinish = null; + runtimeState._timer.forceFinish = false; runtimeState._timer.pausedAt = null; - runtimeState._timer.pausedDuration = 0; + runtimeState._timer.pausedDuration = 0 as Duration; runtimeState._timer.secondaryTarget = null; runtimeState._timer.hasFinished = false; } @@ -160,22 +198,18 @@ export function clearState() { runtimeState.rundown.actualStart = null; runtimeState.rundown.selectedEventIndex = null; - runtimeState.offset.absolute = 0; - runtimeState.offset.relative = 0; - runtimeState.offset.expectedRundownEnd = null; - runtimeState.offset.expectedGroupEnd = null; - runtimeState.offset.expectedFlagStart = null; + runtimeState.offset = { ...runtimePlaceholder.offset }; runtimeState._end = null; runtimeState.timer.playback = Playback.Stop; - runtimeState.clock = timeCore.timeOfDayNow(); - runtimeState.timer = { ...runtimeStorePlaceholder.timer }; + runtimeState._now = timeCore.now(); + runtimeState.timer = { ...runtimePlaceholder.timer }; // when clearing, we maintain the total delay from the rundown - runtimeState._timer.forceFinish = null; + runtimeState._timer.forceFinish = false; runtimeState._timer.pausedAt = null; - runtimeState._timer.pausedDuration = 0; + runtimeState._timer.pausedDuration = 0 as Duration; runtimeState._timer.secondaryTarget = null; runtimeState._timer.hasFinished = false; @@ -217,9 +251,9 @@ export function updateRundownData(rundownData: { runtimeState._rundown.totalDelay = rundownData.totalDelay; runtimeState.rundown.numEvents = rundownData.numEvents; - runtimeState.rundown.plannedStart = rundownData.firstStart; + runtimeState.rundown.plannedStart = rundownData.firstStart as TimeOfDay; runtimeState.rundown.plannedEnd = - rundownData.firstStart === null ? null : rundownData.firstStart + rundownData.totalDuration; + rundownData.firstStart === null ? null : ((rundownData.firstStart + rundownData.totalDuration) as TimeOfDay); if (isPlaybackActive(runtimeState.timer.playback)) getExpectedTimes(); } @@ -253,7 +287,7 @@ export function load( // update state runtimeState.timer.playback = Playback.Armed; - runtimeState.timer.duration = calculateDuration(event.timeStart, event.timeEnd); + runtimeState.timer.duration = event.duration as Duration; runtimeState.timer.current = getCurrent(runtimeState); runtimeState.rundown.numEvents = metadata.timedEventOrder.length; @@ -267,13 +301,13 @@ export function load( (firstStart === null || typeof firstStart === 'number') && (startEpoch === null || typeof startEpoch === 'number') ) { - runtimeState.rundown.actualStart = firstStart; + runtimeState.rundown.actualStart = firstStart as TimeOfDay; runtimeState._startEpoch = startEpoch; if (firstStart !== null && runtimeState.rundown.plannedStart !== null) { runtimeState._startDayOffset = findDayOffset(runtimeState.rundown.plannedStart, firstStart); } if (currentDay !== undefined) { - runtimeState.rundown.currentDay = currentDay; + runtimeState.rundown.currentDay = currentDay as Day; } const { absolute, relative } = getRuntimeOffset(runtimeState); runtimeState.offset.absolute = absolute; @@ -365,16 +399,16 @@ export function updateLoaded(event?: PlayableEvent): string | undefined { // handle edge cases with roll if (runtimeState.timer.playback === Playback.Roll) { - const offsetClock = runtimeState.clock - runtimeState.offset.absolute; + const offsetClock = timeCore.toTimeOfDay(runtimeState._now) - runtimeState.offset.absolute; // if waiting to roll, we update the targets and potentially start the timer if (runtimeState._timer.secondaryTarget !== null) { if (runtimeState.eventNow.timeStart < offsetClock && offsetClock < runtimeState.eventNow.timeEnd) { // if the event is now, we queue a start runtimeState._timer.secondaryTarget = runtimeState.eventNow.timeStart as TimeOfDay; - runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock; + runtimeState.timer.secondaryTimer = (runtimeState._timer.secondaryTarget - offsetClock) as Duration; } else { runtimeState._timer.secondaryTarget = normaliseRollStart( - runtimeState.eventNow.timeStart, + runtimeState.eventNow.timeStart as TimeOfDay, offsetClock, ) as TimeOfDay; } @@ -391,7 +425,7 @@ export function updateLoaded(event?: PlayableEvent): string | undefined { runtimeState.timer.startedAt = null; runtimeState._timer.hasFinished = false; - runtimeState.timer.addedTime = 0; + runtimeState.timer.addedTime = 0 as Duration; runtimeState._timer.pausedAt = null; // this could be looked after by the timer @@ -414,7 +448,7 @@ export function updateAll(rundown: Rundown, metadata: RundownMetadata) { loadGroupFlagAndEnd(rundown, metadata, eventNowIndex); } -export function start(state: RuntimeState = runtimeState): boolean { +export function start(state: InternalRuntimeState = runtimeState): boolean { if (state.eventNow === null) { return false; } @@ -422,22 +456,19 @@ export function start(state: RuntimeState = runtimeState): boolean { return false; } - const epoch = timeCore.now(); - const now = timeCore.toTimeOfDay(epoch); - - state.clock = now; + state._now = timeCore.now(); state.timer.secondaryTimer = null; // add paused time if it exists if (state._timer.pausedAt) { - const timeToAdd = state.clock - state._timer.pausedAt; - state.timer.addedTime += timeToAdd; - state._timer.pausedDuration += timeToAdd; + const timeToAdd = (timeCore.toTimeOfDay(state._now) - state._timer.pausedAt) as Duration; + state.timer.addedTime = timeCore.combineValues(state.timer.addedTime, timeToAdd); + state._timer.pausedDuration = timeCore.combineValues(state._timer.pausedDuration, timeToAdd); state._timer.pausedAt = null; } if (state.timer.startedAt === null) { - state.timer.startedAt = state.clock; + state.timer.startedAt = timeCore.toTimeOfDay(state._now); } state.timer.playback = Playback.Play; @@ -445,14 +476,15 @@ export function start(state: RuntimeState = runtimeState): boolean { state.timer.elapsed = getElapsed(state); if (state.rundown.actualStart === null) { - state._startDayOffset = (findDayOffset(state.eventNow.timeStart, state.clock) + state.eventNow.dayOffset) as Day; + state._startDayOffset = (findDayOffset(state.eventNow.timeStart, timeCore.toTimeOfDay(state._now)) + + state.eventNow.dayOffset) as Day; state.rundown.currentDay = state._startDayOffset; - state._startEpoch = epoch; - state.rundown.actualStart = state.clock; + state._startEpoch = state._now; + state.rundown.actualStart = timeCore.toTimeOfDay(state._now); } if (state.groupNow !== null && state.rundown.actualGroupStart === null) { - state.rundown.actualGroupStart = state.clock; + state.rundown.actualGroupStart = timeCore.toTimeOfDay(state._now); } // update timer phase @@ -475,18 +507,18 @@ export function start(state: RuntimeState = runtimeState): boolean { return true; } -export function pause(state: RuntimeState = runtimeState): boolean { +export function pause(state: InternalRuntimeState = runtimeState): boolean { if (state.timer.playback !== Playback.Play) { return false; } state.timer.playback = Playback.Pause; - state.clock = timeCore.timeOfDayNow(); - state._timer.pausedAt = state.clock; + state._now = timeCore.now(); + state._timer.pausedAt = timeCore.toTimeOfDay(state._now); return true; } -export function stop(state: RuntimeState = runtimeState): boolean { +export function stop(state: InternalRuntimeState = runtimeState): boolean { if (state.timer.playback === Playback.Stop) { return false; } @@ -516,7 +548,7 @@ export function addTime(amount: number) { if (willGoNegative && !runtimeState._timer.hasFinished) { // set finished time so side effects are triggered - runtimeState._timer.forceFinish = timeCore.timeOfDayNow(); + runtimeState._timer.forceFinish = true; } else { const willGoPositive = runtimeState.timer.current < 0 && runtimeState.timer.current + amount > 0; if (willGoPositive) { @@ -525,8 +557,8 @@ export function addTime(amount: number) { } // we can update the state after handling the side effects - runtimeState.timer.addedTime += amount; - runtimeState.timer.current += amount; + runtimeState.timer.addedTime = timeCore.combineValues(runtimeState.timer.addedTime, amount as Duration); + runtimeState.timer.current = timeCore.combineValues(runtimeState.timer.current, amount as Duration); runtimeState.timer.elapsed = getElapsed(runtimeState); // update runtime delays: over - under @@ -546,10 +578,8 @@ export type UpdateResult = { export function update(): UpdateResult { // 0. there are some things we always do - const previousClock = runtimeState.clock; - const epoch = timeCore.now(); - const now = timeCore.toTimeOfDay(epoch); - runtimeState.clock = now; // we update the clock on every update call + const previousClock = timeCore.toTimeOfDay(runtimeState._now); + runtimeState._now = timeCore.now(); // we update the clock on every update call // 1. is playback idle? if (!isPlaybackActive(runtimeState.timer.playback)) { @@ -558,13 +588,13 @@ export function update(): UpdateResult { // calculate currentDay from epoch (days elapsed since playback was started) if (runtimeState._startEpoch !== null && runtimeState._startDayOffset !== null) { - const daysSinceStart = timeCore.daysSinceStart(runtimeState._startEpoch, epoch); - runtimeState.rundown.currentDay = runtimeState._startDayOffset + daysSinceStart; + const daysSinceStart = timeCore.daysSinceStart(runtimeState._startEpoch, runtimeState._now); + runtimeState.rundown.currentDay = timeCore.combineValues(runtimeState._startDayOffset, daysSinceStart); } // 2. are we waiting to roll? if (runtimeState.timer.playback === Playback.Roll && runtimeState.timer.secondaryTimer !== null) { - const clockHasCrossedMidnight = hasCrossedMidnight(previousClock, now); + const clockHasCrossedMidnight = hasCrossedMidnight(previousClock, timeCore.toTimeOfDay(runtimeState._now)); return updateIfWaitingToRoll(clockHasCrossedMidnight); } @@ -618,22 +648,22 @@ export function update(): UpdateResult { } // account for offset - const offsetClock = runtimeState.clock + runtimeState.offset.absolute; + const offsetClock = timeCore.addDuration(runtimeState._now, runtimeState.offset.absolute as Duration); runtimeState.timer.phase = TimerPhase.Pending; if (hasCrossedMidnight) { // if we crossed midnight, we need to update the target // this is the same logic from the roll function runtimeState._timer.secondaryTarget = normaliseRollStart( - runtimeState.eventNow.timeStart, + runtimeState.eventNow.timeStart as TimeOfDay, offsetClock, ) as TimeOfDay; } - runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget! - offsetClock; + runtimeState.timer.secondaryTimer = (runtimeState._timer.secondaryTarget! - offsetClock) as Duration; return { hasTimerFinished: false, - hasSecondaryTimerFinished: runtimeState.timer.secondaryTimer <= 0, + hasSecondaryTimerFinished: runtimeState.timer.secondaryTimer! <= 0, }; } } @@ -650,9 +680,7 @@ export function roll( } // we will need to do some calculations, update the time first - const epoch = timeCore.now(); - const now = timeCore.toTimeOfDay(epoch); - runtimeState.clock = now; + runtimeState._now = timeCore.now(); // 2. if there is an event armed, we use it if (runtimeState.timer.playback === Playback.Armed || runtimeState.timer.phase === TimerPhase.Pending) { @@ -664,7 +692,9 @@ export function roll( } if (offset) { - runtimeState.offset = { ...offset }; + runtimeState.offset = { + ...offset, + } as InternalRuntimeState['offset']; } runtimeState.timer.playback = Playback.Roll; @@ -673,15 +703,16 @@ export function roll( runtimeState.eventNow.timeEnd < runtimeState.eventNow.timeStart ? runtimeState.eventNow.timeEnd + dayInMs : runtimeState.eventNow.timeEnd; - runtimeState.timer.expectedFinish = normalisedEndTime; + runtimeState.timer.expectedFinish = normalisedEndTime as TimeOfDay; // account for offset - const offsetClock = runtimeState.clock - runtimeState.offset.absolute; + // const offsetClock = timeCore.addDuration(runtimeState._now, -runtimeState.offset.absolute as Duration); + const offsetClock = timeCore.toTimeOfDay(runtimeState._now) - runtimeState.offset.absolute; // state catch up runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, normalisedEndTime); runtimeState.timer.current = runtimeState.timer.duration; - runtimeState.timer.elapsed = 0; + runtimeState.timer.elapsed = 0 as Duration; // check if the event is ready to start or if needs to be pending const isNow = checkIsNow(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd, offsetClock); @@ -690,8 +721,8 @@ export function roll( * If we are starting an event in roll mode * we backtrace all the start times to the supposed start time of the event */ - const plannedStart = runtimeState.eventNow.timeStart; - runtimeState.timer.startedAt = plannedStart; + const plannedStart = runtimeState.eventNow.timeStart as TimeOfDay; + runtimeState.timer.startedAt = plannedStart as TimeOfDay; // reset the secondary timer to cancel any countdowns runtimeState.timer.secondaryTimer = null; @@ -709,18 +740,20 @@ export function roll( runtimeState._startDayOffset = (findDayOffset(runtimeState.eventNow.timeStart, plannedStart) + runtimeState.eventNow.dayOffset) as Day; // backdate _startEpoch to when the event conceptually started - const timeElapsed = timeCore.elapsedTime(runtimeState.clock, plannedStart as TimeOfDay); - runtimeState._startEpoch = timeCore.addDuration(epoch, -timeElapsed as Duration); + const timeElapsed = timeCore.elapsedTime(timeCore.toTimeOfDay(runtimeState._now), plannedStart as TimeOfDay); + runtimeState._startEpoch = timeCore.addDuration(runtimeState._now, -timeElapsed as Duration); // calculate currentDay from the backdated epoch - runtimeState.rundown.currentDay = - runtimeState._startDayOffset + timeCore.daysSinceStart(runtimeState._startEpoch, epoch); + runtimeState.rundown.currentDay = timeCore.combineValues( + runtimeState._startDayOffset, + timeCore.daysSinceStart(runtimeState._startEpoch, runtimeState._now), + ); } } else { runtimeState._timer.secondaryTarget = normaliseRollStart( - runtimeState.eventNow.timeStart, + runtimeState.eventNow.timeStart as TimeOfDay, offsetClock, ) as TimeOfDay; - runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock; + runtimeState.timer.secondaryTimer = (runtimeState._timer.secondaryTarget - offsetClock) as Duration; runtimeState.timer.phase = TimerPhase.Pending; } @@ -737,9 +770,9 @@ export function roll( // account for offset but we only keep it if passed to us if (offset) { - runtimeState.offset = { ...offset }; + runtimeState.offset = { ...offset } as InternalRuntimeState['offset']; } - const offsetClock = runtimeState.clock - runtimeState.offset.absolute; + const offsetClock = timeCore.toTimeOfDay(runtimeState._now) - runtimeState.offset.absolute; const { index, isPending } = loadRoll(rundown, metadata, offsetClock); @@ -766,7 +799,7 @@ export function roll( runtimeState.timer.phase = TimerPhase.Pending; // we need to normalise start time in case it is the day after runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, offsetClock) as TimeOfDay; - runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock; + runtimeState.timer.secondaryTimer = (runtimeState._timer.secondaryTarget - offsetClock) as Duration; // preload timer properties runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd); @@ -789,18 +822,18 @@ export function roll( * when we start in roll mode * we need to backtrace all times to the supposed start time of the event */ - const plannedStart = runtimeState.eventNow.timeStart; + const plannedStart = runtimeState.eventNow.timeStart as TimeOfDay; const endTime = runtimeState.eventNow.timeEnd < runtimeState.eventNow.timeStart ? runtimeState.eventNow.timeEnd + dayInMs : runtimeState.eventNow.timeEnd; - runtimeState.timer.startedAt = plannedStart; - runtimeState.timer.expectedFinish = endTime; + runtimeState.timer.startedAt = plannedStart as TimeOfDay; + runtimeState.timer.expectedFinish = endTime as TimeOfDay; // state catch up runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, endTime); runtimeState.timer.current = getCurrent(runtimeState); - runtimeState.timer.elapsed = 0; + runtimeState.timer.elapsed = 0 as Duration; // update runtime runtimeState.rundown.actualStart = plannedStart; @@ -813,11 +846,11 @@ export function roll( runtimeState._startDayOffset = (findDayOffset(runtimeState.eventNow.timeStart, plannedStart) + runtimeState.eventNow.dayOffset) as Day; // backdate _startEpoch to when the event conceptually started - const timeElapsed = timeCore.elapsedTime(runtimeState.clock, plannedStart as TimeOfDay); - runtimeState._startEpoch = timeCore.addDuration(epoch, -timeElapsed as Duration); + const timeElapsed = timeCore.elapsedTime(timeCore.toTimeOfDay(runtimeState._now), plannedStart as TimeOfDay); + runtimeState._startEpoch = timeCore.addDuration(runtimeState._now, -timeElapsed as Duration); // calculate currentDay from the backdated epoch runtimeState.rundown.currentDay = (runtimeState._startDayOffset + - timeCore.daysSinceStart(runtimeState._startEpoch, epoch)) as Day; + timeCore.daysSinceStart(runtimeState._startEpoch, runtimeState._now)) as Day; return { eventId: runtimeState.eventNow.id, didStart: true }; } @@ -852,7 +885,11 @@ function getExpectedTimes(state = runtimeState) { plannedStart, actualStart, }); - state.offset.expectedGroupEnd = getExpectedEnd(lastEvent, lastEventExpectedStart, state.rundown.currentDay!); + state.offset.expectedGroupEnd = getExpectedEnd( + lastEvent, + lastEventExpectedStart, + state.rundown.currentDay!, + ) as TimeOfDay; } } @@ -869,7 +906,7 @@ function getExpectedTimes(state = runtimeState) { plannedStart, actualStart, }); - state.offset.expectedFlagStart = expectedStart; + state.offset.expectedFlagStart = expectedStart as TimeOfDay; } } @@ -884,7 +921,7 @@ function getExpectedTimes(state = runtimeState) { plannedStart, actualStart, }); - state.offset.expectedRundownEnd = getExpectedEnd(event, expectedStart, state.rundown.currentDay!); + state.offset.expectedRundownEnd = getExpectedEnd(event, expectedStart, state.rundown.currentDay!) as TimeOfDay; } } @@ -957,13 +994,13 @@ export function loadGroupFlagAndEnd( if (!foundFlag && metadata.flags.includes(entry.id)) { foundFlag = true; state.eventFlag = entry as PlayableEvent; // we know it is playable as it is coming from the playableEventOrder list - state._flag = { event: entry, isLinkedToLoaded, accumulatedGap }; + state._flag = { event: entry, isLinkedToLoaded, accumulatedGap: accumulatedGap as Duration }; } } if (!foundGroupEnd && entry.id === lastEventInGroup?.id) { foundGroupEnd = true; - state._group = { event: lastEventInGroup, isLinkedToLoaded, accumulatedGap }; + state._group = { event: lastEventInGroup, isLinkedToLoaded, accumulatedGap: accumulatedGap as Duration }; } } } @@ -971,7 +1008,7 @@ export function loadGroupFlagAndEnd( const lastID = playableEventOrder.at(-1); const lastEvent = lastID ? (entries[lastID] as OntimeEvent) : null; if (lastEvent) { - state._end = { event: lastEvent, isLinkedToLoaded, accumulatedGap }; + state._end = { event: lastEvent, isLinkedToLoaded, accumulatedGap: accumulatedGap as Duration }; } } diff --git a/packages/utils/src/validate-times/validateTimes.ts b/packages/utils/src/validate-times/validateTimes.ts index 6c487ca96..9ef01af8b 100644 --- a/packages/utils/src/validate-times/validateTimes.ts +++ b/packages/utils/src/validate-times/validateTimes.ts @@ -1,4 +1,4 @@ -import type { OntimeEvent } from 'ontime-types'; +import type { Duration, OntimeEvent } from 'ontime-types'; import { TimeStrategy } from 'ontime-types'; import { dayInMs } from '../date-utils/conversionUtils.js'; @@ -103,16 +103,17 @@ export function validateTimes( /** * @description calculates event duration considering midnight + * @deprecated - duration is the more useful value from the event data, prefer using that directly * @param {number} timeStart * @param {number} timeEnd * @returns {number} */ -export function calculateDuration(timeStart: number, timeEnd: number): number { +export function calculateDuration(timeStart: number, timeEnd: number): Duration { // Durations must be positive if (timeEnd < timeStart) { - return timeEnd + dayInMs - timeStart; + return (timeEnd + dayInMs - timeStart) as Duration; } - return timeEnd - timeStart; + return (timeEnd - timeStart) as Duration; } export function calculateEnd(timeStart: number, duration: number): number {