diff --git a/apps/server/src/api-data/rundown/rundown.utils.ts b/apps/server/src/api-data/rundown/rundown.utils.ts index d5757e65b..df009a605 100644 --- a/apps/server/src/api-data/rundown/rundown.utils.ts +++ b/apps/server/src/api-data/rundown/rundown.utils.ts @@ -23,6 +23,7 @@ import { import { event as eventDef, block as blockDef, delay as delayDef } from '../../models/eventsDefinition.js'; import { makeString } from '../../utils/parserUtils.js'; +import { RundownMetadata } from './rundown.types.js'; type CompleteEntry = T extends Partial @@ -356,3 +357,23 @@ export function getInsertAfterId(rundown: Rundown, afterId?: EntryId, beforeId?: return null; } + +/** + * converts an index from the timedEventOrder to an index in the playableEventOrder + * or returns null if it can not be found + */ +export function getPlayableIndexFromTimedIndex(metadata: RundownMetadata, index: number): number | null { + const timedId = metadata.timedEventOrder[index]; + const playableIndex = metadata.playableEventOrder.findIndex((id) => id === timedId); + return playableIndex < 0 ? null : playableIndex; +} + +/** + * converts an index from the playableEventOrder to an index in the timedEventOrder + * all indexes in playableEventOrder must also exist in timedEventOrder, otherwise the app is broken + */ +export function getTimedIndexFromPlayableIndex(metadata: RundownMetadata, index: number): number { + const playableId = metadata.playableEventOrder[index]; + const timedIndex = metadata.timedEventOrder.findIndex((id) => id === playableId); + return timedIndex; +} diff --git a/apps/server/src/services/__tests__/rollUtils.test.ts b/apps/server/src/services/__tests__/rollUtils.test.ts index 1d031d1a1..c555fd847 100644 --- a/apps/server/src/services/__tests__/rollUtils.test.ts +++ b/apps/server/src/services/__tests__/rollUtils.test.ts @@ -1,185 +1,265 @@ -import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE } from 'ontime-utils'; +import { dayInMs, MILLIS_PER_HOUR, MILLIS_PER_MINUTE } from 'ontime-utils'; import { loadRoll } from '../rollUtils.js'; -import { prepareTimedEvents, makeOntimeEvent } from '../../api-data/rundown/__mocks__/rundown.mocks.js'; +import { makeRundown } from '../../api-data/rundown/__mocks__/rundown.mocks.js'; +import { PlayableEvent } from 'ontime-types'; +import { initRundown } from '../../api-data/rundown/rundown.service.js'; +import { rundownCache } from '../../api-data/rundown/rundown.dao.js'; + +beforeAll(() => { + vi.mock('../../classes/data-provider/DataProvider.js', () => { + return { + getDataProvider: vi.fn().mockImplementation(() => { + return { + setCustomFields: vi.fn().mockImplementation((newData) => newData), + setRundown: vi.fn().mockImplementation((newData) => newData), + }; + }), + }; + }); +}); + +const mockEvent = { + type: 'event', + id: 'mock', + cue: 'mock', + timeStart: 0, + timeEnd: 1000, + duration: 1000, + skip: false, + parent: null, +} as PlayableEvent; describe('loadRoll()', () => { - const eventlist = [ - { - id: '1', - timeStart: 5, - timeEnd: 10, - }, - { - id: '2', - timeStart: 10, - timeEnd: 20, - }, - { - id: '3', - timeStart: 20, - timeEnd: 30, - }, - { - id: '4', - timeStart: 30, - timeEnd: 40, - }, - { - id: '5', - timeStart: 40, - timeEnd: 50, - }, - { - id: '6', - timeStart: 50, - timeEnd: 60, - }, - { - id: '7', - timeStart: 60, - timeEnd: 70, - }, - { - id: '8', - timeStart: 70, - timeEnd: 80, - }, - ]; - const timedEvents = prepareTimedEvents(eventlist); + beforeEach(async () => { + vi.useFakeTimers(); + await initRundown( + makeRundown({ + entries: { + '1': { + ...mockEvent, + id: '1', + timeStart: 5, + timeEnd: 10, + duration: 5, + }, + '2': { + ...mockEvent, + id: '2', + timeStart: 10, + timeEnd: 20, + duration: 10, + }, + '3': { + ...mockEvent, + id: '3', + timeStart: 20, + timeEnd: 30, + duration: 10, + }, + '4': { + ...mockEvent, + id: '4', + timeStart: 30, + timeEnd: 40, + duration: 10, + }, + '5': { + ...mockEvent, + id: '5', + timeStart: 40, + timeEnd: 50, + duration: 10, + }, + '6': { + ...mockEvent, + id: '6', + timeStart: 50, + timeEnd: 60, + duration: 10, + }, + '7': { + ...mockEvent, + id: '7', + timeStart: 60, + timeEnd: 70, + duration: 10, + }, + '8': { + ...mockEvent, + id: '8', + timeStart: 70, + timeEnd: 80, + duration: 10, + }, + }, + order: ['1', '2', '3', '4', '5', '6', '7', '8'], + }), + {}, + ); + vi.runAllTimers(); + vi.useRealTimers(); + }); it('should roll to the day after if timer is at 100', () => { + const { rundown, metadata } = rundownCache.get(); const now = 100; const expected = { - event: timedEvents[0], + event: rundown.entries['1'], index: 0, isPending: true, }; - const state = loadRoll(timedEvents, now); + const state = loadRoll(rundown, metadata, now); expect(state).toStrictEqual(expected); }); it('should be waiting to start if timer is at 0', () => { + const { rundown, metadata } = rundownCache.get(); const now = 0; const expected = { - event: timedEvents[0], + event: rundown.entries['1'], index: 0, isPending: true, }; - const state = loadRoll(timedEvents, now); + const state = loadRoll(rundown, metadata, now); expect(state).toStrictEqual(expected); }); it('should start the first event if timer is at 5', () => { + const { rundown, metadata } = rundownCache.get(); const now = 5; const expected = { - event: timedEvents[0], + event: rundown.entries['1'], index: 0, }; - const state = loadRoll(timedEvents, now); + const state = loadRoll(rundown, metadata, now); expect(state).toStrictEqual(expected); }); it('should start the second event if timer is at 15', () => { + const { rundown, metadata } = rundownCache.get(); const now = 15; const expected = { - event: timedEvents[1], + event: rundown.entries['2'], index: 1, }; - const state = loadRoll(timedEvents, now); + const state = loadRoll(rundown, metadata, now); expect(state).toStrictEqual(expected); }); it('should start the third event if timer is at 10', () => { + const { rundown, metadata } = rundownCache.get(); const now = 20; const expected = { - event: timedEvents[2], + event: rundown.entries['3'], index: 2, }; - const state = loadRoll(timedEvents, now); + const state = loadRoll(rundown, metadata, now); expect(state).toStrictEqual(expected); }); it('should start the fifth event if timer is at 49', () => { + const { rundown, metadata } = rundownCache.get(); const now = 49; const expected = { - event: timedEvents[4], + event: rundown.entries['5'], index: 4, }; - const state = loadRoll(timedEvents, now); + const state = loadRoll(rundown, metadata, now); expect(state).toStrictEqual(expected); }); it('should start the seventh event if timer is at 63', () => { + const { rundown, metadata } = rundownCache.get(); const now = 63; const expected = { - event: timedEvents[6], + event: rundown.entries['7'], index: 6, }; - const state = loadRoll(timedEvents, now); + const state = loadRoll(rundown, metadata, now); expect(state).toStrictEqual(expected); }); it('should start the eight event if timer is at 75', () => { + const { rundown, metadata } = rundownCache.get(); const now = 75; const expected = { - event: timedEvents[7], + event: rundown.entries['8'], index: 7, }; - const state = loadRoll(timedEvents, now); + const state = loadRoll(rundown, metadata, now); expect(state).toStrictEqual(expected); }); }); describe('loadRoll() handle edge cases with midnight', () => { - it('should find an event that crosses midnight', () => { + it('should find an event that crosses midnight', async () => { const now = 23 * MILLIS_PER_HOUR; - const eventlist = [ - { - id: '0', - timeStart: 9 * MILLIS_PER_HOUR, - timeEnd: 10 * MILLIS_PER_HOUR, - }, - { - id: '1', - timeStart: 20 * MILLIS_PER_HOUR, - timeEnd: 22 * MILLIS_PER_HOUR, - }, - { - id: '2', - timeStart: 22 * MILLIS_PER_HOUR, - timeEnd: 1 * MILLIS_PER_HOUR, - }, - { - id: '3', - timeStart: 1 * MILLIS_PER_HOUR, - timeEnd: 1 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE, - }, - { - id: '4', - timeStart: 1 * MILLIS_PER_HOUR, - timeEnd: 2 * MILLIS_PER_HOUR, - }, - ]; - const timedEvents = prepareTimedEvents(eventlist); + vi.useFakeTimers(); + rundownCache.init( + makeRundown({ + order: ['0', '1', '2', '3', '4'], + entries: { + '0': { + ...mockEvent, + id: '0', + timeStart: 9 * MILLIS_PER_HOUR, + timeEnd: 10 * MILLIS_PER_HOUR, + duration: 1 * MILLIS_PER_HOUR, + }, + '1': { + ...mockEvent, + id: '1', + timeStart: 20 * MILLIS_PER_HOUR, + timeEnd: 22 * MILLIS_PER_HOUR, + duration: 2 * MILLIS_PER_HOUR, + }, + '2': { + ...mockEvent, + id: '2', + timeStart: 22 * MILLIS_PER_HOUR, + timeEnd: 1 * MILLIS_PER_HOUR, + duration: 3 * MILLIS_PER_HOUR, + }, + '3': { + ...mockEvent, + id: '3', + timeStart: 1 * MILLIS_PER_HOUR, + timeEnd: 1 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE, + duration: 10 * MILLIS_PER_MINUTE, + }, + '4': { + ...mockEvent, + id: '4', + timeStart: 1 * MILLIS_PER_HOUR, + timeEnd: 2 * MILLIS_PER_HOUR, + duration: 1 * MILLIS_PER_HOUR, + }, + }, + }), + {}, + ); + vi.runAllTimers(); + vi.useRealTimers(); + const { rundown, metadata } = rundownCache.get(); const expected = { - event: timedEvents[2], + event: rundown.entries['2'], index: 2, }; - const state = loadRoll(timedEvents, now); + const state = loadRoll(rundown, metadata, now); expect(state).toStrictEqual(expected); }); - it('should not skip to the second day', () => { + it('should not skip to the second day', async () => { /** * NOTE: this is a potentially contentious decision * @@ -188,102 +268,160 @@ describe('loadRoll() handle edge cases with midnight', () => { * * On our side, this simplifies logic and makes behaviour more predictable */ + + vi.useFakeTimers(); + await initRundown( + makeRundown({ + order: ['0', '1', '2'], + entries: { + '0': { + ...mockEvent, + id: '0', + timeStart: 21 * MILLIS_PER_HOUR, + timeEnd: 22 * MILLIS_PER_HOUR, + duration: 1 * MILLIS_PER_HOUR, + }, + '1': { + ...mockEvent, + id: '1', + timeStart: 22 * MILLIS_PER_HOUR, + timeEnd: 3 * MILLIS_PER_HOUR, + duration: 5 * MILLIS_PER_HOUR, + }, + '2': { + ...mockEvent, + id: '2', + timeStart: 3 * MILLIS_PER_HOUR, + timeEnd: 10 * MILLIS_PER_HOUR, + duration: 7 * MILLIS_PER_HOUR, + }, + }, + }), + {}, + ); + vi.runAllTimers(); + vi.useRealTimers(); + + const { rundown, metadata } = rundownCache.get(); const now = 8 * MILLIS_PER_HOUR; - const eventlist = [ - { - id: '0', - timeStart: 21 * MILLIS_PER_HOUR, - timeEnd: 22 * MILLIS_PER_HOUR, - }, - { - id: '1', - timeStart: 22 * MILLIS_PER_HOUR, - timeEnd: 3 * MILLIS_PER_HOUR, - }, - { - id: '2', - timeStart: 3 * MILLIS_PER_HOUR, - timeEnd: 10 * MILLIS_PER_HOUR, - }, - ]; - const timedEvents = prepareTimedEvents(eventlist); + const expected = { - event: timedEvents[0], + event: rundown.entries['0'], index: 0, isPending: true, }; - const state = loadRoll(timedEvents, now); + const state = loadRoll(rundown, metadata, now); expect(state).toStrictEqual(expected); }); }); describe('loadRoll() handle rundowns with several days', () => { - it('should find the correct event, when we have many days', () => { - const now = 11 * MILLIS_PER_HOUR + 30 * MILLIS_PER_MINUTE; - const timedEvents = [ - { - id: '0', - timeStart: 10 * MILLIS_PER_HOUR, - timeEnd: 11 * MILLIS_PER_HOUR, - }, - { - id: '2', - timeStart: 11 * MILLIS_PER_HOUR, - timeEnd: 12 * MILLIS_PER_HOUR, - }, - { - id: '3', - timeStart: 12 * MILLIS_PER_HOUR, - timeEnd: 13 * MILLIS_PER_HOUR, - }, - { - id: '4', - timeStart: 11 * MILLIS_PER_HOUR, - timeEnd: 12 * MILLIS_PER_HOUR, - }, - ]; + it('should find the correct event, when we have many days', async () => { + vi.useFakeTimers(); + await initRundown( + makeRundown({ + order: ['0', '2', '3', '4'], + entries: { + '0': { + ...mockEvent, + id: '0', + timeStart: 10 * MILLIS_PER_HOUR, + timeEnd: 11 * MILLIS_PER_HOUR, + duration: 1 * MILLIS_PER_HOUR, + }, + '2': { + ...mockEvent, + id: '2', + timeStart: 11 * MILLIS_PER_HOUR, + timeEnd: 12 * MILLIS_PER_HOUR, + duration: 1 * MILLIS_PER_HOUR, + }, + '3': { + ...mockEvent, + id: '3', + timeStart: 12 * MILLIS_PER_HOUR, + timeEnd: 13 * MILLIS_PER_HOUR, + duration: 1 * MILLIS_PER_HOUR, + }, + '4': { + ...mockEvent, + id: '4', + timeStart: 11 * MILLIS_PER_HOUR, + timeEnd: 12 * MILLIS_PER_HOUR, + duration: 1 * MILLIS_PER_HOUR, + }, + }, + }), + {}, + ); + vi.runAllTimers(); + vi.useRealTimers(); - const state = loadRoll(prepareTimedEvents(timedEvents), now); + const { rundown, metadata } = rundownCache.get(); + const now = 11 * MILLIS_PER_HOUR + 30 * MILLIS_PER_MINUTE; + + const state = loadRoll(rundown, metadata, now); const expected = { - event: timedEvents[1], + event: rundown.entries['2'], index: 1, }; expect(state).toMatchObject(expected); }); - it('should find the correct event, when we have events of zero duration', () => { - const now = 20 * MILLIS_PER_HOUR + 37 * MILLIS_PER_MINUTE; - const timedEvents = [ - { - id: '0', - timeStart: 18 * MILLIS_PER_HOUR, - timeEnd: 19 * MILLIS_PER_HOUR, - }, - { - id: '1 no duration', - timeStart: 0, - timeEnd: 0, - }, - { - id: '2', - timeStart: 19 * MILLIS_PER_HOUR, - timeEnd: 20 * MILLIS_PER_HOUR, - }, - { - id: '3 no duration', - timeStart: 0, - timeEnd: 0, - }, - { - id: '4', - timeStart: 20 * MILLIS_PER_HOUR, - timeEnd: 21 * MILLIS_PER_HOUR, - }, - ]; + it('should find the correct event, when we have events of zero duration', async () => { + vi.useFakeTimers(); + await initRundown( + makeRundown({ + order: ['0', '1 no duration', '2', '3 no duration', '4'], + entries: { + '0': { + ...mockEvent, + id: '0', + timeStart: 18 * MILLIS_PER_HOUR, + timeEnd: 19 * MILLIS_PER_HOUR, + duration: 1 * MILLIS_PER_HOUR, + }, + '1 no duration': { + ...mockEvent, + id: '1 no duration', + timeStart: 0, + timeEnd: 0, + duration: 0, + }, + '2': { + ...mockEvent, + id: '2', + timeStart: 19 * MILLIS_PER_HOUR, + timeEnd: 20 * MILLIS_PER_HOUR, + duration: 1 * MILLIS_PER_HOUR, + }, + ['3 no duration']: { + ...mockEvent, + id: '3 no duration', + timeStart: 0, + timeEnd: 0, + duration: 0, + }, + '4': { + ...mockEvent, + id: '4', + timeStart: 20 * MILLIS_PER_HOUR, + timeEnd: 21 * MILLIS_PER_HOUR, + duration: 1 * MILLIS_PER_HOUR, + }, + }, + }), + {}, + ); + vi.runAllTimers(); + vi.useRealTimers(); - const state = loadRoll(prepareTimedEvents(timedEvents), now); + const { rundown, metadata } = rundownCache.get(); + const now = 20 * MILLIS_PER_HOUR + 37 * MILLIS_PER_MINUTE; + + const state = loadRoll(rundown, metadata, now); const expected = { - event: timedEvents[4], + event: rundown.entries['4'], index: 4, }; expect(state).toMatchObject(expected); @@ -291,194 +429,338 @@ describe('loadRoll() handle rundowns with several days', () => { }); describe('loadRoll() handle edge cases with before and after start', () => { - it('should prepare first event, if we are not yet in the rundown start', () => { + it('should prepare first event, if we are not yet in the rundown start', async () => { + vi.useFakeTimers(); + await initRundown( + makeRundown({ + order: ['1'], + entries: { + '1': { + ...mockEvent, + id: '1', + timeStart: 10 * MILLIS_PER_HOUR, + timeEnd: 11 * MILLIS_PER_HOUR, + duration: 1 * MILLIS_PER_HOUR, + }, + }, + }), + {}, + ); + vi.runAllTimers(); + vi.useRealTimers(); + + const { rundown, metadata } = rundownCache.get(); const now = 7 * MILLIS_PER_HOUR; - const singleEventList = [ - makeOntimeEvent({ - id: '1', - timeStart: 10 * MILLIS_PER_HOUR, - timeEnd: 11 * MILLIS_PER_HOUR, - }), - ]; const expected = { - event: singleEventList[0], + event: rundown.entries['1'], index: 0, isPending: true, }; - const state = loadRoll(singleEventList, now); + const state = loadRoll(rundown, metadata, now); expect(state).toStrictEqual(expected); }); - it('should prepare first event, if we are over the rundown end', () => { + it('should prepare first event, if we are over the rundown end', async () => { + vi.useFakeTimers(); + await initRundown( + makeRundown({ + order: ['1'], + entries: { + '1': { + ...mockEvent, + id: '1', + timeStart: 10 * MILLIS_PER_HOUR, + timeEnd: 11 * MILLIS_PER_HOUR, + duration: 1 * MILLIS_PER_HOUR, + }, + }, + }), + {}, + ); + vi.runAllTimers(); + vi.useRealTimers(); + + const { rundown, metadata } = rundownCache.get(); const now = 18 * MILLIS_PER_HOUR; - const singleEventList = [ - makeOntimeEvent({ - id: '1', - timeStart: 10 * MILLIS_PER_HOUR, - timeEnd: 11 * MILLIS_PER_HOUR, - }), - ]; const expected = { - event: singleEventList[0], + event: rundown.entries['1'], index: 0, isPending: true, }; - const state = loadRoll(singleEventList, now); + const state = loadRoll(rundown, metadata, now); expect(state).toStrictEqual(expected); }); - it('should account for a rundown that goes through midnight', () => { - const now = 1 * MILLIS_PER_HOUR; - const singleEventList = [ - makeOntimeEvent({ - id: '1', - timeStart: 10 * MILLIS_PER_HOUR, - timeEnd: 2 * MILLIS_PER_HOUR, + it('should account for a rundown that goes through midnight', async () => { + vi.useFakeTimers(); + await initRundown( + makeRundown({ + order: ['1'], + entries: { + '1': { + ...mockEvent, + id: '1', + timeStart: 10 * MILLIS_PER_HOUR, + timeEnd: 2 * MILLIS_PER_HOUR, + duration: 16 * MILLIS_PER_HOUR, + }, + }, }), - ]; + {}, + ); + vi.runAllTimers(); + vi.useRealTimers(); + + const { rundown, metadata } = rundownCache.get(); + const now = 1 * MILLIS_PER_HOUR; + const expected = { - event: singleEventList[0], + event: rundown.entries['1'], index: 0, }; - const state = loadRoll(singleEventList, now); + const state = loadRoll(rundown, metadata, now); expect(state.isPending).toBeUndefined(); // we are playing the event expect(state).toStrictEqual(expected); }); - it('loads upcoming event while waiting to roll', () => { - const now = 6000; // 00:01 - const singleEventList = [ - makeOntimeEvent({ - id: '1', - timeStart: 72000000, // 20:00 - timeEnd: 72010000, // 20:10 + it('loads upcoming event while waiting to roll', async () => { + vi.useFakeTimers(); + await initRundown( + makeRundown({ + order: ['1'], + entries: { + ['1']: { + ...mockEvent, + id: '1', + timeStart: 72000000, // 20:00 + timeEnd: 72010000, // 20:10 + duration: 10 * MILLIS_PER_MINUTE, + }, + }, }), - ]; + {}, + ); + vi.runAllTimers(); + vi.useRealTimers(); + + const { rundown, metadata } = rundownCache.get(); + const now = 6000; // 00:01 + const expected = { - event: singleEventList[0], + event: rundown.entries['1'], index: 0, isPending: true, }; - const state = loadRoll(singleEventList, now); + const state = loadRoll(rundown, metadata, now); expect(state).toStrictEqual(expected); }); }); describe('loadRoll() test that roll behaviour with overlapping times', () => { - const eventlist = [ - { - id: '1', - timeStart: 10, - timeEnd: 10, - }, - { - id: '2', - timeStart: 10, - timeEnd: 20, - }, - { - id: '3', - timeStart: 10, - timeEnd: 30, - }, - ]; - const timedEvents = prepareTimedEvents(eventlist); + beforeEach(async () => { + vi.useFakeTimers(); + await initRundown( + makeRundown({ + order: ['1', '2', '3'], + entries: { + ['1']: { + ...mockEvent, + id: '1', + timeStart: 9, + timeEnd: 10, + duration: 1, + }, + ['2']: { + ...mockEvent, + id: '2', + timeStart: 10, + timeEnd: 20, + duration: 10, + }, + ['3']: { + ...mockEvent, + id: '3', + timeStart: 10, + timeEnd: 30, + duration: 10, + }, + }, + }), + {}, + ); + vi.runAllTimers(); + vi.useRealTimers(); + }); it('if timer is at 0', () => { + const { rundown, metadata } = rundownCache.get(); const now = 0; const expected = { - event: timedEvents[0], + event: rundown.entries['1'], index: 0, isPending: true, }; - const state = loadRoll(timedEvents, now); + const state = loadRoll(rundown, metadata, now); expect(state).toStrictEqual(expected); }); it('if timer is at 10, it ignores events with 0 duration', () => { + const { rundown, metadata } = rundownCache.get(); const now = 10; const expected = { - event: timedEvents[1], + event: rundown.entries['2'], index: 1, }; - const state = loadRoll(timedEvents, now); + const state = loadRoll(rundown, metadata, now); expect(state).toStrictEqual(expected); }); it('if timer is at 15', () => { + const { rundown, metadata } = rundownCache.get(); const now = 15; const expected = { - event: timedEvents[1], + event: rundown.entries['2'], index: 1, }; - const state = loadRoll(timedEvents, now); + const state = loadRoll(rundown, metadata, now); expect(state).toStrictEqual(expected); }); it('if timer is at 20', () => { + const { rundown, metadata } = rundownCache.get(); const now = 20; const expected = { - event: timedEvents[2], + event: rundown.entries['3'], index: 2, }; - const state = loadRoll(timedEvents, now); + const state = loadRoll(rundown, metadata, now); expect(state).toStrictEqual(expected); }); it('if timer is at 25', () => { + const { rundown, metadata } = rundownCache.get(); const now = 25; const expected = { - event: timedEvents[2], + event: rundown.entries['3'], index: 2, }; - const state = loadRoll(timedEvents, now); + const state = loadRoll(rundown, metadata, now); expect(state).toStrictEqual(expected); }); }); // issue #58 describe('loadRoll() test that roll behaviour multi day event edge cases', () => { - it('should recognise a playing event where its schedule spans over midnight', () => { - const now = 66600000; // 19:30 - const eventlist = [ - makeOntimeEvent({ - id: '1', - timeStart: 66000000, // 19:20 - timeEnd: 54600000, // 16:10 + it('should recognise a playing event where its schedule spans over midnight', async () => { + vi.useFakeTimers(); + await initRundown( + makeRundown({ + order: ['1'], + entries: { + '1': { + ...mockEvent, + id: '1', + timeStart: 66000000, // 19:20 + timeEnd: 54600000, // 16:10 + duration: 1, // value not important + }, + }, }), - ]; + {}, + ); + vi.runAllTimers(); + vi.useRealTimers(); + + const { rundown, metadata } = rundownCache.get(); + const now = 66600000; // 19:30 const expected = { - event: eventlist[0], + event: rundown.entries['1'], index: 0, }; - const state = loadRoll(eventlist, now); + const state = loadRoll(rundown, metadata, now); expect(state).toStrictEqual(expected); }); - it('if the start time is the day after end time, and both are later than now', () => { - const now = 66840000; // 19:34 - const eventlist = [ - makeOntimeEvent({ - id: '1', - timeStart: 67200000, // 19:40 - timeEnd: 66900000, // 19:35 + it('if the start time is the day after end time, and both are later than now', async () => { + vi.useFakeTimers(); + await initRundown( + makeRundown({ + order: ['1'], + entries: { + '1': { + ...mockEvent, + id: '1', + timeStart: 67200000, // 19:40 + timeEnd: 66900000, // 19:35 + duration: dayInMs - 5 * MILLIS_PER_MINUTE, + }, + }, }), - ]; + {}, + ); + vi.runAllTimers(); + vi.useRealTimers(); + + const { rundown, metadata } = rundownCache.get(); + const now = 66840000; // 19:34 + const expected = { - event: eventlist[0], + event: rundown.entries['1'], index: 0, }; - const state = loadRoll(eventlist, now); + const state = loadRoll(rundown, metadata, now); expect(state.isPending).toBeUndefined(); // we are playing the event expect(state).toStrictEqual(expected); }); }); + +describe('loadRoll() should not roll skipped events', () => { + test('', async () => { + vi.useFakeTimers(); + await initRundown( + makeRundown({ + order: ['1', '2'], + entries: { + '1': { + ...mockEvent, + id: '1', + timeStart: 10, + timeEnd: 20, + duration: 10, + skip: true, + }, + '2': { + ...mockEvent, + id: '2', + timeStart: 20, + timeEnd: 30, + duration: 10, + }, + }, + }), + {}, + ); + vi.runAllTimers(); + vi.useRealTimers(); + + const { rundown, metadata } = rundownCache.get(); + const now = 2; + const expected = { + event: rundown.entries['2'], + index: 1, + isPending: true, + }; + + const state = loadRoll(rundown, metadata, now); + expect(state).toStrictEqual(expected); + }); +}); diff --git a/apps/server/src/services/rollUtils.ts b/apps/server/src/services/rollUtils.ts index b2f7f9bbe..535e55394 100644 --- a/apps/server/src/services/rollUtils.ts +++ b/apps/server/src/services/rollUtils.ts @@ -1,22 +1,25 @@ -import { dayInMs, getFirstEvent } from 'ontime-utils'; -import { OntimeEvent, MaybeNumber, PlayableEvent, isPlayableEvent } from 'ontime-types'; +import { dayInMs } from 'ontime-utils'; +import { MaybeNumber, PlayableEvent, Rundown } from 'ontime-types'; import { normaliseEndTime } from './timerUtils.js'; +import { RundownMetadata } from '../api-data/rundown/rundown.types.js'; +import { getTimedIndexFromPlayableIndex } from '../api-data/rundown/rundown.utils.js'; /** * Finds current event in a rolling rundown */ export function loadRoll( - timedEvents: OntimeEvent[], + rundown: Rundown, + metadata: RundownMetadata, timeNow: number, ): { event: PlayableEvent | null; index: MaybeNumber; isPending?: boolean; } { - const { firstEvent } = getFirstEvent(timedEvents); + const firstEventId = metadata.playableEventOrder[0]; - if (!firstEvent) { + if (!firstEventId) { return { event: null, index: null }; } @@ -24,13 +27,8 @@ export function loadRoll( // account for number of times we went over midnight let daySpan = 0; - for (let i = 0; i < timedEvents.length; i++) { - const event = timedEvents[i]; - - if (!isPlayableEvent(event)) { - continue; - } - + for (let i = 0; i < metadata.playableEventOrder.length; i++) { + const event = rundown.entries[metadata.playableEventOrder[i]] as PlayableEvent; if (event.duration === 0) { continue; } @@ -62,16 +60,17 @@ export function loadRoll( const isFromDayBefore = normalEnd > dayInMs && timeNow < event.timeEnd; const hasStarted = isFromDayBefore || timeNow >= event.timeStart; if (hasStarted) { - return { event, index: i }; + return { event, index: getTimedIndexFromPlayableIndex(metadata, i) }; } // 3. event will run in the future // we set the isPending flag to indicate that the event is currently playing - return { event, index: i, isPending: true }; + return { event, index: getTimedIndexFromPlayableIndex(metadata, i), isPending: true }; } // in case we were unable to find anything, we load the first event - return { event: firstEvent, index: 0, isPending: true }; + console.log('returning first event'); + return { event: rundown.entries[firstEventId] as PlayableEvent, index: 0, isPending: true }; } /** diff --git a/apps/server/src/services/runtime-service/RuntimeService.ts b/apps/server/src/services/runtime-service/RuntimeService.ts index 9e0889c76..b5dfe584d 100644 --- a/apps/server/src/services/runtime-service/RuntimeService.ts +++ b/apps/server/src/services/runtime-service/RuntimeService.ts @@ -30,7 +30,6 @@ import { RestorePoint, restoreService } from '../RestoreService.js'; import { skippedOutOfEvent } from '../timerUtils.js'; import { - filterTimedEvents, findNextPlayableId, findNextPlayableWithCue, findPreviousPlayableId, @@ -190,17 +189,15 @@ class RuntimeService { } private isNewNext() { - const rundown = getCurrentRundown(); const { timedEventOrder } = getRundownMetadata(); - const timedEvents = filterTimedEvents(rundown, timedEventOrder); const state = runtimeState.getState(); const now = state.eventNow?.id; const next = state.eventNext?.id; // check whether the index of now and next are consecutive - const indexNow = timedEvents.findIndex((event) => event.id === now); - const indexNext = timedEvents.findIndex((event) => event.id === next); + const indexNow = timedEventOrder.findIndex((id) => id === now); + const indexNext = timedEventOrder.findIndex((id) => id === next); return indexNext - indexNow !== 1; } @@ -241,8 +238,8 @@ class RuntimeService { runtimeState.updateLoaded(eventNow); } else { const rundown = getCurrentRundown(); - const { timedEventOrder } = getRundownMetadata(); - runtimeState.updateAll(rundown, timedEventOrder); + const metadata = getRundownMetadata(); + runtimeState.updateAll(rundown, metadata); } return; } @@ -252,9 +249,8 @@ class RuntimeService { isNext = this.isNewNext(); if (isNext) { const rundown = getCurrentRundown(); - const { timedEventOrder } = getRundownMetadata(); - const timedEvents = filterTimedEvents(rundown, timedEventOrder); - runtimeState.loadNext(timedEvents); + const metadata = getRundownMetadata(); + runtimeState.loadNext(rundown, metadata); } } @@ -273,8 +269,8 @@ class RuntimeService { // we can ignore events which are not playable const rundown = getCurrentRundown(); - const rundownMetadata = getRundownMetadata(); - const success = runtimeState.load(event, rundown, rundownMetadata.playableEventOrder, initialData); + const metadata = getRundownMetadata(); + const success = runtimeState.load(event, rundown, metadata, initialData); if (success) { logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`); @@ -603,10 +599,10 @@ class RuntimeService { */ private rollLoaded(offset?: number) { const rundown = getCurrentRundown(); - const { timedEventOrder } = getRundownMetadata(); + const metadata = getRundownMetadata(); try { - runtimeState.roll(rundown, timedEventOrder, offset); + runtimeState.roll(rundown, metadata, offset); } catch (error) { logger.error(LogOrigin.Server, `Roll: ${error}`); } @@ -627,8 +623,8 @@ class RuntimeService { try { const rundown = getCurrentRundown(); - const rundownMetadata = getRundownMetadata(); - const result = runtimeState.roll(rundown, rundownMetadata.playableEventOrder); + const metadata = getRundownMetadata(); + const result = runtimeState.roll(rundown, metadata); const newState = runtimeState.getState(); if (result.eventId !== previousState.eventNow?.id) { @@ -681,8 +677,8 @@ class RuntimeService { } const rundown = getCurrentRundown(); - const rundownMetadata = getRundownMetadata(); - runtimeState.resume(restorePoint, event, rundown, rundownMetadata.playableEventOrder); + const metadata = getRundownMetadata(); + runtimeState.resume(restorePoint, event, rundown, metadata); logger.info(LogOrigin.Playback, 'Resuming playback'); } diff --git a/apps/server/src/services/runtime-service/rundownService.utils.ts b/apps/server/src/services/runtime-service/rundownService.utils.ts index ef4ebfbf3..2999294b8 100644 --- a/apps/server/src/services/runtime-service/rundownService.utils.ts +++ b/apps/server/src/services/runtime-service/rundownService.utils.ts @@ -120,10 +120,3 @@ export function getEventAtIndex( return rundown.entries[eventId] as OntimeEvent | undefined; } - -/** - * TODO(v4): we dont need this function - */ -export function filterTimedEvents(rundown: Rundown, timedEventOrder: EntryId[]): OntimeEvent[] { - return timedEventOrder.map((id) => rundown.entries[id] as OntimeEvent); -} diff --git a/apps/server/src/stores/__tests__/runtimeState.test.ts b/apps/server/src/stores/__tests__/runtimeState.test.ts index 8bf17779d..afdceb598 100644 --- a/apps/server/src/stores/__tests__/runtimeState.test.ts +++ b/apps/server/src/stores/__tests__/runtimeState.test.ts @@ -15,6 +15,7 @@ import { start, stop, } from '../runtimeState.js'; +import { rundownCache } from '../../api-data/rundown/rundown.dao.js'; const mockEvent = { type: 'event', @@ -93,7 +94,8 @@ describe('mutation on runtimeState', () => { vi.runAllTimers(); vi.useRealTimers(); - load(mockEvent, mockRundown, mockRundown.order); + const { metadata, rundown } = rundownCache.get(); + load(mockEvent, rundown, metadata); let newState = getState(); expect(newState.eventNow?.id).toBe(mockEvent.id); expect(newState.timer.playback).toBe(Playback.Armed); @@ -162,16 +164,18 @@ describe('mutation on runtimeState', () => { event1: { ...mockEvent, id: 'event1', timeStart: 0, timeEnd: 1000, duration: 1000, parent: null }, event2: { ...mockEvent, id: 'event2', timeStart: 1000, timeEnd: 1500, duration: 500, parent: null }, }; - const rundown = makeRundown({ entries, order: ['event1', 'event2'] }); + const mockRundown = makeRundown({ entries, order: ['event1', 'event2'] }); // force update vi.useFakeTimers(); - await initRundown(rundown, {}); + await initRundown(mockRundown, {}); vi.runAllTimers(); vi.useRealTimers(); + const { metadata, rundown } = rundownCache.get(); + // 1. Load event - load(entries.event1, rundown, rundown.order); + load(entries.event1, rundown, metadata); let newState = getState(); expect(newState.runtime.actualStart).toBeNull(); expect(newState.runtime.plannedStart).toBe(0); @@ -192,7 +196,7 @@ describe('mutation on runtimeState', () => { expect(newState.runtime.expectedEnd).toBe(entries.event2.timeEnd - newState.runtime.offset); // 3. Next event - load(entries.event2, rundown, rundown.order); + load(entries.event2, rundown, metadata); start(); newState = getState(); @@ -237,22 +241,29 @@ describe('roll mode', () => { vi.setSystemTime('jan 1 00:00'); clearState(); }); + afterEach(() => { vi.useRealTimers(); }); - describe('normal roll', () => { - const rundown = makeRundown({ - entries: { - 1: { ...mockEvent, id: '1', timeStart: 1000, duration: 1000, timeEnd: 2000 }, - 2: { ...mockEvent, id: '2', timeStart: 2000, duration: 1000, timeEnd: 3000 }, - 3: { ...mockEvent, id: '3', timeStart: 3000, duration: 1000, timeEnd: 4000 }, - }, - order: ['1', '2', '3'], + describe('normal roll', async () => { + beforeEach(async () => { + vi.useFakeTimers(); + const mockRundown = makeRundown({ + entries: { + 1: { ...mockEvent, id: '1', timeStart: 1000, duration: 1000, timeEnd: 2000 }, + 2: { ...mockEvent, id: '2', timeStart: 2000, duration: 1000, timeEnd: 3000 }, + 3: { ...mockEvent, id: '3', timeStart: 3000, duration: 1000, timeEnd: 4000 }, + }, + order: ['1', '2', '3'], + }); + await initRundown(mockRundown, {}); + vi.runAllTimers(); }); test('pending event', () => { - const { eventId, didStart } = roll(rundown, rundown.order); + const { rundown, metadata } = rundownCache.get(); + const { eventId, didStart } = roll(rundown, metadata); const state = getState(); expect(eventId).toBe('1'); @@ -263,55 +274,22 @@ describe('roll mode', () => { test('roll events', () => { vi.setSystemTime('jan 1 00:00:01'); - let result = roll(rundown, rundown.order); + const { rundown, metadata } = rundownCache.get(); + let result = roll(rundown, metadata); expect(result).toStrictEqual({ eventId: '1', didStart: true }); vi.setSystemTime('jan 1 00:00:02'); - result = roll(rundown, rundown.order); + result = roll(rundown, metadata); expect(result).toStrictEqual({ eventId: '2', didStart: true }); vi.setSystemTime('jan 1 00:00:03:500'); - result = roll(rundown, rundown.order); + result = roll(rundown, metadata); expect(result).toStrictEqual({ eventId: '3', didStart: true }); }); }); - describe('roll takeover', async () => { - const rundown = makeRundown({ - entries: { - 1: { ...mockEvent, id: '1', timeStart: 1000, duration: 1000, timeEnd: 2000 }, - 2: { ...mockEvent, id: '2', timeStart: 2000, duration: 1000, timeEnd: 3000 }, - 3: { ...mockEvent, id: '3', timeStart: 3000, duration: 1000, timeEnd: 4000 }, - }, - order: ['1', '2', '3'], - }); - - // force update - vi.useFakeTimers(); - await initRundown(rundown, {}); - vi.runAllTimers(); - vi.useRealTimers(); - - test('from load', () => { - load(rundown.entries[3] as PlayableEvent, rundown, rundown.order); - const result = roll(rundown, rundown.order); - expect(result).toStrictEqual({ eventId: '3', didStart: false }); - const state = getState(); - expect(state.timer.phase).toBe(TimerPhase.Pending); - expect(state.timer.secondaryTimer).toBe(3000); - }); - - test('from play', () => { - load(rundown.entries[1] as PlayableEvent, rundown, rundown.order); - start(); - const result = roll(rundown, rundown.order); - expect(result).toStrictEqual({ eventId: '1', didStart: false }); - expect(getState().runtime.offset).toBe(1000); - }); - }); - - describe('roll continue with offset', () => { - test('no gaps', async () => { + describe('roll takeover', () => { + beforeEach(async () => { const rundown = makeRundown({ entries: { 1: { ...mockEvent, id: '1', timeStart: 1000, duration: 1000, timeEnd: 2000 }, @@ -325,23 +303,61 @@ describe('roll mode', () => { vi.useFakeTimers(); await initRundown(rundown, {}); vi.runAllTimers(); + }); - load(rundown.entries[1] as PlayableEvent, rundown, rundown.order); + test('from load', () => { + const { rundown, metadata } = rundownCache.get(); + load(rundown.entries[3] as PlayableEvent, rundown, metadata); + const result = roll(rundown, metadata); + expect(result).toStrictEqual({ eventId: '3', didStart: false }); + const state = getState(); + expect(state.timer.phase).toBe(TimerPhase.Pending); + expect(state.timer.secondaryTimer).toBe(3000); + }); + + test('from play', () => { + const { rundown, metadata } = rundownCache.get(); + load(rundown.entries[1] as PlayableEvent, rundown, metadata); + start(); + const result = roll(rundown, metadata); + expect(result).toStrictEqual({ eventId: '1', didStart: false }); + expect(getState().runtime.offset).toBe(1000); + }); + }); + + describe('roll continue with offset', () => { + test('no gaps', async () => { + const mockRundown = makeRundown({ + entries: { + 1: { ...mockEvent, id: '1', timeStart: 1000, duration: 1000, timeEnd: 2000 }, + 2: { ...mockEvent, id: '2', timeStart: 2000, duration: 1000, timeEnd: 3000 }, + 3: { ...mockEvent, id: '3', timeStart: 3000, duration: 1000, timeEnd: 4000 }, + }, + order: ['1', '2', '3'], + }); + + // force update + vi.useFakeTimers(); + await initRundown(mockRundown, {}); + vi.runAllTimers(); + const { rundown, metadata } = rundownCache.get(); + + load(rundown.entries[1] as PlayableEvent, rundown, metadata); start(); // the current offset after manual play const currentOffset = getState().runtime.offset; - let result = roll(rundown, rundown.order, getState().runtime.offset); + let result = roll(rundown, metadata, getState().runtime.offset); expect(result).toStrictEqual({ eventId: '1', didStart: false }); // the current offset should be maintain by roll mode whn taking over from play expect(getState().runtime.offset).toBe(currentOffset); vi.setSystemTime('jan 1 00:00:01'); - result = roll(rundown, rundown.order, getState().runtime.offset); + result = roll(rundown, metadata, getState().runtime.offset); expect(result).toStrictEqual({ eventId: '2', didStart: true }); expect(getState().runtime.offset).toBe(1000); vi.setSystemTime('jan 1 00:00:02'); - result = roll(rundown, rundown.order, getState().runtime.offset); + result = roll(rundown, metadata, getState().runtime.offset); expect(result).toStrictEqual({ eventId: '3', didStart: true }); expect(getState().runtime.offset).toBe(1000); diff --git a/apps/server/src/stores/runtimeState.ts b/apps/server/src/stores/runtimeState.ts index d01230fbe..5a0a1b92f 100644 --- a/apps/server/src/stores/runtimeState.ts +++ b/apps/server/src/stores/runtimeState.ts @@ -1,12 +1,9 @@ import { CurrentBlockState, - EntryId, - isPlayableEvent, MaybeNumber, MaybeString, OffsetMode, OntimeBlock, - OntimeEvent, PlayableEvent, Playback, Rundown, @@ -28,7 +25,8 @@ import { } from '../services/timerUtils.js'; import { loadRoll, normaliseRollStart } from '../services/rollUtils.js'; import { timerConfig } from '../setup/config.js'; -import { filterTimedEvents } from '../services/runtime-service/rundownService.utils.js'; +import { RundownMetadata } from '../api-data/rundown/rundown.types.js'; +import { getPlayableIndexFromTimedIndex } from '../api-data/rundown/rundown.utils.js'; export type RuntimeState = { clock: number; // realtime clock @@ -171,12 +169,13 @@ export function updateRundownData(rundownData: RundownData) { export function load( event: PlayableEvent, rundown: Rundown, - timedEventOrder: EntryId[], + metadata: RundownMetadata, initialData?: Partial, ): boolean { clearEventData(); - if (timedEventOrder.length === 0 || !isPlayableEvent(event)) { + const { timedEventOrder } = metadata; + if (timedEventOrder.length === 0) { return false; } @@ -186,18 +185,16 @@ export function load( return false; } - // TODO(remove public): it is wasteful to recreate the object - const timedEvents = filterTimedEvents(rundown, timedEventOrder); // load events in memory along with their data - loadNow(timedEvents, eventIndex); - loadNext(timedEvents, eventIndex); + loadNow(rundown, metadata, eventIndex); + loadNext(rundown, metadata, eventIndex); loadBlock(rundown); // update state runtimeState.timer.playback = Playback.Armed; runtimeState.timer.duration = calculateDuration(event.timeStart, event.timeEnd); runtimeState.timer.current = getCurrent(runtimeState); - runtimeState.runtime.numEvents = timedEvents.length; + runtimeState.runtime.numEvents = metadata.timedEventOrder.length; // patch with potential provided data if (initialData) { @@ -220,7 +217,11 @@ export function load( /** * Loads current event and its public counterpart */ -export function loadNow(timedEvents: OntimeEvent[], eventIndex: MaybeNumber = runtimeState.runtime.selectedEventIndex) { +export function loadNow( + rundown: Rundown, + metadata: RundownMetadata, + eventIndex: MaybeNumber = runtimeState.runtime.selectedEventIndex, +) { if (eventIndex === null) { // reset the state to indicate there is no selection runtimeState.runtime.selectedEventIndex = null; @@ -228,7 +229,7 @@ export function loadNow(timedEvents: OntimeEvent[], eventIndex: MaybeNumber = ru return; } - const event = timedEvents[eventIndex] as PlayableEvent; + const event = rundown.entries[metadata.timedEventOrder[eventIndex]] as PlayableEvent; runtimeState.runtime.selectedEventIndex = eventIndex; runtimeState.eventNow = event; } @@ -237,7 +238,8 @@ export function loadNow(timedEvents: OntimeEvent[], eventIndex: MaybeNumber = ru * Loads the next event and its public counterpart */ export function loadNext( - timedEvents: OntimeEvent[], + rundown: Rundown, + metadata: RundownMetadata, eventIndex: MaybeNumber = runtimeState.runtime.selectedEventIndex, ) { if (eventIndex === null) { @@ -245,27 +247,22 @@ export function loadNext( runtimeState.eventNext = null; return; } + const nowPlayableIndex = getPlayableIndexFromTimedIndex(metadata, eventIndex); - // temporarily reset this value to simplify loop logic - runtimeState.eventNext = null; - - //TODO: do we already have a it as a list of not skipped events - for (let i = eventIndex + 1; i < timedEvents.length; i++) { - const event = timedEvents[i]; - // we dont deal with events that are not playable - if (!isPlayableEvent(event)) { - continue; - } - runtimeState.eventNext = event; + if (!nowPlayableIndex || nowPlayableIndex > metadata.playableEventOrder.length - 2) { + // we cound not find the event now or the event now is the last playable event + runtimeState.eventNext = null; return; } + const nextId = metadata.playableEventOrder[nowPlayableIndex + 1]; + runtimeState.eventNext = rundown.entries[nextId] as PlayableEvent; } /** * Resume from restore point */ -export function resume(restorePoint: RestorePoint, event: PlayableEvent, rundown: Rundown, timedEventOrder: EntryId[]) { - load(event, rundown, timedEventOrder, restorePoint); +export function resume(restorePoint: RestorePoint, event: PlayableEvent, rundown: Rundown, metadata: RundownMetadata) { + load(event, rundown, metadata, restorePoint); } /** @@ -325,12 +322,12 @@ export function updateLoaded(event?: PlayableEvent): string | undefined { /** * Used in situations when we want to hot-reload all events without interrupting timer */ -export function updateAll(rundown: Rundown, timedEventsOrder: EntryId[]) { - const timedEvents = filterTimedEvents(rundown, timedEventsOrder); - // TODO(remove public): we dont need to make the timedEvents object, we pass primitives and let the functions handle it - const eventNowIndex = timedEventsOrder.findIndex((id) => id === runtimeState.eventNow?.id); - loadNow(timedEvents, eventNowIndex >= 0 ? eventNowIndex : undefined); - loadNext(timedEvents, eventNowIndex >= 0 ? eventNowIndex : undefined); +export function updateAll(rundown: Rundown, metadata: RundownMetadata) { + // event now might have moved so we find the event now id and recalculate the the index again + const eventNowIndex = metadata.timedEventOrder.findIndex((id) => id === runtimeState.eventNow?.id); + + loadNow(rundown, metadata, eventNowIndex >= 0 ? eventNowIndex : undefined); + loadNext(rundown, metadata, eventNowIndex >= 0 ? eventNowIndex : undefined); updateLoaded(runtimeState.eventNow ?? undefined); loadBlock(rundown); } @@ -542,7 +539,7 @@ export function update(): UpdateResult { export function roll( rundown: Rundown, - timedEventOrder: EntryId[], + metadata: RundownMetadata, offset = 0, ): { eventId: MaybeString; didStart: boolean } { // 1. if an event is running, we simply take over the playback @@ -601,8 +598,7 @@ export function roll( } // 3. if there is no event running, we need to find the next event - const timedEvents = filterTimedEvents(rundown, timedEventOrder); - if (timedEvents.length === 0) { + if (metadata.playableEventOrder.length === 0) { throw new Error('No playable events found'); } @@ -613,16 +609,16 @@ export function roll( runtimeState.runtime.offset = offset; const offsetClock = runtimeState.clock + runtimeState.runtime.offset; - const { index, isPending } = loadRoll(timedEvents, offsetClock); + const { index, isPending } = loadRoll(rundown, metadata, offsetClock); // load events in memory along with their data - loadNow(timedEvents, index); - loadNext(timedEvents, index); + loadNow(rundown, metadata, index); + loadNext(rundown, metadata, index); loadBlock(rundown); // update roll state runtimeState.timer.playback = Playback.Roll; - runtimeState.runtime.numEvents = timedEvents.length; + runtimeState.runtime.numEvents = metadata.timedEventOrder.length; // in roll mode spec, there should always be something to load // as long as playableEvents is not empty