mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-28 18:39:12 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c96151fe2f | |||
| 6d79764ffe | |||
| a67dbd8a59 | |||
| 361b6eb875 |
@@ -1,4 +1,4 @@
|
||||
import { EndAction, Playback, TimeOfDay, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
|
||||
import { EndAction, Instant, Playback, TimeOfDay, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
|
||||
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND, dayInMs, millisToString } from 'ontime-utils';
|
||||
|
||||
import type { RuntimeState } from '../../stores/runtimeState.js';
|
||||
@@ -16,6 +16,13 @@ import {
|
||||
|
||||
const asTimeOfDay = (value: number): RuntimeState['clock'] => value as RuntimeState['clock'];
|
||||
|
||||
/**
|
||||
* Instants are epoch based, we anchor them to a fixed day to keep the tests deterministic
|
||||
*/
|
||||
const midnight = new Date(2026, 0, 1).getTime();
|
||||
const instantToday = (timeOfDay: number): Instant => (midnight + timeOfDay) as Instant;
|
||||
const instantYesterday = (timeOfDay: number): Instant => (midnight - dayInMs + timeOfDay) as Instant;
|
||||
|
||||
describe('getElapsed()', () => {
|
||||
it('returns active elapsed time from startedAt without add-time adjustments', () => {
|
||||
const state = {
|
||||
@@ -53,11 +60,12 @@ describe('getElapsed()', () => {
|
||||
it('uses the current pause start while paused', () => {
|
||||
const state = {
|
||||
clock: 10 * MILLIS_PER_MINUTE,
|
||||
_now: instantToday(10 * MILLIS_PER_MINUTE),
|
||||
timer: {
|
||||
startedAt: 2 * MILLIS_PER_MINUTE,
|
||||
},
|
||||
_timer: {
|
||||
pausedAt: 7 * MILLIS_PER_MINUTE,
|
||||
pausedAt: instantToday(7 * MILLIS_PER_MINUTE),
|
||||
pausedDuration: 1 * MILLIS_PER_MINUTE,
|
||||
},
|
||||
} as RuntimeState;
|
||||
@@ -144,6 +152,28 @@ describe('getExpectedFinish()', () => {
|
||||
const calculatedFinish = getExpectedFinish(state);
|
||||
expect(calculatedFinish).toBe(31);
|
||||
});
|
||||
it('adds the time of an ongoing pause which spans midnight', () => {
|
||||
const state = {
|
||||
eventNow: {
|
||||
timeEnd: 1 * MILLIS_PER_HOUR, // 01:00
|
||||
timerType: TimerType.CountDown,
|
||||
},
|
||||
clock: 3 * MILLIS_PER_MINUTE, // 00:03 (after midnight)
|
||||
_now: instantToday(3 * MILLIS_PER_MINUTE),
|
||||
timer: {
|
||||
addedTime: 0,
|
||||
duration: 2 * MILLIS_PER_HOUR,
|
||||
startedAt: 23 * MILLIS_PER_HOUR, // 23:00
|
||||
},
|
||||
_timer: {
|
||||
pausedAt: instantYesterday(23 * MILLIS_PER_HOUR + 58 * MILLIS_PER_MINUTE), // 23:58, before midnight
|
||||
hasFinished: false,
|
||||
},
|
||||
} as RuntimeState;
|
||||
|
||||
// the 5 minute pause pushes the finish from 01:00 to 01:05
|
||||
expect(getExpectedFinish(state)).toBe(1 * MILLIS_PER_HOUR + 5 * MILLIS_PER_MINUTE);
|
||||
});
|
||||
it('added time could be negative', () => {
|
||||
const state = {
|
||||
eventNow: {
|
||||
@@ -400,6 +430,51 @@ describe('getCurrent()', () => {
|
||||
expect(current).toBe(35);
|
||||
});
|
||||
|
||||
it('is frozen while paused', () => {
|
||||
const state = {
|
||||
eventNow: {
|
||||
timeEnd: 10,
|
||||
timerType: TimerType.CountDown,
|
||||
},
|
||||
clock: 5,
|
||||
_now: instantToday(5),
|
||||
timer: {
|
||||
addedTime: 0,
|
||||
duration: 10,
|
||||
startedAt: 0,
|
||||
},
|
||||
_timer: {
|
||||
pausedAt: instantToday(2), // we have been paused for 3ms (see clock)
|
||||
hasFinished: false,
|
||||
},
|
||||
} as RuntimeState;
|
||||
|
||||
// the timer holds the value it had when we paused
|
||||
expect(getCurrent(state)).toBe(8);
|
||||
});
|
||||
it('is frozen while paused, even if the pause spans midnight', () => {
|
||||
const state = {
|
||||
eventNow: {
|
||||
timeEnd: 1 * MILLIS_PER_HOUR, // 01:00
|
||||
timerType: TimerType.CountDown,
|
||||
},
|
||||
clock: 3 * MILLIS_PER_MINUTE, // 00:03 (after midnight)
|
||||
_now: instantToday(3 * MILLIS_PER_MINUTE),
|
||||
timer: {
|
||||
addedTime: 0,
|
||||
duration: 2 * MILLIS_PER_HOUR,
|
||||
startedAt: 23 * MILLIS_PER_HOUR, // 23:00
|
||||
},
|
||||
_timer: {
|
||||
pausedAt: instantYesterday(23 * MILLIS_PER_HOUR + 58 * MILLIS_PER_MINUTE), // 23:58, before midnight
|
||||
hasFinished: false,
|
||||
},
|
||||
} as RuntimeState;
|
||||
|
||||
// we ran for 58 minutes before pausing, so the timer holds at 1h02
|
||||
expect(getCurrent(state)).toBe(1 * MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE);
|
||||
});
|
||||
|
||||
describe('on timers of type count-to-end', () => {
|
||||
it('current time is the time to end even if it hasnt started, this is weird, but by design', () => {
|
||||
const state = {
|
||||
@@ -955,13 +1030,14 @@ describe('getRuntimeOffset()', () => {
|
||||
dayOffset: 0,
|
||||
},
|
||||
clock: 150,
|
||||
_now: instantToday(150),
|
||||
timer: {
|
||||
startedAt: 100, // started on time
|
||||
current: 25, // are 25ms into it
|
||||
addedTime: 0,
|
||||
},
|
||||
_timer: {
|
||||
pausedAt: 125, // we have been paused for 25ms (see clock)
|
||||
pausedAt: instantToday(125), // we have been paused for 25ms (see clock)
|
||||
},
|
||||
rundown: {
|
||||
actualStart: 100,
|
||||
@@ -975,6 +1051,41 @@ describe('getRuntimeOffset()', () => {
|
||||
expect(absolute).toBe(25);
|
||||
});
|
||||
|
||||
it('paused time is delayed time when the pause spans midnight', () => {
|
||||
const state = {
|
||||
eventNow: {
|
||||
id: '1',
|
||||
timeStart: 23 * MILLIS_PER_HOUR, // 23:00
|
||||
timeEnd: 1 * MILLIS_PER_HOUR, // 01:00
|
||||
dayOffset: 0,
|
||||
},
|
||||
clock: 3 * MILLIS_PER_MINUTE, // 00:03 (after midnight)
|
||||
_now: instantToday(3 * MILLIS_PER_MINUTE),
|
||||
timer: {
|
||||
startedAt: 23 * MILLIS_PER_HOUR, // started on time at 23:00
|
||||
current: 25, // still counting down
|
||||
addedTime: 0,
|
||||
},
|
||||
_timer: {
|
||||
pausedAt: instantYesterday(23 * MILLIS_PER_HOUR + 58 * MILLIS_PER_MINUTE), // 23:58, before midnight
|
||||
pausedDuration: 0,
|
||||
},
|
||||
rundown: {
|
||||
actualStart: 23 * MILLIS_PER_HOUR,
|
||||
plannedStart: 23 * MILLIS_PER_HOUR,
|
||||
currentDay: 0,
|
||||
},
|
||||
_startDayOffset: 0,
|
||||
} as RuntimeState;
|
||||
|
||||
// paused from 23:58 to 00:03 -> 5 minutes, regardless of the midnight wrap
|
||||
const { absolute } = getRuntimeOffset(state);
|
||||
expect(absolute).toBe(5 * MILLIS_PER_MINUTE);
|
||||
|
||||
// and the pause is not active time, elapsed is still the 58 minutes we ran before pausing
|
||||
expect(getElapsed(state)).toBe(58 * MILLIS_PER_MINUTE);
|
||||
});
|
||||
|
||||
it('offset doesnt exist if we havent started', () => {
|
||||
const state = {
|
||||
clock: 78480789,
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('isRestorePoint()', () => {
|
||||
selectedEventId: '123',
|
||||
startedAt: 1,
|
||||
addedTime: 2,
|
||||
pausedAt: 3,
|
||||
pausedAt: asInstant(1754745600000),
|
||||
firstStart: 1,
|
||||
startEpoch: asInstant(1),
|
||||
currentDay: 0,
|
||||
@@ -64,6 +64,22 @@ describe('isRestorePoint()', () => {
|
||||
expect(isRestorePoint({ ...restorePoint, pausedDuration: '3000' })).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a pausedAt which is not an instant', () => {
|
||||
const restorePoint = {
|
||||
playback: Playback.Pause,
|
||||
selectedEventId: '123',
|
||||
startedAt: 1,
|
||||
addedTime: 2,
|
||||
// restore points from before pausedAt became an instant contain a time of day
|
||||
pausedAt: 3,
|
||||
firstStart: 1,
|
||||
startEpoch: 1,
|
||||
currentDay: 0,
|
||||
};
|
||||
|
||||
expect(isRestorePoint(restorePoint)).toBe(false);
|
||||
});
|
||||
|
||||
describe('rejects a badly formatted file', () => {
|
||||
it('with invalid playback value', () => {
|
||||
const restorePoint = {
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('restoreService', () => {
|
||||
selectedEventId: 'da5b4',
|
||||
startedAt: 1234,
|
||||
addedTime: 5678,
|
||||
pausedAt: 9087,
|
||||
pausedAt: asInstant(1754745609087),
|
||||
firstStart: 1234,
|
||||
startEpoch: asInstant(1234),
|
||||
currentDay: 0,
|
||||
@@ -55,7 +55,7 @@ describe('restoreService', () => {
|
||||
selectedEventId: 'da5b4',
|
||||
startedAt: 1234,
|
||||
addedTime: 1234,
|
||||
pausedAt: 1234,
|
||||
pausedAt: asInstant(1754745601234),
|
||||
groupStartAt: 10,
|
||||
};
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('restoreService', () => {
|
||||
selectedEventId: '1234',
|
||||
startedAt: 1234,
|
||||
addedTime: 1234,
|
||||
pausedAt: 1234,
|
||||
pausedAt: asInstant(1754745601234),
|
||||
firstStart: 1234,
|
||||
startEpoch: asInstant(1234),
|
||||
currentDay: 0,
|
||||
@@ -101,7 +101,7 @@ describe('restoreService', () => {
|
||||
selectedEventId: '2345',
|
||||
startedAt: 2345,
|
||||
addedTime: 2345,
|
||||
pausedAt: 2345,
|
||||
pausedAt: asInstant(1754745602345),
|
||||
firstStart: 2345,
|
||||
startEpoch: asInstant(2345),
|
||||
currentDay: 0,
|
||||
@@ -112,7 +112,7 @@ describe('restoreService', () => {
|
||||
selectedEventId: '3456',
|
||||
startedAt: 3456,
|
||||
addedTime: 3456,
|
||||
pausedAt: 3456,
|
||||
pausedAt: asInstant(1754745603456),
|
||||
firstStart: 3456,
|
||||
startEpoch: asInstant(3456),
|
||||
currentDay: 1,
|
||||
@@ -134,7 +134,7 @@ describe('restoreService', () => {
|
||||
selectedEventId: '5678',
|
||||
startedAt: 5678,
|
||||
addedTime: 5678,
|
||||
pausedAt: 5678,
|
||||
pausedAt: asInstant(1754745605678),
|
||||
firstStart: 5678,
|
||||
startEpoch: asInstant(5678),
|
||||
currentDay: 0,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Playback } from 'ontime-types';
|
||||
import { dayInMs } from 'ontime-utils';
|
||||
|
||||
import { is } from '../../utils/is.js';
|
||||
import type { RestorePoint } from './restore.type.js';
|
||||
@@ -42,7 +43,9 @@ export function isRestorePoint(restorePoint: unknown): restorePoint is RestorePo
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is.number(restorePoint.pausedAt) && restorePoint.pausedAt !== null) {
|
||||
// pausedAt is an instant, restore points made before this change contained a time of day
|
||||
// we reject those to avoid resuming with a corrupt pause duration
|
||||
if (restorePoint.pausedAt !== null && (!is.number(restorePoint.pausedAt) || restorePoint.pausedAt < dayInMs)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ export type RestorePoint = {
|
||||
selectedEventId: MaybeString;
|
||||
startedAt: MaybeNumber;
|
||||
addedTime: number;
|
||||
pausedAt: MaybeNumber;
|
||||
/** instant the playback was paused at */
|
||||
pausedAt: Maybe<Instant>;
|
||||
pausedDuration?: number;
|
||||
firstStart: MaybeNumber;
|
||||
startEpoch: Maybe<Instant>;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Day, MaybeNumber, TimeOfDay, TimerPhase } from 'ontime-types';
|
||||
import { Day, Duration, Maybe, MaybeNumber, TimeOfDay, TimerPhase } from 'ontime-types';
|
||||
import { MILLIS_PER_HOUR, checkIsNow, dayInMs, isPlaybackActive } from 'ontime-utils';
|
||||
|
||||
import * as timeCore from '../lib/time-core/timeCore.js';
|
||||
import type { RuntimeState } from '../stores/runtimeState.js';
|
||||
|
||||
/**
|
||||
@@ -31,14 +32,12 @@ export function getExpectedFinish(state: RuntimeState): MaybeNumber {
|
||||
}
|
||||
|
||||
const { countToEnd, timeEnd } = state.eventNow;
|
||||
const { pausedAt } = state._timer;
|
||||
const { clock } = state;
|
||||
|
||||
if (startedAt === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pausedTime = pausedAt != null ? clock - pausedAt : 0;
|
||||
const pausedTime = getOngoingPauseDuration(state);
|
||||
|
||||
if (countToEnd) {
|
||||
return timeEnd + addedTime + pausedTime;
|
||||
@@ -57,11 +56,11 @@ export function getExpectedFinish(state: RuntimeState): MaybeNumber {
|
||||
|
||||
/**
|
||||
* Calculates running countdown
|
||||
* The result is a duration (time left in the timer), never a point in time
|
||||
* @param {RuntimeState} state runtime state
|
||||
* @returns {number} current time for timer
|
||||
* @returns {Duration} time remaining in the running timer
|
||||
*/
|
||||
|
||||
export function getCurrent(state: RuntimeState): number {
|
||||
export function getCurrent(state: RuntimeState): Duration {
|
||||
// eslint-disable-next-line no-unused-labels -- dev code path
|
||||
DEV: {
|
||||
if (state.eventNow === null || state.timer.duration === null) {
|
||||
@@ -70,53 +69,58 @@ export function getCurrent(state: RuntimeState): number {
|
||||
}
|
||||
const { startedAt, duration, addedTime } = state.timer;
|
||||
const { countToEnd, timeStart, timeEnd } = state.eventNow;
|
||||
const { pausedAt } = state._timer;
|
||||
const { clock } = state;
|
||||
|
||||
if (countToEnd) {
|
||||
const isEventOverMidnight = timeStart > timeEnd;
|
||||
const correctDay = isEventOverMidnight ? dayInMs : 0;
|
||||
return correctDay - clock + timeEnd + addedTime;
|
||||
return (correctDay - clock + timeEnd + addedTime) as Duration;
|
||||
}
|
||||
|
||||
if (startedAt === null) {
|
||||
return duration;
|
||||
return duration as Duration;
|
||||
}
|
||||
|
||||
if (pausedAt != null) {
|
||||
return startedAt + duration + addedTime - pausedAt;
|
||||
}
|
||||
// an ongoing pause freezes the timer, so we discount the time spent in it
|
||||
const pausedTime = getOngoingPauseDuration(state);
|
||||
const elapsedSinceStart = timeCore.elapsedTime(clock, startedAt as TimeOfDay);
|
||||
|
||||
const hasPassedMidnight = startedAt > clock;
|
||||
const correctDay = hasPassedMidnight ? dayInMs : 0;
|
||||
return startedAt + duration + addedTime - clock - correctDay;
|
||||
return (duration + addedTime - elapsedSinceStart + pausedTime) as Duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates active time elapsed since the timer started.
|
||||
* Calculates active time elapsed since the timer started
|
||||
* Time spent paused is not active time, so it is discounted
|
||||
*/
|
||||
export function getElapsed(state: RuntimeState): MaybeNumber {
|
||||
export function getElapsed(state: RuntimeState): Maybe<Duration> {
|
||||
const { clock } = state;
|
||||
const { startedAt } = state.timer;
|
||||
const { pausedAt, pausedDuration } = state._timer;
|
||||
const { pausedDuration } = state._timer;
|
||||
|
||||
if (startedAt === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const referenceClock = pausedAt ?? clock;
|
||||
const elapsedSinceStart = getTimeSinceStart(referenceClock, startedAt);
|
||||
const activeElapsed = elapsedSinceStart - pausedDuration;
|
||||
const elapsedSinceStart = timeCore.elapsedTime(clock, startedAt as TimeOfDay);
|
||||
const activeElapsed = elapsedSinceStart - pausedDuration - getOngoingPauseDuration(state);
|
||||
|
||||
return Math.max(0, activeElapsed);
|
||||
return Math.max(0, activeElapsed) as Duration;
|
||||
}
|
||||
|
||||
function getTimeSinceStart(clock: TimeOfDay, startedAt: number): number {
|
||||
if (clock < startedAt) {
|
||||
return clock + dayInMs - startedAt;
|
||||
/**
|
||||
* Calculates how long the current pause has been going on for
|
||||
* The pause is tracked as an instant, which makes the calculation
|
||||
* immune to the clock wrapping around midnight
|
||||
* @returns 0 if the playback is not paused
|
||||
*/
|
||||
function getOngoingPauseDuration(state: RuntimeState): Duration {
|
||||
const { pausedAt } = state._timer;
|
||||
|
||||
if (pausedAt == null) {
|
||||
return 0 as Duration;
|
||||
}
|
||||
|
||||
return clock - startedAt;
|
||||
return timeCore.timeSince(state._now, pausedAt);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,7 +156,7 @@ export function skippedOutOfEvent(state: RuntimeState, previousTime: number, ski
|
||||
* Negative offset is under time / ahead of schedule
|
||||
*/
|
||||
export function getRuntimeOffset(state: RuntimeState): { absolute: number; relative: number } {
|
||||
const { eventNow, clock, _startDayOffset } = state;
|
||||
const { eventNow, _startDayOffset } = state;
|
||||
const { addedTime, current, startedAt } = state.timer;
|
||||
// nothing to calculate if there are no loaded events or if we havent started
|
||||
if (eventNow === null || startedAt === null || _startDayOffset === null) {
|
||||
@@ -176,8 +180,8 @@ export function getRuntimeOffset(state: RuntimeState): { absolute: number; relat
|
||||
// how long has the event been running over (is a negative number when in over timer so inverted before adding to offset)
|
||||
const overtime = Math.abs(Math.min(current, 0));
|
||||
|
||||
// time the playback was paused, the different from now to when we paused is added to the offset TODO: brakes when crossing midnight
|
||||
const pausedTime = state._timer.pausedAt === null ? 0 : clock - state._timer.pausedAt;
|
||||
// time the playback was paused, the difference from now to when we paused is added to the offset
|
||||
const pausedTime = getOngoingPauseDuration(state);
|
||||
|
||||
// absolute offset is difference between schedule and playback time
|
||||
// in case of count to end, the absolute offset is overtime and added time
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { OffsetMode, Playback, type TimeOfDay, TimerPhase } from 'ontime-types';
|
||||
import { Instant, OffsetMode, Playback, type TimeOfDay, TimerPhase } from 'ontime-types';
|
||||
import { deepmerge } from 'ontime-utils';
|
||||
|
||||
import type { RuntimeState } from '../runtimeState.js';
|
||||
|
||||
const baseState: RuntimeState = {
|
||||
clock: 0 as TimeOfDay,
|
||||
_now: 0 as Instant,
|
||||
eventNow: null,
|
||||
eventNext: null,
|
||||
eventFlag: null,
|
||||
|
||||
@@ -135,7 +135,7 @@ describe('mutation on runtimeState', () => {
|
||||
playback: Playback.Pause,
|
||||
addedTime: 0,
|
||||
});
|
||||
expect(newState._timer.pausedAt).toEqual(newState.clock);
|
||||
expect(newState._timer.pausedAt).toEqual(newState._now);
|
||||
|
||||
success = pause();
|
||||
expect(success).toBe(false);
|
||||
@@ -248,6 +248,63 @@ describe('mutation on runtimeState', () => {
|
||||
state = getState();
|
||||
expect(state.timer.elapsed).toBe(3 * MILLIS_PER_MINUTE);
|
||||
});
|
||||
|
||||
test('elapsed excludes a pause that spans midnight', async () => {
|
||||
clearState();
|
||||
// an event that runs over midnight (23:00 -> 01:00)
|
||||
const event = {
|
||||
...mockEvent,
|
||||
id: 'elapsed-pause-midnight',
|
||||
timeStart: 23 * MILLIS_PER_HOUR,
|
||||
timeEnd: 1 * MILLIS_PER_HOUR,
|
||||
duration: 2 * MILLIS_PER_HOUR,
|
||||
};
|
||||
const mockRundown = makeRundown({
|
||||
entries: { [event.id]: event },
|
||||
order: [event.id],
|
||||
});
|
||||
|
||||
await initRundown(mockRundown, {});
|
||||
vi.runAllTimers();
|
||||
|
||||
const { metadata, rundown } = rundownCache.get();
|
||||
|
||||
// start before midnight
|
||||
vi.setSystemTime('jan 1 23:50');
|
||||
load(event, rundown, metadata);
|
||||
start();
|
||||
|
||||
// 8 minutes of active running before we pause
|
||||
vi.setSystemTime('jan 1 23:58');
|
||||
update();
|
||||
expect(getState().timer.elapsed).toBe(8 * MILLIS_PER_MINUTE);
|
||||
pause();
|
||||
|
||||
// elapsed is active time since start, so it must not advance while paused,
|
||||
// not even when the pause itself crosses midnight
|
||||
vi.setSystemTime('jan 2 00:01');
|
||||
update();
|
||||
expect(getState().timer.elapsed).toBe(8 * MILLIS_PER_MINUTE);
|
||||
// the timer is frozen at the value it had when we paused
|
||||
expect(getState().timer.current).toBe(2 * MILLIS_PER_HOUR - 8 * MILLIS_PER_MINUTE);
|
||||
// we started 50 minutes late and have been paused for 3 minutes
|
||||
expect(getState().offset.absolute).toBe(53 * MILLIS_PER_MINUTE);
|
||||
|
||||
// resume 5 minutes after pausing, having crossed midnight (23:58 -> 00:03)
|
||||
vi.setSystemTime('jan 2 00:03');
|
||||
start();
|
||||
let state = getState();
|
||||
// the accumulated pause count is 5 minutes, regardless of the midnight wrap
|
||||
expect(state._timer.pausedDuration).toBe(5 * MILLIS_PER_MINUTE);
|
||||
// and elapsed still reflects only the 8 active minutes
|
||||
expect(state.timer.elapsed).toBe(8 * MILLIS_PER_MINUTE);
|
||||
|
||||
// 2 more active minutes after resume -> 10 minutes elapsed
|
||||
vi.setSystemTime('jan 2 00:05');
|
||||
update();
|
||||
state = getState();
|
||||
expect(state.timer.elapsed).toBe(10 * MILLIS_PER_MINUTE);
|
||||
});
|
||||
});
|
||||
|
||||
test('runtime offset', async () => {
|
||||
|
||||
@@ -63,7 +63,11 @@ export type RuntimeState = {
|
||||
// private properties of the timer calculations
|
||||
_timer: {
|
||||
forceFinish: Maybe<TimeOfDay>; // whether we should declare an event as finished, will contain the finish time
|
||||
pausedAt: Maybe<TimeOfDay>;
|
||||
|
||||
/** Instant the playback was paused at, an instant so that pause durations survive midnight */
|
||||
pausedAt: Maybe<Instant>;
|
||||
|
||||
/** Accumulated duration of past pauses, does not include an ongoing pause */
|
||||
pausedDuration: number;
|
||||
secondaryTarget: Maybe<TimeOfDay>;
|
||||
hasFinished: boolean;
|
||||
@@ -76,10 +80,14 @@ export type RuntimeState = {
|
||||
_end: ExpectedMetadata;
|
||||
_startEpoch: Maybe<Instant>;
|
||||
_startDayOffset: Maybe<Day>;
|
||||
|
||||
/** Current instant, kept in parity with the clock (see setClock) */
|
||||
_now: Instant;
|
||||
};
|
||||
|
||||
const runtimeState: RuntimeState = {
|
||||
clock: timeCore.timeOfDayNow(),
|
||||
_now: timeCore.now(),
|
||||
groupNow: null,
|
||||
eventNow: null,
|
||||
eventNext: null,
|
||||
@@ -104,6 +112,12 @@ const runtimeState: RuntimeState = {
|
||||
_startDayOffset: null,
|
||||
};
|
||||
|
||||
/** set the current clock to ensure parity between _now and clock */
|
||||
function setClock(state: RuntimeState) {
|
||||
state._now = timeCore.now();
|
||||
state.clock = timeCore.toTimeOfDay(state._now);
|
||||
}
|
||||
|
||||
export function getState(): Readonly<RuntimeState> {
|
||||
// create a shallow copy of the state
|
||||
return {
|
||||
@@ -136,7 +150,7 @@ export function clearEventData() {
|
||||
runtimeState.rundown.selectedEventIndex = null;
|
||||
|
||||
runtimeState.timer.playback = Playback.Stop;
|
||||
runtimeState.clock = timeCore.timeOfDayNow();
|
||||
setClock(runtimeState);
|
||||
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
|
||||
|
||||
// when clearing, we maintain the total delay from the rundown
|
||||
@@ -169,7 +183,7 @@ export function clearState() {
|
||||
runtimeState._end = null;
|
||||
|
||||
runtimeState.timer.playback = Playback.Stop;
|
||||
runtimeState.clock = timeCore.timeOfDayNow();
|
||||
setClock(runtimeState);
|
||||
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
|
||||
|
||||
// when clearing, we maintain the total delay from the rundown
|
||||
@@ -422,15 +436,12 @@ export function start(state: RuntimeState = runtimeState): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
const epoch = timeCore.now();
|
||||
const now = timeCore.toTimeOfDay(epoch);
|
||||
|
||||
state.clock = now;
|
||||
setClock(state);
|
||||
state.timer.secondaryTimer = null;
|
||||
|
||||
// add paused time if it exists
|
||||
if (state._timer.pausedAt) {
|
||||
const timeToAdd = state.clock - state._timer.pausedAt;
|
||||
const timeToAdd = state._now - state._timer.pausedAt;
|
||||
state.timer.addedTime += timeToAdd;
|
||||
state._timer.pausedDuration += timeToAdd;
|
||||
state._timer.pausedAt = null;
|
||||
@@ -447,7 +458,7 @@ export function start(state: RuntimeState = runtimeState): boolean {
|
||||
if (state.rundown.actualStart === null) {
|
||||
state._startDayOffset = (findDayOffset(state.eventNow.timeStart, state.clock) + state.eventNow.dayOffset) as Day;
|
||||
state.rundown.currentDay = state._startDayOffset;
|
||||
state._startEpoch = epoch;
|
||||
state._startEpoch = state._now;
|
||||
state.rundown.actualStart = state.clock;
|
||||
}
|
||||
|
||||
@@ -481,8 +492,8 @@ export function pause(state: RuntimeState = runtimeState): boolean {
|
||||
}
|
||||
|
||||
state.timer.playback = Playback.Pause;
|
||||
state.clock = timeCore.timeOfDayNow();
|
||||
state._timer.pausedAt = state.clock;
|
||||
setClock(state);
|
||||
state._timer.pausedAt = state._now;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -547,9 +558,7 @@ export type UpdateResult = {
|
||||
export function update(): UpdateResult {
|
||||
// 0. there are some things we always do
|
||||
const previousClock = runtimeState.clock;
|
||||
const epoch = timeCore.now();
|
||||
const now = timeCore.toTimeOfDay(epoch);
|
||||
runtimeState.clock = now; // we update the clock on every update call
|
||||
setClock(runtimeState); // we update the clock on every update call
|
||||
|
||||
// 1. is playback idle?
|
||||
if (!isPlaybackActive(runtimeState.timer.playback)) {
|
||||
@@ -558,13 +567,13 @@ export function update(): UpdateResult {
|
||||
|
||||
// calculate currentDay from epoch (days elapsed since playback was started)
|
||||
if (runtimeState._startEpoch !== null && runtimeState._startDayOffset !== null) {
|
||||
const daysSinceStart = timeCore.daysSinceStart(runtimeState._startEpoch, epoch);
|
||||
const daysSinceStart = timeCore.daysSinceStart(runtimeState._startEpoch, runtimeState._now);
|
||||
runtimeState.rundown.currentDay = runtimeState._startDayOffset + daysSinceStart;
|
||||
}
|
||||
|
||||
// 2. are we waiting to roll?
|
||||
if (runtimeState.timer.playback === Playback.Roll && runtimeState.timer.secondaryTimer !== null) {
|
||||
const clockHasCrossedMidnight = hasCrossedMidnight(previousClock, now);
|
||||
const clockHasCrossedMidnight = hasCrossedMidnight(previousClock, runtimeState.clock);
|
||||
return updateIfWaitingToRoll(clockHasCrossedMidnight);
|
||||
}
|
||||
|
||||
@@ -650,9 +659,8 @@ export function roll(
|
||||
}
|
||||
|
||||
// we will need to do some calculations, update the time first
|
||||
const epoch = timeCore.now();
|
||||
const now = timeCore.toTimeOfDay(epoch);
|
||||
runtimeState.clock = now;
|
||||
setClock(runtimeState);
|
||||
const epoch = runtimeState._now;
|
||||
|
||||
// 2. if there is an event armed, we use it
|
||||
if (runtimeState.timer.playback === Playback.Armed || runtimeState.timer.phase === TimerPhase.Pending) {
|
||||
|
||||
Reference in New Issue
Block a user