From 982cb8e49565edd6ffeae8502000348a464157b8 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sun, 13 Sep 2026 20:20:27 +0200 Subject: [PATCH] fix(rundown): preserve revision and runtime semantics Advance swapped-entry revisions correctly and rename loaded rundowns without restarting playback, while documenting the shared revision invariant. --- .../rundown/__tests__/rundown.dao.test.ts | 6 ++-- .../src/api-data/rundown/rundown.dao.ts | 4 +-- .../src/api-data/rundown/rundown.service.ts | 32 ++++++++----------- docs/agent-guides/domain-invariants.md | 4 +++ 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/apps/server/src/api-data/rundown/__tests__/rundown.dao.test.ts b/apps/server/src/api-data/rundown/__tests__/rundown.dao.test.ts index 0ef4fb7af..e1765f2cd 100644 --- a/apps/server/src/api-data/rundown/__tests__/rundown.dao.test.ts +++ b/apps/server/src/api-data/rundown/__tests__/rundown.dao.test.ts @@ -1718,16 +1718,18 @@ describe('rundownMutation.swap()', () => { expect((testRundown.entries['1'] as OntimeEvent).id).toBe('1'); expect((testRundown.entries['1'] as OntimeEvent).cue).toBe('data2'); expect((testRundown.entries['1'] as OntimeEvent).timeStart).toBe(1); - expect((testRundown.entries['1'] as OntimeEvent).revision).toBe(1); + // the swapped entries changed, their revision has to say so + expect((testRundown.entries['1'] as OntimeEvent).revision).toBe(2); expect((testRundown.entries['2'] as OntimeEvent).id).toBe('2'); expect((testRundown.entries['2'] as OntimeEvent).cue).toBe('data1'); expect((testRundown.entries['2'] as OntimeEvent).timeStart).toBe(2); - expect((testRundown.entries['2'] as OntimeEvent).revision).toBe(1); + expect((testRundown.entries['2'] as OntimeEvent).revision).toBe(2); expect((testRundown.entries['3'] as OntimeEvent).id).toBe('3'); expect((testRundown.entries['3'] as OntimeEvent).cue).toBe('data3'); expect((testRundown.entries['3'] as OntimeEvent).timeStart).toBe(3); + // untouched, so it keeps its revision expect((testRundown.entries['3'] as OntimeEvent).revision).toBe(1); }); }); diff --git a/apps/server/src/api-data/rundown/rundown.dao.ts b/apps/server/src/api-data/rundown/rundown.dao.ts index 52c3412b8..3f045cab7 100644 --- a/apps/server/src/api-data/rundown/rundown.dao.ts +++ b/apps/server/src/api-data/rundown/rundown.dao.ts @@ -422,7 +422,7 @@ function swap(rundown: Rundown, eventFrom: OntimeEvent, eventTo: OntimeEvent) { gap: eventFrom.gap, dayOffset: eventFrom.dayOffset, // keep revision number but increment it - revision: eventFrom.revision++, + revision: eventFrom.revision + 1, }; rundown.entries[eventTo.id] = { @@ -440,7 +440,7 @@ function swap(rundown: Rundown, eventFrom: OntimeEvent, eventTo: OntimeEvent) { gap: eventTo.gap, dayOffset: eventTo.dayOffset, // keep revision number but increment it - revision: eventTo.revision++, + revision: eventTo.revision + 1, }; } 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(); } /** diff --git a/docs/agent-guides/domain-invariants.md b/docs/agent-guides/domain-invariants.md index ca1cf8c25..af361a5b5 100644 --- a/docs/agent-guides/domain-invariants.md +++ b/docs/agent-guides/domain-invariants.md @@ -9,6 +9,10 @@ Load only for touched domains. Add only stable, recurring invariants; not one-of - Preserve entry identity and supported types across patch, clone, group, ungroup, reorder. - Distinguish loaded vs background rundown. Prefer explicit rundown ID over global current state. - No caller-owned rundown mutation unless explicitly contracted. +- Revisions mark change: a rundown revision advances on every commit, an entry revision on every + change to that entry. Delays carry no revision. Optimistic client updates apply the same bump the + server does, so an unchanged entry keeps its identity and clients can compare revisions to tell + what moved on. ## Persistence, realtime, cache