More batch (#1468)

* batch updates in client

* rearange updating logic

* utilety type

* guard undefinde in affectsLoaded

* fix: pause state not saved to restore point
This commit is contained in:
Alex Christoffer Rasmussen
2025-01-26 13:14:24 +01:00
committed by GitHub
parent bb1e9072fd
commit d34280c03d
6 changed files with 112 additions and 62 deletions
+12
View File
@@ -14,6 +14,18 @@ export const runtimeStore = createWithEqualityFn<RuntimeStore>(
export const useRuntimeStore = <T>(selector: (state: RuntimeStore) => T) => export const useRuntimeStore = <T>(selector: (state: RuntimeStore) => T) =>
useStoreWithEqualityFn(runtimeStore, selector, deepCompare); useStoreWithEqualityFn(runtimeStore, selector, deepCompare);
let batchStore: Partial<RuntimeStore> = {};
export function addToBatchUpdates<K extends keyof RuntimeStore>(key: K, value: RuntimeStore[K]) {
batchStore[key] = value;
}
export function flushBatchUpdates() {
const state = runtimeStore.getState();
runtimeStore.setState({ ...state, ...batchStore });
batchStore = {};
}
/** /**
* Allows patching a property of the runtime store * Allows patching a property of the runtime store
*/ */
+16 -12
View File
@@ -14,7 +14,7 @@ import {
} from '../stores/clientStore'; } from '../stores/clientStore';
import { addDialog } from '../stores/dialogStore'; import { addDialog } from '../stores/dialogStore';
import { addLog } from '../stores/logger'; import { addLog } from '../stores/logger';
import { patchRuntime, patchRuntimeProperty } from '../stores/runtime'; import { addToBatchUpdates, flushBatchUpdates, patchRuntime, patchRuntimeProperty } from '../stores/runtime';
export let websocket: WebSocket | null = null; export let websocket: WebSocket | null = null;
let reconnectTimeout: NodeJS.Timeout | null = null; let reconnectTimeout: NodeJS.Timeout | null = null;
@@ -140,57 +140,57 @@ export const connectSocket = () => {
break; break;
} }
case 'ontime-clock': { case 'ontime-clock': {
patchRuntimeProperty('clock', payload); addToBatchUpdates('clock', payload);
updateDevTools({ clock: payload }); updateDevTools({ clock: payload });
break; break;
} }
case 'ontime-timer': { case 'ontime-timer': {
patchRuntimeProperty('timer', payload); addToBatchUpdates('timer', payload);
updateDevTools({ timer: payload }); updateDevTools({ timer: payload });
break; break;
} }
case 'ontime-onAir': { case 'ontime-onAir': {
patchRuntimeProperty('onAir', payload); addToBatchUpdates('onAir', payload);
updateDevTools({ onAir: payload }); updateDevTools({ onAir: payload });
break; break;
} }
case 'ontime-message': { case 'ontime-message': {
patchRuntimeProperty('message', payload); addToBatchUpdates('message', payload);
updateDevTools({ message: payload }); updateDevTools({ message: payload });
break; break;
} }
case 'ontime-runtime': { case 'ontime-runtime': {
patchRuntimeProperty('runtime', payload); addToBatchUpdates('runtime', payload);
updateDevTools({ runtime: payload }); updateDevTools({ runtime: payload });
break; break;
} }
case 'ontime-eventNow': { case 'ontime-eventNow': {
patchRuntimeProperty('eventNow', payload); addToBatchUpdates('eventNow', payload);
updateDevTools({ eventNow: payload }); updateDevTools({ eventNow: payload });
break; break;
} }
case 'ontime-currentBlock': { case 'ontime-currentBlock': {
patchRuntimeProperty('currentBlock', payload); addToBatchUpdates('currentBlock', payload);
updateDevTools({ currentBlock: payload }); updateDevTools({ currentBlock: payload });
break; break;
} }
case 'ontime-publicEventNow': { case 'ontime-publicEventNow': {
patchRuntimeProperty('publicEventNow', payload); addToBatchUpdates('publicEventNow', payload);
updateDevTools({ publicEventNow: payload }); updateDevTools({ publicEventNow: payload });
break; break;
} }
case 'ontime-eventNext': { case 'ontime-eventNext': {
patchRuntimeProperty('eventNext', payload); addToBatchUpdates('eventNext', payload);
updateDevTools({ eventNext: payload }); updateDevTools({ eventNext: payload });
break; break;
} }
case 'ontime-publicEventNext': { case 'ontime-publicEventNext': {
patchRuntimeProperty('publicEventNext', payload); addToBatchUpdates('publicEventNext', payload);
updateDevTools({ publicEventNext: payload }); updateDevTools({ publicEventNext: payload });
break; break;
} }
case 'ontime-auxtimer1': { case 'ontime-auxtimer1': {
patchRuntimeProperty('auxtimer1', payload); addToBatchUpdates('auxtimer1', payload);
updateDevTools({ auxtimer1: payload }); updateDevTools({ auxtimer1: payload });
break; break;
} }
@@ -207,6 +207,10 @@ export const connectSocket = () => {
} }
break; break;
} }
case 'ontime-flush': {
flushBatchUpdates()
break;
}
} }
} catch (_) { } catch (_) {
// ignore unhandled // ignore unhandled
@@ -6,7 +6,6 @@ import {
MaybeNumber, MaybeNumber,
OntimeEvent, OntimeEvent,
Playback, Playback,
RuntimeStore,
TimerLifeCycle, TimerLifeCycle,
TimerPhase, TimerPhase,
TimerState, TimerState,
@@ -22,7 +21,7 @@ import { timerConfig } from '../../config/config.js';
import { eventStore } from '../../stores/EventStore.js'; import { eventStore } from '../../stores/EventStore.js';
import { EventTimer } from '../EventTimer.js'; import { EventTimer } from '../EventTimer.js';
import { type RestorePoint, restoreService } from '../RestoreService.js'; import { RestorePoint, restoreService } from '../RestoreService.js';
import { import {
findNext, findNext,
findPrevious, findPrevious,
@@ -37,6 +36,8 @@ import { getForceUpdate, getShouldClockUpdate, getShouldTimerUpdate } from './ru
import { skippedOutOfEvent } from '../timerUtils.js'; import { skippedOutOfEvent } from '../timerUtils.js';
import { triggerAutomations } from '../../api-data/automation/automation.service.js'; import { triggerAutomations } from '../../api-data/automation/automation.service.js';
type RuntimeStateEventKeys = keyof Pick<RuntimeState, 'eventNext' | 'eventNow' | 'publicEventNow' | 'publicEventNext'>;
/** /**
* Service manages runtime status of app * Service manages runtime status of app
* Coordinating with necessary services * Coordinating with necessary services
@@ -181,10 +182,10 @@ class RuntimeService {
const next = state.eventNext?.id; const next = state.eventNext?.id;
const nextPublic = state.publicEventNext?.id; const nextPublic = state.publicEventNext?.id;
return ( return (
affectedIds.includes(now) || (now !== undefined && affectedIds.includes(now)) ||
affectedIds.includes(nowPublic) || (nowPublic !== undefined && affectedIds.includes(nowPublic)) ||
affectedIds.includes(next) || (next !== undefined && affectedIds.includes(next)) ||
affectedIds.includes(nextPublic) (nextPublic !== undefined && affectedIds.includes(nextPublic))
); );
} }
@@ -681,71 +682,100 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
// we do the comparison by explicitly for each property // we do the comparison by explicitly for each property
// to apply custom logic for different datasets // to apply custom logic for different datasets
const shouldForceTimerUpdate = getForceUpdate( // if a new event was loaded most things should update
RuntimeService.previousTimerUpdate,
state.clock,
state.timer.playback,
);
const shouldUpdateTimer =
shouldForceTimerUpdate || getShouldTimerUpdate(RuntimeService.previousTimerValue, state.timer.current);
const shouldRuntimeUpdate = shouldUpdateTimer || getForceUpdate(RuntimeService.previousRuntimeUpdate, state.clock);
// some changes need an immediate update
const hasNewLoaded = state.eventNow?.id !== RuntimeService.previousState?.eventNow?.id; const hasNewLoaded = state.eventNow?.id !== RuntimeService.previousState?.eventNow?.id;
// for the very fist run there will be nothing in the previousState so we force an update
const justStarted = !RuntimeService.previousState?.timer; const justStarted = !RuntimeService.previousState?.timer;
// if playback changes most things should update
const hasChangedPlayback = RuntimeService.previousState.timer?.playback !== state.timer.playback; const hasChangedPlayback = RuntimeService.previousState.timer?.playback !== state.timer.playback;
// combine all big changes
const hasImmediateChanges = hasNewLoaded || justStarted || hasChangedPlayback; const hasImmediateChanges = hasNewLoaded || justStarted || hasChangedPlayback;
/**
* Timer should be updated if
* - big changes
* - notification rate has been exceeded
* - the timer has rolled over into the next UI display unit
*
* Then check if there is actually a change in the data
*/
const shouldUpdateTimer =
(hasImmediateChanges ||
getForceUpdate(RuntimeService.previousTimerUpdate, state.clock) ||
getShouldTimerUpdate(RuntimeService.previousTimerValue, state.timer.current)) &&
!deepEqual(RuntimeService.previousState?.timer, state.timer);
/**
* Runtime should be updated if
* - big changes
* - the timer is updating so runtime also updates to keep them in sync ???
* - notification rate has been exceeded
*
* Then check if there is actually a change in the data
*/
const shouldRuntimeUpdate =
(hasImmediateChanges || shouldUpdateTimer || getForceUpdate(RuntimeService.previousRuntimeUpdate, state.clock)) &&
!deepEqual(RuntimeService.previousState?.runtime, state.runtime);
/**
* the currentBlock object has no ticking values so we only need to check for equality
*/
const shouldBlockUpdate = !deepEqual(RuntimeService?.previousState.currentBlock, state.currentBlock);
/**
* Many other values are calculated based on the clock
* so if any of them are updated we also need to send the clock
* in case nothing else is updating the clock will bw updated at the notification rate
*/
const shouldUpdateClock =
shouldUpdateTimer ||
shouldRuntimeUpdate ||
shouldBlockUpdate ||
getForceUpdate(RuntimeService.previousClockUpdate, state.clock);
//Now we set all the updates on the eventstore and update the previous value
if (hasChangedPlayback) { if (hasChangedPlayback) {
eventStore.set('onAir', state.timer.playback !== Playback.Stop); eventStore.set('onAir', state.timer.playback !== Playback.Stop);
} }
if (hasImmediateChanges || (shouldUpdateTimer && !deepEqual(RuntimeService.previousState?.timer, state.timer))) { if (shouldUpdateTimer) {
eventStore.set('timer', state.timer);
RuntimeService.previousTimerUpdate = state.clock; RuntimeService.previousTimerUpdate = state.clock;
RuntimeService.previousTimerValue = state.timer.current; RuntimeService.previousTimerValue = state.timer.current;
RuntimeService.previousClockUpdate = state.clock;
eventStore.set('clock', state.clock);
eventStore.set('timer', state.timer);
RuntimeService.previousState.timer = { ...state.timer }; RuntimeService.previousState.timer = { ...state.timer };
} }
if ( if (shouldRuntimeUpdate) {
hasChangedPlayback ||
(shouldRuntimeUpdate && !deepEqual(RuntimeService.previousState?.runtime, state.runtime))
) {
eventStore.set('runtime', state.runtime); eventStore.set('runtime', state.runtime);
RuntimeService.previousClockUpdate = state.clock;
RuntimeService.previousRuntimeUpdate = state.clock; RuntimeService.previousRuntimeUpdate = state.clock;
eventStore.set('clock', state.clock);
RuntimeService.previousState.runtime = { ...state.runtime }; RuntimeService.previousState.runtime = { ...state.runtime };
} }
if (shouldBlockUpdate) {
eventStore.set('currentBlock', state.currentBlock);
RuntimeService.previousState.currentBlock = { ...state.currentBlock };
}
if (hasImmediateChanges) {
saveRestoreState(state);
}
if (shouldUpdateClock) {
RuntimeService.previousClockUpdate = state.clock;
eventStore.set('clock', state.clock);
}
// Update the events if they have changed // Update the events if they have changed
updateEventIfChanged('eventNow', state); updateEventIfChanged('eventNow', state);
updateEventIfChanged('publicEventNow', state); updateEventIfChanged('publicEventNow', state);
updateEventIfChanged('eventNext', state); updateEventIfChanged('eventNext', state);
updateEventIfChanged('publicEventNext', state); updateEventIfChanged('publicEventNext', state);
if (!deepEqual(RuntimeService?.previousState.currentBlock, state.currentBlock)) {
eventStore.set('currentBlock', state.currentBlock);
RuntimeService.previousState.currentBlock = { ...state.currentBlock };
RuntimeService.previousClockUpdate = state.clock;
eventStore.set('clock', state.clock);
}
const shouldUpdateClock = getShouldClockUpdate(RuntimeService.previousClockUpdate, state.clock);
if (shouldUpdateClock) {
RuntimeService.previousClockUpdate = state.clock;
eventStore.set('clock', state.clock);
saveRestoreState(state);
}
// Helper function to update an event if it has changed // Helper function to update an event if it has changed
function updateEventIfChanged(eventKey: keyof RuntimeStore, state: runtimeState.RuntimeState) { function updateEventIfChanged(eventKey: RuntimeStateEventKeys, state: runtimeState.RuntimeState) {
const previous = RuntimeService.previousState?.[eventKey]; const previous = RuntimeService.previousState?.[eventKey];
const now = state[eventKey]; const now = state[eventKey];
@@ -766,7 +796,7 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
return; return;
} }
function storeKey(eventKey: keyof RuntimeStore) { function storeKey(eventKey: RuntimeStateEventKeys) {
eventStore.set(eventKey, state[eventKey]); eventStore.set(eventKey, state[eventKey]);
RuntimeService.previousState[eventKey] = { ...state[eventKey] }; RuntimeService.previousState[eventKey] = { ...state[eventKey] };
} }
@@ -1,7 +1,7 @@
import { millisToSeconds } from 'ontime-utils'; import { millisToSeconds } from 'ontime-utils';
import { timerConfig } from '../../config/config.js'; import { timerConfig } from '../../config/config.js';
import { MaybeNumber, Playback } from 'ontime-types'; import { MaybeNumber } from 'ontime-types';
/** /**
* Checks whether we should update the clock value * Checks whether we should update the clock value
@@ -21,7 +21,7 @@ export function getShouldClockUpdate(previousUpdate: number, now: number): boole
* Checks whether we should update the timer value * Checks whether we should update the timer value
* - we have rolled into a new seconds unit * - we have rolled into a new seconds unit
*/ */
export function getShouldTimerUpdate(previousValue: number, currentValue: MaybeNumber): boolean { export function getShouldTimerUpdate(previousValue: MaybeNumber, currentValue: MaybeNumber): boolean {
if (currentValue === null) { if (currentValue === null) {
return false; return false;
} }
@@ -36,8 +36,7 @@ export function getShouldTimerUpdate(previousValue: number, currentValue: MaybeN
* - if we have escaped the update rate (clock slid forward) * - if we have escaped the update rate (clock slid forward)
* - if we are not playing then there is no need to update the timer * - if we are not playing then there is no need to update the timer
*/ */
export function getForceUpdate(previousUpdate: number, now: number, playbackState: Playback = Playback.Play): boolean { export function getForceUpdate(previousUpdate: number, now: number): boolean {
if (playbackState !== Playback.Play) return false;
const isClockBehind = now < previousUpdate; const isClockBehind = now < previousUpdate;
const hasExceededRate = now - previousUpdate >= timerConfig.notificationRate; const hasExceededRate = now - previousUpdate >= timerConfig.notificationRate;
return isClockBehind || hasExceededRate; return isClockBehind || hasExceededRate;
+1
View File
@@ -35,6 +35,7 @@ export const eventStore = {
for (const dataKey of changedKeys) { for (const dataKey of changedKeys) {
socket.sendAsJson({ type: `ontime-${dataKey}`, payload: store[dataKey] }); socket.sendAsJson({ type: `ontime-${dataKey}`, payload: store[dataKey] });
} }
socket.sendAsJson({ type: `ontime-flush` });
isUpdatePending = null; isUpdatePending = null;
changedKeys.clear(); changedKeys.clear();
}); });
+5 -1
View File
@@ -136,10 +136,14 @@ export function clear() {
* Utility to allow modifying the state from the outside * Utility to allow modifying the state from the outside
* @param newState * @param newState
*/ */
function patchTimer(newState: Partial<TimerState>) { function patchTimer(newState: Partial<TimerState & RestorePoint>) {
for (const key in newState) { for (const key in newState) {
if (key in runtimeState.timer) { if (key in runtimeState.timer) {
runtimeState.timer[key] = newState[key]; runtimeState.timer[key] = newState[key];
} else if (key in runtimeState._timer) {
// in case of a RestorePoint we will receive a pausedAt value
// wiche is needed to resume a paused timer
runtimeState._timer[key] = newState[key];
} }
} }
} }