adding block now/next data (#1661)

* adding block now/next data

* add to ui

* add blockNext

* indicate in UI

* fix test

* restore block start at value

* one loop

* stricter null comparison

* test fails

* dev throw if id cant be found

* fix spelling

* revert client stuff

* rename

* add comment
This commit is contained in:
Alex Christoffer Rasmussen
2025-06-26 21:06:20 +02:00
committed by GitHub
parent 6711cdaf4f
commit 063ae69409
11 changed files with 81 additions and 88 deletions
+3 -2
View File
@@ -17,6 +17,7 @@ export const setClientRemote = {
export const useRundownEditor = createSelector((state: RuntimeStore) => ({ export const useRundownEditor = createSelector((state: RuntimeStore) => ({
playback: state.timer.playback, playback: state.timer.playback,
selectedEventId: state.eventNow?.id ?? null, selectedEventId: state.eventNow?.id ?? null,
selectedBlockId: state.blockNow?.id ?? null,
nextEventId: state.eventNext?.id ?? null, nextEventId: state.eventNext?.id ?? null,
})); }));
@@ -131,7 +132,7 @@ export const useSelectedEventId = createSelector((state: RuntimeStore) => ({
})); }));
export const useCurrentBlockId = createSelector((state: RuntimeStore) => ({ export const useCurrentBlockId = createSelector((state: RuntimeStore) => ({
currentBlockId: state.currentBlock.block?.id ?? null, currentBlockId: state.blockNow?.id ?? null,
})); }));
export const setEventPlayback = { export const setEventPlayback = {
@@ -172,7 +173,7 @@ export const useRuntimePlaybackOverview = createSelector((state: RuntimeStore) =
selectedEventIndex: state.runtime.selectedEventIndex, selectedEventIndex: state.runtime.selectedEventIndex,
offset: state.runtime.offsetMode === OffsetMode.Absolute ? state.runtime.offset : state.runtime.relativeOffset, offset: state.runtime.offsetMode === OffsetMode.Absolute ? state.runtime.offset : state.runtime.relativeOffset,
currentBlock: state.currentBlock, blockStartedAt: state.blockNow?.startedAt ?? null,
})); }));
export const useTimelineStatus = createSelector((state: RuntimeStore) => ({ export const useTimelineStatus = createSelector((state: RuntimeStore) => ({
+3 -10
View File
@@ -131,18 +131,11 @@ function TitlesOverview() {
} }
function CurrentBlockOverview() { function CurrentBlockOverview() {
const { currentBlock, clock } = useRuntimePlaybackOverview(); const { blockStartedAt: blockStartAt, clock } = useRuntimePlaybackOverview();
const timeInBlock = formatedTime(currentBlock.startedAt === null ? null : clock - currentBlock.startedAt); const timeInBlock = formatedTime(blockStartAt ? clock - blockStartAt : null);
return ( return <TimeColumn label='Time in block' value={timeInBlock} className={style.clock} muted={blockStartAt === null} />;
<TimeColumn
label='Time in block'
value={timeInBlock}
className={style.clock}
muted={currentBlock.startedAt === null}
/>
);
} }
function TimerOverview() { function TimerOverview() {
+2 -4
View File
@@ -182,11 +182,9 @@ export const startServer = async (): Promise<{ message: string; serverPort: numb
message: { ...runtimeStorePlaceholder.message }, message: { ...runtimeStorePlaceholder.message },
runtime: state.runtime, runtime: state.runtime,
eventNow: state.eventNow, eventNow: state.eventNow,
currentBlock: {
block: null,
startedAt: null,
},
eventNext: state.eventNext, eventNext: state.eventNext,
blockNow: null,
blockNext: null,
auxtimer1: { auxtimer1: {
duration: timerConfig.auxTimerDefault, duration: timerConfig.auxTimerDefault,
current: timerConfig.auxTimerDefault, current: timerConfig.auxTimerDefault,
@@ -773,7 +773,9 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
/** /**
* the currentBlock object has no ticking values so we only need to check for equality * the currentBlock object has no ticking values so we only need to check for equality
*/ */
const shouldBlockUpdate = !deepEqual(RuntimeService?.previousState.currentBlock, state.currentBlock); const shouldBlockUpdate =
!deepEqual(RuntimeService?.previousState.blockNow, state.blockNow) ||
!deepEqual(RuntimeService?.previousState.blockNext, state.blockNext);
/** /**
* Many other values are calculated based on the clock * Many other values are calculated based on the clock
@@ -797,12 +799,14 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
if (shouldRuntimeUpdate) { if (shouldRuntimeUpdate) {
batch.add('runtime', state.runtime); batch.add('runtime', state.runtime);
RuntimeService.previousRuntimeUpdate = state.clock; RuntimeService.previousRuntimeUpdate = state.clock;
RuntimeService.previousState.runtime = { ...state.runtime }; RuntimeService.previousState.runtime = structuredClone(state.runtime);
} }
if (shouldBlockUpdate) { if (shouldBlockUpdate) {
batch.add('currentBlock', state.currentBlock); batch.add('blockNow', state.blockNow);
RuntimeService.previousState.currentBlock = { ...state.currentBlock }; batch.add('blockNext', state.blockNext);
RuntimeService.previousState.blockNow = structuredClone(state.blockNow);
RuntimeService.previousState.blockNext = structuredClone(state.blockNext);
} }
if (hasImmediateChanges) { if (hasImmediateChanges) {
@@ -857,7 +861,7 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
addedTime: state.timer.addedTime, addedTime: state.timer.addedTime,
pausedAt: state._timer.pausedAt, pausedAt: state._timer.pausedAt,
firstStart: state.runtime.actualStart, firstStart: state.runtime.actualStart,
blockStartAt: state.currentBlock.startedAt, blockStartAt: state.blockNow?.startedAt ?? null,
}) })
.catch((_e) => { .catch((_e) => {
//we don't do anything with the error here //we don't do anything with the error here
@@ -4,12 +4,10 @@ import type { RuntimeState } from '../runtimeState.js';
const baseState: RuntimeState = { const baseState: RuntimeState = {
clock: 0, clock: 0,
currentBlock: {
block: null,
startedAt: null,
},
eventNow: null, eventNow: null,
eventNext: null, eventNext: null,
blockNow: null,
blockNext: null,
runtime: { runtime: {
selectedEventIndex: null, selectedEventIndex: null,
numEvents: 0, numEvents: 0,
@@ -104,7 +104,7 @@ describe('mutation on runtimeState', () => {
expect(newState.eventNext?.id).toBe('event2'); expect(newState.eventNext?.id).toBe('event2');
expect(newState.timer.playback).toBe(Playback.Armed); expect(newState.timer.playback).toBe(Playback.Armed);
expect(newState.clock).not.toBe(666); expect(newState.clock).not.toBe(666);
expect(newState.currentBlock.block).toBeNull(); expect(newState.blockNow).toBeNull();
// 2. Start event // 2. Start event
let success = start(); let success = start();
@@ -184,7 +184,7 @@ describe('mutation on runtimeState', () => {
expect(newState.runtime.actualStart).toBeNull(); expect(newState.runtime.actualStart).toBeNull();
expect(newState.runtime.plannedStart).toBe(0); expect(newState.runtime.plannedStart).toBe(0);
expect(newState.runtime.plannedEnd).toBe(1500); expect(newState.runtime.plannedEnd).toBe(1500);
expect(newState.currentBlock.block).toBeNull(); expect(newState.blockNow).toBeNull();
expect(newState.runtime.offset).toBe(0); expect(newState.runtime.offset).toBe(0);
// 2. Start event // 2. Start event
@@ -217,7 +217,7 @@ describe('mutation on runtimeState', () => {
expect(newState.runtime.offset).toBe(delayBefore); expect(newState.runtime.offset).toBe(delayBefore);
// finish is the difference between the runtime and the schedule // finish is the difference between the runtime and the schedule
expect(newState.runtime.expectedEnd).toBe(entries.event2.timeEnd - newState.runtime.offset); expect(newState.runtime.expectedEnd).toBe(entries.event2.timeEnd - newState.runtime.offset);
expect(newState.currentBlock.block).toBeNull(); expect(newState.blockNow).toBeNull();
// 4. Add time // 4. Add time
addTime(10); addTime(10);
@@ -384,17 +384,14 @@ describe('loadBlock', () => {
}); });
const state = { const state = {
currentBlock: { blockNow: null,
block: null,
startedAt: 123,
},
eventNow: rundown.entries[11], eventNow: rundown.entries[11],
} as RuntimeState; } as unknown as RuntimeState;
loadBlock(rundown, state); loadBlock(rundown, state);
expect(state).toMatchObject({ expect(state).toMatchObject({
currentBlock: { block: rundown.entries[1], startedAt: null }, blockNow: { id: rundown.entries[1].id, startedAt: null },
eventNow: rundown.entries[11], eventNow: rundown.entries[11],
}); });
}); });
@@ -412,17 +409,14 @@ describe('loadBlock', () => {
}); });
const state = { const state = {
currentBlock: { blockNow: { id: rundown.entries[1].id, startedAt: 123 },
block: rundown.entries[1],
startedAt: 123,
},
eventNow: rundown.entries[22], eventNow: rundown.entries[22],
} as RuntimeState; } as RuntimeState;
loadBlock(rundown, state); loadBlock(rundown, state);
expect(state).toMatchObject({ expect(state).toMatchObject({
currentBlock: { block: rundown.entries[2], startedAt: null }, blockNow: { id: rundown.entries[2].id, startedAt: null },
eventNow: rundown.entries[22], eventNow: rundown.entries[22],
}); });
}); });
@@ -440,8 +434,8 @@ describe('loadBlock', () => {
}); });
const state = { const state = {
currentBlock: { blockNow: {
block: rundown.entries[1], id: rundown.entries[1].id,
startedAt: 123, startedAt: 123,
}, },
eventNow: rundown.entries[0], eventNow: rundown.entries[0],
@@ -450,7 +444,7 @@ describe('loadBlock', () => {
loadBlock(rundown, state); loadBlock(rundown, state);
expect(state).toMatchObject({ expect(state).toMatchObject({
currentBlock: { block: null, startedAt: null }, blockNow: null,
eventNow: rundown.entries[0], eventNow: rundown.entries[0],
}); });
}); });
@@ -466,17 +460,13 @@ describe('loadBlock', () => {
}); });
const state = { const state = {
currentBlock: { blockNow: { id: rundown.entries[0].id, startedAt: 123 },
block: rundown.entries[0],
startedAt: 123,
},
eventNow: rundown.entries[2], eventNow: rundown.entries[2],
} as RuntimeState; } as RuntimeState;
loadBlock(rundown, state); loadBlock(rundown, state);
expect(state).toMatchObject({ expect(state).toMatchObject({
currentBlock: { block: rundown.entries[0], startedAt: 123 }, blockNow: { id: rundown.entries[0].id, startedAt: 123 },
eventNow: rundown.entries[2], eventNow: rundown.entries[2],
}); });
}); });
@@ -491,17 +481,14 @@ describe('loadBlock', () => {
}); });
const state = { const state = {
currentBlock: { blockNow: null,
block: null,
startedAt: 123,
},
eventNow: rundown.entries[0], eventNow: rundown.entries[0],
} as RuntimeState; } as RuntimeState;
loadBlock(rundown, state); loadBlock(rundown, state);
expect(state).toMatchObject({ expect(state).toMatchObject({
currentBlock: { block: null, startedAt: 123 }, blockNow: null,
eventNow: rundown.entries[0], eventNow: rundown.entries[0],
}); });
}); });
+37 -25
View File
@@ -1,9 +1,9 @@
import { import {
CurrentBlockState, BlockState,
isOntimeBlock,
MaybeNumber, MaybeNumber,
MaybeString, MaybeString,
OffsetMode, OffsetMode,
OntimeBlock,
PlayableEvent, PlayableEvent,
Playback, Playback,
Rundown, Rundown,
@@ -30,8 +30,9 @@ import { getPlayableIndexFromTimedIndex } from '../api-data/rundown/rundown.util
export type RuntimeState = { export type RuntimeState = {
clock: number; // realtime clock clock: number; // realtime clock
blockNow: BlockState | null;
blockNext: BlockState | null;
eventNow: PlayableEvent | null; eventNow: PlayableEvent | null;
currentBlock: CurrentBlockState;
eventNext: PlayableEvent | null; eventNext: PlayableEvent | null;
runtime: Runtime; runtime: Runtime;
timer: TimerState; timer: TimerState;
@@ -48,7 +49,8 @@ export type RuntimeState = {
const runtimeState: RuntimeState = { const runtimeState: RuntimeState = {
clock: timeNow(), clock: timeNow(),
currentBlock: { ...runtimeStorePlaceholder.currentBlock }, blockNow: null,
blockNext: null,
eventNow: null, eventNow: null,
eventNext: null, eventNext: null,
runtime: { ...runtimeStorePlaceholder.runtime }, runtime: { ...runtimeStorePlaceholder.runtime },
@@ -103,8 +105,8 @@ export function clearState() {
runtimeState.eventNow = null; runtimeState.eventNow = null;
runtimeState.eventNext = null; runtimeState.eventNext = null;
runtimeState.currentBlock.block = null; runtimeState.blockNow = null;
runtimeState.currentBlock.startedAt = null; runtimeState.blockNext = null;
runtimeState.runtime.offset = 0; runtimeState.runtime.offset = 0;
runtimeState.runtime.relativeOffset = 0; runtimeState.runtime.relativeOffset = 0;
@@ -207,8 +209,8 @@ export function load(
runtimeState.runtime.relativeOffset = relativeOffset; runtimeState.runtime.relativeOffset = relativeOffset;
runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState); runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState);
} }
if (typeof initialData.blockStartAt === 'number') { if (typeof initialData.blockStartAt === 'number' && runtimeState.blockNow) {
runtimeState.currentBlock.startedAt = initialData.blockStartAt; runtimeState.blockNow.startedAt = initialData.blockStartAt;
} }
} }
return event.id === runtimeState.eventNow?.id; return event.id === runtimeState.eventNow?.id;
@@ -249,7 +251,7 @@ export function loadNext(
} }
const nowPlayableIndex = getPlayableIndexFromTimedIndex(metadata, eventIndex); const nowPlayableIndex = getPlayableIndexFromTimedIndex(metadata, eventIndex);
if (nowPlayableIndex === null || nowPlayableIndex > metadata.playableEventOrder.length - 2) { if (nowPlayableIndex === null || nowPlayableIndex > metadata.playableEventOrder.length - 2) {
// we cound not find the event now or the event now is the last playable event // we cound not find the event now or the event now is the last playable event
runtimeState.eventNext = null; runtimeState.eventNext = null;
return; return;
@@ -355,8 +357,8 @@ export function start(state: RuntimeState = runtimeState): boolean {
} }
// update block start time // update block start time
if (state.currentBlock.startedAt === null) { if (state.blockNow && state.blockNow.startedAt === null) {
state.currentBlock.startedAt = state.clock; state.blockNow.startedAt = state.clock;
} }
state.timer.playback = Playback.Play; state.timer.playback = Playback.Play;
@@ -581,8 +583,8 @@ export function roll(
runtimeState.timer.startedAt = runtimeState.clock; runtimeState.timer.startedAt = runtimeState.clock;
// update runtime // update runtime
if (runtimeState.currentBlock.startedAt === null) { if (runtimeState.blockNow && runtimeState.blockNow.startedAt === null) {
runtimeState.currentBlock.startedAt = runtimeState.clock; runtimeState.blockNow.startedAt = runtimeState.clock;
} }
if (!runtimeState.runtime.actualStart) { if (!runtimeState.runtime.actualStart) {
runtimeState.runtime.actualStart = runtimeState.clock; runtimeState.runtime.actualStart = runtimeState.clock;
@@ -645,8 +647,8 @@ export function roll(
// there is something to run, load event // there is something to run, load event
// update runtime // update runtime
if (runtimeState.currentBlock.startedAt === null) { if (runtimeState.blockNow && runtimeState.blockNow.startedAt === null) {
runtimeState.currentBlock.startedAt = runtimeState.clock; runtimeState.blockNow.startedAt = runtimeState.clock;
} }
// event will finish on time // event will finish on time
@@ -675,28 +677,38 @@ export function roll(
* handle block loading, not for use outside of runtimeState * handle block loading, not for use outside of runtimeState
*/ */
export function loadBlock(rundown: Rundown, state = runtimeState) { export function loadBlock(rundown: Rundown, state = runtimeState) {
// we need a loaded event to have a block
if (state.eventNow === null) { if (state.eventNow === null) {
// we need a loaded event to have a block state.blockNow = null;
state.currentBlock.block = null; state.blockNext = null;
state.currentBlock.startedAt = null;
return; return;
} }
const currentBlockId = state.eventNow.parent; const currentBlockId = state.eventNow.parent;
// update time only if the block has changed // look for potential next block
if (state.currentBlock.block?.id != currentBlockId) { let foundEventNow = false;
state.currentBlock.startedAt = null; for (const id of rundown.order) {
if (foundEventNow && isOntimeBlock(rundown.entries[id])) {
state.blockNext = { id, startedAt: null }; // the id is set here, the start time is set in other placed that handel starting events
break;
}
if (id === state.eventNow.id) {
foundEventNow = true;
continue;
}
} }
// update the block anyway // not inside a block
if (currentBlockId === null) { if (currentBlockId === null) {
state.currentBlock.block = null; state.blockNow = null;
return; return;
} }
const currentBlock = rundown.entries[currentBlockId]; //we went into a new block - and it is different from the one we might have come from
state.currentBlock.block = currentBlock as OntimeBlock; if ((state.blockNow != null && state.blockNow.id != currentBlockId) || state.blockNow == null) {
state.blockNow = { id: currentBlockId, startedAt: null }; // the id is set here, the start time is set in other placed that handel starting events
}
} }
export function setOffsetMode(mode: OffsetMode) { export function setOffsetMode(mode: OffsetMode) {
@@ -1,7 +1,7 @@
import type { MaybeNumber } from '../../utils/utils.type.js'; import type { MaybeNumber } from '../../utils/utils.type.js';
import type { OntimeBlock } from '../core/OntimeEvent.type.js'; import type { EntryId } from '../core/OntimeEvent.type.js';
export type CurrentBlockState = { export type BlockState = {
block: OntimeBlock | null; id: EntryId;
startedAt: MaybeNumber; startedAt: MaybeNumber;
}; };
@@ -40,10 +40,8 @@ export const runtimeStorePlaceholder: Readonly<RuntimeStore> = {
expectedEnd: null, // changes with runtime, based on offset, overflows over dayInMs expectedEnd: null, // changes with runtime, based on offset, overflows over dayInMs
offsetMode: OffsetMode.Absolute, offsetMode: OffsetMode.Absolute,
}, },
currentBlock: { blockNow: null,
block: null, blockNext: null,
startedAt: null,
},
eventNow: null, eventNow: null,
eventNext: null, eventNext: null,
auxtimer1: { auxtimer1: {
@@ -1,6 +1,6 @@
import type { OntimeEvent } from '../core/OntimeEvent.type.js'; import type { OntimeEvent } from '../core/OntimeEvent.type.js';
import type { SimpleTimerState } from './AuxTimer.type.js'; import type { SimpleTimerState } from './AuxTimer.type.js';
import type { CurrentBlockState } from './CurrentBlockState.type.js'; import type { BlockState } from './CurrentBlockState.type.js';
import type { MessageState } from './MessageControl.type.js'; import type { MessageState } from './MessageControl.type.js';
import type { Runtime } from './Runtime.type.js'; import type { Runtime } from './Runtime.type.js';
import type { TimerState } from './TimerState.type.js'; import type { TimerState } from './TimerState.type.js';
@@ -16,9 +16,11 @@ export type RuntimeStore = {
// rundown data // rundown data
runtime: Runtime; runtime: Runtime;
currentBlock: CurrentBlockState;
eventNow: OntimeEvent | null; eventNow: OntimeEvent | null;
eventNext: OntimeEvent | null; eventNext: OntimeEvent | null;
blockNow: BlockState | null;
blockNext: BlockState | null;
// extra timers // extra timers
auxtimer1: SimpleTimerState; auxtimer1: SimpleTimerState;
+1 -1
View File
@@ -101,7 +101,7 @@ export { OffsetMode } from './definitions/runtime/Runtime.type.js';
export type { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js'; export type { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js';
export { runtimeStorePlaceholder } from './definitions/runtime/RuntimeStore.js'; export { runtimeStorePlaceholder } from './definitions/runtime/RuntimeStore.js';
export { type TimerState, TimerPhase } from './definitions/runtime/TimerState.type.js'; export { type TimerState, TimerPhase } from './definitions/runtime/TimerState.type.js';
export type { CurrentBlockState } from './definitions/runtime/CurrentBlockState.type.js'; export type { BlockState } from './definitions/runtime/CurrentBlockState.type.js';
// ---> Extra Timer // ---> Extra Timer
export { type SimpleTimerState, SimplePlayback, SimpleDirection } from './definitions/runtime/AuxTimer.type.js'; export { type SimpleTimerState, SimplePlayback, SimpleDirection } from './definitions/runtime/AuxTimer.type.js';