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) {
@@ -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;
+}