diff --git a/apps/client/src/common/utils/eventsManager.ts b/apps/client/src/common/utils/eventsManager.ts index 468c4d931..b16288296 100644 --- a/apps/client/src/common/utils/eventsManager.ts +++ b/apps/client/src/common/utils/eventsManager.ts @@ -24,6 +24,7 @@ export const cloneEvent = (event: OntimeEvent): ClonedEvent => { skip: event.skip, colour: event.colour, revision: 0, + delay: 0, timeWarning: event.timeWarning, timeDanger: event.timeDanger, custom: {}, diff --git a/apps/server/src/classes/data-provider/DataProvider.ts b/apps/server/src/classes/data-provider/DataProvider.ts index 5dc6fdea2..07ffe31d6 100644 --- a/apps/server/src/classes/data-provider/DataProvider.ts +++ b/apps/server/src/classes/data-provider/DataProvider.ts @@ -86,7 +86,7 @@ function getCustomFields(): Readonly { } async function setRundown(newData: OntimeRundown): ReadonlyPromise { - db.data.rundown = [...newData]; + db.data.rundown = newData; await persist(); return db.data.rundown; } diff --git a/apps/server/src/classes/data-provider/__tests__/DataProvider.utils.test.ts b/apps/server/src/classes/data-provider/__tests__/DataProvider.utils.test.ts index 5315fc833..c95647c2d 100644 --- a/apps/server/src/classes/data-provider/__tests__/DataProvider.utils.test.ts +++ b/apps/server/src/classes/data-provider/__tests__/DataProvider.utils.test.ts @@ -143,6 +143,7 @@ describe('safeMerge', () => { publicInfo: '', backstageUrl: '', backstageInfo: '', + projectLogo: null, }, settings: { app: 'ontime', diff --git a/apps/server/src/models/demoProject.ts b/apps/server/src/models/demoProject.ts index 0733e0a2d..0de02ef79 100644 --- a/apps/server/src/models/demoProject.ts +++ b/apps/server/src/models/demoProject.ts @@ -20,6 +20,7 @@ export const demoDb: DatabaseModel = { skip: false, colour: '', revision: 0, + delay: 0, timeWarning: 500000, timeDanger: 100000, custom: { @@ -45,6 +46,7 @@ export const demoDb: DatabaseModel = { skip: false, colour: '', revision: 0, + delay: 0, timeWarning: 500000, timeDanger: 100000, custom: { @@ -70,6 +72,7 @@ export const demoDb: DatabaseModel = { skip: false, colour: '', revision: 0, + delay: 0, timeWarning: 500000, timeDanger: 100000, custom: { @@ -95,6 +98,7 @@ export const demoDb: DatabaseModel = { skip: false, colour: '', revision: 0, + delay: 0, timeWarning: 500000, timeDanger: 100000, custom: { @@ -120,6 +124,7 @@ export const demoDb: DatabaseModel = { skip: false, colour: '', revision: 0, + delay: 0, timeWarning: 500000, timeDanger: 100000, custom: { @@ -150,6 +155,7 @@ export const demoDb: DatabaseModel = { skip: false, colour: '', revision: 0, + delay: 0, timeWarning: 500000, timeDanger: 100000, custom: { @@ -175,6 +181,7 @@ export const demoDb: DatabaseModel = { skip: false, colour: '', revision: 0, + delay: 0, timeWarning: 500000, timeDanger: 100000, custom: { @@ -200,6 +207,7 @@ export const demoDb: DatabaseModel = { skip: false, colour: '', revision: 0, + delay: 0, timeWarning: 500000, timeDanger: 100000, custom: { @@ -225,6 +233,7 @@ export const demoDb: DatabaseModel = { skip: false, colour: '', revision: 0, + delay: 0, timeWarning: 500000, timeDanger: 100000, custom: { @@ -250,6 +259,7 @@ export const demoDb: DatabaseModel = { skip: false, colour: '', revision: 0, + delay: 0, timeWarning: 500000, timeDanger: 100000, custom: { @@ -280,6 +290,7 @@ export const demoDb: DatabaseModel = { skip: false, colour: '', revision: 0, + delay: 0, timeWarning: 500000, timeDanger: 100000, custom: { @@ -305,6 +316,7 @@ export const demoDb: DatabaseModel = { skip: false, colour: '', revision: 0, + delay: 0, timeWarning: 500000, timeDanger: 100000, custom: { @@ -330,6 +342,7 @@ export const demoDb: DatabaseModel = { skip: false, colour: '', revision: 0, + delay: 0, timeWarning: 500000, timeDanger: 100000, custom: { @@ -355,6 +368,7 @@ export const demoDb: DatabaseModel = { skip: false, colour: '', revision: 0, + delay: 0, timeWarning: 500000, timeDanger: 100000, custom: { diff --git a/apps/server/src/models/eventsDefinition.ts b/apps/server/src/models/eventsDefinition.ts index 40b7a4650..6a10ede10 100644 --- a/apps/server/src/models/eventsDefinition.ts +++ b/apps/server/src/models/eventsDefinition.ts @@ -8,7 +8,7 @@ import { TimerType, } from 'ontime-types'; -export const event: Omit = { +export const event: Omit = { title: '', note: '', endAction: EndAction.None, @@ -24,6 +24,7 @@ export const event: Omit = { colour: '', type: SupportedEvent.Event, revision: 0, + delay: 0, timeWarning: 120000, timeDanger: 60000, custom: {}, diff --git a/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts b/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts index dc7dced1d..f7a7b58b3 100644 --- a/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts +++ b/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts @@ -438,7 +438,7 @@ describe('add() mutation', () => { test('adds an event to the rundown', () => { const mockEvent = { id: 'mock', cue: 'mock', type: SupportedEvent.Event } as OntimeEvent; const testRundown: OntimeRundown = []; - const { newRundown } = add({ atIndex: 0, event: mockEvent, persistedRundown: testRundown }); + const { newRundown } = add({ atIndex: 0, event: mockEvent, rundown: testRundown }); expect(newRundown.length).toBe(1); expect(newRundown[0]).toMatchObject(mockEvent); }); @@ -448,7 +448,7 @@ describe('remove() mutation', () => { test('deletes an event from the rundown', () => { const mockEvent = { id: 'mock', cue: 'mock', type: SupportedEvent.Event } as OntimeEvent; const testRundown: OntimeRundown = [mockEvent]; - const { newRundown } = remove({ eventIds: [mockEvent.id], persistedRundown: testRundown }); + const { newRundown } = remove({ eventIds: [mockEvent.id], rundown: testRundown }); expect(newRundown.length).toBe(0); }); test('deletes multiple events from the rundown', () => { @@ -460,7 +460,7 @@ describe('remove() mutation', () => { { type: SupportedEvent.Event, id: '5' } as OntimeEvent, { type: SupportedEvent.Event, id: '6' } as OntimeEvent, ]; - const { newRundown } = remove({ eventIds: ['1', '2', '3'], persistedRundown: testRundown }); + const { newRundown } = remove({ eventIds: ['1', '2', '3'], rundown: testRundown }); expect(newRundown.length).toBe(3); expect(newRundown.at(0)?.id).toBe('4'); }); @@ -474,7 +474,7 @@ describe('edit() mutation', () => { const { newRundown, newEvent } = edit({ eventId: mockEvent.id, patch: mockEventPatch, - persistedRundown: testRundown, + rundown: testRundown, }); expect(newRundown.length).toBe(1); expect(newEvent).toMatchObject({ @@ -487,7 +487,7 @@ describe('edit() mutation', () => { describe('batchEdit() mutation', () => { it('should correctly apply the patch to the events with the given IDs', () => { - const persistedRundown: OntimeRundown = [ + const testRundown: OntimeRundown = [ { id: '1', type: SupportedEvent.Event, cue: 'data1' } as OntimeEvent, { id: '2', type: SupportedEvent.Event, cue: 'data2' } as OntimeEvent, { id: '3', type: SupportedEvent.Event, cue: 'data3' } as OntimeEvent, @@ -495,7 +495,7 @@ describe('batchEdit() mutation', () => { const eventIds = ['1', '3']; const patch = { cue: 'newData' }; - const { newRundown } = batchEdit({ persistedRundown, eventIds, patch }); + const { newRundown } = batchEdit({ rundown: testRundown, eventIds, patch }); expect(newRundown).toMatchObject([ { id: '1', type: SupportedEvent.Event, cue: 'newData' }, @@ -507,16 +507,16 @@ describe('batchEdit() mutation', () => { describe('reorder() mutation', () => { it('should correctly reorder two events', () => { - const persistedRundown: OntimeRundown = [ + const testRundown: OntimeRundown = [ { id: '1', type: SupportedEvent.Event, cue: 'data1', revision: 0 } as OntimeEvent, { id: '2', type: SupportedEvent.Event, cue: 'data2', revision: 0 } as OntimeEvent, { id: '3', type: SupportedEvent.Event, cue: 'data3', revision: 0 } as OntimeEvent, ]; const { newRundown } = reorder({ - persistedRundown, - eventId: persistedRundown[0].id, + rundown: testRundown, + eventId: testRundown[0].id, from: 0, - to: persistedRundown.length - 1, + to: testRundown.length - 1, }); expect(newRundown).toMatchObject([ @@ -529,15 +529,15 @@ describe('reorder() mutation', () => { describe('swap() mutation', () => { it('should correctly swap data between events', () => { - const persistedRundown: OntimeRundown = [ + const testRundown: OntimeRundown = [ { id: '1', type: SupportedEvent.Event, cue: 'data1', timeStart: 1, revision: 0 } as OntimeEvent, { id: '2', type: SupportedEvent.Event, cue: 'data2', timeStart: 2, revision: 0 } as OntimeEvent, { id: '3', type: SupportedEvent.Event, cue: 'data3', timeStart: 3, revision: 0 } as OntimeEvent, ]; const { newRundown } = swap({ - persistedRundown, - fromId: persistedRundown[0].id, - toId: persistedRundown[1].id, + rundown: testRundown, + fromId: testRundown[0].id, + toId: testRundown[1].id, }); expect((newRundown[0] as OntimeEvent).id).toBe('1'); @@ -576,6 +576,7 @@ describe('calculateRuntimeDelays', () => { colour: '', type: SupportedEvent.Event, revision: 0, + delay: 0, timeWarning: 120000, timeDanger: 60000, id: '659e1', @@ -603,6 +604,7 @@ describe('calculateRuntimeDelays', () => { colour: '', type: SupportedEvent.Event, revision: 0, + delay: 0, timeWarning: 120000, timeDanger: 60000, id: '1c48f', @@ -630,6 +632,7 @@ describe('calculateRuntimeDelays', () => { colour: '', type: SupportedEvent.Event, revision: 0, + delay: 0, timeWarning: 120000, timeDanger: 60000, id: 'd48c2', @@ -657,6 +660,7 @@ describe('calculateRuntimeDelays', () => { colour: '', type: SupportedEvent.Event, revision: 0, + delay: 0, timeWarning: 120000, timeDanger: 60000, id: '2f185', @@ -935,7 +939,7 @@ describe('calculateRuntimeDelaysFrom()', () => { describe('custom fields', () => { describe('createCustomField()', () => { - it('creates a field from given parameters', async () => { + it('creates a field from given parameters', () => { const expected = { Lighting: { label: 'Lighting', @@ -944,14 +948,14 @@ describe('custom fields', () => { }, }; - const customField = await createCustomField({ label: 'Lighting', type: 'string', colour: 'blue' }); + const customField = createCustomField({ label: 'Lighting', type: 'string', colour: 'blue' }); expect(customField).toStrictEqual(expected); }); }); describe('editCustomField()', () => { - it('edits a field with a given label', async () => { - await createCustomField({ label: 'Sound', type: 'string', colour: 'blue' }); + it('edits a field with a given label', () => { + createCustomField({ label: 'Sound', type: 'string', colour: 'blue' }); const expected = { Lighting: { @@ -966,14 +970,14 @@ describe('custom fields', () => { }, }; - const customField = await editCustomField('Sound', { label: 'Sound', type: 'string', colour: 'green' }); + const customField = editCustomField('Sound', { label: 'Sound', type: 'string', colour: 'green' }); expect(customFieldChangelog).toStrictEqual(new Map()); expect(customField).toStrictEqual(expected); }); - it('renames a field to a new label', async () => { - const created = await createCustomField({ label: 'Video', type: 'string', colour: 'red' }); + it('renames a field to a new label', () => { + const created = createCustomField({ label: 'Video', type: 'string', colour: 'red' }); const expected = { Lighting: { @@ -1015,10 +1019,10 @@ describe('custom fields', () => { // We need to flush all scheduled tasks for the generate function to settle vi.useFakeTimers(); - const customField = await editCustomField('Video', { label: 'AV', type: 'string', colour: 'red' }); + const customField = editCustomField('Video', { label: 'AV', type: 'string', colour: 'red' }); expect(customField).toStrictEqual(expectedAfter); expect(customFieldChangelog).toStrictEqual(new Map([['Video', 'AV']])); - await editCustomField('AV', { label: 'Video' }); + editCustomField('AV', { label: 'Video' }); vi.runAllTimers(); expect(customFieldChangelog).toStrictEqual(new Map()); vi.useRealTimers(); @@ -1026,7 +1030,7 @@ describe('custom fields', () => { }); describe('removeCustomField()', () => { - it('deletes a field with a given label', async () => { + it('deletes a field with a given label', () => { const expected = { Lighting: { label: 'Lighting', @@ -1040,7 +1044,7 @@ describe('custom fields', () => { }, }; - const customField = await removeCustomField('Sound'); + const customField = removeCustomField('Sound'); expect(customField).toStrictEqual(expected); }); diff --git a/apps/server/src/services/rundown-service/rundownCache.ts b/apps/server/src/services/rundown-service/rundownCache.ts index 448c9c7b2..9ffd5c71d 100644 --- a/apps/server/src/services/rundown-service/rundownCache.ts +++ b/apps/server/src/services/rundown-service/rundownCache.ts @@ -2,6 +2,7 @@ import { CustomField, CustomFieldLabel, CustomFields, + isOntimeBlock, isOntimeDelay, isOntimeEvent, isPlayableEvent, @@ -31,14 +32,27 @@ type NormalisedRundown = Record; let persistedRundown: OntimeRundown = []; let persistedCustomFields: CustomFields = {}; -/** Utility function gets to expose data */ +/** + * Get the cached rundown without triggering regeneration + */ export const getPersistedRundown = (): OntimeRundown => persistedRundown; export const getCustomFields = (): CustomFields => persistedCustomFields; -let rundown: NormalisedRundown = {}; +let normalisedRundown: NormalisedRundown = {}; let order: EventID[] = []; let revision = 0; + +/** + * all mutating functions will set this value if there is a need for re-generation + * but will only be cleared by the generate function + */ let isStale = true; + +/** Allows safely setting the stale state without accidentally clearing it */ +function setIsStale() { + isStale = true; +} + let totalDelay = 0; let totalDuration = 0; let firstStart: MaybeNumber = null; @@ -72,18 +86,22 @@ export async function init(initialRundown: Readonly, customFields } /** - * Utility initialises cache - * @param rundown + * Utility generate cache + * @private should not be called outside of `rundownCache.ts` */ export function generate( initialRundown: OntimeRundown = persistedRundown, customFields: CustomFields = persistedCustomFields, ) { + function clearIsStale() { + isStale = false; + } + // we decided to re-write this dataset for every change // instead of maintaining logic to update it assignedCustomFields = {}; - rundown = {}; + normalisedRundown = {}; order = []; links = {}; firstStart = null; @@ -94,11 +112,12 @@ export function generate( let lastEntry: PlayableEvent | null = null; for (let i = 0; i < initialRundown.length; i++) { - // TODO: filter properties that should not be persisted (eg: delay) // we assign a reference to the current entry, this will be mutated in place const currentEntry = initialRundown[i]; if (isOntimeEvent(currentEntry)) { + currentEntry.delay = 0; + // 1. handle links - mutates updatedEvent handleLink(i, initialRundown, currentEntry, links); @@ -145,24 +164,30 @@ export function generate( lastEntry = currentEntry; } } - } - - // calculate delays - // !!! this must happen after handling the links - if (isOntimeDelay(currentEntry)) { + } else if (isOntimeDelay(currentEntry)) { + // calculate delays + // !!! this must happen after handling the links totalDelay += currentEntry.duration; + } else if (isOntimeBlock(currentEntry)) { + // calculate block - nothing yet + } else { + // unknown - type skip it + // this is needed to get the type guard working when we assign the entry to the rundown + continue; } // add id to order order.push(currentEntry.id); // add entry to rundown - rundown[currentEntry.id] = currentEntry; + normalisedRundown[currentEntry.id] = currentEntry; } lastEnd = lastEntry?.timeEnd ?? null; - isStale = false; + clearIsStale(); customFieldChangelog.clear(); - return { rundown, order, links, totalDelay, totalDuration, assignedCustomFields }; + + //The return value is used for testing + return { rundown: normalisedRundown, order, links, totalDelay, totalDuration, assignedCustomFields }; } /** Returns an ID guaranteed to be unique */ @@ -173,7 +198,7 @@ export function getUniqueId(): string { let id = ''; do { id = generateId(); - } while (Object.hasOwn(rundown, id)); + } while (Object.hasOwn(normalisedRundown, id)); return id; } @@ -194,15 +219,15 @@ type RundownCache = { }; /** - * Returns cached data - * @returns {RundownCache} + * Returns the full rundown cache. + * Will triggering regeneration if data is stale. */ export function get(): Readonly { if (isStale) { generate(); } return { - rundown, + rundown: normalisedRundown, order, revision, totalDelay, @@ -212,6 +237,7 @@ export function get(): Readonly { /** * Returns calculated metadata from rundown + * Will triggering regeneration if data is stale. */ export function getMetadata() { if (isStale) { @@ -227,7 +253,7 @@ export function getMetadata() { }; } -type CommonParams = { persistedRundown: OntimeRundown }; +type CommonParams = { rundown: OntimeRundown }; type MutationParams = T & CommonParams; type MutatingReturn = { newRundown: OntimeRundown; @@ -238,23 +264,14 @@ type MutatingFn = (params: MutationParams) => MutatingRetur /** * Decorators injects data into mutation - * @param mutation - * @returns + * ensures order of operations when performing mutations */ export function mutateCache(mutation: MutatingFn) { - async function scopedMutation(params: T) { - /** - * Marking the data set as stale - * doing it before calling the mutation, gives the function a chance - * to prevent recalculation by setting stale = false - */ - isStale = true; - - const { newEvent, newRundown, didMutate } = mutation({ ...params, persistedRundown }); + function scopedMutation(params: T) { + const { newEvent, newRundown, didMutate } = mutation({ ...params, rundown: persistedRundown }); // early return without calling side effects if (!didMutate) { - isStale = false; return { newEvent, newRundown, didMutate }; } @@ -278,31 +295,34 @@ export function mutateCache(mutation: MutatingFn) { } type AddArgs = MutationParams<{ atIndex: number; event: OntimeRundownEntry }>; - -export function add({ persistedRundown, atIndex, event }: AddArgs): Required { +/** + * Add entry to rundown + */ +export function add({ rundown, atIndex, event }: AddArgs): Required { const newEvent: OntimeRundownEntry = { ...event }; - const newRundown = insertAtIndex(atIndex, newEvent, persistedRundown); - + const newRundown = insertAtIndex(atIndex, newEvent, rundown); + setIsStale(); return { newRundown, newEvent, didMutate: true }; } type RemoveArgs = MutationParams<{ eventIds: string[] }>; - -export function remove({ persistedRundown, eventIds }: RemoveArgs): MutatingReturn { - const newRundown = persistedRundown.filter((event) => !eventIds.includes(event.id)); - - return { newRundown, didMutate: persistedRundown.length !== newRundown.length }; +/** + * Remove entry to rundown + */ +export function remove({ rundown, eventIds }: RemoveArgs): MutatingReturn { + const newRundown = rundown.filter((event) => !eventIds.includes(event.id)); + const didMutate = rundown.length !== newRundown.length; + if (didMutate) setIsStale(); + return { newRundown, didMutate }; } export function removeAll(): MutatingReturn { + setIsStale(); return { newRundown: [], didMutate: true }; } /** - * Utility function for patching events - * @param eventFromRundown - * @param patch - * @returns + * Utility function for patching an existing event with new data */ function makeEvent(eventFromRundown: OntimeRundownEntry, patch: Partial): OntimeRundownEntry { if (isOntimeEvent(eventFromRundown)) { @@ -315,120 +335,133 @@ function makeEvent(eventFromRundown: OntimeRundownEntry, patch: Partial }>; - -export function edit({ persistedRundown, eventId, patch }: EditArgs): Required { - const indexAt = persistedRundown.findIndex((event) => event.id === eventId); - +/** + * Apply patch to an entry with given id + */ +export function edit({ rundown, eventId, patch }: EditArgs): Required { + const indexAt = rundown.findIndex((event) => event.id === eventId); if (indexAt < 0) { throw new Error('Event not found'); } - if (patch?.type && persistedRundown[indexAt].type !== patch.type) { + if (patch?.type && rundown[indexAt].type !== patch.type) { throw new Error('Invalid event type'); } - const eventInMemory = persistedRundown[indexAt]; + const eventInMemory = rundown[indexAt]; + if (!hasChanges(eventInMemory, patch)) { - isStale = false; - return { newRundown: persistedRundown, newEvent: eventInMemory, didMutate: false }; + return { newRundown: rundown, newEvent: eventInMemory, didMutate: false }; } const newEvent = makeEvent(eventInMemory, patch); - const newRundown = [...persistedRundown]; + const newRundown = [...rundown]; newRundown[indexAt] = newEvent; // check whether the data warrants recalculation of cache const makeStale = isDataStale(patch); - if (!makeStale) { - rundown[newEvent.id] = newEvent; + if (makeStale) { + setIsStale(); + } else { + normalisedRundown[newEvent.id] = newEvent; } - isStale = makeStale; return { newRundown, newEvent, didMutate: true }; } type BatchEditArgs = MutationParams<{ eventIds: string[]; patch: Partial }>; - -export function batchEdit({ persistedRundown, eventIds, patch }: BatchEditArgs): MutatingReturn { +/** + * Apply patch to multiple entries + */ +export function batchEdit({ rundown, eventIds, patch }: BatchEditArgs): MutatingReturn { const ids = new Set(eventIds); const newRundown = []; - for (let i = 0; i < persistedRundown.length; i++) { - if (ids.has(persistedRundown[i].id)) { - if (patch?.type && persistedRundown[i].type !== patch.type) { + for (let i = 0; i < rundown.length; i++) { + if (ids.has(rundown[i].id)) { + if (patch?.type && rundown[i].type !== patch.type) { continue; } - const newEvent = makeEvent(persistedRundown[i], patch); + const newEvent = makeEvent(rundown[i], patch); newRundown.push(newEvent); } else { - newRundown.push(persistedRundown[i]); + newRundown.push(rundown[i]); } } + setIsStale(); return { newRundown, didMutate: true }; } type ReorderArgs = MutationParams<{ eventId: string; from: number; to: number }>; - -export function reorder({ persistedRundown, eventId, from, to }: ReorderArgs): Required { - const event = persistedRundown[from]; +/** + * Redorder two entries + */ +export function reorder({ rundown, eventId, from, to }: ReorderArgs): Required { + const event = rundown[from]; if (!event || eventId !== event.id) { throw new Error('Event not found'); } - const newRundown = reorderArray(persistedRundown, from, to); + const newRundown = reorderArray(rundown, from, to); for (let i = from; i <= to; i++) { const event = newRundown.at(i); if (isOntimeEvent(event)) { event.revision += 1; } } + setIsStale(); return { newRundown, newEvent: newRundown.at(from) as OntimeRundownEntry, didMutate: true }; } type ApplyDelayArgs = MutationParams<{ eventId: string }>; - -export function applyDelay({ persistedRundown, eventId }: ApplyDelayArgs): MutatingReturn { - const newRundown = apply(eventId, persistedRundown); +/** + * Apply a delay + */ +export function applyDelay({ rundown, eventId }: ApplyDelayArgs): MutatingReturn { + const newRundown = apply(eventId, rundown); + setIsStale(); return { newRundown, didMutate: true }; } type SwapArgs = MutationParams<{ fromId: string; toId: string }>; +/** + * Swap two entries + */ +export function swap({ rundown, fromId, toId }: SwapArgs): MutatingReturn { + const indexA = rundown.findIndex((event) => event.id === fromId); + const eventA = rundown.at(indexA); -export function swap({ persistedRundown, fromId, toId }: SwapArgs): MutatingReturn { - const indexA = persistedRundown.findIndex((event) => event.id === fromId); - const eventA = persistedRundown.at(indexA); - - const indexB = persistedRundown.findIndex((event) => event.id === toId); - const eventB = persistedRundown.at(indexB); + const indexB = rundown.findIndex((event) => event.id === toId); + const eventB = rundown.at(indexB); if (!isOntimeEvent(eventA) || !isOntimeEvent(eventB)) { throw new Error('Swap only available for OntimeEvents'); } const { newA, newB } = swapEventData(eventA, eventB); - const newRundown = [...persistedRundown]; + const newRundown = [...rundown]; newRundown[indexA] = newA; (newRundown[indexA] as OntimeEvent).revision += 1; newRundown[indexB] = newB; (newRundown[indexB] as OntimeEvent).revision += 1; + setIsStale(); return { newRundown, didMutate: true }; } /** - * Invalidates service cache if a custom field is used - * @param label + * Utility for invalidating service cache if a custom field is used */ function invalidateIfUsed(label: CustomFieldLabel) { if (label in assignedCustomFields) { - isStale = true; + setIsStale(); } // if the field was in use, we mark the cache as stale if (label in assignedCustomFields) { - isStale = true; + setIsStale(); } // ... and schedule a cache update // schedule a non priority cache update @@ -439,10 +472,9 @@ function invalidateIfUsed(label: CustomFieldLabel) { } /** - * Schedules a non priority custom field persist - * @param persistedCustomFields + * Utility for scheduling a non priority custom field persist */ -function scheduleCustomFieldPersist(persistedCustomFields: CustomFields) { +function scheduleCustomFieldPersist() { setImmediate(async () => { await getDataProvider().setCustomFields(persistedCustomFields); }); @@ -450,12 +482,15 @@ function scheduleCustomFieldPersist(persistedCustomFields: CustomFields) { /** * Sanitises and creates a custom field in the database - * @param field - * @returns */ -export const createCustomField = async (field: CustomField) => { +export function createCustomField(field: CustomField): CustomFields { const { label, type, colour } = field; const key = customFieldLabelToKey(label); + + if (key === null) { + throw new Error('Unable to convert label to a valid key'); + } + // check if label already exists const alreadyExists = Object.hasOwn(persistedCustomFields, key); @@ -466,18 +501,15 @@ export const createCustomField = async (field: CustomField) => { // update object and persist persistedCustomFields[key] = { label, type, colour }; - scheduleCustomFieldPersist(persistedCustomFields); + scheduleCustomFieldPersist(); return persistedCustomFields; -}; +} /** * Edits an existing custom field in the database - * @param key - * @param newField - * @returns */ -export const editCustomField = async (key: string, newField: Partial) => { +export function editCustomField(key: string, newField: Partial): CustomFields { if (!(key in persistedCustomFields)) { throw new Error('Could not find label'); } @@ -487,7 +519,14 @@ export const editCustomField = async (key: string, newField: Partial { +export function removeCustomField(label: string): CustomFields { if (label in persistedCustomFields) { delete persistedCustomFields[label]; } - scheduleCustomFieldPersist(persistedCustomFields); + scheduleCustomFieldPersist(); invalidateIfUsed(label); return persistedCustomFields; -}; +} diff --git a/apps/server/src/services/rundown-service/rundownCacheUtils.ts b/apps/server/src/services/rundown-service/rundownCacheUtils.ts index 8e8a1ab66..28488ab8d 100644 --- a/apps/server/src/services/rundown-service/rundownCacheUtils.ts +++ b/apps/server/src/services/rundown-service/rundownCacheUtils.ts @@ -86,7 +86,7 @@ export function handleCustomField( // rename the property if it is in the changelog if (customFieldChangelog.has(field)) { const oldData = mutableEvent.custom[field]; - const newLabel = customFieldChangelog.get(field); + const newLabel = customFieldChangelog.get(field) as string; // it os OK to cast to string here since we already checked that it existed mutableEvent.custom[newLabel] = oldData; delete mutableEvent.custom[field]; @@ -143,6 +143,6 @@ export function willCauseRegeneration(key: keyof OntimeEvent): boolean { */ export function hasChanges(existingEvent: T, newEvent: Partial): boolean { return Object.keys(newEvent).some( - (key) => !Object.hasOwn(existingEvent, key) || existingEvent[key] !== newEvent[key], + (key) => !Object.hasOwn(existingEvent, key) || existingEvent[key as keyof T] !== newEvent[key as keyof T], ); } diff --git a/apps/server/src/services/sheet-service/__tests__/sheetUtils.test.ts b/apps/server/src/services/sheet-service/__tests__/sheetUtils.test.ts index 0e7ed8565..5ccd6cffd 100644 --- a/apps/server/src/services/sheet-service/__tests__/sheetUtils.test.ts +++ b/apps/server/src/services/sheet-service/__tests__/sheetUtils.test.ts @@ -36,6 +36,7 @@ describe('cellRequestFromEvent()', () => { isPublic: false, skip: false, colour: 'red', + delay: 0, revision: 0, id: '1358', timeWarning: 0, @@ -82,6 +83,7 @@ describe('cellRequestFromEvent()', () => { skip: false, colour: 'red', revision: 0, + delay: 0, id: '1358', timeWarning: 0, timeDanger: 0, @@ -129,6 +131,7 @@ describe('cellRequestFromEvent()', () => { skip: false, colour: 'red', revision: 0, + delay: 0, id: '1358', timeWarning: 0, timeDanger: 0, @@ -174,6 +177,7 @@ describe('cellRequestFromEvent()', () => { isPublic: true, skip: false, colour: 'red', + delay: 0, revision: 0, id: '1358', timeWarning: 0, @@ -207,6 +211,7 @@ describe('cellRequestFromEvent()', () => { skip: false, colour: 'red', revision: 0, + delay: 0, id: '1358', timeWarning: 0, timeDanger: 0, @@ -240,6 +245,7 @@ describe('cellRequestFromEvent()', () => { skip: false, colour: 'red', revision: 0, + delay: 0, id: '1358', timeWarning: 0, timeDanger: 0, diff --git a/apps/server/src/utils/parser.ts b/apps/server/src/utils/parser.ts index 102230f4e..3caf5e311 100644 --- a/apps/server/src/utils/parser.ts +++ b/apps/server/src/utils/parser.ts @@ -384,6 +384,7 @@ export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial { if (isAlphanumericWithSpace(label)) {