mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-12 02:43:50 +00:00
refactor: roll mode
* chore: escalate stack trace to console * refactor: event finding in roll * docs: initial roll specification * refactor: call to roll * refactor: normalise index * refactor: roll into next event * refactor: hot reload on waiting to roll * refactor: wait to roll next
This commit is contained in:
@@ -19,7 +19,7 @@ import {
|
||||
resolvePublicDirectoy,
|
||||
} from './setup/index.js';
|
||||
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
|
||||
import { consoleSuccess, consoleHighlight } from './utils/console.js';
|
||||
import { consoleSuccess, consoleHighlight, consoleError } from './utils/console.js';
|
||||
|
||||
// Import Routers
|
||||
import { appRouter } from './api-data/index.js';
|
||||
@@ -285,12 +285,18 @@ export const shutdown = async (exitCode = 0) => {
|
||||
process.on('exit', (code) => consoleHighlight(`Ontime shutdown with code: ${code}`));
|
||||
|
||||
process.on('unhandledRejection', async (error) => {
|
||||
if (!isProduction && error instanceof Error && error.stack) {
|
||||
consoleError(error.stack);
|
||||
}
|
||||
generateCrashReport(error);
|
||||
logger.crash(LogOrigin.Server, `Uncaught exception | ${error}`);
|
||||
await shutdown(1);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', async (error) => {
|
||||
if (!isProduction && error instanceof Error && error.stack) {
|
||||
consoleError(error.stack);
|
||||
}
|
||||
generateCrashReport(error);
|
||||
logger.crash(LogOrigin.Server, `Uncaught exception | ${error}`);
|
||||
await shutdown(1);
|
||||
|
||||
@@ -9,7 +9,7 @@ type UpdateCallbackFn = (updateResult: UpdateResult) => void;
|
||||
/**
|
||||
* Service manages Ontime's main timer
|
||||
*/
|
||||
export class TimerService {
|
||||
export class EventTimer {
|
||||
private readonly _interval: NodeJS.Timeout;
|
||||
/** how often we recalculate */
|
||||
static _refreshInterval: number;
|
||||
@@ -26,15 +26,14 @@ export class TimerService {
|
||||
* @param {function} [timerConfig.onUpdateCallback] how often we update the socket
|
||||
*/
|
||||
constructor(timerConfig: { refresh: number; updateInterval: number }) {
|
||||
TimerService._refreshInterval = timerConfig.refresh;
|
||||
EventTimer._refreshInterval = timerConfig.refresh;
|
||||
this._interval = setInterval(() => {
|
||||
this.update();
|
||||
}, TimerService._refreshInterval);
|
||||
}, EventTimer._refreshInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows setting a callback for when the timer updates
|
||||
* @param callback
|
||||
*/
|
||||
setOnUpdateCallback(callback: (updateResult: UpdateResult) => void) {
|
||||
this.onUpdateCallback = callback;
|
||||
@@ -73,7 +72,6 @@ export class TimerService {
|
||||
|
||||
/**
|
||||
* Adds time to running timer by given amount
|
||||
* @param {number} amount
|
||||
*/
|
||||
addTime(amount: number): boolean {
|
||||
if (!runtimeState.addTime(amount)) {
|
||||
@@ -104,10 +102,9 @@ export class TimerService {
|
||||
|
||||
/**
|
||||
* Loads roll information into timer service
|
||||
* @param {OntimeEvent[]} rundown -- list of events to run
|
||||
*/
|
||||
roll(rundown: OntimeRundown) {
|
||||
runtimeState.roll(rundown);
|
||||
return runtimeState.roll(rundown);
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
@@ -0,0 +1,448 @@
|
||||
import { OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE } from 'ontime-utils';
|
||||
|
||||
import { loadRoll } from '../rollUtils.js';
|
||||
|
||||
const baseEvent = {
|
||||
type: SupportedEvent.Event,
|
||||
skip: false,
|
||||
};
|
||||
|
||||
function makeOntimeEvent(patch: Partial<OntimeEvent>): OntimeEvent {
|
||||
return {
|
||||
...baseEvent,
|
||||
...patch,
|
||||
} as OntimeEvent;
|
||||
}
|
||||
|
||||
function prepareTimedEvents(events: Partial<OntimeEvent>[]): OntimeEvent[] {
|
||||
return events.map(makeOntimeEvent);
|
||||
}
|
||||
|
||||
describe('loadRoll()', () => {
|
||||
const eventlist = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 5,
|
||||
timeEnd: 10,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timeStart: 10,
|
||||
timeEnd: 20,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
timeStart: 20,
|
||||
timeEnd: 30,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
timeStart: 30,
|
||||
timeEnd: 40,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
timeStart: 40,
|
||||
timeEnd: 50,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
timeStart: 50,
|
||||
timeEnd: 60,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
timeStart: 60,
|
||||
timeEnd: 70,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
timeStart: 70,
|
||||
timeEnd: 80,
|
||||
isPublic: false,
|
||||
},
|
||||
];
|
||||
const timedEvents = prepareTimedEvents(eventlist);
|
||||
|
||||
it('should roll to the day after if timer is at 100', () => {
|
||||
const now = 100;
|
||||
const expected = {
|
||||
event: timedEvents[0],
|
||||
index: 0,
|
||||
isPending: true,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should be waiting to start if timer is at 0', () => {
|
||||
const now = 0;
|
||||
const expected = {
|
||||
event: timedEvents[0],
|
||||
index: 0,
|
||||
isPending: true,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should start the first event if timer is at 5', () => {
|
||||
const now = 5;
|
||||
const expected = {
|
||||
event: timedEvents[0],
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should start the second event if timer is at 15', () => {
|
||||
const now = 15;
|
||||
const expected = {
|
||||
event: timedEvents[1],
|
||||
index: 1,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should start the third event if timer is at 10', () => {
|
||||
const now = 20;
|
||||
const expected = {
|
||||
event: timedEvents[2],
|
||||
index: 2,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should start the fifth event if timer is at 49', () => {
|
||||
const now = 49;
|
||||
const expected = {
|
||||
event: timedEvents[4],
|
||||
index: 4,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should start the seventh event if timer is at 63', () => {
|
||||
const now = 63;
|
||||
const expected = {
|
||||
event: timedEvents[6],
|
||||
index: 6,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should start the eight event if timer is at 75', () => {
|
||||
const now = 75;
|
||||
const expected = {
|
||||
event: timedEvents[7],
|
||||
index: 7,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadRoll() handle edge cases with midnight', () => {
|
||||
it('should find an event that crosses midnight', () => {
|
||||
const now = 23 * MILLIS_PER_HOUR;
|
||||
const eventlist = [
|
||||
{
|
||||
id: '0',
|
||||
timeStart: 9 * MILLIS_PER_HOUR,
|
||||
timeEnd: 10 * MILLIS_PER_HOUR,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 20 * MILLIS_PER_HOUR,
|
||||
timeEnd: 22 * MILLIS_PER_HOUR,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timeStart: 22 * MILLIS_PER_HOUR,
|
||||
timeEnd: 1 * MILLIS_PER_HOUR,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
timeStart: 1 * MILLIS_PER_HOUR,
|
||||
timeEnd: 1 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
timeStart: 1 * MILLIS_PER_HOUR,
|
||||
timeEnd: 2 * MILLIS_PER_HOUR,
|
||||
isPublic: true,
|
||||
},
|
||||
];
|
||||
const timedEvents = prepareTimedEvents(eventlist);
|
||||
|
||||
const expected = {
|
||||
event: timedEvents[2],
|
||||
index: 2,
|
||||
};
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should not skip to the second day', () => {
|
||||
/**
|
||||
* NOTE: this is a potentially contentious decision
|
||||
*
|
||||
* The idea here is that it makes no sense for us to jump to the second / third day on activating roll
|
||||
* if the user wants to skip a portion of the rundown, they can manually jump to the event and activate roll
|
||||
*
|
||||
* On our side, this simplifies logic and makes behaviour more predictable
|
||||
*/
|
||||
const now = 8 * MILLIS_PER_HOUR;
|
||||
const eventlist = [
|
||||
{
|
||||
id: '0',
|
||||
timeStart: 21 * MILLIS_PER_HOUR,
|
||||
timeEnd: 22 * MILLIS_PER_HOUR,
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 22 * MILLIS_PER_HOUR,
|
||||
timeEnd: 3 * MILLIS_PER_HOUR,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timeStart: 3 * MILLIS_PER_HOUR,
|
||||
timeEnd: 10 * MILLIS_PER_HOUR,
|
||||
},
|
||||
];
|
||||
const timedEvents = prepareTimedEvents(eventlist);
|
||||
const expected = {
|
||||
event: timedEvents[0],
|
||||
index: 0,
|
||||
isPending: true,
|
||||
};
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadRoll() handle edge cases with before and after start', () => {
|
||||
it('should prepare first event, if we are not yet in the rundown start', () => {
|
||||
const now = 7 * MILLIS_PER_HOUR;
|
||||
const singleEventList = [
|
||||
makeOntimeEvent({
|
||||
id: '1',
|
||||
timeStart: 10 * MILLIS_PER_HOUR,
|
||||
timeEnd: 11 * MILLIS_PER_HOUR,
|
||||
isPublic: true,
|
||||
}),
|
||||
];
|
||||
|
||||
const expected = {
|
||||
event: singleEventList[0],
|
||||
index: 0,
|
||||
isPending: true,
|
||||
};
|
||||
const state = loadRoll(singleEventList, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should prepare first event, if we are over the rundown end', () => {
|
||||
const now = 18 * MILLIS_PER_HOUR;
|
||||
const singleEventList = [
|
||||
makeOntimeEvent({
|
||||
id: '1',
|
||||
timeStart: 10 * MILLIS_PER_HOUR,
|
||||
timeEnd: 11 * MILLIS_PER_HOUR,
|
||||
isPublic: true,
|
||||
}),
|
||||
];
|
||||
|
||||
const expected = {
|
||||
event: singleEventList[0],
|
||||
index: 0,
|
||||
isPending: true,
|
||||
};
|
||||
const state = loadRoll(singleEventList, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should account for a rundown that goes through midnight', () => {
|
||||
const now = 1 * MILLIS_PER_HOUR;
|
||||
const singleEventList = [
|
||||
makeOntimeEvent({
|
||||
id: '1',
|
||||
timeStart: 10 * MILLIS_PER_HOUR,
|
||||
timeEnd: 2 * MILLIS_PER_HOUR,
|
||||
isPublic: true,
|
||||
}),
|
||||
];
|
||||
const expected = {
|
||||
event: singleEventList[0],
|
||||
index: 0,
|
||||
};
|
||||
const state = loadRoll(singleEventList, now);
|
||||
expect(state.isPending).toBeUndefined();
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('loads upcoming event while waiting to roll', () => {
|
||||
const now = 6000; // 00:01
|
||||
const singleEventList = [
|
||||
makeOntimeEvent({
|
||||
id: '1',
|
||||
timeStart: 72000000, // 20:00
|
||||
timeEnd: 72010000, // 20:10
|
||||
isPublic: true,
|
||||
}),
|
||||
];
|
||||
const expected = {
|
||||
event: singleEventList[0],
|
||||
index: 0,
|
||||
isPending: true,
|
||||
};
|
||||
const state = loadRoll(singleEventList, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadRoll() test that roll behaviour with overlapping times', () => {
|
||||
const eventlist = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 10,
|
||||
timeEnd: 10,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timeStart: 10,
|
||||
timeEnd: 20,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
timeStart: 10,
|
||||
timeEnd: 30,
|
||||
isPublic: false,
|
||||
},
|
||||
];
|
||||
const timedEvents = prepareTimedEvents(eventlist);
|
||||
|
||||
it('if timer is at 0', () => {
|
||||
const now = 0;
|
||||
const expected = {
|
||||
event: timedEvents[0],
|
||||
index: 0,
|
||||
isPending: true,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 10, it ignores events with 0 duration', () => {
|
||||
const now = 10;
|
||||
const expected = {
|
||||
event: timedEvents[1],
|
||||
index: 1,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 15', () => {
|
||||
const now = 15;
|
||||
const expected = {
|
||||
event: timedEvents[1],
|
||||
index: 1,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 20', () => {
|
||||
const now = 20;
|
||||
const expected = {
|
||||
event: timedEvents[2],
|
||||
index: 2,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 25', () => {
|
||||
const now = 25;
|
||||
const expected = {
|
||||
event: timedEvents[2],
|
||||
index: 2,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
// issue #58
|
||||
describe('loadRoll() test that roll behaviour multi day event edge cases', () => {
|
||||
it('should recognise a playing event where its schedule spans over midnight', () => {
|
||||
const now = 66600000; // 19:30
|
||||
const eventlist = [
|
||||
makeOntimeEvent({
|
||||
id: '1',
|
||||
timeStart: 66000000, // 19:20
|
||||
timeEnd: 54600000, // 16:10
|
||||
isPublic: false,
|
||||
}),
|
||||
];
|
||||
const expected = {
|
||||
event: eventlist[0],
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const state = loadRoll(eventlist, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if the start time is the day after end time, and both are later than now', () => {
|
||||
const now = 66840000; // 19:34
|
||||
const eventlist = [
|
||||
makeOntimeEvent({
|
||||
id: '1',
|
||||
timeStart: 67200000, // 19:40
|
||||
timeEnd: 66900000, // 19:35
|
||||
isPublic: false,
|
||||
}),
|
||||
];
|
||||
const expected = {
|
||||
event: eventlist[0],
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const state = loadRoll(eventlist, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,9 @@
|
||||
import { MILLIS_PER_HOUR, dayInMs, millisToString } from 'ontime-utils';
|
||||
import { EndAction, OntimeEvent, Playback, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
|
||||
import { EndAction, Playback, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
|
||||
|
||||
import {
|
||||
getCurrent,
|
||||
getExpectedFinish,
|
||||
getRollTimers,
|
||||
getRuntimeOffset,
|
||||
getTimerPhase,
|
||||
getTotalDuration,
|
||||
@@ -696,520 +695,6 @@ describe('skippedOutOfEvent()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRollTimers()', () => {
|
||||
const eventlist: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 5,
|
||||
timeEnd: 10,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timeStart: 10,
|
||||
timeEnd: 20,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
timeStart: 20,
|
||||
timeEnd: 30,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
timeStart: 30,
|
||||
timeEnd: 40,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
timeStart: 40,
|
||||
timeEnd: 50,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
timeStart: 50,
|
||||
timeEnd: 60,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
timeStart: 60,
|
||||
timeEnd: 70,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
timeStart: 70,
|
||||
timeEnd: 80,
|
||||
isPublic: false,
|
||||
},
|
||||
];
|
||||
|
||||
it('if timer is at 0', () => {
|
||||
const now = 0;
|
||||
const expected = {
|
||||
nowIndex: null,
|
||||
nowId: null,
|
||||
publicIndex: null,
|
||||
nextIndex: 0,
|
||||
publicNextIndex: 4,
|
||||
timeToNext: 5,
|
||||
nextEvent: eventlist[0],
|
||||
nextPublicEvent: eventlist[4],
|
||||
currentEvent: null,
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 5', () => {
|
||||
const now = 5;
|
||||
const expected = {
|
||||
nowIndex: 0,
|
||||
nowId: eventlist[0].id,
|
||||
publicIndex: null,
|
||||
nextIndex: 1,
|
||||
publicNextIndex: 4,
|
||||
timeToNext: 5,
|
||||
nextEvent: eventlist[1],
|
||||
nextPublicEvent: eventlist[4],
|
||||
currentEvent: eventlist[0],
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 15', () => {
|
||||
const now = 15;
|
||||
const expected = {
|
||||
nowIndex: 1,
|
||||
nowId: eventlist[1].id,
|
||||
publicIndex: null,
|
||||
nextIndex: 2,
|
||||
publicNextIndex: 4,
|
||||
timeToNext: 5,
|
||||
nextEvent: eventlist[2],
|
||||
nextPublicEvent: eventlist[4],
|
||||
currentEvent: eventlist[1],
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 20', () => {
|
||||
const now = 20;
|
||||
const expected = {
|
||||
nowIndex: 2,
|
||||
nowId: eventlist[2].id,
|
||||
publicIndex: null,
|
||||
nextIndex: 3,
|
||||
publicNextIndex: 4,
|
||||
timeToNext: 10,
|
||||
nextEvent: eventlist[3],
|
||||
nextPublicEvent: eventlist[4],
|
||||
currentEvent: eventlist[2],
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 49', () => {
|
||||
const now = 49;
|
||||
const expected = {
|
||||
nowIndex: 4,
|
||||
nowId: eventlist[4].id,
|
||||
publicIndex: 4,
|
||||
nextIndex: 5,
|
||||
publicNextIndex: 6,
|
||||
timeToNext: 1,
|
||||
nextEvent: eventlist[5],
|
||||
nextPublicEvent: eventlist[6],
|
||||
currentEvent: eventlist[4],
|
||||
currentPublicEvent: eventlist[4],
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 63', () => {
|
||||
const now = 63;
|
||||
const expected = {
|
||||
nowIndex: 6,
|
||||
nowId: eventlist[6].id,
|
||||
publicIndex: 6,
|
||||
nextIndex: 7,
|
||||
publicNextIndex: null,
|
||||
timeToNext: 7,
|
||||
nextEvent: eventlist[7],
|
||||
nextPublicEvent: null,
|
||||
currentEvent: eventlist[6],
|
||||
currentPublicEvent: eventlist[6],
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 75', () => {
|
||||
const now = 75;
|
||||
const expected = {
|
||||
nowIndex: 7,
|
||||
nowId: eventlist[7].id,
|
||||
publicIndex: 6,
|
||||
nextIndex: null,
|
||||
publicNextIndex: null,
|
||||
timeToNext: null,
|
||||
nextEvent: null,
|
||||
nextPublicEvent: null,
|
||||
currentEvent: eventlist[7],
|
||||
currentPublicEvent: eventlist[6],
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 100 we roll to day after', () => {
|
||||
const now = 100;
|
||||
const expected = {
|
||||
nowIndex: null,
|
||||
nowId: null,
|
||||
publicIndex: null,
|
||||
nextIndex: 0,
|
||||
publicNextIndex: 4,
|
||||
timeToNext: dayInMs - now + eventlist[0].timeStart!,
|
||||
nextEvent: eventlist[0],
|
||||
nextPublicEvent: eventlist[4],
|
||||
currentEvent: null,
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('handles rolls to next day with real values', () => {
|
||||
const singleEventList: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 36000000, // 10:00
|
||||
timeEnd: 39600000, // 11:00
|
||||
isPublic: true,
|
||||
},
|
||||
];
|
||||
const now = 64800000; // 18:00
|
||||
const expected = {
|
||||
nowIndex: null,
|
||||
nowId: null,
|
||||
publicIndex: null,
|
||||
nextIndex: 0,
|
||||
publicNextIndex: 0,
|
||||
timeToNext: dayInMs - now + singleEventList[0].timeStart!,
|
||||
nextEvent: singleEventList[0],
|
||||
nextPublicEvent: singleEventList[0],
|
||||
currentEvent: null,
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
const state = getRollTimers(singleEventList as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('handles rolls to next day with real values', () => {
|
||||
const singleEventList: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 36000000, // 10:00
|
||||
timeEnd: 3600000, // 01:00
|
||||
isPublic: true,
|
||||
},
|
||||
];
|
||||
const now = 60000; // 00:01
|
||||
const expected = {
|
||||
nowIndex: 0,
|
||||
nowId: singleEventList[0].id,
|
||||
publicIndex: 0,
|
||||
nextIndex: null,
|
||||
publicNextIndex: null,
|
||||
timeToNext: null,
|
||||
nextEvent: null,
|
||||
nextPublicEvent: null,
|
||||
currentEvent: singleEventList[0],
|
||||
currentPublicEvent: singleEventList[0],
|
||||
};
|
||||
const state = getRollTimers(singleEventList as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
it('handles rolls to next day with real values', () => {
|
||||
const singleEventList: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 36000000, // 10:00
|
||||
timeEnd: 3600000, // 01:00
|
||||
isPublic: true,
|
||||
},
|
||||
];
|
||||
const now = 60000; // 00:01
|
||||
const expected = {
|
||||
nowIndex: 0,
|
||||
nowId: singleEventList[0].id,
|
||||
publicIndex: 0,
|
||||
nextIndex: null,
|
||||
publicNextIndex: null,
|
||||
timeToNext: null,
|
||||
nextEvent: null,
|
||||
nextPublicEvent: null,
|
||||
currentEvent: singleEventList[0],
|
||||
currentPublicEvent: singleEventList[0],
|
||||
};
|
||||
const state = getRollTimers(singleEventList as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('loads upcoming event while waiting to roll', () => {
|
||||
const singleEventList: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 72000000, // 20:00
|
||||
timeEnd: 72010000, // 20:10
|
||||
isPublic: true,
|
||||
},
|
||||
];
|
||||
const now = 6000; // 00:01
|
||||
const expected = {
|
||||
nowIndex: null,
|
||||
nowId: null,
|
||||
publicIndex: null,
|
||||
nextIndex: 0,
|
||||
publicNextIndex: 0,
|
||||
timeToNext: 72000000 - now,
|
||||
nextEvent: singleEventList[0],
|
||||
nextPublicEvent: singleEventList[0],
|
||||
currentEvent: null,
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
const state = getRollTimers(singleEventList as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('handles roll that goes over midnight', () => {
|
||||
const singleEventList: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 72000000, // 20:00
|
||||
timeEnd: 60000, // 00:10
|
||||
isPublic: true,
|
||||
},
|
||||
];
|
||||
const now = 6000; // 00:01
|
||||
const expected = {
|
||||
nowIndex: 0,
|
||||
nowId: singleEventList[0].id,
|
||||
publicIndex: 0,
|
||||
nextIndex: null,
|
||||
publicNextIndex: null,
|
||||
timeToNext: null,
|
||||
nextEvent: null,
|
||||
nextPublicEvent: null,
|
||||
currentEvent: singleEventList[0],
|
||||
currentPublicEvent: singleEventList[0],
|
||||
};
|
||||
const state = getRollTimers(singleEventList as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRollTimers() test that roll behaviour with overlapping times', () => {
|
||||
const eventlist: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 10,
|
||||
timeEnd: 10,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timeStart: 10,
|
||||
timeEnd: 20,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
timeStart: 10,
|
||||
timeEnd: 30,
|
||||
isPublic: false,
|
||||
},
|
||||
];
|
||||
|
||||
it('if timer is at 0', () => {
|
||||
const now = 0;
|
||||
const expected = {
|
||||
nowIndex: null,
|
||||
nowId: null,
|
||||
publicIndex: null,
|
||||
nextIndex: 0,
|
||||
publicNextIndex: 1,
|
||||
timeToNext: 10,
|
||||
nextEvent: eventlist[0],
|
||||
nextPublicEvent: eventlist[1],
|
||||
currentEvent: null,
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 10', () => {
|
||||
const now = 10;
|
||||
const expected = {
|
||||
nowIndex: 1,
|
||||
nowId: eventlist[1].id,
|
||||
publicIndex: 1,
|
||||
nextIndex: 2,
|
||||
publicNextIndex: null,
|
||||
timeToNext: 0,
|
||||
nextEvent: eventlist[2],
|
||||
nextPublicEvent: null,
|
||||
currentEvent: eventlist[1],
|
||||
currentPublicEvent: eventlist[1],
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 15', () => {
|
||||
const now = 15;
|
||||
const expected = {
|
||||
nowIndex: 1,
|
||||
nowId: eventlist[1].id,
|
||||
publicIndex: 1,
|
||||
nextIndex: 2,
|
||||
publicNextIndex: null,
|
||||
timeToNext: -5,
|
||||
nextEvent: eventlist[2],
|
||||
nextPublicEvent: null,
|
||||
currentEvent: eventlist[1],
|
||||
currentPublicEvent: eventlist[1],
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 20', () => {
|
||||
const now = 20;
|
||||
const expected = {
|
||||
nowIndex: 2,
|
||||
nowId: eventlist[2].id,
|
||||
publicIndex: 1,
|
||||
nextIndex: null,
|
||||
publicNextIndex: null,
|
||||
timeToNext: null,
|
||||
nextEvent: null,
|
||||
nextPublicEvent: null,
|
||||
currentEvent: eventlist[2],
|
||||
currentPublicEvent: eventlist[1],
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 25', () => {
|
||||
const now = 25;
|
||||
const expected = {
|
||||
nowIndex: 2,
|
||||
nowId: eventlist[2].id,
|
||||
publicIndex: 1,
|
||||
nextIndex: null,
|
||||
publicNextIndex: null,
|
||||
timeToNext: null,
|
||||
nextEvent: null,
|
||||
nextPublicEvent: null,
|
||||
currentEvent: eventlist[2],
|
||||
currentPublicEvent: eventlist[1],
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
// issue #58
|
||||
describe('getRollTimers() test that roll behaviour multi day event edge cases', () => {
|
||||
it('if the start time is the day after end time, and start time is earlier than now', () => {
|
||||
const now = 66600000; // 19:30
|
||||
const eventlist: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 66000000, // 19:20
|
||||
timeEnd: 54600000, // 16:10
|
||||
isPublic: false,
|
||||
},
|
||||
];
|
||||
const expected = {
|
||||
nowIndex: 0,
|
||||
nowId: '1',
|
||||
publicIndex: null,
|
||||
nextIndex: null,
|
||||
publicNextIndex: null,
|
||||
timeToNext: null,
|
||||
nextEvent: null,
|
||||
nextPublicEvent: null,
|
||||
currentEvent: eventlist[0],
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if the start time is the day after end time, and both are later than now', () => {
|
||||
const now = 66840000; // 19:34
|
||||
const eventlist: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 67200000, // 19:40
|
||||
timeEnd: 66900000, // 19:35
|
||||
isPublic: false,
|
||||
},
|
||||
];
|
||||
const expected = {
|
||||
currentEvent: {
|
||||
id: '1',
|
||||
isPublic: false,
|
||||
timeEnd: 66900000,
|
||||
timeStart: 67200000,
|
||||
},
|
||||
currentPublicEvent: null,
|
||||
nextEvent: null,
|
||||
nextIndex: null,
|
||||
nextPublicEvent: null,
|
||||
nowId: '1',
|
||||
nowIndex: 0,
|
||||
publicIndex: null,
|
||||
publicNextIndex: null,
|
||||
timeToNext: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
test('normaliseEndTime()', () => {
|
||||
const t1 = {
|
||||
start: 10,
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { dayInMs, getFirstEvent, getLastEvent } from 'ontime-utils';
|
||||
import { OntimeEvent, MaybeNumber, PlayableEvent, isPlayableEvent } from 'ontime-types';
|
||||
|
||||
import { normaliseEndTime } from './timerUtils.js';
|
||||
|
||||
/**
|
||||
* Finds current event in a rolling rundown
|
||||
*/
|
||||
export function loadRoll(
|
||||
timedEvents: OntimeEvent[],
|
||||
timeNow: number,
|
||||
): {
|
||||
event: PlayableEvent | null;
|
||||
index: MaybeNumber;
|
||||
isPending?: boolean;
|
||||
} {
|
||||
const { firstEvent } = getFirstEvent(timedEvents);
|
||||
const { lastEvent } = getLastEvent(timedEvents);
|
||||
|
||||
if (!firstEvent || !lastEvent) {
|
||||
return { event: null, index: null };
|
||||
}
|
||||
|
||||
// check that the rundown wraps around midnight
|
||||
const wrapsAroundMidnight = firstEvent.timeStart > lastEvent.timeEnd;
|
||||
|
||||
if (!wrapsAroundMidnight) {
|
||||
// check whether we are before or after the rundown
|
||||
const lastNormalEnd = normaliseEndTime(lastEvent.timeStart, lastEvent.timeEnd);
|
||||
const isAfterRundown = timeNow > lastNormalEnd;
|
||||
const isBeforeRundown = timeNow < firstEvent.timeStart && !isAfterRundown;
|
||||
|
||||
if (isAfterRundown || isBeforeRundown) {
|
||||
return { event: firstEvent, index: 0, isPending: true };
|
||||
}
|
||||
}
|
||||
|
||||
// we know we are in the middle of the rundown and we need to find the current event
|
||||
// account for number of times we went over midnight
|
||||
let daySpan = 0;
|
||||
|
||||
for (let i = 0; i < timedEvents.length; i++) {
|
||||
const event = timedEvents[i];
|
||||
|
||||
if (!isPlayableEvent(event)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// we check if event crosses midnight
|
||||
if (event.timeStart > event.timeEnd) {
|
||||
daySpan++;
|
||||
}
|
||||
|
||||
const correctedDays = dayInMs * daySpan;
|
||||
const correctedStart = event.timeStart + correctedDays;
|
||||
const correctedEnd = event.timeEnd + correctedDays;
|
||||
|
||||
/**
|
||||
* there are 3 possible states for an event
|
||||
* 1. event is already finished
|
||||
* 2. event is running
|
||||
* 3. event is in the future
|
||||
*/
|
||||
|
||||
// 1. event is already finished
|
||||
// when does the event end (handle midnight)
|
||||
const normalEnd = normaliseEndTime(correctedStart, correctedEnd);
|
||||
if (normalEnd <= timeNow) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. event is running and is the first event in our time slot
|
||||
const isFromDayBefore = normalEnd > dayInMs && timeNow < event.timeEnd;
|
||||
const hasStarted = isFromDayBefore || timeNow >= event.timeStart;
|
||||
if (hasStarted) {
|
||||
return { event, index: i };
|
||||
}
|
||||
|
||||
// 3. event will run in the future
|
||||
// we set the isPending flag to indicate that the event is currently playing
|
||||
return { event, index: i, isPending: true };
|
||||
}
|
||||
|
||||
// in case we were unable to find anything, we load the first event
|
||||
return { event: firstEvent, index: 0, isPending: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function, checks whether the event start is the day after
|
||||
*/
|
||||
export function normaliseRollStart(start: number, clock: number) {
|
||||
return start < clock ? start + dayInMs : start;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import { updateRundownData } from '../../stores/runtimeState.js';
|
||||
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
||||
|
||||
import * as cache from './rundownCache.js';
|
||||
import { getPlayableEvents } from './rundownUtils.js';
|
||||
import { getPlayableEvents, getTimedEvents } from './rundownUtils.js';
|
||||
import { eventStore } from '../../stores/EventStore.js';
|
||||
|
||||
type PatchWithId = (Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>) & { id: string };
|
||||
@@ -215,8 +215,8 @@ export async function swapEvents(from: string, to: string) {
|
||||
* Called when we make changes to the rundown object
|
||||
*/
|
||||
function updateRuntimeOnChange() {
|
||||
const playableEvents = getPlayableEvents();
|
||||
const numEvents = playableEvents.length;
|
||||
const timedEvents = getTimedEvents();
|
||||
const numEvents = timedEvents.length;
|
||||
const metadata = cache.getMetadata();
|
||||
|
||||
// schedule an update for the end of the event loop
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { OntimeEvent, OntimeRundown, isOntimeEvent, RundownCached, OntimeRundownEntry } from 'ontime-types';
|
||||
import { OntimeEvent, OntimeRundown, RundownCached, OntimeRundownEntry, PlayableEvent } from 'ontime-types';
|
||||
import { filterPlayable, filterTimedEvents } from 'ontime-utils';
|
||||
|
||||
import * as cache from './rundownCache.js';
|
||||
|
||||
/**
|
||||
* returns the normalised rundown
|
||||
*/
|
||||
export function getNormalisedRundown(): RundownCached {
|
||||
return cache.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* returns entire unfiltered rundown
|
||||
* @return {array}
|
||||
*/
|
||||
export function getRundown(): OntimeRundown {
|
||||
return cache.getPersistedRundown();
|
||||
@@ -16,32 +19,20 @@ export function getRundown(): OntimeRundown {
|
||||
|
||||
/**
|
||||
* returns all events of type OntimeEvent
|
||||
* @return {array}
|
||||
*/
|
||||
export function getTimedEvents(): OntimeEvent[] {
|
||||
return getRundown().filter((event) => isOntimeEvent(event)) as OntimeEvent[];
|
||||
return filterTimedEvents(getRundown());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
export function getPlayableEvents(): PlayableEvent[] {
|
||||
return filterPlayable(getRundown());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
@@ -50,8 +41,6 @@ export function getEventAtIndex(eventIndex: number): OntimeEvent | undefined {
|
||||
|
||||
/**
|
||||
* returns first event that matches a given ID
|
||||
* @param {string} eventId
|
||||
* @return {object | undefined}
|
||||
*/
|
||||
export function getEventWithId(eventId: string): OntimeRundownEntry | undefined {
|
||||
const rundown = getRundown();
|
||||
@@ -60,17 +49,14 @@ export function getEventWithId(eventId: string): OntimeRundownEntry | undefined
|
||||
|
||||
/**
|
||||
* returns first event that matches a given cue
|
||||
* @param {string} targetCue
|
||||
* @param {number} currentEventIndex
|
||||
* @return {object | undefined}
|
||||
*/
|
||||
export function getNextEventWithCue(targetCue: string, currentEventIndex = 0): OntimeEvent | undefined {
|
||||
const timedEvents = getPlayableEvents();
|
||||
const playableEvents = getPlayableEvents();
|
||||
const lowerCaseCue = targetCue.toLowerCase();
|
||||
|
||||
for (let i = currentEventIndex; i < timedEvents.length; i++) {
|
||||
const event = timedEvents.at(i);
|
||||
if (event && event.cue.toLowerCase() === lowerCaseCue) {
|
||||
for (let i = currentEventIndex; i < playableEvents.length; i++) {
|
||||
const event = playableEvents.at(i);
|
||||
if (event?.cue.toLowerCase() === lowerCaseCue) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
@@ -78,43 +64,41 @@ export function getNextEventWithCue(targetCue: string, currentEventIndex = 0): O
|
||||
|
||||
/**
|
||||
* finds the previous event
|
||||
* @return {object | undefined}
|
||||
*/
|
||||
export function findPrevious(currentEventId?: string): OntimeEvent | null {
|
||||
const timedEvents = getPlayableEvents();
|
||||
if (!timedEvents || !timedEvents.length) {
|
||||
const playableEvents = getPlayableEvents();
|
||||
if (!playableEvents || !playableEvents.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// if there is no event running, go to first
|
||||
if (!currentEventId) {
|
||||
return timedEvents.at(0) ?? null;
|
||||
return playableEvents.at(0) ?? null;
|
||||
}
|
||||
|
||||
const currentIndex = timedEvents.findIndex((event) => event.id === currentEventId);
|
||||
const currentIndex = playableEvents.findIndex((event) => event.id === currentEventId);
|
||||
const newIndex = Math.max(currentIndex - 1, 0);
|
||||
const previousEvent = timedEvents.at(newIndex) ?? null;
|
||||
const previousEvent = playableEvents.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) {
|
||||
export function findNext(currentEventId?: string): PlayableEvent | null {
|
||||
const playableEvents = getPlayableEvents();
|
||||
if (!playableEvents.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// if there is no event running, go to first
|
||||
if (!currentEventId) {
|
||||
return timedEvents.at(0) ?? null;
|
||||
return playableEvents.at(0) ?? null;
|
||||
}
|
||||
|
||||
const currentIndex = timedEvents.findIndex((event) => event.id === currentEventId);
|
||||
const currentIndex = playableEvents.findIndex((event) => event.id === currentEventId);
|
||||
const newIndex = currentIndex + 1;
|
||||
const nextEvent = timedEvents.at(newIndex);
|
||||
const nextEvent = playableEvents.at(newIndex);
|
||||
return nextEvent ?? null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
EndAction,
|
||||
isOntimeEvent,
|
||||
isPlayableEvent,
|
||||
LogOrigin,
|
||||
MaybeNumber,
|
||||
OntimeEvent,
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
TimerLifeCycle,
|
||||
TimerPhase,
|
||||
} from 'ontime-types';
|
||||
import { filterPlayable, millisToString, validatePlayback } from 'ontime-utils';
|
||||
import { millisToString, validatePlayback } from 'ontime-utils';
|
||||
|
||||
import { deepEqual } from 'fast-equals';
|
||||
|
||||
@@ -19,7 +20,7 @@ import type { RuntimeState } from '../../stores/runtimeState.js';
|
||||
import { timerConfig } from '../../config/config.js';
|
||||
import { eventStore } from '../../stores/EventStore.js';
|
||||
|
||||
import { TimerService } from '../TimerService.js';
|
||||
import { EventTimer } from '../EventTimer.js';
|
||||
import { RestorePoint, restoreService } from '../RestoreService.js';
|
||||
import {
|
||||
findNext,
|
||||
@@ -27,20 +28,20 @@ import {
|
||||
getEventAtIndex,
|
||||
getNextEventWithCue,
|
||||
getEventWithId,
|
||||
getPlayableEvents,
|
||||
getRundown,
|
||||
getTimedEvents,
|
||||
} from '../rundown-service/rundownUtils.js';
|
||||
import { skippedOutOfEvent } from '../timerUtils.js';
|
||||
import { integrationService } from '../integration-service/IntegrationService.js';
|
||||
|
||||
import { getForceUpdate, getShouldClockUpdate, getShouldTimerUpdate } from './rundownService.utils.js';
|
||||
import { skippedOutOfEvent } from '../timerUtils.js';
|
||||
|
||||
/**
|
||||
* Service manages runtime status of app
|
||||
* Coordinating with necessary services
|
||||
*/
|
||||
class RuntimeService {
|
||||
private eventTimer: TimerService;
|
||||
private eventTimer: EventTimer;
|
||||
private lastIntegrationClockUpdate = -1;
|
||||
private lastIntegrationTimerValue = -1;
|
||||
|
||||
@@ -52,8 +53,8 @@ class RuntimeService {
|
||||
/** last known state */
|
||||
static previousState: RuntimeState;
|
||||
|
||||
constructor(timerService: TimerService) {
|
||||
this.eventTimer = timerService;
|
||||
constructor(eventTimer: EventTimer) {
|
||||
this.eventTimer = eventTimer;
|
||||
|
||||
RuntimeService.previousTimerUpdate = -1;
|
||||
RuntimeService.previousTimerValue = -1;
|
||||
@@ -63,7 +64,7 @@ class RuntimeService {
|
||||
|
||||
/** Checks result of an update and notifies integrations as needed */
|
||||
@broadcastResult
|
||||
checkTimerUpdate({ shouldCallRoll, hasTimerFinished }: runtimeState.UpdateResult) {
|
||||
checkTimerUpdate({ hasTimerFinished, hasSecondaryTimerFinished }: runtimeState.UpdateResult) {
|
||||
const newState = runtimeState.getState();
|
||||
|
||||
// 1. find if we need to dispatch integrations related to the phase
|
||||
@@ -82,35 +83,36 @@ class RuntimeService {
|
||||
|
||||
// 2. handle edge cases related to roll
|
||||
if (newState.timer.playback === Playback.Roll) {
|
||||
// check if we need to call roll again
|
||||
const needsEvent =
|
||||
newState.eventNow === null
|
||||
? true
|
||||
: skippedOutOfEvent(newState, this.lastIntegrationClockUpdate, timerConfig.skipLimit);
|
||||
const hasFinishedRoll = hasTimerFinished && shouldCallRoll;
|
||||
if (shouldCallRoll || needsEvent) {
|
||||
if (hasFinishedRoll) {
|
||||
process.nextTick(() => {
|
||||
integrationService.dispatch(TimerLifeCycle.onFinish);
|
||||
});
|
||||
}
|
||||
// check if we need to call any side effects
|
||||
|
||||
// we dont call this.roll because we need to bypass the checks
|
||||
const rundown = getRundown();
|
||||
// TODO: by not calling roll, we dont get the events
|
||||
this.eventTimer.roll(rundown);
|
||||
if (hasSecondaryTimerFinished) {
|
||||
// if the secondary timer has finished, we need to call roll
|
||||
// since event is already loaded
|
||||
this.rollLoaded();
|
||||
} else if (hasTimerFinished) {
|
||||
// if the timer has finished, we need to load next and keep rolling
|
||||
process.nextTick(() => {
|
||||
integrationService.dispatch(TimerLifeCycle.onFinish);
|
||||
});
|
||||
this.loadNext();
|
||||
this.rollLoaded();
|
||||
} else if (skippedOutOfEvent(newState, this.lastIntegrationClockUpdate, timerConfig.skipLimit)) {
|
||||
// if we have skipped out of the event, we will recall roll
|
||||
// to push the playback to the right place
|
||||
// this comes with the caveat that we will lose our runtime data
|
||||
this.roll(true);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. find if we need to process actions related to the timer finishing
|
||||
if (newState.timer.playback !== Playback.Roll && hasTimerFinished) {
|
||||
if (newState.timer.playback === Playback.Play && hasTimerFinished) {
|
||||
process.nextTick(() => {
|
||||
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) {
|
||||
if (newState.eventNow.endAction === EndAction.Stop) {
|
||||
setTimeout(this.stop.bind(this), 0);
|
||||
} else if (newState.eventNow.endAction === EndAction.LoadNext) {
|
||||
@@ -177,7 +179,7 @@ class RuntimeService {
|
||||
}
|
||||
|
||||
private isNewNext() {
|
||||
const timedEvents = getPlayableEvents();
|
||||
const timedEvents = getTimedEvents();
|
||||
const state = runtimeState.getState();
|
||||
const now = state.eventNow?.id;
|
||||
const next = state.eventNext?.id;
|
||||
@@ -233,41 +235,41 @@ class RuntimeService {
|
||||
// 3. the edited event replaces next event
|
||||
let isNext = false;
|
||||
|
||||
// TODO: review logic
|
||||
if (safeOption || eventInMemory) {
|
||||
if (state.timer.playback === Playback.Roll) {
|
||||
this.roll();
|
||||
}
|
||||
// load stuff again, but keep running if our events still exist
|
||||
const eventNow = getEventWithId(state.eventNow.id);
|
||||
if (!isOntimeEvent(eventNow)) {
|
||||
if (state.eventNow !== null) {
|
||||
// load stuff again, but keep running if our events still exist
|
||||
const eventNow = getEventWithId(state.eventNow.id);
|
||||
if (!isOntimeEvent(eventNow) || !isPlayableEvent(eventNow)) {
|
||||
return;
|
||||
}
|
||||
const onlyChangedNow = affectedIds?.length === 1 && affectedIds.at(0) === eventNow.id;
|
||||
if (onlyChangedNow) {
|
||||
runtimeState.reload(eventNow);
|
||||
} else {
|
||||
const rundown = getRundown();
|
||||
runtimeState.reloadAll(rundown);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const onlyChangedNow = affectedIds?.length === 1 && affectedIds.at(0) === eventNow.id;
|
||||
if (onlyChangedNow) {
|
||||
runtimeState.reload(eventNow);
|
||||
} else {
|
||||
const rundown = getRundown();
|
||||
runtimeState.reloadAll(eventNow, rundown);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Maybe the event will become the next
|
||||
isNext = this.isNewNext();
|
||||
if (isNext) {
|
||||
const rundown = getRundown();
|
||||
runtimeState.loadNext(rundown);
|
||||
const timedEvents = getTimedEvents();
|
||||
runtimeState.loadNext(timedEvents);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* makes calls for loading and starting given event
|
||||
* @param {OntimeEvent} event
|
||||
* @param {PlayableEvent} event
|
||||
* @return {boolean} success - whether an event was loaded
|
||||
*/
|
||||
@broadcastResult
|
||||
loadEvent(event: OntimeEvent): boolean {
|
||||
if (event.skip) {
|
||||
if (!isPlayableEvent(event)) {
|
||||
logger.warning(LogOrigin.Playback, `Refused skipped event with ID ${event.id}`);
|
||||
return false;
|
||||
}
|
||||
@@ -407,14 +409,16 @@ class RuntimeService {
|
||||
*/
|
||||
@broadcastResult
|
||||
start(): boolean {
|
||||
const state = runtimeState.getState();
|
||||
const canStart = validatePlayback(state.timer.playback).start;
|
||||
const previousState = runtimeState.getState();
|
||||
const canStart = validatePlayback(previousState.timer.playback).start;
|
||||
if (!canStart) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const didStart = this.eventTimer?.start() ?? false;
|
||||
logger.info(LogOrigin.Playback, `Play Mode ${state.timer.playback.toUpperCase()}`);
|
||||
const newState = runtimeState.getState();
|
||||
logger.info(LogOrigin.Playback, `Play Mode ${newState.timer.playback.toUpperCase()}`);
|
||||
|
||||
if (didStart) {
|
||||
process.nextTick(() => {
|
||||
integrationService.dispatch(TimerLifeCycle.onStart);
|
||||
@@ -505,29 +509,56 @@ class RuntimeService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles special case to call roll on a loaded event which we do not want to discard
|
||||
*/
|
||||
rollLoaded() {
|
||||
const rundown = getRundown();
|
||||
try {
|
||||
this.eventTimer.roll(rundown);
|
||||
} catch (error) {
|
||||
logger.error(LogOrigin.Server, `Roll: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets playback to roll
|
||||
*/
|
||||
@broadcastResult
|
||||
roll() {
|
||||
const beforeState = runtimeState.getState();
|
||||
const canRoll = validatePlayback(beforeState.timer.playback).roll;
|
||||
if (!canRoll) {
|
||||
roll(skipCheck: boolean = false) {
|
||||
const previousState = runtimeState.getState();
|
||||
if (!skipCheck) {
|
||||
const canRoll = validatePlayback(previousState.timer.playback).roll;
|
||||
if (!canRoll) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const rundown = getRundown();
|
||||
const result = this.eventTimer.roll(rundown);
|
||||
if (result.eventId !== previousState.eventNow?.id) {
|
||||
logger.info(LogOrigin.Playback, `Loaded event with ID ${result.eventId}`);
|
||||
process.nextTick(() => {
|
||||
integrationService.dispatch(TimerLifeCycle.onLoad);
|
||||
});
|
||||
}
|
||||
|
||||
if (result.didStart) {
|
||||
process.nextTick(() => {
|
||||
integrationService.dispatch(TimerLifeCycle.onStart);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(LogOrigin.Server, `Roll: ${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const rundown = getRundown();
|
||||
const playableEvents = filterPlayable(rundown);
|
||||
if (playableEvents.length === 0) {
|
||||
logger.warning(LogOrigin.Server, 'Roll: no events found');
|
||||
return;
|
||||
const newState = runtimeState.getState();
|
||||
|
||||
if (previousState.timer.playback !== newState.timer.playback) {
|
||||
logger.info(LogOrigin.Playback, `Play Mode ${newState.timer.playback.toUpperCase()}`);
|
||||
}
|
||||
|
||||
this.eventTimer.roll(rundown);
|
||||
|
||||
const state = runtimeState.getState();
|
||||
const newState = state.timer.playback;
|
||||
logger.info(LogOrigin.Playback, `Play Mode ${newState.toUpperCase()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -549,7 +580,7 @@ class RuntimeService {
|
||||
// the db would have to change for the event not to exist
|
||||
// we do not know the reason for the crash, so we check anyway
|
||||
const event = getEventWithId(selectedEventId);
|
||||
if (!event || !isOntimeEvent(event)) {
|
||||
if (!isOntimeEvent(event) || !isPlayableEvent(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -570,7 +601,7 @@ class RuntimeService {
|
||||
}
|
||||
|
||||
// calculate at 30fps, refresh at 1fps
|
||||
const eventTimer = new TimerService({
|
||||
const eventTimer = new EventTimer({
|
||||
refresh: timerConfig.updateRate,
|
||||
updateInterval: timerConfig.notificationRate,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MaybeNumber, MaybeString, OntimeEvent, Playback, TimerPhase, TimerType } from 'ontime-types';
|
||||
import { MaybeNumber, Playback, TimerPhase, TimerType } from 'ontime-types';
|
||||
import { dayInMs } from 'ontime-utils';
|
||||
import { RuntimeState } from '../stores/runtimeState.js';
|
||||
|
||||
@@ -93,12 +93,11 @@ export function getCurrent(state: RuntimeState): number {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function skippedOutOfEvent(state: RuntimeState, previousTime: number, skipLimit: number): boolean {
|
||||
// eslint-disable-next-line no-unused-labels -- dev code path
|
||||
DEV: {
|
||||
if (state.timer.expectedFinish === null || state.timer.startedAt === null) {
|
||||
throw new Error('timerUtils.skippedOutOfEvent: invalid state received');
|
||||
}
|
||||
// we cant have skipped if we havent started
|
||||
if (state.timer.expectedFinish === null || state.timer.startedAt === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { startedAt, expectedFinish } = state.timer;
|
||||
const { clock } = state;
|
||||
|
||||
@@ -112,155 +111,6 @@ export function skippedOutOfEvent(state: RuntimeState, previousTime: number, ski
|
||||
return hasSkipped && (adjustedClock > adjustedExpectedFinish || adjustedClock < startedAt);
|
||||
}
|
||||
|
||||
type RollTimers = {
|
||||
nowIndex: MaybeNumber;
|
||||
nowId: MaybeString;
|
||||
publicIndex: MaybeNumber;
|
||||
nextIndex: MaybeNumber;
|
||||
publicNextIndex: MaybeNumber;
|
||||
timeToNext: MaybeNumber;
|
||||
nextEvent: OntimeEvent | null;
|
||||
nextPublicEvent: OntimeEvent | null;
|
||||
currentEvent: OntimeEvent | null;
|
||||
currentPublicEvent: OntimeEvent | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds loading information given a current rundown and time
|
||||
* @param {OntimeEvent[]} playableEvents - List of playable events
|
||||
* @param {number} timeNow - time now in ms
|
||||
*/
|
||||
export const getRollTimers = (
|
||||
playableEvents: OntimeEvent[],
|
||||
timeNow: number,
|
||||
currentIndex?: number | null,
|
||||
): RollTimers => {
|
||||
let nowIndex: MaybeNumber = null; // index of event now
|
||||
let nowId: MaybeString = null; // id of event now
|
||||
let publicIndex: MaybeNumber = null; // index of public event now
|
||||
let nextIndex: MaybeNumber = null; // index of next event
|
||||
let publicNextIndex: MaybeNumber = null; // index of next public event
|
||||
let timeToNext: MaybeNumber = null; // counter: time for next event
|
||||
let publicTimeToNext: MaybeNumber = null; // counter: time for next public event
|
||||
|
||||
const hasLoaded = currentIndex !== null;
|
||||
const canFilter = hasLoaded && currentIndex === playableEvents.length - 1;
|
||||
const filteredRundown = canFilter ? playableEvents.slice(currentIndex) : playableEvents;
|
||||
|
||||
const lastEvent = filteredRundown.at(-1);
|
||||
const lastNormalEnd = normaliseEndTime(lastEvent.timeStart, lastEvent.timeEnd);
|
||||
|
||||
let nextEvent: OntimeEvent | null = null;
|
||||
let nextPublicEvent: OntimeEvent | null = null;
|
||||
let currentEvent: OntimeEvent | null = null;
|
||||
let currentPublicEvent: OntimeEvent | null = null;
|
||||
|
||||
if (timeNow > lastNormalEnd) {
|
||||
// we are past last end
|
||||
// preload first and find next
|
||||
|
||||
const firstEvent = filteredRundown.at(0);
|
||||
nextIndex = 0;
|
||||
nextEvent = firstEvent;
|
||||
timeToNext = firstEvent.timeStart + dayInMs - timeNow;
|
||||
|
||||
if (firstEvent.isPublic) {
|
||||
nextPublicEvent = firstEvent;
|
||||
publicNextIndex = 0;
|
||||
} else {
|
||||
// look for next public
|
||||
// dev note: we feel that this is more efficient than filtering
|
||||
// since the next event will likely be close to the one playing
|
||||
for (const event of filteredRundown) {
|
||||
if (event.isPublic) {
|
||||
nextPublicEvent = event;
|
||||
// we need the index before this was sorted
|
||||
publicNextIndex = filteredRundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// flags: select first event if several overlapping
|
||||
let nowFound = false;
|
||||
// keep track of the end times when looking for public
|
||||
let publicTime = -1;
|
||||
|
||||
for (const event of filteredRundown) {
|
||||
// When does the event end (handle midnight)
|
||||
const normalEnd = normaliseEndTime(event.timeStart, event.timeEnd);
|
||||
|
||||
const hasNotEnded = normalEnd > timeNow;
|
||||
const isFromDayBefore = normalEnd > dayInMs && timeNow < event.timeEnd;
|
||||
const hasStarted = isFromDayBefore || timeNow >= event.timeStart;
|
||||
|
||||
if (normalEnd <= timeNow) {
|
||||
// event ran already
|
||||
|
||||
if (event.isPublic && normalEnd > publicTime) {
|
||||
// public event might not be the one running
|
||||
publicTime = normalEnd;
|
||||
currentPublicEvent = event;
|
||||
publicIndex = filteredRundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
|
||||
}
|
||||
} else if (hasNotEnded && hasStarted && !nowFound) {
|
||||
// event is running
|
||||
currentEvent = event;
|
||||
nowIndex = filteredRundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
|
||||
nowId = event.id;
|
||||
nowFound = true;
|
||||
|
||||
// it could also be public
|
||||
if (event.isPublic) {
|
||||
publicTime = normalEnd;
|
||||
currentPublicEvent = event;
|
||||
publicIndex = filteredRundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
|
||||
}
|
||||
} else if (normalEnd > timeNow) {
|
||||
// event will run
|
||||
|
||||
// we already know whats next and next-public
|
||||
if (nextIndex !== null && publicNextIndex !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// look for next events
|
||||
// check how far the start is from now
|
||||
const timeToEventStart = event.timeStart - timeNow;
|
||||
|
||||
// we don't have a next or this one starts sooner than current next
|
||||
if (nextIndex === null || timeToEventStart < timeToNext) {
|
||||
timeToNext = timeToEventStart;
|
||||
nextEvent = event;
|
||||
nextIndex = filteredRundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
|
||||
}
|
||||
|
||||
if (event.isPublic) {
|
||||
// if we don't have a public next or this one start sooner than assigned next
|
||||
if (publicNextIndex === null || timeToEventStart < publicTimeToNext) {
|
||||
publicTimeToNext = timeToEventStart;
|
||||
nextPublicEvent = event;
|
||||
publicNextIndex = filteredRundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nowIndex,
|
||||
nowId,
|
||||
publicIndex,
|
||||
nextIndex,
|
||||
publicNextIndex,
|
||||
timeToNext,
|
||||
nextEvent,
|
||||
nextPublicEvent,
|
||||
currentEvent,
|
||||
currentPublicEvent,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates difference between the runtime and the schedule of an event
|
||||
* Positive offset is time ahead
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OntimeEvent, Playback } from 'ontime-types';
|
||||
import { PlayableEvent, Playback } from 'ontime-types';
|
||||
import { deepmerge } from 'ontime-utils';
|
||||
|
||||
import { RuntimeState, addTime, clear, getState, load, pause, start, stop } from '../runtimeState.js';
|
||||
@@ -11,7 +11,8 @@ const mockEvent = {
|
||||
timeStart: 0,
|
||||
timeEnd: 1000,
|
||||
duration: 1000,
|
||||
} as OntimeEvent;
|
||||
skip: false,
|
||||
} as PlayableEvent;
|
||||
|
||||
const mockState = {
|
||||
clock: 666,
|
||||
@@ -144,6 +145,7 @@ describe('mutation on runtimeState', () => {
|
||||
|
||||
// 5. Stop event
|
||||
success = stop();
|
||||
newState = getState();
|
||||
expect(success).toBe(true);
|
||||
expect(newState.eventNow).toBe(null);
|
||||
expect(newState.timer).toMatchObject({
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import {
|
||||
CurrentBlockState,
|
||||
isPlayableEvent,
|
||||
MaybeNumber,
|
||||
MaybeString,
|
||||
OntimeEvent,
|
||||
OntimeRundown,
|
||||
PlayableEvent,
|
||||
Playback,
|
||||
Runtime,
|
||||
TimerPhase,
|
||||
TimerState,
|
||||
} from 'ontime-types';
|
||||
import { calculateDuration, dayInMs, filterPlayable, getRelevantBlock } from 'ontime-utils';
|
||||
import { calculateDuration, checkIsNow, dayInMs, filterTimedEvents, getRelevantBlock } from 'ontime-utils';
|
||||
|
||||
import { clock } from '../services/Clock.js';
|
||||
import { RestorePoint } from '../services/RestoreService.js';
|
||||
@@ -16,12 +19,12 @@ import {
|
||||
getCurrent,
|
||||
getExpectedEnd,
|
||||
getExpectedFinish,
|
||||
getRollTimers,
|
||||
getRuntimeOffset,
|
||||
getTimerPhase,
|
||||
isPlaybackActive,
|
||||
} from '../services/timerUtils.js';
|
||||
import { timerConfig } from '../config/config.js';
|
||||
import { loadRoll, normaliseRollStart } from '../services/rollUtils.js';
|
||||
|
||||
const initialRuntime: Runtime = {
|
||||
selectedEventIndex: null, // changes if rundown changes or we load a new event
|
||||
@@ -38,8 +41,7 @@ const initialTimer: TimerState = {
|
||||
current: null, // changes on every update
|
||||
duration: null, // only changes if event changes
|
||||
elapsed: null, // changes on every update
|
||||
// TODO: expected finish could account for midnight, we cleanup in the clients
|
||||
expectedFinish: null, // change can only be initiated by user
|
||||
expectedFinish: null, // change can only be initiated by user, can roll over midnight
|
||||
finishedAt: null, // can change on update or user action
|
||||
phase: TimerPhase.None, // can change on update or user action
|
||||
playback: Playback.Stop, // change initiated by user
|
||||
@@ -49,11 +51,11 @@ const initialTimer: TimerState = {
|
||||
|
||||
export type RuntimeState = {
|
||||
clock: number; // realtime clock
|
||||
eventNow: OntimeEvent | null;
|
||||
eventNow: PlayableEvent | null;
|
||||
currentBlock: CurrentBlockState;
|
||||
publicEventNow: OntimeEvent | null;
|
||||
eventNext: OntimeEvent | null;
|
||||
publicEventNext: OntimeEvent | null;
|
||||
publicEventNow: PlayableEvent | null;
|
||||
eventNext: PlayableEvent | null;
|
||||
publicEventNext: PlayableEvent | null;
|
||||
runtime: Runtime;
|
||||
timer: TimerState;
|
||||
// private properties of the timer calculations
|
||||
@@ -61,6 +63,7 @@ export type RuntimeState = {
|
||||
forceFinish: MaybeNumber; // wether we should declare an event as finished, will contain the finish time
|
||||
totalDelay: number; // this value comes from rundown service
|
||||
pausedAt: MaybeNumber;
|
||||
secondaryTarget: MaybeNumber;
|
||||
};
|
||||
_prevCurrentBlock: CurrentBlockState;
|
||||
};
|
||||
@@ -81,6 +84,7 @@ const runtimeState: RuntimeState = {
|
||||
forceFinish: null,
|
||||
totalDelay: 0,
|
||||
pausedAt: null,
|
||||
secondaryTarget: null,
|
||||
},
|
||||
_prevCurrentBlock: {
|
||||
block: null,
|
||||
@@ -89,7 +93,17 @@ const runtimeState: RuntimeState = {
|
||||
};
|
||||
|
||||
export function getState(): Readonly<RuntimeState> {
|
||||
return runtimeState;
|
||||
// create a shallow copy of the state
|
||||
return {
|
||||
...runtimeState,
|
||||
eventNow: runtimeState.eventNow ? { ...runtimeState.eventNow } : null,
|
||||
eventNext: runtimeState.eventNext ? { ...runtimeState.eventNext } : null,
|
||||
publicEventNow: runtimeState.publicEventNow ? { ...runtimeState.publicEventNow } : null,
|
||||
publicEventNext: runtimeState.publicEventNext ? { ...runtimeState.publicEventNext } : null,
|
||||
runtime: { ...runtimeState.runtime },
|
||||
timer: { ...runtimeState.timer },
|
||||
_timer: { ...runtimeState._timer },
|
||||
};
|
||||
}
|
||||
|
||||
export function clear() {
|
||||
@@ -129,7 +143,7 @@ function patchTimer(newState: Partial<TimerState>) {
|
||||
}
|
||||
|
||||
type RundownData = {
|
||||
numEvents: number;
|
||||
numEvents: number; // length of rundown filtered for timed events
|
||||
firstStart: MaybeNumber;
|
||||
lastEnd: MaybeNumber;
|
||||
totalDelay: number;
|
||||
@@ -152,30 +166,33 @@ export function updateRundownData(rundownData: RundownData) {
|
||||
|
||||
/**
|
||||
* Loads a given event into state
|
||||
* @param event
|
||||
* @param {OntimeEvent[]} playableEvents list of events availebe for playback
|
||||
* @param {OntimeRundown} rundown the full rundown
|
||||
* @param initialData potential data from restore point
|
||||
*/
|
||||
export function load(
|
||||
event: OntimeEvent,
|
||||
event: PlayableEvent,
|
||||
rundown: OntimeRundown,
|
||||
initialData?: Partial<TimerState & RestorePoint>,
|
||||
): boolean {
|
||||
clear();
|
||||
|
||||
const eventIndex = rundown.findIndex((eventInMemory) => eventInMemory.id === event.id);
|
||||
// filter rundown
|
||||
const timedEvents = filterTimedEvents(rundown);
|
||||
const eventIndex = timedEvents.findIndex((eventInMemory) => eventInMemory.id === event.id);
|
||||
|
||||
runtimeState.runtime.selectedEventIndex = eventIndex;
|
||||
if (timedEvents.length === 0 || eventIndex === -1 || !isPlayableEvent(event)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
loadNow(event, rundown);
|
||||
loadNext(rundown);
|
||||
// load events in memory along with their data
|
||||
loadNow(timedEvents, eventIndex);
|
||||
loadNext(timedEvents, eventIndex);
|
||||
|
||||
runtimeState.clock = clock.timeNow();
|
||||
// update state
|
||||
runtimeState.timer.playback = Playback.Armed;
|
||||
runtimeState.timer.duration = calculateDuration(event.timeStart, event.timeEnd);
|
||||
runtimeState.timer.current = getCurrent(runtimeState);
|
||||
runtimeState.runtime.numEvents = timedEvents.length;
|
||||
|
||||
// patch with potential provided data
|
||||
if (initialData) {
|
||||
patchTimer(initialData);
|
||||
|
||||
@@ -190,11 +207,25 @@ export function load(
|
||||
return event.id === runtimeState.eventNow?.id;
|
||||
}
|
||||
|
||||
export function loadNow(event: OntimeEvent, rundown: OntimeRundown) {
|
||||
runtimeState.eventNow = event;
|
||||
runtimeState.currentBlock.block = getRelevantBlock(rundown, event.id);
|
||||
/**
|
||||
* Loads current event and its public counterpart
|
||||
*/
|
||||
export function loadNow(timedEvents: OntimeEvent[], eventIndex: MaybeNumber = runtimeState.runtime.selectedEventIndex) {
|
||||
if (eventIndex === null) {
|
||||
// reset the state to indicate there is no selection
|
||||
runtimeState.runtime.selectedEventIndex = null;
|
||||
runtimeState.eventNow = null;
|
||||
runtimeState.currentBlock.block = null;
|
||||
runtimeState.currentBlock.startedAt = null;
|
||||
return;
|
||||
}
|
||||
|
||||
//if we are still in the same block keep the startedAt time
|
||||
const event = timedEvents[eventIndex] as PlayableEvent;
|
||||
runtimeState.runtime.selectedEventIndex = eventIndex;
|
||||
runtimeState.eventNow = event;
|
||||
runtimeState.currentBlock.block = getRelevantBlock(timedEvents, event.id);
|
||||
|
||||
// if we are still in the same block keep the startedAt time
|
||||
if (runtimeState._prevCurrentBlock.block?.id === runtimeState.currentBlock.block?.id) {
|
||||
runtimeState.currentBlock.startedAt = runtimeState._prevCurrentBlock.startedAt;
|
||||
}
|
||||
@@ -207,73 +238,81 @@ export function loadNow(event: OntimeEvent, rundown: OntimeRundown) {
|
||||
runtimeState.publicEventNow = null;
|
||||
|
||||
// if there is nothing before, return
|
||||
if (!runtimeState.runtime.selectedEventIndex) {
|
||||
if (!eventIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const playableEvents = filterPlayable(rundown);
|
||||
|
||||
// iterate backwards to find it
|
||||
for (let i = runtimeState.runtime.selectedEventIndex; i >= 0; i--) {
|
||||
if (playableEvents[i].isPublic) {
|
||||
runtimeState.publicEventNow = playableEvents[i];
|
||||
for (let i = eventIndex; i >= 0; i--) {
|
||||
const event = timedEvents[i];
|
||||
// we dont deal with events that are not playable
|
||||
if (!isPlayableEvent(event)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.isPublic) {
|
||||
runtimeState.publicEventNow = event;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function loadNext(rundown: OntimeRundown) {
|
||||
// assume there are no next events
|
||||
runtimeState.eventNext = null;
|
||||
runtimeState.publicEventNext = null;
|
||||
|
||||
if (runtimeState.runtime.selectedEventIndex === null) {
|
||||
/**
|
||||
* Loads the next event and its public counterpart
|
||||
*/
|
||||
export function loadNext(
|
||||
timedEvents: OntimeEvent[],
|
||||
eventIndex: MaybeNumber = runtimeState.runtime.selectedEventIndex,
|
||||
) {
|
||||
if (eventIndex === null) {
|
||||
// reset the state to indicate there is no future event
|
||||
runtimeState.eventNext = null;
|
||||
runtimeState.publicEventNext = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const playableEvents = filterPlayable(rundown);
|
||||
const numEvents = playableEvents.length;
|
||||
for (let i = eventIndex + 1; i < timedEvents.length; i++) {
|
||||
const event = timedEvents[i];
|
||||
// we dont deal with events that are not playable
|
||||
if (!isPlayableEvent(event)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (runtimeState.runtime.selectedEventIndex < numEvents - 1) {
|
||||
let nextPublic = false;
|
||||
let nextProduction = false;
|
||||
// the private event is the one immediately after the current event
|
||||
if (runtimeState.eventNext === null) {
|
||||
runtimeState.eventNext = event;
|
||||
}
|
||||
|
||||
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 (event.isPublic) {
|
||||
runtimeState.publicEventNext = event;
|
||||
}
|
||||
|
||||
// if event is public
|
||||
if (playableEvents[i].isPublic) {
|
||||
runtimeState.publicEventNext = playableEvents[i];
|
||||
nextPublic = true;
|
||||
}
|
||||
|
||||
// Stop if both are set
|
||||
if (nextPublic && nextProduction) break;
|
||||
// Stop if both are set
|
||||
if (runtimeState.eventNext !== null && runtimeState.publicEventNext !== null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume from restore point
|
||||
* @param restorePoint
|
||||
* @param event
|
||||
* @param playableEvents list of events availebe for playback
|
||||
* @param rundown the full rundown
|
||||
*/
|
||||
export function resume(restorePoint: RestorePoint, event: OntimeEvent, rundown: OntimeRundown) {
|
||||
export function resume(restorePoint: RestorePoint, event: PlayableEvent, rundown: OntimeRundown) {
|
||||
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
|
||||
* @param {PlayableEvent} event only passed if we are changing the data if a playing timer
|
||||
*/
|
||||
export function reload(event?: OntimeEvent) {
|
||||
export function reload(event?: PlayableEvent): string | undefined {
|
||||
// if there is no event loaded, nothing to do
|
||||
if (runtimeState.eventNow === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we only pass an event for hot reloading, ie: the event has changed
|
||||
if (event) {
|
||||
runtimeState.eventNow = event;
|
||||
@@ -282,19 +321,39 @@ export function reload(event?: OntimeEvent) {
|
||||
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd);
|
||||
runtimeState.timer.current = getCurrent(runtimeState);
|
||||
runtimeState.timer.expectedFinish = getExpectedFinish(runtimeState);
|
||||
|
||||
// handle edge cases with roll
|
||||
if (runtimeState.timer.playback === Playback.Roll) {
|
||||
// if waiting to roll, we update the targets and potentially start the timer
|
||||
if (runtimeState._timer.secondaryTarget !== null) {
|
||||
if (
|
||||
runtimeState.eventNow.timeStart < runtimeState.clock &&
|
||||
runtimeState.clock < runtimeState.eventNow.timeEnd
|
||||
) {
|
||||
// if the event is now, we queue a start
|
||||
runtimeState._timer.secondaryTarget = runtimeState.eventNow.timeStart;
|
||||
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - runtimeState.clock;
|
||||
} else {
|
||||
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, runtimeState.clock);
|
||||
}
|
||||
}
|
||||
}
|
||||
return runtimeState.eventNow.id;
|
||||
}
|
||||
|
||||
// reset changes to timer progress
|
||||
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.pausedAt = null;
|
||||
|
||||
// this could be looked after by the timer
|
||||
runtimeState.timer.elapsed = null;
|
||||
runtimeState.timer.expectedFinish = getExpectedFinish(runtimeState);
|
||||
|
||||
runtimeState.currentBlock.startedAt = null;
|
||||
@@ -302,16 +361,13 @@ export function reload(event?: OntimeEvent) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in situations when we want to reload all events
|
||||
* without interrupting timer
|
||||
* @param eventNow
|
||||
* @param playableEvents
|
||||
* @param rundown
|
||||
* Used in situations when we want to hot-reload all events without interrupting timer
|
||||
*/
|
||||
export function reloadAll(eventNow: OntimeEvent, rundown: OntimeRundown) {
|
||||
loadNow(eventNow, rundown);
|
||||
loadNext(rundown);
|
||||
reload(eventNow);
|
||||
export function reloadAll(rundown: OntimeRundown) {
|
||||
const timedEvents = filterTimedEvents(rundown);
|
||||
loadNow(timedEvents);
|
||||
loadNext(timedEvents);
|
||||
reload(runtimeState.eventNow ?? undefined);
|
||||
}
|
||||
|
||||
export function start(state: RuntimeState = runtimeState): boolean {
|
||||
@@ -336,7 +392,6 @@ export function start(state: RuntimeState = runtimeState): boolean {
|
||||
}
|
||||
|
||||
if (state.currentBlock.startedAt === null) {
|
||||
console.log('currentBlock.startedAt is null, setting new start');
|
||||
state.currentBlock.startedAt = state.clock;
|
||||
}
|
||||
|
||||
@@ -381,11 +436,22 @@ export function stop(state: RuntimeState = runtimeState): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes functionality to add user time to the timer externally
|
||||
*/
|
||||
export function addTime(amount: number) {
|
||||
if (runtimeState.timer.current === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// as long as there is a timer, we need an expected finish
|
||||
// eslint-disable-next-line no-unused-labels -- dev code path
|
||||
DEV: {
|
||||
if (runtimeState.timer.expectedFinish === null) {
|
||||
throw new Error('runtimeState.addTime: invalid state received');
|
||||
}
|
||||
}
|
||||
|
||||
// handle edge cases
|
||||
// !!! we need to handle side effects before updating the state
|
||||
const willGoNegative = amount < 0 && Math.abs(amount) > runtimeState.timer.current;
|
||||
@@ -415,7 +481,7 @@ export function addTime(amount: number) {
|
||||
|
||||
export type UpdateResult = {
|
||||
hasTimerFinished: boolean;
|
||||
shouldCallRoll: boolean;
|
||||
hasSecondaryTimerFinished: boolean;
|
||||
};
|
||||
|
||||
export function update(): UpdateResult {
|
||||
@@ -429,18 +495,21 @@ export function update(): UpdateResult {
|
||||
|
||||
// 2. are we waiting to roll?
|
||||
if (runtimeState.timer.playback === Playback.Roll && runtimeState.timer.secondaryTimer !== null) {
|
||||
return updateIfWaitingToRoll(runtimeState.timer.secondaryTimer);
|
||||
return updateIfWaitingToRoll();
|
||||
}
|
||||
|
||||
// 3. at this point we know that we are playing an event
|
||||
// reset data
|
||||
runtimeState.timer.secondaryTimer = null;
|
||||
|
||||
// update timer state
|
||||
if (!runtimeState.timer.duration) {
|
||||
throw new Error('Timer duration is not set');
|
||||
// eslint-disable-next-line no-unused-labels -- dev code path
|
||||
DEV: {
|
||||
if (!runtimeState.timer.duration) {
|
||||
throw new Error('runtimeState.update: invalid state received');
|
||||
}
|
||||
}
|
||||
|
||||
// update timer state
|
||||
runtimeState.timer.current = getCurrent(runtimeState);
|
||||
runtimeState.timer.expectedFinish = getExpectedFinish(runtimeState);
|
||||
runtimeState.timer.phase = getTimerPhase(runtimeState);
|
||||
@@ -462,55 +531,128 @@ export function update(): UpdateResult {
|
||||
runtimeState.timer.expectedFinish = getExpectedFinish(runtimeState);
|
||||
}
|
||||
|
||||
return { hasTimerFinished: finishedNow, shouldCallRoll: finishedNow };
|
||||
return { hasTimerFinished: finishedNow, hasSecondaryTimerFinished: false };
|
||||
|
||||
function updateIfIdle() {
|
||||
// if nothing is running, nothing to do
|
||||
return { hasTimerFinished: false, shouldCallRoll: false };
|
||||
return { hasTimerFinished: false, hasSecondaryTimerFinished: false };
|
||||
}
|
||||
|
||||
function updateIfWaitingToRoll(targetTime: number) {
|
||||
runtimeState.timer.secondaryTimer = targetTime - runtimeState.clock;
|
||||
function updateIfWaitingToRoll() {
|
||||
// eslint-disable-next-line no-unused-labels -- dev code path
|
||||
DEV: {
|
||||
if (runtimeState.eventNow === null || runtimeState._timer.secondaryTarget === null) {
|
||||
throw new Error('runtimeState.updateIfWaitingToRoll: invalid state received');
|
||||
}
|
||||
}
|
||||
|
||||
runtimeState.timer.phase = TimerPhase.Pending;
|
||||
return { hasTimerFinished: false, shouldCallRoll: runtimeState.timer.secondaryTimer < 0 };
|
||||
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - runtimeState.clock;
|
||||
return { hasTimerFinished: false, hasSecondaryTimerFinished: runtimeState.timer.secondaryTimer < 0 };
|
||||
}
|
||||
}
|
||||
|
||||
export function roll(rundown: OntimeRundown) {
|
||||
const selectedEventIndex = runtimeState.runtime.selectedEventIndex;
|
||||
const playableEvents = filterPlayable(rundown);
|
||||
export function roll(rundown: OntimeRundown): { eventId: MaybeString; didStart: boolean } {
|
||||
// 1. if an event is running, we simply take over the playback
|
||||
if (runtimeState.timer.playback === Playback.Play && runtimeState.runtime.selectedEventIndex !== null) {
|
||||
runtimeState.timer.playback = Playback.Roll;
|
||||
return { eventId: runtimeState.eventNow?.id ?? null, didStart: false };
|
||||
}
|
||||
|
||||
clear();
|
||||
runtimeState.runtime.numEvents = playableEvents.length;
|
||||
|
||||
const { nextEvent, currentEvent } = getRollTimers(playableEvents, runtimeState.clock, selectedEventIndex);
|
||||
|
||||
if (currentEvent) {
|
||||
// there is something running, load
|
||||
runtimeState.timer.secondaryTimer = null;
|
||||
// 2. if there is an event armed, we use it
|
||||
if (runtimeState.timer.playback === Playback.Armed && runtimeState.eventNow !== null) {
|
||||
runtimeState.timer.playback = Playback.Roll;
|
||||
|
||||
// account for event that finishes the day after
|
||||
const endTime =
|
||||
currentEvent.timeEnd < currentEvent.timeStart ? currentEvent.timeEnd + dayInMs : currentEvent.timeEnd;
|
||||
const normalisedEndTime =
|
||||
runtimeState.eventNow.timeEnd < runtimeState.eventNow.timeStart
|
||||
? runtimeState.eventNow.timeEnd + dayInMs
|
||||
: runtimeState.eventNow.timeEnd;
|
||||
runtimeState.timer.expectedFinish = normalisedEndTime;
|
||||
|
||||
// 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) {
|
||||
if (nextEvent.isPublic) {
|
||||
runtimeState.publicEventNext = nextEvent;
|
||||
// state catch up
|
||||
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, normalisedEndTime);
|
||||
runtimeState.timer.current = runtimeState.timer.duration;
|
||||
runtimeState.timer.elapsed = 0;
|
||||
|
||||
// check if the event is ready to start or if needs to be waited
|
||||
const isNow = checkIsNow(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd, runtimeState.clock);
|
||||
|
||||
if (isNow) {
|
||||
runtimeState.timer.startedAt = runtimeState.clock;
|
||||
|
||||
// update runtime
|
||||
if (!runtimeState.runtime.actualStart) {
|
||||
runtimeState.runtime.actualStart = runtimeState.clock;
|
||||
}
|
||||
} else {
|
||||
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, runtimeState.clock);
|
||||
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - runtimeState.clock;
|
||||
runtimeState.timer.phase = TimerPhase.Pending;
|
||||
}
|
||||
runtimeState.eventNext = nextEvent;
|
||||
// account for day after
|
||||
const nextStart = nextEvent.timeStart < runtimeState.clock ? nextEvent.timeStart + dayInMs : nextEvent.timeStart;
|
||||
// nothing now, but something coming up
|
||||
runtimeState.timer.phase = TimerPhase.Pending;
|
||||
runtimeState.timer.secondaryTimer = nextStart - runtimeState.clock;
|
||||
|
||||
return { eventId: runtimeState.eventNow.id, didStart: isNow };
|
||||
}
|
||||
|
||||
// 3. if there is no event running, we need to find the next event
|
||||
const timedEvents = filterTimedEvents(rundown);
|
||||
if (timedEvents.length === 0) {
|
||||
throw new Error('No playable events found');
|
||||
}
|
||||
|
||||
clear();
|
||||
const { index, isPending } = loadRoll(timedEvents, runtimeState.clock);
|
||||
|
||||
// load events in memory along with their data
|
||||
loadNow(timedEvents, index);
|
||||
loadNext(timedEvents, index);
|
||||
|
||||
// update roll state
|
||||
runtimeState.timer.playback = Playback.Roll;
|
||||
runtimeState.runtime.numEvents = timedEvents.length;
|
||||
|
||||
// in roll mode spec, there should always be something to load
|
||||
// as long as playableEvents is not empty
|
||||
// eslint-disable-next-line no-unused-labels -- dev code path
|
||||
DEV: {
|
||||
if (runtimeState.eventNow === null) {
|
||||
throw new Error('runtimeState.roll: invalid state received');
|
||||
}
|
||||
}
|
||||
|
||||
if (isPending) {
|
||||
// there is nothing now, but something coming up
|
||||
runtimeState.timer.phase = TimerPhase.Pending;
|
||||
// we need to normalise start time in case it is the day after
|
||||
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, runtimeState.clock);
|
||||
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - runtimeState.clock;
|
||||
|
||||
// preload timer properties
|
||||
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd);
|
||||
runtimeState.timer.current = runtimeState.timer.duration;
|
||||
return { eventId: runtimeState.eventNow.id, didStart: false };
|
||||
}
|
||||
|
||||
// there is something to run, load event
|
||||
|
||||
// event will finish on time
|
||||
// account for event that finishes the day after
|
||||
const endTime =
|
||||
runtimeState.eventNow.timeEnd < runtimeState.eventNow.timeStart
|
||||
? runtimeState.eventNow.timeEnd + dayInMs
|
||||
: runtimeState.eventNow.timeEnd;
|
||||
runtimeState.timer.startedAt = runtimeState.clock;
|
||||
runtimeState.timer.expectedFinish = endTime;
|
||||
|
||||
// we add time to allow timer to catch up
|
||||
runtimeState.timer.addedTime = -(runtimeState.clock - runtimeState.eventNow.timeStart);
|
||||
|
||||
// state catch up
|
||||
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, endTime);
|
||||
runtimeState.timer.current = getCurrent(runtimeState);
|
||||
runtimeState.timer.elapsed = 0;
|
||||
|
||||
// update runtime
|
||||
runtimeState.runtime.actualStart = runtimeState.clock;
|
||||
return { eventId: runtimeState.eventNow.id, didStart: true };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user