refactor: migrate custom fields to transactions

refactor: extract functions to api domain

refactor: strict custom field parsing

refactor: remove rundown cache utilities

refactor: directory restructure
This commit is contained in:
Carlos Valente
2025-06-06 21:08:30 +02:00
committed by arc-alex
parent f3b4ea0155
commit 2498e59156
75 changed files with 2060 additions and 2480 deletions
@@ -23,23 +23,22 @@ import { eventStore } from '../../stores/EventStore.js';
import { triggerReportEntry } from '../../api-data/report/report.service.js';
import { timerConfig } from '../../setup/config.js';
import { triggerAutomations } from '../../api-data/automation/automation.service.js';
import { getCurrentRundown } from '../../api-data/rundown/rundown.dao.js';
import { getCurrentRundown, getEntryWithId, getRundownMetadata } from '../../api-data/rundown/rundown.dao.js';
import { EventTimer } from '../EventTimer.js';
import { RestorePoint, restoreService } from '../RestoreService.js';
import {
findNext,
findPrevious,
getEventAtIndex,
getNextEventWithCue,
getEntryWithId,
getTimedEvents,
getRundownData,
} from '../rundown-service/rundownUtils.js';
import { skippedOutOfEvent } from '../timerUtils.js';
import { getEventOrder } from '../rundown-service/rundownCache.js';
import { getForceUpdate, getShouldClockUpdate, getShouldTimerUpdate } from './rundownService.utils.js';
import {
filterTimedEvents,
findNextPlayableId,
findNextPlayableWithCue,
findPreviousPlayableId,
getEventAtIndex,
getForceUpdate,
getShouldClockUpdate,
getShouldTimerUpdate,
} from './rundownService.utils.js';
type RuntimeStateEventKeys = keyof Pick<RuntimeState, 'eventNext' | 'eventNow' | 'publicEventNow' | 'publicEventNext'>;
@@ -198,7 +197,10 @@ class RuntimeService {
}
private isNewNext() {
const timedEvents = getTimedEvents();
const rundown = getCurrentRundown();
const { timedEventOrder } = getRundownMetadata();
const timedEvents = filterTimedEvents(rundown, timedEventOrder);
const state = runtimeState.getState();
const now = state.eventNow?.id;
const next = state.eventNext?.id;
@@ -271,8 +273,8 @@ class RuntimeService {
runtimeState.updateLoaded(eventNow);
} else {
const rundown = getCurrentRundown();
const { timedEventsOrder } = getEventOrder();
runtimeState.updateAll(rundown, timedEventsOrder);
const { timedEventOrder } = getRundownMetadata();
runtimeState.updateAll(rundown, timedEventOrder);
}
return;
}
@@ -281,7 +283,9 @@ class RuntimeService {
// Maybe the event will become the next
isNext = this.isNewNext();
if (isNext) {
const timedEvents = getTimedEvents();
const rundown = getCurrentRundown();
const { timedEventOrder } = getRundownMetadata();
const timedEvents = filterTimedEvents(rundown, timedEventOrder);
runtimeState.loadNext(timedEvents);
}
}
@@ -299,8 +303,10 @@ class RuntimeService {
}
const previousState = runtimeState.getState();
const { rundown, rundownOrder } = getRundownData();
const success = runtimeState.load(event, rundown, rundownOrder.timedEventsOrder, initialData);
// we can ignore events which are not playable
const rundown = getCurrentRundown();
const rundownMetadata = getRundownMetadata();
const success = runtimeState.load(event, rundown, rundownMetadata.playableEventOrder, initialData);
if (success) {
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
@@ -342,14 +348,20 @@ class RuntimeService {
*/
@broadcastResult
public startByIndex(eventIndex: number): boolean {
const event = getEventAtIndex(eventIndex);
const rundown = getCurrentRundown();
const { timedEventOrder } = getRundownMetadata();
const event = getEventAtIndex(rundown, timedEventOrder, eventIndex);
if (!event) {
return false;
}
const loaded = this.loadEvent(event);
if (!loaded) {
return false;
}
return this.handleStart();
}
@@ -360,10 +372,21 @@ class RuntimeService {
*/
@broadcastResult
public startByCue(cue: string): boolean {
const event = getNextEventWithCue(cue); //TODO: add index
const state = runtimeState.getState();
const rundown = getCurrentRundown();
const { playableEventOrder } = getRundownMetadata();
const event = findNextPlayableWithCue(
rundown,
playableEventOrder,
cue,
state.runtime.selectedEventIndex ?? undefined,
);
if (!event) {
return false;
}
const loaded = this.loadEvent(event);
if (!loaded) {
return false;
@@ -392,7 +415,11 @@ class RuntimeService {
*/
@broadcastResult
public loadByIndex(eventIndex: number): boolean {
const event = getEventAtIndex(eventIndex);
const rundown = getCurrentRundown();
const { timedEventOrder } = getRundownMetadata();
const event = getEventAtIndex(rundown, timedEventOrder, eventIndex);
if (!event) {
return false;
}
@@ -406,10 +433,21 @@ class RuntimeService {
*/
@broadcastResult
public loadByCue(cue: string): boolean {
const event = getNextEventWithCue(cue); //TODO: add index
const state = runtimeState.getState();
const rundown = getCurrentRundown();
const { playableEventOrder } = getRundownMetadata();
const event = findNextPlayableWithCue(
rundown,
playableEventOrder,
cue,
state.runtime.selectedEventIndex ?? undefined,
);
if (!event) {
return false;
}
return this.loadEvent(event);
}
@@ -421,10 +459,16 @@ class RuntimeService {
*/
private handleLoadPrevious(): boolean {
const state = runtimeState.getState();
const previousEvent = findPrevious(state.eventNow?.id);
if (previousEvent) {
return this.loadEvent(previousEvent);
const { playableEventOrder } = getRundownMetadata();
const previousId = findPreviousPlayableId(playableEventOrder, state.eventNow?.id);
if (previousId) {
const previousEvent = getEntryWithId(previousId);
if (previousEvent && isOntimeEvent(previousEvent)) {
return this.loadEvent(previousEvent);
}
}
return false;
}
@@ -446,8 +490,15 @@ class RuntimeService {
*/
private handleLoadNext(): boolean {
const state = runtimeState.getState();
const nextEvent = findNext(state.eventNow?.id);
if (nextEvent) {
const { playableEventOrder } = getRundownMetadata();
const nextId = findNextPlayableId(playableEventOrder, state.eventNow?.id);
if (nextId) {
const nextEvent = getEntryWithId(nextId);
if (!nextEvent || !isOntimeEvent(nextEvent)) {
return false;
}
if (state.timer.playback === Playback.Roll) {
return this.loadEvent(nextEvent, { firstStart: state.runtime.actualStart });
}
@@ -457,7 +508,6 @@ class RuntimeService {
logger.info(LogOrigin.Playback, 'No next event found! Continuing playback');
return false;
}
/**
* Loads event after currently selected
* @return {boolean} success
@@ -585,10 +635,10 @@ class RuntimeService {
*/
private rollLoaded(offset?: number) {
const rundown = getCurrentRundown();
const { timedEventsOrder } = getEventOrder();
const { timedEventOrder } = getRundownMetadata();
try {
runtimeState.roll(rundown, timedEventsOrder, offset);
runtimeState.roll(rundown, timedEventOrder, offset);
} catch (error) {
logger.error(LogOrigin.Server, `Roll: ${error}`);
}
@@ -608,8 +658,10 @@ class RuntimeService {
}
try {
const { rundown, rundownOrder } = getRundownData();
const result = runtimeState.roll(rundown, rundownOrder.timedEventsOrder);
const rundown = getCurrentRundown();
const rundownMetadata = getRundownMetadata();
const result = runtimeState.roll(rundown, rundownMetadata.playableEventOrder);
const newState = runtimeState.getState();
if (result.eventId !== previousState.eventNow?.id) {
logger.info(LogOrigin.Playback, `Loaded event with ID ${result.eventId}`);
@@ -660,8 +712,10 @@ class RuntimeService {
return;
}
const { rundown, rundownOrder } = getRundownData();
runtimeState.resume(restorePoint, event, rundown, rundownOrder.timedEventsOrder);
const rundown = getCurrentRundown();
const rundownMetadata = getRundownMetadata();
runtimeState.resume(restorePoint, event, rundown, rundownMetadata.playableEventOrder);
logger.info(LogOrigin.Playback, 'Resuming playback');
}
@@ -1,5 +1,5 @@
import { millisToSeconds } from 'ontime-utils';
import { MaybeNumber, TimerType } from 'ontime-types';
import { EntryId, isOntimeEvent, isPlayableEvent, MaybeNumber, OntimeEvent, Rundown, TimerType } from 'ontime-types';
import { timerConfig } from '../../setup/config.js';
@@ -33,3 +33,97 @@ export function getForceUpdate(previousUpdate: number, now: number): boolean {
const hasExceededRate = now - previousUpdate >= timerConfig.notificationRate;
return isClockBehind || hasExceededRate;
}
/**
* 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 first event that matches a given cue
*/
export function findNextPlayableWithCue(
rundown: Rundown,
playableEventsOrder: EntryId[],
targetCue: string,
currentEventIndex = 0,
): OntimeEvent | undefined {
const lowerCaseCue = targetCue.toLowerCase();
for (let i = currentEventIndex; i < playableEventsOrder.length; i++) {
const eventId = playableEventsOrder[i];
const event = rundown.entries[eventId];
if (isOntimeEvent(event) && isPlayableEvent(event) && event.cue.toLowerCase() === lowerCaseCue) {
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;
}
/**
* TODO(v4): we dont need this function
*/
export function filterTimedEvents(rundown: Rundown, timedEventOrder: EntryId[]): OntimeEvent[] {
return timedEventOrder.map((id) => rundown.entries[id] as OntimeEvent);
}