mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-11 18:33:53 +00:00
refactor: UI for linking events (#763)
This commit is contained in:
+12
-6
@@ -12,6 +12,7 @@ import {
|
||||
currentDirectory,
|
||||
environment,
|
||||
isProduction,
|
||||
resolveDbPath,
|
||||
resolveExternalsDirectory,
|
||||
resolveStylesDirectory,
|
||||
resolvedPath,
|
||||
@@ -42,13 +43,14 @@ import { restoreService } from './services/RestoreService.js';
|
||||
import { messageService } from './services/message-service/MessageService.js';
|
||||
import { populateDemo } from './modules/loadDemo.js';
|
||||
import { getState, updateNumEvents } from './stores/runtimeState.js';
|
||||
import { getNumEvents } from './services/rundown-service/RundownService.js';
|
||||
import { getNumEvents, setRundown } from './services/rundown-service/RundownService.js';
|
||||
|
||||
console.log(`Starting Ontime version ${ONTIME_VERSION}`);
|
||||
|
||||
if (!isProduction) {
|
||||
console.log(`Ontime running in ${environment} environment`);
|
||||
console.log(`Ontime directory at ${currentDirectory} `);
|
||||
console.log(`Ontime database at ${resolveDbPath}`);
|
||||
}
|
||||
|
||||
// Create express APP
|
||||
@@ -177,16 +179,20 @@ export const startServer = async () => {
|
||||
},
|
||||
});
|
||||
|
||||
// initialise rundown service
|
||||
const persistedRundown = DataProvider.getRundown();
|
||||
setRundown(persistedRundown);
|
||||
|
||||
// TODO: do this on the init of the runtime service
|
||||
const numEvents = getNumEvents();
|
||||
updateNumEvents(numEvents);
|
||||
|
||||
// load restore point if it exists
|
||||
const maybeRestorePoint = await restoreService.load();
|
||||
|
||||
// TODO: pass event store to rundownservice
|
||||
runtimeService.init(maybeRestorePoint);
|
||||
|
||||
// TODO: do this on the init of the runtime service
|
||||
const numEvents = getNumEvents();
|
||||
updateNumEvents(numEvents);
|
||||
|
||||
// eventStore set is a dependency of the services that publish to it
|
||||
messageService.init(eventStore.set.bind(eventStore));
|
||||
|
||||
@@ -276,7 +282,7 @@ export const shutdown = async (exitCode = 0) => {
|
||||
process.exit(exitCode);
|
||||
};
|
||||
|
||||
process.on('exit', (code) => console.log(`Ontime exited with code: ${code}`));
|
||||
process.on('exit', (code) => console.log(`Ontime shutdown with code: ${code}`));
|
||||
|
||||
process.on('unhandledRejection', async (error) => {
|
||||
logger.error(LogOrigin.Server, `Error: unhandled rejection ${error}`);
|
||||
|
||||
@@ -10,23 +10,23 @@ import {
|
||||
deleteAllEvents,
|
||||
deleteEvent,
|
||||
editEvent,
|
||||
getRundown,
|
||||
reorderEvent,
|
||||
swapEvents,
|
||||
} from '../services/rundown-service/RundownService.js';
|
||||
import { get } from '../services/rundown-service/rundownCache.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { get as getCachedRundown } from '../services/rundown-service/rundownCache.js';
|
||||
|
||||
// Create controller for GET request to '/events'
|
||||
// Returns -
|
||||
export const rundownGetAll: RequestHandler = async (_req, res) => {
|
||||
const rundown = DataProvider.getRundown();
|
||||
const rundown = getRundown();
|
||||
res.json(rundown);
|
||||
};
|
||||
|
||||
// Create controller for GET request to '/events/cached'
|
||||
// Returns -
|
||||
export const rundownGetCached: RequestHandler = async (_req: Request, res: Response<RundownCached>) => {
|
||||
const cachedRundown = get();
|
||||
const cachedRundown = getCachedRundown();
|
||||
res.json(cachedRundown);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export const alias = {
|
||||
enabled: false,
|
||||
alias: '',
|
||||
pathAndParams: '',
|
||||
};
|
||||
@@ -1,4 +1,12 @@
|
||||
import { EndAction, OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent, TimerType } from 'ontime-types';
|
||||
import {
|
||||
EndAction,
|
||||
OntimeBlock,
|
||||
OntimeDelay,
|
||||
OntimeEvent,
|
||||
SupportedEvent,
|
||||
TimeStrategy,
|
||||
TimerType,
|
||||
} from 'ontime-types';
|
||||
|
||||
export const event: Omit<OntimeEvent, 'id' | 'delay' | 'cue'> = {
|
||||
title: '',
|
||||
@@ -7,6 +15,8 @@ export const event: Omit<OntimeEvent, 'id' | 'delay' | 'cue'> = {
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStrategy: TimeStrategy.LockDuration,
|
||||
linkStart: null,
|
||||
timeStart: 0,
|
||||
timeEnd: 0,
|
||||
duration: 0,
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from 'ontime-types';
|
||||
import { getCueCandidate } from 'ontime-utils';
|
||||
|
||||
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||
import { block as blockDef, delay as delayDef } from '../../models/eventsDefinition.js';
|
||||
import { sendRefetch } from '../../adapters/websocketAux.js';
|
||||
import { logger } from '../../classes/Logger.js';
|
||||
@@ -26,7 +25,7 @@ function generateEvent(eventData: Partial<OntimeEvent> | Partial<OntimeDelay> |
|
||||
const id = cache.getUniqueId();
|
||||
|
||||
if (isOntimeEvent(eventData)) {
|
||||
return createEvent(eventData, getCueCandidate(DataProvider.getRundown(), eventData?.after)) as OntimeEvent;
|
||||
return createEvent(eventData, getCueCandidate(cache.getPersistedRundown(), eventData?.after)) as OntimeEvent;
|
||||
}
|
||||
|
||||
if (isOntimeDelay(eventData)) {
|
||||
@@ -188,7 +187,7 @@ export function notifyChanges(options: { timer?: boolean | string[]; external?:
|
||||
* @return {array}
|
||||
*/
|
||||
export function getRundown(): OntimeRundown {
|
||||
return DataProvider.getRundown();
|
||||
return cache.getPersistedRundown();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,7 +195,7 @@ export function getRundown(): OntimeRundown {
|
||||
* @return {array}
|
||||
*/
|
||||
export function getTimedEvents(): OntimeEvent[] {
|
||||
return DataProvider.getRundown().filter((event) => isOntimeEvent(event)) as OntimeEvent[];
|
||||
return getRundown().filter((event) => isOntimeEvent(event)) as OntimeEvent[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,7 +203,7 @@ export function getTimedEvents(): OntimeEvent[] {
|
||||
* @return {array}
|
||||
*/
|
||||
export function getPlayableEvents(): OntimeEvent[] {
|
||||
return DataProvider.getRundown().filter((event) => isOntimeEvent(event) && !event.skip) as OntimeEvent[];
|
||||
return getRundown().filter((event) => isOntimeEvent(event) && !event.skip) as OntimeEvent[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -288,7 +287,6 @@ export function findNext(currentEventId?: string): OntimeEvent | null {
|
||||
}
|
||||
|
||||
export async function setRundown(rundown: OntimeRundown) {
|
||||
await DataProvider.setRundown(rundown);
|
||||
cache.init(rundown);
|
||||
notifyChanges({ timer: true });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,174 @@
|
||||
import { EndAction, OntimeEvent, OntimeRundown, SupportedEvent, TimerType } from 'ontime-types';
|
||||
import {
|
||||
EndAction,
|
||||
OntimeBlock,
|
||||
OntimeDelay,
|
||||
OntimeEvent,
|
||||
OntimeRundown,
|
||||
SupportedEvent,
|
||||
TimeStrategy,
|
||||
TimerType,
|
||||
} from 'ontime-types';
|
||||
|
||||
import { calculateRuntimeDelays, getDelayAt, calculateRuntimeDelaysFrom } from '../delayUtils.js';
|
||||
import { add, batchEdit, edit, remove, reorder, swap } from '../rundownCache.js';
|
||||
import { add, batchEdit, edit, generate, remove, reorder, swap } from '../rundownCache.js';
|
||||
|
||||
describe('init() function', () => {
|
||||
it('creates normalised versions of a given rundown', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ type: SupportedEvent.Event, id: '1' } as OntimeEvent,
|
||||
{ type: SupportedEvent.Block, id: '2' } as OntimeBlock,
|
||||
{ type: SupportedEvent.Delay, id: '3' } as OntimeDelay,
|
||||
];
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
expect(initResult.order.length).toBe(3);
|
||||
expect(initResult.order).toStrictEqual(['1', '2', '3']);
|
||||
expect(initResult.rundown['1'].type).toBe(SupportedEvent.Event);
|
||||
expect(initResult.rundown['2'].type).toBe(SupportedEvent.Block);
|
||||
expect(initResult.rundown['3'].type).toBe(SupportedEvent.Delay);
|
||||
});
|
||||
|
||||
it('calculates delays versions of a given rundown', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ type: SupportedEvent.Delay, id: '1', duration: 100 } as OntimeDelay,
|
||||
{ type: SupportedEvent.Event, id: '2', timeStart: 1 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Block, id: '3' } as OntimeBlock,
|
||||
{ type: SupportedEvent.Event, id: '4', timeStart: 2 } as OntimeEvent,
|
||||
];
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
expect(initResult.order.length).toBe(4);
|
||||
expect((initResult.rundown['2'] as OntimeEvent).delay).toBe(100);
|
||||
expect((initResult.rundown['4'] as OntimeEvent).delay).toBe(0);
|
||||
});
|
||||
|
||||
it('links times across events', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '1',
|
||||
timeStart: 1,
|
||||
duration: 1,
|
||||
timeEnd: 2,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
} as OntimeEvent,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '2',
|
||||
timeStart: 11,
|
||||
duration: 1,
|
||||
timeEnd: 12,
|
||||
linkStart: '1',
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
} as OntimeEvent,
|
||||
{ type: SupportedEvent.Block, id: 'block' } as OntimeBlock,
|
||||
{ type: SupportedEvent.Delay, id: 'delay' } as OntimeDelay,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '3',
|
||||
timeStart: 21,
|
||||
duration: 1,
|
||||
timeEnd: 22,
|
||||
linkStart: '2',
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
} as OntimeEvent,
|
||||
];
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
expect(initResult.order.length).toBe(5);
|
||||
expect((initResult.rundown['2'] as OntimeEvent).timeStart).toBe(2);
|
||||
expect((initResult.rundown['2'] as OntimeEvent).timeEnd).toBe(12);
|
||||
expect((initResult.rundown['2'] as OntimeEvent).duration).toBe(10);
|
||||
|
||||
expect((initResult.rundown['3'] as OntimeEvent).timeStart).toBe(12);
|
||||
expect((initResult.rundown['3'] as OntimeEvent).timeEnd).toBe(22);
|
||||
expect((initResult.rundown['3'] as OntimeEvent).duration).toBe(10);
|
||||
|
||||
expect(initResult.links['1']).toBe('2');
|
||||
expect(initResult.links['2']).toBe('3');
|
||||
});
|
||||
|
||||
it('links times across events, reordered', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ type: SupportedEvent.Event, id: '1', timeStart: 1, timeEnd: 2 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '3', timeStart: 21, timeEnd: 22, linkStart: '2' } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '2', timeStart: 11, timeEnd: 12, linkStart: '1' } as OntimeEvent,
|
||||
];
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
expect(initResult.order.length).toBe(3);
|
||||
expect((initResult.rundown['3'] as OntimeEvent).timeStart).toBe(2);
|
||||
expect(initResult.links['1']).toBe('3');
|
||||
expect(initResult.links['3']).toBe('2');
|
||||
});
|
||||
|
||||
it('handles updating event sequence', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '97cc3e',
|
||||
timeStart: 0,
|
||||
timeEnd: 600000,
|
||||
duration: 600000,
|
||||
timeStrategy: TimeStrategy.LockDuration,
|
||||
linkStart: null,
|
||||
} as OntimeEvent,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: 'e01948',
|
||||
timeStart: 600000,
|
||||
timeEnd: 601000,
|
||||
duration: 85801000, // <------------- value out of sync
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: '97cc3e',
|
||||
} as OntimeEvent,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '25c1af',
|
||||
timeStart: 100, // <------------- value out of sync
|
||||
timeEnd: 602000,
|
||||
duration: 0,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: 'e01948',
|
||||
} as OntimeEvent,
|
||||
];
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
expect(initResult.rundown).toMatchObject({
|
||||
'97cc3e': {
|
||||
timeStart: 0,
|
||||
timeEnd: 600000,
|
||||
duration: 600000,
|
||||
timeStrategy: 'lock-duration',
|
||||
linkStart: null,
|
||||
},
|
||||
e01948: {
|
||||
timeStart: 600000,
|
||||
timeEnd: 601000,
|
||||
duration: 1000,
|
||||
timeStrategy: 'lock-end',
|
||||
linkStart: '97cc3e',
|
||||
},
|
||||
'25c1af': {
|
||||
timeStart: 601000,
|
||||
timeEnd: 602000,
|
||||
duration: 1000,
|
||||
timeStrategy: 'lock-end',
|
||||
linkStart: 'e01948',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes links if invalid', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ type: SupportedEvent.Event, id: '1', timeStart: 1, linkStart: '10' } as OntimeEvent,
|
||||
];
|
||||
const initResult = generate(testRundown);
|
||||
expect(initResult.order.length).toBe(1);
|
||||
expect((initResult.rundown['1'] as OntimeEvent).timeStart).toBe(1);
|
||||
expect(Object.keys(initResult.links).length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('add() mutation', () => {
|
||||
test('adds an event to the rundown', () => {
|
||||
@@ -137,6 +304,8 @@ describe('calculateRuntimeDelays', () => {
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
@@ -172,6 +341,8 @@ describe('calculateRuntimeDelays', () => {
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 1200000,
|
||||
timeEnd: 1200000,
|
||||
duration: 0,
|
||||
@@ -207,6 +378,8 @@ describe('calculateRuntimeDelays', () => {
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
@@ -242,6 +415,8 @@ describe('calculateRuntimeDelays', () => {
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 1200000,
|
||||
timeEnd: 1800000,
|
||||
duration: 600000,
|
||||
@@ -286,6 +461,8 @@ describe('getDelayAt()', () => {
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
@@ -322,6 +499,8 @@ describe('getDelayAt()', () => {
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 1200000,
|
||||
timeEnd: 1200000,
|
||||
duration: 0,
|
||||
@@ -358,6 +537,8 @@ describe('getDelayAt()', () => {
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
@@ -394,6 +575,8 @@ describe('getDelayAt()', () => {
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 1200000,
|
||||
timeEnd: 1800000,
|
||||
duration: 600000,
|
||||
@@ -456,6 +639,8 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
@@ -492,6 +677,8 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 1200000,
|
||||
timeEnd: 1200000,
|
||||
duration: 0,
|
||||
@@ -528,6 +715,8 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
@@ -564,6 +753,8 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 1200000,
|
||||
timeEnd: 1800000,
|
||||
duration: 600000,
|
||||
|
||||
@@ -6,80 +6,123 @@ import {
|
||||
OntimeRundown,
|
||||
OntimeRundownEntry,
|
||||
} from 'ontime-types';
|
||||
import { generateId, deleteAtIndex, insertAtIndex, reorderArray, swapEventData } from 'ontime-utils';
|
||||
import {
|
||||
generateId,
|
||||
deleteAtIndex,
|
||||
insertAtIndex,
|
||||
reorderArray,
|
||||
swapEventData,
|
||||
getLinkedTimes,
|
||||
formatFromMillis,
|
||||
} from 'ontime-utils';
|
||||
|
||||
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||
import { createPatch } from '../../utils/parser.js';
|
||||
import { apply } from './delayUtils.js';
|
||||
|
||||
type NormalisedRundown = Record<string, OntimeRundownEntry>;
|
||||
type EventID = string;
|
||||
type NormalisedRundown = Record<EventID, OntimeRundownEntry>;
|
||||
|
||||
let persistedRundown: OntimeRundown = [];
|
||||
/** Utility function gets rundown from DataProvider */
|
||||
export const getPersistedRundown = (): OntimeRundown => persistedRundown;
|
||||
|
||||
let rundown: NormalisedRundown = {};
|
||||
let order: string[] = [];
|
||||
let order: EventID[] = [];
|
||||
let revision = 0;
|
||||
let isStale = true;
|
||||
|
||||
/**
|
||||
* Utility initialises cache
|
||||
* @param persistedRundown
|
||||
*/
|
||||
export function init(persistedRundown: Readonly<OntimeRundown>) {
|
||||
// we decided to try and re-write this dataset for every change
|
||||
// instead of maintaining logic to update it
|
||||
rundown = {};
|
||||
order = [];
|
||||
let links: Record<EventID, EventID> = {};
|
||||
|
||||
let accumulatedDelay = 0;
|
||||
for (let i = 0; i < persistedRundown.length; i++) {
|
||||
const event = persistedRundown[i];
|
||||
|
||||
// calculate delays
|
||||
if (isOntimeDelay(event)) {
|
||||
accumulatedDelay += event.duration;
|
||||
} else if (isOntimeBlock(event)) {
|
||||
accumulatedDelay = 0;
|
||||
} else if (isOntimeEvent(event)) {
|
||||
event.delay = accumulatedDelay;
|
||||
}
|
||||
|
||||
order.push(event.id);
|
||||
rundown[event.id] = { ...event };
|
||||
}
|
||||
isStale = false;
|
||||
export async function init(initialRundown: OntimeRundown) {
|
||||
persistedRundown = structuredClone(initialRundown);
|
||||
generate();
|
||||
await DataProvider.setRundown(persistedRundown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ID guaranteed to be unique
|
||||
* @returns
|
||||
* Utility initialises cache
|
||||
* @param rundown
|
||||
*/
|
||||
export function getUniqueId(persistedRundown: Readonly<OntimeRundown> = getPersistedRundown()): string {
|
||||
export function generate(initialRundown: OntimeRundown = persistedRundown) {
|
||||
// we decided to re-write this dataset for every change
|
||||
// instead of maintaining logic to update it
|
||||
|
||||
function getLink(currentIndex: number): OntimeEvent | null {
|
||||
// currently the link is the previous event
|
||||
for (let i = currentIndex - 1; i >= 0; i--) {
|
||||
const event = initialRundown[i];
|
||||
if (isOntimeEvent(event)) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
rundown = {};
|
||||
order = [];
|
||||
links = {};
|
||||
|
||||
let accumulatedDelay = 0;
|
||||
for (let i = 0; i < initialRundown.length; i++) {
|
||||
const currentEvent = initialRundown[i];
|
||||
let updatedEvent = { ...currentEvent };
|
||||
|
||||
// handle links
|
||||
if (isOntimeEvent(updatedEvent)) {
|
||||
if (updatedEvent.linkStart) {
|
||||
const linkedEvent = getLink(i);
|
||||
// link is always the previous event for now
|
||||
if (linkedEvent) {
|
||||
links[linkedEvent.id] = currentEvent.id;
|
||||
|
||||
const timePatch = getLinkedTimes(updatedEvent, linkedEvent);
|
||||
updatedEvent = { ...updatedEvent, ...timePatch };
|
||||
} else {
|
||||
updatedEvent.linkStart = null;
|
||||
}
|
||||
// update the persisted event
|
||||
initialRundown[i] = updatedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
// calculate delays
|
||||
if (isOntimeDelay(updatedEvent)) {
|
||||
accumulatedDelay += updatedEvent.duration;
|
||||
} else if (isOntimeBlock(updatedEvent)) {
|
||||
accumulatedDelay = 0;
|
||||
} else if (isOntimeEvent(updatedEvent)) {
|
||||
updatedEvent.delay = accumulatedDelay;
|
||||
}
|
||||
|
||||
order.push(updatedEvent.id);
|
||||
rundown[updatedEvent.id] = { ...updatedEvent };
|
||||
}
|
||||
|
||||
isStale = false;
|
||||
return { rundown, order, links };
|
||||
}
|
||||
|
||||
/** Returns an ID guaranteed to be unique */
|
||||
export function getUniqueId(): string {
|
||||
if (isStale) {
|
||||
generate();
|
||||
}
|
||||
let id = '';
|
||||
do {
|
||||
id = generateId();
|
||||
} while (!isIdUnique(persistedRundown, id));
|
||||
} while (Object.hasOwn(rundown, id));
|
||||
return id;
|
||||
}
|
||||
|
||||
export function isIdUnique(persistedRundown: Readonly<OntimeRundown>, eventId: string) {
|
||||
if (isStale) {
|
||||
init(persistedRundown);
|
||||
}
|
||||
return !Object.hasOwn(rundown, eventId);
|
||||
}
|
||||
|
||||
/** Returns index of an event with a given id */
|
||||
export function getIndexOf(eventId: string) {
|
||||
if (isStale) {
|
||||
init(getPersistedRundown());
|
||||
generate();
|
||||
}
|
||||
return order.indexOf(eventId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function gets rundown from DataProvider
|
||||
* @returns {OntimeRundown}
|
||||
*/
|
||||
export const getPersistedRundown = (): OntimeRundown => DataProvider.getRundown();
|
||||
|
||||
type RundownCache = {
|
||||
rundown: NormalisedRundown;
|
||||
order: string[];
|
||||
@@ -93,7 +136,7 @@ type RundownCache = {
|
||||
export function get(): Readonly<RundownCache> {
|
||||
if (isStale) {
|
||||
console.time('rundownCache__init');
|
||||
init(getPersistedRundown());
|
||||
generate();
|
||||
console.timeEnd('rundownCache__init');
|
||||
}
|
||||
return {
|
||||
@@ -117,21 +160,25 @@ type MutatingFn<T extends object> = (params: MutationParams<T>) => MutatingRetur
|
||||
*/
|
||||
export function mutateCache<T extends object>(mutation: MutatingFn<T>) {
|
||||
async function scopedMutation(params: T) {
|
||||
const persistedRundown = getPersistedRundown();
|
||||
const { newEvent, newRundown } = mutation({ ...params, persistedRundown });
|
||||
|
||||
revision = revision + 1;
|
||||
isStale = true;
|
||||
persistedRundown = newRundown;
|
||||
|
||||
DataProvider.setRundown(newRundown);
|
||||
// schedule the update to the next tick
|
||||
|
||||
process.nextTick(() => {
|
||||
// schedule a non priority cache update
|
||||
setImmediate(() => {
|
||||
console.time('rundownCache__init');
|
||||
init(newRundown);
|
||||
generate();
|
||||
console.timeEnd('rundownCache__init');
|
||||
});
|
||||
|
||||
// TODO: should we trottle this?
|
||||
// defer writing to the database
|
||||
setImmediate(() => {
|
||||
DataProvider.setRundown(persistedRundown);
|
||||
});
|
||||
|
||||
// TODO: could we return a patch object?
|
||||
return { newEvent };
|
||||
}
|
||||
@@ -186,8 +233,12 @@ export function edit({ persistedRundown, eventId, patch }: EditArgs): Required<M
|
||||
throw new Error('Invalid event type');
|
||||
}
|
||||
|
||||
// @ts-expect-error -- testing
|
||||
console.log('patch', formatFromMillis(patch?.timeStart ?? 0, 'HH:mm:ss'));
|
||||
|
||||
const eventInMemory = persistedRundown[indexAt];
|
||||
const newEvent = makeEvent(eventInMemory, patch);
|
||||
|
||||
const newRundown = [...persistedRundown];
|
||||
newRundown[indexAt] = newEvent;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ensureDirectory } from './utils/fileManagement.js';
|
||||
|
||||
/**
|
||||
* @description Returns public path depending on OS
|
||||
* This is the correct path for the app running in production mode
|
||||
*/
|
||||
export function getAppDataPath(): string {
|
||||
// handle docker
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ProjectData,
|
||||
Settings,
|
||||
SupportedEvent,
|
||||
TimeStrategy,
|
||||
TimerType,
|
||||
ViewSettings,
|
||||
} from 'ontime-types';
|
||||
@@ -33,6 +34,8 @@ describe('test json parser with valid def', () => {
|
||||
timeStart: 31500000,
|
||||
timeEnd: 32400000,
|
||||
duration: 32400000 - 31500000,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
isPublic: false,
|
||||
skip: false,
|
||||
colour: '',
|
||||
@@ -63,6 +66,8 @@ describe('test json parser with valid def', () => {
|
||||
timeStart: 32400000,
|
||||
timeEnd: 36000000,
|
||||
duration: 36000000 - 32400000,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
isPublic: true,
|
||||
skip: true,
|
||||
colour: 'red',
|
||||
@@ -93,6 +98,8 @@ describe('test json parser with valid def', () => {
|
||||
timeStart: 32400000,
|
||||
timeEnd: 37200000,
|
||||
duration: 37200000 - 32400000,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
isPublic: false,
|
||||
skip: false,
|
||||
colour: '',
|
||||
@@ -144,6 +151,8 @@ describe('test json parser with valid def', () => {
|
||||
timeStart: 39600000,
|
||||
timeEnd: 45000000,
|
||||
duration: 37200000 - 32400000,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
isPublic: false,
|
||||
skip: false,
|
||||
colour: '',
|
||||
@@ -174,6 +183,8 @@ describe('test json parser with valid def', () => {
|
||||
timeStart: 46800000,
|
||||
timeEnd: 50400000,
|
||||
duration: 37200000 - 32400000,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
isPublic: true,
|
||||
skip: true,
|
||||
colour: '',
|
||||
@@ -566,10 +577,12 @@ describe('test event validator', () => {
|
||||
};
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const validated = createEvent(event);
|
||||
expect(typeof validated.timeStart).toEqual('number');
|
||||
assertType<number>(validated.timeStart);
|
||||
assertType<number>(validated.timeEnd);
|
||||
assertType<number>(validated.duration);
|
||||
expect(validated.timeStart).toEqual(0);
|
||||
expect(typeof validated.timeEnd).toEqual('number');
|
||||
expect(validated.timeEnd).toEqual(2);
|
||||
expect(validated.duration).toEqual(2);
|
||||
});
|
||||
|
||||
it('handles bad objects', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EndAction, OntimeRundownEntry, SupportedEvent, TimerType } from 'ontime-types';
|
||||
import { EndAction, OntimeRundownEntry, SupportedEvent, TimeStrategy, TimerType } from 'ontime-types';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
|
||||
import { getA1Notation, cellRequestFromEvent } from '../sheetUtils.js';
|
||||
@@ -29,6 +29,8 @@ describe('cellRequestFromEvent()', () => {
|
||||
note: 'Blue button on the right',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
duration: 10800000,
|
||||
@@ -97,6 +99,8 @@ describe('cellRequestFromEvent()', () => {
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
duration: 10800000,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
isPublic: false,
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
@@ -163,6 +167,8 @@ describe('cellRequestFromEvent()', () => {
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
duration: 10800000,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
@@ -228,6 +234,8 @@ describe('cellRequestFromEvent()', () => {
|
||||
timeEnd: 57600000,
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
duration: 10800000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
@@ -272,6 +280,8 @@ describe('cellRequestFromEvent()', () => {
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
duration: 10800000,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
@@ -315,6 +325,8 @@ describe('cellRequestFromEvent()', () => {
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
duration: 10800000,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
validateTimerType,
|
||||
type ExcelImportOptions,
|
||||
validateTimes,
|
||||
isKnownTimerType,
|
||||
validateLinkStart,
|
||||
} from 'ontime-utils';
|
||||
import {
|
||||
DatabaseModel,
|
||||
@@ -16,6 +18,7 @@ import {
|
||||
UserFields,
|
||||
EndAction,
|
||||
TimerType,
|
||||
TimeStrategy,
|
||||
} from 'ontime-types';
|
||||
|
||||
import fs from 'fs';
|
||||
@@ -38,7 +41,6 @@ import {
|
||||
import { parseExcelDate } from './time.js';
|
||||
import { configService } from '../services/ConfigService.js';
|
||||
import { coerceBoolean } from './coerceType.js';
|
||||
import { isKnownTimerType } from '../../../../packages/utils/src/validate-events/validateEvent.js';
|
||||
|
||||
export const EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
export const JSON_MIME = 'application/json';
|
||||
@@ -335,16 +337,36 @@ export const parseJson = async (jsonData: Partial<DatabaseModel>): Promise<Datab
|
||||
return returnData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Function infers strategy for a patch with only partial timer data
|
||||
* @param end
|
||||
* @param duration
|
||||
* @param fallback
|
||||
* @returns
|
||||
*/
|
||||
function inferStrategy(end: unknown, duration: unknown, fallback: TimeStrategy): TimeStrategy {
|
||||
if (end && !duration) {
|
||||
return TimeStrategy.LockEnd;
|
||||
}
|
||||
|
||||
if (!end && duration) {
|
||||
return TimeStrategy.LockDuration;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<OntimeEvent>): OntimeEvent {
|
||||
if (Object.keys(patchEvent).length === 0) {
|
||||
return originalEvent;
|
||||
}
|
||||
|
||||
const { timeStart, timeEnd, duration } = validateTimes(
|
||||
const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(
|
||||
patchEvent?.timeStart ?? originalEvent.timeStart,
|
||||
patchEvent?.timeEnd ?? originalEvent.timeEnd,
|
||||
patchEvent?.duration ?? originalEvent.duration,
|
||||
patchEvent?.timeStrategy ?? inferStrategy(patchEvent?.timeEnd, patchEvent?.duration, originalEvent.timeStrategy),
|
||||
);
|
||||
const maybeLinkStart = patchEvent.linkStart !== undefined ? patchEvent.linkStart : originalEvent.linkStart;
|
||||
|
||||
return {
|
||||
id: originalEvent.id,
|
||||
@@ -355,6 +377,8 @@ export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<Onti
|
||||
timeStart,
|
||||
timeEnd,
|
||||
duration,
|
||||
timeStrategy,
|
||||
linkStart: validateLinkStart(maybeLinkStart),
|
||||
endAction: validateEndAction(patchEvent.endAction, EndAction.None),
|
||||
timerType: validateTimerType(patchEvent.timerType, TimerType.CountDown),
|
||||
isPublic: typeof patchEvent.isPublic === 'boolean' ? patchEvent.isPublic : originalEvent.isPublic,
|
||||
@@ -374,8 +398,8 @@ export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<Onti
|
||||
// short circuit empty string
|
||||
cue: makeString(patchEvent.cue ?? null, originalEvent.cue),
|
||||
revision: originalEvent.revision,
|
||||
timeWarning: patchEvent.timeWarning,
|
||||
timeDanger: patchEvent.timeDanger,
|
||||
timeWarning: patchEvent.timeWarning ?? originalEvent.timeWarning,
|
||||
timeDanger: patchEvent.timeDanger ?? originalEvent.timeDanger,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ import { join } from 'path';
|
||||
import { URL } from 'url';
|
||||
|
||||
import { logger } from '../classes/Logger.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { getAppDataPath } from '../setup.js';
|
||||
import { ensureDirectory } from './fileManagement.js';
|
||||
import { cellRequestFromEvent, getA1Notation } from './sheetUtils.js';
|
||||
import { parseExcel } from './parser.js';
|
||||
import { parseRundown, parseUserFields } from './parserFunctions.js';
|
||||
import { getRundown } from '../services/rundown-service/RundownService.js';
|
||||
|
||||
type ResponseOK = {
|
||||
data: Partial<DatabaseModel>;
|
||||
@@ -281,7 +281,7 @@ class Sheet {
|
||||
});
|
||||
if (readResponse.status === 200) {
|
||||
const { rundownMetadata } = parseExcel(readResponse.data.values, options);
|
||||
const rundown = DataProvider.getRundown();
|
||||
const rundown = getRundown();
|
||||
const titleRow = Object.values(rundownMetadata)[0]['row'];
|
||||
|
||||
const updateRundown = Array<sheets_v4.Schema$Request>();
|
||||
|
||||
Reference in New Issue
Block a user