mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-10 09:53:48 +00:00
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:
committed by
GitHub
parent
bb1e9072fd
commit
d34280c03d
@@ -14,6 +14,18 @@ export const runtimeStore = createWithEqualityFn<RuntimeStore>(
|
||||
export const useRuntimeStore = <T>(selector: (state: RuntimeStore) => T) =>
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from '../stores/clientStore';
|
||||
import { addDialog } from '../stores/dialogStore';
|
||||
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;
|
||||
let reconnectTimeout: NodeJS.Timeout | null = null;
|
||||
@@ -140,57 +140,57 @@ export const connectSocket = () => {
|
||||
break;
|
||||
}
|
||||
case 'ontime-clock': {
|
||||
patchRuntimeProperty('clock', payload);
|
||||
addToBatchUpdates('clock', payload);
|
||||
updateDevTools({ clock: payload });
|
||||
break;
|
||||
}
|
||||
case 'ontime-timer': {
|
||||
patchRuntimeProperty('timer', payload);
|
||||
addToBatchUpdates('timer', payload);
|
||||
updateDevTools({ timer: payload });
|
||||
break;
|
||||
}
|
||||
case 'ontime-onAir': {
|
||||
patchRuntimeProperty('onAir', payload);
|
||||
addToBatchUpdates('onAir', payload);
|
||||
updateDevTools({ onAir: payload });
|
||||
break;
|
||||
}
|
||||
case 'ontime-message': {
|
||||
patchRuntimeProperty('message', payload);
|
||||
addToBatchUpdates('message', payload);
|
||||
updateDevTools({ message: payload });
|
||||
break;
|
||||
}
|
||||
case 'ontime-runtime': {
|
||||
patchRuntimeProperty('runtime', payload);
|
||||
addToBatchUpdates('runtime', payload);
|
||||
updateDevTools({ runtime: payload });
|
||||
break;
|
||||
}
|
||||
case 'ontime-eventNow': {
|
||||
patchRuntimeProperty('eventNow', payload);
|
||||
addToBatchUpdates('eventNow', payload);
|
||||
updateDevTools({ eventNow: payload });
|
||||
break;
|
||||
}
|
||||
case 'ontime-currentBlock': {
|
||||
patchRuntimeProperty('currentBlock', payload);
|
||||
addToBatchUpdates('currentBlock', payload);
|
||||
updateDevTools({ currentBlock: payload });
|
||||
break;
|
||||
}
|
||||
case 'ontime-publicEventNow': {
|
||||
patchRuntimeProperty('publicEventNow', payload);
|
||||
addToBatchUpdates('publicEventNow', payload);
|
||||
updateDevTools({ publicEventNow: payload });
|
||||
break;
|
||||
}
|
||||
case 'ontime-eventNext': {
|
||||
patchRuntimeProperty('eventNext', payload);
|
||||
addToBatchUpdates('eventNext', payload);
|
||||
updateDevTools({ eventNext: payload });
|
||||
break;
|
||||
}
|
||||
case 'ontime-publicEventNext': {
|
||||
patchRuntimeProperty('publicEventNext', payload);
|
||||
addToBatchUpdates('publicEventNext', payload);
|
||||
updateDevTools({ publicEventNext: payload });
|
||||
break;
|
||||
}
|
||||
case 'ontime-auxtimer1': {
|
||||
patchRuntimeProperty('auxtimer1', payload);
|
||||
addToBatchUpdates('auxtimer1', payload);
|
||||
updateDevTools({ auxtimer1: payload });
|
||||
break;
|
||||
}
|
||||
@@ -207,6 +207,10 @@ export const connectSocket = () => {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ontime-flush': {
|
||||
flushBatchUpdates()
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore unhandled
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
MaybeNumber,
|
||||
OntimeEvent,
|
||||
Playback,
|
||||
RuntimeStore,
|
||||
TimerLifeCycle,
|
||||
TimerPhase,
|
||||
TimerState,
|
||||
@@ -22,7 +21,7 @@ import { timerConfig } from '../../config/config.js';
|
||||
import { eventStore } from '../../stores/EventStore.js';
|
||||
|
||||
import { EventTimer } from '../EventTimer.js';
|
||||
import { type RestorePoint, restoreService } from '../RestoreService.js';
|
||||
import { RestorePoint, restoreService } from '../RestoreService.js';
|
||||
import {
|
||||
findNext,
|
||||
findPrevious,
|
||||
@@ -37,6 +36,8 @@ import { getForceUpdate, getShouldClockUpdate, getShouldTimerUpdate } from './ru
|
||||
import { skippedOutOfEvent } from '../timerUtils.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
|
||||
* Coordinating with necessary services
|
||||
@@ -181,10 +182,10 @@ class RuntimeService {
|
||||
const next = state.eventNext?.id;
|
||||
const nextPublic = state.publicEventNext?.id;
|
||||
return (
|
||||
affectedIds.includes(now) ||
|
||||
affectedIds.includes(nowPublic) ||
|
||||
affectedIds.includes(next) ||
|
||||
affectedIds.includes(nextPublic)
|
||||
(now !== undefined && affectedIds.includes(now)) ||
|
||||
(nowPublic !== undefined && affectedIds.includes(nowPublic)) ||
|
||||
(next !== undefined && affectedIds.includes(next)) ||
|
||||
(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
|
||||
// to apply custom logic for different datasets
|
||||
|
||||
const shouldForceTimerUpdate = getForceUpdate(
|
||||
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
|
||||
// if a new event was loaded most things should update
|
||||
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;
|
||||
|
||||
// if playback changes most things should update
|
||||
const hasChangedPlayback = RuntimeService.previousState.timer?.playback !== state.timer.playback;
|
||||
|
||||
// combine all big changes
|
||||
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) {
|
||||
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.previousTimerValue = state.timer.current;
|
||||
RuntimeService.previousClockUpdate = state.clock;
|
||||
eventStore.set('clock', state.clock);
|
||||
eventStore.set('timer', state.timer);
|
||||
RuntimeService.previousState.timer = { ...state.timer };
|
||||
}
|
||||
|
||||
if (
|
||||
hasChangedPlayback ||
|
||||
(shouldRuntimeUpdate && !deepEqual(RuntimeService.previousState?.runtime, state.runtime))
|
||||
) {
|
||||
if (shouldRuntimeUpdate) {
|
||||
eventStore.set('runtime', state.runtime);
|
||||
RuntimeService.previousClockUpdate = state.clock;
|
||||
RuntimeService.previousRuntimeUpdate = state.clock;
|
||||
eventStore.set('clock', state.clock);
|
||||
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
|
||||
updateEventIfChanged('eventNow', state);
|
||||
updateEventIfChanged('publicEventNow', state);
|
||||
updateEventIfChanged('eventNext', 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
|
||||
function updateEventIfChanged(eventKey: keyof RuntimeStore, state: runtimeState.RuntimeState) {
|
||||
function updateEventIfChanged(eventKey: RuntimeStateEventKeys, state: runtimeState.RuntimeState) {
|
||||
const previous = RuntimeService.previousState?.[eventKey];
|
||||
const now = state[eventKey];
|
||||
|
||||
@@ -766,7 +796,7 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
|
||||
return;
|
||||
}
|
||||
|
||||
function storeKey(eventKey: keyof RuntimeStore) {
|
||||
function storeKey(eventKey: RuntimeStateEventKeys) {
|
||||
eventStore.set(eventKey, state[eventKey]);
|
||||
RuntimeService.previousState[eventKey] = { ...state[eventKey] };
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { millisToSeconds } from 'ontime-utils';
|
||||
|
||||
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
|
||||
@@ -21,7 +21,7 @@ export function getShouldClockUpdate(previousUpdate: number, now: number): boole
|
||||
* Checks whether we should update the timer value
|
||||
* - 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) {
|
||||
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 are not playing then there is no need to update the timer
|
||||
*/
|
||||
export function getForceUpdate(previousUpdate: number, now: number, playbackState: Playback = Playback.Play): boolean {
|
||||
if (playbackState !== Playback.Play) return false;
|
||||
export function getForceUpdate(previousUpdate: number, now: number): boolean {
|
||||
const isClockBehind = now < previousUpdate;
|
||||
const hasExceededRate = now - previousUpdate >= timerConfig.notificationRate;
|
||||
return isClockBehind || hasExceededRate;
|
||||
|
||||
@@ -35,6 +35,7 @@ export const eventStore = {
|
||||
for (const dataKey of changedKeys) {
|
||||
socket.sendAsJson({ type: `ontime-${dataKey}`, payload: store[dataKey] });
|
||||
}
|
||||
socket.sendAsJson({ type: `ontime-flush` });
|
||||
isUpdatePending = null;
|
||||
changedKeys.clear();
|
||||
});
|
||||
|
||||
@@ -136,10 +136,14 @@ export function clear() {
|
||||
* Utility to allow modifying the state from the outside
|
||||
* @param newState
|
||||
*/
|
||||
function patchTimer(newState: Partial<TimerState>) {
|
||||
function patchTimer(newState: Partial<TimerState & RestorePoint>) {
|
||||
for (const key in newState) {
|
||||
if (key in runtimeState.timer) {
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user