From a5086aed0bd48fe986551762db1de21ee2ca61b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 10:51:20 +0000 Subject: [PATCH] fix(rundown): narrow rundown invalidation Two independent issues in the revision strategy. The client guard only skipped a refetch when the revision matched exactly, while its comment described skipping anything not newer. A message which arrives after a refetch has already brought in a later revision forced a needless refetch. Compare with <= so the guard does what it claims. Renaming a rundown re-initialised it, which stops playback. Renaming the loaded rundown during a show therefore stopped the show. A title has no bearing on the schedule, so it now goes through an ordinary transaction: the title is persisted, the revision bumped and clients notified, with no runtime involvement. Renaming to the current title is a no-op. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011a5cbVjNC5XXF88b2PkUCa --- apps/client/src/common/utils/socket.ts | 6 ++-- .../src/api-data/rundown/rundown.service.ts | 32 ++++++++----------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/apps/client/src/common/utils/socket.ts b/apps/client/src/common/utils/socket.ts index a9366cc36..6a16ba04d 100644 --- a/apps/client/src/common/utils/socket.ts +++ b/apps/client/src/common/utils/socket.ts @@ -242,12 +242,12 @@ export function maybeInvalidateRundownCache(revision: MaybeNumber, rundownId?: s return; } - // skip if we dont recognise the ID the revision is lower const queryKey = getRundownQueryKey(rundownId); const cachedRundown = ontimeQueryClient.getQueryData<{ revision: number }>(queryKey); - if (revision === cachedRundown?.revision) { - // we already have the latest change + // we already have this change, or something newer + // messages can arrive after a refetch has already brought in a later revision + if (revision !== null && cachedRundown !== undefined && revision <= cachedRundown.revision) { return; } diff --git a/apps/server/src/api-data/rundown/rundown.service.ts b/apps/server/src/api-data/rundown/rundown.service.ts index a1155dbf9..7aafcc84b 100644 --- a/apps/server/src/api-data/rundown/rundown.service.ts +++ b/apps/server/src/api-data/rundown/rundown.service.ts @@ -816,27 +816,23 @@ export async function createNewRundown(title: string) { * @throws if the provided id does not exist */ export async function renameRundown(id: string, title: string) { - const dataProvider = getDataProvider(); - const rundown = dataProvider.getRundown(id); + const { rundown, commit } = createTransaction({ rundownId: id, mutableRundown: true }); - await dataProvider.setRundown(id, { ...rundown, title, revision: rundown.revision + 1 }); - - /** - * If we are modifying the loaded rundown we re-init it - * This is likely over-kill but the simplest way to ensure state consistency - */ - if (isCurrentRundown(id)) { - const rundown = dataProvider.getRundown(id); - const customField = dataProvider.getCustomFields(); - // init rundown does its own refetch - await initRundown(rundown, customField); - } else { - setImmediate(() => { - sendRefetch(RefetchKey.ProjectRundowns); - }); + if (rundown.title === title) { + return getDataProvider().getProjectRundowns(); } - return dataProvider.getProjectRundowns(); + rundown.title = title; + + // a title has no bearing on the schedule, there is nothing to process and no runtime to notify + const { rundownMetadata, revision } = await commit(false); + + setImmediate(() => { + notifyChanges(id, rundownMetadata, revision, { external: true }); + sendRefetch(RefetchKey.ProjectRundowns); + }); + + return getDataProvider().getProjectRundowns(); } /**