diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index b15390e11..a0da4b3ed 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -17,6 +17,7 @@ export const setClientRemote = { export const useRundownEditor = createSelector((state: RuntimeStore) => ({ playback: state.timer.playback, selectedEventId: state.eventNow?.id ?? null, + selectedBlockId: state.blockNow?.id ?? null, nextEventId: state.eventNext?.id ?? null, })); @@ -131,7 +132,7 @@ export const useSelectedEventId = createSelector((state: RuntimeStore) => ({ })); export const useCurrentBlockId = createSelector((state: RuntimeStore) => ({ - currentBlockId: state.currentBlock.block?.id ?? null, + currentBlockId: state.blockNow?.id ?? null, })); export const setEventPlayback = { @@ -172,7 +173,7 @@ export const useRuntimePlaybackOverview = createSelector((state: RuntimeStore) = selectedEventIndex: state.runtime.selectedEventIndex, 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) => ({ diff --git a/apps/client/src/features/overview/Overview.tsx b/apps/client/src/features/overview/Overview.tsx index a29c1d69d..5b10cc3e9 100644 --- a/apps/client/src/features/overview/Overview.tsx +++ b/apps/client/src/features/overview/Overview.tsx @@ -131,18 +131,11 @@ function TitlesOverview() { } 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 ; } function TimerOverview() { diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 1e14acc01..7afc36af3 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -182,11 +182,9 @@ export const startServer = async (): Promise<{ message: string; serverPort: numb message: { ...runtimeStorePlaceholder.message }, runtime: state.runtime, eventNow: state.eventNow, - currentBlock: { - block: null, - startedAt: null, - }, eventNext: state.eventNext, + blockNow: null, + blockNext: null, auxtimer1: { duration: timerConfig.auxTimerDefault, current: timerConfig.auxTimerDefault, diff --git a/apps/server/src/services/runtime-service/RuntimeService.ts b/apps/server/src/services/runtime-service/RuntimeService.ts index b5dfe584d..b66aab324 100644 --- a/apps/server/src/services/runtime-service/RuntimeService.ts +++ b/apps/server/src/services/runtime-service/RuntimeService.ts @@ -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 */ - 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 @@ -797,12 +799,14 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert if (shouldRuntimeUpdate) { batch.add('runtime', state.runtime); RuntimeService.previousRuntimeUpdate = state.clock; - RuntimeService.previousState.runtime = { ...state.runtime }; + RuntimeService.previousState.runtime = structuredClone(state.runtime); } if (shouldBlockUpdate) { - batch.add('currentBlock', state.currentBlock); - RuntimeService.previousState.currentBlock = { ...state.currentBlock }; + batch.add('blockNow', state.blockNow); + batch.add('blockNext', state.blockNext); + RuntimeService.previousState.blockNow = structuredClone(state.blockNow); + RuntimeService.previousState.blockNext = structuredClone(state.blockNext); } if (hasImmediateChanges) { @@ -857,7 +861,7 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert addedTime: state.timer.addedTime, pausedAt: state._timer.pausedAt, firstStart: state.runtime.actualStart, - blockStartAt: state.currentBlock.startedAt, + blockStartAt: state.blockNow?.startedAt ?? null, }) .catch((_e) => { //we don't do anything with the error here diff --git a/apps/server/src/stores/__mocks__/runtimeState.mocks.ts b/apps/server/src/stores/__mocks__/runtimeState.mocks.ts index 177c532e7..932972a2e 100644 --- a/apps/server/src/stores/__mocks__/runtimeState.mocks.ts +++ b/apps/server/src/stores/__mocks__/runtimeState.mocks.ts @@ -4,12 +4,10 @@ import type { RuntimeState } from '../runtimeState.js'; const baseState: RuntimeState = { clock: 0, - currentBlock: { - block: null, - startedAt: null, - }, eventNow: null, eventNext: null, + blockNow: null, + blockNext: null, runtime: { selectedEventIndex: null, numEvents: 0, diff --git a/apps/server/src/stores/__tests__/runtimeState.test.ts b/apps/server/src/stores/__tests__/runtimeState.test.ts index e9353d010..40fa29044 100644 --- a/apps/server/src/stores/__tests__/runtimeState.test.ts +++ b/apps/server/src/stores/__tests__/runtimeState.test.ts @@ -104,7 +104,7 @@ describe('mutation on runtimeState', () => { expect(newState.eventNext?.id).toBe('event2'); expect(newState.timer.playback).toBe(Playback.Armed); expect(newState.clock).not.toBe(666); - expect(newState.currentBlock.block).toBeNull(); + expect(newState.blockNow).toBeNull(); // 2. Start event let success = start(); @@ -184,7 +184,7 @@ describe('mutation on runtimeState', () => { expect(newState.runtime.actualStart).toBeNull(); expect(newState.runtime.plannedStart).toBe(0); expect(newState.runtime.plannedEnd).toBe(1500); - expect(newState.currentBlock.block).toBeNull(); + expect(newState.blockNow).toBeNull(); expect(newState.runtime.offset).toBe(0); // 2. Start event @@ -217,7 +217,7 @@ describe('mutation on runtimeState', () => { expect(newState.runtime.offset).toBe(delayBefore); // finish is the difference between the runtime and the schedule expect(newState.runtime.expectedEnd).toBe(entries.event2.timeEnd - newState.runtime.offset); - expect(newState.currentBlock.block).toBeNull(); + expect(newState.blockNow).toBeNull(); // 4. Add time addTime(10); @@ -384,17 +384,14 @@ describe('loadBlock', () => { }); const state = { - currentBlock: { - block: null, - startedAt: 123, - }, + blockNow: null, eventNow: rundown.entries[11], - } as RuntimeState; + } as unknown as RuntimeState; loadBlock(rundown, state); expect(state).toMatchObject({ - currentBlock: { block: rundown.entries[1], startedAt: null }, + blockNow: { id: rundown.entries[1].id, startedAt: null }, eventNow: rundown.entries[11], }); }); @@ -412,17 +409,14 @@ describe('loadBlock', () => { }); const state = { - currentBlock: { - block: rundown.entries[1], - startedAt: 123, - }, + blockNow: { id: rundown.entries[1].id, startedAt: 123 }, eventNow: rundown.entries[22], } as RuntimeState; loadBlock(rundown, state); expect(state).toMatchObject({ - currentBlock: { block: rundown.entries[2], startedAt: null }, + blockNow: { id: rundown.entries[2].id, startedAt: null }, eventNow: rundown.entries[22], }); }); @@ -440,8 +434,8 @@ describe('loadBlock', () => { }); const state = { - currentBlock: { - block: rundown.entries[1], + blockNow: { + id: rundown.entries[1].id, startedAt: 123, }, eventNow: rundown.entries[0], @@ -450,7 +444,7 @@ describe('loadBlock', () => { loadBlock(rundown, state); expect(state).toMatchObject({ - currentBlock: { block: null, startedAt: null }, + blockNow: null, eventNow: rundown.entries[0], }); }); @@ -466,17 +460,13 @@ describe('loadBlock', () => { }); const state = { - currentBlock: { - block: rundown.entries[0], - startedAt: 123, - }, + blockNow: { id: rundown.entries[0].id, startedAt: 123 }, eventNow: rundown.entries[2], } as RuntimeState; loadBlock(rundown, state); - expect(state).toMatchObject({ - currentBlock: { block: rundown.entries[0], startedAt: 123 }, + blockNow: { id: rundown.entries[0].id, startedAt: 123 }, eventNow: rundown.entries[2], }); }); @@ -491,17 +481,14 @@ describe('loadBlock', () => { }); const state = { - currentBlock: { - block: null, - startedAt: 123, - }, + blockNow: null, eventNow: rundown.entries[0], } as RuntimeState; loadBlock(rundown, state); expect(state).toMatchObject({ - currentBlock: { block: null, startedAt: 123 }, + blockNow: null, eventNow: rundown.entries[0], }); }); diff --git a/apps/server/src/stores/runtimeState.ts b/apps/server/src/stores/runtimeState.ts index d9f1e0483..88b992492 100644 --- a/apps/server/src/stores/runtimeState.ts +++ b/apps/server/src/stores/runtimeState.ts @@ -1,9 +1,9 @@ import { - CurrentBlockState, + BlockState, + isOntimeBlock, MaybeNumber, MaybeString, OffsetMode, - OntimeBlock, PlayableEvent, Playback, Rundown, @@ -30,8 +30,9 @@ import { getPlayableIndexFromTimedIndex } from '../api-data/rundown/rundown.util export type RuntimeState = { clock: number; // realtime clock + blockNow: BlockState | null; + blockNext: BlockState | null; eventNow: PlayableEvent | null; - currentBlock: CurrentBlockState; eventNext: PlayableEvent | null; runtime: Runtime; timer: TimerState; @@ -48,7 +49,8 @@ export type RuntimeState = { const runtimeState: RuntimeState = { clock: timeNow(), - currentBlock: { ...runtimeStorePlaceholder.currentBlock }, + blockNow: null, + blockNext: null, eventNow: null, eventNext: null, runtime: { ...runtimeStorePlaceholder.runtime }, @@ -103,8 +105,8 @@ export function clearState() { runtimeState.eventNow = null; runtimeState.eventNext = null; - runtimeState.currentBlock.block = null; - runtimeState.currentBlock.startedAt = null; + runtimeState.blockNow = null; + runtimeState.blockNext = null; runtimeState.runtime.offset = 0; runtimeState.runtime.relativeOffset = 0; @@ -207,8 +209,8 @@ export function load( runtimeState.runtime.relativeOffset = relativeOffset; runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState); } - if (typeof initialData.blockStartAt === 'number') { - runtimeState.currentBlock.startedAt = initialData.blockStartAt; + if (typeof initialData.blockStartAt === 'number' && runtimeState.blockNow) { + runtimeState.blockNow.startedAt = initialData.blockStartAt; } } return event.id === runtimeState.eventNow?.id; @@ -249,7 +251,7 @@ export function loadNext( } 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 runtimeState.eventNext = null; return; @@ -355,8 +357,8 @@ export function start(state: RuntimeState = runtimeState): boolean { } // update block start time - if (state.currentBlock.startedAt === null) { - state.currentBlock.startedAt = state.clock; + if (state.blockNow && state.blockNow.startedAt === null) { + state.blockNow.startedAt = state.clock; } state.timer.playback = Playback.Play; @@ -581,8 +583,8 @@ export function roll( runtimeState.timer.startedAt = runtimeState.clock; // update runtime - if (runtimeState.currentBlock.startedAt === null) { - runtimeState.currentBlock.startedAt = runtimeState.clock; + if (runtimeState.blockNow && runtimeState.blockNow.startedAt === null) { + runtimeState.blockNow.startedAt = runtimeState.clock; } if (!runtimeState.runtime.actualStart) { runtimeState.runtime.actualStart = runtimeState.clock; @@ -645,8 +647,8 @@ export function roll( // there is something to run, load event // update runtime - if (runtimeState.currentBlock.startedAt === null) { - runtimeState.currentBlock.startedAt = runtimeState.clock; + if (runtimeState.blockNow && runtimeState.blockNow.startedAt === null) { + runtimeState.blockNow.startedAt = runtimeState.clock; } // event will finish on time @@ -675,28 +677,38 @@ export function roll( * handle block loading, not for use outside of runtimeState */ export function loadBlock(rundown: Rundown, state = runtimeState) { + // we need a loaded event to have a block if (state.eventNow === null) { - // we need a loaded event to have a block - state.currentBlock.block = null; - state.currentBlock.startedAt = null; + state.blockNow = null; + state.blockNext = null; return; } const currentBlockId = state.eventNow.parent; - // update time only if the block has changed - if (state.currentBlock.block?.id != currentBlockId) { - state.currentBlock.startedAt = null; + // look for potential next block + let foundEventNow = false; + 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) { - state.currentBlock.block = null; + state.blockNow = null; return; } - const currentBlock = rundown.entries[currentBlockId]; - state.currentBlock.block = currentBlock as OntimeBlock; + //we went into a new block - and it is different from the one we might have come from + 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) { diff --git a/packages/types/src/definitions/runtime/CurrentBlockState.type.ts b/packages/types/src/definitions/runtime/CurrentBlockState.type.ts index 4e6fd80e4..f32d8cff3 100644 --- a/packages/types/src/definitions/runtime/CurrentBlockState.type.ts +++ b/packages/types/src/definitions/runtime/CurrentBlockState.type.ts @@ -1,7 +1,7 @@ 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 = { - block: OntimeBlock | null; +export type BlockState = { + id: EntryId; startedAt: MaybeNumber; }; diff --git a/packages/types/src/definitions/runtime/RuntimeStore.ts b/packages/types/src/definitions/runtime/RuntimeStore.ts index 62c0efa21..52059be3f 100644 --- a/packages/types/src/definitions/runtime/RuntimeStore.ts +++ b/packages/types/src/definitions/runtime/RuntimeStore.ts @@ -40,10 +40,8 @@ export const runtimeStorePlaceholder: Readonly = { expectedEnd: null, // changes with runtime, based on offset, overflows over dayInMs offsetMode: OffsetMode.Absolute, }, - currentBlock: { - block: null, - startedAt: null, - }, + blockNow: null, + blockNext: null, eventNow: null, eventNext: null, auxtimer1: { diff --git a/packages/types/src/definitions/runtime/RuntimeStore.type.ts b/packages/types/src/definitions/runtime/RuntimeStore.type.ts index eaa0de41c..0aad41ebc 100644 --- a/packages/types/src/definitions/runtime/RuntimeStore.type.ts +++ b/packages/types/src/definitions/runtime/RuntimeStore.type.ts @@ -1,6 +1,6 @@ import type { OntimeEvent } from '../core/OntimeEvent.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 { Runtime } from './Runtime.type.js'; import type { TimerState } from './TimerState.type.js'; @@ -16,9 +16,11 @@ export type RuntimeStore = { // rundown data runtime: Runtime; - currentBlock: CurrentBlockState; eventNow: OntimeEvent | null; eventNext: OntimeEvent | null; + + blockNow: BlockState | null; + blockNext: BlockState | null; // extra timers auxtimer1: SimpleTimerState; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 2d22cee08..2a7886c8d 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -101,7 +101,7 @@ export { OffsetMode } from './definitions/runtime/Runtime.type.js'; export type { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js'; export { runtimeStorePlaceholder } from './definitions/runtime/RuntimeStore.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 export { type SimpleTimerState, SimplePlayback, SimpleDirection } from './definitions/runtime/AuxTimer.type.js';