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": { "scripts": {
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js", "addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
"postinstall": "pnpm addversion", "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:electron": "pnpm dev",
"dev:inspect": "cross-env NODE_ENV=development tsx watch --tsconfig tsconfig.app.json --inspect ./src/index.ts", "dev:inspect": "cross-env NODE_ENV=development tsx watch --tsconfig tsconfig.app.json --inspect ./src/index.ts",
"lint": "oxlint --quiet --type-aware", "lint": "oxlint --quiet --type-aware",
@@ -268,29 +268,31 @@ describe('getExpectedFinish()', () => {
const calculatedFinish = getExpectedFinish(state); const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(30); 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 = { const state = {
eventNow: { eventNow: {
timeEnd: 600000, // 00:10:00 timeStart: 20 * MILLIS_PER_HOUR, // 20:00:00
timeEnd: 21 * MILLIS_PER_MINUTE, // 21:00:00
countToEnd: true, countToEnd: true,
}, },
timer: { timer: {
addedTime: 0, addedTime: 0,
startedAt: 79200000, // 22:00:00 startedAt: 22 * MILLIS_PER_HOUR, // 22:00:00 <--------------
}, },
_timer: { _timer: {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
rundown: { rundown: {
actualStart: 79200000, actualStart: 20 * MILLIS_PER_HOUR, // 20:00:00
plannedEnd: 600000, plannedEnd: 21 * MILLIS_PER_HOUR, // 21:00:00
}, },
} as RuntimeState; } as RuntimeState;
const calculatedFinish = getExpectedFinish(state); const calculatedFinish = getExpectedFinish(state);
// expected finish is not a duration but a point in time // timeEnd is numerically before startedAt for overnight events, so expectedFinish is clamped to startedAt
expect(calculatedFinish).toBe(600000); 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 { MILLIS_PER_HOUR, checkIsNow, dayInMs, isPlaybackActive } from 'ontime-utils';
import type { RuntimeState } from '../stores/runtimeState.js'; import type { RuntimeState } from '../stores/runtimeState.js';
import { InvalidStateError } from './generic.errors.js';
/** /**
* handle events that span over midnight * 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 * @returns {number | null} new current time or null if nothing is running
*/ */
export function getExpectedFinish(state: RuntimeState): MaybeNumber { export function getExpectedFinish(state: RuntimeState): MaybeNumber {
const { startedAt, duration, addedTime } = state.timer; // 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) { if (state.eventNow === null || state.timer.startedAt === null) {
return null; return null;
} }
const { countToEnd, timeEnd } = state.eventNow; if (state.eventNow.countToEnd) {
const { pausedAt } = state._timer;
const { clock } = state;
if (startedAt === null) {
return null;
}
const pausedTime = pausedAt != null ? clock - pausedAt : 0;
if (countToEnd) {
// count to end events are anchored to their fixed end: added time and pauses // 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) // 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 // handle events that finish the day after
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- duration exists if ther eis a timer // 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) { if (expectedFinish > dayInMs) {
return expectedFinish - dayInMs; return expectedFinish - dayInMs;
} }
// an event cannot finish before it started (user added too much negative time) // 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);
} }
/** /**