mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-25 17:09:09 +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) =>
|
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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user