diff --git a/apps/client/src/features/viewers/common/viewerUtils.ts b/apps/client/src/features/viewers/common/viewerUtils.ts index 6b2b9f858..e2dd841bf 100644 --- a/apps/client/src/features/viewers/common/viewerUtils.ts +++ b/apps/client/src/features/viewers/common/viewerUtils.ts @@ -20,8 +20,7 @@ export function getTimerByType(timerObject?: TimerTypeParams): string | number | if (timerObject.timerType === TimerType.CountDown) { timer = timerObject.current; } else if (timerObject.timerType === TimerType.CountUp) { - //todo: fix counting down issue in backend later - timer = (timerObject.elapsed ?? 0) < 0 ? 0 : timerObject.elapsed; + timer = timerObject.elapsed; } else if (timerObject.timerType === TimerType.Clock) { timer = formatTime(timerObject.clock, formatOptions); } diff --git a/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx b/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx index fd2ababe0..555b3a0c8 100644 --- a/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx +++ b/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx @@ -142,8 +142,8 @@ export default function MinimalTimer(props: MinimalTimerProps) { const stageTimerCharacters = display.replace('/:/g', '').length; const timerFontSize = (89 / (stageTimerCharacters - 1)) * (userOptions.size || 1); - const timerClasseNames = `timer ${!isPlaying ? 'timer--paused' : ''} ${showFinished ? 'timer--finished' : ''}`; + const timerClasses = `timer ${!isPlaying ? 'timer--paused' : ''} ${showFinished ? 'timer--finished' : ''}`; const baseClasses = `minimal-timer ${isMirrored ? 'mirror' : ''}`; return ( @@ -166,7 +166,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
{general.endMessage}
) : (
@@ -98,7 +98,7 @@ export default function Timer(props) {
{general.endMessage}
) : (
this.update(), timerConfig?.refresh || 1000); } - /** - * Get current time in ms from midnight - * @static - * @return {number} - */ - static getCurrentTime() { - const now = new Date(); - - // extract milliseconds since midnight - let elapsed = now.getHours() * 3600000; - elapsed += now.getMinutes() * 60000; - elapsed += now.getSeconds() * 1000; - elapsed += now.getMilliseconds(); - return elapsed; - } - - /** - * Returns expected time finish - * @private - */ - _getExpectedFinish() { - if (this.timer.startedAt === null) { - return null; - } - - if (this.timer.finishedAt) { - return this.timer.finishedAt; - } - - return Math.max( - this.timer.startedAt + this.timer.duration + this._pausedInterval + this.timer.addedTime, - this.timer.startedAt, - ); - } - /** * Clears internal state * @private @@ -82,7 +49,7 @@ export class TimerService { _clear() { this.playback = 'stop'; this.timer = { - clock: TimerService.getCurrentTime(), + clock: clock.timeNow(), current: null, elapsed: null, expectedFinish: null, @@ -95,9 +62,9 @@ export class TimerService { timerType: null, }; this.loadedTimerId = null; - this._pausedInterval = 0; - this._pausedAt = null; - this._secondaryTarget = null; + this.pausedTime = 0; + this.pausedAt = null; + this.secondaryTarget = null; } /** @@ -127,7 +94,13 @@ export class TimerService { // this might not be ideal this.timer.finishedAt = null; - this.timer.expectedFinish = this._getExpectedFinish(); + this.timer.expectedFinish = getExpectedFinish( + this.timer.startedAt, + this.timer.finishedAt, + this.timer.duration, + this.pausedTime, + this.timer.addedTime, + ); if (this.timer.startedAt === null) { this.timer.current = timer.duration; } @@ -157,8 +130,8 @@ export class TimerService { this.timer.current = timer.duration; this.playback = 'armed'; this.timer.timerType = timer.timerType; - this._pausedInterval = 0; - this._pausedAt = 0; + this.pausedTime = 0; + this.pausedAt = 0; this._onLoad(); } @@ -182,19 +155,25 @@ export class TimerService { return; } - this.timer.clock = TimerService.getCurrentTime(); + this.timer.clock = clock.timeNow(); // add paused time - if (this._pausedInterval) { - this.timer.addedTime += this._pausedInterval; - this._pausedAt = null; - this._pausedInterval = 0; + if (this.pausedTime) { + this.timer.addedTime += this.pausedTime; + this.pausedAt = null; + this.pausedTime = 0; } else { this.timer.startedAt = this.timer.clock; } this.playback = 'play'; - this.timer.expectedFinish = this._getExpectedFinish(); + this.timer.expectedFinish = getExpectedFinish( + this.timer.startedAt, + this.timer.finishedAt, + this.timer.duration, + this.pausedTime, + this.timer.addedTime, + ); this._onStart(); } @@ -214,8 +193,8 @@ export class TimerService { } this.playback = 'pause'; - this.timer.clock = TimerService.getCurrentTime(); - this._pausedAt = this.timer.clock; + this.timer.clock = clock.timeNow(); + this.pausedAt = this.timer.clock; this._onPause(); } @@ -250,14 +229,12 @@ export class TimerService { } this.timer.addedTime += amount; - this.timer.current += amount; - this.timer.elapsed += amount; // handle edge cases if (amount < 0 && Math.abs(amount) > this.timer.current) { if (this.timer.finishedAt === null) { // if we will make the clock negative - this.timer.finishedAt = TimerService.getCurrentTime(); + this.timer.finishedAt = clock.timeNow(); } } else if (this.timer.current < 0 && this.timer.current + amount > 0) { // clock will go from negative to positive @@ -269,7 +246,7 @@ export class TimerService { } update() { - this.timer.clock = TimerService.getCurrentTime(); + this.timer.clock = clock.timeNow(); if (this.playback === 'roll') { const tempCurrentTimer = { @@ -283,7 +260,7 @@ export class TimerService { clock: this.timer.clock, secondaryTimer: this.timer.secondaryTimer, - _secondaryTarget: this._secondaryTarget, + _secondaryTarget: this.secondaryTarget, }; const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } = updateRoll(tempCurrentTimer); @@ -303,45 +280,52 @@ export class TimerService { // we only update timer if a timer has been started if (this.timer.startedAt !== null) { if (this.playback === 'pause') { - this._pausedInterval = this.timer.clock - this._pausedAt; + this.pausedTime = this.timer.clock - this.pausedAt; } - this.timer.current = - this.timer.startedAt + this.timer.duration + this.timer.addedTime + this._pausedInterval - this.timer.clock; - this.timer.elapsed = this.timer.duration - this.timer.current; - if (this.playback === 'play' && this.timer.current <= 0 && this.timer.finishedAt === null) { this.timer.finishedAt = this.timer.clock; this._onFinish(); } else { - this.timer.finishedAt = null; + this.timer.expectedFinish = getExpectedFinish( + this.timer.startedAt, + this.timer.finishedAt, + this.timer.duration, + this.pausedTime, + this.timer.addedTime, + ); } - this.timer.expectedFinish = this._getExpectedFinish(); + this.timer.current = getCurrent( + this.timer.startedAt, + this.timer.duration, + this.timer.addedTime, + this.pausedTime, + this.timer.clock, + ); + this.timer.elapsed = getElapsed(this.timer.startedAt, this.timer.clock); } } this._onUpdate(); } _onUpdate() { - eventStore.set('playback', this.playback); eventStore.set('timer', this.timer); integrationService.dispatch(TimerLifeCycle.onUpdate); } _onFinish() { - eventStore.set('playback', this.playback); eventStore.set('timer', this.timer); integrationService.dispatch(TimerLifeCycle.onFinish); } roll(currentEvent, nextEvent, timers) { this._clear(); - this.timer.clock = TimerService.getCurrentTime(); + this.timer.clock = clock.timeNow(); if (currentEvent) { // there is something running, load this.timer.secondaryTimer = null; - this._secondaryTarget = null; + this.secondaryTarget = null; this.loadedTimerId = currentEvent.id; this.timer.startedAt = currentEvent.timeStart; @@ -351,7 +335,7 @@ export class TimerService { } else if (nextEvent) { // nothing now, but something coming up this.timer.secondaryTimer = nextEvent.timeStart - this.timer.clock; - this._secondaryTarget = nextEvent.timeStart; + this.secondaryTarget = nextEvent.timeStart; } this.playback = 'roll'; diff --git a/apps/server/src/services/__tests__/timerUtils.test.ts b/apps/server/src/services/__tests__/timerUtils.test.ts new file mode 100644 index 000000000..99127a555 --- /dev/null +++ b/apps/server/src/services/__tests__/timerUtils.test.ts @@ -0,0 +1,143 @@ +import { getCurrent, getElapsed, getExpectedFinish } from '../timerUtils.js'; + +describe('getExpectedFinish()', () => { + it('is null if we havent started', () => { + const startedAt = null; + const finishedAt = null; + const duration = 10; + const pausedTime = 0; + const addedTime = 0; + const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); + expect(calculatedFinish).toBe(null); + }); + it('is finishedAt if defined', () => { + const startedAt = 10; + const finishedAt = 20; + const duration = 10; + const pausedTime = 0; + const addedTime = 0; + const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); + expect(calculatedFinish).toBe(finishedAt); + }); + it('calculates the finish time', () => { + const startedAt = 1; + const finishedAt = null; + const duration = 10; + const pausedTime = 0; + const addedTime = 0; + const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); + expect(calculatedFinish).toBe(11); + }); + it('adds paused and added times', () => { + const startedAt = 1; + const finishedAt = null; + const duration = 10; + const pausedTime = 10; + const addedTime = 10; + const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); + expect(calculatedFinish).toBe(31); + }); + it('added time could be negative', () => { + const startedAt = 1; + const finishedAt = null; + const duration = 10; + const pausedTime = 10; + const addedTime = -10; + const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); + expect(calculatedFinish).toBe(11); + }); + it('user could add enough time for it to be negative', () => { + const startedAt = 1; + const finishedAt = null; + const duration = 10; + const pausedTime = 0; + const addedTime = -100; + const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); + expect(calculatedFinish).toBe(1); + }); + it('timer can have no duration', () => { + const startedAt = 1; + const finishedAt = null; + const duration = 0; + const pausedTime = 0; + const addedTime = 0; + const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); + expect(calculatedFinish).toBe(1); + }); +}); + +describe('getCurrent()', () => { + it('is null if it hasnt started', () => { + const startedAt = null; + const duration = 0; + const pausedTime = 0; + const addedTime = 0; + const clock = 0; + const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock); + expect(current).toBe(null); + }); + it('is the remaining time in clock', () => { + const startedAt = 0; + const duration = 10; + const pausedTime = 0; + const addedTime = 0; + const clock = 1; + const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock); + expect(current).toBe(9); + }); + it('accounts for added times', () => { + const startedAt = 0; + const duration = 10; + const pausedTime = 5; + const addedTime = 5; + const clock = 1; + const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock); + expect(current).toBe(19); + }); +}); + +describe('getElapsedTime()', () => { + it('time since we started', () => { + const startedAt = 0; + const clock = 5; + const elapsed = getElapsed(startedAt, clock); + expect(elapsed).toBe(5); + }); + it('clock cannot be lower than started time', () => { + const startedAt = 10; + const clock = 5; + expect(() => getElapsed(startedAt, clock)).toThrow(); + }); +}); + +describe('getExpectedFinish() getElapsedTime() and getCurrentTime() combined', () => { + it('without added times, they combine to be duration', () => { + const startedAt = 0; + const duration = 10; + const finishedAt = null; + const pausedTime = 0; + const addedTime = 0; + const clock = 0; + const expectedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); + const elapsed = getElapsed(startedAt, clock); + const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock); + expect(expectedFinish).toBe(10); + expect(elapsed).toBe(0); + expect(current).toBe(10); + expect(elapsed + current).toBe(10); + }); + it('added times influence expected finish', () => { + const startedAt = 0; + const duration = 10; + const finishedAt = null; + const pausedTime = 1; + const addedTime = 2; + const clock = 5; + const expectedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); + const elapsed = getElapsed(startedAt, clock); + const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock); + expect(expectedFinish).toBe(13); + expect(elapsed).toBe(5); + expect(current).toBe(8); + }); +}); diff --git a/apps/server/src/services/timerUtils.ts b/apps/server/src/services/timerUtils.ts new file mode 100644 index 000000000..720045aae --- /dev/null +++ b/apps/server/src/services/timerUtils.ts @@ -0,0 +1,48 @@ +type MaybeNumber = number | null; + +/** + * Calculates expected finish time of a running timer + */ +export function getExpectedFinish( + startedAt: MaybeNumber, + finishedAt: MaybeNumber, + duration: number, + pausedTime: number, + addedTime: number, +) { + if (startedAt === null) { + return null; + } + + if (finishedAt !== null) { + return finishedAt; + } + + return Math.max(startedAt + duration + pausedTime + addedTime, startedAt); +} + +/** + * Calculates running countdown + */ +export function getCurrent( + startedAt: MaybeNumber, + duration: number, + addedTime: number, + pausedTime: number, + clock: number, +) { + if (startedAt === null) { + return null; + } + return startedAt + duration + addedTime + pausedTime - clock; +} + +/** + * Calculates elapsed time + */ +export function getElapsed(startedAt: number, clock: number) { + if (startedAt > clock) { + throw new Error('clock cannot be higher than startedAt'); + } + return clock - startedAt; +}