Refactor: split runtime state (#1725)

* refactor: runtime types

* refactor: runtimeState functions to use the split type

* refactor: RuntimeService to use the new split data

* refactor: use the split data in the UI

* update comments

* refactor: rename runtime to offset

* fix utils test
This commit is contained in:
Alex Christoffer Rasmussen
2025-08-14 19:29:25 +02:00
committed by Carlos Valente
parent ed93cf313a
commit 048a9c96ea
26 changed files with 400 additions and 518 deletions
@@ -12,7 +12,6 @@ export type RestorePoint = {
addedTime: number;
pausedAt: MaybeNumber;
firstStart: MaybeNumber;
groupStartAt: MaybeNumber;
};
/**
@@ -51,10 +50,6 @@ export function isRestorePoint(obj: unknown): obj is RestorePoint {
return false;
}
if (typeof restorePoint.groupStartAt !== 'number' && restorePoint.groupStartAt !== null) {
return false;
}
return true;
}
@@ -14,7 +14,6 @@ describe('isRestorePoint()', () => {
addedTime: 2,
pausedAt: 3,
firstStart: 1,
groupStartAt: 10,
};
expect(isRestorePoint(restorePoint)).toBe(true);
@@ -25,7 +24,6 @@ describe('isRestorePoint()', () => {
addedTime: 0,
pausedAt: null,
firstStart: 1,
groupStartAt: null,
};
expect(isRestorePoint(restorePoint)).toBe(true);
});
@@ -76,7 +74,6 @@ describe('RestoreService()', () => {
addedTime: 5678,
pausedAt: 9087,
firstStart: 1234,
groupStartAt: 1652,
};
const restoreService = new RestoreService('/path/to/restore/file');
@@ -94,7 +91,6 @@ describe('RestoreService()', () => {
addedTime: 0,
pausedAt: null,
firstStart: 1234,
groupStartAt: null,
};
const restoreService = new RestoreService('/path/to/restore/file');
@@ -132,7 +128,6 @@ describe('RestoreService()', () => {
addedTime: 1234,
pausedAt: 1234,
firstStart: 1234,
groupStartAt: null,
};
const restoreService = new RestoreService('/path/to/restore/file');
@@ -204,7 +204,7 @@ describe('getExpectedFinish()', () => {
pausedAt: null,
hasFinished: false,
},
runtime: {
rundown: {
actualStart: 79200000,
plannedEnd: 600000,
},
@@ -358,7 +358,7 @@ describe('getCurrent()', () => {
duration: 100,
startedAt: null,
},
runtime: {
rundown: {
plannedEnd: null,
},
_timer: {
@@ -383,7 +383,7 @@ describe('getCurrent()', () => {
duration: 100,
startedAt: 10,
},
runtime: {
rundown: {
plannedEnd: 100,
},
_timer: {
@@ -408,7 +408,7 @@ describe('getCurrent()', () => {
duration: 100,
startedAt: 10,
},
runtime: {
rundown: {
plannedEnd: 100,
},
_timer: {
@@ -434,7 +434,7 @@ describe('getCurrent()', () => {
duration: Infinity, // not relevant,
startedAt: 79200000, // 22:00:00
},
runtime: {
rundown: {
actualStart: 79200000,
plannedEnd: 600000,
},
@@ -462,7 +462,7 @@ describe('getCurrent()', () => {
duration: Infinity, // not relevant,
startedAt: 79200000, // 22:00:00
},
runtime: {
rundown: {
actualStart: 82000000, // 22:46:40 <--- started now
plannedEnd: 81000000, // 22:30:00
},
@@ -735,14 +735,14 @@ describe('getRuntimeOffset()', () => {
_timer: {
pausedAt: null,
},
runtime: {
rundown: {
actualStart: 150,
plannedStart: 100,
},
} as RuntimeState;
const { offsetAbs } = getRuntimeOffset(state);
expect(offsetAbs).toBe(50);
const { absolute } = getRuntimeOffset(state);
expect(absolute).toBe(50);
});
it('added time subtracts time offset (positive offset)', () => {
@@ -759,14 +759,14 @@ describe('getRuntimeOffset()', () => {
_timer: {
pausedAt: null,
},
runtime: {
rundown: {
actualStart: 150,
plannedStart: 100,
},
} as RuntimeState;
const { offsetAbs } = getRuntimeOffset(state);
expect(offsetAbs).toBe(60);
const { absolute } = getRuntimeOffset(state);
expect(absolute).toBe(60);
});
it('considers running overtime (negative offset)', () => {
@@ -784,14 +784,14 @@ describe('getRuntimeOffset()', () => {
_timer: {
pausedAt: null,
},
runtime: {
rundown: {
actualStart: 100,
plannedStart: 100,
},
} as RuntimeState;
const { offsetAbs } = getRuntimeOffset(state);
expect(offsetAbs).toBe(10);
const { absolute } = getRuntimeOffset(state);
expect(absolute).toBe(10);
});
it('paused time is delayed time (negative offset)', () => {
@@ -810,14 +810,14 @@ describe('getRuntimeOffset()', () => {
_timer: {
pausedAt: 125, // we have been paused for 25ms (see clock)
},
runtime: {
rundown: {
actualStart: 100,
plannedStart: 100,
},
} as RuntimeState;
const { offsetAbs } = getRuntimeOffset(state);
expect(offsetAbs).toBe(25);
const { absolute } = getRuntimeOffset(state);
expect(absolute).toBe(25);
});
it('offset doesnt exist if we havent started', () => {
@@ -831,14 +831,16 @@ describe('getRuntimeOffset()', () => {
timeStrategy: 'lock-duration',
linkStart: false,
},
runtime: {
rundown: {
selectedEventIndex: 0,
numEvents: 2,
offsetAbs: -77400000,
plannedStart: 77400000,
plannedEnd: 84600000,
actualStart: null,
expectedEnd: null,
},
offset: {
absolute: -77400000,
expectedRundownEnd: null,
},
timer: {
addedTime: 0,
@@ -853,8 +855,8 @@ describe('getRuntimeOffset()', () => {
_timer: { pausedAt: null },
} as RuntimeState;
const { offsetAbs } = getRuntimeOffset(state);
expect(offsetAbs).toBe(0);
const { absolute } = getRuntimeOffset(state);
expect(absolute).toBe(0);
});
it('with time-to-end, offsets dont exist if we are not in overtime', () => {
@@ -882,14 +884,16 @@ describe('getRuntimeOffset()', () => {
custom: {},
delay: 0,
},
runtime: {
rundown: {
selectedEventIndex: 0,
numEvents: 1,
offsetAbs: 0,
plannedStart: 77400000, // 21:30:00
plannedEnd: 81000000, // 22:30:00
actualStart: 78000000, // 21:40:00
expectedEnd: 81600000, // 22:40:00
},
offset: {
absolute: 0,
expectedRundownEnd: 81600000, // 22:40:00
},
timer: {
addedTime: 0,
@@ -904,8 +908,8 @@ describe('getRuntimeOffset()', () => {
_timer: { pausedAt: null },
} as RuntimeState;
const { offsetAbs } = getRuntimeOffset(state);
expect(offsetAbs).toBe(0);
const { absolute } = getRuntimeOffset(state);
expect(absolute).toBe(0);
});
it('with time-to-end, offset is the overtime', () => {
@@ -933,14 +937,16 @@ describe('getRuntimeOffset()', () => {
custom: {},
delay: 0,
},
runtime: {
rundown: {
selectedEventIndex: 0,
numEvents: 1,
offsetAbs: 0,
plannedStart: 77400000, // 21:30:00
plannedEnd: 81000000, // 22:30:00
actualStart: 78000000, // 21:40:00
expectedEnd: 81600000, // 22:40:00
},
offset: {
absolute: 0,
expectedRundownEnd: 81600000, // 22:40:00
},
timer: {
addedTime: -200000,
@@ -955,8 +961,8 @@ describe('getRuntimeOffset()', () => {
_timer: { pausedAt: null },
} as RuntimeState;
const { offsetAbs } = getRuntimeOffset(state);
expect(offsetAbs).toBe(400000); // <--- offset is always the overtime
const { absolute } = getRuntimeOffset(state);
expect(absolute).toBe(400000); // <--- offset is always the overtime
});
it('handles time-to-end started after the end time', () => {
@@ -973,14 +979,16 @@ describe('getRuntimeOffset()', () => {
timerType: TimerType.CountDown,
countToEnd: true,
},
runtime: {
rundown: {
selectedEventIndex: 0,
numEvents: 1,
offsetAbs: 0,
plannedStart: 77400000, // 21:30:00
plannedEnd: 81000000, // 22:30:00
actualStart: 82000000, // 22:46:40 <--- started now
expectedEnd: 82000000 + 3600000, // <--- now + duration
},
offset: {
absolute: 0,
expectedRundownEnd: 82000000 + 3600000, // <--- now + duration
},
timer: {
addedTime: 0,
@@ -997,9 +1005,9 @@ describe('getRuntimeOffset()', () => {
const updateCurrent = getCurrent(state);
state.timer.current = updateCurrent;
const { offsetAbs } = getRuntimeOffset(state);
expect(millisToString(offsetAbs)).toBe('00:16:40');
expect(offsetAbs).toBe(82000000 - 81000000); // <-- now - planned end
const { absolute } = getRuntimeOffset(state);
expect(millisToString(absolute)).toBe('00:16:40');
expect(absolute).toBe(82000000 - 81000000); // <-- now - planned end
});
});
@@ -1018,15 +1026,15 @@ describe('getoffsetRel()', () => {
_timer: {
pausedAt: null,
},
runtime: {
rundown: {
actualStart: 150,
plannedStart: 150,
},
} as RuntimeState;
const { offsetAbs, offsetRel } = getRuntimeOffset(state);
expect(offsetAbs).toBe(0);
expect(offsetRel).toBe(0);
const { absolute, relative } = getRuntimeOffset(state);
expect(absolute).toBe(0);
expect(relative).toBe(0);
});
it('relative offset is 0 when starting after the planed time', () => {
const state = {
@@ -1042,15 +1050,15 @@ describe('getoffsetRel()', () => {
_timer: {
pausedAt: null,
},
runtime: {
rundown: {
actualStart: 150,
plannedStart: 100,
},
} as RuntimeState;
const { offsetAbs, offsetRel } = getRuntimeOffset(state);
expect(offsetAbs).toBe(50);
expect(offsetRel).toBe(0);
const { absolute, relative } = getRuntimeOffset(state);
expect(absolute).toBe(50);
expect(relative).toBe(0);
});
it('relative offset is 0 when starting before the planed time', () => {
const state = {
@@ -1066,15 +1074,15 @@ describe('getoffsetRel()', () => {
_timer: {
pausedAt: null,
},
runtime: {
rundown: {
actualStart: 100,
plannedStart: 150,
},
} as RuntimeState;
const { offsetAbs, offsetRel } = getRuntimeOffset(state);
expect(offsetAbs).toBe(-50);
expect(offsetRel).toBe(0);
const { absolute, relative } = getRuntimeOffset(state);
expect(absolute).toBe(-50);
expect(relative).toBe(0);
});
});
@@ -1175,14 +1183,16 @@ describe('getTimerPhase()', () => {
clock: 55691050,
eventNow: null,
eventNext: null,
runtime: {
rundown: {
selectedEventIndex: null,
numEvents: 1,
offsetAbs: 0,
plannedStart: 55860000,
plannedEnd: 55880000,
actualStart: null,
expectedEnd: null,
},
offset: {
absolute: 0,
expectedRundownEnd: null,
},
timer: {
addedTime: 0,
@@ -1213,14 +1223,16 @@ describe('getTimerPhase()', () => {
clock: 55691050,
eventNow: null,
eventNext: null,
runtime: {
rundown: {
selectedEventIndex: null,
numEvents: 1,
offsetAbs: 0,
plannedStart: 55860000,
plannedEnd: 55880000,
actualStart: null,
expectedEnd: null,
},
offset: {
absolute: 0,
expectedRundownEnd: null,
},
timer: {
addedTime: 0,
@@ -3,10 +3,10 @@ import {
isOntimeEvent,
isPlayableEvent,
LogOrigin,
MaybeNumber,
OffsetMode,
OntimeEvent,
Playback,
RuntimeStore,
TimerLifeCycle,
TimerPhase,
TimerState,
@@ -34,16 +34,12 @@ import {
findPreviousPlayableId,
getEventAtIndex,
getShouldClockUpdate,
getShouldFlagUpdate,
getShouldGroupUpdate,
getShouldRuntimeUpdate,
getShouldOffsetUpdate,
getShouldTimerUpdate,
isNewSecond,
} from './rundownService.utils.js';
import { RundownMetadata } from '../../api-data/rundown/rundown.types.js';
type RuntimeStateEventKeys = keyof Pick<RuntimeState, 'eventNext' | 'eventNow'>;
/**
* Service manages runtime status of app
* Coordinating with necessary services
@@ -53,22 +49,11 @@ class RuntimeService {
private lastIntegrationClockUpdate = -1;
private lastIntegrationTimerValue = -1;
/** last time we updated the socket */
static previousTimerUpdate: number;
static previousRuntimeUpdate: number;
static previousTimerValue: MaybeNumber; // previous timer value, could be null
static previousClockUpdate: number;
/** last known state */
static previousState: RuntimeState;
constructor(eventTimer: EventTimer) {
this.eventTimer = eventTimer;
RuntimeService.previousTimerUpdate = -1;
RuntimeService.previousRuntimeUpdate = -1;
RuntimeService.previousTimerValue = -1;
RuntimeService.previousClockUpdate = -1;
RuntimeService.previousState = {} as RuntimeState;
}
@@ -101,7 +86,7 @@ class RuntimeService {
// 2. handle edge cases related to roll
if (newState.timer.playback === Playback.Roll) {
// check if we need to call any side effects
const keepOffset = newState.runtime.offsetAbs;
const keepOffset = newState.offset.absolute;
if (hasSecondaryTimerFinished) {
// if the secondary timer has finished, we need to call roll
// since event is already loaded
@@ -292,7 +277,7 @@ class RuntimeService {
rundown,
playableEventOrder,
cue,
state.runtime.selectedEventIndex ?? undefined,
state.rundown.selectedEventIndex ?? undefined,
);
if (!event) {
@@ -353,7 +338,7 @@ class RuntimeService {
rundown,
playableEventOrder,
cue,
state.runtime.selectedEventIndex ?? undefined,
state.rundown.selectedEventIndex ?? undefined,
);
if (!event) {
@@ -412,7 +397,7 @@ class RuntimeService {
}
if (state.timer.playback === Playback.Roll) {
return this.loadEvent(nextEvent, { firstStart: state.runtime.actualStart });
return this.loadEvent(nextEvent, { firstStart: state.rundown.actualStart });
}
return this.loadEvent(nextEvent);
}
@@ -667,20 +652,24 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
// we do the comparison by explicitly for each property
// to apply custom logic for different datasets
// if a new event was loaded most things should update
const hasNewLoaded = state.eventNow?.id !== RuntimeService.previousState?.eventNow?.id;
// Update the entry if they have changed
let entryChanged = false;
entryChanged ||= updateMaybeEntryIfChanged('eventNow');
entryChanged ||= updateMaybeEntryIfChanged('eventNext');
entryChanged ||= updateMaybeEntryIfChanged('eventFlag');
entryChanged ||= updateMaybeEntryIfChanged('groupNow');
// for the very fist run there will be nothing in the previousState so we force an update
const justStarted = !RuntimeService.previousState?.timer;
// offset mode has been changed
const offsetModeChanged = RuntimeService.previousState?.runtime?.offsetMode !== state.runtime.offsetMode;
const offsetModeChanged = RuntimeService.previousState?.offset?.mode !== state.offset.mode;
// if playback changes most things should update
const hasChangedPlayback = RuntimeService.previousState.timer?.playback !== state.timer.playback;
// combine all big changes
const hasImmediateChanges = hasNewLoaded || justStarted || hasChangedPlayback || offsetModeChanged;
const hasImmediateChanges = entryChanged || justStarted || hasChangedPlayback || offsetModeChanged;
// clock has changed by a second or more
const updateClock = getShouldClockUpdate(RuntimeService.previousState.clock, state.clock);
@@ -693,74 +682,43 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
const updateTimer = getShouldTimerUpdate(RuntimeService.previousState?.timer, state.timer);
if (updateTimer) {
batch.add('timer', state.timer);
RuntimeService.previousTimerUpdate = state.clock;
RuntimeService.previousTimerValue = state.timer.current;
RuntimeService.previousState.timer = { ...state.timer };
}
// if any values have changed, values that have the possibility to tick are modulated by `hasClockUpdate`
const updateRuntime = getShouldRuntimeUpdate(
RuntimeService.previousState?.runtime,
state.runtime,
const updateRuntime = getShouldOffsetUpdate(
RuntimeService.previousState?.offset,
state.offset,
updateClock || hasImmediateChanges,
);
if (updateRuntime) {
batch.add('runtime', state.runtime);
RuntimeService.previousRuntimeUpdate = state.clock;
RuntimeService.previousState.runtime = structuredClone(state.runtime);
batch.add('offset', state.offset);
RuntimeService.previousState.offset = structuredClone(state.offset);
}
// if any values have changed, values that have the possibility to tick are modulated by `hasClockUpdate`
const updateGroupNow = getShouldGroupUpdate(
RuntimeService.previousState.groupNow,
state.groupNow,
updateClock || hasImmediateChanges,
);
if (updateGroupNow) {
batch.add('groupNow', state.groupNow);
RuntimeService.previousState.groupNow = structuredClone(state.groupNow);
// if any values have changed
const updateRundownData = !deepEqual(RuntimeService.previousState.rundown, state.rundown);
if (updateRundownData) {
batch.add('rundown', state.rundown);
RuntimeService.previousState.rundown = structuredClone(state.rundown);
}
// next group is just a simple string or null compare
const updateGroupNext = RuntimeService.previousState.groupNext !== state.groupNext;
if (updateGroupNext) {
batch.add('groupNext', state.groupNext);
RuntimeService.previousState.groupNext = structuredClone(state.groupNext);
}
function updateMaybeEntryIfChanged<
K extends keyof Pick<RuntimeState, 'eventNow' | 'eventNext' | 'eventFlag' | 'groupNow'>,
>(key: K) {
const previousEntry = RuntimeService.previousState[key];
const currentEntry = state[key];
// if any values have changed, values that have the possibility to tick are modulated by `hasClockUpdate`
const updateFlag = getShouldFlagUpdate(
RuntimeService.previousState.nextFlag,
state.nextFlag,
updateClock || hasImmediateChanges,
);
if (updateFlag) {
batch.add('nextFlag', state.nextFlag);
RuntimeService.previousState.nextFlag = structuredClone(state.nextFlag);
}
if (!previousEntry && !currentEntry) return false; // if both are null -> skip
// Update the events if they have changed
updateEventIfChanged('eventNow', state);
updateEventIfChanged('eventNext', state);
// Helper function to update an event if it has changed
function updateEventIfChanged(eventKey: RuntimeStateEventKeys, state: runtimeState.RuntimeState) {
const previous = RuntimeService.previousState?.[eventKey];
const now = state[eventKey];
// if there was nothing, and there is nothing, noop
if (!previous?.id && !now?.id) return;
const eventChanged =
// if load status changed, save new
previous?.id !== now?.id ||
// maybe the event itself has changed
!deepEqual(RuntimeService.previousState?.[eventKey], state[eventKey]);
if (!eventChanged) return;
batch.add(eventKey, state[eventKey]);
RuntimeService.previousState[eventKey] = structuredClone(state[eventKey]);
// if they have the same id the check if the contents have changed
if (previousEntry?.id === currentEntry?.id) {
if (deepEqual(previousEntry, currentEntry)) return false; // contents are the same -> skip
}
// at this point we know that either the id or the contents has changed
batch.add(key, currentEntry as RuntimeStore[K]); // we know that there is the necessary overlap in the types to cast this
RuntimeService.previousState[key] = structuredClone(currentEntry);
return true;
}
// save the restore state
@@ -772,8 +730,7 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
startedAt: state.timer.startedAt,
addedTime: state.timer.addedTime,
pausedAt: state._timer.pausedAt,
firstStart: state.runtime.actualStart,
groupStartAt: state.groupNow?.startedAt ?? null,
firstStart: state.rundown.actualStart,
})
.catch((_e) => {
//we don't do anything with the error here
@@ -1,16 +1,14 @@
import { millisToSeconds } from 'ontime-utils';
import {
EntryId,
GroupState,
isOntimeEvent,
isPlayableEvent,
MaybeNumber,
OntimeEvent,
Rundown,
Runtime,
Offset,
TimerState,
TimerType,
UpcomingEntry,
} from 'ontime-types';
import { deepEqual } from 'fast-equals';
@@ -55,51 +53,15 @@ export function getShouldTimerUpdate(previousValue: TimerState | undefined, curr
);
}
export function getShouldRuntimeUpdate(
previousValue: Runtime | undefined,
currentValue: Runtime,
export function getShouldOffsetUpdate(
previousValue: Offset | undefined,
currentValue: Offset,
didDependencyUpdate: boolean,
): boolean {
if (previousValue === undefined) return true;
if (didDependencyUpdate) return !deepEqual(previousValue, currentValue);
return (
previousValue.selectedEventIndex !== currentValue.selectedEventIndex ||
previousValue.numEvents !== currentValue.numEvents ||
previousValue.plannedStart !== currentValue.plannedStart ||
previousValue.plannedEnd !== currentValue.plannedEnd ||
previousValue.actualStart !== currentValue.actualStart ||
previousValue.offsetMode !== currentValue.offsetMode
// offsetAbs, offsetRel and expectedEnd are ticked with `didDependencyUpdate`
);
}
export function getShouldGroupUpdate(
previousValue: GroupState | null | undefined,
currentValue: GroupState | null,
didDependencyUpdate: boolean,
): boolean {
if (previousValue === undefined) return true;
if (didDependencyUpdate) return !deepEqual(previousValue, currentValue);
return (
previousValue?.id !== currentValue?.id || previousValue?.startedAt !== currentValue?.startedAt
// expectedEnd are ticked with `didDependencyUpdate`
);
}
export function getShouldFlagUpdate(
previousValue: UpcomingEntry | null | undefined,
currentValue: UpcomingEntry | null,
didDependencyUpdate: boolean,
): boolean {
if (previousValue === undefined) return true;
if (didDependencyUpdate) return !deepEqual(previousValue, currentValue);
return (
previousValue?.id !== currentValue?.id
// expectedStart
);
if (previousValue.mode !== currentValue.mode) return true;
// absolute, relative, expected*End are ticked with `didDependencyUpdate`
return didDependencyUpdate && !deepEqual(previousValue, currentValue);
}
/**
+8 -8
View File
@@ -112,23 +112,23 @@ export function skippedOutOfEvent(state: RuntimeState, previousTime: number, ski
* Positive offset is over time / behind schedule
* Negative offset is under time / ahead of schedule
*/
export function getRuntimeOffset(state: RuntimeState): { offsetAbs: number; offsetRel: number } {
export function getRuntimeOffset(state: RuntimeState): { absolute: number; relative: number } {
const { eventNow, clock } = 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) {
return { offsetAbs: 0, offsetRel: 0 };
return { absolute: 0, relative: 0 };
}
const { countToEnd, timeStart } = eventNow;
const { plannedStart, actualStart } = state.runtime;
const { plannedStart, actualStart } = state.rundown;
// eslint-disable-next-line no-unused-labels -- dev code path
DEV: {
// we know current exists as long as eventNow exists
if (current === null) throw new Error('timerUtils.getRuntimeOffset: state.timer.current must be set');
if (plannedStart === null) throw new Error('timerUtils.getRuntimeOffset: state.runtime.plannedStart must be set');
if (actualStart === null) throw new Error('timerUtils.getRuntimeOffset: state.runtime.actualStart must be set');
if (plannedStart === null) throw new Error('timerUtils.getRuntimeOffset: state.rundown.plannedStart must be set');
if (actualStart === null) throw new Error('timerUtils.getRuntimeOffset: state.rundown.plannedStart must be set');
}
// difference between planned event start and actual event start (will be positive if we stared behind )
@@ -140,13 +140,13 @@ export function getRuntimeOffset(state: RuntimeState): { offsetAbs: number; offs
// 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;
const offsetAbs = eventStartOffset + overtime + pausedTime + addedTime;
const absolute = eventStartOffset + overtime + pausedTime + addedTime;
// the relative offset i the same as the absolute offset but adjusted relative to the actual start time
const offsetRel = offsetAbs + plannedStart - actualStart;
const relative = absolute + plannedStart - actualStart;
// in case of count to end, the absolute offset is just the overtime
return countToEnd ? { offsetAbs: overtime, offsetRel } : { offsetAbs, offsetRel };
return countToEnd ? { absolute: overtime, relative } : { absolute, relative };
}
/**