mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-12 19:03:47 +00:00
refactor: partial state updates (#780)
* refactor: partial state updates * refactor: less calls to restore * refactor: add error stack trace to log
This commit is contained in:
@@ -1,52 +1,88 @@
|
||||
import { EndAction, OntimeEvent, Playback, TimerLifeCycle } from 'ontime-types';
|
||||
import { OntimeEvent, RuntimeStore } from 'ontime-types';
|
||||
|
||||
import { deepEqual } from 'fast-equals';
|
||||
|
||||
import * as runtimeState from '../stores/runtimeState.js';
|
||||
import { integrationService } from './integration-service/IntegrationService.js';
|
||||
import { eventStore } from '../stores/EventStore.js';
|
||||
import * as runtimeState from '../stores/runtimeState.js';
|
||||
import type { RuntimeState, UpdateResult } from '../stores/runtimeState.js';
|
||||
|
||||
import { restoreService } from './RestoreService.js';
|
||||
import { runtimeService } from './runtime-service/RuntimeService.js';
|
||||
import { getPlayableEvents } from './rundown-service/RundownService.js';
|
||||
|
||||
/**
|
||||
* Service manages Ontime's main timer
|
||||
* It is responsible for streaming the data to the event store
|
||||
*/
|
||||
export class TimerService {
|
||||
private _interval: NodeJS.Timer;
|
||||
private _updateInterval: number;
|
||||
private _refreshInterval: number;
|
||||
private readonly _interval: NodeJS.Timer;
|
||||
/** how often we update the socket */
|
||||
static _updateInterval: number;
|
||||
/** how often we recalculate */
|
||||
static _refreshInterval: number;
|
||||
/** last time we updated the socket */
|
||||
static previousUpdate: number;
|
||||
/** last known state */
|
||||
static previousState: RuntimeState;
|
||||
|
||||
/** when timer will be finished */
|
||||
private endCallback: NodeJS.Timer;
|
||||
|
||||
private onUpdateCallback: (updateResult: UpdateResult) => void;
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {number} [timerConfig.refresh]
|
||||
* @param {number} [timerConfig.updateInterval]
|
||||
* @param {number} [timerConfig.refresh] how often we recalculate
|
||||
* @param {number} [timerConfig.updateInterval] how often we update the socket
|
||||
* @param {function} [timerConfig.onUpdateCallback] how often we update the socket
|
||||
*/
|
||||
constructor(timerConfig: { refresh: number; updateInterval: number }) {
|
||||
this._refreshInterval = timerConfig.refresh;
|
||||
this._updateInterval = timerConfig.updateInterval;
|
||||
this._interval = setInterval(this.update, 32);
|
||||
constructor(timerConfig: {
|
||||
refresh: number;
|
||||
updateInterval: number;
|
||||
onUpdateCallback: (updateResult: UpdateResult) => void;
|
||||
}) {
|
||||
TimerService.previousUpdate = -1;
|
||||
TimerService.previousState = {} as RuntimeState;
|
||||
|
||||
TimerService._updateInterval = timerConfig.updateInterval;
|
||||
TimerService._refreshInterval = timerConfig.refresh;
|
||||
|
||||
this.onUpdateCallback = timerConfig.onUpdateCallback;
|
||||
this._interval = setInterval(() => {
|
||||
this.update();
|
||||
this.onUpdateCallback;
|
||||
}, TimerService._updateInterval);
|
||||
}
|
||||
|
||||
@broadcastResult
|
||||
start() {
|
||||
// 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
|
||||
if (runtimeState.start()) {
|
||||
integrationService.dispatch(TimerLifeCycle.onStart);
|
||||
if (!runtimeState.start()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const state = runtimeState.getState();
|
||||
this.endCallback = setTimeout(this.update, state.timer.expectedFinish);
|
||||
return true;
|
||||
}
|
||||
|
||||
@broadcastResult
|
||||
pause() {
|
||||
if (runtimeState.pause()) {
|
||||
integrationService.dispatch(TimerLifeCycle.onPause);
|
||||
if (!runtimeState.pause()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// cancel end callback
|
||||
clearTimeout(this.endCallback);
|
||||
return true;
|
||||
}
|
||||
|
||||
@broadcastResult
|
||||
stop() {
|
||||
if (runtimeState.stop()) {
|
||||
integrationService.dispatch(TimerLifeCycle.onStop);
|
||||
if (!runtimeState.stop()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// cancel end callback
|
||||
clearTimeout(this.endCallback);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,62 +91,40 @@ export class TimerService {
|
||||
*/
|
||||
@broadcastResult
|
||||
addTime(amount: number): boolean {
|
||||
return runtimeState.addTime(amount);
|
||||
if (!runtimeState.addTime(amount)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// renew end callback
|
||||
clearTimeout(this.endCallback);
|
||||
const state = runtimeState.getState();
|
||||
this.endCallback = setTimeout(this.update, state.timer.expectedFinish);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the app at regular intervals
|
||||
* @param {boolean} force whether we should force a broadcast of state
|
||||
*/
|
||||
@broadcastResult
|
||||
update(force = false) {
|
||||
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);
|
||||
}
|
||||
update() {
|
||||
const updateResult = runtimeState.update();
|
||||
|
||||
if (doRoll) {
|
||||
// TODO: escalate to parent
|
||||
const rundown = 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 && newState.eventNow) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
// pass the result to the parent
|
||||
this.onUpdateCallback(updateResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads roll information into timer service
|
||||
* @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');
|
||||
}
|
||||
|
||||
runtimeState.roll(rundown);
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
clearInterval(this._interval);
|
||||
clearTimeout(this.endCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,29 +132,64 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
descriptor.value = function (...args: any[]) {
|
||||
// call the original method and get the state
|
||||
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 do the comparison by explicitly for each property
|
||||
// to apply custom logic for different datasets
|
||||
|
||||
// some of the data, we only update at intervals
|
||||
const isTimeToUpdate = state.clock - TimerService.previousUpdate >= TimerService._updateInterval;
|
||||
|
||||
// some changes need an immediate update
|
||||
const hasSkippedBack = state.clock < TimerService.previousUpdate;
|
||||
const justStarted = !TimerService.previousState?.timer;
|
||||
const hasChangedPlayback = TimerService.previousState.timer?.playback !== state.timer.playback;
|
||||
const hasImmediateChanges = hasSkippedBack || justStarted || hasChangedPlayback;
|
||||
|
||||
if (hasImmediateChanges || (isTimeToUpdate && !deepEqual(TimerService.previousState?.timer, state.timer))) {
|
||||
eventStore.set('timer', state.timer);
|
||||
TimerService.previousState.timer = { ...state.timer };
|
||||
}
|
||||
|
||||
if (isTimeToUpdate && !deepEqual(TimerService.previousState?.runtime, state.runtime)) {
|
||||
eventStore.set('runtime', state.runtime);
|
||||
TimerService.previousState.runtime = { ...state.runtime };
|
||||
}
|
||||
|
||||
// Update the events if they have changed
|
||||
updateEventIfChanged('eventNow', state);
|
||||
updateEventIfChanged('publicEventNow', state);
|
||||
updateEventIfChanged('eventNext', state);
|
||||
updateEventIfChanged('publicEventNext', state);
|
||||
|
||||
if (isTimeToUpdate) {
|
||||
TimerService.previousUpdate = state.clock;
|
||||
eventStore.set('clock', state.clock);
|
||||
saveRestoreState(state);
|
||||
}
|
||||
|
||||
// Helper function to update an event if it has changed
|
||||
function updateEventIfChanged(eventKey: keyof RuntimeStore, state: RuntimeState) {
|
||||
if (!deepEqual(TimerService.previousState?.[eventKey], state[eventKey])) {
|
||||
eventStore.set(eventKey, state[eventKey]);
|
||||
TimerService.previousState[eventKey] = { ...state[eventKey] };
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to save the restore state
|
||||
function saveRestoreState(state: RuntimeState) {
|
||||
restoreService.save({
|
||||
playback: state.timer.playback,
|
||||
selectedEventId: state.eventNow?.id ?? null,
|
||||
startedAt: state.timer.startedAt,
|
||||
addedTime: state.timer.addedTime,
|
||||
pausedAt: state._timer.pausedAt,
|
||||
firstStart: state.runtime.actualStart,
|
||||
});
|
||||
}
|
||||
|
||||
// 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,
|
||||
firstStart: state.runtime.actualStart,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user