Make createTransaction's rundown deep-readonly when not mutable

Overload createTransaction() on the literal mutableRundown option: with
mutableRundown: true it returns rundown: Rundown as before; otherwise it
returns rundown: DeepReadonly<Rundown> (ts-essentials, already a convention
in this codebase for read-only snapshots).

Previously a non-mutable transaction's rundown was typed as plain Rundown
even though it's the live cachedRundown reference itself (or a background
rundown read straight from disk) - nothing stopped a future mutation
function from writing into it outside the commit() flow, since
Readonly<T> (used elsewhere for the same purpose) only blocks top-level
reassignment, not nested writes like array.push() or entry.field = x.

Scoped to rundown.dao.ts only: every existing call site in rundown.service.ts
passes a literal mutableRundown: true, so this changes no call-site types.
The one mutableRundown: false site doesn't destructure rundown at all.
Verified with a throwaway probe file (removed) that mutating a non-mutable
transaction's rundown is now a compile error, and that a mutable one still
compiles as before. Typecheck, lint and full test suite (695 tests) pass
unchanged.
This commit is contained in:
Claude
2026-08-23 18:19:18 +00:00
parent b1059e7bff
commit c6c25cebcc
@@ -27,6 +27,7 @@ import {
isPlayableEvent, isPlayableEvent,
} from 'ontime-types'; } from 'ontime-types';
import { addToRundown, createGroup, customFieldLabelToKey, getInsertAfterId, insertAtIndex } from 'ontime-utils'; import { addToRundown, createGroup, customFieldLabelToKey, getInsertAfterId, insertAtIndex } from 'ontime-utils';
import type { DeepReadonly } from 'ts-essentials';
import { getDataProvider } from '../../classes/data-provider/DataProvider.js'; import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
import { consoleError } from '../../utils/console.js'; import { consoleError } from '../../utils/console.js';
@@ -80,9 +81,16 @@ export const getRundownMetadata = (): Readonly<RundownMetadata> => rundownMetada
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];
type Transaction = { /**
* @param R the type callers see for `rundown` - a plain, mutable `Rundown` when the
* transaction was opened with `mutableRundown: true`, otherwise a `DeepReadonly<Rundown>`
* so that accidentally mutating an entry (or an order array) on a non-mutable transaction
* - which would silently corrupt the live cache without going through commit() - is a
* compile-time error instead of a runtime bug.
*/
type Transaction<R> = {
customFields: CustomFields; customFields: CustomFields;
rundown: Rundown; rundown: R;
commit: (shouldProcess?: boolean) => Promise<{ commit: (shouldProcess?: boolean) => Promise<{
rundown: Readonly<Rundown>; rundown: Readonly<Rundown>;
@@ -103,7 +111,13 @@ type TransactionOptions = {
rundownId?: string; rundownId?: string;
}; };
export function createTransaction(options: TransactionOptions): Transaction { export function createTransaction(options: TransactionOptions & { mutableRundown: true }): Transaction<Rundown>;
export function createTransaction(
options: TransactionOptions & { mutableRundown?: false },
): Transaction<DeepReadonly<Rundown>>;
export function createTransaction(
options: TransactionOptions,
): Transaction<Rundown> | Transaction<DeepReadonly<Rundown>> {
const targetId = options.rundownId ?? cachedRundown.id; const targetId = options.rundownId ?? cachedRundown.id;
const isLoaded = targetId === cachedRundown.id; const isLoaded = targetId === cachedRundown.id;
const sourceRundown: Rundown = isLoaded ? cachedRundown : (getDataProvider().getRundown(targetId) as Rundown); const sourceRundown: Rundown = isLoaded ? cachedRundown : (getDataProvider().getRundown(targetId) as Rundown);