From 023aac4eadd907b2cd90ab3e994f9d600b06b603 Mon Sep 17 00:00:00 2001
From: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
Date: Sat, 12 Aug 2023 10:42:43 +0200
Subject: [PATCH] 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
---
.../utils/__tests__/timesManager.test.ts | 27 --
apps/client/src/common/utils/timeConstants.js | 7 -
apps/client/src/common/utils/timesManager.ts | 11 -
.../composite/EventEditorTimes.tsx | 4 +-
.../src/features/rundown/RundownEntry.tsx | 2 +-
.../composite/EventBlockProgressBar.tsx | 28 ++-
.../composite/EventBlockTimers.tsx | 4 +-
.../__tests__/EventBlockProgressBar.test.ts | 27 ++
.../countdown/__tests__/Countdown.test.js | 13 +-
.../src/classes/event-loader/EventLoader.ts | 138 ++++++----
apps/server/src/services/PlaybackService.ts | 2 +-
apps/server/src/services/TimerService.ts | 23 +-
.../{rollUtils.test.js => rollUtils.test.ts} | 237 +++++++++++++-----
.../src/services/__tests__/timerUtils.test.ts | 44 +++-
apps/server/src/services/rollUtils.ts | 27 +-
.../rundown-service/RundownService.ts | 36 +--
apps/server/src/services/timerUtils.ts | 24 +-
.../server/src/utils/__tests__/parser.test.js | 34 +--
...rserUtils.tests.js => parserUtils.test.ts} | 3 -
apps/server/src/utils/parser.ts | 6 +-
.../utils/{parserUtils.js => parserUtils.ts} | 18 +-
apps/server/src/utils/time.ts | 1 -
packages/types/src/index.ts | 4 +
packages/types/src/utils/utils.type.ts | 1 +
packages/utils/index.ts | 13 +-
.../src/rundown-utils/rundownUtils.test.ts | 32 +++
.../utils/src/rundown-utils/rundownUtils.ts | 15 ++
packages/utils/src/timeConstants.ts | 10 +-
28 files changed, 520 insertions(+), 271 deletions(-)
delete mode 100644 apps/client/src/common/utils/__tests__/timesManager.test.ts
create mode 100644 apps/client/src/features/rundown/event-block/composite/__tests__/EventBlockProgressBar.test.ts
rename apps/server/src/services/__tests__/{rollUtils.test.js => rollUtils.test.ts} (69%)
rename apps/server/src/utils/__tests__/{parserUtils.tests.js => parserUtils.test.ts} (95%)
rename apps/server/src/utils/{parserUtils.js => parserUtils.ts} (80%)
create mode 100644 packages/types/src/utils/utils.type.ts
create mode 100644 packages/utils/src/rundown-utils/rundownUtils.test.ts
create mode 100644 packages/utils/src/rundown-utils/rundownUtils.ts
diff --git a/apps/client/src/common/utils/__tests__/timesManager.test.ts b/apps/client/src/common/utils/__tests__/timesManager.test.ts
deleted file mode 100644
index d8ac400e7..000000000
--- a/apps/client/src/common/utils/__tests__/timesManager.test.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { calculateDuration, DAY_TO_MS } from '../timesManager';
-
-describe('calculateDuration()', () => {
- describe('Given start and end values', () => {
- it('calculates duration correctly', () => {
- const testStart = 1;
- const testEnd = 2;
- const val = calculateDuration(testStart, testEnd);
- expect(val).toBe(testEnd - testStart);
- });
- });
-
- describe('Handles edge cases', () => {
- it('when start is after end', () => {
- const testStart = 3;
- const testEnd = 2;
- const val = calculateDuration(testStart, testEnd);
- expect(val).toBe(testEnd + DAY_TO_MS - testStart);
- });
- it('when both are equal', () => {
- const testStart = 1;
- const testEnd = 1;
- const val = calculateDuration(testStart, testEnd);
- expect(val).toBe(testEnd - testStart);
- });
- });
-});
diff --git a/apps/client/src/common/utils/timeConstants.js b/apps/client/src/common/utils/timeConstants.js
index 39a5c34aa..10ed97a8e 100644
--- a/apps/client/src/common/utils/timeConstants.js
+++ b/apps/client/src/common/utils/timeConstants.js
@@ -10,15 +10,8 @@ export const mts = 1000;
*/
export const mtm = 1000 * 60;
-
/**
* millis to hours
* @type {number}
*/
export const mth = 1000 * 60 * 60;
-
-/**
- * milliseconds in a day
- * @type {number}
- */
-export const DAY_TO_MS = 86400000;
diff --git a/apps/client/src/common/utils/timesManager.ts b/apps/client/src/common/utils/timesManager.ts
index 435fdf1ab..4c17af915 100644
--- a/apps/client/src/common/utils/timesManager.ts
+++ b/apps/client/src/common/utils/timesManager.ts
@@ -1,16 +1,5 @@
export type TimeEntryField = 'timeStart' | 'timeEnd' | 'durationOverride';
-/**
- * @description Milliseconds in a day
- */
-export const DAY_TO_MS = 86400000;
-
-/**
- * @description calculates duration from given values
- */
-export const calculateDuration = (start: number, end: number): number =>
- start > end ? end + DAY_TO_MS - start : end - start;
-
/**
* @description Checks which field the value relates to
*/
diff --git a/apps/client/src/features/event-editor/composite/EventEditorTimes.tsx b/apps/client/src/features/event-editor/composite/EventEditorTimes.tsx
index 21021b99b..b568d5166 100644
--- a/apps/client/src/features/event-editor/composite/EventEditorTimes.tsx
+++ b/apps/client/src/features/event-editor/composite/EventEditorTimes.tsx
@@ -1,13 +1,13 @@
import { memo, useState } from 'react';
import { Select, Switch } from '@chakra-ui/react';
import { EndAction, OntimeEvent, TimerType } from 'ontime-types';
-import { millisToString } from 'ontime-utils';
+import { calculateDuration, millisToString } from 'ontime-utils';
import TimeInput from '../../../common/components/input/time-input/TimeInput';
import { useEventAction } from '../../../common/hooks/useEventAction';
import { millisToDelayString } from '../../../common/utils/dateConfig';
import { cx } from '../../../common/utils/styleUtils';
-import { calculateDuration, TimeEntryField, validateEntry } from '../../../common/utils/timesManager';
+import { TimeEntryField, validateEntry } from '../../../common/utils/timesManager';
import style from '../EventEditor.module.scss';
diff --git a/apps/client/src/features/rundown/RundownEntry.tsx b/apps/client/src/features/rundown/RundownEntry.tsx
index c727646a0..095b20998 100644
--- a/apps/client/src/features/rundown/RundownEntry.tsx
+++ b/apps/client/src/features/rundown/RundownEntry.tsx
@@ -1,12 +1,12 @@
import { useCallback } from 'react';
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
+import { calculateDuration } from 'ontime-utils';
import { useEventAction } from '../../common/hooks/useEventAction';
import { useAppMode } from '../../common/stores/appModeStore';
import { useEditorSettings } from '../../common/stores/editorSettings';
import { useEmitLog } from '../../common/stores/logger';
import { cloneEvent } from '../../common/utils/eventsManager';
-import { calculateDuration } from '../../common/utils/timesManager';
import BlockBlock from './block-block/BlockBlock';
import DelayBlock from './delay-block/DelayBlock';
diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockProgressBar.tsx b/apps/client/src/features/rundown/event-block/composite/EventBlockProgressBar.tsx
index 10d6504b5..91d1dc7d8 100644
--- a/apps/client/src/features/rundown/event-block/composite/EventBlockProgressBar.tsx
+++ b/apps/client/src/features/rundown/event-block/composite/EventBlockProgressBar.tsx
@@ -1,4 +1,4 @@
-import { Playback } from 'ontime-types';
+import { MaybeNumber, Playback } from 'ontime-types';
import { useTimer } from '../../../../common/hooks/useSocket';
import { clamp } from '../../../../common/utils/math';
@@ -9,18 +9,26 @@ interface EventBlockProgressBarProps {
playback?: Playback;
}
+export function getPercentComplete(remaining: MaybeNumber, total: MaybeNumber): number {
+ if (remaining === null || total === null) {
+ return 0;
+ }
+
+ if (remaining <= 0) {
+ return 100;
+ }
+
+ if (remaining === total) {
+ return 0;
+ }
+
+ return clamp(100 - (remaining * 100) / total, 0, 100);
+}
+
export default function EventBlockProgressBar(props: EventBlockProgressBarProps) {
const { playback } = props;
const timer = useTimer();
- const now = Math.floor(Math.max((timer?.current ?? 1) / 1000, 0));
- const complete = (timer?.duration ?? 1) / 1000;
- const elapsed = clamp(100 - (now * 100) / complete, 0, 100);
- const progress = `${elapsed}%`;
-
- if ((timer?.current ?? 0) < 0) {
- return
;
- }
-
+ const progress = `${getPercentComplete(timer.current, timer.duration)}%`;
return ;
}
diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.tsx b/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.tsx
index 81ac70f97..4a4fc648a 100644
--- a/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.tsx
+++ b/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.tsx
@@ -1,11 +1,11 @@
import { memo, useCallback, useState } from 'react';
import { OntimeEvent } from 'ontime-types';
-import { millisToString } from 'ontime-utils';
+import { calculateDuration, millisToString } from 'ontime-utils';
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
import { useEventAction } from '../../../../common/hooks/useEventAction';
import { millisToDelayString } from '../../../../common/utils/dateConfig';
-import { calculateDuration, TimeEntryField, validateEntry } from '../../../../common/utils/timesManager';
+import { TimeEntryField, validateEntry } from '../../../../common/utils/timesManager';
import style from '../EventBlock.module.scss';
diff --git a/apps/client/src/features/rundown/event-block/composite/__tests__/EventBlockProgressBar.test.ts b/apps/client/src/features/rundown/event-block/composite/__tests__/EventBlockProgressBar.test.ts
new file mode 100644
index 000000000..cf07ab972
--- /dev/null
+++ b/apps/client/src/features/rundown/event-block/composite/__tests__/EventBlockProgressBar.test.ts
@@ -0,0 +1,27 @@
+import { dayInMs } from 'ontime-utils';
+
+import { getPercentComplete } from '../EventBlockProgressBar';
+
+describe('getPercentComplete()', () => {
+ describe('calculates progress in normal cases', () => {
+ const testScenarios = [
+ { current: 0, duration: 0, expect: 100 },
+ { current: 0, duration: 100, expect: 100 },
+ { current: 0, duration: dayInMs, expect: 100 },
+ { current: 10, duration: 100, expect: 90 },
+ { current: 50, duration: 100, expect: 50 },
+ { current: 100, duration: 100, expect: 0 },
+ ];
+
+ testScenarios.forEach((testCase) => {
+ it(`handles ${testCase.current} / ${testCase.duration}`, () => {
+ const progress = getPercentComplete(testCase.current, testCase.duration);
+ expect(progress).toBe(testCase.expect);
+ });
+ });
+ });
+ it('is 0 if we dont have a current or duration', () => {
+ const progress = getPercentComplete(null, null);
+ expect(progress).toBe(0);
+ });
+});
diff --git a/apps/client/src/features/viewers/countdown/__tests__/Countdown.test.js b/apps/client/src/features/viewers/countdown/__tests__/Countdown.test.js
index fdf64106e..b87f3fed4 100644
--- a/apps/client/src/features/viewers/countdown/__tests__/Countdown.test.js
+++ b/apps/client/src/features/viewers/countdown/__tests__/Countdown.test.js
@@ -1,4 +1,5 @@
-import { DAY_TO_MS } from '../../../../common/utils/timeConstants';
+import { dayInMs } from 'ontime-utils';
+
import { fetchTimerData, sanitiseTitle, TimerMessage } from '../countdown.helpers';
describe('sanitiseTitle() function', () => {
@@ -73,11 +74,11 @@ describe('fetchTimerData() function', () => {
const timeNow = 15000;
const followId = 'testId';
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
- const time = { clock: timeNow, current: DAY_TO_MS + endMockValue - startMockValue };
+ const time = { clock: timeNow, current: dayInMs + endMockValue - startMockValue };
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
expect(message).toBe(TimerMessage.waiting);
- expect(timer).toBe(DAY_TO_MS + endMockValue - startMockValue);
+ expect(timer).toBe(dayInMs + endMockValue - startMockValue);
});
it('handle an current event that finishes after midnight', () => {
@@ -86,11 +87,11 @@ describe('fetchTimerData() function', () => {
const timeNow = 15000;
const followId = 'testId';
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
- const time = { clock: timeNow, current: DAY_TO_MS + endMockValue - startMockValue };
+ const time = { clock: timeNow, current: dayInMs + endMockValue - startMockValue };
const { message, timer } = fetchTimerData(time, follow, followId);
expect(message).toBe(TimerMessage.running);
- expect(timer).toBe(DAY_TO_MS + endMockValue - startMockValue);
+ expect(timer).toBe(dayInMs + endMockValue - startMockValue);
});
it('handle an event that finishes after midnight but hasnt started', () => {
@@ -99,7 +100,7 @@ describe('fetchTimerData() function', () => {
const timeNow = 2000;
const followId = 'testId';
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
- const time = { clock: timeNow, current: DAY_TO_MS + endMockValue - startMockValue };
+ const time = { clock: timeNow, current: dayInMs + endMockValue - startMockValue };
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
expect(message).toBe(TimerMessage.toStart);
diff --git a/apps/server/src/classes/event-loader/EventLoader.ts b/apps/server/src/classes/event-loader/EventLoader.ts
index e2821d443..4fd9d77eb 100644
--- a/apps/server/src/classes/event-loader/EventLoader.ts
+++ b/apps/server/src/classes/event-loader/EventLoader.ts
@@ -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}`);
}
}
}
diff --git a/apps/server/src/services/PlaybackService.ts b/apps/server/src/services/PlaybackService.ts
index c463e5cce..cb099c0c7 100644
--- a/apps/server/src/services/PlaybackService.ts
+++ b/apps/server/src/services/PlaybackService.ts
@@ -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';
diff --git a/apps/server/src/services/TimerService.ts b/apps/server/src/services/TimerService.ts
index 8831692fb..ce76befab 100644
--- a/apps/server/src/services/TimerService.ts
+++ b/apps/server/src/services/TimerService.ts
@@ -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;
diff --git a/apps/server/src/services/__tests__/rollUtils.test.js b/apps/server/src/services/__tests__/rollUtils.test.ts
similarity index 69%
rename from apps/server/src/services/__tests__/rollUtils.test.js
rename to apps/server/src/services/__tests__/rollUtils.test.ts
index 498dde448..329ece996 100644
--- a/apps/server/src/services/__tests__/rollUtils.test.js
+++ b/apps/server/src/services/__tests__/rollUtils.test.ts
@@ -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[] = [
{
- 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[] = [
{
- 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[] = [
+ {
+ 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[] = [
+ {
+ 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[] = [
+ {
+ 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[] = [
{
- 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[] = [
{
- 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[] = [
{
- 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);
+ });
});
diff --git a/apps/server/src/services/__tests__/timerUtils.test.ts b/apps/server/src/services/__tests__/timerUtils.test.ts
index 99127a555..30d3b946d 100644
--- a/apps/server/src/services/__tests__/timerUtils.test.ts
+++ b/apps/server/src/services/__tests__/timerUtils.test.ts
@@ -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);
});
});
diff --git a/apps/server/src/services/rollUtils.ts b/apps/server/src/services/rollUtils.ts
index ef2562898..98a6feac2 100644
--- a/apps/server/src/services/rollUtils.ts
+++ b/apps/server/src/services/rollUtils.ts
@@ -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 = (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
diff --git a/apps/server/src/services/rundown-service/RundownService.ts b/apps/server/src/services/rundown-service/RundownService.ts
index f1db79314..e75244919 100644
--- a/apps/server/src/services/rundown-service/RundownService.ts
+++ b/apps/server/src/services/rundown-service/RundownService.ts
@@ -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;
}
diff --git a/apps/server/src/services/timerUtils.ts b/apps/server/src/services/timerUtils.ts
index 720045aae..619eae5bb 100644
--- a/apps/server/src/services/timerUtils.ts
+++ b/apps/server/src/services/timerUtils.ts
@@ -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;
}
diff --git a/apps/server/src/utils/__tests__/parser.test.js b/apps/server/src/utils/__tests__/parser.test.js
index 16ae1d4aa..65ab9a783 100644
--- a/apps/server/src/utils/__tests__/parser.test.js
+++ b/apps/server/src/utils/__tests__/parser.test.js
@@ -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);
- });
- });
- });
-});
diff --git a/apps/server/src/utils/__tests__/parserUtils.tests.js b/apps/server/src/utils/__tests__/parserUtils.test.ts
similarity index 95%
rename from apps/server/src/utils/__tests__/parserUtils.tests.js
rename to apps/server/src/utils/__tests__/parserUtils.test.ts
index 6bef1ca22..0b348a083 100644
--- a/apps/server/src/utils/__tests__/parserUtils.tests.js
+++ b/apps/server/src/utils/__tests__/parserUtils.test.ts
@@ -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);
diff --git a/apps/server/src/utils/parser.ts b/apps/server/src/utils/parser.ts
index 9848226fa..f7201ce72 100644
--- a/apps/server/src/utils/parser.ts
+++ b/apps/server/src/utils/parser.ts
@@ -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),
diff --git a/apps/server/src/utils/parserUtils.js b/apps/server/src/utils/parserUtils.ts
similarity index 80%
rename from apps/server/src/utils/parserUtils.js
rename to apps/server/src/utils/parserUtils.ts
index a66d17916..3db806970 100644
--- a/apps/server/src/utils/parserUtils.js
+++ b/apps/server/src/utils/parserUtils.ts
@@ -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')
diff --git a/apps/server/src/utils/time.ts b/apps/server/src/utils/time.ts
index ef761464e..edac0e539 100644
--- a/apps/server/src/utils/time.ts
+++ b/apps/server/src/utils/time.ts
@@ -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
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index 8dd8526c4..b78d6629c 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -23,6 +23,7 @@ import { TimerType } from './definitions/TimerType.type.js';
import { TitleBlock } from './definitions/runtime/TitleBlock.type.js';
import { UserFields } from './definitions/core/UserFields.type.js';
import { ViewSettings } from './definitions/core/Views.type.js';
+import { MaybeNumber } from './utils/utils.type.js';
// DATA MODEL
export type { DatabaseModel };
@@ -69,3 +70,6 @@ export type { TimerState };
export type { TitleBlock };
// CLIENT
+
+// UTILITIES
+export type { MaybeNumber };
diff --git a/packages/types/src/utils/utils.type.ts b/packages/types/src/utils/utils.type.ts
new file mode 100644
index 000000000..1a22c2055
--- /dev/null
+++ b/packages/types/src/utils/utils.type.ts
@@ -0,0 +1 @@
+export type MaybeNumber = number | null;
diff --git a/packages/utils/index.ts b/packages/utils/index.ts
index 6fceea01a..358d33256 100644
--- a/packages/utils/index.ts
+++ b/packages/utils/index.ts
@@ -1,6 +1,15 @@
+// runtime utils
+export { validatePlayback } from './src/validate-action/validatePlayback.js';
+
+// rundown utils
+export { generateId } from './src/generate-id/generateId.js';
+export { calculateDuration } from './src/rundown-utils/rundownUtils.js';
+
+// format utils
export { formatDisplay } from './src/date-utils/formatDisplay.js';
export { formatFromMillis } from './src/date-utils/formatFromMillis.js';
export { isTimeString } from './src/date-utils/isTimeString.js';
export { millisToString } from './src/date-utils/millisToString.js';
-export { generateId } from './src/generate-id/generateId.js';
-export { validatePlayback } from './src/validate-action/validatePlayback.js';
+
+// time utils
+export { dayInMs, mts } from './src/timeConstants.js';
diff --git a/packages/utils/src/rundown-utils/rundownUtils.test.ts b/packages/utils/src/rundown-utils/rundownUtils.test.ts
new file mode 100644
index 000000000..88fed6ca6
--- /dev/null
+++ b/packages/utils/src/rundown-utils/rundownUtils.test.ts
@@ -0,0 +1,32 @@
+import { dayInMs } from '../timeConstants.js';
+import { calculateDuration } from './rundownUtils.js';
+
+describe('calculateDuration()', () => {
+ describe('Given start and end values', () => {
+ it('is the difference between end and start', () => {
+ const duration = calculateDuration(10, 20);
+ expect(duration).toBe(10);
+ });
+ });
+
+ describe('Handles edge cases', () => {
+ it('handles events that go over midnight', () => {
+ const duration = calculateDuration(51, 50);
+ expect(duration).not.toBe(-50);
+ expect(duration).toBe(dayInMs - 1);
+ });
+ it('when both are equal', () => {
+ const testStart = 1;
+ const testEnd = 1;
+ const val = calculateDuration(testStart, testEnd);
+ expect(val).toBe(testEnd - testStart);
+ });
+
+ it('handles no difference', () => {
+ const duration1 = calculateDuration(0, 0);
+ const duration2 = calculateDuration(dayInMs, dayInMs);
+ expect(duration1).toBe(0);
+ expect(duration2).toBe(0);
+ });
+ });
+});
diff --git a/packages/utils/src/rundown-utils/rundownUtils.ts b/packages/utils/src/rundown-utils/rundownUtils.ts
new file mode 100644
index 000000000..b5e101dfa
--- /dev/null
+++ b/packages/utils/src/rundown-utils/rundownUtils.ts
@@ -0,0 +1,15 @@
+import { dayInMs } from '../timeConstants.js';
+
+/**
+ * @description calculates event duration considering midnight
+ * @param {number} timeStart
+ * @param {number} timeEnd
+ * @returns {number}
+ */
+export const calculateDuration = (timeStart: number, timeEnd: number): number => {
+ // Durations must be positive
+ if (timeEnd < timeStart) {
+ return timeEnd + dayInMs - timeStart;
+ }
+ return timeEnd - timeStart;
+};
diff --git a/packages/utils/src/timeConstants.ts b/packages/utils/src/timeConstants.ts
index 3afa656a7..e3e7e2f47 100644
--- a/packages/utils/src/timeConstants.ts
+++ b/packages/utils/src/timeConstants.ts
@@ -1 +1,9 @@
-export const mts = 1000; // millis to seconds
+/**
+ * Milliseconds in a second
+ */
+export const mts = 1000;
+
+/**
+ * Milliseconds in a day
+ */
+export const dayInMs = 86400000;