Replace structuredClone with shape-aware clones in rundown/data hot paths

structuredClone's generic serialization algorithm does far more work than
plain object spreads need for these known shapes. Adds cloneEntryData()
and cloneRundown() as drop-in replacements (same "independent copy" contract,
same call sites) and swaps them in everywhere a rundown or a single entry
was being deep-cloned via structuredClone:

- createTransaction()/init() in rundown.dao.ts - the main per-mutation clone
- DataProvider.setRundown() - was re-cloning the whole rundown a second time
  on every single commit
- rundown.service.ts background-rundown clones (custom field rename/remove,
  duplicateExistingRundown)
- the per-entry clone in processRundown's non-mutating path (rundown.parser.ts)
- mergeRundownPreservingFields's per-entry clone

Also:
- safeMerge() (DataProvider.utils.ts) was deep-cloning the entire DatabaseModel,
  including all rundowns, just to read a handful of small config properties
  that never touch rundowns - it now only clones the properties it actually
  merges.
- sheets.service.ts's per-row clone before building a (read-only) Google
  Sheets cell request was unnecessary and is removed.
- runtime.service.ts's previous-state snapshot for eventNow/eventNext/
  eventFlag/groupNow now uses cloneEntryData.

Benchmarked on a synthetic 1000-event rundown: a cue-only edit (no
reprocessing needed) went from ~5ms to ~1.2ms end to end, including the
DataProvider clone. Verified against the existing test suite (695 passing)
plus typecheck and lint.
This commit is contained in:
Claude
2026-08-23 16:21:53 +00:00
parent a84b8b5530
commit b1059e7bff
8 changed files with 65 additions and 17 deletions
@@ -34,6 +34,7 @@ import { ProcessedRundownMetadata, makeRundownMetadata } from './rundown.parser.
import type { RundownMetadata } from './rundown.types.js';
import {
applyPatchToEntry,
cloneRundown,
cloneSimpleRundownEntry,
deleteById,
doesInvalidateMetadata,
@@ -106,7 +107,7 @@ export function createTransaction(options: TransactionOptions): Transaction {
const targetId = options.rundownId ?? cachedRundown.id;
const isLoaded = targetId === cachedRundown.id;
const sourceRundown: Rundown = isLoaded ? cachedRundown : (getDataProvider().getRundown(targetId) as Rundown);
const rundown = options.mutableRundown ? structuredClone(sourceRundown) : sourceRundown;
const rundown = options.mutableRundown ? cloneRundown(sourceRundown) : sourceRundown;
const customFields = options.mutableCustomFields ? structuredClone(projectCustomFields) : projectCustomFields;
/**
@@ -707,7 +708,7 @@ export const customFieldMutation = {
* Expose function to add an initial rundown to the system
*/
export function init(initialRundown: Readonly<Rundown>, initialCustomFields: Readonly<CustomFields>) {
const rundown = structuredClone(initialRundown);
const rundown = cloneRundown(initialRundown);
const customFields = structuredClone(initialCustomFields);
const processedData = processRundown(rundown, customFields, { mutate: true });