Files
ontime/apps/server/src/services/TimerService.ts
T
2024-01-31 14:02:27 +01:00

147 lines
4.3 KiB
TypeScript

import { EndAction, OntimeEvent, Playback, TimerLifeCycle } from 'ontime-types';
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
*/
export class TimerService {
private _interval: NodeJS.Timer;
private _updateInterval: number;
private _refreshInterval: number;
/**
* @constructor
* @param {number} [timerConfig.refresh]
* @param {number} [timerConfig.updateInterval]
*/
constructor(timerConfig: { refresh: number; updateInterval: number }) {
this._refreshInterval = timerConfig.refresh;
this._updateInterval = timerConfig.updateInterval;
this._interval = setInterval(this.update, 32);
}
@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);
}
}
@broadcastResult
pause() {
if (runtimeState.pause()) {
integrationService.dispatch(TimerLifeCycle.onPause);
}
}
@broadcastResult
stop() {
if (runtimeState.stop()) {
integrationService.dispatch(TimerLifeCycle.onStop);
}
}
/**
* Adds time to running timer by given amount
* @param {number} 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) {
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();
}
}
}
}
/**
* 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);
}
}
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;
}