mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-06 23:09:08 +00:00
refactor(server): replace defensive copies with cow patterns
This commit is contained in:
committed by
Carlos Valente
parent
9806f97d27
commit
d9dd147d4b
@@ -1,5 +1,7 @@
|
||||
import { deepEqual } from 'fast-equals';
|
||||
import {
|
||||
EndAction,
|
||||
OntimeEntry,
|
||||
OntimeEvent,
|
||||
OntimeGroup,
|
||||
OntimeMilestone,
|
||||
@@ -8,13 +10,21 @@ import {
|
||||
TimerType,
|
||||
Trigger,
|
||||
} from 'ontime-types';
|
||||
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, createEvent } from 'ontime-utils';
|
||||
import {
|
||||
MILLIS_PER_HOUR,
|
||||
MILLIS_PER_MINUTE,
|
||||
createDelay,
|
||||
createEvent,
|
||||
createGroup,
|
||||
createMilestone,
|
||||
} from 'ontime-utils';
|
||||
import { assertType } from 'vitest';
|
||||
|
||||
import { makeOntimeEvent, makeOntimeGroup, makeOntimeMilestone, makeRundown } from '../__mocks__/rundown.mocks.js';
|
||||
import { parseRundown } from '../rundown.parser.js';
|
||||
import {
|
||||
calculateDayOffset,
|
||||
cloneEntryData,
|
||||
deleteById,
|
||||
doesInvalidateMetadata,
|
||||
getIntegerAndFraction,
|
||||
@@ -715,3 +725,68 @@ describe('eventDurationMatchGroupTarget()', () => {
|
||||
expect(result).toStrictEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cloneEntryData()', () => {
|
||||
const trigger: Trigger = {
|
||||
id: 'trigger-1',
|
||||
title: 'Go on air',
|
||||
trigger: TimerLifeCycle.onStart,
|
||||
automationId: 'automation-1',
|
||||
};
|
||||
|
||||
// the real factories, so these are complete entries exactly as the rundown holds them
|
||||
const entries: [string, OntimeEntry][] = [
|
||||
['event', createEvent({ custom: { sponsor: 'a value' }, triggers: [trigger] }, 'cue-1') as OntimeEvent],
|
||||
['group', createGroup({ id: 'group-1', entries: ['a', 'b'], custom: { sponsor: 'a value' } })],
|
||||
['milestone', createMilestone({ id: 'milestone-1', custom: { sponsor: 'a value' } })],
|
||||
['delay', createDelay({ id: 'delay-1', duration: 10 })],
|
||||
];
|
||||
|
||||
/**
|
||||
* Fails if any nested object or array in the clone is the same reference as the source,
|
||||
* so a field added later that needs a copy of its own is caught here without the clone
|
||||
* having to enumerate fields.
|
||||
*/
|
||||
function expectNoSharedReferences(clone: unknown, source: unknown, path: string) {
|
||||
if (typeof source !== 'object' || source === null) return;
|
||||
expect(clone, `${path} is shared with the source`).not.toBe(source);
|
||||
const cloneRecord = clone as Record<string, unknown>;
|
||||
const sourceRecord = source as Record<string, unknown>;
|
||||
for (const key of Object.keys(sourceRecord)) {
|
||||
expectNoSharedReferences(cloneRecord[key], sourceRecord[key], `${path}.${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** the two halves of the structuredClone contract: same value, no shared references */
|
||||
it.each(entries)('clones a %s to the same value structuredClone would produce', (_type, entry) => {
|
||||
expect(cloneEntryData(entry)).toStrictEqual(structuredClone(entry));
|
||||
});
|
||||
|
||||
it.each(entries)('shares no nested object or array with the source %s', (_type, entry) => {
|
||||
expectNoSharedReferences(cloneEntryData(entry), entry, 'entry');
|
||||
});
|
||||
|
||||
/**
|
||||
* Regression: normalising an absent container to an empty one makes deepEqual report a
|
||||
* change on every comparison, which would have the runtime re-broadcast and re-save the
|
||||
* restore point on every tick. See PR #2178.
|
||||
*/
|
||||
it.each([
|
||||
['event', makeOntimeEvent({ id: 'partial' })],
|
||||
['group', makeOntimeGroup({ id: 'partial', entries: undefined })],
|
||||
])('gives a partial %s exactly the keys structuredClone would, so it stays deep-equal', (_type, entry) => {
|
||||
const clone = cloneEntryData(entry);
|
||||
// asserting on keys, not values: `toBeUndefined()` cannot tell an absent key from an own
|
||||
// key holding undefined, and it is key presence that decides the deepEqual below
|
||||
expect(Object.keys(clone).sort()).toEqual(Object.keys(structuredClone(entry)).sort());
|
||||
// this is the comparison runtime.service.ts uses to decide whether to re-broadcast an
|
||||
// entry; if the clone gains a key, every tick looks like a change
|
||||
expect(deepEqual(clone, entry)).toBe(true);
|
||||
});
|
||||
|
||||
it('throws on an entry type it does not know how to clone', () => {
|
||||
expect(() => cloneEntryData({ id: 'x', type: 'unknown' } as unknown as OntimeEvent)).toThrow(
|
||||
'Unsupported entry type for cloning',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user