fix: midnight roll (#484)

* refactor: extract utilities and types

* refactor: extract and simplify progress bar logic

* refactor: duration validation handles midnight

* fix: prevent stale event loader

* refactor: consider next event in timer invalidation

* refactor: reload changes on changed roll target

* refactor: timer load accounts for midnight

* fix: issues with midnight on roll

* refactor: prevent stale secondary event
This commit is contained in:
Carlos Valente
2023-08-12 10:42:43 +02:00
committed by GitHub
parent 8c9cb908cf
commit 023aac4ead
28 changed files with 520 additions and 271 deletions
@@ -354,46 +354,83 @@ export class EventLoader {
* @private
*/
private _loadThisTitles(event, type) {
if (!event) {
return;
if (type === 'now') {
if (event === null) {
// public
this.titlesPublic.titleNow = null;
this.titlesPublic.subtitleNow = null;
this.titlesPublic.presenterNow = null;
this.titlesPublic.noteNow = null;
this.loaded.selectedPublicEventId = null;
// private
this.titles.titleNow = null;
this.titles.subtitleNow = null;
this.titles.presenterNow = null;
this.titles.noteNow = null;
this.loaded.selectedEventId = null;
} else {
// public
this.titlesPublic.titleNow = event.title;
this.titlesPublic.subtitleNow = event.subtitle;
this.titlesPublic.presenterNow = event.presenter;
this.titlesPublic.noteNow = event.note;
this.loaded.selectedPublicEventId = event.id;
// private
this.titles.titleNow = event.title;
this.titles.subtitleNow = event.subtitle;
this.titles.presenterNow = event.presenter;
this.titles.noteNow = event.note;
this.loaded.selectedEventId = event.id;
}
} else if (type === 'now-public') {
if (event === null) {
this.titlesPublic.titleNow = null;
this.titlesPublic.subtitleNow = null;
this.titlesPublic.presenterNow = null;
this.titlesPublic.noteNow = null;
this.loaded.selectedPublicEventId = null;
} else {
this.titlesPublic.titleNow = event.title;
this.titlesPublic.subtitleNow = event.subtitle;
this.titlesPublic.presenterNow = event.presenter;
this.titlesPublic.noteNow = event.note;
this.loaded.selectedPublicEventId = event.id;
}
} else if (type === 'now-private') {
if (event === null) {
this.titles.titleNow = null;
this.titles.subtitleNow = null;
this.titles.presenterNow = null;
this.titles.noteNow = null;
this.loaded.selectedEventId = null;
} else {
this.titles.titleNow = event.title;
this.titles.subtitleNow = event.subtitle;
this.titles.presenterNow = event.presenter;
this.titles.noteNow = event.note;
this.loaded.selectedEventId = event.id;
}
}
switch (type) {
// now, load to both public and private
case 'now':
// next, load to both public and private
else if (type === 'next') {
if (event === null) {
// public
this.titlesPublic.titleNow = event.title;
this.titlesPublic.subtitleNow = event.subtitle;
this.titlesPublic.presenterNow = event.presenter;
this.titlesPublic.noteNow = event.note;
this.loaded.selectedPublicEventId = event.id;
this.titlesPublic.titleNext = null;
this.titlesPublic.subtitleNext = null;
this.titlesPublic.presenterNext = null;
this.titlesPublic.noteNext = null;
this.loaded.nextPublicEventId = null;
// private
this.titles.titleNow = event.title;
this.titles.subtitleNow = event.subtitle;
this.titles.presenterNow = event.presenter;
this.titles.noteNow = event.note;
this.loaded.selectedEventId = event.id;
break;
case 'now-public':
this.titlesPublic.titleNow = event.title;
this.titlesPublic.subtitleNow = event.subtitle;
this.titlesPublic.presenterNow = event.presenter;
this.titlesPublic.noteNow = event.note;
this.loaded.selectedPublicEventId = event.id;
break;
case 'now-private':
this.titles.titleNow = event.title;
this.titles.subtitleNow = event.subtitle;
this.titles.presenterNow = event.presenter;
this.titles.noteNow = event.note;
this.loaded.selectedEventId = event.id;
break;
// next, load to both public and private
case 'next':
this.titles.titleNext = null;
this.titles.subtitleNext = null;
this.titles.presenterNext = null;
this.titles.noteNext = null;
this.loaded.nextEventId = null;
} else {
// public
this.titlesPublic.titleNext = event.title;
this.titlesPublic.subtitleNext = event.subtitle;
@@ -407,26 +444,37 @@ export class EventLoader {
this.titles.presenterNext = event.presenter;
this.titles.noteNext = event.note;
this.loaded.nextEventId = event.id;
break;
case 'next-public':
}
} else if (type === 'next-public') {
if (event === null) {
this.titlesPublic.titleNext = null;
this.titlesPublic.subtitleNext = null;
this.titlesPublic.presenterNext = null;
this.titlesPublic.noteNext = null;
this.loaded.nextPublicEventId = null;
} else {
this.titlesPublic.titleNext = event.title;
this.titlesPublic.subtitleNext = event.subtitle;
this.titlesPublic.presenterNext = event.presenter;
this.titlesPublic.noteNext = event.note;
this.loaded.nextPublicEventId = event.id;
break;
case 'next-private':
}
} else if (type === 'next-private') {
if (event === null) {
this.titles.titleNext = null;
this.titles.subtitleNext = null;
this.titles.presenterNext = null;
this.titles.noteNext = null;
this.loaded.nextEventId = null;
} else {
this.titles.titleNext = event.title;
this.titles.subtitleNext = event.subtitle;
this.titles.presenterNext = event.presenter;
this.titles.noteNext = event.note;
this.loaded.nextEventId = event.id;
break;
default:
throw new Error(`Unhandled title type: ${type}`);
}
} else {
throw new Error(`Unhandled title type: ${type}`);
}
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { LogOrigin, OntimeEvent, Playback } from 'ontime-types';
import { LogOrigin, OntimeEvent } from 'ontime-types';
import { validatePlayback } from 'ontime-utils';
import { eventLoader, EventLoader } from '../classes/event-loader/EventLoader.js';
+15 -8
View File
@@ -1,9 +1,9 @@
import { EndAction, OntimeEvent, Playback, TimerLifeCycle, TimerState } from 'ontime-types';
import { calculateDuration, dayInMs } from 'ontime-utils';
import { eventStore } from '../stores/EventStore.js';
import { PlaybackService } from './PlaybackService.js';
import { updateRoll } from './rollUtils.js';
import { DAY_TO_MS } from '../utils/time.js';
import { integrationService } from './integration-service/IntegrationService.js';
import { getCurrent, getElapsed, getExpectedFinish } from './timerUtils.js';
import { clock } from './Clock.js';
@@ -90,7 +90,7 @@ export class TimerService {
// TODO: check if any relevant information warrants update
// update relevant information and force update
this.timer.duration = timer.duration;
this.timer.duration = calculateDuration(timer.timeStart, timer.timeEnd);
this.timer.timerType = timer.timerType;
this.timer.endAction = timer.endAction;
@@ -104,7 +104,7 @@ export class TimerService {
this.timer.addedTime,
);
if (this.timer.startedAt === null) {
this.timer.current = timer.duration;
this.timer.current = this.timer.duration;
}
this.update(true);
}
@@ -112,6 +112,7 @@ export class TimerService {
/**
* Loads given timer to object
* @param {object} timer
* @param initialData
* @param {number} timer.id
* @param {number} timer.timeStart
* @param {number} timer.timeEnd
@@ -128,8 +129,8 @@ export class TimerService {
this._clear();
this.loadedTimerId = timer.id;
this.timer.duration = timer.duration;
this.timer.current = timer.duration;
this.timer.duration = calculateDuration(timer.timeStart, timer.timeEnd);
this.timer.current = this.timer.duration;
this.playback = Playback.Armed;
this.timer.timerType = timer.timerType;
this.timer.endAction = timer.endAction;
@@ -168,6 +169,8 @@ export class TimerService {
}
this.timer.clock = clock.timeNow();
this.timer.secondaryTimer = null;
this.secondaryTarget = null;
// add paused time if it exists
if (this.pausedTime) {
@@ -267,7 +270,7 @@ export class TimerService {
_finishAt:
this.timer.expectedFinish >= this.timer.startedAt
? this.timer.expectedFinish
: this.timer.expectedFinish + DAY_TO_MS,
: this.timer.expectedFinish + dayInMs,
clock: this.timer.clock,
secondaryTimer: this.timer.secondaryTimer,
@@ -382,16 +385,20 @@ export class TimerService {
this.timer.secondaryTimer = null;
this.secondaryTarget = null;
// account for event that finishes the day after
const endTime =
currentEvent.timeEnd < currentEvent.timeStart ? currentEvent.timeEnd + dayInMs : currentEvent.timeEnd;
// 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
this.load(currentEvent, {
startedAt: currentEvent.timeStart,
expectedFinish: currentEvent.timeEnd,
current: currentEvent.timeEnd - this.timer.clock,
current: endTime - this.timer.clock,
});
} else if (nextEvent) {
// account for day after
const nextStart = nextEvent.timeStart < this.timer.clock ? nextEvent.timeStart + DAY_TO_MS : nextEvent.timeStart;
const nextStart = nextEvent.timeStart < this.timer.clock ? nextEvent.timeStart + dayInMs : nextEvent.timeStart;
// nothing now, but something coming up
this.timer.secondaryTimer = nextStart - this.timer.clock;
this.secondaryTarget = nextStart;
@@ -1,10 +1,7 @@
import {
DAY_TO_MS,
getRollTimers,
normaliseEndTime,
sortArrayByProperty,
updateRoll,
} from '../rollUtils.ts';
import { OntimeEvent } from 'ontime-types';
import { dayInMs } from 'ontime-utils';
import { getRollTimers, normaliseEndTime, sortArrayByProperty, updateRoll } from '../rollUtils.js';
// test sortArrayByProperty()
describe('sort simple arrays of objects', () => {
@@ -43,51 +40,51 @@ describe('sort simple arrays of objects', () => {
// test getRollTimers()
describe('test that roll loads selection in right order', () => {
const eventlist = [
const eventlist: Partial<OntimeEvent>[] = [
{
id: 1,
id: '1',
timeStart: 5,
timeEnd: 10,
isPublic: false,
},
{
id: 2,
id: '2',
timeStart: 10,
timeEnd: 20,
isPublic: false,
},
{
id: 3,
id: '3',
timeStart: 20,
timeEnd: 30,
isPublic: false,
},
{
id: 4,
id: '4',
timeStart: 30,
timeEnd: 40,
isPublic: false,
},
{
id: 5,
id: '5',
timeStart: 40,
timeEnd: 50,
isPublic: true,
},
{
id: 6,
id: '6',
timeStart: 50,
timeEnd: 60,
isPublic: false,
},
{
id: 7,
id: '7',
timeStart: 60,
timeEnd: 70,
isPublic: true,
},
{
id: 8,
id: '8',
timeStart: 70,
timeEnd: 80,
isPublic: false,
@@ -109,7 +106,7 @@ describe('test that roll loads selection in right order', () => {
currentPublicEvent: null,
};
const state = getRollTimers(eventlist, now);
const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected);
});
@@ -128,7 +125,7 @@ describe('test that roll loads selection in right order', () => {
currentPublicEvent: null,
};
const state = getRollTimers(eventlist, now);
const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected);
});
@@ -147,7 +144,7 @@ describe('test that roll loads selection in right order', () => {
currentPublicEvent: null,
};
const state = getRollTimers(eventlist, now);
const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected);
});
@@ -166,7 +163,7 @@ describe('test that roll loads selection in right order', () => {
currentPublicEvent: null,
};
const state = getRollTimers(eventlist, now);
const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected);
});
@@ -185,7 +182,7 @@ describe('test that roll loads selection in right order', () => {
currentPublicEvent: eventlist[4],
};
const state = getRollTimers(eventlist, now);
const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected);
});
@@ -204,7 +201,7 @@ describe('test that roll loads selection in right order', () => {
currentPublicEvent: eventlist[6],
};
const state = getRollTimers(eventlist, now);
const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected);
});
@@ -223,11 +220,11 @@ describe('test that roll loads selection in right order', () => {
currentPublicEvent: eventlist[6],
};
const state = getRollTimers(eventlist, now);
const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected);
});
it('if timer is at 100', () => {
it('if timer is at 100 we roll to day after', () => {
const now = 100;
const expected = {
nowIndex: null,
@@ -235,21 +232,21 @@ describe('test that roll loads selection in right order', () => {
publicIndex: null,
nextIndex: 0,
publicNextIndex: 4,
timeToNext: DAY_TO_MS - now + eventlist[0].timeStart,
timeToNext: dayInMs - now + eventlist[0].timeStart,
nextEvent: eventlist[0],
nextPublicEvent: eventlist[4],
currentEvent: null,
currentPublicEvent: null,
};
const state = getRollTimers(eventlist, now);
const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected);
});
it('handles rolls to next day with real values', () => {
const singleEventList = [
const singleEventList: Partial<OntimeEvent>[] = [
{
id: 1,
id: '1',
timeStart: 36000000, // 10:00
timeEnd: 39600000, // 11:00
isPublic: true,
@@ -262,34 +259,111 @@ describe('test that roll loads selection in right order', () => {
publicIndex: null,
nextIndex: 0,
publicNextIndex: 0,
timeToNext: DAY_TO_MS - now + singleEventList[0].timeStart,
timeToNext: dayInMs - now + singleEventList[0].timeStart,
nextEvent: singleEventList[0],
nextPublicEvent: singleEventList[0],
currentEvent: null,
currentPublicEvent: null,
};
const state = getRollTimers(singleEventList, now);
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('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);
});
});
// test getRollTimers()
describe('test that roll behaviour with overlapping times', () => {
const eventlist = [
const eventlist: Partial<OntimeEvent>[] = [
{
id: 1,
id: '1',
timeStart: 10,
timeEnd: 10,
isPublic: false,
},
{
id: 2,
id: '2',
timeStart: 10,
timeEnd: 20,
isPublic: true,
},
{
id: 3,
id: '3',
timeStart: 10,
timeEnd: 30,
isPublic: false,
@@ -311,7 +385,7 @@ describe('test that roll behaviour with overlapping times', () => {
currentPublicEvent: null,
};
const state = getRollTimers(eventlist, now);
const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected);
});
@@ -330,7 +404,7 @@ describe('test that roll behaviour with overlapping times', () => {
currentPublicEvent: eventlist[1],
};
const state = getRollTimers(eventlist, now);
const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected);
});
@@ -349,7 +423,7 @@ describe('test that roll behaviour with overlapping times', () => {
currentPublicEvent: eventlist[1],
};
const state = getRollTimers(eventlist, now);
const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected);
});
@@ -368,7 +442,7 @@ describe('test that roll behaviour with overlapping times', () => {
currentPublicEvent: eventlist[1],
};
const state = getRollTimers(eventlist, now);
const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected);
});
@@ -387,7 +461,7 @@ describe('test that roll behaviour with overlapping times', () => {
currentPublicEvent: eventlist[1],
};
const state = getRollTimers(eventlist, now);
const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected);
});
});
@@ -396,9 +470,9 @@ describe('test that roll behaviour with overlapping times', () => {
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
const eventlist = [
const eventlist: Partial<OntimeEvent>[] = [
{
id: 1,
id: '1',
timeStart: 66000000, // 19:20
timeEnd: 54600000, // 16:10
isPublic: false,
@@ -406,7 +480,7 @@ describe('test that roll behaviour multi day event edge cases', () => {
];
const expected = {
nowIndex: 0,
nowId: 1,
nowId: '1',
publicIndex: null,
nextIndex: null,
publicNextIndex: null,
@@ -417,34 +491,39 @@ describe('test that roll behaviour multi day event edge cases', () => {
currentPublicEvent: null,
};
const state = getRollTimers(eventlist, now);
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 = [
const eventlist: Partial<OntimeEvent>[] = [
{
id: 1,
id: '1',
timeStart: 67200000, // 19:40
timeEnd: 66900000, // 19:35
isPublic: false,
},
];
const expected = {
nowIndex: null,
nowId: null,
publicIndex: null,
nextIndex: 0,
publicNextIndex: null,
timeToNext: eventlist[0].timeStart - now,
nextEvent: eventlist[0],
nextPublicEvent: null,
currentEvent: null,
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, now);
const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected);
});
});
@@ -460,10 +539,10 @@ test('test typical scenarios', () => {
expect(normaliseEndTime(t1.start, t1.end)).toBe(t1_expected);
const t2 = {
start: 10 + DAY_TO_MS,
start: 10 + dayInMs,
end: 20,
};
const t2_expected = 20 + DAY_TO_MS;
const t2_expected = 20 + dayInMs;
expect(normaliseEndTime(t2.start, t2.end)).toBe(t2_expected);
@@ -480,7 +559,7 @@ test('test typical scenarios', () => {
describe('typical scenarios', () => {
it('it updates running events correctly', () => {
const timers = {
selectedEventId: 1,
selectedEventId: '1',
current: 10,
_finishAt: 15,
clock: 11,
@@ -527,7 +606,7 @@ describe('typical scenarios', () => {
it('flags an event end', () => {
const timers = {
selectedEventId: 1,
selectedEventId: '1',
current: 10,
_finishAt: 11,
clock: 12,
@@ -584,4 +663,44 @@ describe('typical scenarios', () => {
expect(updateRoll(timers)).toStrictEqual(expected);
});
it('counts over midnight', () => {
const timers = {
selectedEventId: '1',
current: 25,
_finishAt: 10 + dayInMs,
clock: dayInMs - 10,
secondaryTimer: null,
secondaryTarget: null,
};
const expected = {
updatedTimer: 20,
updatedSecondaryTimer: null,
doRollLoad: false,
isFinished: false,
};
expect(updateRoll(timers)).toStrictEqual(expected);
});
it('rolls over midnight', () => {
const timers = {
selectedEventId: '1',
current: dayInMs,
_finishAt: 10 + dayInMs,
clock: 10,
secondaryTimer: null,
secondaryTarget: null,
};
const expected = {
updatedTimer: dayInMs,
updatedSecondaryTimer: null,
doRollLoad: false,
isFinished: false,
};
expect(updateRoll(timers)).toStrictEqual(expected);
});
});
@@ -1,3 +1,5 @@
import { dayInMs } from 'ontime-utils';
import { getCurrent, getElapsed, getExpectedFinish } from '../timerUtils.js';
describe('getExpectedFinish()', () => {
@@ -64,6 +66,15 @@ describe('getExpectedFinish()', () => {
const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
expect(calculatedFinish).toBe(1);
});
it('finish can be the day after', () => {
const startedAt = 10;
const finishedAt = null;
const duration = dayInMs;
const pausedTime = 0;
const addedTime = 0;
const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
expect(calculatedFinish).toBe(10);
});
});
describe('getCurrent()', () => {
@@ -94,6 +105,33 @@ describe('getCurrent()', () => {
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
expect(current).toBe(19);
});
it('counts over midnight', () => {
const startedAt = 10;
const duration = dayInMs + 10;
const pausedTime = 0;
const addedTime = 0;
const clock = 10;
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
expect(current).toBe(dayInMs + 10);
});
it('rolls over midnight', () => {
const startedAt = 10;
const duration = dayInMs + 10;
const pausedTime = 0;
const addedTime = 0;
const clock = 5;
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
expect(current).toBe(15);
});
it('midnight holds delays', () => {
const startedAt = 10;
const duration = dayInMs + 10;
const pausedTime = 10;
const addedTime = 10;
const clock = 5;
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
expect(current).toBe(35);
});
});
describe('getElapsedTime()', () => {
@@ -103,10 +141,12 @@ describe('getElapsedTime()', () => {
const elapsed = getElapsed(startedAt, clock);
expect(elapsed).toBe(5);
});
it('clock cannot be lower than started time', () => {
it('rolls past midnight', () => {
const startedAt = 10;
const clock = 5;
expect(() => getElapsed(startedAt, clock)).toThrow();
const elapsed = getElapsed(startedAt, clock);
expect(elapsed).toBe(dayInMs - startedAt + clock);
});
});
+12 -15
View File
@@ -1,14 +1,10 @@
import { OntimeEvent } from 'ontime-types';
/**
* Utility variable: 24 hour in milliseconds .
*/
export const DAY_TO_MS = 86400000;
import { dayInMs } from 'ontime-utils';
/**
* handle events that span over midnight
*/
export const normaliseEndTime = (start: number, end: number) => (end < start ? end + DAY_TO_MS : end);
export const normaliseEndTime = (start: number, end: number) => (end < start ? end + dayInMs : end);
/**
* @description Sorts an array of objects by given property
@@ -23,13 +19,6 @@ export const sortArrayByProperty = <T>(arr: T[], property: string): T[] => {
});
};
type Timer = {
_startedAt: number;
_finishAt: number;
duration: number;
current: number;
};
/**
* Finds loading information given a current rundown and time
* @param {OntimeEvent[]} rundown - List of playable events
@@ -61,7 +50,7 @@ export const getRollTimers = (rundown: OntimeEvent[], timeNow: number) => {
const firstEvent = orderedEvents[0];
nextIndex = 0;
nextEvent = firstEvent;
timeToNext = firstEvent.timeStart + DAY_TO_MS - timeNow;
timeToNext = firstEvent.timeStart + dayInMs - timeNow;
if (firstEvent.isPublic) {
nextPublicEvent = firstEvent;
@@ -89,6 +78,10 @@ export const getRollTimers = (rundown: OntimeEvent[], timeNow: number) => {
// 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
@@ -98,7 +91,7 @@ export const getRollTimers = (rundown: OntimeEvent[], timeNow: number) => {
currentPublicEvent = event;
publicIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
}
} else if (normalEnd > timeNow && timeNow >= event.timeStart && !nowFound) {
} else if (hasNotEnded && hasStarted && !nowFound) {
// event is running
currentEvent = event;
nowIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
@@ -185,6 +178,10 @@ export const updateRoll = (currentTimers: CurrentTimers) => {
// if we have something selected and a timer, we are running
updatedTimer = _finishAt - clock;
if (updatedTimer > dayInMs) {
updatedTimer -= dayInMs;
}
if (updatedTimer < 0) {
isPrimaryFinished = true;
// we need a new event
@@ -5,26 +5,20 @@ import {
OntimeDelay,
OntimeEvent,
OntimeRundown,
Playback,
SupportedEvent,
} from 'ontime-types';
import { generateId } from 'ontime-utils';
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
import { block as blockDef, delay, delay as delayDef, event as eventDef } from '../../models/eventsDefinition.js';
import { block as blockDef, delay as delayDef, event as eventDef } from '../../models/eventsDefinition.js';
import { MAX_EVENTS } from '../../settings.js';
import { EventLoader, eventLoader } from '../../classes/event-loader/EventLoader.js';
import { eventTimer } from '../TimerService.js';
import { sendRefetch } from '../../adapters/websocketAux.js';
import { runtimeCacheStore } from '../../stores/cachingStore.js';
import {
cachedAdd,
cachedDelete,
cachedEdit,
cachedReorder,
calculateRuntimeDelaysFrom,
delayedRundownCacheKey,
getDelayedRundown,
} from './delayedRundown.utils.js';
import { cachedAdd, cachedDelete, cachedEdit, cachedReorder, delayedRundownCacheKey } from './delayedRundown.utils.js';
import { logger } from '../../classes/Logger.js';
import { clock } from '../Clock.js';
/**
* Forces rundown to be recalculated
@@ -97,8 +91,9 @@ const isNewNext = () => {
*/
export function updateTimer(affectedIds?: string[]) {
const runningEventId = eventLoader.loaded.selectedEventId;
const nextEventId = eventLoader.loaded.nextEventId;
if (runningEventId === null) {
if (runningEventId === null && nextEventId === null) {
return false;
}
@@ -119,11 +114,22 @@ export function updateTimer(affectedIds?: string[]) {
if (eventInMemory) {
eventLoader.reset();
const { loadedEvent } = eventLoader.loadById(runningEventId) || {};
if (!loadedEvent) {
eventTimer.stop();
if (eventTimer.playback === Playback.Roll) {
const rollTimers = eventLoader.findRoll(clock.timeNow());
if (rollTimers === null) {
eventTimer.stop();
} else {
const { currentEvent, nextEvent } = rollTimers;
eventTimer.roll(currentEvent, nextEvent);
}
} else {
eventTimer.hotReload(loadedEvent);
const { loadedEvent } = eventLoader.loadById(runningEventId) || {};
if (loadedEvent) {
eventTimer.hotReload(loadedEvent);
} else {
eventTimer.stop();
}
}
return true;
}
+20 -4
View File
@@ -1,4 +1,5 @@
type MaybeNumber = number | null;
import { MaybeNumber } from 'ontime-types';
import { dayInMs } from 'ontime-utils';
/**
* Calculates expected finish time of a running timer
@@ -18,7 +19,14 @@ export function getExpectedFinish(
return finishedAt;
}
return Math.max(startedAt + duration + pausedTime + addedTime, startedAt);
// handle events that finish the day after
const expectedFinish = startedAt + duration + pausedTime + addedTime;
if (expectedFinish > dayInMs) {
return expectedFinish - dayInMs;
}
// an event cannot finish before it started (user added too much negative time)
return Math.max(expectedFinish, startedAt);
}
/**
@@ -34,15 +42,23 @@ export function getCurrent(
if (startedAt === null) {
return null;
}
if (startedAt > clock) {
return startedAt + duration + addedTime + pausedTime - clock - dayInMs;
}
return startedAt + duration + addedTime + pausedTime - clock;
}
/**
* Calculates elapsed time
*/
export function getElapsed(startedAt: number, clock: number) {
export function getElapsed(startedAt: number | null, clock: number): number | null {
if (startedAt === null) {
return null;
}
// we are in the day after
if (startedAt > clock) {
throw new Error('clock cannot be higher than startedAt');
return dayInMs - startedAt + clock;
}
return clock - startedAt;
}
+2 -32
View File
@@ -1,9 +1,10 @@
import { vi } from 'vitest';
import { dbModel } from '../../models/dataModel.ts';
import { parseExcel, parseJson, validateEvent } from '../parser.ts';
import { makeString, validateDuration } from '../parserUtils.js';
import { makeString } from '../parserUtils.ts';
import { parseAliases, parseUserFields, parseViewSettings } from '../parserFunctions.ts';
import { EndAction, TimerType } from 'ontime-types';
import { dayInMs } from 'ontime-utils';
describe('test json parser with valid def', () => {
const testData = {
@@ -898,34 +899,3 @@ describe('test views import', () => {
expect(parsed).toStrictEqual(expectedParsedViewSettings);
});
});
describe('test validateDuration()', () => {
describe('handles valid inputs', () => {
const valid = [
{ test: 'zero values', timeStart: 0, timeEnd: 0 },
{ test: 'end after start', timeStart: 0, timeEnd: 1 },
];
valid.forEach((t) => {
it(t.test, () => {
const d = validateDuration(t.timeStart, t.timeEnd);
expect(d).toBe(t.timeEnd - t.timeStart);
});
});
});
describe('handles edge cases', () => {
// edge cases
const testData = [
{ test: 'negative 0', timeStart: -0, timeEnd: -0, expected: 0 },
{ test: 'end before start', timeStart: 2, timeEnd: 1, expected: 0 },
];
testData.forEach((t) => {
it(t.test, () => {
const d = validateDuration(t.timeStart, t.timeEnd);
expect(d).toBe(t.expected);
});
});
});
});
@@ -5,9 +5,6 @@ describe('isEmptyObject()', () => {
const isEmpty = isEmptyObject({});
expect(isEmpty).toBe(true);
});
test('throws on other types', () => {
expect(() => isEmptyObject(12)).toThrow();
});
test('resolves an object with methods', () => {
const isEmpty = isEmptyObject({ test: 'yes' });
expect(isEmpty).toBe(false);
+3 -3
View File
@@ -3,11 +3,11 @@
import fs from 'fs';
import xlsx from 'node-xlsx';
import { generateId } from 'ontime-utils';
import { generateId, calculateDuration } from 'ontime-utils';
import { DatabaseModel, EventData, OntimeEvent, OntimeRundown, UserFields } from 'ontime-types';
import { event as eventDef } from '../models/eventsDefinition.js';
import { dbModel } from '../models/dataModel.js';
import { deleteFile, makeString, validateDuration } from './parserUtils.js';
import { deleteFile, makeString } from './parserUtils.js';
import {
parseAliases,
parseEventData,
@@ -322,7 +322,7 @@ export const validateEvent = (eventArgs) => {
timeEnd: end,
endAction: makeString(e.endAction, d.endAction),
timerType: makeString(e.timerType, d.timerType),
duration: validateDuration(start, end),
duration: calculateDuration(start, end),
isPublic: typeof e.isPublic === 'boolean' ? e.isPublic : d.isPublic,
skip: typeof e.skip === 'boolean' ? e.skip : d.skip,
note: makeString(e.note, d.note),
@@ -1,4 +1,5 @@
import fs from 'fs';
import { dayInMs } from 'ontime-utils';
/**
* @description Ensures variable is string, it skips object types
@@ -6,23 +7,12 @@ import fs from 'fs';
* @param {string} [fallback=''] - fallback value
* @returns {string} - value as string or fallback if not possible
*/
export const makeString = (val, fallback = '') => {
export const makeString = (val: any, fallback = ''): string => {
if (typeof val === 'string') return val;
else if (val == null || val.constructor === Object) return fallback;
return val.toString();
};
/**
* @description validates a duration value against options
* @param {number} timeStart
* @param {number} timeEnd
* @returns {number}
*/
export const validateDuration = (timeStart, timeEnd) => {
// Durations must be positive
return Math.max(timeEnd - timeStart, 0);
};
/**
* @description Delete file from system
* @param {string} file - reference to file
@@ -54,7 +44,7 @@ export const validateFile = (file) => {
* @description Verifies if object is empty
* @param {object} obj
*/
export const isEmptyObject = (obj) => {
export const isEmptyObject = (obj: object) => {
if (typeof obj === 'object' && obj !== null && !Array.isArray(obj)) {
return Object.keys(obj).length === 0;
}
@@ -78,7 +68,7 @@ export const mergeObject = (a, b) => {
* @description Removes undefined
* @param {object} obj
*/
export const removeUndefined = (obj) => {
export const removeUndefined = (obj: object) => {
const patched = {};
Object.keys({ ...obj })
.filter((key) => typeof obj[key] !== 'undefined')
-1
View File
@@ -6,7 +6,6 @@ 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 Converts an excel date to milliseconds