Files
ontime/apps/server/src/services/runtime-service/runtime.utils.ts
T
Carlos Valente 1849b4d39f Deps migration (#1988)
* chore: migrate eslint to oxlint

* chore: migrate prettier to oxfmt

* chore: migrate typescript

* chore: toThrow should have a expected value

* chore: cast test value as Day

* chore: small title fix

* chore: mocks should be hoisted

* chore: incorrect async useage

* chore: test should be inside description

* chore: test sohuld include an expeced

* chore: oxfmt

---------

Co-authored-by: alex-Arc <omnivox@LAPTOP-RC5SNBVV.localdomain>
2026-03-08 16:22:12 +01:00

167 lines
5.1 KiB
TypeScript

import { deepEqual } from 'fast-equals';
import {
EntryId,
MaybeNumber,
Offset,
OntimeEvent,
Rundown,
TimerState,
TimerType,
isOntimeEvent,
isPlayableEvent,
} from 'ontime-types';
import { millisToSeconds } from 'ontime-utils';
export function isNewSecond(
previousValue: MaybeNumber | undefined,
currentValue: MaybeNumber | undefined,
direction: TimerType.CountDown | TimerType.CountUp = TimerType.CountDown,
) {
return millisToSeconds(currentValue ?? null, direction) !== millisToSeconds(previousValue ?? null, direction);
}
/**
* Checks whether we should update the clock value
* - we have rolled into a new seconds unit
*/
export function getShouldClockUpdate(previousUpdate: number, now: number): boolean {
const newSeconds = millisToSeconds(now, TimerType.CountUp) !== millisToSeconds(previousUpdate, TimerType.CountUp);
return newSeconds;
}
/**
* Checks whether we should update the timer values
* - `current` and `secondaryTimer` trigger on seconds roll over
* - the rest trigger on any change
* - `elapsed` and `expectedFinish` is not checked
*/
export function getShouldTimerUpdate(previousValue: TimerState | undefined, currentValue: TimerState): boolean {
if (previousValue === undefined) return true;
return (
// current timer value
isNewSecond(previousValue.current, currentValue.current) ||
//secondary timer value, when in pre-roll
isNewSecond(previousValue.secondaryTimer, currentValue.secondaryTimer) ||
// other timer values that could have changed
previousValue.addedTime !== currentValue.addedTime ||
previousValue.duration !== currentValue.duration ||
previousValue.phase !== currentValue.phase ||
previousValue.playback !== currentValue.playback ||
previousValue.startedAt !== currentValue.startedAt
// elapsed - this would be the direct invert of current value so no need to check
// expectedFinish - this will be moved out by the current value going into over time, no need to check
);
}
/**
* Checks whether we should update the offset values
* - `mode` triggers update
* - `absolute`, `relative`, `expected**End` are ticked with `didDependencyUpdate`
*/
export function getShouldOffsetUpdate(
previousValue: Offset | undefined,
currentValue: Offset,
didDependencyUpdate: boolean,
): boolean {
if (previousValue === undefined) return true;
if (previousValue.mode !== currentValue.mode) return true;
return didDependencyUpdate && !deepEqual(previousValue, currentValue);
}
/**
* finds the previous playable event, if it exists
*/
export function findPreviousPlayableId(playableEventsOrder: EntryId[], currentEventId?: string): EntryId | undefined {
if (!playableEventsOrder.length) {
return;
}
// if there is no event running, go to first
if (!currentEventId) {
return getFirstPlayableId(playableEventsOrder);
}
const currentIndex = playableEventsOrder.findIndex((eventId) => eventId === currentEventId);
if (currentIndex < 1) {
return getFirstPlayableId(playableEventsOrder);
}
return playableEventsOrder.at(currentIndex - 1);
}
/**
* finds the next event playable event, if it exists
*/
export function findNextPlayableId(playableEventsOrder: EntryId[], currentEventId?: string): EntryId | undefined {
if (!playableEventsOrder.length) {
return;
}
// if there is no event running, go to first
if (!currentEventId) {
return getFirstPlayableId(playableEventsOrder);
}
const currentIndex = playableEventsOrder.findIndex((eventId) => eventId === currentEventId);
if (currentIndex === -1 || currentIndex >= playableEventsOrder.length - 1) {
return getFirstPlayableId(playableEventsOrder);
}
return playableEventsOrder.at(currentIndex + 1);
}
/**
* returns next event that matches a given cue
* then loops from the start
*/
export function findNextPlayableWithCue(
rundown: Rundown,
timedEventsOrder: EntryId[],
targetCue: string,
currentEventIndex = 0,
allowCurrent = false,
): OntimeEvent | undefined {
const startFromIndex = allowCurrent ? currentEventIndex : currentEventIndex + 1;
for (let i = startFromIndex; i < timedEventsOrder.length; i++) {
const eventId = timedEventsOrder[i];
const event = rundown.entries[eventId];
if (isOntimeEvent(event) && isPlayableEvent(event) && event.cue === targetCue) {
return event;
}
}
for (let i = 0; i < startFromIndex; i++) {
const eventId = timedEventsOrder[i];
const event = rundown.entries[eventId];
if (isOntimeEvent(event) && isPlayableEvent(event) && event.cue === targetCue) {
return event;
}
}
}
/**
* Utility returns the first playable event in rundown, if it exists
*/
export function getFirstPlayableId(playableOrder: EntryId[]): EntryId | undefined {
return playableOrder.at(0);
}
/**
* This is a utility function to return an event at a given index
* It uses the timedEventOrder so that the index is the same as the one in the UI
*/
export function getEventAtIndex(
rundown: Rundown,
timedEventOrder: EntryId[],
eventIndex: number,
): OntimeEvent | undefined {
const eventId = timedEventOrder[eventIndex];
if (!eventId) {
return undefined;
}
return rundown.entries[eventId] as OntimeEvent | undefined;
}