* ux: delete flow

* style: small tweaks in interface

* refactor: gracefully quit on error

* fix: logic around updating events

* chore: cleanup dictionary

* refactor: improve DX on creating aux files

* fix: safe destructure function return

* refactor: safe handling of falsy timer values

* fix: improve ux on stopping roll mode

* chore: upgrade deps

* feat: roll mode

* refactor: remove unused
This commit is contained in:
Carlos Valente
2023-01-07 22:44:21 +01:00
committed by GitHub
parent 832c060940
commit 3918758d32
31 changed files with 305 additions and 1290 deletions
@@ -51,7 +51,7 @@ export default function Transport(props: TransportProps) {
<Tooltip label='Unload Event' openDelay={tooltipDelayMid}>
<TapButton
onClick={() => setPlayback.stop()}
disabled={!selectedId}
disabled={!selectedId && !isRolling}
theme='stop'
>
<IoStop />
@@ -246,7 +246,7 @@ export default function EventBlock(props: EventBlockProps) {
showDelay
showBlock
showClone
enableDelete
enableDelete={!selected}
actionHandler={actionHandler}
/>
</div>
@@ -16,7 +16,7 @@ export default function EventBlockProgressBar(props: EventBlockProgressBarProps)
const elapsed = clamp(100 - (now * 100) / complete, 0, 100);
const progress = `${elapsed}%`;
if (timer?.current != null && timer?.current < 0) {
if ((timer?.current ?? 0) < 0) {
return (
<div
className={`${style.progressBar} ${style.overtime}`}
+15 -15
View File
@@ -92,18 +92,18 @@ $red-1200: #520000;
$red-1300: #440000;
$red-1350: #360000;
$violet-50: #F9F7FE;
$violet-100: #F3EFFC;
$violet-200: #E7DFF6;
$violet-300: #CEBFEC;
$violet-400: #B8A0E3;
$violet-500: #AB8DB8;
$violet-600: #9771D3;
$violet-700: #8B60CA;
$violet-800: #8352C6;
$violet-900: #7945c1;
$violet-1000: #7248AD;
$violet-1100: #573486;
$violet-1200: #3C235F;
$violet-1300: #301A4D;
$violet-1350: #231339;
$violet-50: #F6F6F6;
$violet-100: #EBE5FF;
$violet-200: #DFD5FF;
$violet-300: #C7B8FD;
$violet-400: #A790F5;
$violet-500: #9379EC;
$violet-600: #8064E1;
$violet-700: #674ACB;
$violet-800: #4E31B1;
$violet-900: #3F249D;
$violet-1000: #311887;
$violet-1100: #1F0B62;
$violet-1200: #140545;
$violet-1300: #0F0336;
$violet-1350: #0A0126;
+1 -2
View File
@@ -22,11 +22,10 @@ export const ontimeButtonFilled = {
export const ontimeButtonOutlined = {
...commonStyles,
backgroundColor: '#2d2d2d', // $gray-1100
color: '#9d9d9d', // $blue-400
color: '#e2e2e2', // $blue-400
border: '1px solid rgba(255, 255, 255, 0.10)', // white-10
_hover: {
backgroundColor: '#404040', // $gray-1000
color: '#e2e2e2', // $gray-200
},
_active: {
backgroundColor: '#2d2d2d', // $gray-1100
+1 -1
View File
@@ -26,7 +26,7 @@
"supertest": "^6.2.2"
},
"scripts": {
"setup": "yarn install && yarn setdb && yarn addversion",
"postinstall": "yarn setdb && yarn addversion",
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.js",
"nodestart": "NODE_ENV=development node src/app.js",
"setdb": "cp demo-db/db.json src/preloaded-db/db.json",
+18 -4
View File
@@ -156,20 +156,34 @@ export const startIntegrations = async (overrideConfig = null) => {
/**
* @description clean shutdown app services
* @param {number} exitCode
* @return {Promise<void>}
*/
export const shutdown = async () => {
export const shutdown = async (exitCode) => {
// shutdown express server
server.close();
shutdownOSCServer();
eventTimer.shutdown();
socket.shutdown();
process.exit(exitCode || 0);
};
process.on('unhandledRejection', async (error, promise) => {
console.error(error, 'Error: unhandled rejection', promise);
socket.error('SERVER', 'Error: unhandled rejection');
await shutdown(1);
});
process.on('uncaughtException', async (error, promise) => {
console.error(error, 'Error: uncaught exception', promise);
socket.error('SERVER', 'Error: uncaught exception');
await shutdown(1);
});
// register shutdown signals
process.once('SIGHUP', shutdown);
process.once('SIGINT', shutdown);
process.once('SIGTERM', shutdown);
process.once('SIGHUP', async () => shutdown(0));
process.once('SIGINT', async () => shutdown(0));
process.once('SIGTERM', async () => shutdown(0));
export { server, app };
@@ -30,9 +30,10 @@ export class DataProvider {
static async updateEventById(eventId, newData) {
const eventIndex = data.rundown.findIndex((e) => e.id === eventId);
const e = data.rundown[eventIndex];
data.rundown[eventIndex] = { ...e, ...newData };
data.rundown[eventIndex].revision++;
const persistedEvent = data.rundown[eventIndex];
const newEvent = { ...persistedEvent, ...newData };
newEvent.revision++;
data.rundown[eventIndex] = newEvent;
await this.persist();
return data.rundown[eventIndex];
}
+34 -7
View File
@@ -1,6 +1,5 @@
import { DataProvider } from '../data-provider/DataProvider.js';
import { getSelectionByRoll } from '../timer/classUtils.js';
import { Timer } from '../timer/Timer.js';
import { getRollTimers } from '../../services/rollUtils.js';
let instance;
@@ -136,12 +135,36 @@ export class EventLoader {
/**
* finds next event within Roll context
* @returns {{nowIndex: null, timers: null, nowId: null, publicNextIndex: null, nextIndex: null, timeToNext: null, publicIndex: null}|{nowIndex: null, timers: null, nowId: null, publicNextIndex: null, nextIndex: null, timeToNext: null, publicIndex: null}}
* @param {number} timeNow - current time in ms
*/
findRoll() {
findRoll(timeNow) {
const timedEvents = EventLoader.getPlayableEvents();
const millisNow = Timer.getCurrentTime();
return getSelectionByRoll(timedEvents, millisNow);
if (!timedEvents.length) {
return null;
}
const {
nowIndex,
timers,
timeToNext,
nextEvent,
nextPublicEvent,
currentEvent,
currentPublicEvent,
} = getRollTimers(timedEvents, timeNow);
this.loadedEvent = currentEvent;
this.selectedEventIndex = nowIndex;
this.selectedEventId = currentEvent?.id || null;
this.numEvents = timedEvents.length;
// titles
this._loadThisTitles(currentEvent, 'now-private');
this._loadThisTitles(currentPublicEvent, 'now-public');
this._loadThisTitles(nextEvent, 'next-private');
this._loadThisTitles(nextPublicEvent, 'next-public');
return { currentEvent, nextEvent, timeToNext, timers };
}
/**
@@ -300,6 +323,10 @@ export class EventLoader {
* @private
*/
_loadThisTitles(event, type) {
if (!event) {
return;
}
switch (type) {
// now, load to both public and private
case 'now':
@@ -364,7 +391,7 @@ export class EventLoader {
break;
default:
break;
throw new Error(`Unhandled title type: ${type}`);
}
}
}
@@ -3,12 +3,11 @@ import { Server } from 'socket.io';
import getRandomName from '../../utils/getRandomName.js';
import { generateId } from '../../utils/generate_id.js';
import { stringFromMillis } from '../../utils/time.js';
import { Timer } from '../timer/Timer.js';
import { messageManager } from '../message-manager/MessageManager.js';
import { PlaybackService } from '../../services/playbackService.js';
import { PlaybackService } from '../../services/PlaybackService.js';
import { ADDRESS_MESSAGE_CONTROL } from './socketConfig.js';
import { eventTimer } from '../../services/TimerService.js';
import { eventTimer, TimerService } from '../../services/TimerService.js';
import { EventLoader, eventLoader } from '../event-loader/EventLoader.js';
class SocketController {
@@ -342,7 +341,7 @@ class SocketController {
level,
origin,
text,
time: stringFromMillis(Timer.getCurrentTime() || 0),
time: stringFromMillis(TimerService.getCurrentTime() || 0),
};
this.messageStack.unshift(logMessage);
-773
View File
@@ -1,773 +0,0 @@
import { Timer } from './Timer.js';
import { DAY_TO_MS, replacePlaceholder, updateRoll } from './classUtils.js';
import { OSCIntegration } from './integrations/Osc.js';
import { HTTPIntegration } from './integrations/Http.js';
import { cleanURL } from '../../utils/url.js';
import { EventLoader, eventLoader } from '../event-loader/EventLoader.js';
/*
* Class EventTimer adds functions specific to APP
* @extends Timer
*/
export class EventTimer extends Timer {
/**
* Instantiates an event timer object
* @param {object} socket
* @param {object} timerConfig
* @param {object} [oscConfig]
* @param {object} [httpConfig]
*/
constructor(socket, timerConfig, oscConfig, httpConfig) {
// call super constructor
super();
this.cycleState = {
/* idle: before it is initialised */
idle: 'idle',
/* onLoad: when a new event is loaded */
onLoad: 'onLoad',
/* armed: when a new event is loaded but hasn't started */
armed: 'armed',
onStart: 'onStart',
/* update: every update call cycle (1 x second) */
onUpdate: 'onUpdate',
onPause: 'onPause',
onStop: 'onStop',
onFinish: 'onFinish',
};
this.ontimeCycle = 'idle';
this.prevCycle = null;
// Socket Object
this.socket = socket;
// OSC Object
this.osc = null;
// HTTP Client Object
this.http = null;
// call general title reset
this._resetSelection();
// set recurrent emits
this._interval = setInterval(() => this.runCycle(), timerConfig?.refresh || 1000);
if (oscConfig != null) {
this._initOscClient(oscConfig);
}
if (httpConfig != null) {
this._initHTTPClient(httpConfig);
}
}
/**
* @description Shutdown process
*/
shutdown() {
clearInterval(this._interval);
if (this.osc != null) {
this.socket.info('TX', '... Closing OSC Client');
this.osc.shutdown();
}
if (this.http != null) {
this.socket.info('TX', '... Closing HTTP Client');
this.http.shutdown();
}
}
/**
* Initialises OSC Integration object
* @param {object} oscConfig
* @private
*/
_initOscClient(oscConfig) {
this.osc = new OSCIntegration();
const r = this.osc.init(oscConfig);
r.success ? this.socket.info('TX', r.message) : this.socket.error('TX', r.message);
}
/**
* Initialises HTTP Integration object
* @param {object} httpConfig
* @private
*/
_initHTTPClient(httpConfig) {
this.socket.info('TX', `Initialise HTTP Client on port`);
this.http = new HTTPIntegration();
this.http.init(httpConfig);
this.httpMessages = httpConfig.messages;
}
/**
* Sends time object over websockets
*/
broadcastTimer() {
// through websockets
this.socket.send('timer', this.getTimeObject());
}
/**
* @description Broadcast timer data
* @private
*/
_broadcastFeatureTimer() {
const featureData = {
clock: this.clock,
current: this.current,
secondaryTimer: this.secondaryTimer,
duration: this.duration,
startedAt: this._startedAt,
expectedFinish: this._getExpectedFinish(),
};
this.socket.send('ontime-timer', featureData);
}
/**
* @description Broadcast data for Event List feature
* @private
*/
_broadcastFeatureRundown() {
const featureData = {
selectedEventId: this.selectedEventId,
nextEventId: this.nextEventId,
playback: this.state,
};
this.socket.send('feat-rundown', featureData);
}
/**
* @description Broadcast data for Playback Control feature
* @private
*/
_broadcastFeaturePlaybackControl() {
const numEvents = EventLoader.getNumEvents();
const featureData = {
playback: this.state,
selectedEventId: this.selectedEventId,
numEvents: numEvents,
};
this.socket.send('feat-playbackcontrol', featureData);
}
/**
* @description Broadcast data for Info feature
* @private
*/
_broadcastFeatureInfo() {
const numEvents = EventLoader.getNumEvents();
const featureData = {
titles: this.titles,
playback: this.state,
selectedEventId: this.selectedEventId,
selectedEventIndex: this.selectedEventIndex,
numEvents: numEvents,
};
this.socket.send('feat-info', featureData);
}
_broadcastFeatureCuesheet() {
const numEvents = EventLoader.getNumEvents();
const featureData = {
playback: this.state,
selectedEventId: this.selectedEventId,
selectedEventIndex: this.selectedEventIndex,
numEvents: numEvents,
titleNow: this.titles.titleNow,
};
this.socket.send('feat-cuesheet', featureData);
}
/**
* Broadcasts complete object state
*/
broadcastState() {
// feature sync
this._broadcastFeatureRundown();
this._broadcastFeaturePlaybackControl();
this._broadcastFeatureInfo();
this._broadcastFeatureCuesheet();
this._broadcastFeatureTimer();
this.broadcastTimer();
this.socket.send('playstate', this.state);
this.socket.send('selected-id', this.selectedEventId);
this.socket.send('next-id', this.nextEventId);
this.socket.send('publicselected-id', this.selectedPublicEventId);
this.socket.send('publicnext-id', this.nextPublicEventId);
this.socket.send('titles', this.titles);
this.socket.send('publictitles', this.titlesPublic);
}
/**
* @description State machine checks what actions need to
* happen at every app cycle
*/
runCycle() {
const h = this.httpMessages?.messages;
let httpMessage = null;
switch (this.ontimeCycle) {
case 'idle':
break;
case 'armed':
// if we come from roll, see if we can start
if (this.state === 'roll') {
this.update();
}
break;
case 'onLoad':
// check integrations - http
if (h?.onLoad?.enabled) {
if (h?.onLoad?.url != null || h?.onLoad?.url !== '') {
httpMessage = h?.onLoad?.url;
}
}
// update lifecycle: armed
this.ontimeCycle = this.cycleState.armed;
break;
case 'onStart':
// send OSC if there is something running
// _finish at is only set when an event is loaded
if (this._finishAt > 0) {
this.sendOsc(this.osc.implemented.play);
this.sendOsc(this.osc.implemented.eventNumber, this.selectedEventIndex || 0);
}
// check integrations - http
if (h?.onLoad?.enabled) {
if (h?.onLoad?.url != null || h?.onStart?.url !== '') {
httpMessage = h?.onStart?.url;
}
}
// update lifecycle: onUpdate
this.ontimeCycle = this.cycleState.onUpdate;
break;
case 'onUpdate':
// call update
this.update();
// through OSC, only if running
if (this.state === 'start' || this.state === 'roll') {
if (this.current != null && this.secondaryTimer == null) {
this.sendOsc(this.osc.implemented.time, this.timeTag);
this.sendOsc(this.osc.implemented.overtime, this.current > 0 ? 0 : 1);
this.sendOsc(this.osc.implemented.title, this.titles?.titleNow || '');
this.sendOsc(this.osc.implemented.presenter, this.titles?.presenterNow || '');
}
}
// check integrations - http
if (h?.onLoad?.enabled) {
if (h?.onLoad?.url != null || h?.onUpdate?.url !== '') {
httpMessage = h?.onUpdate?.url;
}
}
break;
case 'onPause':
// send OSC
if (this.prevCycle === this.cycleState.onUpdate) {
this.sendOsc(this.osc.implemented.pause);
}
// check integrations - http
if (h?.onLoad?.enabled) {
if (h?.onLoad?.url != null || h?.onPause?.url !== '') {
httpMessage = h?.onPause?.url;
}
}
// update lifecycle: armed
this.ontimeCycle = this.cycleState.armed;
break;
case 'onStop':
// send OSC if something was actually stopped
if (this.prevCycle === this.cycleState.onUpdate) {
this.sendOsc(this.osc.implemented.stop);
}
// check integrations - http
if (h?.onLoad?.enabled) {
if (h?.onLoad?.url != null || h?.onStop?.url !== '') {
httpMessage = h?.onStop?.url;
}
}
// update lifecycle: idle
this.ontimeCycle = this.cycleState.idle;
break;
case 'onFinish':
// finished an event
this.sendOsc(this.osc.implemented.finished);
// check integrations - http
if (h?.onLoad?.enabled) {
if (h?.onLoad?.url != null || h?.onFinish?.url !== '') {
httpMessage = h?.onFinish?.url;
}
}
// update lifecycle: onUpdate
this.ontimeCycle = this.cycleState.onUpdate;
break;
default:
this.socket.error('SERVER', `Unhandled cycle: ${this.ontimeCycle}`);
}
// send http message if any
if (httpMessage != null) {
const v = {
$timer: this.timeTag,
$title: this.titles.titleNow,
$presenter: this.titles.presenterNow,
$subtitle: this.titles.subtitleNow,
'$next-title': this.titles.titleNext,
'$next-presenter': this.titles.presenterNext,
'$next-subtitle': this.titles.subtitleNext,
};
const m = cleanURL(replacePlaceholder(httpMessage, v));
this.http.send(m);
}
// update
this.update();
this.broadcastState();
// reset cycle
this.prevCycle = this.ontimeCycle;
}
update() {
// if there is nothing selected, update clock
this.clock = Timer.getCurrentTime();
this._broadcastFeatureTimer();
this.broadcastTimer();
// if we are not updating, send the timers
if (this.ontimeCycle !== this.cycleState.onUpdate) {
this.socket.send('timer', this.getTimeObject());
}
// Have we skipped onStart?
if (this.state === 'start' || this.state === 'roll') {
if (this.ontimeCycle === this.cycleState.armed) {
// update lifecycle: onStart
this.ontimeCycle = this.cycleState.onStart;
this.runCycle();
}
}
// update default functions
super.update();
if (this._finishedFlag) {
// update lifecycle: onFinish and call cycle
this.ontimeCycle = this.cycleState.onFinish;
this._finishedFlag = false;
this.runCycle();
}
// only implement roll here, rest implemented in super
if (this.state === 'roll') {
const u = {
selectedEventId: this.selectedEventId,
current: this.current,
// safeguard on midnight rollover
_finishAt: this._finishAt >= this._startedAt ? this._finishAt : this._finishAt + DAY_TO_MS,
clock: this.clock,
secondaryTimer: this.secondaryTimer,
_secondaryTarget: this._secondaryTarget,
};
const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } = updateRoll(u);
this.current = updatedTimer;
this.secondaryTimer = updatedSecondaryTimer;
if (isFinished) {
// update lifecycle: onFinish
this.ontimeCycle = this.cycleState.onFinish;
this.runCycle();
}
if (doRollLoad) {
this.rollLoad();
}
}
}
/**
*
* @param {string} eventId
*/
syncLoaded(eventId) {
if (this.state === 'roll') {
this.rollLoad();
} else {
const event = EventLoader.getEventWithId(eventId);
this.loadEvent(event, 'reload');
}
}
/**
* Loads a given event by index
* @typedef ('load'|'reload') loadEventOptions
* @param {object} event
* @param {string} [type='load'] - 'load' or 'reload', whether we are keeping running time
*/
loadEvent(event, type = 'load') {
const loadedData = eventLoader.loadById(event.id);
if (!loadedData) {
return;
}
const { loadedEvent, selectedEventIndex, selectedEventId, nextEventId, titles, titlesPublic } =
loadedData;
const start = loadedEvent.timeStart || 0;
let end = loadedEvent.timeEnd || 0;
// in case the end is earlier than start, we assume is the day after
if (end < start) {
end += DAY_TO_MS;
}
this.duration = end - start;
this.selectedEventIndex = selectedEventIndex;
this.selectedEventId = selectedEventId;
this.nextEventId = nextEventId;
if (type === 'load') {
this._resetTimers();
this.current = this.duration;
} else {
const now = Timer.getCurrentTime();
const elapsed = this.getElapsed();
this._finishAt = now + (this.duration - elapsed);
}
this.titles = titles;
this.titlesPublic = titlesPublic;
// update lifecycle: onLoad
this.ontimeCycle = this.cycleState.onLoad;
}
/**
* @description resets selected event data
* @private
*/
_resetSelection() {
this.titles = {
titleNow: null,
subtitleNow: null,
presenterNow: null,
noteNow: null,
titleNext: null,
subtitleNext: null,
presenterNext: null,
noteNext: null,
};
this.titlesPublic = {
titleNow: null,
subtitleNow: null,
presenterNow: null,
titleNext: null,
subtitleNext: null,
presenterNext: null,
};
this.selectedEventIndex = null;
this.selectedEventId = null;
this.nextEventId = null;
this.selectedPublicEventId = null;
this.nextPublicEventId = null;
}
/**
* @description start timer
* @return {('start'|'pause'|'stop'|'roll')} Playback state
*/
start() {
// do we need to change
if (this.state === 'start') return 'start';
// call super
super.start();
// update lifecycle: onStart
this.ontimeCycle = this.cycleState.onStart;
return this.state;
}
/**
* @description pause timer
* @return {('start'|'pause'|'stop')} Playback state
*/
pause() {
// do we need to change
if (this.state === 'pause') return 'pause';
// call super
super.pause();
// update lifecycle: onPause
this.ontimeCycle = this.cycleState.onPause;
return this.state;
}
/**
* @description stop timer
* @return {('start'|'pause'|'stop'|'roll')} Playback state
*/
stop() {
// do we need to change
if (this.state === 'stop') return 'stop';
// call super
super.stop();
this._resetTimers(true);
this._resetSelection();
// update lifecycle: onStop
this.ontimeCycle = this.cycleState.onStop;
// broadcast state
this.broadcastState();
return this.state;
}
/**
* @description increment timer by amount
* @param amount
*/
increment(amount) {
// call super
super.increment(amount);
// run cycle
this.runCycle();
}
/**
* @description Look for current event considering local clock
*/
rollLoad() {
const prevLoaded = this.selectedEventId;
// maybe roll has already been loaded
if (this.secondaryTimer === null) {
this._resetTimers(true);
this._resetSelection();
}
const { nowIndex, nowId, publicIndex, nextIndex, publicNextIndex, timers, timeToNext } =
eventLoader.findRoll();
// nothing to play, unload
if (nowIndex === null && nextIndex === null) {
this.stop();
this.socket.warning('SERVER', 'Roll: no events found');
return;
}
// there is something running, load
if (nowIndex !== null) {
// clear secondary timers
this.secondaryTimer = null;
this._secondaryTarget = null;
// set timers
this._startedAt = timers._startedAt;
this._finishAt = timers._finishAt;
this.duration = timers.duration;
this.current = timers.current;
// set selection
this.selectedEventId = nowId;
this.selectedEventIndex = nowIndex;
}
// found something to run next
if (nextIndex != null) {
const eventNext = EventLoader.getPlayableAtIndex(nextIndex);
// Set running timers
if (nowIndex === null) {
// only warn the first time
if (this.secondaryTimer === null) {
this.socket.info('SERVER', 'Roll: waiting for event start');
}
// reset running timer
// ??? should this not have been reset?
this.current = null;
// timer counts to next event
this.secondaryTimer = timeToNext;
this._secondaryTarget = eventNext.timeStart;
}
// TITLES: Load next private
// Todo: this should be an ID
// todo: this logic should be removed
if (eventNext) {
this.titles.titleNext = eventNext.title;
this.titles.subtitleNext = eventNext.subtitle;
this.titles.presenterNext = eventNext.presenter;
this.titles.noteNext = eventNext.note;
this.nextEventId = eventNext.id;
}
}
// Todo: this should be an ID
// todo: this logic should be removed
// TITLES: Load next public
if (publicNextIndex !== null) {
const eventNextPublic = EventLoader.getPlayableAtIndex(publicNextIndex);
if (eventNextPublic) {
this.titlesPublic.titleNext = eventNextPublic.title;
this.titlesPublic.subtitleNext = eventNextPublic.subtitle;
this.titlesPublic.presenterNext = eventNextPublic.presenter;
this.titlesPublic.noteNext = eventNextPublic.note;
this.nextPublicEventId = eventNextPublic.id;
}
}
// Todo: this should be an ID
// todo: this logic should be removed
// TITLES: Load now private
if (nowIndex !== null) {
const eventNowPrivate = EventLoader.getPlayableAtIndex(nowIndex);
if (eventNowPrivate) {
this.titles.titleNow = eventNowPrivate.title;
this.titles.subtitleNow = eventNowPrivate.subtitle;
this.titles.presenterNow = eventNowPrivate.presenter;
this.titles.noteNow = eventNowPrivate.note;
this.selectedEventId = eventNowPrivate.id;
}
}
// Todo: this should be an ID
// todo: this logic should be removed
// TITLES: Load now public
if (publicIndex !== null) {
const eventNowPublic = EventLoader.getPlayableAtIndex(nowIndex);
if (eventNowPublic) {
this.titlesPublic.titleNow = eventNowPublic.title;
this.titlesPublic.subtitleNow = eventNowPublic.subtitle;
this.titlesPublic.presenterNow = eventNowPublic.presenter;
this.titlesPublic.noteNow = eventNowPublic.note;
this.selectedPublicEventId = eventNowPublic.id;
}
}
if (prevLoaded !== this.selectedEventId) {
// update lifecycle: onLoad
this.ontimeCycle = this.cycleState.onLoad;
// ensure we go through onLoad cycle
this.runCycle();
}
}
/**
* Starts roll mode
* @return {('start'|'pause'|'stop'|'roll')} Playback state
*/
roll() {
if (this.state === 'roll') {
return 'roll';
}
this.state = 'roll';
// update lifecycle: armed
this.ontimeCycle = this.cycleState.armed;
// load into event
this.rollLoad();
return this.state;
}
previous() {
this.sendOsc(this.osc.implemented.previous);
this.pause();
this.runCycle();
}
next() {
this.sendOsc(this.osc.implemented.next);
this.pause();
this.runCycle();
}
/**
* @description reloads current event
* @return {('start'|'pause'|'stop'|'roll')} Playback state
*/
reload() {
if (!this.selectedEventId) {
return this.state;
}
// change playstate
this.pause();
// send OSC
this.sendOsc(this.osc.implemented.reload);
// reload data
const event = EventLoader.getEventWithId(this.selectedEventId);
this.loadEvent(event);
this.runCycle();
return this.state;
}
/****************************************************************************/
/**
* Integrations
* -------------
*
* Code related to integrations
*
*/
/**
* Calls OSC send message and resolves reply to logger
* @param {string} message
* @param {any} [payload]
*/
async sendOsc(message, payload) {
const reply = await this.osc.send(message, payload);
if (!reply.success) {
this.socket.error('TX', reply.message);
}
}
/**
* @description Builds sync object
*/
poll() {
return {
currentId: this.selectedEventId,
timer: this.timeTag,
clock: this.clock,
playback: this.state,
currentColour: null,
title: this.titles.titleNow,
presenter: this.titles.presenterNow,
};
}
}
-230
View File
@@ -1,230 +0,0 @@
import { stringFromMillis } from '../../utils/time.js';
/**
* @description Implements simple countdown timer functions
* @class
*/
export class Timer {
constructor() {
this.clock = null;
this._resetTimers(true);
this.state = 'stop';
}
/**
* @description updates the running timer
*/
update() {
// get current time
const now = Timer.getCurrentTime();
this.clock = now;
let checkFinish = false;
// check playstate
switch (this.state) {
case 'start':
// ensure we have a start time
if (this._startedAt == null) this._startedAt = now;
// update current timer
this.current = this._startedAt + this.duration + this._pausedTotal - now;
// enable flag
checkFinish = true;
break;
case 'pause':
// update paused time
this._pausedInterval = now - this._pausedAt;
if (this._startedAt != null) {
// update current timer
this.current =
this._startedAt + this.duration + this._pausedTotal + this._pausedInterval - now;
}
// enable flag
checkFinish = true;
break;
case 'stop':
// nothing here yet
break;
}
if (checkFinish) {
// is event finished?
const isTimeOver = this.current <= 0;
const isUpdating = this.state !== 'pause';
if (isTimeOver && isUpdating && this._finishedAt == null) {
if (this._finishedAt === null) this._finishedAt = now;
this._finishedFlag = true;
}
}
this.timeTag = stringFromMillis(this.current);
}
// helpers
/**
* @description converts a value in millis to seconds
* @param millis
* @return {number}
*/
static toSeconds(millis) {
if (millis == null) return 0;
return millis < 0 ? Math.ceil(millis * 0.001) : Math.floor(millis * 0.001);
}
/**
* @description get current time in ms from midnight
* @return {number}
*/
static getCurrentTime() {
const now = new Date();
// extract milliseconds since midnight
let elapsed = now.getHours() * 3600000;
elapsed += now.getMinutes() * 60000;
elapsed += now.getSeconds() * 1000;
elapsed += now.getMilliseconds();
return elapsed;
}
/**
* @description when is timer finishing
* @return {null|*|null|number}
* @private
*/
_getExpectedFinish() {
if (this._startedAt == null) return null;
if (this._finishedAt) return this._finishedAt;
return Math.max(
this._startedAt + this.duration + this._pausedInterval + this._pausedTotal,
this._startedAt
);
}
/**
* @description resets timer parameters
* @param total
* @private
*/
_resetTimers(total = false) {
if (total) this.duration = null;
this.current = this.duration;
this.timeTag = null;
this.running = null;
this.secondaryTimer = null;
this._secondaryTarget = null;
this._finishAt = null;
this._finishedAt = null;
this._finishedFlag = false;
this._startedAt = null;
this._pausedAt = null;
this._pausedInterval = null;
this._pausedTotal = null;
}
/**
* @description get elapsed time
* @return {number}
*/
getElapsed() {
return this.duration - this.current;
}
/**
* @description Builds time object
* @returns {{running: number, secondary: number, expectedFinish: number, durationSeconds: number, startedAt: null, clock: null}}
*/
getTimeObject() {
return {
clock: this.clock,
isNegative: this.current < 0,
running: Timer.toSeconds(this.current),
secondary: Timer.toSeconds(this.secondaryTimer),
durationSeconds: Timer.toSeconds(this.duration),
expectedFinish: this._getExpectedFinish(),
startedAt: this._startedAt,
};
}
// playback
/**
* @description start current time
*/
start() {
// do we need to change
if (this.state === 'start') return;
else if (this._startedAt == null) {
// it hasn't started yet
const now = Timer.getCurrentTime();
// set start time as now
this._startedAt = now;
// calculate expected finish time
this._finishAt = now + this.duration;
// reset pauses
this._pausedTotal = null;
this._pausedInterval = null;
} else {
// check if there is paused time
if (this._pausedInterval) {
this._pausedTotal += this._pausedInterval;
this._pausedInterval = null;
}
}
// change state
this.state = 'start';
}
/**
* @description pause current timer
*/
pause() {
// do we need to change
if (this.state === 'pause') return;
if (this._pausedInterval) {
this._pausedTotal += this._pausedInterval;
this._pausedInterval = null;
}
// set pause time
this._pausedAt = Timer.getCurrentTime();
// change state
this.state = 'pause';
}
/**
* @description stop current timer
*/
stop() {
// do we need to change
if (this.state === 'stop') return;
// clear all timers
this._resetTimers();
// change state
this.state = 'stop';
}
/**
* @description increments a given amout to the timer
* @param amount
*/
increment(amount) {
this.duration += amount;
if (amount < 0 && Math.abs(amount) > this.current) {
// if we will make the clock negative
if (this._finishedAt == null) this._finishedAt = Timer.getCurrentTime();
} else if (this.current < 0 && this.current + amount > 0) {
// clock will go from negative to positive
this._finishedAt = null;
}
}
}
@@ -1,65 +0,0 @@
import { EventTimer } from '../EventTimer';
import jest from 'jest-mock';
// necessary config
const timerConfig = { refresh: 1000 };
const mockSocket = {
error: jest.fn(),
send: jest.fn(),
info: jest.fn(),
};
test('object instantiates correctly', async () => {
const t = new EventTimer(mockSocket, timerConfig);
// it contains everything from Timer
expect(t.clock).toBeNull();
expect(t.duration).toBeNull();
expect(t.current).toBeNull();
expect(t.timeTag).toBeNull();
expect(t.secondaryTimer).toBeNull();
expect(t._secondaryTarget).toBeNull();
expect(t._finishAt).toBeNull();
expect(t._finishedAt).toBeNull();
expect(t._finishedFlag).toBeFalsy();
expect(t._startedAt).toBeNull();
expect(t._pausedAt).toBeNull();
expect(t._pausedInterval).toBeNull();
expect(t._pausedTotal).toBeNull();
expect(t.state).toBe('stop');
// and its own properties
expect(t.ontimeCycle).toBe('idle');
expect(t.prevCycle).toBeNull();
expect(t.io).not.toBeNull();
expect(t.osc).toBeNull();
expect(t.http).toBeNull();
expect(t._interval).not.toBeNull();
const expectTitlesPublic = {
titleNow: null,
subtitleNow: null,
presenterNow: null,
titleNext: null,
subtitleNext: null,
presenterNext: null,
};
const expectTitles = {
...expectTitlesPublic,
noteNow: null,
noteNext: null,
};
expect(t.titlesPublic).toStrictEqual(expectTitlesPublic);
expect(t.titles).toStrictEqual(expectTitles);
expect(t.selectedEventIndex).toBeNull();
expect(t.selectedEventId).toBeNull();
expect(t.nextEventId).toBeNull();
expect(t.selectedPublicEventId).toBeNull();
expect(t.nextPublicEventId).toBeNull();
t.shutdown();
});
@@ -1,54 +0,0 @@
import { Timer } from '../Timer';
test('object instantiates correctly', () => {
const t = new Timer();
expect(t.clock).toBeNull;
expect(t.duration).toBeNull;
expect(t.current).toBeNull;
expect(t.timeTag).toBeNull;
expect(t.secondaryTimer).toBeNull;
expect(t._secondaryTarget).toBeNull;
expect(t._finishAt).toBeNull;
expect(t._finishedAt).toBeNull;
expect(t._finishedFlag).toBeFalsy;
expect(t._startedAt).toBeNull;
expect(t._pausedAt).toBeNull;
expect(t._pausedInterval).toBeNull;
expect(t._pausedTotal).toBeNull;
expect(t.state).toBe('stop');
});
test('convert between mills and seconds correctly', () => {
expect(Timer.toSeconds(10000)).toBe(10);
expect(Timer.toSeconds(9016)).toBe(9);
expect(Timer.toSeconds(8016)).toBe(8);
expect(Timer.toSeconds(7010)).toBe(7);
expect(Timer.toSeconds(6006)).toBe(6);
expect(Timer.toSeconds(4999)).toBe(4);
expect(Timer.toSeconds(2995)).toBe(2);
expect(Timer.toSeconds(1991)).toBe(1);
expect(Timer.toSeconds(992)).toBe(0);
expect(Timer.toSeconds(127)).toBe(0);
expect(Timer.toSeconds(0)).toBe(0);
expect(Timer.toSeconds(-0)).toBe(-0);
expect(Timer.toSeconds(-127)).toBe(-0);
expect(Timer.toSeconds(-992)).toBe(-0);
expect(Timer.toSeconds(-1991)).toBe(-1);
expect(Timer.toSeconds(-2995)).toBe(-2);
expect(Timer.toSeconds(-4999)).toBe(-4);
expect(Timer.toSeconds(-6006)).toBe(-6);
expect(Timer.toSeconds(-7010)).toBe(-7);
expect(Timer.toSeconds(-8016)).toBe(-8);
expect(Timer.toSeconds(-10000)).toBe(-10);
});
test('converting between millis to seconds handles partials correctly', () => {
const finish = 82162001;
const now = 80364519;
const runningMs = finish - now;
expect(Timer.toSeconds(runningMs)).toBe(1797);
expect(Timer.toSeconds(1800000)).toBe(1800);
expect(Timer.toSeconds(1799761)).toBe(1799);
});
+2 -2
View File
@@ -1,5 +1,5 @@
import { Server } from 'node-osc';
import { PlaybackService } from '../services/playbackService.js';
import { PlaybackService } from '../services/PlaybackService.js';
import { messageManager } from '../classes/message-manager/MessageManager.js';
import { socketProvider } from '../classes/socket/SocketController.js';
import { ADDRESS_MESSAGE_CONTROL } from '../classes/socket/socketConfig.js';
@@ -22,7 +22,7 @@ export const initiateOSC = (config) => {
oscServer.on('error', console.error);
oscServer.on('message', function(msg) {
oscServer.on('message', function (msg) {
// message should look like /ontime/{path} {args} where
// ontime: fixed message for app
// path: command to be called
+1 -1
View File
@@ -6,7 +6,7 @@ import { resolveDbPath } from '../modules/loadDb.js';
import { DataProvider } from '../classes/data-provider/DataProvider.js';
import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
import { mergeObject } from '../utils/parserUtils.js';
import { PlaybackService } from '../services/playbackService.js';
import { PlaybackService } from '../services/PlaybackService.js';
import { runtimeState } from '../stores/EventStore.js';
// Create controller for GET request to '/ontime/poll'
+1 -1
View File
@@ -1,6 +1,6 @@
// Create controller for GET request to '/playback'
// Returns ACK message
import { PlaybackService } from '../services/playbackService.js';
import { PlaybackService } from '../services/PlaybackService.js';
// Create controller for POST request to '/playback'
// Returns playback state
+1 -1
View File
@@ -7,7 +7,7 @@ import {
deleteEvent,
editEvent,
reorderEvent,
} from '../services/rundownService.js';
} from '../services/RundownService.js';
// Create controller for GET request to '/eventlist'
// Returns -
+2 -1
View File
@@ -1,4 +1,5 @@
import { JSONFile, Low } from 'lowdb';
import { Low } from 'lowdb';
import { JSONFile } from 'lowdb/node';
import { dirname, join } from 'path';
import { copyFileSync, existsSync } from 'fs';
import { fileURLToPath } from 'url';
+1 -1
View File
@@ -10,7 +10,7 @@
"express": "^4.18.1",
"express-session": "^1.17.3",
"express-validator": "^6.14.2",
"lowdb": "3.0.0",
"lowdb": "^5.0.5",
"multer": "^1.4.4",
"nanoid": "^4.0.0",
"node-osc": "^8.0.6",
@@ -3,7 +3,7 @@
*/
import { socketProvider } from '../classes/socket/SocketController.js';
import { eventLoader, EventLoader } from '../classes/event-loader/EventLoader.js';
import { eventTimer } from './TimerService.js';
import { eventTimer, TimerService } from './TimerService.js';
/**
* Service manages playback status of app
@@ -121,7 +121,7 @@ export class PlaybackService {
if (eventLoader.selectedEventId) {
eventTimer.start();
const newState = eventTimer.playback;
socketProvider.info('PLAYBACK', `Play Mode ${newState}`);
socketProvider.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
}
socketProvider.broadcastState();
}
@@ -133,7 +133,7 @@ export class PlaybackService {
if (eventLoader.selectedEventId) {
eventTimer.pause();
const newState = eventTimer.playback;
socketProvider.info('PLAYBACK', `Play Mode ${newState}`);
socketProvider.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
}
socketProvider.broadcastState();
}
@@ -146,7 +146,7 @@ export class PlaybackService {
eventLoader.reset();
eventTimer.stop();
const newState = eventTimer.playback;
socketProvider.info('PLAYBACK', `Play Mode ${newState}`);
socketProvider.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
}
socketProvider.broadcastState();
}
@@ -165,13 +165,29 @@ export class PlaybackService {
* Sets playback to roll
*/
static roll() {
if (EventLoader.getNumEvents() && eventTimer.playback !== 'roll') {
eventTimer.roll();
if (EventLoader.getPlayableEvents()) {
const rollTimers = eventLoader.findRoll(TimerService.getCurrentTime());
// nothing to play
if (rollTimers === null) {
socketProvider.error('SERVER', 'Roll: no events found');
PlaybackService.stop();
return;
}
const { currentEvent, nextEvent, timers } = rollTimers;
if (!currentEvent && !nextEvent) {
socketProvider.error('SERVER', 'Roll: no events found');
PlaybackService.stop();
return;
}
eventTimer.roll(currentEvent, nextEvent, timers);
const newState = eventTimer.playback;
socketProvider.info('PLAYBACK', `Play Mode ${newState}`);
socketProvider.send('playback', newState);
socketProvider.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
socketProvider.broadcastState();
}
socketProvider.broadcastState();
}
/**
@@ -86,16 +86,15 @@ export function updateTimer(affectedIds) {
if (safeOption) {
eventLoader.reset();
const loadedEvent = eventLoader.loadById(runningEventId);
const { loadedEvent } = eventLoader.loadById(runningEventId) || {};
eventTimer.hotReload(loadedEvent);
return true;
}
if (eventInMemory) {
const loadedEvent = eventLoader.loadById(runningEventId);
eventLoader.reset();
const { loadedEvent } = eventLoader.loadById(runningEventId) || {};
if (!loadedEvent) {
// event was deleted
eventLoader.reset();
eventTimer.stop();
} else {
eventTimer.hotReload(loadedEvent);
@@ -104,7 +103,7 @@ export function updateTimer(affectedIds) {
}
if (isNext) {
const loadedEvent = eventLoader.loadById(runningEventId);
const { loadedEvent } = eventLoader.loadById(runningEventId) || {};
eventTimer.hotReload(loadedEvent);
return true;
}
+90 -24
View File
@@ -1,6 +1,9 @@
import { runtimeState } from '../stores/EventStore.js';
import { PlaybackService } from './PlaybackService.js';
import { updateRoll } from './rollUtils.js';
import { DAY_TO_MS } from '../utils/time.js';
class TimerService {
export class TimerService {
/**
* @constructor
* @param {object} [timerConfig]
@@ -62,10 +65,10 @@ class TimerService {
finishedAt: null,
secondaryTimer: null,
};
this.loadedTimer = null;
this.loadedTimerId = null;
this._pausedInterval = 0;
this._pausedAt = null;
this._secondaryTarget = null;
}
/**
@@ -73,16 +76,28 @@ class TimerService {
* @param timer
*/
hotReload(timer) {
if (timer?.id !== this.loadedTimerId) {
if (typeof timer === 'undefined') {
this.stop();
return;
}
if (timer?.id !== this.loadedTimerId) {
// event timer only concerns itself with current event
return;
}
if (timer?.skip) {
this.stop();
}
// TODO: check if any relevant information warrants update
// update relevant information and force update
this.loadedTimer = timer;
this.timer.duration = timer.duration;
// this might not be ideal
this.timer.finishedAt = null;
this.timer.expectedFinish = this._getExpectedFinish();
if (this.timer.startedAt === null) {
this.timer.current = timer.duration;
}
@@ -106,7 +121,6 @@ class TimerService {
this._clear();
this.loadedTimer = timer;
this.loadedTimerId = timer.id;
this.timer.duration = timer.duration;
this.timer.current = timer.duration;
@@ -221,27 +235,58 @@ class TimerService {
update() {
this.timer.clock = TimerService.getCurrentTime();
// we only update timer if a timer has been started
if (this.timer.startedAt !== null) {
if (this.playback === 'pause') {
this._pausedInterval = this.timer.clock - this._pausedAt;
}
if (this.playback === 'roll') {
const tempCurrentTimer = {
selectedEventId: this.loadedTimerId,
current: this.timer.current,
// safeguard on midnight rollover
_finishAt:
this.timer.expectedFinish >= this.timer.startedAt
? this.timer.expectedFinish
: this.timer.expectedFinish + DAY_TO_MS,
this.timer.current =
this.timer.startedAt +
this.timer.duration +
this.timer.addedTime +
this._pausedInterval -
this.timer.clock;
this.timer.elapsed = this.timer.duration - this.timer.current;
clock: this.timer.clock,
secondaryTimer: this.timer.secondaryTimer,
_secondaryTarget: this._secondaryTarget,
};
const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } =
updateRoll(tempCurrentTimer);
if (this.playback === 'play' && this.timer.current <= 0 && this.timer.finishedAt === null) {
this.timer.finishedAt = this.timer.clock;
this.timer.current = updatedTimer;
this.timer.secondaryTimer = updatedSecondaryTimer;
if (isFinished) {
this.timer.selectedEventId = null;
this.loadedTimerId = null;
this._onFinish();
} else {
this.timer.finishedAt = null;
}
this.timer.expectedFinish = this._getExpectedFinish();
if (doRollLoad) {
PlaybackService.roll();
}
} else {
// we only update timer if a timer has been started
if (this.timer.startedAt !== null) {
if (this.playback === 'pause') {
this._pausedInterval = this.timer.clock - this._pausedAt;
}
this.timer.current =
this.timer.startedAt +
this.timer.duration +
this.timer.addedTime +
this._pausedInterval -
this.timer.clock;
this.timer.elapsed = this.timer.duration - this.timer.current;
if (this.playback === 'play' && this.timer.current <= 0 && this.timer.finishedAt === null) {
this.timer.finishedAt = this.timer.clock;
this._onFinish();
} else {
this.timer.finishedAt = null;
}
this.timer.expectedFinish = this._getExpectedFinish();
}
}
this._onUpdate();
}
@@ -256,12 +301,33 @@ class TimerService {
runtimeState.set('ontime-timer', this.timer);
}
roll() {
roll(currentEvent, nextEvent, timers) {
this._clear();
this.timer.clock = TimerService.getCurrentTime();
if (currentEvent) {
// there is something running, load
this.timer.secondaryTimer = null;
this._secondaryTarget = null;
this.loadedTimerId = currentEvent.id;
this.timer.startedAt = currentEvent.timeStart;
this.timer.expectedFinish = currentEvent.timeEnd;
this.timer.duration = timers.duration;
this.timer.current = timers.current;
} else if (nextEvent) {
// nothing now, but something coming up
this.timer.secondaryTimer = nextEvent.timeStart - this.timer.clock;
this._secondaryTarget = nextEvent.timeStart;
}
this.playback = 'roll';
this._onRoll();
this.update();
}
_onRoll() {
throw new Error('Roll not implemented');
this._onLoad();
}
shutdown() {
@@ -1,11 +1,11 @@
import {
DAY_TO_MS,
getSelectionByRoll,
getRollTimers,
normaliseEndTime,
replacePlaceholder,
sortArrayByProperty,
updateRoll,
} from '../classUtils.js';
} from '../rollUtils.js';
// test sortArrayByProperty()
describe('sort simple arrays of objects', () => {
@@ -54,7 +54,7 @@ describe('sort simple arrays of objects', () => {
});
});
// test getSelectionByRoll()
// test getRollTimers()
describe('test that roll loads selection in right order', () => {
const eventlist = [
{
@@ -119,7 +119,7 @@ describe('test that roll loads selection in right order', () => {
timeToNext: 5,
};
const state = getSelectionByRoll(eventlist, now);
const state = getRollTimers(eventlist, now);
expect(state).toStrictEqual(expected);
});
@@ -140,7 +140,7 @@ describe('test that roll loads selection in right order', () => {
timeToNext: 5,
};
const state = getSelectionByRoll(eventlist, now);
const state = getRollTimers(eventlist, now);
expect(state).toStrictEqual(expected);
});
@@ -161,7 +161,7 @@ describe('test that roll loads selection in right order', () => {
timeToNext: 5,
};
const state = getSelectionByRoll(eventlist, now);
const state = getRollTimers(eventlist, now);
expect(state).toStrictEqual(expected);
});
@@ -182,7 +182,7 @@ describe('test that roll loads selection in right order', () => {
timeToNext: 10,
};
const state = getSelectionByRoll(eventlist, now);
const state = getRollTimers(eventlist, now);
expect(state).toStrictEqual(expected);
});
@@ -203,7 +203,7 @@ describe('test that roll loads selection in right order', () => {
timeToNext: 1,
};
const state = getSelectionByRoll(eventlist, now);
const state = getRollTimers(eventlist, now);
expect(state).toStrictEqual(expected);
});
@@ -224,7 +224,7 @@ describe('test that roll loads selection in right order', () => {
timeToNext: 7,
};
const state = getSelectionByRoll(eventlist, now);
const state = getRollTimers(eventlist, now);
expect(state).toStrictEqual(expected);
});
@@ -245,7 +245,7 @@ describe('test that roll loads selection in right order', () => {
timeToNext: null,
};
const state = getSelectionByRoll(eventlist, now);
const state = getRollTimers(eventlist, now);
expect(state).toStrictEqual(expected);
});
@@ -261,7 +261,7 @@ describe('test that roll loads selection in right order', () => {
timeToNext: DAY_TO_MS - now + eventlist[0].timeStart,
};
const state = getSelectionByRoll(eventlist, now);
const state = getRollTimers(eventlist, now);
expect(state).toStrictEqual(expected);
});
@@ -284,12 +284,12 @@ describe('test that roll loads selection in right order', () => {
timers: null,
timeToNext: DAY_TO_MS - now + singleEventList[0].timeStart,
};
const state = getSelectionByRoll(singleEventList, now);
const state = getRollTimers(singleEventList, now);
expect(state).toStrictEqual(expected);
});
});
// test getSelectionByRoll()
// test getRollTimers()
describe('test that roll behaviour with overlapping times', () => {
const eventlist = [
{
@@ -324,7 +324,7 @@ describe('test that roll behaviour with overlapping times', () => {
timeToNext: 10,
};
const state = getSelectionByRoll(eventlist, now);
const state = getRollTimers(eventlist, now);
expect(state).toStrictEqual(expected);
});
@@ -345,7 +345,7 @@ describe('test that roll behaviour with overlapping times', () => {
timeToNext: 0,
};
const state = getSelectionByRoll(eventlist, now);
const state = getRollTimers(eventlist, now);
expect(state).toStrictEqual(expected);
});
@@ -366,7 +366,7 @@ describe('test that roll behaviour with overlapping times', () => {
timeToNext: -5,
};
const state = getSelectionByRoll(eventlist, now);
const state = getRollTimers(eventlist, now);
expect(state).toStrictEqual(expected);
});
@@ -387,7 +387,7 @@ describe('test that roll behaviour with overlapping times', () => {
timeToNext: null,
};
const state = getSelectionByRoll(eventlist, now);
const state = getRollTimers(eventlist, now);
expect(state).toStrictEqual(expected);
});
@@ -408,7 +408,7 @@ describe('test that roll behaviour with overlapping times', () => {
timeToNext: null,
};
const state = getSelectionByRoll(eventlist, now);
const state = getRollTimers(eventlist, now);
expect(state).toStrictEqual(expected);
});
});
@@ -475,7 +475,7 @@ describe('test that it replaces data correctly', () => {
});
});
// test getSelectionByRoll() on issue #58
// test getRollTimers() on issue #58
describe('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
@@ -502,7 +502,7 @@ describe('test that roll behaviour multi day event edge cases', () => {
timeToNext: null,
};
const state = getSelectionByRoll(eventlist, now);
const state = getRollTimers(eventlist, now);
expect(state).toStrictEqual(expected);
});
@@ -526,7 +526,7 @@ describe('test that roll behaviour multi day event edge cases', () => {
timeToNext: eventlist[0].timeStart - now,
};
const state = getSelectionByRoll(eventlist, now);
const state = getRollTimers(eventlist, now);
expect(state).toStrictEqual(expected);
});
});
@@ -646,4 +646,24 @@ describe('typical scenarios', () => {
expect(updateRoll(timers)).toStrictEqual(expected);
});
it('when a secondary timer is finished, it prompts for new event load', () => {
const timers = {
selectedEventId: null,
current: null,
_finishAt: null,
clock: 15,
secondaryTimer: 0,
_secondaryTarget: 15,
};
const expected = {
updatedTimer: null,
updatedSecondaryTimer: timers._secondaryTarget - timers.clock,
doRollLoad: true,
isFinished: false,
};
expect(updateRoll(timers)).toStrictEqual(expected);
});
});
@@ -40,56 +40,43 @@ export const replacePlaceholder = (str, values) => {
};
/**
* @description Used in roll mode, returns selection variables from array
* @param {array} arr - event list
* @param {number} now - time now in millis
* @returns {object} object with selection variables
*
* @param rundown
* @param timeNow
* @returns {{}}
*/
export const getSelectionByRoll = (arr, now) => {
// Events now
export const getRollTimers = (rundown, timeNow) => {
let nowIndex = null; // index of event now
let nowId = null; // id of event now
let publicIndex = null; // index of public event now
let publicTime = -1; // counter:
// Events next
let publicTime = -1;
let nextIndex = null; // index of next event
let publicNextIndex = null; // index of next public event
let timeToNext = null; // counter: time for next event
let publicTimeToNext = null; // counter: time for next public event
// current timer
let timers = null;
// exit early if there are no events
if (arr.length < 1) {
return {
nowIndex,
nowId,
publicIndex,
nextIndex,
publicNextIndex,
timers,
timeToNext,
};
}
// Order events by startTime
const orderedEvents = sortArrayByProperty(arr, 'timeStart');
const orderedEvents = sortArrayByProperty(rundown, 'timeStart');
// preload first if we are past events
const lastEvent = orderedEvents[orderedEvents.length - 1];
const lastNormalEnd = normaliseEndTime(lastEvent.timeStart, lastEvent.timeEnd);
if (now > lastNormalEnd) {
let nextEvent = null;
let nextPublicEvent = null;
let currentEvent = null;
let currentPublicEvent = null;
if (timeNow > lastNormalEnd) {
nextIndex = 0;
timeToNext = orderedEvents[0].timeStart + DAY_TO_MS - now;
timeToNext = orderedEvents[0].timeStart + DAY_TO_MS - timeNow;
// look for next public
for (const e of orderedEvents) {
if (e.isPublic) {
publicNextIndex = arr.findIndex((a) => a.id === e.id);
for (const event of orderedEvents) {
if (event.isPublic) {
nextPublicEvent = event;
publicNextIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
break;
}
}
@@ -98,39 +85,42 @@ export const getSelectionByRoll = (arr, now) => {
let nowFound = false;
// loop through events, look for where we should be
for (const e of orderedEvents) {
for (const event of orderedEvents) {
// When does the event end (handle midnight)
const normalEnd = normaliseEndTime(e.timeStart, e.timeEnd);
const normalEnd = normaliseEndTime(event.timeStart, event.timeEnd);
if (normalEnd <= now) {
if (normalEnd <= timeNow) {
// event ran already
// public event might not be the one running
if (e.isPublic && normalEnd > publicTime) {
if (event.isPublic && normalEnd > publicTime) {
publicTime = normalEnd;
publicIndex = arr.findIndex((a) => a.id === e.id);
currentPublicEvent = event;
publicIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
}
} else if (normalEnd > now && now >= e.timeStart && !nowFound) {
} else if (normalEnd > timeNow && timeNow >= event.timeStart && !nowFound) {
// event is running
// it could also be public
if (e.isPublic) {
if (event.isPublic) {
publicTime = normalEnd;
publicIndex = arr.findIndex((a) => a.id === e.id);
currentPublicEvent = event;
publicIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
}
nowIndex = arr.findIndex((a) => a.id === e.id);
nowId = e.id;
currentEvent = event;
nowIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
nowId = event.id;
// set timers
timers = {
_startedAt: e.timeStart,
_finishAt: e.timeEnd,
duration: normalEnd - e.timeStart,
current: normalEnd - now,
_startedAt: event.timeStart,
_finishAt: event.timeEnd,
duration: normalEnd - event.timeStart,
current: normalEnd - timeNow,
};
nowFound = true;
} else if (normalEnd > now) {
} else if (normalEnd > timeNow) {
// event will run
// no need to look after found first
@@ -138,15 +128,17 @@ export const getSelectionByRoll = (arr, now) => {
// look for next events
// check how far the start is from now
const wait = e.timeStart - now;
const wait = event.timeStart - timeNow;
if (nextIndex === null || wait < timeToNext) {
timeToNext = wait;
nextIndex = arr.findIndex((a) => a.id === e.id);
nextEvent = event;
nextIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
}
if ((publicNextIndex === null || wait < publicTimeToNext) && e.isPublic) {
if ((publicNextIndex === null || wait < publicTimeToNext) && event.isPublic) {
publicTimeToNext = wait;
publicNextIndex = arr.findIndex((a) => a.id === e.id);
nextPublicEvent = event;
publicNextIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
}
}
}
@@ -160,6 +152,10 @@ export const getSelectionByRoll = (arr, now) => {
publicNextIndex,
timers,
timeToNext,
nextEvent,
nextPublicEvent,
currentEvent,
currentPublicEvent,
};
};
@@ -183,7 +179,7 @@ export const updateRoll = (currentTimers) => {
let updatedSecondaryTimer = secondaryTimer;
// whether rollLoad should be called
let doRollLoad = false;
// whether runCycle should be called
// whether finished event should trigger
let isFinished = false;
if (selectedEventId && current >= 0) {
@@ -194,6 +190,7 @@ export const updateRoll = (currentTimers) => {
updatedTimer = _finishAt - clock;
if (updatedTimer < 0) {
isFinished = true;
updatedTimer = null;
}
} else if (secondaryTimer >= 0) {
// if secondaryTimer is running we are in waiting to roll
-3
View File
@@ -327,8 +327,6 @@ const adjective = [
'far-flung',
'far-off',
'fast',
'fat',
'fatal',
'fatherly',
'favorable',
'favorite',
@@ -1647,7 +1645,6 @@ const object = [
'hall',
'historian',
'hospital',
'injury',
'instruction',
'maintenance',
'manufacturer',
+1
View File
@@ -4,6 +4,7 @@ const mth = 1000 * 60 * 60; // millis to hours
export const timeFormat = 'HH:mm';
export const timeFormatSeconds = 'HH:mm:ss';
export const DAY_TO_MS = 86400000;
/**
* @description Validates a time string
+9 -9
View File
@@ -952,12 +952,12 @@ lodash@^4.17.21:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
lowdb@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lowdb/-/lowdb-3.0.0.tgz#c10ab4e7eb86f1cbe255e35e60ffb0c6f42049e0"
integrity sha512-9KZRulmIcU8fZuWiaM0d5e2/nPnrFyXkeXVpqT+MJS+vgbgOf1EbtvgQmba8HwUFgDl1oeZR6XqEJnkJmQdKmg==
lowdb@^5.0.5:
version "5.0.5"
resolved "https://registry.yarnpkg.com/lowdb/-/lowdb-5.0.5.tgz#30315e5a42432df188dcd17f1e5a288de68848e8"
integrity sha512-7EWKmIMhNKA8TXFhL8t0p6N2LC53l3ZqsWQGSksGhhjrcms9rbKlyrAh2PzSGK5v0KPJ2W5VItBnC3NDRzOnzQ==
dependencies:
steno "^2.1.0"
steno "^3.0.0"
lru_map@^0.3.3:
version "0.3.3"
@@ -1453,10 +1453,10 @@ statuses@2.0.1:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
steno@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/steno/-/steno-2.1.0.tgz#05a9c378ce42ed04f642cda6fcb41787a10e4e33"
integrity sha512-mauOsiaqTNGFkWqIfwcm3y/fq+qKKaIWf1vf3ocOuTdco9XoHCO2AGF1gFYXuZFSWuP38Q8LBHBGJv2KnJSXyA==
steno@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/steno/-/steno-3.0.0.tgz#212a11e8ef3646b610efc8953842f556fd0df28f"
integrity sha512-uZtn7Ht9yXLiYgOsmo8btj4+f7VxyYheMt8g6F1ANjyqByQXEE2Gygjgenp3otHH1TlHsS4JAaRGv5wJ1wvMNw==
streamsearch@0.1.2:
version "0.1.2"