Skip to content

Commit

Permalink
fix: midnight roll (#484)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
cpvalente authored Aug 12, 2023
1 parent 8c9cb90 commit 023aac4
Show file tree
Hide file tree
Showing 28 changed files with 499 additions and 250 deletions.
27 changes: 0 additions & 27 deletions apps/client/src/common/utils/__tests__/timesManager.test.ts

This file was deleted.

7 changes: 0 additions & 7 deletions apps/client/src/common/utils/timeConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
11 changes: 0 additions & 11 deletions apps/client/src/common/utils/timesManager.ts
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
2 changes: 1 addition & 1 deletion apps/client/src/features/rundown/RundownEntry.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -9,18 +9,26 @@ interface EventBlockProgressBarProps {
playback?: Playback;
}

export default function EventBlockProgressBar(props: EventBlockProgressBarProps) {
const { playback } = props;
const timer = useTimer();
export function getPercentComplete(remaining: MaybeNumber, total: MaybeNumber): number {
if (remaining === null || total === null) {
return 0;
}

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 (remaining <= 0) {
return 100;
}

if ((timer?.current ?? 0) < 0) {
return <div className={`${style.progressBar} ${style.overtime}`} style={{ width: '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 progress = `${getPercentComplete(timer.current, timer.duration)}%`;
return <div className={`${style.progressBar} ${playback ? style[playback] : ''}`} style={{ width: progress }} />;
}
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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);
Expand Down
98 changes: 73 additions & 25 deletions apps/server/src/classes/event-loader/EventLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,13 +354,22 @@ 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;

switch (type) {
// now, load to both public and private
case 'now':
// 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;
Expand All @@ -374,26 +383,54 @@ export class EventLoader {
this.titles.presenterNow = event.presenter;
this.titles.noteNow = event.note;
this.loaded.selectedEventId = event.id;
break;

case 'now-public':
}
} 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;
break;

case 'now-private':
}
} 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;
break;
}
}

// next, load to both public and private
case 'next':
// next, load to both public and private
else if (type === 'next') {
if (event === null) {
// public
this.titlesPublic.titleNext = null;
this.titlesPublic.subtitleNext = null;
this.titlesPublic.presenterNext = null;
this.titlesPublic.noteNext = null;
this.loaded.nextPublicEventId = null;

// private
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;
Expand All @@ -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}`);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/services/PlaybackService.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Loading

0 comments on commit 023aac4

Please sign in to comment.