mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-19 14:14:17 +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:
@@ -29,7 +29,7 @@
|
|||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"typeface-open-sans": "^1.1.13",
|
"typeface-open-sans": "^1.1.13",
|
||||||
"web-vitals": "^3.1.1",
|
"web-vitals": "^3.1.1",
|
||||||
"zustand": "^4.4.7"
|
"zustand": "^4.5.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ export const LOWER_THIRD_OPTIONS: ParamField[] = [
|
|||||||
title: 'Title',
|
title: 'Title',
|
||||||
subtitle: 'Subtitle',
|
subtitle: 'Subtitle',
|
||||||
presenter: 'Presenter',
|
presenter: 'Presenter',
|
||||||
lowerMsg: 'Lower Thrid Message',
|
lowerMsg: 'Lower Third Message',
|
||||||
},
|
},
|
||||||
defaultValue: 'title',
|
defaultValue: 'title',
|
||||||
},
|
},
|
||||||
@@ -260,7 +260,7 @@ export const LOWER_THIRD_OPTIONS: ParamField[] = [
|
|||||||
title: 'Title',
|
title: 'Title',
|
||||||
subtitle: 'Subtitle',
|
subtitle: 'Subtitle',
|
||||||
presenter: 'Presenter',
|
presenter: 'Presenter',
|
||||||
lowerMsg: 'Lower Thrid Message',
|
lowerMsg: 'Lower Third Message',
|
||||||
},
|
},
|
||||||
defaultValue: 'subtitle',
|
defaultValue: 'subtitle',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -68,3 +68,14 @@ export const runtimeStore = createWithEqualityFn<RuntimeStore>(
|
|||||||
|
|
||||||
export const useRuntimeStore = <T>(selector: (state: RuntimeStore) => T) =>
|
export const useRuntimeStore = <T>(selector: (state: RuntimeStore) => T) =>
|
||||||
useStoreWithEqualityFn(runtimeStore, selector, deepCompare);
|
useStoreWithEqualityFn(runtimeStore, selector, deepCompare);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows patching a property of the runtime store
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
export function patchRuntime<K extends keyof RuntimeStore>(key: K, value: RuntimeStore[K]): void {
|
||||||
|
const state = runtimeStore.getState();
|
||||||
|
state[key] = value;
|
||||||
|
runtimeStore.setState({ ...state });
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { isProduction, RUNTIME, websocketUrl } from '../api/apiConstants';
|
|||||||
import { ontimeQueryClient } from '../queryClient';
|
import { ontimeQueryClient } from '../queryClient';
|
||||||
import { socketClientName } from '../stores/connectionName';
|
import { socketClientName } from '../stores/connectionName';
|
||||||
import { addLog } from '../stores/logger';
|
import { addLog } from '../stores/logger';
|
||||||
import { runtimeStore } from '../stores/runtime';
|
import { patchRuntime, runtimeStore } from '../stores/runtime';
|
||||||
|
|
||||||
export let websocket: WebSocket | null = null;
|
export let websocket: WebSocket | null = null;
|
||||||
let reconnectTimeout: NodeJS.Timeout | null = null;
|
let reconnectTimeout: NodeJS.Timeout | null = null;
|
||||||
@@ -12,6 +12,7 @@ const reconnectInterval = 1000;
|
|||||||
export let shouldReconnect = true;
|
export let shouldReconnect = true;
|
||||||
export let hasConnected = false;
|
export let hasConnected = false;
|
||||||
export let reconnectAttempts = 0;
|
export let reconnectAttempts = 0;
|
||||||
|
|
||||||
export const connectSocket = (preferredClientName?: string) => {
|
export const connectSocket = (preferredClientName?: string) => {
|
||||||
websocket = new WebSocket(websocketUrl);
|
websocket = new WebSocket(websocketUrl);
|
||||||
|
|
||||||
@@ -52,7 +53,6 @@ export const connectSocket = (preferredClientName?: string) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: implement partial store updates
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'client-name': {
|
case 'client-name': {
|
||||||
socketClientName.getState().setName(payload);
|
socketClientName.getState().setName(payload);
|
||||||
@@ -69,34 +69,54 @@ export const connectSocket = (preferredClientName?: string) => {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ontime-playback': {
|
case 'ontime-clock': {
|
||||||
const state = runtimeStore.getState();
|
patchRuntime('clock', payload);
|
||||||
state.timer.playback = payload;
|
updateDevTools({ clock: payload });
|
||||||
runtimeStore.setState(state);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ontime-timer': {
|
case 'ontime-timer': {
|
||||||
const state = runtimeStore.getState();
|
patchRuntime('timer', payload);
|
||||||
state.timer = payload;
|
updateDevTools({ timer: payload });
|
||||||
runtimeStore.setState(state);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'ontime-runtime': {
|
|
||||||
const state = runtimeStore.getState();
|
|
||||||
state.runtime = payload;
|
|
||||||
runtimeStore.setState(state);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'ontime-message': {
|
|
||||||
const state = runtimeStore.getState();
|
|
||||||
state.message = payload;
|
|
||||||
runtimeStore.setState(state);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ontime-onAir': {
|
case 'ontime-onAir': {
|
||||||
const state = runtimeStore.getState();
|
patchRuntime('onAir', payload);
|
||||||
state.onAir = payload;
|
updateDevTools({ onAir: payload });
|
||||||
runtimeStore.setState(state);
|
break;
|
||||||
|
}
|
||||||
|
case 'ontime-message': {
|
||||||
|
patchRuntime('message', payload);
|
||||||
|
updateDevTools({ message: payload });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ontime-runtime': {
|
||||||
|
patchRuntime('runtime', payload);
|
||||||
|
updateDevTools({ runtime: payload });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ontime-eventNow': {
|
||||||
|
patchRuntime('eventNow', payload);
|
||||||
|
updateDevTools({ eventNow: payload });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ontime-publicEventNow': {
|
||||||
|
patchRuntime('publicEventNow', payload);
|
||||||
|
updateDevTools({ publicEventNow: payload });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ontime-eventNext': {
|
||||||
|
patchRuntime('eventNext', payload);
|
||||||
|
updateDevTools({ eventNext: payload });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ontime-publicEventNext': {
|
||||||
|
patchRuntime('publicEventNext', payload);
|
||||||
|
updateDevTools({ publicEventNext: payload });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ontime-timer1': {
|
||||||
|
patchRuntime('timer1', payload);
|
||||||
|
updateDevTools({ timer1: payload });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,3 +145,12 @@ export const socketSendJson = (type: string, payload?: unknown) => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function updateDevTools(newData: Partial<RuntimeStore>) {
|
||||||
|
if (!isProduction) {
|
||||||
|
ontimeQueryClient.setQueryData(RUNTIME, (oldData: RuntimeStore) => ({
|
||||||
|
...oldData,
|
||||||
|
...newData,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
export const cycles = [
|
import { TimerLifeCycle } from 'ontime-types';
|
||||||
|
|
||||||
|
type CycleLabel = {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
value: keyof typeof TimerLifeCycle;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cycles: CycleLabel[] = [
|
||||||
{ id: 1, label: 'On Load', value: 'onLoad' },
|
{ id: 1, label: 'On Load', value: 'onLoad' },
|
||||||
{ id: 2, label: 'On Start', value: 'onStart' },
|
{ id: 2, label: 'On Start', value: 'onStart' },
|
||||||
{ id: 3, label: 'On Pause', value: 'onPause' },
|
{ id: 3, label: 'On Pause', value: 'onPause' },
|
||||||
{ id: 4, label: 'On Stop', value: 'onStop' },
|
{ id: 4, label: 'On Stop', value: 'onStop' },
|
||||||
{ id: 5, label: 'Every second', value: 'onUpdate' },
|
{ id: 5, label: 'Every second', value: 'onClock' },
|
||||||
|
{ id: 5, label: 'On Timer Update', value: 'onUpdate' },
|
||||||
{ id: 6, label: 'On Finish', value: 'onFinish' },
|
{ id: 6, label: 'On Finish', value: 'onFinish' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"express-static-gzip": "^2.1.7",
|
"express-static-gzip": "^2.1.7",
|
||||||
"express-validator": "^6.14.2",
|
"express-validator": "^6.14.2",
|
||||||
|
"fast-equals": "^5.0.1",
|
||||||
"google-auth-library": "^9.4.2",
|
"google-auth-library": "^9.4.2",
|
||||||
"got": "^14.0.0",
|
"got": "^14.0.0",
|
||||||
"lowdb": "^7.0.1",
|
"lowdb": "^7.0.1",
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ import { restoreService } from './services/RestoreService.js';
|
|||||||
import { messageService } from './services/message-service/MessageService.js';
|
import { messageService } from './services/message-service/MessageService.js';
|
||||||
import { populateDemo } from './modules/loadDemo.js';
|
import { populateDemo } from './modules/loadDemo.js';
|
||||||
import { getState, updateRundownData } from './stores/runtimeState.js';
|
import { getState, updateRundownData } from './stores/runtimeState.js';
|
||||||
import { setRundown, getPlayableEvents } from './services/rundown-service/RundownService.js';
|
import { setRundown } from './services/rundown-service/RundownService.js';
|
||||||
|
import { getPlayableEvents } from './services/rundown-service/rundownUtils.js';
|
||||||
|
|
||||||
console.log(`Starting Ontime version ${ONTIME_VERSION}`);
|
console.log(`Starting Ontime version ${ONTIME_VERSION}`);
|
||||||
|
|
||||||
@@ -284,11 +285,13 @@ export const shutdown = async (exitCode = 0) => {
|
|||||||
process.on('exit', (code) => console.log(`Ontime shutdown with code: ${code}`));
|
process.on('exit', (code) => console.log(`Ontime shutdown with code: ${code}`));
|
||||||
|
|
||||||
process.on('unhandledRejection', async (error) => {
|
process.on('unhandledRejection', async (error) => {
|
||||||
|
console.error('Error: unhandled rejection', error);
|
||||||
logger.error(LogOrigin.Server, `Error: unhandled rejection ${error}`);
|
logger.error(LogOrigin.Server, `Error: unhandled rejection ${error}`);
|
||||||
await shutdown(1);
|
await shutdown(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('uncaughtException', async (error) => {
|
process.on('uncaughtException', async (error) => {
|
||||||
|
console.error('Error: uncaught exception', error);
|
||||||
logger.error(LogOrigin.Server, `Error: uncaught exception ${error}`);
|
logger.error(LogOrigin.Server, `Error: uncaught exception ${error}`);
|
||||||
await shutdown(1);
|
await shutdown(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,5 +16,7 @@ export const config = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const timerConfig = {
|
export const timerConfig = {
|
||||||
timeSkipLimit: 1000,
|
skipLimit: 1000, // threshold of skip for recalculating
|
||||||
|
updateRate: 32, // how often do we update the timer
|
||||||
|
notificationRate: 1000, // how often do we notify clients and integrations
|
||||||
};
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { OntimeEvent, isKeyOfType, isOntimeEvent } from 'ontime-types';
|
import { OntimeEvent, isKeyOfType, isOntimeEvent } from 'ontime-types';
|
||||||
import { editEvent, getEventWithId } from '../services/rundown-service/RundownService.js';
|
import { editEvent } from '../services/rundown-service/RundownService.js';
|
||||||
|
import { getEventWithId } from '../services/rundown-service/rundownUtils.js';
|
||||||
import { coerceString, coerceNumber, coerceBoolean, coerceColour } from '../utils/coerceType.js';
|
import { coerceString, coerceNumber, coerceBoolean, coerceColour } from '../utils/coerceType.js';
|
||||||
|
|
||||||
const whitelistedPayload = {
|
const whitelistedPayload = {
|
||||||
@@ -48,21 +49,23 @@ export function updateEvent(
|
|||||||
newValue: OntimeEvent[typeof propertyName],
|
newValue: OntimeEvent[typeof propertyName],
|
||||||
) {
|
) {
|
||||||
const event = getEventWithId(eventId);
|
const event = getEventWithId(eventId);
|
||||||
if (event) {
|
if (!event) {
|
||||||
if (!isOntimeEvent(event)) {
|
|
||||||
throw new Error('Can only update events');
|
|
||||||
}
|
|
||||||
const propertiesToUpdate = { [propertyName]: newValue };
|
|
||||||
|
|
||||||
// Handles the special case for duration
|
|
||||||
// needs to be converted to milliseconds
|
|
||||||
if (propertyName === 'duration') {
|
|
||||||
propertiesToUpdate.duration = (newValue as number) * 1000;
|
|
||||||
propertiesToUpdate.timeEnd = event.timeStart + propertiesToUpdate.duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
editEvent({ id: eventId, ...propertiesToUpdate });
|
|
||||||
} else {
|
|
||||||
throw new Error(`Event with ID ${eventId} not found`);
|
throw new Error(`Event with ID ${eventId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isOntimeEvent(event)) {
|
||||||
|
throw new Error('Can only update events');
|
||||||
|
}
|
||||||
|
|
||||||
|
const propertiesToUpdate = { [propertyName]: newValue };
|
||||||
|
|
||||||
|
// Handles the special case for duration
|
||||||
|
// needs to be converted to milliseconds
|
||||||
|
if (propertyName === 'duration') {
|
||||||
|
propertiesToUpdate.duration = (newValue as number) * 1000;
|
||||||
|
propertiesToUpdate.timeEnd = event.timeStart + propertiesToUpdate.duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newEvent = editEvent({ id: eventId, ...propertiesToUpdate });
|
||||||
|
return newEvent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,9 +185,10 @@ const actionHandlers: Record<string, ActionHandler> = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error('Invalid extratimer payload');
|
throw new Error('Invalid extra-timer payload');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a value of type number, converting if necessary
|
* Returns a value of type number, converting if necessary
|
||||||
* Otherwise throws
|
* Otherwise throws
|
||||||
|
|||||||
@@ -10,11 +10,10 @@ import {
|
|||||||
deleteAllEvents,
|
deleteAllEvents,
|
||||||
deleteEvent,
|
deleteEvent,
|
||||||
editEvent,
|
editEvent,
|
||||||
getRundown,
|
|
||||||
reorderEvent,
|
reorderEvent,
|
||||||
swapEvents,
|
swapEvents,
|
||||||
} from '../services/rundown-service/RundownService.js';
|
} from '../services/rundown-service/RundownService.js';
|
||||||
import { get as getCachedRundown } from '../services/rundown-service/rundownCache.js';
|
import { getNormalisedRundown, getRundown } from '../services/rundown-service/rundownUtils.js';
|
||||||
|
|
||||||
// Create controller for GET request to '/events'
|
// Create controller for GET request to '/events'
|
||||||
// Returns -
|
// Returns -
|
||||||
@@ -26,7 +25,7 @@ export const rundownGetAll: RequestHandler = async (_req, res) => {
|
|||||||
// Create controller for GET request to '/events/cached'
|
// Create controller for GET request to '/events/cached'
|
||||||
// Returns -
|
// Returns -
|
||||||
export const rundownGetCached: RequestHandler = async (_req: Request, res: Response<RundownCached>) => {
|
export const rundownGetCached: RequestHandler = async (_req: Request, res: Response<RundownCached>) => {
|
||||||
const cachedRundown = getCachedRundown();
|
const cachedRundown = getNormalisedRundown();
|
||||||
res.json(cachedRundown);
|
res.json(cachedRundown);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 { 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 { restoreService } from './RestoreService.js';
|
||||||
import { runtimeService } from './runtime-service/RuntimeService.js';
|
|
||||||
import { getPlayableEvents } from './rundown-service/RundownService.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service manages Ontime's main timer
|
* Service manages Ontime's main timer
|
||||||
|
* It is responsible for streaming the data to the event store
|
||||||
*/
|
*/
|
||||||
export class TimerService {
|
export class TimerService {
|
||||||
private _interval: NodeJS.Timer;
|
private readonly _interval: NodeJS.Timer;
|
||||||
private _updateInterval: number;
|
/** how often we update the socket */
|
||||||
private _refreshInterval: number;
|
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
|
* @constructor
|
||||||
* @param {number} [timerConfig.refresh]
|
* @param {number} [timerConfig.refresh] how often we recalculate
|
||||||
* @param {number} [timerConfig.updateInterval]
|
* @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 }) {
|
constructor(timerConfig: {
|
||||||
this._refreshInterval = timerConfig.refresh;
|
refresh: number;
|
||||||
this._updateInterval = timerConfig.updateInterval;
|
updateInterval: number;
|
||||||
this._interval = setInterval(this.update, 32);
|
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
|
@broadcastResult
|
||||||
start() {
|
start() {
|
||||||
// TODO: when we start a timer, we schedule an update to its expected end - 16ms
|
if (!runtimeState.start()) {
|
||||||
// we need to cancel this timer on pause, stop and addTime
|
return false;
|
||||||
if (runtimeState.start()) {
|
|
||||||
integrationService.dispatch(TimerLifeCycle.onStart);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const state = runtimeState.getState();
|
||||||
|
this.endCallback = setTimeout(this.update, state.timer.expectedFinish);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@broadcastResult
|
@broadcastResult
|
||||||
pause() {
|
pause() {
|
||||||
if (runtimeState.pause()) {
|
if (!runtimeState.pause()) {
|
||||||
integrationService.dispatch(TimerLifeCycle.onPause);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cancel end callback
|
||||||
|
clearTimeout(this.endCallback);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@broadcastResult
|
@broadcastResult
|
||||||
stop() {
|
stop() {
|
||||||
if (runtimeState.stop()) {
|
if (!runtimeState.stop()) {
|
||||||
integrationService.dispatch(TimerLifeCycle.onStop);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cancel end callback
|
||||||
|
clearTimeout(this.endCallback);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,62 +91,40 @@ export class TimerService {
|
|||||||
*/
|
*/
|
||||||
@broadcastResult
|
@broadcastResult
|
||||||
addTime(amount: number): boolean {
|
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
|
* Update the app at regular intervals
|
||||||
* @param {boolean} force whether we should force a broadcast of state
|
|
||||||
*/
|
*/
|
||||||
@broadcastResult
|
@broadcastResult
|
||||||
update(force = false) {
|
update() {
|
||||||
const { didUpdate, doRoll, isFinished, shouldNotify } = runtimeState.update(force, this._updateInterval);
|
const updateResult = runtimeState.update();
|
||||||
if (didUpdate && shouldNotify) {
|
|
||||||
// TODO: can we distinguish between a clock update and a timer update?
|
|
||||||
integrationService.dispatch(TimerLifeCycle.onUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (doRoll) {
|
// pass the result to the parent
|
||||||
// TODO: escalate to parent
|
this.onUpdateCallback(updateResult);
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads roll information into timer service
|
* Loads roll information into timer service
|
||||||
* @throws {Error} if rundown is empty
|
|
||||||
* @param {OntimeEvent[]} rundown -- list of events to run
|
* @param {OntimeEvent[]} rundown -- list of events to run
|
||||||
*/
|
*/
|
||||||
@broadcastResult
|
@broadcastResult
|
||||||
roll(rundown: OntimeEvent[]) {
|
roll(rundown: OntimeEvent[]) {
|
||||||
if (rundown.length === 0) {
|
|
||||||
throw new Error('No events found');
|
|
||||||
}
|
|
||||||
|
|
||||||
runtimeState.roll(rundown);
|
runtimeState.roll(rundown);
|
||||||
}
|
}
|
||||||
|
|
||||||
shutdown() {
|
shutdown() {
|
||||||
clearInterval(this._interval);
|
clearInterval(this._interval);
|
||||||
|
clearTimeout(this.endCallback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,29 +132,64 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
|
|||||||
const originalMethod = descriptor.value;
|
const originalMethod = descriptor.value;
|
||||||
|
|
||||||
descriptor.value = function (...args: any[]) {
|
descriptor.value = function (...args: any[]) {
|
||||||
|
// call the original method and get the state
|
||||||
const result = originalMethod.apply(this, args);
|
const result = originalMethod.apply(this, args);
|
||||||
const state = runtimeState.getState();
|
const state = runtimeState.getState();
|
||||||
|
|
||||||
// TODO: compare datasets to see what needs to be emitted
|
// we do the comparison by explicitly for each property
|
||||||
eventStore.batchSet({
|
// to apply custom logic for different datasets
|
||||||
clock: state.clock,
|
|
||||||
eventNow: state.eventNow,
|
// some of the data, we only update at intervals
|
||||||
publicEventNow: state.publicEventNow,
|
const isTimeToUpdate = state.clock - TimerService.previousUpdate >= TimerService._updateInterval;
|
||||||
eventNext: state.eventNext,
|
|
||||||
publicEventNext: state.publicEventNext,
|
// some changes need an immediate update
|
||||||
runtime: state.runtime,
|
const hasSkippedBack = state.clock < TimerService.previousUpdate;
|
||||||
timer: state.timer,
|
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;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import {
|
|||||||
OntimeBlock,
|
OntimeBlock,
|
||||||
OntimeDelay,
|
OntimeDelay,
|
||||||
OntimeEvent,
|
OntimeEvent,
|
||||||
OntimeRundown,
|
|
||||||
OntimeRundownEntry,
|
OntimeRundownEntry,
|
||||||
isOntimeBlock,
|
isOntimeBlock,
|
||||||
isOntimeDelay,
|
isOntimeDelay,
|
||||||
isOntimeEvent,
|
isOntimeEvent,
|
||||||
|
OntimeRundown,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { getCueCandidate } from 'ontime-utils';
|
import { getCueCandidate } from 'ontime-utils';
|
||||||
|
|
||||||
@@ -19,6 +19,7 @@ import { updateRundownData } from '../../stores/runtimeState.js';
|
|||||||
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
||||||
|
|
||||||
import * as cache from './rundownCache.js';
|
import * as cache from './rundownCache.js';
|
||||||
|
import { getPlayableEvents } from './rundownUtils.js';
|
||||||
|
|
||||||
function generateEvent(eventData: Partial<OntimeEvent> | Partial<OntimeDelay> | Partial<OntimeBlock>) {
|
function generateEvent(eventData: Partial<OntimeEvent> | Partial<OntimeDelay> | Partial<OntimeBlock>) {
|
||||||
// we discard any UI provided events and add our own
|
// we discard any UI provided events and add our own
|
||||||
@@ -44,7 +45,9 @@ function generateEvent(eventData: Partial<OntimeEvent> | Partial<OntimeDelay> |
|
|||||||
* @param {object} eventData
|
* @param {object} eventData
|
||||||
* @return {OntimeRundownEntry}
|
* @return {OntimeRundownEntry}
|
||||||
*/
|
*/
|
||||||
export async function addEvent(eventData: Partial<OntimeEvent> | Partial<OntimeDelay> | Partial<OntimeBlock>) {
|
export async function addEvent(
|
||||||
|
eventData: Partial<OntimeEvent> | Partial<OntimeDelay> | Partial<OntimeBlock>,
|
||||||
|
): Promise<OntimeRundownEntry> {
|
||||||
// if the user didnt provide an index, we add the event to start
|
// if the user didnt provide an index, we add the event to start
|
||||||
let atIndex = 0;
|
let atIndex = 0;
|
||||||
if (eventData?.after !== undefined) {
|
if (eventData?.after !== undefined) {
|
||||||
@@ -65,7 +68,7 @@ export async function addEvent(eventData: Partial<OntimeEvent> | Partial<OntimeD
|
|||||||
notifyChanges({ timer: [newEvent.id], external: true });
|
notifyChanges({ timer: [newEvent.id], external: true });
|
||||||
|
|
||||||
// notify runtime that rundown size has changed
|
// notify runtime that rundown size has changed
|
||||||
updateChangeNumEvents();
|
updateRuntimeOnChange();
|
||||||
|
|
||||||
return newEvent;
|
return newEvent;
|
||||||
}
|
}
|
||||||
@@ -73,7 +76,6 @@ export async function addEvent(eventData: Partial<OntimeEvent> | Partial<OntimeD
|
|||||||
/**
|
/**
|
||||||
* deletes event by its ID
|
* deletes event by its ID
|
||||||
* @param eventId
|
* @param eventId
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
*/
|
||||||
export async function deleteEvent(eventId: string) {
|
export async function deleteEvent(eventId: string) {
|
||||||
const scopedMutation = cache.mutateCache(cache.remove);
|
const scopedMutation = cache.mutateCache(cache.remove);
|
||||||
@@ -82,12 +84,11 @@ export async function deleteEvent(eventId: string) {
|
|||||||
notifyChanges({ timer: [eventId], external: true });
|
notifyChanges({ timer: [eventId], external: true });
|
||||||
|
|
||||||
// notify event loader that rundown size has changed
|
// notify event loader that rundown size has changed
|
||||||
updateChangeNumEvents();
|
updateRuntimeOnChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* deletes all events in database
|
* deletes all events in database
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
*/
|
||||||
export async function deleteAllEvents() {
|
export async function deleteAllEvents() {
|
||||||
const scopedMutation = cache.mutateCache(cache.removeAll);
|
const scopedMutation = cache.mutateCache(cache.removeAll);
|
||||||
@@ -97,6 +98,10 @@ export async function deleteAllEvents() {
|
|||||||
notifyChanges({ external: true });
|
notifyChanges({ external: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply patch to an element in rundown
|
||||||
|
* @param patch
|
||||||
|
*/
|
||||||
export async function editEvent(patch: Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>) {
|
export async function editEvent(patch: Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>) {
|
||||||
if (isOntimeEvent(patch) && patch?.cue === '') {
|
if (isOntimeEvent(patch) && patch?.cue === '') {
|
||||||
throw new Error('Cue value invalid');
|
throw new Error('Cue value invalid');
|
||||||
@@ -111,6 +116,11 @@ export async function editEvent(patch: Partial<OntimeEvent> | Partial<OntimeBloc
|
|||||||
return newEvent;
|
return newEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a patch to several elements in a rundown
|
||||||
|
* @param ids
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
export async function batchEditEvents(ids: string[], data: Partial<OntimeEvent>) {
|
export async function batchEditEvents(ids: string[], data: Partial<OntimeEvent>) {
|
||||||
const scopedMutation = cache.mutateCache(cache.batchEdit);
|
const scopedMutation = cache.mutateCache(cache.batchEdit);
|
||||||
await scopedMutation({ patch: data, eventIds: ids });
|
await scopedMutation({ patch: data, eventIds: ids });
|
||||||
@@ -123,7 +133,6 @@ export async function batchEditEvents(ids: string[], data: Partial<OntimeEvent>)
|
|||||||
* @param {string} eventId - ID of event from, for sanity check
|
* @param {string} eventId - ID of event from, for sanity check
|
||||||
* @param {number} from - index of event from
|
* @param {number} from - index of event from
|
||||||
* @param {number} to - index of event to
|
* @param {number} to - index of event to
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
*/
|
||||||
export async function reorderEvent(eventId: string, from: number, to: number) {
|
export async function reorderEvent(eventId: string, from: number, to: number) {
|
||||||
const scopedMutation = cache.mutateCache(cache.reorder);
|
const scopedMutation = cache.mutateCache(cache.reorder);
|
||||||
@@ -158,7 +167,7 @@ export async function swapEvents(from: string, to: string) {
|
|||||||
* Forces update in the store
|
* Forces update in the store
|
||||||
* Called when we make changes to the rundown object
|
* Called when we make changes to the rundown object
|
||||||
*/
|
*/
|
||||||
function updateChangeNumEvents() {
|
function updateRuntimeOnChange() {
|
||||||
updateRundownData(getPlayableEvents());
|
updateRundownData(getPlayableEvents());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,12 +176,13 @@ function updateChangeNumEvents() {
|
|||||||
*/
|
*/
|
||||||
export function notifyChanges(options: { timer?: boolean | string[]; external?: boolean }) {
|
export function notifyChanges(options: { timer?: boolean | string[]; external?: boolean }) {
|
||||||
if (options.timer) {
|
if (options.timer) {
|
||||||
|
const playableEvents = getPlayableEvents();
|
||||||
// notify timer service of changed events
|
// notify timer service of changed events
|
||||||
// timer can be true or an array of changed IDs
|
// timer can be true or an array of changed IDs
|
||||||
if (Array.isArray(options.timer)) {
|
if (Array.isArray(options.timer)) {
|
||||||
runtimeService.update(options.timer);
|
runtimeService.maybeUpdate(playableEvents, options.timer);
|
||||||
}
|
}
|
||||||
runtimeService.update();
|
runtimeService.maybeUpdate(playableEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.external) {
|
if (options.external) {
|
||||||
@@ -181,115 +191,11 @@ export function notifyChanges(options: { timer?: boolean | string[]; external?:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* returns entire unfiltered rundown
|
|
||||||
* @return {array}
|
|
||||||
*/
|
|
||||||
export function getRundown(): OntimeRundown {
|
|
||||||
return cache.getPersistedRundown();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* returns all events of type OntimeEvent
|
|
||||||
* @return {array}
|
|
||||||
*/
|
|
||||||
export function getTimedEvents(): OntimeEvent[] {
|
|
||||||
return getRundown().filter((event) => isOntimeEvent(event)) as OntimeEvent[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* returns all events that can be loaded
|
|
||||||
* @return {array}
|
|
||||||
*/
|
|
||||||
export function getPlayableEvents(): OntimeEvent[] {
|
|
||||||
return getRundown().filter((event) => isOntimeEvent(event) && !event.skip) as OntimeEvent[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* returns number of events that can be loaded
|
|
||||||
* @return {number}
|
|
||||||
*/
|
|
||||||
export function getNumEvents(): number {
|
|
||||||
return getPlayableEvents().length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* returns an event given its index after filtering for OntimeEvents
|
|
||||||
* @param {number} eventIndex
|
|
||||||
* @return {OntimeEvent | undefined}
|
|
||||||
*/
|
|
||||||
export function getEventAtIndex(eventIndex: number): OntimeEvent | undefined {
|
|
||||||
const timedEvents = getTimedEvents();
|
|
||||||
return timedEvents.at(eventIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* returns first event that matches a given ID
|
|
||||||
* @param {string} eventId
|
|
||||||
* @return {object | undefined}
|
|
||||||
*/
|
|
||||||
export function getEventWithId(eventId: string): OntimeEvent | undefined {
|
|
||||||
const timedEvents = getTimedEvents();
|
|
||||||
return timedEvents.find((event) => event.id === eventId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* returns first event that matches a given cue
|
|
||||||
* @param {string} cue
|
|
||||||
* @return {object | undefined}
|
|
||||||
*/
|
|
||||||
export function getEventWithCue(cue: string): OntimeEvent | undefined {
|
|
||||||
const timedEvents = getTimedEvents();
|
|
||||||
return timedEvents.find((event) => event.cue.toLowerCase() === cue.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* finds the previous event
|
|
||||||
* @return {object | undefined}
|
|
||||||
*/
|
|
||||||
export function findPrevious(currentEventId?: string): OntimeEvent | null {
|
|
||||||
const timedEvents = getPlayableEvents();
|
|
||||||
if (!timedEvents || !timedEvents.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there is no event running, go to first
|
|
||||||
if (!currentEventId) {
|
|
||||||
return timedEvents.at(0) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentIndex = timedEvents.findIndex((event) => event.id === currentEventId);
|
|
||||||
const newIndex = Math.max(currentIndex - 1, 0);
|
|
||||||
const previousEvent = timedEvents.at(newIndex) ?? null;
|
|
||||||
return previousEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* finds the next event
|
|
||||||
* @return {object | undefined}
|
|
||||||
*/
|
|
||||||
export function findNext(currentEventId?: string): OntimeEvent | null {
|
|
||||||
const timedEvents = getPlayableEvents();
|
|
||||||
if (!timedEvents || !timedEvents.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there is no event running, go to first
|
|
||||||
if (!currentEventId) {
|
|
||||||
return timedEvents.at(0) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentIndex = timedEvents.findIndex((event) => event.id === currentEventId);
|
|
||||||
const newIndex = (currentIndex + 1) % timedEvents.length;
|
|
||||||
const nextEvent = timedEvents.at(newIndex);
|
|
||||||
return nextEvent ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overrides the rundown with the given
|
* Overrides the rundown with the given
|
||||||
* @param rundown
|
* @param rundown
|
||||||
*/
|
*/
|
||||||
export async function setRundown(rundown: OntimeRundown) {
|
export async function setRundown(rundown: OntimeRundown) {
|
||||||
cache.init(rundown);
|
await cache.init(rundown);
|
||||||
notifyChanges({ timer: true });
|
notifyChanges({ timer: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { OntimeEvent, OntimeRundown, isOntimeEvent, RundownCached } from 'ontime-types';
|
||||||
|
|
||||||
|
import * as cache from './rundownCache.js';
|
||||||
|
|
||||||
|
export function getNormalisedRundown(): RundownCached {
|
||||||
|
return cache.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns entire unfiltered rundown
|
||||||
|
* @return {array}
|
||||||
|
*/
|
||||||
|
export function getRundown(): OntimeRundown {
|
||||||
|
return cache.getPersistedRundown();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns all events of type OntimeEvent
|
||||||
|
* @return {array}
|
||||||
|
*/
|
||||||
|
export function getTimedEvents(): OntimeEvent[] {
|
||||||
|
return getRundown().filter((event) => isOntimeEvent(event)) as OntimeEvent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns all events that can be loaded
|
||||||
|
* @return {array}
|
||||||
|
*/
|
||||||
|
export function getPlayableEvents(): OntimeEvent[] {
|
||||||
|
return getRundown().filter((event) => isOntimeEvent(event) && !event.skip) as OntimeEvent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns number of events that can be loaded
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
export function getNumEvents(): number {
|
||||||
|
return getPlayableEvents().length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns an event given its index after filtering for OntimeEvents
|
||||||
|
* @param {number} eventIndex
|
||||||
|
* @return {OntimeEvent | undefined}
|
||||||
|
*/
|
||||||
|
export function getEventAtIndex(eventIndex: number): OntimeEvent | undefined {
|
||||||
|
const timedEvents = getTimedEvents();
|
||||||
|
return timedEvents.at(eventIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns first event that matches a given ID
|
||||||
|
* @param {string} eventId
|
||||||
|
* @return {object | undefined}
|
||||||
|
*/
|
||||||
|
export function getEventWithId(eventId: string): OntimeEvent | undefined {
|
||||||
|
const timedEvents = getTimedEvents();
|
||||||
|
return timedEvents.find((event) => event.id === eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns first event that matches a given cue
|
||||||
|
* @param {string} cue
|
||||||
|
* @return {object | undefined}
|
||||||
|
*/
|
||||||
|
export function getEventWithCue(cue: string): OntimeEvent | undefined {
|
||||||
|
const timedEvents = getTimedEvents();
|
||||||
|
return timedEvents.find((event) => event.cue.toLowerCase() === cue.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* finds the previous event
|
||||||
|
* @return {object | undefined}
|
||||||
|
*/
|
||||||
|
export function findPrevious(currentEventId?: string): OntimeEvent | null {
|
||||||
|
const timedEvents = getPlayableEvents();
|
||||||
|
if (!timedEvents || !timedEvents.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is no event running, go to first
|
||||||
|
if (!currentEventId) {
|
||||||
|
return timedEvents.at(0) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIndex = timedEvents.findIndex((event) => event.id === currentEventId);
|
||||||
|
const newIndex = Math.max(currentIndex - 1, 0);
|
||||||
|
const previousEvent = timedEvents.at(newIndex) ?? null;
|
||||||
|
return previousEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* finds the next event
|
||||||
|
* @return {object | undefined}
|
||||||
|
*/
|
||||||
|
export function findNext(currentEventId?: string): OntimeEvent | null {
|
||||||
|
const timedEvents = getPlayableEvents();
|
||||||
|
if (!timedEvents || !timedEvents.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is no event running, go to first
|
||||||
|
if (!currentEventId) {
|
||||||
|
return timedEvents.at(0) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIndex = timedEvents.findIndex((event) => event.id === currentEventId);
|
||||||
|
const newIndex = (currentIndex + 1) % timedEvents.length;
|
||||||
|
const nextEvent = timedEvents.at(newIndex);
|
||||||
|
return nextEvent ?? null;
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { LogOrigin, OntimeEvent, Playback } from 'ontime-types';
|
import { EndAction, LogOrigin, OntimeEvent, Playback, TimerLifeCycle } from 'ontime-types';
|
||||||
import { millisToString, validatePlayback } from 'ontime-utils';
|
import { millisToString, validatePlayback } from 'ontime-utils';
|
||||||
|
|
||||||
import { TimerService } from '../TimerService.js';
|
import { TimerService } from '../TimerService.js';
|
||||||
import { logger } from '../../classes/Logger.js';
|
import { logger } from '../../classes/Logger.js';
|
||||||
import { RestorePoint } from '../RestoreService.js';
|
import { RestorePoint } from '../RestoreService.js';
|
||||||
|
|
||||||
import * as runtimeState from '../../stores/runtimeState.js';
|
import * as runtimeState from '../../stores/runtimeState.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
findNext,
|
findNext,
|
||||||
findPrevious,
|
findPrevious,
|
||||||
@@ -12,7 +14,9 @@ import {
|
|||||||
getEventWithCue,
|
getEventWithCue,
|
||||||
getEventWithId,
|
getEventWithId,
|
||||||
getPlayableEvents,
|
getPlayableEvents,
|
||||||
} from '../rundown-service/RundownService.js';
|
} from '../rundown-service/rundownUtils.js';
|
||||||
|
import { integrationService } from '../integration-service/IntegrationService.js';
|
||||||
|
import { timerConfig } from '../../config/config.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service manages runtime status of app
|
* Service manages runtime status of app
|
||||||
@@ -20,12 +24,55 @@ import {
|
|||||||
*/
|
*/
|
||||||
class RuntimeService {
|
class RuntimeService {
|
||||||
private eventTimer: TimerService | null = null;
|
private eventTimer: TimerService | null = null;
|
||||||
|
private lastOnUpdate = -1;
|
||||||
|
|
||||||
|
/** Checks result of an update and notifies integrations as needed */
|
||||||
|
checkTimerUpdate({ shouldCallRoll, hasTimerFinished }: runtimeState.UpdateResult) {
|
||||||
|
const newState = runtimeState.getState();
|
||||||
|
|
||||||
|
if (hasTimerFinished) {
|
||||||
|
integrationService.dispatch(TimerLifeCycle.onFinish);
|
||||||
|
|
||||||
|
// handle end action if there was a timer playing
|
||||||
|
// actions are added to the queue stack to ensure that the order of operations is maintained
|
||||||
|
if (newState.timer.playback === Playback.Play && newState.eventNow) {
|
||||||
|
if (newState.eventNow.endAction === EndAction.Stop) {
|
||||||
|
setTimeout(this.stop, 0);
|
||||||
|
} else if (newState.eventNow.endAction === EndAction.LoadNext) {
|
||||||
|
setTimeout(this.loadNext, 0);
|
||||||
|
} else if (newState.eventNow.endAction === EndAction.PlayNext) {
|
||||||
|
setTimeout(this.startNext, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update normal cycle
|
||||||
|
if (newState.clock - this.lastOnUpdate >= timerConfig.notificationRate) {
|
||||||
|
const hasRunningTimer = Boolean(newState.eventNow) && newState.timer.playback === Playback.Play;
|
||||||
|
if (hasRunningTimer) {
|
||||||
|
integrationService.dispatch(TimerLifeCycle.onUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
integrationService.dispatch(TimerLifeCycle.onClock);
|
||||||
|
this.lastOnUpdate = newState.clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldCallRoll) {
|
||||||
|
// we dont call this.roll because we need to bypass the checks
|
||||||
|
const rundown = getPlayableEvents();
|
||||||
|
this.eventTimer.roll(rundown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** delay initialisation until we have a restore point */
|
||||||
init(resumable: RestorePoint | null) {
|
init(resumable: RestorePoint | null) {
|
||||||
logger.info(LogOrigin.Server, 'Runtime service started');
|
logger.info(LogOrigin.Server, 'Runtime service started');
|
||||||
// TODO: refresh at 32ms, slowing down now to keep UI responsive while we dont have granular updates
|
|
||||||
// calculate at 30fps, refresh at 1fps
|
// calculate at 30fps, refresh at 1fps
|
||||||
this.eventTimer = new TimerService({ refresh: 1000, updateInterval: 1000 });
|
this.eventTimer = new TimerService({
|
||||||
|
refresh: timerConfig.updateRate,
|
||||||
|
updateInterval: timerConfig.notificationRate,
|
||||||
|
onUpdateCallback: this.checkTimerUpdate.bind(this),
|
||||||
|
});
|
||||||
|
|
||||||
if (resumable) {
|
if (resumable) {
|
||||||
this.resume(resumable);
|
this.resume(resumable);
|
||||||
@@ -94,14 +141,11 @@ class RuntimeService {
|
|||||||
return foundNew;
|
return foundNew;
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
|
||||||
runtimeState.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* check whether underlying data of runtime has changed
|
* Called when the underlying data has changed,
|
||||||
|
* we check if the change affects the runtime
|
||||||
*/
|
*/
|
||||||
update(affectedIds?: string[]) {
|
maybeUpdate(playableEvents: OntimeEvent[], affectedIds?: string[]) {
|
||||||
const state = runtimeState.getState();
|
const state = runtimeState.getState();
|
||||||
const hasLoadedElements = state.eventNow || state.eventNext;
|
const hasLoadedElements = state.eventNow || state.eventNext;
|
||||||
if (!hasLoadedElements) {
|
if (!hasLoadedElements) {
|
||||||
@@ -131,7 +175,6 @@ class RuntimeService {
|
|||||||
isNext = this.isNewNext();
|
isNext = this.isNewNext();
|
||||||
if (isNext) {
|
if (isNext) {
|
||||||
// TODO: do i need to load here?
|
// TODO: do i need to load here?
|
||||||
const playableEvents = getPlayableEvents();
|
|
||||||
runtimeState.loadNext(playableEvents);
|
runtimeState.loadNext(playableEvents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,6 +197,7 @@ class RuntimeService {
|
|||||||
const success = event.id === state.eventNow?.id;
|
const success = event.id === state.eventNow?.id;
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
integrationService.dispatch(TimerLifeCycle.onLoad);
|
||||||
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
|
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
|
||||||
}
|
}
|
||||||
return success;
|
return success;
|
||||||
@@ -220,8 +264,7 @@ class RuntimeService {
|
|||||||
if (!event) {
|
if (!event) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const success = this.loadEvent(event);
|
return this.loadEvent(event);
|
||||||
return success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -234,8 +277,7 @@ class RuntimeService {
|
|||||||
if (!event) {
|
if (!event) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const success = this.loadEvent(event);
|
return this.loadEvent(event);
|
||||||
return success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -248,8 +290,7 @@ class RuntimeService {
|
|||||||
if (!event) {
|
if (!event) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const success = this.loadEvent(event);
|
return this.loadEvent(event);
|
||||||
return success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -260,8 +301,7 @@ class RuntimeService {
|
|||||||
const state = runtimeState.getState();
|
const state = runtimeState.getState();
|
||||||
const previousEvent = findPrevious(state.eventNow?.id);
|
const previousEvent = findPrevious(state.eventNow?.id);
|
||||||
if (previousEvent) {
|
if (previousEvent) {
|
||||||
const success = this.loadEvent(previousEvent);
|
return this.loadEvent(previousEvent);
|
||||||
return success;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -274,8 +314,7 @@ class RuntimeService {
|
|||||||
const state = runtimeState.getState();
|
const state = runtimeState.getState();
|
||||||
const nextEvent = findNext(state.eventNow?.id);
|
const nextEvent = findNext(state.eventNow?.id);
|
||||||
if (nextEvent) {
|
if (nextEvent) {
|
||||||
const success = this.loadEvent(nextEvent);
|
return this.loadEvent(nextEvent);
|
||||||
return success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(LogOrigin.Playback, 'No next event found! Continuing playback');
|
logger.info(LogOrigin.Playback, 'No next event found! Continuing playback');
|
||||||
@@ -288,9 +327,14 @@ class RuntimeService {
|
|||||||
start() {
|
start() {
|
||||||
const state = runtimeState.getState();
|
const state = runtimeState.getState();
|
||||||
const canStart = validatePlayback(state.timer.playback).start;
|
const canStart = validatePlayback(state.timer.playback).start;
|
||||||
if (canStart) {
|
if (!canStart) {
|
||||||
this.eventTimer.start();
|
return false;
|
||||||
logger.info(LogOrigin.Playback, `Play Mode ${state.timer.playback.toUpperCase()}`);
|
}
|
||||||
|
|
||||||
|
const didStart = this.eventTimer.start();
|
||||||
|
logger.info(LogOrigin.Playback, `Play Mode ${state.timer.playback.toUpperCase()}`);
|
||||||
|
if (didStart) {
|
||||||
|
integrationService.dispatch(TimerLifeCycle.onStart);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,9 +343,11 @@ class RuntimeService {
|
|||||||
*/
|
*/
|
||||||
startNext() {
|
startNext() {
|
||||||
const hasNext = this.loadNext();
|
const hasNext = this.loadNext();
|
||||||
if (hasNext) {
|
if (!hasNext) {
|
||||||
this.start();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -309,11 +355,14 @@ class RuntimeService {
|
|||||||
*/
|
*/
|
||||||
pause() {
|
pause() {
|
||||||
const state = runtimeState.getState();
|
const state = runtimeState.getState();
|
||||||
if (validatePlayback(state.timer.playback).pause) {
|
const canPause = validatePlayback(state.timer.playback).pause;
|
||||||
this.eventTimer.pause();
|
if (!canPause) {
|
||||||
const newState = state.timer.playback;
|
return;
|
||||||
logger.info(LogOrigin.Playback, `Play Mode ${newState.toUpperCase()}`);
|
|
||||||
}
|
}
|
||||||
|
this.eventTimer.pause();
|
||||||
|
const newState = state.timer.playback;
|
||||||
|
logger.info(LogOrigin.Playback, `Play Mode ${newState.toUpperCase()}`);
|
||||||
|
integrationService.dispatch(TimerLifeCycle.onPause);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -321,11 +370,14 @@ class RuntimeService {
|
|||||||
*/
|
*/
|
||||||
stop() {
|
stop() {
|
||||||
const state = runtimeState.getState();
|
const state = runtimeState.getState();
|
||||||
if (validatePlayback(state.timer.playback).stop) {
|
const canStop = validatePlayback(state.timer.playback).stop;
|
||||||
this.eventTimer.stop();
|
if (!canStop) {
|
||||||
const newState = state.timer.playback;
|
return;
|
||||||
logger.info(LogOrigin.Playback, `Play Mode ${newState.toUpperCase()}`);
|
|
||||||
}
|
}
|
||||||
|
this.eventTimer.stop();
|
||||||
|
const newState = state.timer.playback;
|
||||||
|
logger.info(LogOrigin.Playback, `Play Mode ${newState.toUpperCase()}`);
|
||||||
|
integrationService.dispatch(TimerLifeCycle.onStop);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -342,13 +394,20 @@ class RuntimeService {
|
|||||||
* Sets playback to roll
|
* Sets playback to roll
|
||||||
*/
|
*/
|
||||||
roll() {
|
roll() {
|
||||||
const playableEvents = getPlayableEvents();
|
const beforeState = runtimeState.getState();
|
||||||
try {
|
const canRoll = validatePlayback(beforeState.timer.playback).roll;
|
||||||
this.eventTimer.roll(playableEvents);
|
if (!canRoll) {
|
||||||
} catch (error) {
|
return;
|
||||||
logger.warning(LogOrigin.Server, `Roll: ${error}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const playableEvents = getPlayableEvents();
|
||||||
|
if (playableEvents.length === 0) {
|
||||||
|
logger.warning(LogOrigin.Server, 'Roll: no events found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventTimer.roll(playableEvents);
|
||||||
|
|
||||||
const state = runtimeState.getState();
|
const state = runtimeState.getState();
|
||||||
const newState = state.timer.playback;
|
const newState = state.timer.playback;
|
||||||
logger.info(LogOrigin.Playback, `Play Mode ${newState.toUpperCase()}`);
|
logger.info(LogOrigin.Playback, `Play Mode ${newState.toUpperCase()}`);
|
||||||
|
|||||||
@@ -22,14 +22,12 @@ export const eventStore = {
|
|||||||
},
|
},
|
||||||
set<T extends keyof RuntimeStore>(key: T, value: RuntimeStore[T]) {
|
set<T extends keyof RuntimeStore>(key: T, value: RuntimeStore[T]) {
|
||||||
store[key] = value;
|
store[key] = value;
|
||||||
// TODO: Partial updates seems to cause issues on the client
|
socket.sendAsJson({
|
||||||
// socket.send({
|
type: `ontime-${key}`,
|
||||||
// type: `ontime-${key}`,
|
payload: value,
|
||||||
// payload: value,
|
});
|
||||||
// });
|
|
||||||
this.broadcast();
|
|
||||||
},
|
},
|
||||||
batchSet<K extends keyof RuntimeStore>(values: Record<K, RuntimeStore[K]>) {
|
batchSet(values: Partial<RuntimeStore>) {
|
||||||
Object.entries(values).forEach(([key, value]) => {
|
Object.entries(values).forEach(([key, value]) => {
|
||||||
store[key] = value;
|
store[key] = value;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ const mockState = {
|
|||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
lastUpdate: null,
|
|
||||||
secondaryTarget: null,
|
secondaryTarget: null,
|
||||||
},
|
},
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Runtime, OntimeEvent, Playback, TimerState, TimerType, MaybeNumber } from 'ontime-types';
|
import { MaybeNumber, OntimeEvent, Playback, Runtime, TimerState, TimerType } from 'ontime-types';
|
||||||
import { calculateDuration, dayInMs, getFirstEvent, getLastEvent } from 'ontime-utils';
|
import { calculateDuration, dayInMs, getFirstEvent, getLastEvent } from 'ontime-utils';
|
||||||
|
|
||||||
import { clock } from '../services/Clock.js';
|
import { clock } from '../services/Clock.js';
|
||||||
import { RestorePoint } from '../services/RestoreService.js';
|
import { RestorePoint } from '../services/RestoreService.js';
|
||||||
import { getPlayableEvents } from '../services/rundown-service/RundownService.js';
|
|
||||||
import {
|
import {
|
||||||
getCurrent,
|
getCurrent,
|
||||||
getExpectedFinish,
|
getExpectedFinish,
|
||||||
@@ -48,7 +48,6 @@ export type RuntimeState = {
|
|||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: MaybeNumber;
|
pausedAt: MaybeNumber;
|
||||||
finishedNow: boolean;
|
finishedNow: boolean;
|
||||||
lastUpdate: MaybeNumber;
|
|
||||||
secondaryTarget: MaybeNumber;
|
secondaryTarget: MaybeNumber;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -63,7 +62,6 @@ const runtimeState: RuntimeState = {
|
|||||||
timer: { ...initialTimer },
|
timer: { ...initialTimer },
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
lastUpdate: null,
|
|
||||||
secondaryTarget: null,
|
secondaryTarget: null,
|
||||||
get finishedNow() {
|
get finishedNow() {
|
||||||
return this.current <= 0 && this.finishedAt === null;
|
return this.current <= 0 && this.finishedAt === null;
|
||||||
@@ -81,16 +79,18 @@ export function clear() {
|
|||||||
runtimeState.eventNext = null;
|
runtimeState.eventNext = null;
|
||||||
runtimeState.publicEventNext = null;
|
runtimeState.publicEventNext = null;
|
||||||
|
|
||||||
runtimeState.runtime = { ...initialRuntime, actualStart: runtimeState.runtime.actualStart };
|
runtimeState.runtime = {
|
||||||
// TODO: can we cleanup the initialisation of runtime state?
|
...initialRuntime,
|
||||||
runtimeState.runtime.numEvents = fetchNumEvents();
|
// persist session stuff
|
||||||
|
actualStart: runtimeState.runtime.actualStart,
|
||||||
|
numEvents: runtimeState.runtime.numEvents,
|
||||||
|
};
|
||||||
|
|
||||||
runtimeState.timer.playback = Playback.Stop;
|
runtimeState.timer.playback = Playback.Stop;
|
||||||
runtimeState.clock = clock.timeNow();
|
runtimeState.clock = clock.timeNow();
|
||||||
runtimeState.timer = { ...initialTimer };
|
runtimeState.timer = { ...initialTimer };
|
||||||
runtimeState._timer = {
|
runtimeState._timer = {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
lastUpdate: null,
|
|
||||||
secondaryTarget: null,
|
secondaryTarget: null,
|
||||||
finishedNow: false,
|
finishedNow: false,
|
||||||
};
|
};
|
||||||
@@ -108,18 +108,9 @@ function patchTimer(newState: Partial<TimerState>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility, getches number of events from EventLoader
|
|
||||||
* @param numEvents
|
|
||||||
*/
|
|
||||||
function fetchNumEvents(): number {
|
|
||||||
// TODO: could we avoid having this dependency?
|
|
||||||
return getPlayableEvents().length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility, allows updating data derived from the rundown
|
* Utility, allows updating data derived from the rundown
|
||||||
* @param numEvents
|
* @param playableRundown
|
||||||
*/
|
*/
|
||||||
export function updateRundownData(playableRundown: OntimeEvent[]) {
|
export function updateRundownData(playableRundown: OntimeEvent[]) {
|
||||||
runtimeState.runtime.numEvents = playableRundown.length;
|
runtimeState.runtime.numEvents = playableRundown.length;
|
||||||
@@ -342,61 +333,43 @@ export function addTime(amount: number) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function update(force: boolean, updateInterval: number) {
|
export type UpdateResult = {
|
||||||
// TODO: should this logic be moved to consumer?
|
hasTimerFinished: boolean;
|
||||||
// force indicates whether the state change should be broadcast to socket
|
shouldCallRoll: boolean;
|
||||||
let _force = force;
|
};
|
||||||
let _didUpdate = false;
|
|
||||||
let _doRoll = false;
|
export function update(): UpdateResult {
|
||||||
let _isFinished = false;
|
let hasTimerFinished = false;
|
||||||
let _shouldNotify = false;
|
let shouldCallRoll = false; // we also need to call roll if a secondary timer has finished
|
||||||
|
|
||||||
const previousTime = runtimeState.clock;
|
const previousTime = runtimeState.clock;
|
||||||
runtimeState.clock = clock.timeNow();
|
runtimeState.clock = clock.timeNow();
|
||||||
const hasSkippedBack = previousTime > runtimeState.clock;
|
|
||||||
|
|
||||||
if (hasSkippedBack) {
|
|
||||||
_force = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update offset
|
// update offset
|
||||||
runtimeState.runtime.offset = getRuntimeOffset(runtimeState);
|
runtimeState.runtime.offset = getRuntimeOffset(runtimeState);
|
||||||
|
|
||||||
// we call integrations if we update timers
|
// we call integrations if we update timers
|
||||||
if (runtimeState.timer.playback === Playback.Roll) {
|
if (runtimeState.timer.playback === Playback.Roll) {
|
||||||
const result = roll();
|
const result = onRollUpdate();
|
||||||
_shouldNotify = true;
|
shouldCallRoll = result.doRoll;
|
||||||
_doRoll = result.doRoll;
|
hasTimerFinished = result.isFinished;
|
||||||
_isFinished = result.isFinished;
|
|
||||||
} else if (runtimeState.timer.startedAt !== null) {
|
} else if (runtimeState.timer.startedAt !== null) {
|
||||||
// we only update timer if a timer has been started
|
// we only update timer if a timer has been started
|
||||||
const result = play();
|
const result = onPlayUpdate();
|
||||||
_shouldNotify = true;
|
hasTimerFinished = result.isFinished;
|
||||||
_isFinished = result.isFinished;
|
|
||||||
} else if (runtimeState.eventNow?.timerType === TimerType.TimeToEnd) {
|
} else if (runtimeState.eventNow?.timerType === TimerType.TimeToEnd) {
|
||||||
// or if we are in a time-to-end timer
|
// or if we are in a time-to-end timer
|
||||||
runtimeState.timer.current = getCurrent(runtimeState);
|
runtimeState.timer.current = getCurrent(runtimeState);
|
||||||
runtimeState.timer.duration = runtimeState.timer.current;
|
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 {
|
return {
|
||||||
didUpdate: _didUpdate,
|
hasTimerFinished,
|
||||||
doRoll: _doRoll,
|
shouldCallRoll,
|
||||||
isFinished: _isFinished,
|
|
||||||
shouldNotify: _shouldNotify,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function roll() {
|
function onRollUpdate() {
|
||||||
const hasSkippedOutOfEvent = skippedOutOfEvent(runtimeState, previousTime, timerConfig.timeSkipLimit);
|
const hasSkippedOutOfEvent = skippedOutOfEvent(runtimeState, previousTime, timerConfig.skipLimit);
|
||||||
if (hasSkippedOutOfEvent) {
|
if (hasSkippedOutOfEvent) {
|
||||||
return { doRoll: true };
|
return { doRoll: true };
|
||||||
}
|
}
|
||||||
@@ -408,7 +381,7 @@ export function update(force: boolean, updateInterval: number) {
|
|||||||
return { doRoll: doRollLoad, isFinished };
|
return { doRoll: doRollLoad, isFinished };
|
||||||
}
|
}
|
||||||
|
|
||||||
function play() {
|
function onPlayUpdate() {
|
||||||
let isFinished = false;
|
let isFinished = false;
|
||||||
runtimeState.timer.current = getCurrent(runtimeState);
|
runtimeState.timer.current = getCurrent(runtimeState);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export function isString(value: unknown): asserts value is string {
|
export function isString(value: unknown): asserts value is string {
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
throw new Error(`Unexpected payload type: ${value}`);
|
throw new Error(`Unexpected payload type: ${String(value)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,12 +12,12 @@ export function isDefined<T>(value: T | undefined): asserts value is T {
|
|||||||
|
|
||||||
export function isNumber(value: unknown): asserts value is number {
|
export function isNumber(value: unknown): asserts value is number {
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
throw new Error(`Unexpected payload type: ${value}`);
|
throw new Error(`Unexpected payload type: ${String(value)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isObject(value: unknown): asserts value is object {
|
export function isObject(value: unknown): asserts value is object {
|
||||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
||||||
throw new Error(`Unexpected payload type: ${value}`);
|
throw new Error(`Unexpected payload type: ${String(value)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { ensureDirectory } from './fileManagement.js';
|
|||||||
import { cellRequestFromEvent, getA1Notation } from './sheetUtils.js';
|
import { cellRequestFromEvent, getA1Notation } from './sheetUtils.js';
|
||||||
import { parseExcel } from './parser.js';
|
import { parseExcel } from './parser.js';
|
||||||
import { parseRundown, parseUserFields } from './parserFunctions.js';
|
import { parseRundown, parseUserFields } from './parserFunctions.js';
|
||||||
import { getRundown } from '../services/rundown-service/RundownService.js';
|
import { getRundown } from '../services/rundown-service/rundownUtils.js';
|
||||||
|
|
||||||
type ResponseOK = {
|
type ResponseOK = {
|
||||||
data: Partial<DatabaseModel>;
|
data: Partial<DatabaseModel>;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export enum TimerLifeCycle {
|
|||||||
onStart = 'onStart',
|
onStart = 'onStart',
|
||||||
onPause = 'onPause',
|
onPause = 'onPause',
|
||||||
onStop = 'onStop',
|
onStop = 'onStop',
|
||||||
|
onClock = 'onClock',
|
||||||
onUpdate = 'onUpdate',
|
onUpdate = 'onUpdate',
|
||||||
onFinish = 'onFinish',
|
onFinish = 'onFinish',
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+21
-13
@@ -126,8 +126,8 @@ importers:
|
|||||||
specifier: ^3.1.1
|
specifier: ^3.1.1
|
||||||
version: 3.1.1
|
version: 3.1.1
|
||||||
zustand:
|
zustand:
|
||||||
specifier: ^4.4.7
|
specifier: ^4.5.0
|
||||||
version: 4.4.7(@types/react@18.0.26)(react@18.2.0)
|
version: 4.5.0(@types/react@18.0.26)(react@18.2.0)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@sentry/vite-plugin':
|
'@sentry/vite-plugin':
|
||||||
specifier: ^2.14.0
|
specifier: ^2.14.0
|
||||||
@@ -143,7 +143,7 @@ importers:
|
|||||||
version: 13.4.0(react-dom@18.2.0)(react@18.2.0)
|
version: 13.4.0(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@testing-library/user-event':
|
'@testing-library/user-event':
|
||||||
specifier: ^14.1.1
|
specifier: ^14.1.1
|
||||||
version: 14.4.3(@testing-library/dom@9.3.3)
|
version: 14.4.3(@testing-library/dom@9.3.4)
|
||||||
'@types/color':
|
'@types/color':
|
||||||
specifier: ^3.0.3
|
specifier: ^3.0.3
|
||||||
version: 3.0.3
|
version: 3.0.3
|
||||||
@@ -267,6 +267,9 @@ importers:
|
|||||||
express-validator:
|
express-validator:
|
||||||
specifier: ^6.14.2
|
specifier: ^6.14.2
|
||||||
version: 6.14.2
|
version: 6.14.2
|
||||||
|
fast-equals:
|
||||||
|
specifier: ^5.0.1
|
||||||
|
version: 5.0.1
|
||||||
google-auth-library:
|
google-auth-library:
|
||||||
specifier: ^9.4.2
|
specifier: ^9.4.2
|
||||||
version: 9.4.2
|
version: 9.4.2
|
||||||
@@ -694,8 +697,8 @@ packages:
|
|||||||
regenerator-runtime: 0.13.11
|
regenerator-runtime: 0.13.11
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@babel/runtime@7.23.7:
|
/@babel/runtime@7.23.9:
|
||||||
resolution: {integrity: sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==}
|
resolution: {integrity: sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime: 0.14.1
|
regenerator-runtime: 0.14.1
|
||||||
@@ -3061,12 +3064,12 @@ packages:
|
|||||||
pretty-format: 27.5.1
|
pretty-format: 27.5.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@testing-library/dom@9.3.3:
|
/@testing-library/dom@9.3.4:
|
||||||
resolution: {integrity: sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==}
|
resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.23.5
|
'@babel/code-frame': 7.23.5
|
||||||
'@babel/runtime': 7.23.7
|
'@babel/runtime': 7.23.9
|
||||||
'@types/aria-query': 5.0.4
|
'@types/aria-query': 5.0.4
|
||||||
aria-query: 5.1.3
|
aria-query: 5.1.3
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
@@ -3104,13 +3107,13 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@testing-library/user-event@14.4.3(@testing-library/dom@9.3.3):
|
/@testing-library/user-event@14.4.3(@testing-library/dom@9.3.4):
|
||||||
resolution: {integrity: sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==}
|
resolution: {integrity: sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==}
|
||||||
engines: {node: '>=12', npm: '>=6'}
|
engines: {node: '>=12', npm: '>=6'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@testing-library/dom': '>=7.21.4'
|
'@testing-library/dom': '>=7.21.4'
|
||||||
dependencies:
|
dependencies:
|
||||||
'@testing-library/dom': 9.3.3
|
'@testing-library/dom': 9.3.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@tootallnate/once@2.0.0:
|
/@tootallnate/once@2.0.0:
|
||||||
@@ -5688,6 +5691,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
|
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/fast-equals@5.0.1:
|
||||||
|
resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/fast-fifo@1.3.2:
|
/fast-fifo@1.3.2:
|
||||||
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
|
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -9599,12 +9607,12 @@ packages:
|
|||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/zustand@4.4.7(@types/react@18.0.26)(react@18.2.0):
|
/zustand@4.5.0(@types/react@18.0.26)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==}
|
resolution: {integrity: sha512-zlVFqS5TQ21nwijjhJlx4f9iGrXSL0o/+Dpy4txAP22miJ8Ti6c1Ol1RLNN98BMib83lmDH/2KmLwaNXpjrO1A==}
|
||||||
engines: {node: '>=12.7.0'}
|
engines: {node: '>=12.7.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': '>=16.8'
|
'@types/react': '>=16.8'
|
||||||
immer: '>=9.0'
|
immer: '>=9.0.6'
|
||||||
react: '>=16.8'
|
react: '>=16.8'
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
'@types/react':
|
'@types/react':
|
||||||
|
|||||||
Reference in New Issue
Block a user