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.
This commit is contained in:
Carlos Valente
2026-09-13 20:20:27 +02:00
parent 48c76947c5
commit 982cb8e495
4 changed files with 24 additions and 22 deletions
@@ -1718,16 +1718,18 @@ describe('rundownMutation.swap()', () => {
expect((testRundown.entries['1'] as OntimeEvent).id).toBe('1'); expect((testRundown.entries['1'] as OntimeEvent).id).toBe('1');
expect((testRundown.entries['1'] as OntimeEvent).cue).toBe('data2'); expect((testRundown.entries['1'] as OntimeEvent).cue).toBe('data2');
expect((testRundown.entries['1'] as OntimeEvent).timeStart).toBe(1); 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).id).toBe('2');
expect((testRundown.entries['2'] as OntimeEvent).cue).toBe('data1'); expect((testRundown.entries['2'] as OntimeEvent).cue).toBe('data1');
expect((testRundown.entries['2'] as OntimeEvent).timeStart).toBe(2); 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).id).toBe('3');
expect((testRundown.entries['3'] as OntimeEvent).cue).toBe('data3'); expect((testRundown.entries['3'] as OntimeEvent).cue).toBe('data3');
expect((testRundown.entries['3'] as OntimeEvent).timeStart).toBe(3); expect((testRundown.entries['3'] as OntimeEvent).timeStart).toBe(3);
// untouched, so it keeps its revision
expect((testRundown.entries['3'] as OntimeEvent).revision).toBe(1); expect((testRundown.entries['3'] as OntimeEvent).revision).toBe(1);
}); });
}); });
@@ -422,7 +422,7 @@ function swap(rundown: Rundown, eventFrom: OntimeEvent, eventTo: OntimeEvent) {
gap: eventFrom.gap, gap: eventFrom.gap,
dayOffset: eventFrom.dayOffset, dayOffset: eventFrom.dayOffset,
// keep revision number but increment it // keep revision number but increment it
revision: eventFrom.revision++, revision: eventFrom.revision + 1,
}; };
rundown.entries[eventTo.id] = { rundown.entries[eventTo.id] = {
@@ -440,7 +440,7 @@ function swap(rundown: Rundown, eventFrom: OntimeEvent, eventTo: OntimeEvent) {
gap: eventTo.gap, gap: eventTo.gap,
dayOffset: eventTo.dayOffset, dayOffset: eventTo.dayOffset,
// keep revision number but increment it // keep revision number but increment it
revision: eventTo.revision++, revision: eventTo.revision + 1,
}; };
} }
@@ -816,27 +816,23 @@ export async function createNewRundown(title: string) {
* @throws if the provided id does not exist * @throws if the provided id does not exist
*/ */
export async function renameRundown(id: string, title: string) { export async function renameRundown(id: string, title: string) {
const dataProvider = getDataProvider(); const { rundown, commit } = createTransaction({ rundownId: id, mutableRundown: true });
const rundown = dataProvider.getRundown(id);
await dataProvider.setRundown(id, { ...rundown, title, revision: rundown.revision + 1 }); if (rundown.title === title) {
return getDataProvider().getProjectRundowns();
/**
* 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);
});
} }
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();
} }
/** /**
+4
View File
@@ -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. - 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. - Distinguish loaded vs background rundown. Prefer explicit rundown ID over global current state.
- No caller-owned rundown mutation unless explicitly contracted. - 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 ## Persistence, realtime, cache