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
@@ -33,7 +33,7 @@ const mockState = {
clock: 666,
eventNow: null,
eventNext: null,
runtime: {
rundown: {
selectedEventIndex: null,
numEvents: 0,
},
@@ -116,7 +116,7 @@ describe('mutation on runtimeState', () => {
expect(newState.timer).toMatchObject({
playback: Playback.Play,
});
expect(newState.runtime.actualStart).toBe(newState.clock);
expect(newState.rundown.actualStart).toBe(newState.clock);
// 3. Pause event
vi.setSystemTime('jan 1 00:03');
@@ -166,7 +166,7 @@ describe('mutation on runtimeState', () => {
expectedFinish: null,
startedAt: null,
});
expect(newState.runtime.actualStart).toBeNull();
expect(newState.rundown.actualStart).toBeNull();
});
});
@@ -187,24 +187,24 @@ describe('mutation on runtimeState', () => {
vi.setSystemTime('jan 1 00:09');
load(entries.event1, rundown, metadata);
let newState = getState();
expect(newState.runtime.actualStart).toBeNull();
expect(newState.runtime.plannedStart).toBe(0);
expect(newState.runtime.plannedEnd).toBe(1500);
expect(newState.rundown.actualStart).toBeNull();
expect(newState.rundown.plannedStart).toBe(0);
expect(newState.rundown.plannedEnd).toBe(1500);
expect(newState.groupNow).toBeNull();
expect(newState.runtime.offsetAbs).toBe(0);
expect(newState.offset.absolute).toBe(0);
// 2. Start event
vi.setSystemTime('jan 1 00:10');
start();
newState = getState();
const firstStart = newState.clock;
if (newState.runtime.offsetAbs === null) {
if (newState.offset.absolute === null) {
throw new Error('Value cannot be null at this stage');
}
expect(newState.runtime.actualStart).toBe(newState.clock);
expect(newState.runtime.offsetAbs).toBe(newState.clock - entries.event1.timeStart);
expect(newState.runtime.expectedEnd).toBe(entries.event2.timeEnd + newState.runtime.offsetAbs);
expect(newState.rundown.actualStart).toBe(newState.clock);
expect(newState.offset.absolute).toBe(newState.clock - entries.event1.timeStart);
expect(newState.offset.expectedRundownEnd).toBe(entries.event2.timeEnd + newState.offset.absolute);
// 3. Next event
vi.setSystemTime('jan 1 00:12');
@@ -212,37 +212,37 @@ describe('mutation on runtimeState', () => {
start();
newState = getState();
if (newState.runtime.actualStart === null || newState.runtime.offsetAbs === null) {
if (newState.rundown.actualStart === null || newState.offset.absolute === null) {
throw new Error('Value cannot be null at this stage');
}
// there is a case where the calculation time overflows the millisecond which makes
// tests fail
const forgivingActualStart = Math.abs(newState.runtime.actualStart - firstStart);
const forgivingActualStart = Math.abs(newState.rundown.actualStart - firstStart);
expect(forgivingActualStart).toBeLessThanOrEqual(1);
// we are over-under, the difference between the schedule and the actual start
const delayBefore = newState.clock - entries.event2.timeStart;
expect(newState.runtime.offsetAbs).toBe(delayBefore);
expect(newState.offset.absolute).toBe(delayBefore);
// finish is the difference between the runtime and the schedule
expect(newState.runtime.expectedEnd).toBe(newState.runtime.offsetAbs + entries.event2.timeEnd);
expect(newState.offset.expectedRundownEnd).toBe(newState.offset.absolute + entries.event2.timeEnd);
expect(newState.groupNow).toBeNull();
// 4. Add time
addTime(10);
newState = getState();
if (newState.runtime.offsetAbs === null) {
if (newState.offset.absolute === null) {
throw new Error('Value cannot be null at this stage');
}
expect(newState.runtime.offsetAbs).toBe(delayBefore + 10);
expect(newState.runtime.expectedEnd).toBe(entries.event2.timeEnd + newState.runtime.offsetAbs);
expect(newState.offset.absolute).toBe(delayBefore + 10);
expect(newState.offset.expectedRundownEnd).toBe(entries.event2.timeEnd + newState.offset.absolute);
// 5. Stop event
stop();
newState = getState();
expect(newState.runtime.actualStart).toBeNull();
expect(newState.runtime.offsetAbs).toBe(0);
expect(newState.runtime.expectedEnd).toBeNull();
expect(newState.rundown.actualStart).toBeNull();
expect(newState.offset.absolute).toBe(0);
expect(newState.offset.expectedRundownEnd).toBeNull();
});
});
@@ -332,7 +332,7 @@ describe('roll mode', () => {
start();
const result = roll(rundown, metadata);
expect(result).toStrictEqual({ eventId: '1', didStart: false });
expect(getState().runtime.offsetAbs).toBe(-1000);
expect(getState().offset.absolute).toBe(-1000);
});
});
@@ -356,29 +356,29 @@ describe('roll mode', () => {
load(rundown.entries[1] as PlayableEvent, rundown, metadata);
start();
// the current offset after manual play
const currentOffset = getState().runtime.offsetAbs;
let result = roll(rundown, metadata, getState().runtime.offsetAbs);
const currentOffset = getState().offset.absolute;
let result = roll(rundown, metadata, getState().offset.absolute);
expect(result).toStrictEqual({ eventId: '1', didStart: false });
// the current offset should be maintain by roll mode when taking over from play
expect(getState().runtime.offsetAbs).toBe(currentOffset);
expect(getState().offset.absolute).toBe(currentOffset);
vi.setSystemTime('jan 1 00:00:01');
result = roll(rundown, metadata, getState().runtime.offsetAbs);
result = roll(rundown, metadata, getState().offset.absolute);
expect(result).toStrictEqual({ eventId: '2', didStart: true });
expect(getState().runtime.offsetAbs).toBe(-1000);
expect(getState().offset.absolute).toBe(-1000);
vi.setSystemTime('jan 1 00:00:02');
result = roll(rundown, metadata, getState().runtime.offsetAbs);
result = roll(rundown, metadata, getState().offset.absolute);
expect(result).toStrictEqual({ eventId: '3', didStart: true });
expect(getState().runtime.offsetAbs).toBe(-1000);
expect(getState().offset.absolute).toBe(-1000);
vi.useRealTimers();
});
});
});
describe('loadGroup', () => {
test('from no-group to a group will clear startedAt', () => {
describe('loadGroupFlagAndEnd()', () => {
test('from no-group to a group will clear expectedGroupEnd', () => {
const rundown = makeRundown({
entries: {
0: makeOntimeEvent({ id: '0', parent: null }),
@@ -393,6 +393,7 @@ describe('loadGroup', () => {
const state = {
groupNow: null,
eventNow: rundown.entries[11],
offset: { expectedGroupEnd: 123 },
} as RuntimeState;
const metadata = { playableEventOrder: ['0', '11', '3'], flags: ['1'] } as RundownMetadata;
@@ -400,12 +401,13 @@ describe('loadGroup', () => {
loadGroupFlagAndEnd(rundown, metadata, 2, state);
expect(state).toMatchObject({
groupNow: { id: rundown.entries[1].id, startedAt: null },
groupNow: rundown.entries[1],
offset: { expectedGroupEnd: null },
eventNow: rundown.entries[11],
});
});
test('from a group to a different group will clear startedAt', () => {
test('from a group to a different group will clear expectedGroupEnd', () => {
const rundown = makeRundown({
entries: {
0: makeOntimeEvent({ id: '0', parent: null }),
@@ -418,7 +420,8 @@ describe('loadGroup', () => {
});
const state = {
groupNow: { id: rundown.entries[1].id, startedAt: 123 },
groupNow: rundown.entries[1],
offset: { expectedGroupEnd: 123 },
eventNow: rundown.entries[22],
} as RuntimeState;
@@ -427,12 +430,13 @@ describe('loadGroup', () => {
loadGroupFlagAndEnd(rundown, metadata, 1, state);
expect(state).toMatchObject({
groupNow: { id: rundown.entries[2].id, startedAt: null },
groupNow: rundown.entries[2],
offset: { expectedGroupEnd: null },
eventNow: rundown.entries[22],
});
});
test('from group to a no-group will clear startedAt', () => {
test('from group to a no-group will clear expectedGroupEnd', () => {
const rundown = makeRundown({
entries: {
0: makeOntimeEvent({ id: '0', parent: null }),
@@ -445,10 +449,8 @@ describe('loadGroup', () => {
});
const state = {
groupNow: {
id: rundown.entries[1].id,
startedAt: 123,
},
groupNow: rundown.entries[1],
offset: { expectedGroupEnd: 123 },
eventNow: rundown.entries[0],
} as RuntimeState;
@@ -458,35 +460,11 @@ describe('loadGroup', () => {
expect(state).toMatchObject({
groupNow: null,
offset: { expectedGroupEnd: null },
eventNow: rundown.entries[0],
});
});
test('from a group to same group will keep startedAt', () => {
const rundown = makeRundown({
entries: {
0: makeOntimeGroup({ id: '0', entries: ['1', '2'] }),
1: makeOntimeEvent({ id: '1', parent: '0' }),
2: makeOntimeEvent({ id: '2', parent: '0' }),
},
order: ['0'],
});
const state = {
groupNow: { id: rundown.entries[0].id, startedAt: 123 },
eventNow: rundown.entries[2],
} as RuntimeState;
const metadata = { playableEventOrder: ['1', '2'], flags: ['1'] } as RundownMetadata;
loadGroupFlagAndEnd(rundown, metadata, 0, state);
expect(state).toMatchObject({
groupNow: { id: rundown.entries[0].id, startedAt: 123 },
eventNow: rundown.entries[2],
});
});
test('from no-group to no-group will keep startedAt', () => {
const rundown = makeRundown({
entries: {