refactor: finish of count-to-end is plan or start time

This commit is contained in:
Carlos Valente
2026-07-17 21:41:21 +02:00
parent 41d3031a39
commit 8aa22ebc09
4 changed files with 35 additions and 25 deletions
+1 -1
View File
@@ -42,7 +42,7 @@
"scripts": {
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
"postinstall": "pnpm addversion",
"dev": "cross-env NODE_ENV=development tsx watch --tsconfig tsconfig.app.json ./src/index.ts",
"dev": "cross-env PORT=4001 NODE_ENV=development tsx watch --tsconfig tsconfig.app.json ./src/index.ts",
"dev:electron": "pnpm dev",
"dev:inspect": "cross-env NODE_ENV=development tsx watch --tsconfig tsconfig.app.json --inspect ./src/index.ts",
"lint": "oxlint --quiet --type-aware",
@@ -268,29 +268,31 @@ describe('getExpectedFinish()', () => {
const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(30);
});
it('handles events that finish the day after', () => {
it('returns the start time for count to end times which started late', () => {
const state = {
eventNow: {
timeEnd: 600000, // 00:10:00
timeStart: 20 * MILLIS_PER_HOUR, // 20:00:00
timeEnd: 21 * MILLIS_PER_MINUTE, // 21:00:00
countToEnd: true,
},
timer: {
addedTime: 0,
startedAt: 79200000, // 22:00:00
startedAt: 22 * MILLIS_PER_HOUR, // 22:00:00 <--------------
},
_timer: {
pausedAt: null,
hasFinished: false,
},
rundown: {
actualStart: 79200000,
plannedEnd: 600000,
actualStart: 20 * MILLIS_PER_HOUR, // 20:00:00
plannedEnd: 21 * MILLIS_PER_HOUR, // 21:00:00
},
} as RuntimeState;
const calculatedFinish = getExpectedFinish(state);
// expected finish is not a duration but a point in time
expect(calculatedFinish).toBe(600000);
// timeEnd is numerically before startedAt for overnight events, so expectedFinish is clamped to startedAt
expect(calculatedFinish).toBe(22 * MILLIS_PER_HOUR);
});
});
});
@@ -0,0 +1,9 @@
/**
* Expose errors where we reach invalid states
* used mostly in shouldCrashDev patterns
*/
export class InvalidStateError extends Error {
constructor(message: string) {
super(message);
}
}
+16 -17
View File
@@ -2,6 +2,7 @@ import { Day, MaybeNumber, TimeOfDay, TimerPhase } from 'ontime-types';
import { MILLIS_PER_HOUR, checkIsNow, dayInMs, isPlaybackActive } from 'ontime-utils';
import type { RuntimeState } from '../stores/runtimeState.js';
import { InvalidStateError } from './generic.errors.js';
/**
* handle events that span over midnight
@@ -24,37 +25,35 @@ export function hasCrossedMidnight(previous: TimeOfDay, current: TimeOfDay): boo
* @returns {number | null} new current time or null if nothing is running
*/
export function getExpectedFinish(state: RuntimeState): MaybeNumber {
const { startedAt, duration, addedTime } = state.timer;
if (state.eventNow === null) {
// if there is a loaded event it must have started
// either way, we have no expected finish if nothing is playing
if (state.eventNow === null || state.timer.startedAt === null) {
return null;
}
const { countToEnd, timeEnd } = state.eventNow;
const { pausedAt } = state._timer;
const { clock } = state;
if (startedAt === null) {
return null;
}
const pausedTime = pausedAt != null ? clock - pausedAt : 0;
if (countToEnd) {
if (state.eventNow.countToEnd) {
// count to end events are anchored to their fixed end: added time and pauses
// do not move the end, they surface as offset instead (see getRuntimeOffset)
return timeEnd;
return Math.max(state.eventNow.timeEnd, state.timer.startedAt);
}
const pausedTime = state._timer.pausedAt != null ? state.clock - state._timer.pausedAt : 0;
DEV: {
if (state.timer.duration === null) {
throw new InvalidStateError('a running timer cannot have null duration');
}
}
// handle events that finish the day after
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- duration exists if ther eis a timer
const expectedFinish = startedAt + duration! + addedTime + pausedTime;
const expectedFinish = state.timer.startedAt + state.timer.duration + state.timer.addedTime + pausedTime;
if (expectedFinish > dayInMs) {
return expectedFinish - dayInMs;
}
// an event cannot finish before it started (user added too much negative time)
return Math.max(expectedFinish, startedAt);
return Math.max(expectedFinish, state.timer.startedAt);
}
/**