mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-07 08:23:55 +00:00
refactor: runtime service (#1722)
* refactor: trim type * refactor: update runtime service data push logic * keep using just deep * reword didDependencyUpdate * remove dulicate
This commit is contained in:
committed by
Carlos Valente
parent
e0149c1cbf
commit
022a58e142
@@ -33,9 +33,12 @@ import {
|
||||
findNextPlayableWithCue,
|
||||
findPreviousPlayableId,
|
||||
getEventAtIndex,
|
||||
getForceUpdate,
|
||||
getShouldClockUpdate,
|
||||
getShouldFlagUpdate,
|
||||
getShouldGroupUpdate,
|
||||
getShouldRuntimeUpdate,
|
||||
getShouldTimerUpdate,
|
||||
isNewSecond,
|
||||
} from './rundownService.utils.js';
|
||||
import { RundownMetadata } from '../../api-data/rundown/rundown.types.js';
|
||||
|
||||
@@ -141,7 +144,7 @@ class RuntimeService {
|
||||
}
|
||||
|
||||
// 4. find if we need to update the timer
|
||||
const shouldUpdateTimer = getShouldTimerUpdate(this.lastIntegrationTimerValue, newState.timer.current);
|
||||
const shouldUpdateTimer = isNewSecond(this.lastIntegrationTimerValue, newState.timer.current);
|
||||
if (shouldUpdateTimer) {
|
||||
process.nextTick(() => {
|
||||
triggerAutomations(TimerLifeCycle.onUpdate, newState);
|
||||
@@ -659,7 +662,6 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
|
||||
// call the original method and get the state
|
||||
const result = originalMethod.apply(this, args);
|
||||
const state = runtimeState.getState();
|
||||
|
||||
const batch = eventStore.createBatch();
|
||||
|
||||
// we do the comparison by explicitly for each property
|
||||
@@ -680,91 +682,63 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
|
||||
// combine all big changes
|
||||
const hasImmediateChanges = hasNewLoaded || justStarted || hasChangedPlayback || offsetModeChanged;
|
||||
|
||||
// we would like the wall clock to tick on a regular rate
|
||||
const normalClockUpdate =
|
||||
getShouldClockUpdate(RuntimeService.previousClockUpdate, state.clock) ||
|
||||
getForceUpdate(RuntimeService.previousClockUpdate, state.clock);
|
||||
// clock has changed by a second or more
|
||||
const updateClock = getShouldClockUpdate(RuntimeService.previousState.clock, state.clock);
|
||||
if (updateClock) {
|
||||
batch.add('clock', state.clock);
|
||||
RuntimeService.previousState.clock = state.clock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timer should be updated if
|
||||
* - big changes
|
||||
* - notification rate has been exceeded
|
||||
* - the timer has rolled over into the next UI display unit
|
||||
*
|
||||
* Then check if there is actually a change in the data
|
||||
*/
|
||||
const shouldUpdateTimer =
|
||||
(hasImmediateChanges ||
|
||||
getForceUpdate(RuntimeService.previousTimerUpdate, state.clock) ||
|
||||
getShouldTimerUpdate(RuntimeService.previousTimerValue, state.timer.current)) &&
|
||||
!deepEqual(RuntimeService.previousState?.timer, state.timer);
|
||||
|
||||
/**
|
||||
* Runtime should be updated if
|
||||
* - clock tick
|
||||
* - big changes
|
||||
* - the timer is updating so runtime also updates to keep them in sync ???
|
||||
* - notification rate has been exceeded
|
||||
*
|
||||
* Then check if there is actually a change in the data
|
||||
*/
|
||||
const shouldRuntimeUpdate =
|
||||
(normalClockUpdate ||
|
||||
hasImmediateChanges ||
|
||||
shouldUpdateTimer ||
|
||||
getForceUpdate(RuntimeService.previousRuntimeUpdate, state.clock)) &&
|
||||
!deepEqual(RuntimeService.previousState?.runtime, state.runtime);
|
||||
|
||||
// TODO: the value shows up one tick to late
|
||||
const shouldGroupUpdate =
|
||||
!deepEqual(RuntimeService?.previousState.groupNow, state.groupNow) ||
|
||||
RuntimeService?.previousState.groupNext !== state.groupNext;
|
||||
|
||||
// TODO: the value shows up one tick to late
|
||||
const shouldNextFlagUpdate = !deepEqual(RuntimeService?.previousState?.nextFlag, state.nextFlag);
|
||||
|
||||
/**
|
||||
* Many other values are calculated based on the clock
|
||||
* so if any of them are updated we also need to send the clock
|
||||
* in case nothing else is updating the clock will be updated at the notification rate
|
||||
*/
|
||||
const shouldUpdateClock = shouldRuntimeUpdate || shouldGroupUpdate || normalClockUpdate;
|
||||
|
||||
// Now we set all the updates on the eventstore and update the previous value
|
||||
if (shouldUpdateTimer) {
|
||||
// if any values have changed, values that have the possibility to tick are updated when the seconds roll over
|
||||
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 (shouldRuntimeUpdate) {
|
||||
// if any values have changed, values that have the possibility to tick are modulated by `hasClockUpdate`
|
||||
const updateRuntime = getShouldRuntimeUpdate(
|
||||
RuntimeService.previousState?.runtime,
|
||||
state.runtime,
|
||||
updateClock || hasImmediateChanges,
|
||||
);
|
||||
if (updateRuntime) {
|
||||
batch.add('runtime', state.runtime);
|
||||
RuntimeService.previousRuntimeUpdate = state.clock;
|
||||
RuntimeService.previousState.runtime = structuredClone(state.runtime);
|
||||
}
|
||||
|
||||
if (shouldGroupUpdate) {
|
||||
// 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);
|
||||
batch.add('groupNext', state.groupNext);
|
||||
RuntimeService.previousState.groupNow = structuredClone(state.groupNow);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
if (shouldNextFlagUpdate) {
|
||||
// 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 (hasImmediateChanges) {
|
||||
saveRestoreState(state);
|
||||
}
|
||||
|
||||
if (shouldUpdateClock) {
|
||||
RuntimeService.previousClockUpdate = state.clock;
|
||||
batch.add('clock', state.clock);
|
||||
}
|
||||
|
||||
// Update the events if they have changed
|
||||
updateEventIfChanged('eventNow', state);
|
||||
updateEventIfChanged('eventNext', state);
|
||||
@@ -775,31 +749,22 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
|
||||
const now = state[eventKey];
|
||||
|
||||
// if there was nothing, and there is nothing, noop
|
||||
if (!previous?.id && !now?.id) {
|
||||
return;
|
||||
}
|
||||
if (!previous?.id && !now?.id) return;
|
||||
|
||||
// if load status changed, save new
|
||||
if (previous?.id !== now?.id) {
|
||||
storeKey(eventKey);
|
||||
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]);
|
||||
|
||||
// maybe the event itself has changed
|
||||
if (!deepEqual(RuntimeService.previousState?.[eventKey], state[eventKey])) {
|
||||
storeKey(eventKey);
|
||||
return;
|
||||
}
|
||||
if (!eventChanged) return;
|
||||
|
||||
function storeKey(eventKey: RuntimeStateEventKeys) {
|
||||
batch.add(eventKey, state[eventKey]);
|
||||
// @ts-expect-error -- not sure how to type this in a sane way
|
||||
RuntimeService.previousState[eventKey] = { ...state[eventKey] };
|
||||
}
|
||||
batch.add(eventKey, state[eventKey]);
|
||||
RuntimeService.previousState[eventKey] = structuredClone(state[eventKey]);
|
||||
}
|
||||
|
||||
// Helper function to save the restore state
|
||||
function saveRestoreState(state: runtimeState.RuntimeState) {
|
||||
// save the restore state
|
||||
if (hasImmediateChanges) {
|
||||
restoreService
|
||||
.save({
|
||||
playback: state.timer.playback,
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { MILLIS_PER_MINUTE } from 'ontime-utils';
|
||||
import { getShouldClockUpdate, getShouldTimerUpdate } from '../rundownService.utils.js';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(0);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('getShouldClockUpdate()', () => {
|
||||
it('should return true when we slid forwards', () => {
|
||||
const previousUpdate = Date.now(); // 2 seconds ago
|
||||
const now = Date.now() + 2000;
|
||||
const result = getShouldClockUpdate(previousUpdate, now);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when we slid backwards', () => {
|
||||
const previousUpdate = Date.now() + 2000;
|
||||
const now = Date.now(); // 2 seconds ago
|
||||
const result = getShouldClockUpdate(previousUpdate, now);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when clock is a second ahead', () => {
|
||||
const previousUpdate = MILLIS_PER_MINUTE - 100;
|
||||
const now = MILLIS_PER_MINUTE;
|
||||
const result = getShouldClockUpdate(previousUpdate, now);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when clock is not a second ahead and force update is not required', () => {
|
||||
const previousUpdate = Date.now();
|
||||
const now = Date.now() + 32;
|
||||
const result = getShouldClockUpdate(previousUpdate, now);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getShouldTimerUpdate', () => {
|
||||
it('should return false when currentValue is null', () => {
|
||||
const previousValue = 0;
|
||||
const currentValue = null;
|
||||
const result = getShouldTimerUpdate(previousValue, currentValue);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when timer is a second ahead', () => {
|
||||
const previousValue = 6500;
|
||||
const currentValue = 5000;
|
||||
const result = getShouldTimerUpdate(previousValue, currentValue);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when timer is not a second ahead', () => {
|
||||
const previousValue = 5500;
|
||||
const currentValue = 5200;
|
||||
const result = getShouldTimerUpdate(previousValue, currentValue);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('timer value is ceiled', () => {
|
||||
const previousValue = 5001; // 6
|
||||
const currentValue = 4999; // 5
|
||||
const result = getShouldTimerUpdate(previousValue, currentValue);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,27 @@
|
||||
import { millisToSeconds } from 'ontime-utils';
|
||||
import { EntryId, isOntimeEvent, isPlayableEvent, MaybeNumber, OntimeEvent, Rundown, TimerType } from 'ontime-types';
|
||||
import {
|
||||
EntryId,
|
||||
GroupState,
|
||||
isOntimeEvent,
|
||||
isPlayableEvent,
|
||||
MaybeNumber,
|
||||
OntimeEvent,
|
||||
Rundown,
|
||||
Runtime,
|
||||
TimerState,
|
||||
TimerType,
|
||||
UpcomingEntry,
|
||||
} from 'ontime-types';
|
||||
|
||||
import { timerConfig } from '../../setup/config.js';
|
||||
import { deepEqual } from 'fast-equals';
|
||||
|
||||
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
|
||||
@@ -17,21 +37,69 @@ export function getShouldClockUpdate(previousUpdate: number, now: number): boole
|
||||
* Checks whether we should update the timer value
|
||||
* - we have rolled into a new seconds unit
|
||||
*/
|
||||
export function getShouldTimerUpdate(previousValue: MaybeNumber, currentValue: MaybeNumber): boolean {
|
||||
const shouldUpdateTimer = millisToSeconds(currentValue) !== millisToSeconds(previousValue);
|
||||
return shouldUpdateTimer;
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* In some cases we want to force an update to the timer
|
||||
* - if the clock has slid back
|
||||
* - if we have escaped the update rate (clock slid forward)
|
||||
* - if we are not playing then there is no need to update the timer
|
||||
*/
|
||||
export function getForceUpdate(previousUpdate: number, now: number): boolean {
|
||||
const isClockBehind = now < previousUpdate;
|
||||
const hasExceededRate = now - previousUpdate >= timerConfig.notificationRate;
|
||||
return isClockBehind || hasExceededRate;
|
||||
export function getShouldRuntimeUpdate(
|
||||
previousValue: Runtime | undefined,
|
||||
currentValue: Runtime,
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
CurrentGroupState,
|
||||
EntryMetaData,
|
||||
GroupState,
|
||||
UpcomingEntry,
|
||||
isOntimeEvent,
|
||||
MaybeNumber,
|
||||
MaybeString,
|
||||
@@ -36,9 +36,9 @@ type ExpectedMetadata = { event: OntimeEvent; accumulatedGap: number; isLinkedTo
|
||||
|
||||
export type RuntimeState = {
|
||||
clock: number; // realtime clock
|
||||
groupNow: CurrentGroupState | null;
|
||||
groupNow: GroupState | null;
|
||||
groupNext: MaybeString;
|
||||
nextFlag: EntryMetaData | null;
|
||||
nextFlag: UpcomingEntry | null;
|
||||
eventNow: PlayableEvent | null;
|
||||
eventNext: PlayableEvent | null;
|
||||
runtime: Runtime;
|
||||
@@ -802,7 +802,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, actualStart: null, expectedStart: null, expectedEnd: null };
|
||||
state.nextFlag = { id: entry.id, expectedStart: null };
|
||||
state._flag = { event: entry, isLinkedToLoaded, accumulatedGap };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { MaybeNumber } from '../../utils/utils.type.js';
|
||||
import type { EntryId } from '../core/OntimeEntry.js';
|
||||
|
||||
export type CurrentGroupState = {
|
||||
export type GroupState = {
|
||||
id: EntryId;
|
||||
startedAt: MaybeNumber;
|
||||
expectedEnd: MaybeNumber;
|
||||
@@ -9,12 +9,5 @@ export type CurrentGroupState = {
|
||||
|
||||
export type UpcomingEntry = {
|
||||
id: EntryId;
|
||||
start: number;
|
||||
};
|
||||
|
||||
export type EntryMetaData = {
|
||||
id: EntryId;
|
||||
actualStart: MaybeNumber;
|
||||
expectedStart: MaybeNumber;
|
||||
expectedEnd: MaybeNumber;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { MaybeString } from '../../utils/utils.type.js';
|
||||
import type { OntimeEvent } from '../core/OntimeEntry.js';
|
||||
import type { SimpleTimerState } from './AuxTimer.type.js';
|
||||
import type { CurrentGroupState, EntryMetaData } from './CurrentGroupState.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 { TimerState } from './TimerState.type.js';
|
||||
@@ -19,9 +19,9 @@ export type RuntimeStore = {
|
||||
eventNow: OntimeEvent | null;
|
||||
eventNext: OntimeEvent | null;
|
||||
|
||||
groupNow: CurrentGroupState | null;
|
||||
groupNow: GroupState | null;
|
||||
groupNext: MaybeString;
|
||||
nextFlag: EntryMetaData | null;
|
||||
nextFlag: UpcomingEntry | null;
|
||||
|
||||
// extra timers
|
||||
auxtimer1: SimpleTimerState;
|
||||
|
||||
@@ -101,7 +101,7 @@ export { OffsetMode } from './definitions/runtime/Runtime.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 { CurrentGroupState, UpcomingEntry, EntryMetaData } from './definitions/runtime/CurrentGroupState.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';
|
||||
|
||||
Reference in New Issue
Block a user