refactor: flatten state mutations (#750)

This commit is contained in:
Carlos Valente
2024-01-31 14:02:27 +01:00
committed by GitHub
parent ec8952be56
commit 7eb9ee8d03
11 changed files with 720 additions and 637 deletions
+83 -24
View File
@@ -1,6 +1,11 @@
import { OntimeEvent, Playback } from 'ontime-types';
import { EndAction, OntimeEvent, Playback, TimerLifeCycle } from 'ontime-types';
import { stateMutations, state } from '../state.js';
import * as runtimeState from '../stores/runtimeState.js';
import { integrationService } from './integration-service/IntegrationService.js';
import { eventStore } from '../stores/EventStore.js';
import { restoreService } from './RestoreService.js';
import { runtimeService } from './runtime-service/RuntimeService.js';
import { EventLoader } from '../classes/event-loader/EventLoader.js';
/**
* Service manages Ontime's main timer
@@ -21,51 +26,72 @@ export class TimerService {
this._interval = setInterval(this.update, 32);
}
@broadcastResult
start() {
if (!state.eventNow) {
return;
}
if (state.timer.playback === Playback.Play) {
return;
}
// TODO: when we start a timer, we schedule an update to its expected end - 16ms
// we need to cancel this timer on pause, stop and addTime
stateMutations.timer.start();
if (runtimeState.start()) {
integrationService.dispatch(TimerLifeCycle.onStart);
}
}
@broadcastResult
pause() {
if (state.timer.playback !== Playback.Play) {
return;
if (runtimeState.pause()) {
integrationService.dispatch(TimerLifeCycle.onPause);
}
stateMutations.timer.pause();
}
@broadcastResult
stop() {
if (state.timer.playback === Playback.Stop) {
return;
if (runtimeState.stop()) {
integrationService.dispatch(TimerLifeCycle.onStop);
}
stateMutations.timer.stop();
}
/**
* Adds time to running timer by given amount
* @param {number} amount
*/
addTime(amount: number) {
if (state.timer.current === null) {
return;
}
stateMutations.timer.addTime(amount);
@broadcastResult
addTime(amount: number): boolean {
return runtimeState.addTime(amount);
}
/**
* Update the app at regular intervals
* @param {boolean} force whether we should force a broadcast of state
*/
@broadcastResult
update(force = false) {
stateMutations.timer.update(force, this._updateInterval);
const { didUpdate, doRoll, isFinished, shouldNotify } = runtimeState.update(force, this._updateInterval);
if (didUpdate && shouldNotify) {
// TODO: can we distinguish between a clock update and a timer update?
integrationService.dispatch(TimerLifeCycle.onUpdate);
}
if (doRoll) {
const rundown = EventLoader.getPlayableEvents();
runtimeState.roll(rundown);
}
if (isFinished) {
integrationService.dispatch(TimerLifeCycle.onFinish);
const newState = runtimeState.getState();
// handle end action if there was a timer playing
if (newState.timer.playback === Playback.Play) {
if (newState.eventNow.endAction === EndAction.Stop) {
runtimeState.stop();
} else if (newState.eventNow.endAction === EndAction.LoadNext) {
// we need to delay here to put this action in the queue stack. otherwise it won't be executed properly
setTimeout(runtimeState.loadNext, 0);
} else if (newState.eventNow.endAction === EndAction.PlayNext) {
// TODO: avoid calling the runtime service here
runtimeService.startNext();
}
}
}
}
/**
@@ -73,15 +99,48 @@ export class TimerService {
* @throws {Error} if rundown is empty
* @param {OntimeEvent[]} rundown -- list of events to run
*/
@broadcastResult
roll(rundown: OntimeEvent[]) {
if (rundown.length === 0) {
throw new Error('No events found');
}
stateMutations.timer.roll(rundown);
runtimeState.roll(rundown);
}
shutdown() {
clearInterval(this._interval);
}
}
function broadcastResult(_target: any, _propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const result = originalMethod.apply(this, args);
const state = runtimeState.getState();
// TODO: compare datasets to see what needs to be emitted
eventStore.batchSet({
clock: state.clock,
eventNow: state.eventNow,
publicEventNow: state.publicEventNow,
eventNext: state.eventNext,
publicEventNext: state.publicEventNext,
runtime: state.runtime,
timer: state.timer,
});
// we write to restore service if the underlying data changes
restoreService.save({
playback: state.timer.playback,
selectedEventId: state.eventNow?.id ?? null,
startedAt: state.timer.startedAt,
addedTime: state.timer.addedTime,
pausedAt: state._timer.pausedAt,
});
return result;
};
return descriptor;
}
@@ -9,7 +9,7 @@ import {
skippedOutOfEvent,
updateRoll,
} from '../timerUtils.js';
import { TState } from '../../state.js';
import { RuntimeState } from '../../stores/runtimeState.js';
describe('getExpectedFinish()', () => {
it('is null if we havent started', () => {
@@ -27,7 +27,7 @@ describe('getExpectedFinish()', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(null);
});
@@ -46,7 +46,7 @@ describe('getExpectedFinish()', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(20);
});
@@ -65,7 +65,7 @@ describe('getExpectedFinish()', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(11);
});
@@ -84,7 +84,7 @@ describe('getExpectedFinish()', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(31);
@@ -104,7 +104,7 @@ describe('getExpectedFinish()', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(1);
@@ -124,7 +124,7 @@ describe('getExpectedFinish()', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(1);
@@ -144,7 +144,7 @@ describe('getExpectedFinish()', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(1);
@@ -164,7 +164,7 @@ describe('getExpectedFinish()', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(10);
@@ -185,7 +185,7 @@ describe('getExpectedFinish()', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(40);
@@ -204,7 +204,7 @@ describe('getExpectedFinish()', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const calculatedFinish = getExpectedFinish(state);
// expected finish is not a duration but a point in time
@@ -230,7 +230,7 @@ describe('getCurrent()', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(111);
@@ -251,7 +251,7 @@ describe('getCurrent()', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(9);
@@ -272,7 +272,7 @@ describe('getCurrent()', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(19);
@@ -293,7 +293,7 @@ describe('getCurrent()', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(dayInMs + 10);
@@ -314,7 +314,7 @@ describe('getCurrent()', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(15);
@@ -335,7 +335,7 @@ describe('getCurrent()', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(35);
@@ -358,7 +358,7 @@ describe('getCurrent()', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(70);
@@ -380,7 +380,7 @@ describe('getCurrent()', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(70);
@@ -401,7 +401,7 @@ describe('getCurrent()', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(77);
@@ -422,7 +422,7 @@ describe('getCurrent()', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(600000 + dayInMs - 79500000);
@@ -443,7 +443,7 @@ describe('getCurrent()', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(600000 + dayInMs - 79500000);
@@ -469,7 +469,7 @@ describe('getExpectedFinish() and getCurrentTime() combined', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const expectedFinish = getExpectedFinish(state);
const current = getCurrent(state);
@@ -497,7 +497,7 @@ describe('getExpectedFinish() and getCurrentTime() combined', () => {
_timer: {
pausedAt: null,
},
} as TState;
} as RuntimeState;
const expectedFinish = getExpectedFinish(state);
const current = getCurrent(state);
@@ -522,11 +522,10 @@ describe('skippedOutOfEvent()', () => {
expectedFinish,
startedAt,
},
} as TState;
} as RuntimeState;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
// @ts-expect-error -- cheating in tests
state.clock += testSkipLimit;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
});
@@ -543,11 +542,10 @@ describe('skippedOutOfEvent()', () => {
expectedFinish,
startedAt,
},
} as TState;
} as RuntimeState;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
// @ts-expect-error -- cheating in tests
state.clock += testSkipLimit;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
});
@@ -563,11 +561,10 @@ describe('skippedOutOfEvent()', () => {
expectedFinish,
startedAt,
},
} as TState;
} as RuntimeState;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
// @ts-expect-error -- cheating in tests
state.clock = testSkipLimit - 2;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
});
@@ -583,11 +580,10 @@ describe('skippedOutOfEvent()', () => {
expectedFinish,
startedAt,
},
} as TState;
} as RuntimeState;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
// @ts-expect-error -- cheating in tests
state.clock -= testSkipLimit;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
});
@@ -604,11 +600,10 @@ describe('skippedOutOfEvent()', () => {
expectedFinish,
startedAt,
},
} as TState;
} as RuntimeState;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
// @ts-expect-error -- cheating in tests
state.clock += testSkipLimit + 1;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(true);
});
@@ -625,11 +620,10 @@ describe('skippedOutOfEvent()', () => {
expectedFinish,
startedAt,
},
} as TState;
} as RuntimeState;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
// @ts-expect-error -- cheating in tests
state.clock -= testSkipLimit + 1;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(true);
});
@@ -645,11 +639,10 @@ describe('skippedOutOfEvent()', () => {
expectedFinish,
startedAt,
},
} as TState;
} as RuntimeState;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
// @ts-expect-error -- cheating in tests
state.clock = testSkipLimit - 2;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(true);
});
@@ -665,11 +658,10 @@ describe('skippedOutOfEvent()', () => {
expectedFinish,
startedAt,
},
} as TState;
} as RuntimeState;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
// @ts-expect-error -- cheating in tests
state.clock -= testSkipLimit + 1;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(true);
});
@@ -1205,7 +1197,7 @@ describe('updateRoll()', () => {
_timer: {
secondaryTarget: null,
},
} as TState;
} as RuntimeState;
const expected = {
updatedTimer: 15 - 11,
@@ -1217,9 +1209,7 @@ describe('updateRoll()', () => {
expect(updateRoll(timers)).toStrictEqual(expected);
// test that it can jump time
// @ts-expect-error -- cheating for tests
timers.timer.expectedFinish = 1000;
// @ts-expect-error -- cheating for tests
timers.clock = 600;
expected.updatedTimer = 1000 - 600;
@@ -1238,7 +1228,7 @@ describe('updateRoll()', () => {
_timer: {
secondaryTarget: 15,
},
} as TState;
} as RuntimeState;
const expected = {
updatedTimer: null,
@@ -1265,7 +1255,7 @@ describe('updateRoll()', () => {
_timer: {
secondaryTarget: null,
},
} as TState;
} as RuntimeState;
const expected = {
updatedTimer: -1,
@@ -1290,7 +1280,7 @@ describe('updateRoll()', () => {
_timer: {
secondaryTarget: 15,
},
} as TState;
} as RuntimeState;
const expected = {
updatedTimer: null,
@@ -1314,7 +1304,7 @@ describe('updateRoll()', () => {
_timer: {
secondaryTarget: 15,
},
} as TState;
} as RuntimeState;
const expected = {
updatedTimer: null,
@@ -1341,7 +1331,7 @@ describe('updateRoll()', () => {
_timer: {
secondaryTarget: null,
},
} as TState;
} as RuntimeState;
const expected = {
updatedTimer: 20,
@@ -1368,7 +1358,7 @@ describe('updateRoll()', () => {
_timer: {
secondaryTarget: null,
},
} as TState;
} as RuntimeState;
const expected = {
updatedTimer: dayInMs,
@@ -26,7 +26,7 @@ import {
} from './rundownCache.js';
import { logger } from '../../classes/Logger.js';
import { createEvent } from '../../utils/parser.js';
import { stateMutations } from '../../state.js';
import { updateNumEvents } from '../../stores/runtimeState.js';
import { runtimeService } from '../runtime-service/RuntimeService.js';
/**
@@ -169,7 +169,7 @@ export async function swapEvents(from: string, to: string) {
*/
function updateChangeNumEvents() {
const numEvents = EventLoader.getPlayableEvents().length;
stateMutations.updateNumEvents(numEvents);
updateNumEvents(numEvents);
}
/**
@@ -5,7 +5,7 @@ import { EventLoader } from '../../classes/event-loader/EventLoader.js';
import { TimerService } from '../TimerService.js';
import { logger } from '../../classes/Logger.js';
import { RestorePoint } from '../RestoreService.js';
import { state, stateMutations } from '../../state.js';
import * as runtimeState from '../../stores/runtimeState.js';
/**
* Service manages runtime status of app
@@ -38,6 +38,7 @@ class RuntimeService {
* Checks if a list of IDs is in the current selection
*/
private affectsLoaded(affectedIds: string[]): boolean {
const state = runtimeState.getState();
const now = state.eventNow?.id;
const nowPublic = state.publicEventNow?.id;
const next = state.eventNext?.id;
@@ -52,6 +53,7 @@ class RuntimeService {
private isNewNext() {
const timedEvents = EventLoader.getPlayableEvents();
const state = runtimeState.getState();
const now = state.eventNow?.id;
const next = state.eventNext?.id;
@@ -88,13 +90,14 @@ class RuntimeService {
}
reset() {
stateMutations.timer.clear();
runtimeState.clear();
}
/**
* check whether underlying data of runtime has changed
*/
update(affectedIds?: string[]) {
const state = runtimeState.getState();
const hasLoadedElements = state.eventNow || state.eventNext;
if (!hasLoadedElements) {
return;
@@ -115,7 +118,7 @@ class RuntimeService {
// load stuff again, but keep running if our events still exist
const eventNow = EventLoader.getEventWithId(state.eventNow.id);
if (eventNow) {
stateMutations.reload(eventNow);
runtimeState.reload(eventNow);
}
return;
}
@@ -124,7 +127,7 @@ class RuntimeService {
if (isNext) {
// TODO: do i need to load here?
const playableEvents = EventLoader.getPlayableEvents();
stateMutations.loadNext(playableEvents);
runtimeState.loadNext(playableEvents);
}
}
@@ -140,7 +143,9 @@ class RuntimeService {
}
const timedEvents = EventLoader.getPlayableEvents();
stateMutations.load(event, timedEvents);
const state = runtimeState.getState();
// TODO: return success boolean from runtimeState
runtimeState.load(event, timedEvents);
const success = event.id === state.eventNow?.id;
if (success) {
@@ -229,6 +234,7 @@ class RuntimeService {
* @return {boolean} success - whether an event was loaded
*/
loadPrevious(): boolean {
const state = runtimeState.getState();
const previousEvent = EventLoader.findPrevious(state.eventNow?.id);
if (previousEvent) {
const success = this.loadEvent(previousEvent);
@@ -242,6 +248,7 @@ class RuntimeService {
* @return {boolean} success
*/
loadNext(): boolean {
const state = runtimeState.getState();
const nextEvent = EventLoader.findNext(state.eventNow?.id);
if (nextEvent) {
const success = this.loadEvent(nextEvent);
@@ -256,6 +263,7 @@ class RuntimeService {
* Starts playback on selected event
*/
start() {
const state = runtimeState.getState();
const canStart = validatePlayback(state.timer.playback).start;
if (canStart) {
this.eventTimer.start();
@@ -277,6 +285,7 @@ class RuntimeService {
* Pauses playback on selected event
*/
pause() {
const state = runtimeState.getState();
if (validatePlayback(state.timer.playback).pause) {
this.eventTimer.pause();
const newState = state.timer.playback;
@@ -288,6 +297,7 @@ class RuntimeService {
* Stops timer and unloads any events
*/
stop() {
const state = runtimeState.getState();
if (validatePlayback(state.timer.playback).stop) {
this.eventTimer.stop();
const newState = state.timer.playback;
@@ -299,8 +309,9 @@ class RuntimeService {
* Reloads current event
*/
reload() {
const state = runtimeState.getState();
if (state.eventNow) {
stateMutations.reload();
runtimeState.reload();
}
}
@@ -315,6 +326,7 @@ class RuntimeService {
logger.warning(LogOrigin.Server, `Roll: ${error}`);
}
const state = runtimeState.getState();
const newState = state.timer.playback;
logger.info(LogOrigin.Playback, `Play Mode ${newState.toUpperCase()}`);
}
@@ -337,7 +349,7 @@ class RuntimeService {
}
const timedEvents = EventLoader.getPlayableEvents();
stateMutations.resume(restorePoint, event, timedEvents);
runtimeState.resume(restorePoint, event, timedEvents);
logger.info(LogOrigin.Playback, 'Resuming playback');
}
@@ -346,8 +358,9 @@ class RuntimeService {
* @param {number} time - time to add in milliseconds
*/
addTime(time: number) {
this.eventTimer.addTime(time);
logger.info(LogOrigin.Playback, `${time > 0 ? 'Added' : 'Removed'} ${millisToString(time)}`);
if (this.eventTimer.addTime(time)) {
logger.info(LogOrigin.Playback, `${time > 0 ? 'Added' : 'Removed'} ${millisToString(time)}`);
}
}
}
+9 -9
View File
@@ -1,6 +1,6 @@
import { MaybeNumber, OntimeEvent, TimerType } from 'ontime-types';
import { dayInMs } from 'ontime-utils';
import { TState } from '../state.js';
import { RuntimeState } from '../stores/runtimeState.js';
import { sortArrayByProperty } from '../utils/arrayUtils.js';
/**
@@ -10,10 +10,10 @@ export const normaliseEndTime = (start: number, end: number) => (end < start ? e
/**
* Calculates expected finish time of a running timer
* @param {TState} state runtime state
* @param {RuntimeState} state runtime state
* @returns {number | null} new current time or null if nothing is running
*/
export function getExpectedFinish(state: TState): MaybeNumber {
export function getExpectedFinish(state: RuntimeState): MaybeNumber {
const { startedAt, finishedAt, duration, addedTime } = state.timer;
const { timerType, timeEnd } = state.eventNow;
const { pausedAt } = state._timer;
@@ -45,10 +45,10 @@ export function getExpectedFinish(state: TState): MaybeNumber {
/**
* Calculates running countdown
* @param {TState} state runtime state
* @param {RuntimeState} state runtime state
* @returns {number} current time for timer
*/
export function getCurrent(state: TState): number {
export function getCurrent(state: RuntimeState): number {
const { startedAt, duration, addedTime } = state.timer;
const { timerType, timeEnd } = state.eventNow;
const { pausedAt } = state._timer;
@@ -75,12 +75,12 @@ export function getCurrent(state: TState): number {
/**
* Checks whether we have skipped out of the event
* @param {TState} state runtime state
* @param {RuntimeState} state runtime state
* @param {number} previousTime previous clock
* @param {number} skipLimit how much time can we skip
* @returns {boolean}
*/
export function skippedOutOfEvent(state: TState, previousTime: number, skipLimit: number): boolean {
export function skippedOutOfEvent(state: RuntimeState, previousTime: number, skipLimit: number): boolean {
const { startedAt, expectedFinish } = state.timer;
const { clock } = state;
@@ -226,10 +226,10 @@ export const getRollTimers = (rundown: OntimeEvent[], timeNow: number) => {
/**
* @description Implements update functions for roll mode
* @param {TState}
* @param {RuntimeState}
* @returns object with selection variables
*/
export const updateRoll = (state: TState) => {
export const updateRoll = (state: RuntimeState) => {
const { current, expectedFinish, startedAt, secondaryTimer } = state.timer;
const { secondaryTarget } = state._timer;
const { clock } = state;