diff --git a/apps/client/src/common/hooks-query/useRundown.ts b/apps/client/src/common/hooks-query/useRundown.ts index a5881dab1..052ba273d 100644 --- a/apps/client/src/common/hooks-query/useRundown.ts +++ b/apps/client/src/common/hooks-query/useRundown.ts @@ -40,6 +40,7 @@ export default function useRundown() { queryClient.setQueryData(getRundownQueryKey(data.id), data); }, [data, loadedRundownId, queryClient]); + // Once we have the ID, drop the temporary current cache useEffect(() => { if (!loadedRundownId) return; queryClient.removeQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true }); diff --git a/apps/client/src/common/utils/socket.ts b/apps/client/src/common/utils/socket.ts index 3c1068048..7eef67966 100644 --- a/apps/client/src/common/utils/socket.ts +++ b/apps/client/src/common/utils/socket.ts @@ -3,9 +3,7 @@ import { Log, MaybeNumber, MessageTag, - ProjectRundownsList, RefetchKey, - Rundown, RuntimeStore, WsPacketToClient, WsPacketToServer, @@ -17,15 +15,16 @@ import { CLIENT_LIST, CSS_OVERRIDE, CUSTOM_FIELDS, - PROJECT_DATA, CURRENT_RUNDOWN_QUERY_KEY, - PROJECT_RUNDOWNS, + PROJECT_DATA, REPORT, + RUNDOWN, RUNTIME, TRANSLATION, URL_PRESETS, VIEW_SETTINGS, getRundownQueryKey, + PROJECT_RUNDOWNS, } from '../api/constants'; import { invalidateAllCaches } from '../api/utils'; import { ontimeQueryClient } from '../queryClient'; @@ -181,7 +180,7 @@ export const connectSocket = () => { } case MessageTag.Refetch: { // the refetch message signals that the rundown has changed in the server side - const { target, revision } = payload; + const { target, revision, rundownId } = payload; switch (target) { case RefetchKey.All: invalidateAllCaches(); @@ -196,7 +195,7 @@ export const connectSocket = () => { ontimeQueryClient.invalidateQueries({ queryKey: REPORT }); break; case RefetchKey.Rundown: { - maybeInvalidateRundownCache(revision); + maybeInvalidateRundownCache(revision, rundownId); break; } case RefetchKey.UrlPresets: @@ -232,34 +231,28 @@ export const connectSocket = () => { }; }; -/** - * When we receive a refetch message for the rundown - * check which rundown needs to be invalidated - */ -export function maybeInvalidateRundownCache(revision: MaybeNumber) { - const loadedRundownId: string | undefined = (ontimeQueryClient.getQueryData(PROJECT_RUNDOWNS) as ProjectRundownsList) - ?.loaded; - - const activeRundownQueryKey = loadedRundownId ? getRundownQueryKey(loadedRundownId) : CURRENT_RUNDOWN_QUERY_KEY; - const cachedRundown = ontimeQueryClient.getQueryData(activeRundownQueryKey); - if (revision === cachedRundown?.revision) { +export function maybeInvalidateRundownCache(revision: MaybeNumber, rundownId?: string) { + if (!rundownId) { + // we omit rundownId to signify invalidate all rundowns + ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN }); + ontimeQueryClient.invalidateQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true }); return; } - ontimeQueryClient.invalidateQueries({ queryKey: activeRundownQueryKey, exact: true }); - - if (loadedRundownId) { - // Keep bootstrap alias in sync with the ID-based cache - ontimeQueryClient.invalidateQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true }); - } 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', - }); + // skip if we dont recognise the ID the revision is lower + const queryKey = getRundownQueryKey(rundownId); + const cachedRundown = ontimeQueryClient.getQueryData<{ revision: number }>(queryKey); + if (revision !== null && revision === cachedRundown?.revision) { + return; } - 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( diff --git a/apps/server/src/adapters/WebsocketAdapter.ts b/apps/server/src/adapters/WebsocketAdapter.ts index 57622493c..e786c2f03 100644 --- a/apps/server/src/adapters/WebsocketAdapter.ts +++ b/apps/server/src/adapters/WebsocketAdapter.ts @@ -262,6 +262,6 @@ export const socket = new SocketServer(); /** * Utility function to notify clients that the REST data is stale */ -export function sendRefetch(target: RefetchKey, revision: MaybeNumber = null) { - socket.sendAsJson(MessageTag.Refetch, { target, revision }); +export function sendRefetch(target: RefetchKey, revision: MaybeNumber = null, rundownId?: string) { + socket.sendAsJson(MessageTag.Refetch, { target, revision, rundownId }); } diff --git a/apps/server/src/api-data/rundown/rundown.dao.ts b/apps/server/src/api-data/rundown/rundown.dao.ts index 8cb1a81f1..2e8b308df 100644 --- a/apps/server/src/api-data/rundown/rundown.dao.ts +++ b/apps/server/src/api-data/rundown/rundown.dao.ts @@ -74,6 +74,7 @@ let rundownMetadata: RundownMetadata = { let projectCustomFields: CustomFields = {}; export const getCurrentRundown = (): Readonly => cachedRundown; +export const getCurrentRundownId = (): string => cachedRundown.id; export const getRundownMetadata = (): Readonly => rundownMetadata; export const getProjectCustomFields = (): Readonly => projectCustomFields; export const getEntryWithId = (entryId: EntryId): OntimeEntry | undefined => cachedRundown.entries[entryId]; diff --git a/apps/server/src/api-data/rundown/rundown.service.ts b/apps/server/src/api-data/rundown/rundown.service.ts index 5534c0083..e8c1630bd 100644 --- a/apps/server/src/api-data/rundown/rundown.service.ts +++ b/apps/server/src/api-data/rundown/rundown.service.ts @@ -27,7 +27,7 @@ import { updateRundownData } from '../../stores/runtimeState.js'; import { createTransaction, customFieldMutation, - getCurrentRundown, + getCurrentRundownId, rundownCache, rundownMutation, updateBackgroundRundown, @@ -74,7 +74,7 @@ export async function addEntry(eventData: EventPostPayload): Promise { updateRuntimeOnChange(rundownMetadata); // 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; @@ -181,7 +184,10 @@ export async function batchEditEntries(ids: EntryId[], patch: Partial { updateRuntimeOnChange(rundownMetadata); // 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; @@ -231,7 +237,7 @@ export async function deleteAllEntries(): Promise { updateRuntimeOnChange(rundownMetadata); // 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; @@ -263,7 +269,7 @@ export async function reorderEntry(entryId: EntryId, destinationId: EntryId, ord updateRuntimeOnChange(rundownMetadata); // 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; @@ -287,7 +293,7 @@ export function renumberEntries(ids: EntryId[], prefix: string, start: string, i setImmediate(() => { updateRuntimeOnChange(rundownMetadata); - notifyChanges(rundownMetadata, revision, { timer: ids, external: true }); + notifyChanges(rundown.id, rundownMetadata, revision, { timer: ids, external: true }); }); return rundownResult; @@ -318,7 +324,7 @@ export async function applyDelay(delayId: EntryId): Promise { updateRuntimeOnChange(rundownMetadata); // 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; @@ -351,7 +357,7 @@ export async function swapEvents(fromId: EntryId, toId: EntryId): Promise { updateRuntimeOnChange(rundownMetadata); // 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; @@ -432,7 +438,7 @@ export async function ungroupEntries(groupId: EntryId): Promise { updateRuntimeOnChange(rundownMetadata); // 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; @@ -522,7 +528,7 @@ export async function editCustomField( // schedule the side effects setImmediate(() => { sendRefetch(RefetchKey.CustomFields); - notifyChanges(rundownMetadata, revision, { timer: true, external: true }); + notifyChanges(undefined, rundownMetadata, revision, { timer: true, external: true }); }); return resultCustomFields; @@ -561,7 +567,7 @@ export async function deleteCustomField(key: CustomFieldKey, projectRundowns: Pr // schedule the side effects setImmediate(() => { sendRefetch(RefetchKey.CustomFields); - notifyChanges(rundownMetadata, revision, { timer: true, external: true }); + notifyChanges(undefined, rundownMetadata, revision, { timer: true, external: true }); }); return resultCustomFields; @@ -592,10 +598,17 @@ type NotifyChangesOptions = { /** * 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) { - // notify timer service of changed events - if (options.timer) { +function notifyChanges( + rundownId: string | undefined, + rundownMetadata: RundownMetadata, + revision: number, + options: NotifyChangesOptions, +) { + // notify timer service of changed event + if (options.timer && rundownId && isCurrentRundown(rundownId)) { runtimeService.notifyOfChangedEvents(rundownMetadata); } @@ -603,18 +616,23 @@ function notifyChanges(rundownMetadata: RundownMetadata, revision: number, optio if (options.reload) { sendRefetch(RefetchKey.All); } 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 */ export async function loadRundown(id: string) { const dataProvider = getDataProvider(); - if (id === getCurrentRundown().id) { + if (isCurrentRundown(id)) { return dataProvider.getProjectRundowns(); } + const rundown = dataProvider.getRundown(id); const customField = dataProvider.getCustomFields(); await initRundown(rundown, customField); @@ -637,7 +655,7 @@ export async function initRundown( updateRuntimeOnChange(rundownMetadata); setImmediate(() => { - notifyChanges(rundownMetadata, revision, { timer: true, external: true, reload }); + notifyChanges(rundown.id, rundownMetadata, revision, { timer: true, external: true, reload }); setLastLoadedRundown(rundown.id).catch((error) => { logger.error(LogOrigin.Server, `Failed to persist last loaded rundown: ${error}`); }); diff --git a/packages/types/src/api/websocket/data.type.ts b/packages/types/src/api/websocket/data.type.ts index d66dc6659..3d09f2158 100644 --- a/packages/types/src/api/websocket/data.type.ts +++ b/packages/types/src/api/websocket/data.type.ts @@ -41,6 +41,7 @@ type RefetchPacket = { payload: { target: RefetchKey; revision: MaybeNumber; + rundownId?: string; // undefined means refetch all rundowns }; };