mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-06 16:03:52 +00:00
refactor: flatten state mutations (#750)
This commit is contained in:
@@ -34,7 +34,7 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
|
||||
const noEvents = numEvents === 0;
|
||||
|
||||
const disableGo = isRolling || noEvents || (isLast && !isArmed);
|
||||
const disablePrev = noEvents || isFirst;
|
||||
const disablePrev = isRolling || noEvents || isFirst;
|
||||
|
||||
const playbackCan = validatePlayback(playback);
|
||||
const disableStart = !playbackCan.start;
|
||||
|
||||
@@ -42,7 +42,7 @@ import { runtimeService } from './services/runtime-service/RuntimeService.js';
|
||||
import { restoreService } from './services/RestoreService.js';
|
||||
import { messageService } from './services/message-service/MessageService.js';
|
||||
import { populateDemo } from './modules/loadDemo.js';
|
||||
import { state, stateMutations } from './state.js';
|
||||
import { getState, updateNumEvents } from './stores/runtimeState.js';
|
||||
|
||||
console.log(`Starting Ontime version ${ONTIME_VERSION}`);
|
||||
|
||||
@@ -158,6 +158,7 @@ export const startServer = async () => {
|
||||
/**
|
||||
* Module initialises the services and provides initial payload for the store
|
||||
*/
|
||||
const state = getState();
|
||||
eventStore.init({
|
||||
clock: state.clock,
|
||||
timer: state.timer,
|
||||
@@ -184,7 +185,7 @@ export const startServer = async () => {
|
||||
|
||||
// TODO: do this on the init of the runtime service
|
||||
const numEvents = EventLoader.getNumEvents();
|
||||
stateMutations.updateNumEvents(numEvents);
|
||||
updateNumEvents(numEvents);
|
||||
|
||||
// eventStore set is a dependency of the services that publish to it
|
||||
messageService.init(eventStore.set.bind(eventStore));
|
||||
|
||||
@@ -14,3 +14,7 @@ export const config = {
|
||||
},
|
||||
restoreFile: 'ontime.restore',
|
||||
};
|
||||
|
||||
export const timerConfig = {
|
||||
timeSkipLimit: 1000,
|
||||
};
|
||||
|
||||
@@ -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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,541 +0,0 @@
|
||||
import type { DeepReadonly, DeepWritable } from 'ts-essentials';
|
||||
|
||||
import {
|
||||
EndAction,
|
||||
Runtime,
|
||||
OntimeEvent,
|
||||
Playback,
|
||||
TimerLifeCycle,
|
||||
TimerState,
|
||||
TimerType,
|
||||
MaybeNumber,
|
||||
} from 'ontime-types';
|
||||
import { calculateDuration, dayInMs } from 'ontime-utils';
|
||||
|
||||
import { clock } from './services/Clock.js';
|
||||
import { RestorePoint, restoreService } from './services/RestoreService.js';
|
||||
import { getCurrent, getExpectedFinish, getRollTimers, skippedOutOfEvent, updateRoll } from './services/timerUtils.js';
|
||||
import { eventStore } from './stores/EventStore.js';
|
||||
import { integrationService } from './services/integration-service/IntegrationService.js';
|
||||
import { runtimeService } from './services/runtime-service/RuntimeService.js';
|
||||
import { EventLoader } from './classes/event-loader/EventLoader.js';
|
||||
|
||||
// TODO: move to timer config
|
||||
const timeSkipLimit = 1000;
|
||||
|
||||
const initialRuntime: Runtime = {
|
||||
selectedEventIndex: null,
|
||||
numEvents: 0,
|
||||
};
|
||||
|
||||
const initialTimer: TimerState = {
|
||||
addedTime: 0,
|
||||
current: null,
|
||||
duration: null,
|
||||
elapsed: null,
|
||||
expectedFinish: null, // TODO: expected finish could account for midnight, we cleanup in the clients
|
||||
finishedAt: null,
|
||||
playback: Playback.Stop,
|
||||
secondaryTimer: null,
|
||||
startedAt: null,
|
||||
};
|
||||
|
||||
export type TState = DeepReadonly<{
|
||||
clock: number; // realtime clock
|
||||
eventNow: OntimeEvent | null;
|
||||
publicEventNow: OntimeEvent | null;
|
||||
eventNext: OntimeEvent | null;
|
||||
publicEventNext: OntimeEvent | null;
|
||||
runtime: Runtime;
|
||||
timer: TimerState;
|
||||
// private properties of the timer calculations
|
||||
_timer: {
|
||||
pausedAt: MaybeNumber;
|
||||
finishedNow: boolean;
|
||||
lastUpdate: MaybeNumber;
|
||||
secondaryTarget: MaybeNumber;
|
||||
};
|
||||
}>;
|
||||
|
||||
export const state: TState = {
|
||||
clock: clock.timeNow(),
|
||||
eventNow: null,
|
||||
publicEventNow: null,
|
||||
eventNext: null,
|
||||
publicEventNext: null,
|
||||
runtime: initialRuntime,
|
||||
timer: { ...initialTimer },
|
||||
_timer: {
|
||||
pausedAt: null,
|
||||
lastUpdate: null,
|
||||
secondaryTarget: null,
|
||||
get finishedNow() {
|
||||
return this.current <= 0 && this.finishedAt === null;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const stateMutations = {
|
||||
load(event: OntimeEvent, rundown: OntimeEvent[], initialData?: Partial<TimerState>) {
|
||||
mutate((state) => {
|
||||
stateMutations.timer.clear();
|
||||
|
||||
const eventIndex = rundown.findIndex((eventInMemory) => eventInMemory.id === event.id);
|
||||
|
||||
state.runtime.selectedEventIndex = eventIndex;
|
||||
state.runtime.numEvents = rundown.length;
|
||||
|
||||
this.loadNow(event, rundown);
|
||||
this.loadNext(rundown);
|
||||
|
||||
state.clock = clock.timeNow();
|
||||
state.timer.playback = Playback.Armed;
|
||||
state.timer.duration = calculateDuration(event.timeStart, event.timeEnd);
|
||||
state.timer.current = getCurrent(state);
|
||||
|
||||
if (initialData) {
|
||||
stateMutations.timer.patch(initialData);
|
||||
}
|
||||
});
|
||||
},
|
||||
loadNow(event: OntimeEvent, playableEvents: OntimeEvent[]) {
|
||||
mutate((state) => {
|
||||
state.eventNow = event;
|
||||
|
||||
// check if current is also public
|
||||
if (event.isPublic) {
|
||||
state.publicEventNow = event;
|
||||
} else {
|
||||
// assume there is no public event
|
||||
state.publicEventNow = null;
|
||||
|
||||
// if there is nothing before, return
|
||||
if (!state.runtime.selectedEventIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
// iterate backwards to find it
|
||||
for (let i = state.runtime.selectedEventIndex; i >= 0; i--) {
|
||||
if (playableEvents[i].isPublic) {
|
||||
state.publicEventNow = playableEvents[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
loadNext(playableEvents: OntimeEvent[]) {
|
||||
mutate((state) => {
|
||||
// assume there are no next events
|
||||
state.eventNext = null;
|
||||
state.publicEventNext = null;
|
||||
|
||||
if (state.runtime.selectedEventIndex === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const numEvents = playableEvents.length;
|
||||
|
||||
if (state.runtime.selectedEventIndex < numEvents - 1) {
|
||||
let nextPublic = false;
|
||||
let nextProduction = false;
|
||||
|
||||
for (let i = state.runtime.selectedEventIndex + 1; i < numEvents; i++) {
|
||||
// if we have not set private
|
||||
if (!nextProduction) {
|
||||
state.eventNext = playableEvents[i];
|
||||
nextProduction = true;
|
||||
}
|
||||
|
||||
// if event is public
|
||||
if (playableEvents[i].isPublic) {
|
||||
state.publicEventNext = playableEvents[i];
|
||||
nextPublic = true;
|
||||
}
|
||||
|
||||
// Stop if both are set
|
||||
if (nextPublic && nextProduction) break;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
resume(restorePoint: RestorePoint, event: OntimeEvent, rundown: OntimeEvent[]) {
|
||||
mutate((_state) => {
|
||||
stateMutations.load(event, rundown, restorePoint);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* We only pass an event if we are hot reloading
|
||||
* @param {OntimeEvent} event only passed if we are changing the data if a playing timer
|
||||
*/
|
||||
reload(event?: OntimeEvent) {
|
||||
mutate((state) => {
|
||||
if (event) {
|
||||
state.eventNow = event;
|
||||
|
||||
// update data which is duplicate between eventNow and timer objects
|
||||
state.timer.duration = calculateDuration(state.eventNow.timeStart, state.eventNow.timeEnd);
|
||||
state.timer.current = getCurrent(state);
|
||||
state.timer.expectedFinish = getExpectedFinish(state);
|
||||
return;
|
||||
}
|
||||
state.timer.playback = Playback.Armed;
|
||||
|
||||
state.timer.duration = calculateDuration(state.eventNow.timeStart, state.eventNow.timeEnd);
|
||||
state.timer.current = state.timer.duration;
|
||||
state.timer.elapsed = null;
|
||||
|
||||
state.timer.startedAt = null;
|
||||
state.timer.finishedAt = null;
|
||||
state._timer.pausedAt = null;
|
||||
state.timer.addedTime = 0;
|
||||
|
||||
state.timer.expectedFinish = getExpectedFinish(state);
|
||||
});
|
||||
},
|
||||
updateNumEvents(numEvents: number) {
|
||||
mutate((state) => {
|
||||
state.runtime.numEvents = numEvents;
|
||||
});
|
||||
},
|
||||
timer: {
|
||||
// utility to reset the state of the timer
|
||||
// TODO: create smaller utilities to reset parts of the state
|
||||
clear() {
|
||||
mutate((state) => {
|
||||
// TODO: check that entire state is reset here
|
||||
state.eventNow = null;
|
||||
state.publicEventNow = null;
|
||||
state.eventNext = null;
|
||||
state.publicEventNext = null;
|
||||
|
||||
state.runtime = { ...initialRuntime };
|
||||
// TODO: could we avoid having this dependency?
|
||||
state.runtime.numEvents = EventLoader.getPlayableEvents().length;
|
||||
|
||||
state.timer.playback = Playback.Stop;
|
||||
state.clock = clock.timeNow();
|
||||
state.timer = { ...initialTimer };
|
||||
state._timer = {
|
||||
pausedAt: null,
|
||||
lastUpdate: null,
|
||||
secondaryTarget: null,
|
||||
finishedNow: false,
|
||||
};
|
||||
});
|
||||
},
|
||||
/** utility to allow modifying the state from the outside */
|
||||
patch(timer: Partial<TimerState>) {
|
||||
mutate((state) => {
|
||||
for (const key in timer) {
|
||||
if (key in state.timer) {
|
||||
state.timer[key] = timer[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
start() {
|
||||
mutate(
|
||||
(state) => {
|
||||
state.clock = clock.timeNow();
|
||||
state.timer.secondaryTimer = null;
|
||||
state._timer.secondaryTarget = null;
|
||||
|
||||
// add paused time if it exists
|
||||
if (state._timer.pausedAt) {
|
||||
const timeToAdd = state.clock - state._timer.pausedAt;
|
||||
state.timer.addedTime += timeToAdd;
|
||||
state._timer.pausedAt = null;
|
||||
}
|
||||
|
||||
if (state.timer.startedAt === null) {
|
||||
state.timer.startedAt = state.clock;
|
||||
}
|
||||
|
||||
state.timer.playback = Playback.Play;
|
||||
state.timer.expectedFinish = getExpectedFinish(state);
|
||||
},
|
||||
{
|
||||
sideEffect() {
|
||||
integrationService.dispatch(TimerLifeCycle.onStart);
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
stop() {
|
||||
mutate(
|
||||
(_state) => {
|
||||
stateMutations.timer.clear();
|
||||
},
|
||||
{
|
||||
sideEffect() {
|
||||
integrationService.dispatch(TimerLifeCycle.onStop);
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
pause() {
|
||||
mutate(
|
||||
(state) => {
|
||||
if (state.timer.playback !== Playback.Play) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state.timer.playback = Playback.Pause;
|
||||
state.clock = clock.timeNow();
|
||||
state._timer.pausedAt = state.clock;
|
||||
return true;
|
||||
},
|
||||
{
|
||||
sideEffect(hasChanged: boolean) {
|
||||
if (hasChanged) {
|
||||
integrationService.dispatch(TimerLifeCycle.onPause);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
resume(event: OntimeEvent, restorePoint: RestorePoint) {
|
||||
mutate((state) => {
|
||||
state.clock = clock.timeNow();
|
||||
|
||||
state.timer.startedAt = restorePoint.startedAt;
|
||||
state.timer.duration = calculateDuration(event.timeStart, event.timeEnd);
|
||||
state.timer.current = state.timer.duration;
|
||||
|
||||
state.timer.playback = restorePoint.playback;
|
||||
state._timer.pausedAt = restorePoint.pausedAt;
|
||||
state.timer.addedTime = restorePoint.addedTime;
|
||||
|
||||
// check if event finished meanwhile
|
||||
if (event.timerType === TimerType.TimeToEnd) {
|
||||
state.timer.current = getCurrent(state);
|
||||
}
|
||||
});
|
||||
},
|
||||
addTime(amount: number) {
|
||||
mutate((state) => {
|
||||
// TODO: what kind of validation go here or in the consumer?
|
||||
state.timer.addedTime += amount;
|
||||
state.timer.expectedFinish += amount;
|
||||
state.timer.current += amount;
|
||||
|
||||
// handle edge cases
|
||||
const willGoNegative = amount < 0 && Math.abs(amount) > state.timer.current;
|
||||
const hasFinished = state.timer.finishedAt !== null;
|
||||
if (willGoNegative && !hasFinished) {
|
||||
state.timer.finishedAt = clock.timeNow();
|
||||
} else {
|
||||
const willGoPositive = state.timer.current < 0 && state.timer.current + amount > 0;
|
||||
if (willGoPositive) {
|
||||
state.timer.finishedAt = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
// TODO: make options an object ??? maybe we can remove the options altogether?
|
||||
// TODO: should we have a semaphore to stop update while other things are running?
|
||||
update(force: boolean, updateInterval: number) {
|
||||
return mutate(
|
||||
(state) => {
|
||||
function roll() {
|
||||
const hasSkippedOutOfEvent = skippedOutOfEvent(state, previousTime, timeSkipLimit);
|
||||
if (hasSkippedOutOfEvent) {
|
||||
return { doRoll: true };
|
||||
}
|
||||
const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } = updateRoll(state);
|
||||
state.timer.current = updatedTimer;
|
||||
state.timer.secondaryTimer = updatedSecondaryTimer;
|
||||
state.timer.elapsed = state.timer.duration - state.timer.current;
|
||||
|
||||
return { doRoll: doRollLoad, isFinished };
|
||||
}
|
||||
|
||||
function play() {
|
||||
let isFinished = false;
|
||||
state.timer.current = getCurrent(state);
|
||||
|
||||
if (state.timer.playback === Playback.Play && state._timer.finishedNow) {
|
||||
state.timer.finishedAt = state.clock;
|
||||
isFinished = true;
|
||||
} else {
|
||||
state.timer.expectedFinish = getExpectedFinish(state);
|
||||
}
|
||||
|
||||
state.timer.elapsed = state.timer.duration - state.timer.current;
|
||||
|
||||
return { isFinished };
|
||||
}
|
||||
|
||||
// TODO: should this logic be moved up?
|
||||
// force indicates whether the state change should be broadcast to socket
|
||||
let _force = force;
|
||||
let _didUpdate = false;
|
||||
let _doRoll = false;
|
||||
let _isFinished = false;
|
||||
let _shouldNotify = false;
|
||||
|
||||
const previousTime = state.clock;
|
||||
state.clock = clock.timeNow();
|
||||
const hasSkippedBack = previousTime > state.clock;
|
||||
|
||||
if (hasSkippedBack) {
|
||||
_force = true;
|
||||
}
|
||||
|
||||
// we call integrations if we update timers
|
||||
if (state.timer.playback === Playback.Roll) {
|
||||
const result = roll();
|
||||
_shouldNotify = true;
|
||||
_doRoll = result.doRoll;
|
||||
_isFinished = result.isFinished;
|
||||
} else if (state.timer.startedAt !== null) {
|
||||
// we only update timer if a timer has been started
|
||||
const result = play();
|
||||
_shouldNotify = true;
|
||||
_isFinished = result.isFinished;
|
||||
} else if (state.eventNow?.timerType === TimerType.TimeToEnd) {
|
||||
// or if we are in a time-to-end timer
|
||||
state.timer.current = getCurrent(state);
|
||||
state.timer.duration = state.timer.current;
|
||||
}
|
||||
|
||||
// we only update the store at the updateInterval
|
||||
// side effects such as onFinish will still be triggered in the update functions
|
||||
const isTimeToUpdate = state.clock > state._timer.lastUpdate + updateInterval;
|
||||
if (_force || isTimeToUpdate) {
|
||||
state._timer.lastUpdate = state.clock;
|
||||
// TODO: can we simplify the didUpdate and shouldNotify
|
||||
_didUpdate = true;
|
||||
}
|
||||
|
||||
return {
|
||||
didUpdate: _didUpdate,
|
||||
doRoll: _doRoll,
|
||||
isFinished: _isFinished,
|
||||
shouldNotify: _shouldNotify,
|
||||
};
|
||||
},
|
||||
{
|
||||
sideEffect({ didUpdate, doRoll, isFinished, shouldNotify }, newState) {
|
||||
// TODO: we cant cyclic calls to PlaybackService
|
||||
if (didUpdate && shouldNotify) {
|
||||
// TODO: can we distinguish between a clock update and a timer update?
|
||||
integrationService.dispatch(TimerLifeCycle.onUpdate);
|
||||
}
|
||||
|
||||
if (doRoll) {
|
||||
runtimeService.roll();
|
||||
}
|
||||
|
||||
if (isFinished) {
|
||||
integrationService.dispatch(TimerLifeCycle.onFinish);
|
||||
|
||||
// handle end action if there was a timer playing
|
||||
if (newState.timer.playback === Playback.Play) {
|
||||
if (newState.eventNow.endAction === EndAction.Stop) {
|
||||
runtimeService.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(runtimeService.loadNext, 0);
|
||||
} else if (newState.eventNow.endAction === EndAction.PlayNext) {
|
||||
runtimeService.startNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
roll(rundown: OntimeEvent[]) {
|
||||
mutate((state) => {
|
||||
stateMutations.timer.clear();
|
||||
|
||||
const { nextEvent, currentEvent } = getRollTimers(rundown, state.clock);
|
||||
|
||||
if (currentEvent) {
|
||||
// there is something running, load
|
||||
state.timer.secondaryTimer = null;
|
||||
state._timer.secondaryTarget = null;
|
||||
|
||||
// account for event that finishes the day after
|
||||
const endTime =
|
||||
currentEvent.timeEnd < currentEvent.timeStart ? currentEvent.timeEnd + dayInMs : currentEvent.timeEnd;
|
||||
|
||||
// when we load a timer in roll, we do the same things as before
|
||||
// but also pre-populate some data as to the running state
|
||||
stateMutations.load(currentEvent, rundown, {
|
||||
startedAt: currentEvent.timeStart,
|
||||
expectedFinish: currentEvent.timeEnd,
|
||||
current: endTime - state.clock,
|
||||
});
|
||||
} else if (nextEvent) {
|
||||
// account for day after
|
||||
const nextStart = nextEvent.timeStart < state.clock ? nextEvent.timeStart + dayInMs : nextEvent.timeStart;
|
||||
// nothing now, but something coming up
|
||||
state.timer.secondaryTimer = nextStart - state.clock;
|
||||
state._timer.secondaryTarget = nextStart;
|
||||
}
|
||||
state.timer.playback = Playback.Roll;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* This function is the only way to write to the state.
|
||||
* Mock this in the unit tests to assert state transitions and side effects.
|
||||
*/
|
||||
export function mutate<R>(
|
||||
/**
|
||||
* A pure function that receives the current state and modifies it in place.
|
||||
* The function can return a value that will be passed to the side effects, and
|
||||
* will be returned to the caller of the mutation.
|
||||
*/
|
||||
fn: (state: DeepWritable<TState>) => R,
|
||||
opts?: {
|
||||
/**
|
||||
* Side effects are functions that execute code related to the change in state.
|
||||
*
|
||||
* @param result The result of the mutation function `fn`
|
||||
* @param newState The new state after the mutation
|
||||
* @param prevState The state before the mutation
|
||||
*/
|
||||
sideEffect?: (result: R, newState: TState, prevState: TState) => void;
|
||||
},
|
||||
) {
|
||||
const prevState = state;
|
||||
|
||||
const result = fn(state as DeepWritable<TState>);
|
||||
|
||||
const newState = state;
|
||||
|
||||
// run action specific side effect before global side effects
|
||||
opts?.sideEffect?.(result, newState, prevState);
|
||||
|
||||
// TODO: how would we handle granular updates
|
||||
// once more state is migrated here
|
||||
|
||||
// set eventStore any time a mutation happens
|
||||
// this means we are pushing this data to the client every 32ms
|
||||
eventStore.batchSet({
|
||||
clock: newState.clock,
|
||||
eventNow: newState.eventNow,
|
||||
publicEventNow: newState.publicEventNow,
|
||||
eventNext: newState.eventNext,
|
||||
publicEventNext: newState.publicEventNext,
|
||||
runtime: newState.runtime,
|
||||
timer: newState.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;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { OntimeEvent, Playback } from 'ontime-types';
|
||||
import { deepmerge } from 'ontime-utils';
|
||||
|
||||
import { RuntimeState, clear, getState, load, pause, start, stop } from '../runtimeState.js';
|
||||
|
||||
const mockEvent = {
|
||||
id: 'mock',
|
||||
cue: 'mock',
|
||||
timeStart: 0,
|
||||
timeEnd: 1000,
|
||||
duration: 1000,
|
||||
} as OntimeEvent;
|
||||
|
||||
const mockState = {
|
||||
clock: 666,
|
||||
eventNow: null,
|
||||
publicEventNow: null,
|
||||
eventNext: null,
|
||||
publicEventNext: null,
|
||||
runtime: {
|
||||
selectedEventIndex: null,
|
||||
numEvents: 0,
|
||||
},
|
||||
timer: {
|
||||
addedTime: 0,
|
||||
current: null,
|
||||
duration: null,
|
||||
elapsed: null,
|
||||
expectedFinish: null,
|
||||
finishedAt: null,
|
||||
playback: Playback.Stop,
|
||||
secondaryTimer: null,
|
||||
startedAt: null,
|
||||
},
|
||||
_timer: {
|
||||
pausedAt: null,
|
||||
lastUpdate: null,
|
||||
secondaryTarget: null,
|
||||
},
|
||||
} as RuntimeState;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const makeMockState = (patch: RuntimeState): RuntimeState => {
|
||||
return deepmerge(mockState, patch);
|
||||
};
|
||||
|
||||
describe('mutation on runtimeState', () => {
|
||||
beforeEach(() => {
|
||||
clear();
|
||||
|
||||
vi.mock('../../classes/event-loader/EventLoader.js', () => ({
|
||||
EventLoader: {
|
||||
getPlayableEvents: vi.fn().mockReturnValue([
|
||||
{
|
||||
id: 'mock',
|
||||
cue: 'mock',
|
||||
timeStart: 0,
|
||||
timeEnd: 1000,
|
||||
duration: 1000,
|
||||
},
|
||||
]),
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('playback operations', () => {
|
||||
it('refuses if nothing is loaded', () => {
|
||||
let success = start(mockState);
|
||||
expect(success).toBe(false);
|
||||
|
||||
success = pause();
|
||||
expect(success).toBe(false);
|
||||
});
|
||||
test('normal playback cycle', () => {
|
||||
// 1. Load event
|
||||
load(mockEvent, [mockEvent]);
|
||||
let newState = getState();
|
||||
expect(newState.eventNow.id).toBe(mockEvent.id);
|
||||
expect(newState.timer.playback).toBe(Playback.Armed);
|
||||
expect(newState.clock).not.toBe(666);
|
||||
|
||||
// 2. Start event
|
||||
let success = start();
|
||||
newState = getState();
|
||||
expect(success).toBe(true);
|
||||
expect(newState.timer).toMatchObject({
|
||||
playback: Playback.Play,
|
||||
});
|
||||
|
||||
// 3. Pause event
|
||||
success = pause();
|
||||
newState = getState();
|
||||
expect(success).toBe(true);
|
||||
expect(newState.clock).not.toBe(666);
|
||||
expect(newState.timer).toMatchObject({
|
||||
playback: Playback.Pause,
|
||||
addedTime: 0,
|
||||
});
|
||||
expect(newState._timer.pausedAt).toEqual(newState.clock);
|
||||
|
||||
success = pause();
|
||||
expect(success).toBe(false);
|
||||
|
||||
// 4. Restart event
|
||||
success = start();
|
||||
newState = getState();
|
||||
expect(success).toBe(true);
|
||||
expect(newState.timer).toMatchObject({
|
||||
playback: Playback.Play,
|
||||
secondaryTimer: null,
|
||||
});
|
||||
expect(newState.timer).toEqual(
|
||||
expect.objectContaining({
|
||||
current: expect.any(Number),
|
||||
duration: expect.any(Number),
|
||||
elapsed: expect.any(Number),
|
||||
expectedFinish: expect.any(Number),
|
||||
startedAt: expect.any(Number),
|
||||
}),
|
||||
);
|
||||
expect(newState._timer.pausedAt).toBeNull();
|
||||
|
||||
// 4. Stop event
|
||||
success = stop();
|
||||
expect(success).toBe(true);
|
||||
expect(newState.eventNow).toBe(null);
|
||||
expect(newState.timer).toMatchObject({
|
||||
playback: Playback.Stop,
|
||||
duration: null,
|
||||
elapsed: null,
|
||||
expectedFinish: null,
|
||||
startedAt: null,
|
||||
});
|
||||
});
|
||||
|
||||
test.todo('roll mode', () => {});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,415 @@
|
||||
import { Runtime, OntimeEvent, Playback, TimerState, TimerType, MaybeNumber } from 'ontime-types';
|
||||
import { calculateDuration, dayInMs } from 'ontime-utils';
|
||||
|
||||
import { clock } from '../services/Clock.js';
|
||||
import { RestorePoint } from '../services/RestoreService.js';
|
||||
import { getCurrent, getExpectedFinish, getRollTimers, skippedOutOfEvent, updateRoll } from '../services/timerUtils.js';
|
||||
import { EventLoader } from '../classes/event-loader/EventLoader.js';
|
||||
import { timerConfig } from '../config/config.js';
|
||||
|
||||
const initialRuntime: Runtime = {
|
||||
selectedEventIndex: null,
|
||||
numEvents: 0,
|
||||
};
|
||||
|
||||
const initialTimer: TimerState = {
|
||||
addedTime: 0,
|
||||
current: null,
|
||||
duration: null,
|
||||
elapsed: null,
|
||||
expectedFinish: null, // TODO: expected finish could account for midnight, we cleanup in the clients
|
||||
finishedAt: null,
|
||||
playback: Playback.Stop,
|
||||
secondaryTimer: null,
|
||||
startedAt: null,
|
||||
};
|
||||
|
||||
export type RuntimeState = {
|
||||
clock: number; // realtime clock
|
||||
eventNow: OntimeEvent | null;
|
||||
publicEventNow: OntimeEvent | null;
|
||||
eventNext: OntimeEvent | null;
|
||||
publicEventNext: OntimeEvent | null;
|
||||
runtime: Runtime;
|
||||
timer: TimerState;
|
||||
// private properties of the timer calculations
|
||||
_timer: {
|
||||
pausedAt: MaybeNumber;
|
||||
finishedNow: boolean;
|
||||
lastUpdate: MaybeNumber;
|
||||
secondaryTarget: MaybeNumber;
|
||||
};
|
||||
};
|
||||
|
||||
const runtimeState: RuntimeState = {
|
||||
clock: clock.timeNow(),
|
||||
eventNow: null,
|
||||
publicEventNow: null,
|
||||
eventNext: null,
|
||||
publicEventNext: null,
|
||||
runtime: initialRuntime,
|
||||
timer: { ...initialTimer },
|
||||
_timer: {
|
||||
pausedAt: null,
|
||||
lastUpdate: null,
|
||||
secondaryTarget: null,
|
||||
get finishedNow() {
|
||||
return this.current <= 0 && this.finishedAt === null;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function getState(): Readonly<RuntimeState> {
|
||||
return runtimeState;
|
||||
}
|
||||
|
||||
export function clear() {
|
||||
// TODO: check that entire state is reset here
|
||||
runtimeState.eventNow = null;
|
||||
runtimeState.publicEventNow = null;
|
||||
runtimeState.eventNext = null;
|
||||
runtimeState.publicEventNext = null;
|
||||
|
||||
runtimeState.runtime = { ...initialRuntime };
|
||||
runtimeState.runtime.numEvents = fetchNumEvents();
|
||||
|
||||
runtimeState.timer.playback = Playback.Stop;
|
||||
runtimeState.clock = clock.timeNow();
|
||||
runtimeState.timer = { ...initialTimer };
|
||||
runtimeState._timer = {
|
||||
pausedAt: null,
|
||||
lastUpdate: null,
|
||||
secondaryTarget: null,
|
||||
finishedNow: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to allow modifying the state from the outside
|
||||
* @param newState
|
||||
*/
|
||||
function patchTimer(newState: Partial<TimerState>) {
|
||||
for (const key in newState) {
|
||||
if (key in runtimeState.timer) {
|
||||
runtimeState.timer[key] = newState[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility, getches number of events from EventLoader
|
||||
* @param numEvents
|
||||
*/
|
||||
function fetchNumEvents(): number {
|
||||
// TODO: could we avoid having this dependency?
|
||||
return EventLoader.getPlayableEvents().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility, allows updating the number of events
|
||||
* @param numEvents
|
||||
*/
|
||||
export function updateNumEvents(numEvents: number) {
|
||||
runtimeState.runtime.numEvents = numEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a given event into state
|
||||
* @param event
|
||||
* @param rundown
|
||||
* @param initialData
|
||||
*/
|
||||
export function load(event: OntimeEvent, rundown: OntimeEvent[], initialData?: Partial<TimerState>) {
|
||||
clear();
|
||||
|
||||
const eventIndex = rundown.findIndex((eventInMemory) => eventInMemory.id === event.id);
|
||||
|
||||
runtimeState.runtime.selectedEventIndex = eventIndex;
|
||||
runtimeState.runtime.numEvents = rundown.length;
|
||||
|
||||
loadNow(event, rundown);
|
||||
loadNext(rundown);
|
||||
|
||||
runtimeState.clock = clock.timeNow();
|
||||
runtimeState.timer.playback = Playback.Armed;
|
||||
runtimeState.timer.duration = calculateDuration(event.timeStart, event.timeEnd);
|
||||
runtimeState.timer.current = getCurrent(runtimeState);
|
||||
|
||||
if (initialData) {
|
||||
patchTimer(initialData);
|
||||
}
|
||||
}
|
||||
|
||||
export function loadNow(event: OntimeEvent, playableEvents: OntimeEvent[]) {
|
||||
runtimeState.eventNow = event;
|
||||
|
||||
// check if current is also public
|
||||
if (event.isPublic) {
|
||||
runtimeState.publicEventNow = event;
|
||||
} else {
|
||||
// assume there is no public event
|
||||
runtimeState.publicEventNow = null;
|
||||
|
||||
// if there is nothing before, return
|
||||
if (!runtimeState.runtime.selectedEventIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
// iterate backwards to find it
|
||||
for (let i = runtimeState.runtime.selectedEventIndex; i >= 0; i--) {
|
||||
if (playableEvents[i].isPublic) {
|
||||
runtimeState.publicEventNow = playableEvents[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function loadNext(playableEvents: OntimeEvent[]) {
|
||||
// assume there are no next events
|
||||
runtimeState.eventNext = null;
|
||||
runtimeState.publicEventNext = null;
|
||||
|
||||
if (runtimeState.runtime.selectedEventIndex === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const numEvents = playableEvents.length;
|
||||
|
||||
if (runtimeState.runtime.selectedEventIndex < numEvents - 1) {
|
||||
let nextPublic = false;
|
||||
let nextProduction = false;
|
||||
|
||||
for (let i = runtimeState.runtime.selectedEventIndex + 1; i < numEvents; i++) {
|
||||
// if we have not set private
|
||||
if (!nextProduction) {
|
||||
runtimeState.eventNext = playableEvents[i];
|
||||
nextProduction = true;
|
||||
}
|
||||
|
||||
// if event is public
|
||||
if (playableEvents[i].isPublic) {
|
||||
runtimeState.publicEventNext = playableEvents[i];
|
||||
nextPublic = true;
|
||||
}
|
||||
|
||||
// Stop if both are set
|
||||
if (nextPublic && nextProduction) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function resume(restorePoint: RestorePoint, event: OntimeEvent, rundown: OntimeEvent[]) {
|
||||
load(event, rundown, restorePoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* We only pass an event if we are hot reloading
|
||||
* @param {OntimeEvent} event only passed if we are changing the data if a playing timer
|
||||
*/
|
||||
export function reload(event?: OntimeEvent) {
|
||||
if (event) {
|
||||
runtimeState.eventNow = event;
|
||||
|
||||
// update data which is duplicate between eventNow and timer objects
|
||||
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd);
|
||||
runtimeState.timer.current = getCurrent(runtimeState);
|
||||
runtimeState.timer.expectedFinish = getExpectedFinish(runtimeState);
|
||||
return;
|
||||
}
|
||||
runtimeState.timer.playback = Playback.Armed;
|
||||
|
||||
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd);
|
||||
runtimeState.timer.current = runtimeState.timer.duration;
|
||||
runtimeState.timer.elapsed = null;
|
||||
|
||||
runtimeState.timer.startedAt = null;
|
||||
runtimeState.timer.finishedAt = null;
|
||||
runtimeState._timer.pausedAt = null;
|
||||
runtimeState.timer.addedTime = 0;
|
||||
|
||||
runtimeState.timer.expectedFinish = getExpectedFinish(runtimeState);
|
||||
}
|
||||
|
||||
export function start(state: RuntimeState = runtimeState): boolean {
|
||||
if (state.eventNow === null) {
|
||||
return false;
|
||||
}
|
||||
if (state.timer.playback === Playback.Play) {
|
||||
return false;
|
||||
}
|
||||
state.clock = clock.timeNow();
|
||||
state.timer.secondaryTimer = null;
|
||||
state._timer.secondaryTarget = null;
|
||||
|
||||
// add paused time if it exists
|
||||
if (state._timer.pausedAt) {
|
||||
const timeToAdd = state.clock - state._timer.pausedAt;
|
||||
state.timer.addedTime += timeToAdd;
|
||||
state._timer.pausedAt = null;
|
||||
}
|
||||
|
||||
if (state.timer.startedAt === null) {
|
||||
state.timer.startedAt = state.clock;
|
||||
}
|
||||
|
||||
state.timer.playback = Playback.Play;
|
||||
state.timer.expectedFinish = getExpectedFinish(state);
|
||||
state.timer.elapsed = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function pause(state: RuntimeState = runtimeState): boolean {
|
||||
if (state.timer.playback !== Playback.Play) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state.timer.playback = Playback.Pause;
|
||||
state.clock = clock.timeNow();
|
||||
state._timer.pausedAt = state.clock;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function stop(state: RuntimeState = runtimeState): boolean {
|
||||
if (state.timer.playback === Playback.Stop) {
|
||||
return false;
|
||||
}
|
||||
clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
export function addTime(amount: number) {
|
||||
if (runtimeState.timer.current === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
runtimeState.timer.addedTime += amount;
|
||||
runtimeState.timer.expectedFinish += amount;
|
||||
runtimeState.timer.current += amount;
|
||||
|
||||
// handle edge cases
|
||||
const willGoNegative = amount < 0 && Math.abs(amount) > runtimeState.timer.current;
|
||||
const hasFinished = runtimeState.timer.finishedAt !== null;
|
||||
if (willGoNegative && !hasFinished) {
|
||||
runtimeState.timer.finishedAt = clock.timeNow();
|
||||
} else {
|
||||
const willGoPositive = runtimeState.timer.current < 0 && runtimeState.timer.current + amount > 0;
|
||||
if (willGoPositive) {
|
||||
runtimeState.timer.finishedAt = null;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function update(force: boolean, updateInterval: number) {
|
||||
// TODO: should this logic be moved to consumer?
|
||||
// force indicates whether the state change should be broadcast to socket
|
||||
let _force = force;
|
||||
let _didUpdate = false;
|
||||
let _doRoll = false;
|
||||
let _isFinished = false;
|
||||
let _shouldNotify = false;
|
||||
|
||||
const previousTime = runtimeState.clock;
|
||||
runtimeState.clock = clock.timeNow();
|
||||
const hasSkippedBack = previousTime > runtimeState.clock;
|
||||
|
||||
if (hasSkippedBack) {
|
||||
_force = true;
|
||||
}
|
||||
|
||||
// we call integrations if we update timers
|
||||
if (runtimeState.timer.playback === Playback.Roll) {
|
||||
const result = roll();
|
||||
_shouldNotify = true;
|
||||
_doRoll = result.doRoll;
|
||||
_isFinished = result.isFinished;
|
||||
} else if (runtimeState.timer.startedAt !== null) {
|
||||
// we only update timer if a timer has been started
|
||||
const result = play();
|
||||
_shouldNotify = true;
|
||||
_isFinished = result.isFinished;
|
||||
} else if (runtimeState.eventNow?.timerType === TimerType.TimeToEnd) {
|
||||
// or if we are in a time-to-end timer
|
||||
runtimeState.timer.current = getCurrent(runtimeState);
|
||||
runtimeState.timer.duration = runtimeState.timer.current;
|
||||
}
|
||||
|
||||
// we only update the store at the updateInterval
|
||||
// side effects such as onFinish will still be triggered in the update functions
|
||||
const isTimeToUpdate = runtimeState.clock > runtimeState._timer.lastUpdate + updateInterval;
|
||||
if (_force || isTimeToUpdate) {
|
||||
runtimeState._timer.lastUpdate = runtimeState.clock;
|
||||
// TODO: can we simplify the didUpdate and shouldNotify
|
||||
_didUpdate = true;
|
||||
}
|
||||
|
||||
return {
|
||||
didUpdate: _didUpdate,
|
||||
doRoll: _doRoll,
|
||||
isFinished: _isFinished,
|
||||
shouldNotify: _shouldNotify,
|
||||
};
|
||||
|
||||
function roll() {
|
||||
const hasSkippedOutOfEvent = skippedOutOfEvent(runtimeState, previousTime, timerConfig.timeSkipLimit);
|
||||
if (hasSkippedOutOfEvent) {
|
||||
return { doRoll: true };
|
||||
}
|
||||
const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } = updateRoll(runtimeState);
|
||||
runtimeState.timer.current = updatedTimer;
|
||||
runtimeState.timer.secondaryTimer = updatedSecondaryTimer;
|
||||
runtimeState.timer.elapsed = runtimeState.timer.duration - runtimeState.timer.current;
|
||||
|
||||
return { doRoll: doRollLoad, isFinished };
|
||||
}
|
||||
|
||||
function play() {
|
||||
let isFinished = false;
|
||||
runtimeState.timer.current = getCurrent(runtimeState);
|
||||
|
||||
if (runtimeState.timer.playback === Playback.Play && runtimeState._timer.finishedNow) {
|
||||
runtimeState.timer.finishedAt = runtimeState.clock;
|
||||
isFinished = true;
|
||||
} else {
|
||||
runtimeState.timer.expectedFinish = getExpectedFinish(runtimeState);
|
||||
}
|
||||
|
||||
runtimeState.timer.elapsed = runtimeState.timer.duration - runtimeState.timer.current;
|
||||
|
||||
return { isFinished };
|
||||
}
|
||||
}
|
||||
|
||||
export function roll(rundown: OntimeEvent[]) {
|
||||
clear();
|
||||
|
||||
runtimeState.runtime.numEvents = rundown.length;
|
||||
const { nextEvent, currentEvent } = getRollTimers(rundown, runtimeState.clock);
|
||||
|
||||
if (currentEvent) {
|
||||
// there is something running, load
|
||||
runtimeState.timer.secondaryTimer = null;
|
||||
runtimeState._timer.secondaryTarget = null;
|
||||
|
||||
// account for event that finishes the day after
|
||||
const endTime =
|
||||
currentEvent.timeEnd < currentEvent.timeStart ? currentEvent.timeEnd + dayInMs : currentEvent.timeEnd;
|
||||
|
||||
// when we load a timer in roll, we do the same things as before
|
||||
// but also pre-populate some data as to the running state
|
||||
load(currentEvent, rundown, {
|
||||
startedAt: currentEvent.timeStart,
|
||||
expectedFinish: currentEvent.timeEnd,
|
||||
current: endTime - runtimeState.clock,
|
||||
});
|
||||
} else if (nextEvent) {
|
||||
// account for day after
|
||||
const nextStart = nextEvent.timeStart < runtimeState.clock ? nextEvent.timeStart + dayInMs : nextEvent.timeStart;
|
||||
// nothing now, but something coming up
|
||||
runtimeState.timer.secondaryTimer = nextStart - runtimeState.clock;
|
||||
runtimeState._timer.secondaryTarget = nextStart;
|
||||
}
|
||||
|
||||
runtimeState.timer.playback = Playback.Roll;
|
||||
}
|
||||
Reference in New Issue
Block a user