Compare commits

...

4 Commits

Author SHA1 Message Date
Claude c96151fe2f fix(timer): make all pause maths wrap-aware
pausedAt is now an instant, but getExpectedFinish, getCurrent and
getRuntimeOffset were still deriving the pause duration from the time of
day clock (clock - pausedAt), which mixes units and produces a garbage
result on every paused timer.

- derive the ongoing pause from the instant in a single helper, so the
  duration is correct also when the pause spans midnight
- getCurrent no longer special cases a paused timer: it discounts the
  ongoing pause from the elapsed time, which keeps the midnight
  correction that the paused branch was missing
- reuse timeCore.elapsedTime instead of a local copy of the same logic
- type getCurrent as a Duration and getElapsed as Maybe<Duration> to
  document that these are durations, never points in time
- keep _now in parity with the clock in roll()
- type the restore point pausedAt as an instant and reject restore points
  which still carry a time of day, resuming those would corrupt the timer

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019WA6Z38L7VY2oD6B4vG8hn
2026-08-28 09:53:18 +00:00
alex-arc 6d79764ffe fix: pause timer over midnight 2026-08-09 16:46:31 +02:00
Claude a67dbd8a59 test(timer): assert elapsed stays frozen while paused over midnight
Make the midnight pause test's intent explicit: elapsed is active time
since start and must not advance during a pause (even one crossing
midnight). Add a frozen-elapsed assertion while paused and keep
pausedDuration - the corrupted pause count - as the headline assertion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0136N3FnyuUmLJbMNJiZd6YX
2026-08-09 16:46:31 +02:00
Claude 361b6eb875 test(timer): expose pause-over-midnight duration bug
Pause is tracked as pausedAt (TimeOfDay, ms since local midnight) and
paused duration is derived via the naive `clock - pausedAt`. When a pause
spans midnight the clock has wrapped to a small value while pausedAt is
still large, so the subtraction goes negative and every paused-duration
result is corrupted (runtimeState.start resume accumulation, and
getExpectedFinish/getCurrent/getRuntimeOffset in timerUtils).

Add two currently-failing tests that reproduce this:
- runtimeState: full start/pause/resume cycle where the pause crosses
  midnight, asserting pausedDuration and elapsed exclude the pause.
- timerUtils.getRuntimeOffset: over-midnight variant of the paused-offset
  case (the site carrying the "brakes when crossing midnight" TODO).

Both fail today (report ~ -86,100,000 instead of the real 5-minute pause)
and will pass once the pause math adopts the wrap-aware primitives
(timeCore.elapsedTime / epoch-based tracking).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0136N3FnyuUmLJbMNJiZd6YX
2026-08-09 16:46:31 +02:00
9 changed files with 264 additions and 63 deletions
@@ -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>;
+34 -30
View File
@@ -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 () => {
+27 -19
View File
@@ -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) {