From 048a9c96ea2950e112450efc0d5cc2c3e4853747 Mon Sep 17 00:00:00 2001 From: Alex Christoffer Rasmussen Date: Thu, 14 Aug 2025 19:29:25 +0200 Subject: [PATCH] Refactor: split runtime state (#1725) * refactor: runtime types * refactor: runtimeState functions to use the split type * refactor: RuntimeService to use the new split data * refactor: use the split data in the UI * update comments * refactor: rename runtime to offset * fix utils test --- apps/client/src/common/hooks/useSocket.ts | 47 ++-- apps/client/src/common/utils/time.ts | 4 +- .../template-input/templateInput.utils.ts | 19 +- .../overview/composite/TimeElements.tsx | 4 +- apps/client/src/views/backstage/Backstage.tsx | 6 +- apps/client/src/views/studio/StudioTimers.tsx | 10 +- .../src/views/studio/studioTimers.utils.ts | 15 +- apps/server/src/app.ts | 6 +- apps/server/src/services/RestoreService.ts | 5 - .../services/__tests__/RestoreService.test.ts | 5 - .../src/services/__tests__/timerUtils.test.ts | 126 +++++----- .../runtime-service/RuntimeService.ts | 121 +++------ .../runtime-service/rundownService.utils.ts | 52 +--- apps/server/src/services/timerUtils.ts | 16 +- .../stores/__mocks__/runtimeState.mocks.ts | 17 +- .../src/stores/__tests__/runtimeState.test.ts | 108 ++++---- apps/server/src/stores/runtimeState.ts | 231 ++++++++---------- .../runtime/CurrentGroupState.type.ts | 13 - .../src/definitions/runtime/Offset.type.ts | 15 ++ .../definitions/runtime/RundownState.type.ts | 9 + .../src/definitions/runtime/Runtime.type.ts | 18 -- .../src/definitions/runtime/RuntimeStore.ts | 19 +- .../definitions/runtime/RuntimeStore.type.ts | 20 +- packages/types/src/index.ts | 6 +- .../src/date-utils/getExpectedStart.test.ts | 20 +- .../utils/src/date-utils/getExpectedStart.ts | 6 +- 26 files changed, 400 insertions(+), 518 deletions(-) delete mode 100644 packages/types/src/definitions/runtime/CurrentGroupState.type.ts create mode 100644 packages/types/src/definitions/runtime/Offset.type.ts create mode 100644 packages/types/src/definitions/runtime/RundownState.type.ts delete mode 100644 packages/types/src/definitions/runtime/Runtime.type.ts diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index f1a7da0de..44738657b 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -58,8 +58,8 @@ export const setMessage = { export const usePlaybackControl = createSelector((state: RuntimeStore) => ({ playback: state.timer.playback, - selectedEventIndex: state.runtime.selectedEventIndex, - numEvents: state.runtime.numEvents, + selectedEventIndex: state.rundown.selectedEventIndex, + numEvents: state.rundown.numEvents, timerPhase: state.timer.phase, })); @@ -150,8 +150,8 @@ export const useClock = createSelector((state: RuntimeStore) => ({ })); export const useNextFlag = createSelector((state: RuntimeStore) => ({ - id: state.nextFlag?.id ?? null, - expectedStart: state.nextFlag?.expectedStart ?? null, + id: state.eventFlag?.id ?? null, + expectedStart: state.offset.expectedFlagStart, })); /** Used by the progress bar components */ @@ -162,35 +162,35 @@ export const useProgressData = createSelector((state: RuntimeStore) => ({ timeDanger: state.eventNow?.timeDanger ?? null, })); -export const useRuntimeOverview = createSelector((state: RuntimeStore) => ({ - plannedStart: state.runtime.plannedStart, - actualStart: state.runtime.actualStart, - plannedEnd: state.runtime.plannedEnd, - expectedEnd: state.runtime.expectedEnd, +export const useRundownOverview = createSelector((state: RuntimeStore) => ({ + plannedStart: state.rundown.plannedStart, + actualStart: state.rundown.actualStart, + plannedEnd: state.rundown.plannedEnd, + expectedEnd: state.offset.expectedRundownEnd, })); export const useRuntimePlaybackOverview = createSelector((state: RuntimeStore) => ({ playback: state.timer.playback, clock: state.clock, - numEvents: state.runtime.numEvents, - selectedEventIndex: state.runtime.selectedEventIndex, - offset: state.runtime.offsetMode === OffsetMode.Absolute ? state.runtime.offsetAbs : state.runtime.offsetRel, + numEvents: state.rundown.numEvents, + selectedEventIndex: state.rundown.selectedEventIndex, + offset: state.offset.mode === OffsetMode.Absolute ? state.offset.absolute : state.offset.relative, - groupExpectedEnd: state.groupNow?.expectedEnd ?? null, + groupExpectedEnd: state.offset.expectedGroupEnd, })); export const useTimelineStatus = createSelector((state: RuntimeStore) => ({ clock: state.clock, - offset: state.runtime.offsetAbs, + offset: state.offset.absolute, })); export const useExpectedStartData = createSelector((state: RuntimeStore) => ({ - offset: state.runtime.offsetMode === OffsetMode.Absolute ? state.runtime.offsetAbs : state.runtime.offsetRel, - offsetMode: state.runtime.offsetMode, + offset: state.offset.mode === OffsetMode.Absolute ? state.offset.absolute : state.offset.relative, + mode: state.offset.mode, currentDay: state.eventNow?.dayOffset ?? 0, - actualStart: state.runtime.actualStart, - plannedStart: state.runtime.plannedStart, + actualStart: state.rundown.actualStart, + plannedStart: state.rundown.plannedStart, clock: state.clock, })); @@ -199,7 +199,7 @@ export const useCurrentDay = createSelector((state: RuntimeStore) => ({ })); export const useRuntimeOffset = createSelector((state: RuntimeStore) => ({ - offset: state.runtime.offsetAbs, + offset: state.offset.absolute, })); export const usePing = createSelector((state: RuntimeStore) => ({ @@ -212,7 +212,7 @@ export const useIsOnline = createSelector((state: RuntimeStore) => ({ })); export const useOffsetMode = createSelector((state: RuntimeStore) => ({ - offsetMode: state.runtime.offsetMode, + offsetMode: state.offset.mode, })); export const setOffsetMode = (payload: OffsetMode) => sendSocket('offsetmode', payload); @@ -251,7 +251,7 @@ export const useCountdownSocket = createSelector((state: RuntimeStore) => ({ export const useBackstageSocket = createSelector((state: RuntimeStore) => ({ eventNext: state.eventNext, eventNow: state.eventNow, - runtime: state.runtime, + rundown: state.rundown, selectedEventId: state.eventNow?.id ?? null, time: state.timer, })); @@ -266,10 +266,11 @@ export const useStudioTimersSocket = createSelector((state: RuntimeStore) => ({ eventNow: state.eventNow, message: state.message, time: state.timer, - runtime: state.runtime, + offset: state.offset, + rundown: state.rundown, })); export const useTimelineSocket = createSelector((state: RuntimeStore) => ({ clock: state.clock, - offsetAbs: state.runtime.offsetAbs, + offsetAbs: state.offset.absolute, })); diff --git a/apps/client/src/common/utils/time.ts b/apps/client/src/common/utils/time.ts index be1799709..1be0e4118 100644 --- a/apps/client/src/common/utils/time.ts +++ b/apps/client/src/common/utils/time.ts @@ -145,12 +145,12 @@ export function useTimeUntilExpectedStart( isLinkedToLoaded: boolean; }, ): number { - const { offset, currentDay, offsetMode, actualStart, plannedStart, clock } = useExpectedStartData(); + const { offset, currentDay, mode, actualStart, plannedStart, clock } = useExpectedStartData(); if (event === null) return 0; const expectedStart = getExpectedStart( { ...event }, - { ...state, currentDay, offset, offsetMode, actualStart, plannedStart }, + { ...state, currentDay, offset, mode, actualStart, plannedStart }, ); return expectedStart - clock; } diff --git a/apps/client/src/features/app-settings/panel/automations-panel/template-input/templateInput.utils.ts b/apps/client/src/features/app-settings/panel/automations-panel/template-input/templateInput.utils.ts index 139fda596..71f731ede 100644 --- a/apps/client/src/features/app-settings/panel/automations-panel/template-input/templateInput.utils.ts +++ b/apps/client/src/features/app-settings/panel/automations-panel/template-input/templateInput.utils.ts @@ -18,15 +18,16 @@ const staticAutocompleteOptions = [ '{{timer.expectedFinish}}', '{{timer.secondaryTimer}}', '{{timer.startedAt}}', - '{{runtime.selectedEventIndex}}', - '{{runtime.numEvents}}', - '{{runtime.offset}}', - '{{runtime.plannedStart}}', - '{{runtime.plannedEnd}}', - '{{runtime.actualStart}}', - '{{runtime.expectedEnd}}', - '{{currentGroup.id}}', - '{{currentGroup.startedAt}}', + '{{rundown.selectedEventIndex}}', + '{{rundown.numEvents}}', + '{{rundown.plannedStart}}', + '{{rundown.plannedEnd}}', + '{{rundown.actualStart}}', + '{{offset.absolute}}', + '{{offset.relative}}', + '{{offset.expectedRundownEnd}}', + '{{offset.expectedGroupEnd}}', + '{{offset.expectedFlagStart}}', ]; const eventStaticPropertiesNow = [ diff --git a/apps/client/src/features/overview/composite/TimeElements.tsx b/apps/client/src/features/overview/composite/TimeElements.tsx index 9caaae1f0..614662a67 100644 --- a/apps/client/src/features/overview/composite/TimeElements.tsx +++ b/apps/client/src/features/overview/composite/TimeElements.tsx @@ -16,7 +16,7 @@ import { useClock, useCurrentGroupId, useNextFlag, - useRuntimeOverview, + useRundownOverview, useRuntimePlaybackOverview, useTimer, } from '../../../common/hooks/useSocket'; @@ -31,7 +31,7 @@ import { OverUnder, TimeColumn } from './TimeLayout'; import style from './TimeElements.module.scss'; export function StartTimes() { - const { plannedEnd, plannedStart, actualStart, expectedEnd } = useRuntimeOverview(); + const { plannedEnd, plannedStart, actualStart, expectedEnd } = useRundownOverview(); const plannedStartText = plannedStart === null ? timerPlaceholder : formatTime(plannedStart); diff --git a/apps/client/src/views/backstage/Backstage.tsx b/apps/client/src/views/backstage/Backstage.tsx index 66d1cfcb5..0ef444ecb 100644 --- a/apps/client/src/views/backstage/Backstage.tsx +++ b/apps/client/src/views/backstage/Backstage.tsx @@ -44,7 +44,7 @@ export default function BackstageLoader() { function Backstage({ events, customFields, projectData, isMirrored, settings }: BackstageData) { const { getLocalizedString } = useTranslation(); const { secondarySource, extraInfo } = useBackstageOptions(); - const { eventNext, eventNow, runtime, selectedEventId, time } = useBackstageSocket(); + const { eventNext, eventNow, rundown, selectedEventId, time } = useBackstageSocket(); const [blinkClass, setBlinkClass] = useState(false); const { height: screenHeight } = useViewportSize(); @@ -76,13 +76,13 @@ function Backstage({ events, customFields, projectData, isMirrored, settings }: const scheduledStart = (() => { if (showNow) return undefined; if (!hasEvents) return undefined; - return formatTime(runtime.plannedStart, { format12: 'hh:mm a', format24: 'HH:mm' }); + return formatTime(rundown.plannedStart, { format12: 'hh:mm a', format24: 'HH:mm' }); })(); const scheduledEnd = (() => { if (showNow) return undefined; if (!hasEvents) return undefined; - return formatTime(runtime.plannedEnd, { format12: 'hh:mm a', format24: 'HH:mm' }); + return formatTime(rundown.plannedEnd, { format12: 'hh:mm a', format24: 'HH:mm' }); })(); let displayTimer = millisToString(time.current, { fallback: timerPlaceholderMin }); diff --git a/apps/client/src/views/studio/StudioTimers.tsx b/apps/client/src/views/studio/StudioTimers.tsx index 9c2e37102..15674e0cf 100644 --- a/apps/client/src/views/studio/StudioTimers.tsx +++ b/apps/client/src/views/studio/StudioTimers.tsx @@ -17,9 +17,13 @@ interface StudioTimersProps { export default function StudioTimers({ viewSettings }: StudioTimersProps) { const { getLocalizedString } = useTranslation(); - const { eventNow, eventNext, message, time, runtime } = useStudioTimersSocket(); + const { eventNow, eventNext, message, time, offset, rundown } = useStudioTimersSocket(); - const schedule = getFormattedScheduleTimes(runtime); + const schedule = getFormattedScheduleTimes({ + offset: offset.absolute, + actualStart: rundown.actualStart, + expectedEnd: offset.expectedRundownEnd, + }); const event = getFormattedEventData(eventNow, time); const eventNextTitle = eventNext?.title || '-'; const formattedTimerMessage = (message.timer.visible && message.timer.text) || '-'; @@ -33,7 +37,7 @@ export default function StudioTimers({ viewSettings }: StudioTimersProps) { time.phase === TimerPhase.Danger, ); - const offsetState = getOffsetState(runtime.offsetAbs); + const offsetState = getOffsetState(offset.absolute); return (
diff --git a/apps/client/src/views/studio/studioTimers.utils.ts b/apps/client/src/views/studio/studioTimers.utils.ts index c42cd3830..953b53f9e 100644 --- a/apps/client/src/views/studio/studioTimers.utils.ts +++ b/apps/client/src/views/studio/studioTimers.utils.ts @@ -1,16 +1,19 @@ -import { OntimeEvent, Runtime, TimerState } from 'ontime-types'; +import { MaybeNumber, OntimeEvent, TimerState } from 'ontime-types'; import { millisToString } from 'ontime-utils'; import { getOffsetText } from '../../common/utils/offset'; import { formatTime } from '../../common/utils/time'; const timeFormat = { format12: 'h:mm a', format24: 'HH:mm' }; -export function getFormattedScheduleTimes(runtime: Runtime) { - const correctedOffset = runtime.offsetAbs !== null ? runtime.offsetAbs * -1 : null; +export function getFormattedScheduleTimes(data: { + offset: number; + actualStart: MaybeNumber; + expectedEnd: MaybeNumber; +}) { return { - actualStart: formatTime(runtime.actualStart, timeFormat), - expectedEnd: formatTime(runtime.expectedEnd, timeFormat), - offset: getOffsetText(correctedOffset), + actualStart: formatTime(data.actualStart, timeFormat), + expectedEnd: formatTime(data.expectedEnd, timeFormat), + offset: getOffsetText(data.offset), }; } diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 9ab5c4c95..914ca1a11 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -184,12 +184,12 @@ export const startServer = async (): Promise<{ message: string; serverPort: numb clock: state.clock, timer: state.timer, message: { ...runtimeStorePlaceholder.message }, - runtime: state.runtime, + offset: state.offset, + rundown: state.rundown, eventNow: state.eventNow, eventNext: state.eventNext, + eventFlag: null, groupNow: null, - groupNext: null, - nextFlag: null, auxtimer1: { duration: timerConfig.auxTimerDefault, current: timerConfig.auxTimerDefault, diff --git a/apps/server/src/services/RestoreService.ts b/apps/server/src/services/RestoreService.ts index 5338b361e..11c749181 100644 --- a/apps/server/src/services/RestoreService.ts +++ b/apps/server/src/services/RestoreService.ts @@ -12,7 +12,6 @@ export type RestorePoint = { addedTime: number; pausedAt: MaybeNumber; firstStart: MaybeNumber; - groupStartAt: MaybeNumber; }; /** @@ -51,10 +50,6 @@ export function isRestorePoint(obj: unknown): obj is RestorePoint { return false; } - if (typeof restorePoint.groupStartAt !== 'number' && restorePoint.groupStartAt !== null) { - return false; - } - return true; } diff --git a/apps/server/src/services/__tests__/RestoreService.test.ts b/apps/server/src/services/__tests__/RestoreService.test.ts index 5857cb9c4..42341f3ac 100644 --- a/apps/server/src/services/__tests__/RestoreService.test.ts +++ b/apps/server/src/services/__tests__/RestoreService.test.ts @@ -14,7 +14,6 @@ describe('isRestorePoint()', () => { addedTime: 2, pausedAt: 3, firstStart: 1, - groupStartAt: 10, }; expect(isRestorePoint(restorePoint)).toBe(true); @@ -25,7 +24,6 @@ describe('isRestorePoint()', () => { addedTime: 0, pausedAt: null, firstStart: 1, - groupStartAt: null, }; expect(isRestorePoint(restorePoint)).toBe(true); }); @@ -76,7 +74,6 @@ describe('RestoreService()', () => { addedTime: 5678, pausedAt: 9087, firstStart: 1234, - groupStartAt: 1652, }; const restoreService = new RestoreService('/path/to/restore/file'); @@ -94,7 +91,6 @@ describe('RestoreService()', () => { addedTime: 0, pausedAt: null, firstStart: 1234, - groupStartAt: null, }; const restoreService = new RestoreService('/path/to/restore/file'); @@ -132,7 +128,6 @@ describe('RestoreService()', () => { addedTime: 1234, pausedAt: 1234, firstStart: 1234, - groupStartAt: null, }; const restoreService = new RestoreService('/path/to/restore/file'); diff --git a/apps/server/src/services/__tests__/timerUtils.test.ts b/apps/server/src/services/__tests__/timerUtils.test.ts index 1496b3b5c..6c6e7873c 100644 --- a/apps/server/src/services/__tests__/timerUtils.test.ts +++ b/apps/server/src/services/__tests__/timerUtils.test.ts @@ -204,7 +204,7 @@ describe('getExpectedFinish()', () => { pausedAt: null, hasFinished: false, }, - runtime: { + rundown: { actualStart: 79200000, plannedEnd: 600000, }, @@ -358,7 +358,7 @@ describe('getCurrent()', () => { duration: 100, startedAt: null, }, - runtime: { + rundown: { plannedEnd: null, }, _timer: { @@ -383,7 +383,7 @@ describe('getCurrent()', () => { duration: 100, startedAt: 10, }, - runtime: { + rundown: { plannedEnd: 100, }, _timer: { @@ -408,7 +408,7 @@ describe('getCurrent()', () => { duration: 100, startedAt: 10, }, - runtime: { + rundown: { plannedEnd: 100, }, _timer: { @@ -434,7 +434,7 @@ describe('getCurrent()', () => { duration: Infinity, // not relevant, startedAt: 79200000, // 22:00:00 }, - runtime: { + rundown: { actualStart: 79200000, plannedEnd: 600000, }, @@ -462,7 +462,7 @@ describe('getCurrent()', () => { duration: Infinity, // not relevant, startedAt: 79200000, // 22:00:00 }, - runtime: { + rundown: { actualStart: 82000000, // 22:46:40 <--- started now plannedEnd: 81000000, // 22:30:00 }, @@ -735,14 +735,14 @@ describe('getRuntimeOffset()', () => { _timer: { pausedAt: null, }, - runtime: { + rundown: { actualStart: 150, plannedStart: 100, }, } as RuntimeState; - const { offsetAbs } = getRuntimeOffset(state); - expect(offsetAbs).toBe(50); + const { absolute } = getRuntimeOffset(state); + expect(absolute).toBe(50); }); it('added time subtracts time offset (positive offset)', () => { @@ -759,14 +759,14 @@ describe('getRuntimeOffset()', () => { _timer: { pausedAt: null, }, - runtime: { + rundown: { actualStart: 150, plannedStart: 100, }, } as RuntimeState; - const { offsetAbs } = getRuntimeOffset(state); - expect(offsetAbs).toBe(60); + const { absolute } = getRuntimeOffset(state); + expect(absolute).toBe(60); }); it('considers running overtime (negative offset)', () => { @@ -784,14 +784,14 @@ describe('getRuntimeOffset()', () => { _timer: { pausedAt: null, }, - runtime: { + rundown: { actualStart: 100, plannedStart: 100, }, } as RuntimeState; - const { offsetAbs } = getRuntimeOffset(state); - expect(offsetAbs).toBe(10); + const { absolute } = getRuntimeOffset(state); + expect(absolute).toBe(10); }); it('paused time is delayed time (negative offset)', () => { @@ -810,14 +810,14 @@ describe('getRuntimeOffset()', () => { _timer: { pausedAt: 125, // we have been paused for 25ms (see clock) }, - runtime: { + rundown: { actualStart: 100, plannedStart: 100, }, } as RuntimeState; - const { offsetAbs } = getRuntimeOffset(state); - expect(offsetAbs).toBe(25); + const { absolute } = getRuntimeOffset(state); + expect(absolute).toBe(25); }); it('offset doesnt exist if we havent started', () => { @@ -831,14 +831,16 @@ describe('getRuntimeOffset()', () => { timeStrategy: 'lock-duration', linkStart: false, }, - runtime: { + rundown: { selectedEventIndex: 0, numEvents: 2, - offsetAbs: -77400000, plannedStart: 77400000, plannedEnd: 84600000, actualStart: null, - expectedEnd: null, + }, + offset: { + absolute: -77400000, + expectedRundownEnd: null, }, timer: { addedTime: 0, @@ -853,8 +855,8 @@ describe('getRuntimeOffset()', () => { _timer: { pausedAt: null }, } as RuntimeState; - const { offsetAbs } = getRuntimeOffset(state); - expect(offsetAbs).toBe(0); + const { absolute } = getRuntimeOffset(state); + expect(absolute).toBe(0); }); it('with time-to-end, offsets dont exist if we are not in overtime', () => { @@ -882,14 +884,16 @@ describe('getRuntimeOffset()', () => { custom: {}, delay: 0, }, - runtime: { + rundown: { selectedEventIndex: 0, numEvents: 1, - offsetAbs: 0, plannedStart: 77400000, // 21:30:00 plannedEnd: 81000000, // 22:30:00 actualStart: 78000000, // 21:40:00 - expectedEnd: 81600000, // 22:40:00 + }, + offset: { + absolute: 0, + expectedRundownEnd: 81600000, // 22:40:00 }, timer: { addedTime: 0, @@ -904,8 +908,8 @@ describe('getRuntimeOffset()', () => { _timer: { pausedAt: null }, } as RuntimeState; - const { offsetAbs } = getRuntimeOffset(state); - expect(offsetAbs).toBe(0); + const { absolute } = getRuntimeOffset(state); + expect(absolute).toBe(0); }); it('with time-to-end, offset is the overtime', () => { @@ -933,14 +937,16 @@ describe('getRuntimeOffset()', () => { custom: {}, delay: 0, }, - runtime: { + rundown: { selectedEventIndex: 0, numEvents: 1, - offsetAbs: 0, plannedStart: 77400000, // 21:30:00 plannedEnd: 81000000, // 22:30:00 actualStart: 78000000, // 21:40:00 - expectedEnd: 81600000, // 22:40:00 + }, + offset: { + absolute: 0, + expectedRundownEnd: 81600000, // 22:40:00 }, timer: { addedTime: -200000, @@ -955,8 +961,8 @@ describe('getRuntimeOffset()', () => { _timer: { pausedAt: null }, } as RuntimeState; - const { offsetAbs } = getRuntimeOffset(state); - expect(offsetAbs).toBe(400000); // <--- offset is always the overtime + const { absolute } = getRuntimeOffset(state); + expect(absolute).toBe(400000); // <--- offset is always the overtime }); it('handles time-to-end started after the end time', () => { @@ -973,14 +979,16 @@ describe('getRuntimeOffset()', () => { timerType: TimerType.CountDown, countToEnd: true, }, - runtime: { + rundown: { selectedEventIndex: 0, numEvents: 1, - offsetAbs: 0, plannedStart: 77400000, // 21:30:00 plannedEnd: 81000000, // 22:30:00 actualStart: 82000000, // 22:46:40 <--- started now - expectedEnd: 82000000 + 3600000, // <--- now + duration + }, + offset: { + absolute: 0, + expectedRundownEnd: 82000000 + 3600000, // <--- now + duration }, timer: { addedTime: 0, @@ -997,9 +1005,9 @@ describe('getRuntimeOffset()', () => { const updateCurrent = getCurrent(state); state.timer.current = updateCurrent; - const { offsetAbs } = getRuntimeOffset(state); - expect(millisToString(offsetAbs)).toBe('00:16:40'); - expect(offsetAbs).toBe(82000000 - 81000000); // <-- now - planned end + const { absolute } = getRuntimeOffset(state); + expect(millisToString(absolute)).toBe('00:16:40'); + expect(absolute).toBe(82000000 - 81000000); // <-- now - planned end }); }); @@ -1018,15 +1026,15 @@ describe('getoffsetRel()', () => { _timer: { pausedAt: null, }, - runtime: { + rundown: { actualStart: 150, plannedStart: 150, }, } as RuntimeState; - const { offsetAbs, offsetRel } = getRuntimeOffset(state); - expect(offsetAbs).toBe(0); - expect(offsetRel).toBe(0); + const { absolute, relative } = getRuntimeOffset(state); + expect(absolute).toBe(0); + expect(relative).toBe(0); }); it('relative offset is 0 when starting after the planed time', () => { const state = { @@ -1042,15 +1050,15 @@ describe('getoffsetRel()', () => { _timer: { pausedAt: null, }, - runtime: { + rundown: { actualStart: 150, plannedStart: 100, }, } as RuntimeState; - const { offsetAbs, offsetRel } = getRuntimeOffset(state); - expect(offsetAbs).toBe(50); - expect(offsetRel).toBe(0); + const { absolute, relative } = getRuntimeOffset(state); + expect(absolute).toBe(50); + expect(relative).toBe(0); }); it('relative offset is 0 when starting before the planed time', () => { const state = { @@ -1066,15 +1074,15 @@ describe('getoffsetRel()', () => { _timer: { pausedAt: null, }, - runtime: { + rundown: { actualStart: 100, plannedStart: 150, }, } as RuntimeState; - const { offsetAbs, offsetRel } = getRuntimeOffset(state); - expect(offsetAbs).toBe(-50); - expect(offsetRel).toBe(0); + const { absolute, relative } = getRuntimeOffset(state); + expect(absolute).toBe(-50); + expect(relative).toBe(0); }); }); @@ -1175,14 +1183,16 @@ describe('getTimerPhase()', () => { clock: 55691050, eventNow: null, eventNext: null, - runtime: { + rundown: { selectedEventIndex: null, numEvents: 1, - offsetAbs: 0, plannedStart: 55860000, plannedEnd: 55880000, actualStart: null, - expectedEnd: null, + }, + offset: { + absolute: 0, + expectedRundownEnd: null, }, timer: { addedTime: 0, @@ -1213,14 +1223,16 @@ describe('getTimerPhase()', () => { clock: 55691050, eventNow: null, eventNext: null, - runtime: { + rundown: { selectedEventIndex: null, numEvents: 1, - offsetAbs: 0, plannedStart: 55860000, plannedEnd: 55880000, actualStart: null, - expectedEnd: null, + }, + offset: { + absolute: 0, + expectedRundownEnd: null, }, timer: { addedTime: 0, diff --git a/apps/server/src/services/runtime-service/RuntimeService.ts b/apps/server/src/services/runtime-service/RuntimeService.ts index b2b9c730a..2982ed989 100644 --- a/apps/server/src/services/runtime-service/RuntimeService.ts +++ b/apps/server/src/services/runtime-service/RuntimeService.ts @@ -3,10 +3,10 @@ import { isOntimeEvent, isPlayableEvent, LogOrigin, - MaybeNumber, OffsetMode, OntimeEvent, Playback, + RuntimeStore, TimerLifeCycle, TimerPhase, TimerState, @@ -34,16 +34,12 @@ import { findPreviousPlayableId, getEventAtIndex, getShouldClockUpdate, - getShouldFlagUpdate, - getShouldGroupUpdate, - getShouldRuntimeUpdate, + getShouldOffsetUpdate, getShouldTimerUpdate, isNewSecond, } from './rundownService.utils.js'; import { RundownMetadata } from '../../api-data/rundown/rundown.types.js'; -type RuntimeStateEventKeys = keyof Pick; - /** * Service manages runtime status of app * Coordinating with necessary services @@ -53,22 +49,11 @@ class RuntimeService { private lastIntegrationClockUpdate = -1; private lastIntegrationTimerValue = -1; - /** last time we updated the socket */ - static previousTimerUpdate: number; - static previousRuntimeUpdate: number; - static previousTimerValue: MaybeNumber; // previous timer value, could be null - static previousClockUpdate: number; - /** last known state */ static previousState: RuntimeState; constructor(eventTimer: EventTimer) { this.eventTimer = eventTimer; - - RuntimeService.previousTimerUpdate = -1; - RuntimeService.previousRuntimeUpdate = -1; - RuntimeService.previousTimerValue = -1; - RuntimeService.previousClockUpdate = -1; RuntimeService.previousState = {} as RuntimeState; } @@ -101,7 +86,7 @@ class RuntimeService { // 2. handle edge cases related to roll if (newState.timer.playback === Playback.Roll) { // check if we need to call any side effects - const keepOffset = newState.runtime.offsetAbs; + const keepOffset = newState.offset.absolute; if (hasSecondaryTimerFinished) { // if the secondary timer has finished, we need to call roll // since event is already loaded @@ -292,7 +277,7 @@ class RuntimeService { rundown, playableEventOrder, cue, - state.runtime.selectedEventIndex ?? undefined, + state.rundown.selectedEventIndex ?? undefined, ); if (!event) { @@ -353,7 +338,7 @@ class RuntimeService { rundown, playableEventOrder, cue, - state.runtime.selectedEventIndex ?? undefined, + state.rundown.selectedEventIndex ?? undefined, ); if (!event) { @@ -412,7 +397,7 @@ class RuntimeService { } if (state.timer.playback === Playback.Roll) { - return this.loadEvent(nextEvent, { firstStart: state.runtime.actualStart }); + return this.loadEvent(nextEvent, { firstStart: state.rundown.actualStart }); } return this.loadEvent(nextEvent); } @@ -667,20 +652,24 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert // we do the comparison by explicitly for each property // to apply custom logic for different datasets - // if a new event was loaded most things should update - const hasNewLoaded = state.eventNow?.id !== RuntimeService.previousState?.eventNow?.id; + // Update the entry if they have changed + let entryChanged = false; + entryChanged ||= updateMaybeEntryIfChanged('eventNow'); + entryChanged ||= updateMaybeEntryIfChanged('eventNext'); + entryChanged ||= updateMaybeEntryIfChanged('eventFlag'); + entryChanged ||= updateMaybeEntryIfChanged('groupNow'); // for the very fist run there will be nothing in the previousState so we force an update const justStarted = !RuntimeService.previousState?.timer; // offset mode has been changed - const offsetModeChanged = RuntimeService.previousState?.runtime?.offsetMode !== state.runtime.offsetMode; + const offsetModeChanged = RuntimeService.previousState?.offset?.mode !== state.offset.mode; // if playback changes most things should update const hasChangedPlayback = RuntimeService.previousState.timer?.playback !== state.timer.playback; // combine all big changes - const hasImmediateChanges = hasNewLoaded || justStarted || hasChangedPlayback || offsetModeChanged; + const hasImmediateChanges = entryChanged || justStarted || hasChangedPlayback || offsetModeChanged; // clock has changed by a second or more const updateClock = getShouldClockUpdate(RuntimeService.previousState.clock, state.clock); @@ -693,74 +682,43 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert const updateTimer = getShouldTimerUpdate(RuntimeService.previousState?.timer, state.timer); if (updateTimer) { batch.add('timer', state.timer); - RuntimeService.previousTimerUpdate = state.clock; - RuntimeService.previousTimerValue = state.timer.current; RuntimeService.previousState.timer = { ...state.timer }; } // if any values have changed, values that have the possibility to tick are modulated by `hasClockUpdate` - const updateRuntime = getShouldRuntimeUpdate( - RuntimeService.previousState?.runtime, - state.runtime, + const updateRuntime = getShouldOffsetUpdate( + RuntimeService.previousState?.offset, + state.offset, updateClock || hasImmediateChanges, ); if (updateRuntime) { - batch.add('runtime', state.runtime); - RuntimeService.previousRuntimeUpdate = state.clock; - RuntimeService.previousState.runtime = structuredClone(state.runtime); + batch.add('offset', state.offset); + RuntimeService.previousState.offset = structuredClone(state.offset); } - // if any values have changed, values that have the possibility to tick are modulated by `hasClockUpdate` - const updateGroupNow = getShouldGroupUpdate( - RuntimeService.previousState.groupNow, - state.groupNow, - updateClock || hasImmediateChanges, - ); - if (updateGroupNow) { - batch.add('groupNow', state.groupNow); - RuntimeService.previousState.groupNow = structuredClone(state.groupNow); + // if any values have changed + const updateRundownData = !deepEqual(RuntimeService.previousState.rundown, state.rundown); + if (updateRundownData) { + batch.add('rundown', state.rundown); + RuntimeService.previousState.rundown = structuredClone(state.rundown); } - // next group is just a simple string or null compare - const updateGroupNext = RuntimeService.previousState.groupNext !== state.groupNext; - if (updateGroupNext) { - batch.add('groupNext', state.groupNext); - RuntimeService.previousState.groupNext = structuredClone(state.groupNext); - } + function updateMaybeEntryIfChanged< + K extends keyof Pick, + >(key: K) { + const previousEntry = RuntimeService.previousState[key]; + const currentEntry = state[key]; - // if any values have changed, values that have the possibility to tick are modulated by `hasClockUpdate` - const updateFlag = getShouldFlagUpdate( - RuntimeService.previousState.nextFlag, - state.nextFlag, - updateClock || hasImmediateChanges, - ); - if (updateFlag) { - batch.add('nextFlag', state.nextFlag); - RuntimeService.previousState.nextFlag = structuredClone(state.nextFlag); - } + if (!previousEntry && !currentEntry) return false; // if both are null -> skip - // Update the events if they have changed - updateEventIfChanged('eventNow', state); - updateEventIfChanged('eventNext', state); - - // Helper function to update an event if it has changed - function updateEventIfChanged(eventKey: RuntimeStateEventKeys, state: runtimeState.RuntimeState) { - const previous = RuntimeService.previousState?.[eventKey]; - const now = state[eventKey]; - - // if there was nothing, and there is nothing, noop - if (!previous?.id && !now?.id) return; - - const eventChanged = - // if load status changed, save new - previous?.id !== now?.id || - // maybe the event itself has changed - !deepEqual(RuntimeService.previousState?.[eventKey], state[eventKey]); - - if (!eventChanged) return; - - batch.add(eventKey, state[eventKey]); - RuntimeService.previousState[eventKey] = structuredClone(state[eventKey]); + // if they have the same id the check if the contents have changed + if (previousEntry?.id === currentEntry?.id) { + if (deepEqual(previousEntry, currentEntry)) return false; // contents are the same -> skip + } + // at this point we know that either the id or the contents has changed + batch.add(key, currentEntry as RuntimeStore[K]); // we know that there is the necessary overlap in the types to cast this + RuntimeService.previousState[key] = structuredClone(currentEntry); + return true; } // save the restore state @@ -772,8 +730,7 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert startedAt: state.timer.startedAt, addedTime: state.timer.addedTime, pausedAt: state._timer.pausedAt, - firstStart: state.runtime.actualStart, - groupStartAt: state.groupNow?.startedAt ?? null, + firstStart: state.rundown.actualStart, }) .catch((_e) => { //we don't do anything with the error here diff --git a/apps/server/src/services/runtime-service/rundownService.utils.ts b/apps/server/src/services/runtime-service/rundownService.utils.ts index a23bc1db0..19280d367 100644 --- a/apps/server/src/services/runtime-service/rundownService.utils.ts +++ b/apps/server/src/services/runtime-service/rundownService.utils.ts @@ -1,16 +1,14 @@ import { millisToSeconds } from 'ontime-utils'; import { EntryId, - GroupState, isOntimeEvent, isPlayableEvent, MaybeNumber, OntimeEvent, Rundown, - Runtime, + Offset, TimerState, TimerType, - UpcomingEntry, } from 'ontime-types'; import { deepEqual } from 'fast-equals'; @@ -55,51 +53,15 @@ export function getShouldTimerUpdate(previousValue: TimerState | undefined, curr ); } -export function getShouldRuntimeUpdate( - previousValue: Runtime | undefined, - currentValue: Runtime, +export function getShouldOffsetUpdate( + previousValue: Offset | undefined, + currentValue: Offset, didDependencyUpdate: boolean, ): boolean { if (previousValue === undefined) return true; - if (didDependencyUpdate) return !deepEqual(previousValue, currentValue); - - return ( - previousValue.selectedEventIndex !== currentValue.selectedEventIndex || - previousValue.numEvents !== currentValue.numEvents || - previousValue.plannedStart !== currentValue.plannedStart || - previousValue.plannedEnd !== currentValue.plannedEnd || - previousValue.actualStart !== currentValue.actualStart || - previousValue.offsetMode !== currentValue.offsetMode - // offsetAbs, offsetRel and expectedEnd are ticked with `didDependencyUpdate` - ); -} - -export function getShouldGroupUpdate( - previousValue: GroupState | null | undefined, - currentValue: GroupState | null, - didDependencyUpdate: boolean, -): boolean { - if (previousValue === undefined) return true; - if (didDependencyUpdate) return !deepEqual(previousValue, currentValue); - - return ( - previousValue?.id !== currentValue?.id || previousValue?.startedAt !== currentValue?.startedAt - // expectedEnd are ticked with `didDependencyUpdate` - ); -} - -export function getShouldFlagUpdate( - previousValue: UpcomingEntry | null | undefined, - currentValue: UpcomingEntry | null, - didDependencyUpdate: boolean, -): boolean { - if (previousValue === undefined) return true; - if (didDependencyUpdate) return !deepEqual(previousValue, currentValue); - - return ( - previousValue?.id !== currentValue?.id - // expectedStart - ); + if (previousValue.mode !== currentValue.mode) return true; + // absolute, relative, expected*End are ticked with `didDependencyUpdate` + return didDependencyUpdate && !deepEqual(previousValue, currentValue); } /** diff --git a/apps/server/src/services/timerUtils.ts b/apps/server/src/services/timerUtils.ts index ff075f3fb..45d33768e 100644 --- a/apps/server/src/services/timerUtils.ts +++ b/apps/server/src/services/timerUtils.ts @@ -112,23 +112,23 @@ 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): { offsetAbs: number; offsetRel: number } { +export function getRuntimeOffset(state: RuntimeState): { absolute: number; relative: number } { const { eventNow, clock } = 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) { - return { offsetAbs: 0, offsetRel: 0 }; + return { absolute: 0, relative: 0 }; } const { countToEnd, timeStart } = eventNow; - const { plannedStart, actualStart } = state.runtime; + const { plannedStart, actualStart } = state.rundown; // eslint-disable-next-line no-unused-labels -- dev code path DEV: { // we know current exists as long as eventNow exists if (current === null) throw new Error('timerUtils.getRuntimeOffset: state.timer.current must be set'); - if (plannedStart === null) throw new Error('timerUtils.getRuntimeOffset: state.runtime.plannedStart must be set'); - if (actualStart === null) throw new Error('timerUtils.getRuntimeOffset: state.runtime.actualStart must be set'); + if (plannedStart === null) throw new Error('timerUtils.getRuntimeOffset: state.rundown.plannedStart must be set'); + if (actualStart === null) throw new Error('timerUtils.getRuntimeOffset: state.rundown.plannedStart must be set'); } // difference between planned event start and actual event start (will be positive if we stared behind ) @@ -140,13 +140,13 @@ export function getRuntimeOffset(state: RuntimeState): { offsetAbs: number; offs // 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 offsetAbs = eventStartOffset + overtime + pausedTime + addedTime; + const absolute = eventStartOffset + overtime + pausedTime + addedTime; // the relative offset i the same as the absolute offset but adjusted relative to the actual start time - const offsetRel = offsetAbs + plannedStart - actualStart; + const relative = absolute + plannedStart - actualStart; // in case of count to end, the absolute offset is just the overtime - return countToEnd ? { offsetAbs: overtime, offsetRel } : { offsetAbs, offsetRel }; + return countToEnd ? { absolute: overtime, relative } : { absolute, relative }; } /** diff --git a/apps/server/src/stores/__mocks__/runtimeState.mocks.ts b/apps/server/src/stores/__mocks__/runtimeState.mocks.ts index c15bb43d3..a562dc92f 100644 --- a/apps/server/src/stores/__mocks__/runtimeState.mocks.ts +++ b/apps/server/src/stores/__mocks__/runtimeState.mocks.ts @@ -6,19 +6,22 @@ const baseState: RuntimeState = { clock: 0, eventNow: null, eventNext: null, + eventFlag: null, groupNow: null, - groupNext: null, - nextFlag: null, - runtime: { + rundown: { selectedEventIndex: null, numEvents: 0, - offsetAbs: 0, - offsetRel: 0, plannedStart: 0, plannedEnd: 0, actualStart: null, - expectedEnd: null, - offsetMode: OffsetMode.Absolute, + }, + offset: { + absolute: 0, + relative: 0, + mode: OffsetMode.Absolute, + expectedRundownEnd: null, + expectedGroupEnd: null, + expectedFlagStart: null, }, timer: { addedTime: 0, diff --git a/apps/server/src/stores/__tests__/runtimeState.test.ts b/apps/server/src/stores/__tests__/runtimeState.test.ts index dbeb61abf..011ef2791 100644 --- a/apps/server/src/stores/__tests__/runtimeState.test.ts +++ b/apps/server/src/stores/__tests__/runtimeState.test.ts @@ -33,7 +33,7 @@ const mockState = { clock: 666, eventNow: null, eventNext: null, - runtime: { + rundown: { selectedEventIndex: null, numEvents: 0, }, @@ -116,7 +116,7 @@ describe('mutation on runtimeState', () => { expect(newState.timer).toMatchObject({ playback: Playback.Play, }); - expect(newState.runtime.actualStart).toBe(newState.clock); + expect(newState.rundown.actualStart).toBe(newState.clock); // 3. Pause event vi.setSystemTime('jan 1 00:03'); @@ -166,7 +166,7 @@ describe('mutation on runtimeState', () => { expectedFinish: null, startedAt: null, }); - expect(newState.runtime.actualStart).toBeNull(); + expect(newState.rundown.actualStart).toBeNull(); }); }); @@ -187,24 +187,24 @@ describe('mutation on runtimeState', () => { vi.setSystemTime('jan 1 00:09'); load(entries.event1, rundown, metadata); let newState = getState(); - expect(newState.runtime.actualStart).toBeNull(); - expect(newState.runtime.plannedStart).toBe(0); - expect(newState.runtime.plannedEnd).toBe(1500); + expect(newState.rundown.actualStart).toBeNull(); + expect(newState.rundown.plannedStart).toBe(0); + expect(newState.rundown.plannedEnd).toBe(1500); expect(newState.groupNow).toBeNull(); - expect(newState.runtime.offsetAbs).toBe(0); + expect(newState.offset.absolute).toBe(0); // 2. Start event vi.setSystemTime('jan 1 00:10'); start(); newState = getState(); const firstStart = newState.clock; - if (newState.runtime.offsetAbs === null) { + if (newState.offset.absolute === null) { throw new Error('Value cannot be null at this stage'); } - expect(newState.runtime.actualStart).toBe(newState.clock); - expect(newState.runtime.offsetAbs).toBe(newState.clock - entries.event1.timeStart); - expect(newState.runtime.expectedEnd).toBe(entries.event2.timeEnd + newState.runtime.offsetAbs); + expect(newState.rundown.actualStart).toBe(newState.clock); + expect(newState.offset.absolute).toBe(newState.clock - entries.event1.timeStart); + expect(newState.offset.expectedRundownEnd).toBe(entries.event2.timeEnd + newState.offset.absolute); // 3. Next event vi.setSystemTime('jan 1 00:12'); @@ -212,37 +212,37 @@ describe('mutation on runtimeState', () => { start(); newState = getState(); - if (newState.runtime.actualStart === null || newState.runtime.offsetAbs === null) { + if (newState.rundown.actualStart === null || newState.offset.absolute === null) { throw new Error('Value cannot be null at this stage'); } // there is a case where the calculation time overflows the millisecond which makes // tests fail - const forgivingActualStart = Math.abs(newState.runtime.actualStart - firstStart); + const forgivingActualStart = Math.abs(newState.rundown.actualStart - firstStart); expect(forgivingActualStart).toBeLessThanOrEqual(1); // we are over-under, the difference between the schedule and the actual start const delayBefore = newState.clock - entries.event2.timeStart; - expect(newState.runtime.offsetAbs).toBe(delayBefore); + expect(newState.offset.absolute).toBe(delayBefore); // finish is the difference between the runtime and the schedule - expect(newState.runtime.expectedEnd).toBe(newState.runtime.offsetAbs + entries.event2.timeEnd); + expect(newState.offset.expectedRundownEnd).toBe(newState.offset.absolute + entries.event2.timeEnd); expect(newState.groupNow).toBeNull(); // 4. Add time addTime(10); newState = getState(); - if (newState.runtime.offsetAbs === null) { + if (newState.offset.absolute === null) { throw new Error('Value cannot be null at this stage'); } - expect(newState.runtime.offsetAbs).toBe(delayBefore + 10); - expect(newState.runtime.expectedEnd).toBe(entries.event2.timeEnd + newState.runtime.offsetAbs); + expect(newState.offset.absolute).toBe(delayBefore + 10); + expect(newState.offset.expectedRundownEnd).toBe(entries.event2.timeEnd + newState.offset.absolute); // 5. Stop event stop(); newState = getState(); - expect(newState.runtime.actualStart).toBeNull(); - expect(newState.runtime.offsetAbs).toBe(0); - expect(newState.runtime.expectedEnd).toBeNull(); + expect(newState.rundown.actualStart).toBeNull(); + expect(newState.offset.absolute).toBe(0); + expect(newState.offset.expectedRundownEnd).toBeNull(); }); }); @@ -332,7 +332,7 @@ describe('roll mode', () => { start(); const result = roll(rundown, metadata); expect(result).toStrictEqual({ eventId: '1', didStart: false }); - expect(getState().runtime.offsetAbs).toBe(-1000); + expect(getState().offset.absolute).toBe(-1000); }); }); @@ -356,29 +356,29 @@ describe('roll mode', () => { load(rundown.entries[1] as PlayableEvent, rundown, metadata); start(); // the current offset after manual play - const currentOffset = getState().runtime.offsetAbs; - let result = roll(rundown, metadata, getState().runtime.offsetAbs); + const currentOffset = getState().offset.absolute; + let result = roll(rundown, metadata, getState().offset.absolute); expect(result).toStrictEqual({ eventId: '1', didStart: false }); // the current offset should be maintain by roll mode when taking over from play - expect(getState().runtime.offsetAbs).toBe(currentOffset); + expect(getState().offset.absolute).toBe(currentOffset); vi.setSystemTime('jan 1 00:00:01'); - result = roll(rundown, metadata, getState().runtime.offsetAbs); + result = roll(rundown, metadata, getState().offset.absolute); expect(result).toStrictEqual({ eventId: '2', didStart: true }); - expect(getState().runtime.offsetAbs).toBe(-1000); + expect(getState().offset.absolute).toBe(-1000); vi.setSystemTime('jan 1 00:00:02'); - result = roll(rundown, metadata, getState().runtime.offsetAbs); + result = roll(rundown, metadata, getState().offset.absolute); expect(result).toStrictEqual({ eventId: '3', didStart: true }); - expect(getState().runtime.offsetAbs).toBe(-1000); + expect(getState().offset.absolute).toBe(-1000); vi.useRealTimers(); }); }); }); -describe('loadGroup', () => { - test('from no-group to a group will clear startedAt', () => { +describe('loadGroupFlagAndEnd()', () => { + test('from no-group to a group will clear expectedGroupEnd', () => { const rundown = makeRundown({ entries: { 0: makeOntimeEvent({ id: '0', parent: null }), @@ -393,6 +393,7 @@ describe('loadGroup', () => { const state = { groupNow: null, eventNow: rundown.entries[11], + offset: { expectedGroupEnd: 123 }, } as RuntimeState; const metadata = { playableEventOrder: ['0', '11', '3'], flags: ['1'] } as RundownMetadata; @@ -400,12 +401,13 @@ describe('loadGroup', () => { loadGroupFlagAndEnd(rundown, metadata, 2, state); expect(state).toMatchObject({ - groupNow: { id: rundown.entries[1].id, startedAt: null }, + groupNow: rundown.entries[1], + offset: { expectedGroupEnd: null }, eventNow: rundown.entries[11], }); }); - test('from a group to a different group will clear startedAt', () => { + test('from a group to a different group will clear expectedGroupEnd', () => { const rundown = makeRundown({ entries: { 0: makeOntimeEvent({ id: '0', parent: null }), @@ -418,7 +420,8 @@ describe('loadGroup', () => { }); const state = { - groupNow: { id: rundown.entries[1].id, startedAt: 123 }, + groupNow: rundown.entries[1], + offset: { expectedGroupEnd: 123 }, eventNow: rundown.entries[22], } as RuntimeState; @@ -427,12 +430,13 @@ describe('loadGroup', () => { loadGroupFlagAndEnd(rundown, metadata, 1, state); expect(state).toMatchObject({ - groupNow: { id: rundown.entries[2].id, startedAt: null }, + groupNow: rundown.entries[2], + offset: { expectedGroupEnd: null }, eventNow: rundown.entries[22], }); }); - test('from group to a no-group will clear startedAt', () => { + test('from group to a no-group will clear expectedGroupEnd', () => { const rundown = makeRundown({ entries: { 0: makeOntimeEvent({ id: '0', parent: null }), @@ -445,10 +449,8 @@ describe('loadGroup', () => { }); const state = { - groupNow: { - id: rundown.entries[1].id, - startedAt: 123, - }, + groupNow: rundown.entries[1], + offset: { expectedGroupEnd: 123 }, eventNow: rundown.entries[0], } as RuntimeState; @@ -458,35 +460,11 @@ describe('loadGroup', () => { expect(state).toMatchObject({ groupNow: null, + offset: { expectedGroupEnd: null }, eventNow: rundown.entries[0], }); }); - test('from a group to same group will keep startedAt', () => { - const rundown = makeRundown({ - entries: { - 0: makeOntimeGroup({ id: '0', entries: ['1', '2'] }), - 1: makeOntimeEvent({ id: '1', parent: '0' }), - 2: makeOntimeEvent({ id: '2', parent: '0' }), - }, - order: ['0'], - }); - - const state = { - groupNow: { id: rundown.entries[0].id, startedAt: 123 }, - eventNow: rundown.entries[2], - } as RuntimeState; - - const metadata = { playableEventOrder: ['1', '2'], flags: ['1'] } as RundownMetadata; - - loadGroupFlagAndEnd(rundown, metadata, 0, state); - - expect(state).toMatchObject({ - groupNow: { id: rundown.entries[0].id, startedAt: 123 }, - eventNow: rundown.entries[2], - }); - }); - test('from no-group to no-group will keep startedAt', () => { const rundown = makeRundown({ entries: { diff --git a/apps/server/src/stores/runtimeState.ts b/apps/server/src/stores/runtimeState.ts index 7cdd07ca7..9af939f11 100644 --- a/apps/server/src/stores/runtimeState.ts +++ b/apps/server/src/stores/runtimeState.ts @@ -1,6 +1,4 @@ import { - GroupState, - UpcomingEntry, isOntimeEvent, MaybeNumber, MaybeString, @@ -10,10 +8,11 @@ import { PlayableEvent, Playback, Rundown, - Runtime, + Offset, runtimeStorePlaceholder, TimerPhase, TimerState, + RundownState, } from 'ontime-types'; import { calculateDuration, @@ -36,13 +35,13 @@ type ExpectedMetadata = { event: OntimeEvent; accumulatedGap: number; isLinkedTo export type RuntimeState = { clock: number; // realtime clock - groupNow: GroupState | null; - groupNext: MaybeString; - nextFlag: UpcomingEntry | null; + groupNow: OntimeGroup | null; eventNow: PlayableEvent | null; eventNext: PlayableEvent | null; - runtime: Runtime; + eventFlag: PlayableEvent | null; + offset: Offset; timer: TimerState; + rundown: RundownState; // private properties of the timer calculations _timer: { forceFinish: MaybeNumber; // whether we should declare an event as finished, will contain the finish time @@ -61,12 +60,12 @@ export type RuntimeState = { const runtimeState: RuntimeState = { clock: timeNow(), groupNow: null, - groupNext: null, - nextFlag: null, eventNow: null, eventNext: null, - runtime: { ...runtimeStorePlaceholder.runtime }, + eventFlag: null, + offset: { ...runtimeStorePlaceholder.offset }, timer: { ...runtimeStorePlaceholder.timer }, + rundown: { ...runtimeStorePlaceholder.rundown }, _timer: { forceFinish: null, pausedAt: null, @@ -87,7 +86,10 @@ export function getState(): Readonly { ...runtimeState, eventNow: runtimeState.eventNow ? { ...runtimeState.eventNow } : null, eventNext: runtimeState.eventNext ? { ...runtimeState.eventNext } : null, - runtime: { ...runtimeState.runtime }, + eventFlag: runtimeState.eventFlag ? { ...runtimeState.eventFlag } : null, + groupNow: runtimeState.groupNow ? { ...runtimeState.groupNow } : null, + offset: { ...runtimeState.offset }, + rundown: { ...runtimeState.rundown }, timer: { ...runtimeState.timer }, _timer: { ...runtimeState._timer }, _rundown: { ...runtimeState._rundown }, @@ -101,13 +103,16 @@ export function clearEventData() { runtimeState.eventNow = null; runtimeState.eventNext = null; - runtimeState.runtime.offsetAbs = 0; - runtimeState.runtime.offsetRel = 0; - runtimeState.runtime.expectedEnd = null; - runtimeState.runtime.selectedEventIndex = null; + runtimeState.offset.absolute = 0; + runtimeState.offset.relative = 0; + runtimeState.offset.expectedFlagStart = null; + + runtimeState.rundown.selectedEventIndex = null; //TODO: is there any ExpectedMetadata stuff we need to clear here - if (runtimeState.groupNow) runtimeState.groupNow.expectedEnd = null; + runtimeState.offset.expectedGroupEnd = null; + runtimeState.offset.expectedFlagStart = null; + runtimeState.offset.expectedRundownEnd = null; runtimeState.timer.playback = Playback.Stop; runtimeState.clock = timeNow(); @@ -124,20 +129,22 @@ export function clearEventData() { export function clearState() { runtimeState.eventNow = null; runtimeState.eventNext = null; - - runtimeState.groupNow = null; - runtimeState.groupNext = null; - runtimeState._group = null; - - runtimeState.nextFlag = null; + runtimeState.eventFlag = null; runtimeState._flag = null; - runtimeState.runtime.offsetAbs = 0; - runtimeState.runtime.offsetRel = 0; - runtimeState.runtime.actualStart = null; - runtimeState.runtime.expectedEnd = null; + runtimeState.groupNow = null; + runtimeState._group = null; + + 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._end = null; - runtimeState.runtime.selectedEventIndex = null; runtimeState.timer.playback = Playback.Stop; runtimeState.clock = timeNow(); @@ -168,25 +175,23 @@ function patchTimer(newState: Partial) { } } -type RundownData = { +/** + * Utility, allows updating data derived from the rundown + * @param playableRundown + */ +export function updateRundownData(rundownData: { numEvents: number; // length of rundown filtered for timed events firstStart: MaybeNumber; lastEnd: MaybeNumber; totalDelay: number; totalDuration: number; -}; - -/** - * Utility, allows updating data derived from the rundown - * @param playableRundown - */ -export function updateRundownData(rundownData: RundownData) { +}) { // we keep this in private state since there is no UI use case for it runtimeState._rundown.totalDelay = rundownData.totalDelay; - runtimeState.runtime.numEvents = rundownData.numEvents; - runtimeState.runtime.plannedStart = rundownData.firstStart; - runtimeState.runtime.plannedEnd = + runtimeState.rundown.numEvents = rundownData.numEvents; + runtimeState.rundown.plannedStart = rundownData.firstStart; + runtimeState.rundown.plannedEnd = rundownData.firstStart === null ? null : rundownData.firstStart + rundownData.totalDuration; getExpectedTimes(); } @@ -222,22 +227,19 @@ export function load( runtimeState.timer.playback = Playback.Armed; runtimeState.timer.duration = calculateDuration(event.timeStart, event.timeEnd); runtimeState.timer.current = getCurrent(runtimeState); - runtimeState.runtime.numEvents = metadata.timedEventOrder.length; + runtimeState.rundown.numEvents = metadata.timedEventOrder.length; // patch with potential provided data if (initialData) { patchTimer(initialData); const firstStart = initialData?.firstStart; if (firstStart === null || typeof firstStart === 'number') { - runtimeState.runtime.actualStart = firstStart; - const { offsetAbs, offsetRel } = getRuntimeOffset(runtimeState); - runtimeState.runtime.offsetAbs = offsetAbs; - runtimeState.runtime.offsetRel = offsetRel; + runtimeState.rundown.actualStart = firstStart; + const { absolute, relative } = getRuntimeOffset(runtimeState); + runtimeState.offset.absolute = absolute; + runtimeState.offset.relative = relative; getExpectedTimes(); } - if (typeof initialData.groupStartAt === 'number' && runtimeState.groupNow) { - runtimeState.groupNow.startedAt = initialData.groupStartAt; - } } return event.id === runtimeState.eventNow?.id; } @@ -248,17 +250,17 @@ export function load( export function loadNow( rundown: Rundown, metadata: RundownMetadata, - eventIndex: MaybeNumber = runtimeState.runtime.selectedEventIndex, + eventIndex: MaybeNumber = runtimeState.rundown.selectedEventIndex, ) { if (eventIndex === null) { // reset the state to indicate there is no selection - runtimeState.runtime.selectedEventIndex = null; + runtimeState.rundown.selectedEventIndex = null; runtimeState.eventNow = null; return; } const event = rundown.entries[metadata.timedEventOrder[eventIndex]] as PlayableEvent; - runtimeState.runtime.selectedEventIndex = eventIndex; + runtimeState.rundown.selectedEventIndex = eventIndex; runtimeState.eventNow = event; } @@ -268,7 +270,7 @@ export function loadNow( export function loadNext( rundown: Rundown, metadata: RundownMetadata, - eventIndex: MaybeNumber = runtimeState.runtime.selectedEventIndex, + eventIndex: MaybeNumber = runtimeState.rundown.selectedEventIndex, ) { if (eventIndex === null) { // reset the state to indicate there is no future event @@ -314,7 +316,7 @@ export function updateLoaded(event?: PlayableEvent): string | undefined { // handle edge cases with roll if (runtimeState.timer.playback === Playback.Roll) { - const offsetClock = runtimeState.clock - runtimeState.runtime.offsetAbs; + const offsetClock = runtimeState.clock - 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) { @@ -358,13 +360,6 @@ export function updateAll(rundown: Rundown, metadata: RundownMetadata) { loadNext(rundown, metadata, eventNowIndex >= 0 ? eventNowIndex : undefined); updateLoaded(runtimeState.eventNow ?? undefined); loadGroupFlagAndEnd(rundown, metadata, eventNowIndex); - - // catch the edge case where the playing event is moved into a group - // if the group already has a start value then we dont overwrite it - // use the start value from the timer, so if the we are not running it will be set to null - if (runtimeState.groupNow && runtimeState.groupNow.startedAt === null) { - runtimeState.groupNow.startedAt = runtimeState.timer.startedAt; - } } export function start(state: RuntimeState = runtimeState): boolean { @@ -389,31 +384,26 @@ export function start(state: RuntimeState = runtimeState): boolean { state.timer.startedAt = state.clock; } - // update group start time - if (state.groupNow && state.groupNow.startedAt === null) { - state.groupNow.startedAt = state.clock; - } - state.timer.playback = Playback.Play; state.timer.expectedFinish = getExpectedFinish(state); state.timer.elapsed = 0; - if (state.runtime.actualStart === null) { - state.runtime.actualStart = state.clock; + if (state.rundown.actualStart === null) { + state.rundown.actualStart = state.clock; } // update timer phase runtimeState.timer.phase = getTimerPhase(runtimeState); // update offset - const { offsetAbs, offsetRel } = getRuntimeOffset(runtimeState); - runtimeState.runtime.offsetAbs = offsetAbs; - runtimeState.runtime.offsetRel = offsetRel; + const { absolute, relative } = getRuntimeOffset(runtimeState); + runtimeState.offset.absolute = absolute; + runtimeState.offset.relative = relative; // as long as there is a timer, we need an planned end // eslint-disable-next-line no-unused-labels -- dev code path DEV: { - if (state.runtime.plannedEnd === null) { + if (state.rundown.plannedEnd === null) { throw new Error('runtimeState.start: invalid state received'); } } @@ -477,9 +467,9 @@ export function addTime(amount: number) { runtimeState.timer.current += amount; // update runtime delays: over - under - const { offsetAbs, offsetRel } = getRuntimeOffset(runtimeState); - runtimeState.runtime.offsetAbs = offsetAbs; - runtimeState.runtime.offsetRel = offsetRel; + const { absolute, relative } = getRuntimeOffset(runtimeState); + runtimeState.offset.absolute = absolute; + runtimeState.offset.relative = relative; getExpectedTimes(); return true; @@ -524,9 +514,9 @@ export function update(): UpdateResult { runtimeState.timer.elapsed = runtimeState.timer.duration - runtimeState.timer.current; // update runtime, needs up-to-date timer state - const { offsetAbs, offsetRel } = getRuntimeOffset(runtimeState); - runtimeState.runtime.offsetAbs = offsetAbs; - runtimeState.runtime.offsetRel = offsetRel; + const { absolute, relative } = getRuntimeOffset(runtimeState); + runtimeState.offset.absolute = absolute; + runtimeState.offset.relative = relative; const finishedNow = Boolean(runtimeState._timer.forceFinish) || @@ -556,7 +546,7 @@ export function update(): UpdateResult { } // account for offset - const offsetClock = runtimeState.clock + runtimeState.runtime.offsetAbs; + const offsetClock = runtimeState.clock + runtimeState.offset.absolute; runtimeState.timer.phase = TimerPhase.Pending; if (hasCrossedMidnight) { @@ -576,7 +566,7 @@ export function roll( offset = 0, ): { eventId: MaybeString; didStart: boolean } { // 1. if an event is running, we simply take over the playback - if (runtimeState.timer.playback === Playback.Play && runtimeState.runtime.selectedEventIndex !== null) { + if (runtimeState.timer.playback === Playback.Play && runtimeState.rundown.selectedEventIndex !== null) { runtimeState.timer.playback = Playback.Roll; return { eventId: runtimeState.eventNow?.id ?? null, didStart: false }; } @@ -590,7 +580,7 @@ export function roll( } } - runtimeState.runtime.offsetAbs = offset; + runtimeState.offset.absolute = offset; runtimeState.timer.playback = Playback.Roll; // account for event that finishes the day after @@ -601,7 +591,7 @@ export function roll( runtimeState.timer.expectedFinish = normalisedEndTime; //account for offset - const offsetClock = runtimeState.clock - runtimeState.runtime.offsetAbs; + const offsetClock = runtimeState.clock - runtimeState.offset.absolute; // state catch up runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, normalisedEndTime); @@ -613,12 +603,8 @@ export function roll( if (isNow) { runtimeState.timer.startedAt = runtimeState.clock; - // update runtime - if (runtimeState.groupNow && runtimeState.groupNow.startedAt === null) { - runtimeState.groupNow.startedAt = runtimeState.clock; - } - if (!runtimeState.runtime.actualStart) { - runtimeState.runtime.actualStart = runtimeState.clock; + if (runtimeState.rundown.actualStart === null) { + runtimeState.rundown.actualStart = runtimeState.clock; } runtimeState.timer.secondaryTimer = null; } else { @@ -639,8 +625,8 @@ export function roll( clearEventData(); //account for offset but we only keep it if passed to us - runtimeState.runtime.offsetAbs = offset; - const offsetClock = runtimeState.clock - runtimeState.runtime.offsetAbs; + runtimeState.offset.absolute = offset; + const offsetClock = runtimeState.clock - runtimeState.offset.absolute; const { index, isPending } = loadRoll(rundown, metadata, offsetClock); @@ -651,7 +637,7 @@ export function roll( // update roll state runtimeState.timer.playback = Playback.Roll; - runtimeState.runtime.numEvents = metadata.timedEventOrder.length; + runtimeState.rundown.numEvents = metadata.timedEventOrder.length; // in roll mode spec, there should always be something to load // as long as playableEvents is not empty @@ -676,12 +662,6 @@ export function roll( } // there is something to run, load event - - // update runtime - if (runtimeState.groupNow && runtimeState.groupNow.startedAt === null) { - runtimeState.groupNow.startedAt = runtimeState.clock; - } - // event will finish on time // account for event that finishes the day after const endTime = @@ -700,25 +680,26 @@ export function roll( runtimeState.timer.elapsed = 0; // update runtime - runtimeState.runtime.actualStart = runtimeState.clock; + runtimeState.rundown.actualStart = runtimeState.clock; return { eventId: runtimeState.eventNow.id, didStart: true }; } /** * calculates and sets values directly in state - * - runtime.expectedEnd - * - groupNow.expectedEnd - * - nextFlag.expectedStart + * - offset.expectedRundownEnd + * - offset.expectedGroupEnd + * - offset.expectedFlagStart */ function getExpectedTimes(state = runtimeState) { - const { offsetMode, offsetAbs, offsetRel, plannedStart, actualStart } = state.runtime; + const { offset } = state; + const { plannedStart, actualStart } = state.rundown; const { eventNow } = state; if (!eventNow) return; - state.runtime.expectedEnd = null; + state.offset.expectedRundownEnd = null; if (state.groupNow) { - state.groupNow.expectedEnd = null; + state.offset.expectedGroupEnd = null; const { _group } = state; if (_group !== null) { const { event: lastEvent, accumulatedGap, isLinkedToLoaded } = _group; @@ -726,17 +707,17 @@ function getExpectedTimes(state = runtimeState) { currentDay: eventNow.dayOffset, totalGap: accumulatedGap, isLinkedToLoaded, - offsetMode, - offset: offsetMode === OffsetMode.Absolute ? offsetAbs : offsetRel, + mode: offset.mode, + offset: offset.mode === OffsetMode.Absolute ? offset.absolute : offset.relative, plannedStart, actualStart, }); - state.groupNow.expectedEnd = lastEventExpectedStart + lastEvent.duration; + state.offset.expectedGroupEnd = lastEventExpectedStart + lastEvent.duration; } } - if (state.nextFlag) { - state.nextFlag.expectedStart = null; + if (state.eventFlag) { + state.offset.expectedFlagStart = null; const { _flag } = state; if (_flag) { const { event, accumulatedGap, isLinkedToLoaded } = _flag; @@ -744,12 +725,12 @@ function getExpectedTimes(state = runtimeState) { currentDay: eventNow.dayOffset, totalGap: accumulatedGap, isLinkedToLoaded, - offsetMode, - offset: offsetMode === OffsetMode.Absolute ? offsetAbs : offsetRel, + mode: offset.mode, + offset: offset.mode === OffsetMode.Absolute ? offset.absolute : offset.relative, plannedStart, actualStart, }); - state.nextFlag.expectedStart = expectedStart; + state.offset.expectedFlagStart = expectedStart; } } @@ -759,14 +740,14 @@ function getExpectedTimes(state = runtimeState) { currentDay: eventNow.dayOffset, totalGap: accumulatedGap, isLinkedToLoaded, - offsetMode, - offset: offsetMode === OffsetMode.Absolute ? offsetAbs : offsetRel, + mode: offset.mode, + offset: offset.mode === OffsetMode.Absolute ? offset.absolute : offset.relative, plannedStart, actualStart, }); - state.runtime.expectedEnd = expectedStart + event.duration; + state.offset.expectedRundownEnd = expectedStart + event.duration; } else { - state.runtime.expectedEnd = null; + state.offset.expectedRundownEnd = null; } } @@ -790,7 +771,6 @@ export function loadGroupFlagAndEnd( // if we don't have a any flags in the rundown then no need to look for it let foundFlag = !flagsPresent; - let foundNextGroup = false; // if we don't have a last event for the group there is no need to find its end time let foundGroupEnd = lastEventInGroup === null; @@ -809,7 +789,7 @@ export function loadGroupFlagAndEnd( // and the loaded event is not allowed to be the next flag if (!foundFlag && metadata.flags.includes(entry.id)) { foundFlag = true; - state.nextFlag = { id: entry.id, expectedStart: null }; + state.eventFlag = entry as PlayableEvent; // we know it is playable as it is coming from the playableEventOrder list state._flag = { event: entry, isLinkedToLoaded, accumulatedGap }; } } @@ -818,11 +798,6 @@ export function loadGroupFlagAndEnd( foundGroupEnd = true; state._group = { event: lastEventInGroup, isLinkedToLoaded, accumulatedGap }; } - - if (!foundNextGroup && entry.parent !== currentGroupId) { - foundNextGroup = true; - state.groupNext = entry.parent; - } } } @@ -832,26 +807,24 @@ export function loadGroupFlagAndEnd( state._end = { event: lastEvent, isLinkedToLoaded, accumulatedGap }; } - if (!foundFlag) state.nextFlag = null; + if (!foundFlag) state.eventFlag = null; - if (currentGroupId === null) { - state.groupNow = null; - } else if ((state.groupNow != null && state.groupNow.id != currentGroupId) || state.groupNow == null) { + if ((state.groupNow?.id ?? null) !== currentGroupId) { // we went into a new group - and it is different from the one we might have come from - // the id is set here, the start time is set when starting events - state.groupNow = { id: currentGroupId, startedAt: null, expectedEnd: null }; + state.offset.expectedGroupEnd = null; } + + state.groupNow = currentGroupId ? (entries[currentGroupId] as OntimeGroup) : null; } const resetMetaData = (state = runtimeState) => { state.groupNow = null; - state.groupNext = null; state._group = null; - state.nextFlag = null; + state.eventFlag = null; state._flag = null; state._end = null; }; export function setOffsetMode(mode: OffsetMode) { - runtimeState.runtime.offsetMode = mode; + runtimeState.offset.mode = mode; } diff --git a/packages/types/src/definitions/runtime/CurrentGroupState.type.ts b/packages/types/src/definitions/runtime/CurrentGroupState.type.ts deleted file mode 100644 index 9405ec088..000000000 --- a/packages/types/src/definitions/runtime/CurrentGroupState.type.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { MaybeNumber } from '../../utils/utils.type.js'; -import type { EntryId } from '../core/OntimeEntry.js'; - -export type GroupState = { - id: EntryId; - startedAt: MaybeNumber; - expectedEnd: MaybeNumber; -}; - -export type UpcomingEntry = { - id: EntryId; - expectedStart: MaybeNumber; -}; diff --git a/packages/types/src/definitions/runtime/Offset.type.ts b/packages/types/src/definitions/runtime/Offset.type.ts new file mode 100644 index 000000000..85c6581f1 --- /dev/null +++ b/packages/types/src/definitions/runtime/Offset.type.ts @@ -0,0 +1,15 @@ +import type { MaybeNumber } from '../../utils/utils.type.js'; + +export enum OffsetMode { + Absolute = 'absolute', + Relative = 'relative', +} + +export type Offset = { + absolute: number; // a positive value means that we are in over time aka behind schedule + relative: number; + mode: OffsetMode; + expectedGroupEnd: MaybeNumber; + expectedRundownEnd: MaybeNumber; + expectedFlagStart: MaybeNumber; +}; diff --git a/packages/types/src/definitions/runtime/RundownState.type.ts b/packages/types/src/definitions/runtime/RundownState.type.ts new file mode 100644 index 000000000..e10e50b9a --- /dev/null +++ b/packages/types/src/definitions/runtime/RundownState.type.ts @@ -0,0 +1,9 @@ +import type { MaybeNumber } from '../../utils/utils.type.js'; + +export type RundownState = { + selectedEventIndex: MaybeNumber; + numEvents: number; + plannedStart: MaybeNumber; + actualStart: MaybeNumber; + plannedEnd: MaybeNumber; +}; diff --git a/packages/types/src/definitions/runtime/Runtime.type.ts b/packages/types/src/definitions/runtime/Runtime.type.ts deleted file mode 100644 index 4a0ea88ef..000000000 --- a/packages/types/src/definitions/runtime/Runtime.type.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { MaybeNumber } from '../../utils/utils.type.js'; - -export enum OffsetMode { - Absolute = 'absolute', - Relative = 'relative', -} - -export type Runtime = { - selectedEventIndex: MaybeNumber; - numEvents: number; - offsetAbs: number; // a positive value means that we are in over time aka behind schedule - offsetRel: number; - plannedStart: MaybeNumber; - actualStart: MaybeNumber; - plannedEnd: MaybeNumber; - expectedEnd: MaybeNumber; - offsetMode: OffsetMode; -}; diff --git a/packages/types/src/definitions/runtime/RuntimeStore.ts b/packages/types/src/definitions/runtime/RuntimeStore.ts index 1167f62c7..5f0bd8de9 100644 --- a/packages/types/src/definitions/runtime/RuntimeStore.ts +++ b/packages/types/src/definitions/runtime/RuntimeStore.ts @@ -1,6 +1,6 @@ import { SimpleDirection, SimplePlayback } from './AuxTimer.type.js'; +import { OffsetMode } from './Offset.type.js'; import { Playback } from './Playback.type.js'; -import { OffsetMode } from './Runtime.type.js'; import type { RuntimeStore } from './RuntimeStore.type.js'; import { TimerPhase } from './TimerState.type.js'; @@ -27,22 +27,25 @@ export const runtimeStorePlaceholder: Readonly = { }, secondary: '', }, - runtime: { + rundown: { selectedEventIndex: null, // changes if rundown changes or we load a new event numEvents: 0, // change initiated by user - offsetAbs: 0, // changes at runtime - offsetRel: 0, // changes at runtime plannedStart: 0, // only changes if event changes plannedEnd: 0, // only changes if event changes, overflows over dayInMs actualStart: null, // set once we start the timer - expectedEnd: null, // changes with runtime, based on offset, overflows over dayInMs - offsetMode: OffsetMode.Absolute, + }, + offset: { + absolute: 0, // changes at runtime + relative: 0, // changes at runtime + mode: OffsetMode.Absolute, + expectedFlagStart: null, + expectedGroupEnd: null, + expectedRundownEnd: null, }, groupNow: null, - groupNext: null, - nextFlag: null, eventNow: null, eventNext: null, + eventFlag: null, auxtimer1: { current: 0, direction: SimpleDirection.CountUp, diff --git a/packages/types/src/definitions/runtime/RuntimeStore.type.ts b/packages/types/src/definitions/runtime/RuntimeStore.type.ts index f863d1d5a..d2517c3f6 100644 --- a/packages/types/src/definitions/runtime/RuntimeStore.type.ts +++ b/packages/types/src/definitions/runtime/RuntimeStore.type.ts @@ -1,9 +1,8 @@ -import type { MaybeString } from '../../utils/utils.type.js'; -import type { OntimeEvent } from '../core/OntimeEntry.js'; +import type { OntimeEvent, OntimeGroup } from '../core/OntimeEntry.js'; import type { SimpleTimerState } from './AuxTimer.type.js'; -import type { GroupState, UpcomingEntry } from './CurrentGroupState.type.js'; import type { MessageState } from './MessageControl.type.js'; -import type { Runtime } from './Runtime.type.js'; +import type { Offset } from './Offset.type.js'; +import type { RundownState } from './RundownState.type.js'; import type { TimerState } from './TimerState.type.js'; export type RuntimeStore = { @@ -15,13 +14,16 @@ export type RuntimeStore = { message: MessageState; // rundown data - runtime: Runtime; + rundown: RundownState; + + // runtime + offset: Offset; + + // relevant entries eventNow: OntimeEvent | null; eventNext: OntimeEvent | null; - - groupNow: GroupState | null; - groupNext: MaybeString; - nextFlag: UpcomingEntry | null; + eventFlag: OntimeEvent | null; + groupNow: OntimeGroup | null; // extra timers auxtimer1: SimpleTimerState; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 3023cc1a2..2f65df8e1 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -96,12 +96,12 @@ export { Playback } from './definitions/runtime/Playback.type.js'; export { TimerLifeCycle, timerLifecycleValues } from './definitions/core/TimerLifecycle.type.js'; export type { TimerMessage, MessageState, SecondarySource } from './definitions/runtime/MessageControl.type.js'; -export type { Runtime } from './definitions/runtime/Runtime.type.js'; -export { OffsetMode } from './definitions/runtime/Runtime.type.js'; +export type { RundownState } from './definitions/runtime/RundownState.type.js'; +export type { Offset } from './definitions/runtime/Offset.type.js'; +export { OffsetMode } from './definitions/runtime/Offset.type.js'; export type { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js'; export { runtimeStorePlaceholder } from './definitions/runtime/RuntimeStore.js'; export { type TimerState, TimerPhase } from './definitions/runtime/TimerState.type.js'; -export type { GroupState, UpcomingEntry } from './definitions/runtime/CurrentGroupState.type.js'; // ---> Extra Timer export { type SimpleTimerState, SimplePlayback, SimpleDirection } from './definitions/runtime/AuxTimer.type.js'; diff --git a/packages/utils/src/date-utils/getExpectedStart.test.ts b/packages/utils/src/date-utils/getExpectedStart.test.ts index 30082f87c..a4ed5d4a6 100644 --- a/packages/utils/src/date-utils/getExpectedStart.test.ts +++ b/packages/utils/src/date-utils/getExpectedStart.test.ts @@ -15,7 +15,7 @@ describe('getExpectedStart()', () => { currentDay: 0, totalGap: 0, offset: 0, - offsetMode: OffsetMode.Absolute, + mode: OffsetMode.Absolute, actualStart: null, plannedStart: null, }; @@ -34,7 +34,7 @@ describe('getExpectedStart()', () => { currentDay: 0, totalGap: 0, offset: 20, - offsetMode: OffsetMode.Absolute, + mode: OffsetMode.Absolute, actualStart: null, plannedStart: null, }; @@ -53,7 +53,7 @@ describe('getExpectedStart()', () => { currentDay: 0, totalGap: 0, offset: -10, - offsetMode: OffsetMode.Absolute, + mode: OffsetMode.Absolute, actualStart: null, plannedStart: null, }; @@ -72,7 +72,7 @@ describe('getExpectedStart()', () => { currentDay: 0, totalGap: 20, offset: 20, - offsetMode: OffsetMode.Absolute, + mode: OffsetMode.Absolute, actualStart: null, plannedStart: null, }; @@ -91,7 +91,7 @@ describe('getExpectedStart()', () => { currentDay: 0, totalGap: 10, offset: 20, - offsetMode: OffsetMode.Absolute, + mode: OffsetMode.Absolute, actualStart: 0, plannedStart: 0, }; @@ -109,7 +109,7 @@ describe('getExpectedStart()', () => { actualStart: 100, plannedStart: 0, offset: 0, - offsetMode: OffsetMode.Relative, + mode: OffsetMode.Relative, }; const timeStartEvent2 = 10; @@ -189,7 +189,7 @@ describe('getExpectedStart()', () => { actualStart: 100, plannedStart: 0, offset: 0, - offsetMode: OffsetMode.Relative, + mode: OffsetMode.Relative, }; expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(120); @@ -213,7 +213,7 @@ describe('getExpectedStart()', () => { actualStart: 100, plannedStart: 0, offset: 0, - offsetMode: OffsetMode.Relative, + mode: OffsetMode.Relative, }; expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(120); @@ -236,7 +236,7 @@ describe('getExpectedStart()', () => { actualStart: 100, plannedStart: 0, offset: 0, - offsetMode: OffsetMode.Relative, + mode: OffsetMode.Relative, }; // this event will start the current day @@ -270,7 +270,7 @@ describe('getExpectedStart()', () => { actualStart: 100, plannedStart: 0, offset: 0, - offsetMode: OffsetMode.Relative, + mode: OffsetMode.Relative, isLinkedToLoaded: false, }; diff --git a/packages/utils/src/date-utils/getExpectedStart.ts b/packages/utils/src/date-utils/getExpectedStart.ts index 7fb9d3395..c51bb44ca 100644 --- a/packages/utils/src/date-utils/getExpectedStart.ts +++ b/packages/utils/src/date-utils/getExpectedStart.ts @@ -19,13 +19,13 @@ export function getExpectedStart( totalGap: number; isLinkedToLoaded: boolean; offset: number; - offsetMode: OffsetMode; + mode: OffsetMode; actualStart: MaybeNumber; plannedStart: MaybeNumber; }, ): number { const { timeStart, dayOffset, delay } = event; - const { currentDay, totalGap, isLinkedToLoaded, offset, offsetMode, actualStart, plannedStart } = state; + const { currentDay, totalGap, isLinkedToLoaded, offset, mode, actualStart, plannedStart } = state; //How many days from the currently running event to this one const relativeDayOffset = dayOffset - currentDay; @@ -37,7 +37,7 @@ export function getExpectedStart( let relativeStartOffset = 0; - if (offsetMode === OffsetMode.Relative) { + if (mode === OffsetMode.Relative) { relativeStartOffset = (actualStart ?? 0) - (plannedStart ?? 0); }