Feat: Time until group and flag (#1708)

* refactor: split event data and state data

* refactor: calculate the start time insted of until

* cleanup

* send next flag expected start from server

* refactor: render

* fix test

* use same icon

* refactor: group duration

* Optimize (#1711)

* refactor: consolidate block, flag and end loading and expected times

* work on test

* cal end value

* lint

* fix test

* remove todo
This commit is contained in:
Alex Christoffer Rasmussen
2025-08-07 07:55:28 +12:00
committed by GitHub
parent e37d2ce50f
commit 35347e8d63
19 changed files with 567 additions and 539 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ export { validateEndAction, validateTimerType } from './src/validate-events/vali
// feature business logic
export { calculateTimeUntilStart } from './src/date-utils/calculateTimeUntilStart.js';
export { getExpectedStart } from './src/date-utils/getExpectedStart.js';
// feature business logic - rundown
export { checkIsNow } from './src/date-utils/checkIsNow.js';
@@ -1,258 +0,0 @@
import { OffsetMode } from 'ontime-types';
import { calculateTimeUntilStart } from './calculateTimeUntilStart';
import { dayInMs } from './conversionUtils';
describe('calculateTimeUntilStart()', () => {
describe('Absolute offset mode', () => {
test('ontime', () => {
const test = {
timeStart: 100,
dayOffset: 0,
delay: 0,
currentDay: 0,
totalGap: 0,
clock: 90,
offset: 0,
offsetMode: OffsetMode.Absolute,
actualStart: null,
plannedStart: null,
};
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(10);
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(10);
});
test('running behind', () => {
const test = {
timeStart: 100,
dayOffset: 0,
delay: 0,
currentDay: 0,
totalGap: 0,
clock: 90,
offset: -20,
offsetMode: OffsetMode.Absolute,
actualStart: null,
plannedStart: null,
};
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(30);
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(30);
});
test('running ahead', () => {
const test = {
timeStart: 100,
dayOffset: 0,
delay: 0,
currentDay: 0,
totalGap: 0,
clock: 80,
offset: 10,
offsetMode: OffsetMode.Absolute,
actualStart: null,
plannedStart: null,
};
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20); // <-- when running ahead the unlinked timer stays put
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(10);
});
test('running behind with enough gaps', () => {
const test = {
timeStart: 100,
dayOffset: 0,
delay: 0,
currentDay: 0,
totalGap: 20,
clock: 50,
offset: -20,
offsetMode: OffsetMode.Absolute,
actualStart: null,
plannedStart: null,
};
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(50); // <-- when gap is enough to compensate for the running behind
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(70); // This should not be possible
});
test('running behind with too little gaps', () => {
const test = {
timeStart: 100,
dayOffset: 0,
delay: 0,
currentDay: 0,
totalGap: 10,
clock: 50,
offset: -20,
offsetMode: OffsetMode.Absolute,
actualStart: 0,
plannedStart: 0,
};
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(60); // <-- when gap is not enough to compensate for the running behind it absorbs at much as possible
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(70); // This should not be possible
});
});
describe('Relative offset mode', () => {
test('basic function', () => {
const test = {
timeStart: 0,
dayOffset: 0,
delay: 0,
currentDay: 0,
totalGap: 0,
clock: 100,
actualStart: 100,
plannedStart: 0,
offset: 0,
offsetMode: OffsetMode.Relative,
};
const timeStartEvent2 = 10;
const timeStartEvent3 = 20;
//event 1 is the currently running event
//event 2
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: true })).toBe(10);
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: false })).toBe(10);
//event 3
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: true })).toBe(20);
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: false })).toBe(20);
// When clock advances by 5ms, time until start should decrease by 5ms
test.clock = 105;
//event 2
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: true })).toBe(5);
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: false })).toBe(5);
//event 3
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: true })).toBe(15);
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: false })).toBe(15);
});
test('gaps', () => {
const test = {
timeStart: 20,
dayOffset: 0,
delay: 0,
currentDay: 0,
totalGap: 10,
clock: 100,
actualStart: 100,
plannedStart: 0,
offset: 0,
offsetMode: OffsetMode.Relative,
};
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(20);
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20);
// When clock advances by 5ms, time until start should decrease by 5ms
test.clock = 105;
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(15);
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(15);
});
test('added/remove time', () => {
const test = {
timeStart: 20,
dayOffset: 0,
delay: 0,
currentDay: 0,
totalGap: 0,
clock: 100,
actualStart: 100,
plannedStart: 0,
offset: 0,
offsetMode: OffsetMode.Relative,
};
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(20);
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20);
test.offset = 5; // remove 5 with addtime - we are ahead of time
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(15);
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20); // unlocked evets will stay on schedule
test.offset = -5; // add 5 with addtime - we are behind
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(25);
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(25);
});
test('next day', () => {
const test = {
delay: 0,
currentDay: 0,
clock: 100,
actualStart: 100,
plannedStart: 0,
offset: 0,
offsetMode: OffsetMode.Relative,
};
// this event will start the current day
expect(
calculateTimeUntilStart({
...test,
timeStart: 10,
dayOffset: 0,
totalGap: 0,
isLinkedToLoaded: false,
}),
).toBe(10);
// this event will start the next day
// in absolute mode this would start in dayInMs - 100 since the gap would compensate
// but in relative mode with and actual start that is 100 offset it starts in dayInMs
expect(
calculateTimeUntilStart({
...test,
timeStart: 0,
dayOffset: 1,
totalGap: dayInMs - 20,
isLinkedToLoaded: false,
}),
).toBe(dayInMs);
// advancing 100ms
test.clock = 200;
expect(
calculateTimeUntilStart({
...test,
timeStart: 0,
dayOffset: 1,
totalGap: dayInMs - 20,
isLinkedToLoaded: false,
}),
).toBe(dayInMs - 100);
});
});
test('overlap with negative total gap', () => {
const test = {
dayOffset: 0,
delay: 0,
currentDay: 0,
clock: 100,
actualStart: 100,
plannedStart: 0,
offset: 0,
offsetMode: OffsetMode.Relative,
isLinkedToLoaded: false,
};
// the overlap will be pushed out to the expected available time
expect(calculateTimeUntilStart({ ...test, timeStart: 5, totalGap: -5 })).toBe(10);
test.clock = 105;
});
});
@@ -0,0 +1,280 @@
import { OffsetMode } from 'ontime-types';
import { dayInMs } from './conversionUtils';
import { getExpectedStart } from './getExpectedStart';
describe('getExpectedStart()', () => {
describe('Absolute offset mode', () => {
test('ontime', () => {
const testEvent = {
timeStart: 100,
delay: 0,
dayOffset: 0,
};
const testState = {
currentDay: 0,
totalGap: 0,
offset: 0,
offsetMode: OffsetMode.Absolute,
actualStart: null,
plannedStart: null,
};
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: false })).toBe(testEvent.timeStart);
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(testEvent.timeStart);
});
test('running behind', () => {
const testEvent = {
timeStart: 100,
dayOffset: 0,
delay: 0,
};
const testState = {
currentDay: 0,
totalGap: 0,
offset: -20,
offsetMode: OffsetMode.Absolute,
actualStart: null,
plannedStart: null,
};
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: false })).toBe(120);
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(120);
});
test('running ahead', () => {
const testEvent = {
timeStart: 100,
dayOffset: 0,
delay: 0,
};
const testState = {
currentDay: 0,
totalGap: 0,
offset: 10,
offsetMode: OffsetMode.Absolute,
actualStart: null,
plannedStart: null,
};
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: false })).toBe(100); // <-- when running ahead the unlinked timer stays put
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(90);
});
test('running behind with enough gaps', () => {
const testEvent = {
timeStart: 100,
dayOffset: 0,
delay: 0,
};
const testState = {
currentDay: 0,
totalGap: 20,
offset: -20,
offsetMode: OffsetMode.Absolute,
actualStart: null,
plannedStart: null,
};
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: false })).toBe(100); // <-- when gap is enough to compensate for the running behind
// expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(70); This should not be possible
});
test('running behind with too little gaps', () => {
const testEvent = {
timeStart: 100,
dayOffset: 0,
delay: 0,
};
const testState = {
currentDay: 0,
totalGap: 10,
offset: -20,
offsetMode: OffsetMode.Absolute,
actualStart: 0,
plannedStart: 0,
};
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: false })).toBe(110); // <-- when gap is not enough to compensate for the running behind it absorbs at much as possible
// expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(70); This should not be possible
});
});
describe('Relative offset mode', () => {
test('basic function', () => {
const testState = {
currentDay: 0,
totalGap: 0,
actualStart: 100,
plannedStart: 0,
offset: 0,
offsetMode: OffsetMode.Relative,
};
const timeStartEvent2 = 10;
const timeStartEvent3 = 20;
//event 1 is the currently running event
//event 2
expect(
getExpectedStart(
{ dayOffset: 0, delay: 0, timeStart: timeStartEvent2 },
{ ...testState, isLinkedToLoaded: true },
),
).toBe(110);
expect(
getExpectedStart(
{ dayOffset: 0, delay: 0, timeStart: timeStartEvent2 },
{ ...testState, isLinkedToLoaded: false },
),
).toBe(110);
//event 3
expect(
getExpectedStart(
{ dayOffset: 0, delay: 0, timeStart: timeStartEvent3 },
{ ...testState, isLinkedToLoaded: true },
),
).toBe(120);
expect(
getExpectedStart(
{ dayOffset: 0, delay: 0, timeStart: timeStartEvent3 },
{ ...testState, isLinkedToLoaded: false },
),
).toBe(120);
// if we actually started 5ms later
testState.actualStart = 105;
//event 2
expect(
getExpectedStart(
{ dayOffset: 0, delay: 0, timeStart: timeStartEvent2 },
{ ...testState, isLinkedToLoaded: true },
),
).toBe(115);
expect(
getExpectedStart(
{ dayOffset: 0, delay: 0, timeStart: timeStartEvent2 },
{ ...testState, isLinkedToLoaded: false },
),
).toBe(115);
//event 3
expect(
getExpectedStart(
{ dayOffset: 0, delay: 0, timeStart: timeStartEvent3 },
{ ...testState, isLinkedToLoaded: true },
),
).toBe(125);
expect(
getExpectedStart(
{ dayOffset: 0, delay: 0, timeStart: timeStartEvent3 },
{ ...testState, isLinkedToLoaded: false },
),
).toBe(125);
});
test('gaps', () => {
const testEvent = {
timeStart: 20,
dayOffset: 0,
delay: 0,
};
const testState = {
currentDay: 0,
totalGap: 10,
actualStart: 100,
plannedStart: 0,
offset: 0,
offsetMode: OffsetMode.Relative,
};
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(120);
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: false })).toBe(120);
// if we actually started 5ms later
testState.actualStart = 105;
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(125);
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: false })).toBe(125);
});
test('added/remove time', () => {
const testEvent = {
timeStart: 20,
dayOffset: 0,
delay: 0,
};
const testState = {
currentDay: 0,
totalGap: 0,
actualStart: 100,
plannedStart: 0,
offset: 0,
offsetMode: OffsetMode.Relative,
};
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(120);
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: false })).toBe(120);
testState.offset = 5; // remove 5 with addtime - we are ahead of time
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(115);
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: false })).toBe(120); // unlocked evets will stay on schedule
testState.offset = -5; // add 5 with addtime - we are behind
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(125);
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: false })).toBe(125);
});
test('next day', () => {
const testState = {
currentDay: 0,
actualStart: 100,
plannedStart: 0,
offset: 0,
offsetMode: OffsetMode.Relative,
};
// this event will start the current day
expect(
getExpectedStart(
{ timeStart: 10, delay: 0, dayOffset: 0 },
{ ...testState, totalGap: 0, isLinkedToLoaded: false },
),
).toBe(110);
// this event will start the next day
// in absolute mode this would start in dayInMs - 100 since the gap would compensate
// but in relative mode with and actual start that is 100 offset it starts in dayInMs
expect(
getExpectedStart(
{ timeStart: 0, delay: 0, dayOffset: 1 },
{ ...testState, totalGap: dayInMs - 20, isLinkedToLoaded: false },
),
).toBe(dayInMs + 100);
});
});
test('overlap with negative total gap', () => {
const testEvent = {
timeStart: 5,
dayOffset: 0,
delay: 0,
};
const testState = {
currentDay: 0,
actualStart: 100,
plannedStart: 0,
offset: 0,
offsetMode: OffsetMode.Relative,
isLinkedToLoaded: false,
};
// the overlap will be pushed out to the expected available time
expect(getExpectedStart(testEvent, { ...testState, totalGap: -5 })).toBe(110);
});
});
@@ -4,6 +4,7 @@ import { OffsetMode } from 'ontime-types';
import { dayInMs } from './conversionUtils.js';
/**
* @param event the event that we are counting to
* @param currentDay the day offset of the currently running event
* @param totalGap accumulated gap from the current event
* @param isLinkedToLoaded is this event part of a chain linking back to the current loaded event
@@ -11,31 +12,20 @@ import { dayInMs } from './conversionUtils.js';
* @param offset
* @returns
*/
export function calculateTimeUntilStart(
data: Pick<OntimeEvent, 'timeStart' | 'dayOffset' | 'delay'> & {
export function getExpectedStart(
event: Pick<OntimeEvent, 'timeStart' | 'dayOffset' | 'delay'>,
state: {
currentDay: number;
totalGap: number;
isLinkedToLoaded: boolean;
clock: number;
offset: number;
offsetMode: OffsetMode;
actualStart: MaybeNumber;
plannedStart: MaybeNumber;
},
): number {
const {
timeStart,
dayOffset,
currentDay,
totalGap,
isLinkedToLoaded,
clock,
offset,
delay,
offsetMode,
actualStart,
plannedStart,
} = data;
const { timeStart, dayOffset, delay } = event;
const { currentDay, totalGap, isLinkedToLoaded, offset, offsetMode, actualStart, plannedStart } = state;
//How many days from the currently running event to this one
const relativeDayOffset = dayOffset - currentDay;
@@ -51,22 +41,22 @@ export function calculateTimeUntilStart(
relativeStartOffset = (actualStart ?? 0) - (plannedStart ?? 0);
}
const scheduledTimeUntil = normalisedTimeStart - clock + relativeStartOffset;
const scheduledStartTime = normalisedTimeStart + relativeStartOffset;
const offsetTimeUntil = scheduledTimeUntil - offset;
const offsetStartTime = scheduledStartTime - offset;
if (isLinkedToLoaded) {
//if we are directly linked back to the loaded event we just follow the offset
return offsetTimeUntil;
return offsetStartTime;
}
const gapsCanCompensateForOffset = totalGap + offset >= 0;
if (gapsCanCompensateForOffset) {
// if we are ahead of schedule or the gap can compensate for the amount we are behind then expect to start at the scheduled time
return scheduledTimeUntil;
return scheduledStartTime;
}
// otherwise consume as much of the offset as possible with the gap
const offsetTimeUntilBufferedByGaps = offsetTimeUntil - totalGap;
return offsetTimeUntilBufferedByGaps;
const offsetStartTimeBufferedByGaps = offsetStartTime - totalGap;
return offsetStartTimeBufferedByGaps;
}