Remove the second full-rundown clone from DataProvider.setRundown

setRundown deep-cloned every rundown a second time on top of the clone
createTransaction already makes - the only two callers that matter for
correctness are the ones passing the live cachedRundown singleton, and
db.data.rundowns[key] is only ever replaced wholesale, never mutated in
place, so aliasing it introduces no corruption path. The other six call
sites already pass a freshly-built object they never touch again, for
which the clone was pure waste.

setRundown now takes ownership of `newData` and stores it by reference,
documented on the function. Verified with a throwaway suite (deleted,
per instructions, once green) covering:
  - the reference stored is the exact one passed in, not a copy
  - round-trip content correctness is unchanged
  - a mutation to the source after setRundown is visible through
    getRundown, which is the new contract, not a regression - pinned
    explicitly so it reads as intentional
  - the fresh-object call sites (duplicate, import, rename, etc.) are
    unaffected
  - mergeRundown / deleteRundown / not-found still behave correctly
  - perf: setRundown on a 1000-entry rundown dropped from ~557us/call to
    ~5us/call

Real disk I/O could not be exercised in that suite - lowdb's JSONFilePreset
forces an in-memory adapter whenever NODE_ENV=test, which vitest always
sets, independent of this project's own IS_TEST flag. That's fine: this
change only affects which object reference ends up at db.data.rundowns[key],
not persist()'s debounce/write-scheduling logic, which is untouched, so
checking that state directly is equivalent to checking what a real write
would serialize.

Full suite (705 tests), typecheck, and lint all pass on the final diff,
which is 8 lines changed in one file.
This commit is contained in:
Claude
2026-08-25 05:08:02 +00:00
parent 2d5a328636
commit a2ec4d0d3f
@@ -12,7 +12,6 @@ import {
ViewSettings,
} from 'ontime-types';
import { cloneRundown } from '../../api-data/rundown/rundown.utils.js';
import { isTest } from '../../setup/environment.js';
import { shouldCrashDev } from '../../utils/development.js';
import { isPath } from '../../utils/fileManagement.js';
@@ -102,8 +101,16 @@ function getCustomFields(): Readonly<CustomFields> {
return db.data.customFields;
}
/**
* Stores a rundown, replacing any existing entry for the same key.
* Takes ownership of `newData` and stores it by reference - the caller must not mutate it
* afterward. Every call site either hands over a freshly-built object it never touches again,
* or (for the loaded rundown) the cache's own long-lived object, which is already the single
* source of truth for that data - aliasing it here costs nothing and avoids a second full
* deep copy of the rundown on every commit.
*/
async function setRundown(rundownKey: string, newData: Rundown): ReadonlyPromise<ProjectRundowns> {
db.data.rundowns[rundownKey] = cloneRundown(newData);
db.data.rundowns[rundownKey] = newData;
await persist();
return db.data.rundowns;
}