From 8aa22ebc09899ce927219087b84f063fcaed4948 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Fri, 17 Jul 2026 21:41:21 +0200 Subject: [PATCH] refactor: finish of count-to-end is plan or start time --- apps/server/package.json | 2 +- .../src/services/__tests__/timerUtils.test.ts | 16 +++++---- apps/server/src/services/generic.errors.ts | 9 +++++ apps/server/src/services/timerUtils.ts | 33 +++++++++---------- 4 files changed, 35 insertions(+), 25 deletions(-) create mode 100644 apps/server/src/services/generic.errors.ts diff --git a/apps/server/package.json b/apps/server/package.json index a5ddc2f4b..5e46509c2 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -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", diff --git a/apps/server/src/services/__tests__/timerUtils.test.ts b/apps/server/src/services/__tests__/timerUtils.test.ts index 5de0d5da3..dbb09e3e4 100644 --- a/apps/server/src/services/__tests__/timerUtils.test.ts +++ b/apps/server/src/services/__tests__/timerUtils.test.ts @@ -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); }); }); }); diff --git a/apps/server/src/services/generic.errors.ts b/apps/server/src/services/generic.errors.ts new file mode 100644 index 000000000..d3a3e7d98 --- /dev/null +++ b/apps/server/src/services/generic.errors.ts @@ -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); + } +} diff --git a/apps/server/src/services/timerUtils.ts b/apps/server/src/services/timerUtils.ts index 7429e2b21..34ac1e351 100644 --- a/apps/server/src/services/timerUtils.ts +++ b/apps/server/src/services/timerUtils.ts @@ -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); } /**