refactor: invalidate changed rundown

This commit is contained in:
Carlos Valente
2026-04-25 07:49:01 +02:00
committed by Carlos Valente
parent ab8da07518
commit 3bebbf9ce9
6 changed files with 68 additions and 54 deletions
@@ -40,6 +40,7 @@ export default function useRundown() {
queryClient.setQueryData(getRundownQueryKey(data.id), data); queryClient.setQueryData(getRundownQueryKey(data.id), data);
}, [data, loadedRundownId, queryClient]); }, [data, loadedRundownId, queryClient]);
// Once we have the ID, drop the temporary current cache
useEffect(() => { useEffect(() => {
if (!loadedRundownId) return; if (!loadedRundownId) return;
queryClient.removeQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true }); queryClient.removeQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true });
+22 -29
View File
@@ -3,9 +3,7 @@ import {
Log, Log,
MaybeNumber, MaybeNumber,
MessageTag, MessageTag,
ProjectRundownsList,
RefetchKey, RefetchKey,
Rundown,
RuntimeStore, RuntimeStore,
WsPacketToClient, WsPacketToClient,
WsPacketToServer, WsPacketToServer,
@@ -17,15 +15,16 @@ import {
CLIENT_LIST, CLIENT_LIST,
CSS_OVERRIDE, CSS_OVERRIDE,
CUSTOM_FIELDS, CUSTOM_FIELDS,
PROJECT_DATA,
CURRENT_RUNDOWN_QUERY_KEY, CURRENT_RUNDOWN_QUERY_KEY,
PROJECT_RUNDOWNS, PROJECT_DATA,
REPORT, REPORT,
RUNDOWN,
RUNTIME, RUNTIME,
TRANSLATION, TRANSLATION,
URL_PRESETS, URL_PRESETS,
VIEW_SETTINGS, VIEW_SETTINGS,
getRundownQueryKey, getRundownQueryKey,
PROJECT_RUNDOWNS,
} from '../api/constants'; } from '../api/constants';
import { invalidateAllCaches } from '../api/utils'; import { invalidateAllCaches } from '../api/utils';
import { ontimeQueryClient } from '../queryClient'; import { ontimeQueryClient } from '../queryClient';
@@ -181,7 +180,7 @@ export const connectSocket = () => {
} }
case MessageTag.Refetch: { case MessageTag.Refetch: {
// the refetch message signals that the rundown has changed in the server side // the refetch message signals that the rundown has changed in the server side
const { target, revision } = payload; const { target, revision, rundownId } = payload;
switch (target) { switch (target) {
case RefetchKey.All: case RefetchKey.All:
invalidateAllCaches(); invalidateAllCaches();
@@ -196,7 +195,7 @@ export const connectSocket = () => {
ontimeQueryClient.invalidateQueries({ queryKey: REPORT }); ontimeQueryClient.invalidateQueries({ queryKey: REPORT });
break; break;
case RefetchKey.Rundown: { case RefetchKey.Rundown: {
maybeInvalidateRundownCache(revision); maybeInvalidateRundownCache(revision, rundownId);
break; break;
} }
case RefetchKey.UrlPresets: case RefetchKey.UrlPresets:
@@ -232,34 +231,28 @@ export const connectSocket = () => {
}; };
}; };
/** export function maybeInvalidateRundownCache(revision: MaybeNumber, rundownId?: string) {
* When we receive a refetch message for the rundown if (!rundownId) {
* check which rundown needs to be invalidated // we omit rundownId to signify invalidate all rundowns
*/ ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN });
export function maybeInvalidateRundownCache(revision: MaybeNumber) { ontimeQueryClient.invalidateQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true });
const loadedRundownId: string | undefined = (ontimeQueryClient.getQueryData(PROJECT_RUNDOWNS) as ProjectRundownsList)
?.loaded;
const activeRundownQueryKey = loadedRundownId ? getRundownQueryKey(loadedRundownId) : CURRENT_RUNDOWN_QUERY_KEY;
const cachedRundown = ontimeQueryClient.getQueryData<Rundown>(activeRundownQueryKey);
if (revision === cachedRundown?.revision) {
return; return;
} }
ontimeQueryClient.invalidateQueries({ queryKey: activeRundownQueryKey, exact: true }); // skip if we dont recognise the ID the revision is lower
const queryKey = getRundownQueryKey(rundownId);
if (loadedRundownId) { const cachedRundown = ontimeQueryClient.getQueryData<{ revision: number }>(queryKey);
// Keep bootstrap alias in sync with the ID-based cache if (revision !== null && revision === cachedRundown?.revision) {
ontimeQueryClient.invalidateQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true }); return;
} else {
// During bootstrap, loadedRundownId is not yet known.
// Invalidate any ID-based rundown caches that may have been seeded early.
ontimeQueryClient.invalidateQueries({
predicate: (query) => query.queryKey[0] === 'rundown' && query.queryKey[1] !== 'current',
});
} }
ontimeQueryClient.invalidateQueries({ queryKey: CUSTOM_FIELDS }); ontimeQueryClient.invalidateQueries({ queryKey, exact: true });
// keep current alias in sync with the ID-based cache
const loadedRundownId = ontimeQueryClient.getQueryData<{ loaded: string }>(PROJECT_RUNDOWNS)?.loaded;
if (!loadedRundownId || loadedRundownId === rundownId) {
ontimeQueryClient.invalidateQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true });
}
} }
export function sendSocket<T extends MessageTag | ApiActionTag>( export function sendSocket<T extends MessageTag | ApiActionTag>(
+2 -2
View File
@@ -262,6 +262,6 @@ export const socket = new SocketServer();
/** /**
* Utility function to notify clients that the REST data is stale * Utility function to notify clients that the REST data is stale
*/ */
export function sendRefetch(target: RefetchKey, revision: MaybeNumber = null) { export function sendRefetch(target: RefetchKey, revision: MaybeNumber = null, rundownId?: string) {
socket.sendAsJson(MessageTag.Refetch, { target, revision }); socket.sendAsJson(MessageTag.Refetch, { target, revision, rundownId });
} }
@@ -74,6 +74,7 @@ let rundownMetadata: RundownMetadata = {
let projectCustomFields: CustomFields = {}; let projectCustomFields: CustomFields = {};
export const getCurrentRundown = (): Readonly<Rundown> => cachedRundown; export const getCurrentRundown = (): Readonly<Rundown> => cachedRundown;
export const getCurrentRundownId = (): string => cachedRundown.id;
export const getRundownMetadata = (): Readonly<RundownMetadata> => rundownMetadata; export const getRundownMetadata = (): Readonly<RundownMetadata> => rundownMetadata;
export const getProjectCustomFields = (): Readonly<CustomFields> => projectCustomFields; export const getProjectCustomFields = (): Readonly<CustomFields> => projectCustomFields;
export const getEntryWithId = (entryId: EntryId): OntimeEntry | undefined => cachedRundown.entries[entryId]; export const getEntryWithId = (entryId: EntryId): OntimeEntry | undefined => cachedRundown.entries[entryId];
@@ -27,7 +27,7 @@ import { updateRundownData } from '../../stores/runtimeState.js';
import { import {
createTransaction, createTransaction,
customFieldMutation, customFieldMutation,
getCurrentRundown, getCurrentRundownId,
rundownCache, rundownCache,
rundownMutation, rundownMutation,
updateBackgroundRundown, updateBackgroundRundown,
@@ -74,7 +74,7 @@ export async function addEntry(eventData: EventPostPayload): Promise<OntimeEntry
updateRuntimeOnChange(rundownMetadata); updateRuntimeOnChange(rundownMetadata);
// notify timer and external services of change // notify timer and external services of change
notifyChanges(rundownMetadata, revision, { timer: [newEntry.id], external: true }); notifyChanges(rundown.id, rundownMetadata, revision, { timer: [newEntry.id], external: true });
}); });
return newEntry; return newEntry;
@@ -118,7 +118,10 @@ export async function editEntry(patch: PatchWithId): Promise<OntimeEntry> {
updateRuntimeOnChange(rundownMetadata); updateRuntimeOnChange(rundownMetadata);
// notify timer and external services of change // notify timer and external services of change
notifyChanges(rundownMetadata, revision, { timer: didInvalidate ? true : [entry.id], external: true }); notifyChanges(rundown.id, rundownMetadata, revision, {
timer: didInvalidate ? true : [entry.id],
external: true,
});
}); });
return entry; return entry;
@@ -181,7 +184,10 @@ export async function batchEditEntries(ids: EntryId[], patch: Partial<OntimeEntr
updateRuntimeOnChange(rundownMetadata); updateRuntimeOnChange(rundownMetadata);
// notify timer and external services of change // notify timer and external services of change
notifyChanges(rundownMetadata, revision, { timer: batchDidInvalidate ? true : changedIds, external: true }); notifyChanges(rundown.id, rundownMetadata, revision, {
timer: batchDidInvalidate ? true : changedIds,
external: true,
});
}); });
return rundownResult; return rundownResult;
@@ -209,7 +215,7 @@ export async function deleteEntries(entryIds: EntryId[]): Promise<Rundown> {
updateRuntimeOnChange(rundownMetadata); updateRuntimeOnChange(rundownMetadata);
// notify timer and external services of change // notify timer and external services of change
notifyChanges(rundownMetadata, revision, { timer: entryIds, external: true }); notifyChanges(rundown.id, rundownMetadata, revision, { timer: entryIds, external: true });
}); });
return rundownResult; return rundownResult;
@@ -231,7 +237,7 @@ export async function deleteAllEntries(): Promise<Rundown> {
updateRuntimeOnChange(rundownMetadata); updateRuntimeOnChange(rundownMetadata);
// notify timer and external services of change // notify timer and external services of change
notifyChanges(rundownMetadata, revision, { timer: true, external: true }); notifyChanges(rundown.id, rundownMetadata, revision, { timer: true, external: true });
}); });
return rundownResult; return rundownResult;
@@ -263,7 +269,7 @@ export async function reorderEntry(entryId: EntryId, destinationId: EntryId, ord
updateRuntimeOnChange(rundownMetadata); updateRuntimeOnChange(rundownMetadata);
// notify timer and external services of change // notify timer and external services of change
notifyChanges(rundownMetadata, revision, { timer: true, external: true }); notifyChanges(rundown.id, rundownMetadata, revision, { timer: true, external: true });
}); });
return rundownResult; return rundownResult;
@@ -287,7 +293,7 @@ export function renumberEntries(ids: EntryId[], prefix: string, start: string, i
setImmediate(() => { setImmediate(() => {
updateRuntimeOnChange(rundownMetadata); updateRuntimeOnChange(rundownMetadata);
notifyChanges(rundownMetadata, revision, { timer: ids, external: true }); notifyChanges(rundown.id, rundownMetadata, revision, { timer: ids, external: true });
}); });
return rundownResult; return rundownResult;
@@ -318,7 +324,7 @@ export async function applyDelay(delayId: EntryId): Promise<Rundown> {
updateRuntimeOnChange(rundownMetadata); updateRuntimeOnChange(rundownMetadata);
// notify timer and external services of change // notify timer and external services of change
notifyChanges(rundownMetadata, revision, { timer: true, external: true }); notifyChanges(rundown.id, rundownMetadata, revision, { timer: true, external: true });
}); });
return rundownResult; return rundownResult;
@@ -351,7 +357,7 @@ export async function swapEvents(fromId: EntryId, toId: EntryId): Promise<Rundow
updateRuntimeOnChange(rundownMetadata); updateRuntimeOnChange(rundownMetadata);
// notify timer and external services of change // notify timer and external services of change
notifyChanges(rundownMetadata, revision, { timer: true, external: true }); notifyChanges(rundown.id, rundownMetadata, revision, { timer: true, external: true });
}); });
return rundownResult; return rundownResult;
@@ -380,11 +386,11 @@ export async function cloneEntry(entryId: EntryId, options: InsertOptions): Prom
// notify timer and external services of change // notify timer and external services of change
if (isOntimeGroup(newEntry)) { if (isOntimeGroup(newEntry)) {
notifyChanges(rundownMetadata, revision, { timer: newEntry.entries, external: true }); notifyChanges(rundown.id, rundownMetadata, revision, { timer: newEntry.entries, external: true });
} else if (isOntimeEvent(newEntry)) { } else if (isOntimeEvent(newEntry)) {
notifyChanges(rundownMetadata, revision, { timer: [newEntry.id], external: true }); notifyChanges(rundown.id, rundownMetadata, revision, { timer: [newEntry.id], external: true });
} else if (isOntimeDelay(newEntry)) { } else if (isOntimeDelay(newEntry)) {
notifyChanges(rundownMetadata, revision, { external: true }); notifyChanges(rundown.id, rundownMetadata, revision, { external: true });
} }
}); });
@@ -406,7 +412,7 @@ export async function groupEntries(entryIds: EntryId[]): Promise<Rundown> {
updateRuntimeOnChange(rundownMetadata); updateRuntimeOnChange(rundownMetadata);
// we need to notify the timer since we might be grouping a running event // we need to notify the timer since we might be grouping a running event
notifyChanges(rundownMetadata, revision, { external: true, timer: true }); notifyChanges(rundown.id, rundownMetadata, revision, { external: true, timer: true });
}); });
return rundownResult; return rundownResult;
@@ -432,7 +438,7 @@ export async function ungroupEntries(groupId: EntryId): Promise<Rundown> {
updateRuntimeOnChange(rundownMetadata); updateRuntimeOnChange(rundownMetadata);
// we dont need to notify the timer since the grouping does not affect the runtime // we dont need to notify the timer since the grouping does not affect the runtime
notifyChanges(rundownMetadata, revision, { external: true }); notifyChanges(rundown.id, rundownMetadata, revision, { external: true });
}); });
return rundownResult; return rundownResult;
@@ -522,7 +528,7 @@ export async function editCustomField(
// schedule the side effects // schedule the side effects
setImmediate(() => { setImmediate(() => {
sendRefetch(RefetchKey.CustomFields); sendRefetch(RefetchKey.CustomFields);
notifyChanges(rundownMetadata, revision, { timer: true, external: true }); notifyChanges(undefined, rundownMetadata, revision, { timer: true, external: true });
}); });
return resultCustomFields; return resultCustomFields;
@@ -561,7 +567,7 @@ export async function deleteCustomField(key: CustomFieldKey, projectRundowns: Pr
// schedule the side effects // schedule the side effects
setImmediate(() => { setImmediate(() => {
sendRefetch(RefetchKey.CustomFields); sendRefetch(RefetchKey.CustomFields);
notifyChanges(rundownMetadata, revision, { timer: true, external: true }); notifyChanges(undefined, rundownMetadata, revision, { timer: true, external: true });
}); });
return resultCustomFields; return resultCustomFields;
@@ -592,10 +598,17 @@ type NotifyChangesOptions = {
/** /**
* Notify services of changes in the rundown * Notify services of changes in the rundown
* TODO: we could receive a runtime flag to call updateRuntimeOnChange
* instead of having it in every consumer
*/ */
function notifyChanges(rundownMetadata: RundownMetadata, revision: number, options: NotifyChangesOptions) { function notifyChanges(
// notify timer service of changed events rundownId: string | undefined,
if (options.timer) { rundownMetadata: RundownMetadata,
revision: number,
options: NotifyChangesOptions,
) {
// notify timer service of changed event
if (options.timer && rundownId && isCurrentRundown(rundownId)) {
runtimeService.notifyOfChangedEvents(rundownMetadata); runtimeService.notifyOfChangedEvents(rundownMetadata);
} }
@@ -603,18 +616,23 @@ function notifyChanges(rundownMetadata: RundownMetadata, revision: number, optio
if (options.reload) { if (options.reload) {
sendRefetch(RefetchKey.All); sendRefetch(RefetchKey.All);
} else if (options.external) { } else if (options.external) {
sendRefetch(RefetchKey.Rundown, revision); sendRefetch(RefetchKey.Rundown, revision, rundownId);
} }
} }
export function isCurrentRundown(id: string) {
return id === getCurrentRundownId();
}
/** /**
* @throws if the provided id does not exist * @throws if the provided id does not exist
*/ */
export async function loadRundown(id: string) { export async function loadRundown(id: string) {
const dataProvider = getDataProvider(); const dataProvider = getDataProvider();
if (id === getCurrentRundown().id) { if (isCurrentRundown(id)) {
return dataProvider.getProjectRundowns(); return dataProvider.getProjectRundowns();
} }
const rundown = dataProvider.getRundown(id); const rundown = dataProvider.getRundown(id);
const customField = dataProvider.getCustomFields(); const customField = dataProvider.getCustomFields();
await initRundown(rundown, customField); await initRundown(rundown, customField);
@@ -637,7 +655,7 @@ export async function initRundown(
updateRuntimeOnChange(rundownMetadata); updateRuntimeOnChange(rundownMetadata);
setImmediate(() => { setImmediate(() => {
notifyChanges(rundownMetadata, revision, { timer: true, external: true, reload }); notifyChanges(rundown.id, rundownMetadata, revision, { timer: true, external: true, reload });
setLastLoadedRundown(rundown.id).catch((error) => { setLastLoadedRundown(rundown.id).catch((error) => {
logger.error(LogOrigin.Server, `Failed to persist last loaded rundown: ${error}`); logger.error(LogOrigin.Server, `Failed to persist last loaded rundown: ${error}`);
}); });
@@ -41,6 +41,7 @@ type RefetchPacket = {
payload: { payload: {
target: RefetchKey; target: RefetchKey;
revision: MaybeNumber; revision: MaybeNumber;
rundownId?: string; // undefined means refetch all rundowns
}; };
}; };