mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 06:59:09 +00:00
refactor: improve project loading
This commit is contained in:
@@ -2,7 +2,7 @@ import { GetInfo } from 'ontime-types';
|
|||||||
|
|
||||||
export const ontimePlaceholderInfo: GetInfo = {
|
export const ontimePlaceholderInfo: GetInfo = {
|
||||||
networkInterfaces: [],
|
networkInterfaces: [],
|
||||||
version: '2.0.0',
|
version: '4.0.0',
|
||||||
serverPort: 4001,
|
serverPort: 4001,
|
||||||
publicDir: '',
|
publicDir: '',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Settings } from 'ontime-types';
|
import { Settings } from 'ontime-types';
|
||||||
|
|
||||||
export const ontimePlaceholderSettings: Settings = {
|
export const ontimePlaceholderSettings: Settings = {
|
||||||
app: 'ontime',
|
version: '4.0.0',
|
||||||
version: '2.0.0',
|
|
||||||
serverPort: 4001,
|
serverPort: 4001,
|
||||||
editorKey: null,
|
editorKey: null,
|
||||||
operatorKey: null,
|
operatorKey: null,
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ async function startBackend() {
|
|||||||
const ontimeServer = require(nodePath);
|
const ontimeServer = require(nodePath);
|
||||||
const { initAssets, startServer, startIntegrations } = ontimeServer;
|
const { initAssets, startServer, startIntegrations } = ontimeServer;
|
||||||
|
|
||||||
await initAssets();
|
await initAssets(escalateError);
|
||||||
|
|
||||||
const result = await startServer(escalateError);
|
const result = await startServer();
|
||||||
loaded = result.message;
|
loaded = result.message;
|
||||||
|
|
||||||
await startIntegrations();
|
await startIntegrations();
|
||||||
|
|||||||
@@ -140,8 +140,11 @@ const checkStart = (currentState: OntimeStartOrder) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initAssets = async () => {
|
export const initAssets = async (escalateErrorFn?: (error: string, unrecoverable: boolean) => void) => {
|
||||||
checkStart(OntimeStartOrder.InitAssets);
|
checkStart(OntimeStartOrder.InitAssets);
|
||||||
|
// initialise logging service, escalateErrorFn only exists in electron
|
||||||
|
logger.init(escalateErrorFn);
|
||||||
|
|
||||||
await clearUploadfolder();
|
await clearUploadfolder();
|
||||||
populateStyles();
|
populateStyles();
|
||||||
await populateDemo();
|
await populateDemo();
|
||||||
@@ -152,12 +155,8 @@ export const initAssets = async () => {
|
|||||||
/**
|
/**
|
||||||
* Starts servers
|
* Starts servers
|
||||||
*/
|
*/
|
||||||
export const startServer = async (
|
export const startServer = async (): Promise<{ message: string; serverPort: number }> => {
|
||||||
escalateErrorFn?: (error: string, unrecoverable: boolean) => void,
|
|
||||||
): Promise<{ message: string; serverPort: number }> => {
|
|
||||||
checkStart(OntimeStartOrder.InitServer);
|
checkStart(OntimeStartOrder.InitServer);
|
||||||
// initialise logging service, escalateErrorFn only exists in electron
|
|
||||||
logger.init(escalateErrorFn);
|
|
||||||
const settings = getDataProvider().getSettings();
|
const settings = getDataProvider().getSettings();
|
||||||
const { serverPort: desiredPort } = settings;
|
const { serverPort: desiredPort } = settings;
|
||||||
|
|
||||||
@@ -208,7 +207,6 @@ export const startServer = async (
|
|||||||
// load restore point if it exists
|
// load restore point if it exists
|
||||||
const maybeRestorePoint = await restoreService.load();
|
const maybeRestorePoint = await restoreService.load();
|
||||||
|
|
||||||
// TODO: pass event store to rundownservice
|
|
||||||
runtimeService.init(maybeRestorePoint);
|
runtimeService.init(maybeRestorePoint);
|
||||||
|
|
||||||
const nif = getNetworkInterfaces();
|
const nif = getNetworkInterfaces();
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ type ReadonlyPromise<T> = Promise<Readonly<T>>;
|
|||||||
|
|
||||||
let db = {} as Low<DatabaseModel>;
|
let db = {} as Low<DatabaseModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialises the JSON adapter to persist data to a file
|
||||||
|
*/
|
||||||
export async function initPersistence(filePath: string, fallbackData: DatabaseModel) {
|
export async function initPersistence(filePath: string, fallbackData: DatabaseModel) {
|
||||||
// eslint-disable-next-line no-unused-labels -- dev code path
|
// eslint-disable-next-line no-unused-labels -- dev code path
|
||||||
DEV: shouldCrashDev(!isPath(filePath), 'initPersistence should be called with a path');
|
DEV: shouldCrashDev(!isPath(filePath), 'initPersistence should be called with a path');
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ describe('safeMerge', () => {
|
|||||||
} as Settings,
|
} as Settings,
|
||||||
});
|
});
|
||||||
expect(mergedData.settings).toStrictEqual({
|
expect(mergedData.settings).toStrictEqual({
|
||||||
app: 'ontime',
|
|
||||||
version: 'new',
|
version: 'new',
|
||||||
serverPort: 3000,
|
serverPort: 3000,
|
||||||
operatorKey: null,
|
operatorKey: null,
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export const dbModel: DatabaseModel = {
|
|||||||
projectLogo: null,
|
projectLogo: null,
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
app: 'ontime',
|
|
||||||
version: ONTIME_VERSION,
|
version: ONTIME_VERSION,
|
||||||
serverPort: 4001,
|
serverPort: 4001,
|
||||||
editorKey: null,
|
editorKey: null,
|
||||||
|
|||||||
@@ -495,7 +495,6 @@ export const demoDb: DatabaseModel = {
|
|||||||
projectLogo: null,
|
projectLogo: null,
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
app: 'ontime',
|
|
||||||
version: '-',
|
version: '-',
|
||||||
serverPort: 4001,
|
serverPort: 4001,
|
||||||
editorKey: null,
|
editorKey: null,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
setLastLoadedProject,
|
setLastLoadedProject,
|
||||||
} from '../app-state-service/AppStateService.js';
|
} from '../app-state-service/AppStateService.js';
|
||||||
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
||||||
|
import { getFirstRundown } from '../rundown-service/rundownUtils.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
copyCorruptFile,
|
copyCorruptFile,
|
||||||
@@ -40,7 +41,6 @@ import {
|
|||||||
moveCorruptFile,
|
moveCorruptFile,
|
||||||
parseJsonFile,
|
parseJsonFile,
|
||||||
} from './projectServiceUtils.js';
|
} from './projectServiceUtils.js';
|
||||||
import { getFirstRundown } from '../rundown-service/rundownUtils.js';
|
|
||||||
|
|
||||||
type ProjectState =
|
type ProjectState =
|
||||||
| {
|
| {
|
||||||
@@ -70,26 +70,50 @@ function init() {
|
|||||||
|
|
||||||
export async function getCurrentProject(): Promise<{ filename: string; pathToFile: string }> {
|
export async function getCurrentProject(): Promise<{ filename: string; pathToFile: string }> {
|
||||||
if (currentProjectState.status === 'PENDING') {
|
if (currentProjectState.status === 'PENDING') {
|
||||||
const lastLoadedProject = await initialiseProject();
|
await initialiseProject();
|
||||||
currentProjectState = {
|
|
||||||
status: 'INITIALIZED',
|
|
||||||
currentProjectName: lastLoadedProject,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
const pathToFile = getPathToProject(currentProjectState.currentProjectName);
|
// we know the project is loaded since we force initialisation above
|
||||||
|
const pathToFile = getPathToProject(currentProjectState.currentProjectName as string);
|
||||||
|
|
||||||
return { filename: currentProjectState.currentProjectName, pathToFile };
|
return { filename: currentProjectState.currentProjectName as string, pathToFile };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private function loads a project file and handles necessary side effects
|
||||||
|
*/
|
||||||
|
async function loadProject(projectData: DatabaseModel, projectName: string) {
|
||||||
|
// we need to make sure the file name is unique in the projects directory
|
||||||
|
const pathToNewFile = generateUniqueFileName(publicDir.projectsDir, projectName);
|
||||||
|
|
||||||
|
// change LowDB to point to new file
|
||||||
|
await initPersistence(getPathToProject(pathToNewFile), projectData);
|
||||||
|
logger.info(LogOrigin.Server, `Loaded project ${projectName}`);
|
||||||
|
|
||||||
|
// stop the runtime service
|
||||||
|
runtimeService.stop();
|
||||||
|
|
||||||
|
// load the first rundown in the project
|
||||||
|
const firstRundown = getFirstRundown(projectData.rundowns);
|
||||||
|
await initRundown(firstRundown, projectData.customFields);
|
||||||
|
|
||||||
|
// persist the project selection
|
||||||
|
const newName = getFileNameFromPath(pathToNewFile);
|
||||||
|
await setLastLoadedProject(newName);
|
||||||
|
|
||||||
|
// update the service state
|
||||||
|
currentProjectState = {
|
||||||
|
status: 'INITIALIZED',
|
||||||
|
currentProjectName: newName,
|
||||||
|
};
|
||||||
|
|
||||||
|
return newName;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the demo project
|
* Loads the demo project
|
||||||
*/
|
*/
|
||||||
export async function loadDemoProject(): Promise<string> {
|
export async function loadDemoProject(): Promise<string> {
|
||||||
const pathToNewFile = generateUniqueFileName(publicDir.projectsDir, config.demoProject);
|
return loadProject(demoDb, config.demoProject);
|
||||||
await initPersistence(getPathToProject(pathToNewFile), demoDb);
|
|
||||||
const newName = getFileNameFromPath(pathToNewFile);
|
|
||||||
await setLastLoadedProject(newName);
|
|
||||||
return newName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,11 +121,7 @@ export async function loadDemoProject(): Promise<string> {
|
|||||||
* to be composed in the loading functions
|
* to be composed in the loading functions
|
||||||
*/
|
*/
|
||||||
async function loadNewProject(): Promise<string> {
|
async function loadNewProject(): Promise<string> {
|
||||||
const pathToNewFile = generateUniqueFileName(publicDir.projectsDir, config.newProject);
|
return loadProject(dbModel, config.newProject);
|
||||||
await initPersistence(getPathToProject(pathToNewFile), dbModel);
|
|
||||||
const newName = getFileNameFromPath(pathToNewFile);
|
|
||||||
await setLastLoadedProject(newName);
|
|
||||||
return newName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -129,46 +149,32 @@ export async function initialiseProject(): Promise<string> {
|
|||||||
// check what was loaded before
|
// check what was loaded before
|
||||||
const previousProject = await getLastLoadedProject();
|
const previousProject = await getLastLoadedProject();
|
||||||
|
|
||||||
|
// in normal circumstances we dont have a previous project if it is the first app start
|
||||||
|
// in which case we want to load a demo project
|
||||||
if (!previousProject) {
|
if (!previousProject) {
|
||||||
return loadDemoProject();
|
return loadDemoProject();
|
||||||
}
|
}
|
||||||
|
|
||||||
// try and load the previous project
|
|
||||||
const filePath = doesProjectExist(previousProject);
|
|
||||||
if (filePath === null) {
|
|
||||||
logger.warning(LogOrigin.Server, `Previous project file ${previousProject} not found`);
|
|
||||||
return loadNewProject();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fileData = await parseJsonFile(filePath);
|
const projectName = await loadProjectFile(previousProject);
|
||||||
const result = parseDatabaseModel(fileData);
|
return projectName;
|
||||||
let parsedFileName = previousProject;
|
|
||||||
let parsedFilePath = filePath;
|
|
||||||
|
|
||||||
if (result.errors.length > 0) {
|
|
||||||
logger.warning(LogOrigin.Server, 'Project loaded with errors');
|
|
||||||
parsedFileName = await handleCorruptedFile(filePath, previousProject);
|
|
||||||
parsedFilePath = getPathToProject(parsedFileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
await initPersistence(parsedFilePath, result.data);
|
|
||||||
await setLastLoadedProject(parsedFileName);
|
|
||||||
return parsedFileName;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// if we are here, most likely the json parsing failed and the file is corrupt
|
||||||
logger.warning(LogOrigin.Server, `Unable to load previous project ${previousProject}: ${getErrorMessage(error)}`);
|
logger.warning(LogOrigin.Server, `Unable to load previous project ${previousProject}: ${getErrorMessage(error)}`);
|
||||||
await moveCorruptFile(filePath, previousProject).catch((_) => {
|
try {
|
||||||
|
const pathToFile = getPathToProject(previousProject);
|
||||||
|
await moveCorruptFile(pathToFile, previousProject);
|
||||||
|
} catch (_) {
|
||||||
/* while we have to catch the error, we dont need to handle it */
|
/* while we have to catch the error, we dont need to handle it */
|
||||||
});
|
}
|
||||||
|
|
||||||
return loadNewProject();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return loadNewProject();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads a data from a file into the runtime
|
* Loads a data from a file into the runtime
|
||||||
*/
|
*/
|
||||||
export async function loadProjectFile(name: string) {
|
export async function loadProjectFile(name: string): Promise<string> {
|
||||||
const filePath = doesProjectExist(name);
|
const filePath = doesProjectExist(name);
|
||||||
if (filePath === null) {
|
if (filePath === null) {
|
||||||
throw new Error('Project file not found');
|
throw new Error('Project file not found');
|
||||||
@@ -178,31 +184,14 @@ export async function loadProjectFile(name: string) {
|
|||||||
const fileData = await parseJsonFile(filePath);
|
const fileData = await parseJsonFile(filePath);
|
||||||
const result = parseDatabaseModel(fileData);
|
const result = parseDatabaseModel(fileData);
|
||||||
let parsedFileName = name;
|
let parsedFileName = name;
|
||||||
let parsedFilePath = filePath;
|
|
||||||
|
|
||||||
if (result.errors.length > 0) {
|
if (result.errors.length > 0) {
|
||||||
logger.warning(LogOrigin.Server, 'Project loaded with errors');
|
logger.warning(LogOrigin.Server, 'Project loaded with errors');
|
||||||
parsedFileName = await handleCorruptedFile(filePath, name);
|
parsedFileName = await handleCorruptedFile(filePath, name);
|
||||||
parsedFilePath = getPathToProject(parsedFileName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// change LowDB to point to new file
|
const projectName = await loadProject(result.data, parsedFileName);
|
||||||
await initPersistence(parsedFilePath, result.data);
|
return projectName;
|
||||||
logger.info(LogOrigin.Server, `Loaded project ${parsedFileName}`);
|
|
||||||
|
|
||||||
// persist the project selection
|
|
||||||
await setLastLoadedProject(parsedFileName);
|
|
||||||
|
|
||||||
// since load happens at runtime, we need to update the services that depend on the data
|
|
||||||
|
|
||||||
// apply data model
|
|
||||||
runtimeService.stop();
|
|
||||||
|
|
||||||
const { rundowns, customFields } = result.data;
|
|
||||||
|
|
||||||
// apply the rundown
|
|
||||||
const firstRundown = getFirstRundown(rundowns);
|
|
||||||
await initRundown(firstRundown, customFields);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -239,7 +228,7 @@ export async function duplicateProjectFile(originalFile: string, newFilename: st
|
|||||||
/**
|
/**
|
||||||
* Renames an existing project file
|
* Renames an existing project file
|
||||||
*/
|
*/
|
||||||
export async function renameProjectFile(originalFile: string, newFilename: string) {
|
export async function renameProjectFile(originalFile: string, newFilename: string): Promise<string> {
|
||||||
const projectFilePath = doesProjectExist(originalFile);
|
const projectFilePath = doesProjectExist(originalFile);
|
||||||
if (projectFilePath === null) {
|
if (projectFilePath === null) {
|
||||||
throw new Error('Project file not found');
|
throw new Error('Project file not found');
|
||||||
@@ -257,50 +246,23 @@ export async function renameProjectFile(originalFile: string, newFilename: strin
|
|||||||
const isLoaded = await isLastLoadedProject(originalFile);
|
const isLoaded = await isLastLoadedProject(originalFile);
|
||||||
if (isLoaded) {
|
if (isLoaded) {
|
||||||
const fileData = await parseJsonFile(pathToRenamed);
|
const fileData = await parseJsonFile(pathToRenamed);
|
||||||
const result = parseDatabaseModel(fileData);
|
const projectData = parseDatabaseModel(fileData);
|
||||||
|
|
||||||
// change LowDB to point to new file
|
const newFileName = await loadProject(projectData.data, newFilename);
|
||||||
await initPersistence(pathToRenamed, result.data);
|
return newFileName;
|
||||||
logger.info(LogOrigin.Server, `Loaded project ${newFilename}`);
|
|
||||||
|
|
||||||
// persist the project selection
|
|
||||||
await setLastLoadedProject(newFilename);
|
|
||||||
|
|
||||||
// apply data model
|
|
||||||
runtimeService.stop();
|
|
||||||
|
|
||||||
const { rundowns, customFields } = result.data;
|
|
||||||
|
|
||||||
// apply the rundown
|
|
||||||
const firstRundown = getFirstRundown(rundowns);
|
|
||||||
await initRundown(firstRundown, customFields);
|
|
||||||
}
|
}
|
||||||
|
return newFilename;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new project file and applies its result
|
* Creates a new project file and applies its result
|
||||||
*/
|
*/
|
||||||
export async function createProject(filename: string, initialData: Partial<DatabaseModel>) {
|
export async function createProject(filename: string, initialData: Partial<DatabaseModel>): Promise<string> {
|
||||||
const data = safeMerge(dbModel, initialData);
|
const data = safeMerge(dbModel, initialData);
|
||||||
|
|
||||||
const fileNameWithExtension = ensureJsonExtension(filename);
|
const fileNameWithExtension = ensureJsonExtension(filename);
|
||||||
const uniqueFileName = generateUniqueFileName(publicDir.projectsDir, fileNameWithExtension);
|
const newFilename = await loadProject(data, fileNameWithExtension);
|
||||||
const newFile = getPathToProject(uniqueFileName);
|
return newFilename;
|
||||||
|
|
||||||
// change LowDB to point to new file
|
|
||||||
await initPersistence(newFile, data);
|
|
||||||
|
|
||||||
// apply data to running services
|
|
||||||
// we dont need to parse since we are creating a new file
|
|
||||||
await patchCurrentProject(data);
|
|
||||||
|
|
||||||
// update app state to point to new value
|
|
||||||
setLastLoadedProject(uniqueFileName);
|
|
||||||
|
|
||||||
// update the service state
|
|
||||||
currentProjectState.currentProjectName = uniqueFileName;
|
|
||||||
|
|
||||||
return uniqueFileName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -338,7 +300,7 @@ export async function patchCurrentProject(data: Partial<DatabaseModel>) {
|
|||||||
* We currently ignore all other rundowns
|
* We currently ignore all other rundowns
|
||||||
*/
|
*/
|
||||||
const firstRundown = getFirstRundown(result.rundowns);
|
const firstRundown = getFirstRundown(result.rundowns);
|
||||||
initRundown(firstRundown, result.customFields);
|
await initRundown(firstRundown, result.customFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedData = await getDataProvider().getData();
|
const updatedData = await getDataProvider().getData();
|
||||||
@@ -346,17 +308,13 @@ export async function patchCurrentProject(data: Partial<DatabaseModel>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the title of a project
|
* Changes the current project data
|
||||||
* it handles invalidating the necessary data
|
* it handles invalidating the necessary data
|
||||||
*/
|
*/
|
||||||
export async function editCurrentProjectData(newData: Partial<ProjectData>) {
|
export async function editCurrentProjectData(newData: Partial<ProjectData>) {
|
||||||
const currentProjectData = getDataProvider().getProjectData();
|
const currentProjectData = getDataProvider().getProjectData();
|
||||||
const updatedProjectData = await getDataProvider().setProjectData(newData);
|
const updatedProjectData = await getDataProvider().setProjectData(newData);
|
||||||
|
|
||||||
if (currentProjectData.title !== updatedProjectData.title) {
|
|
||||||
// something
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the old logo if the logo has been removed
|
// Delete the old logo if the logo has been removed
|
||||||
if (!updatedProjectData.projectLogo && currentProjectData.projectLogo) {
|
if (!updatedProjectData.projectLogo && currentProjectData.projectLogo) {
|
||||||
const filePath = join(publicDir.logoDir, currentProjectData.projectLogo);
|
const filePath = join(publicDir.logoDir, currentProjectData.projectLogo);
|
||||||
|
|||||||
@@ -282,8 +282,8 @@ function notifyChanges(options: NotifyChangesOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overrides the rundown with the given
|
* Sets a new rundown in the cache
|
||||||
* @param rundown
|
* and marks it as the currently loaded one
|
||||||
*/
|
*/
|
||||||
export async function initRundown(rundown: Readonly<Rundown>, customFields: Readonly<CustomFields>) {
|
export async function initRundown(rundown: Readonly<Rundown>, customFields: Readonly<CustomFields>) {
|
||||||
await cache.init(rundown, customFields);
|
await cache.init(rundown, customFields);
|
||||||
|
|||||||
@@ -34,11 +34,10 @@ beforeAll(() => {
|
|||||||
|
|
||||||
describe('generate()', () => {
|
describe('generate()', () => {
|
||||||
test('benchmark function execution time', () => {
|
test('benchmark function execution time', () => {
|
||||||
const rundown = demoDb.rundowns.default;
|
|
||||||
const t1 = performance.now();
|
const t1 = performance.now();
|
||||||
let result: ProcessedRundownMetadata | null = null;
|
let result: ProcessedRundownMetadata | null = null;
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
result = generate(rundown);
|
result = generate(demoDb.rundowns.default, demoDb.customFields);
|
||||||
}
|
}
|
||||||
const t2 = performance.now();
|
const t2 = performance.now();
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -60,7 +59,7 @@ describe('generate()', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const initResult = generate(rundown);
|
const initResult = generate(rundown, {});
|
||||||
expect(initResult.order.length).toBe(3);
|
expect(initResult.order.length).toBe(3);
|
||||||
expect(initResult.order).toStrictEqual(['1', '2', '3']);
|
expect(initResult.order).toStrictEqual(['1', '2', '3']);
|
||||||
expect(initResult.entries['1'].type).toBe(SupportedEvent.Event);
|
expect(initResult.entries['1'].type).toBe(SupportedEvent.Event);
|
||||||
@@ -77,7 +76,7 @@ describe('generate()', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const initResult = generate(rundown);
|
const initResult = generate(rundown, {});
|
||||||
expect(initResult.order.length).toBe(2);
|
expect(initResult.order.length).toBe(2);
|
||||||
expect((initResult.entries['2'] as OntimeEvent).delay).toBe(100);
|
expect((initResult.entries['2'] as OntimeEvent).delay).toBe(100);
|
||||||
expect(initResult.totalDelay).toBe(100);
|
expect(initResult.totalDelay).toBe(100);
|
||||||
@@ -97,7 +96,7 @@ describe('generate()', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const initResult = generate(rundown);
|
const initResult = generate(rundown, {});
|
||||||
expect(initResult.order.length).toBe(7);
|
expect(initResult.order.length).toBe(7);
|
||||||
expect((initResult.entries['1'] as OntimeEvent).delay).toBe(0);
|
expect((initResult.entries['1'] as OntimeEvent).delay).toBe(0);
|
||||||
expect((initResult.entries['2'] as OntimeEvent).delay).toBe(200);
|
expect((initResult.entries['2'] as OntimeEvent).delay).toBe(200);
|
||||||
@@ -117,7 +116,7 @@ describe('generate()', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const initResult = generate(rundown);
|
const initResult = generate(rundown, {});
|
||||||
expect(initResult.totalDuration).toBe(10500 - 9000); // last end - first start
|
expect(initResult.totalDuration).toBe(10500 - 9000); // last end - first start
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -132,7 +131,7 @@ describe('generate()', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const initResult = generate(rundown);
|
const initResult = generate(rundown, {});
|
||||||
expect(initResult.totalDuration).toBe(20000 - 9000); // last end - first start
|
expect(initResult.totalDuration).toBe(20000 - 9000); // last end - first start
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -167,7 +166,7 @@ describe('generate()', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const initResult = generate(rundown);
|
const initResult = generate(rundown, {});
|
||||||
expect(initResult.totalDuration).toBe(dayInMs + MILLIS_PER_HOUR); // day + last end - first start
|
expect(initResult.totalDuration).toBe(dayInMs + MILLIS_PER_HOUR); // day + last end - first start
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -185,7 +184,7 @@ describe('generate()', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const initResult = generate(rundown);
|
const initResult = generate(rundown, {});
|
||||||
expect(initResult.order.length).toBe(7);
|
expect(initResult.order.length).toBe(7);
|
||||||
expect((initResult.entries['1'] as OntimeEvent).delay).toBe(0);
|
expect((initResult.entries['1'] as OntimeEvent).delay).toBe(0);
|
||||||
expect((initResult.entries['2'] as OntimeEvent).delay).toBe(-200);
|
expect((initResult.entries['2'] as OntimeEvent).delay).toBe(-200);
|
||||||
@@ -227,7 +226,7 @@ describe('generate()', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const initResult = generate(rundown);
|
const initResult = generate(rundown, {});
|
||||||
expect(initResult.order.length).toBe(5);
|
expect(initResult.order.length).toBe(5);
|
||||||
expect((initResult.entries['2'] as OntimeEvent).timeStart).toBe(2);
|
expect((initResult.entries['2'] as OntimeEvent).timeStart).toBe(2);
|
||||||
expect((initResult.entries['2'] as OntimeEvent).timeEnd).toBe(12);
|
expect((initResult.entries['2'] as OntimeEvent).timeEnd).toBe(12);
|
||||||
@@ -248,7 +247,7 @@ describe('generate()', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const initResult = generate(rundown);
|
const initResult = generate(rundown, {});
|
||||||
expect(initResult.order.length).toBe(3);
|
expect(initResult.order.length).toBe(3);
|
||||||
expect((initResult.entries['3'] as OntimeEvent).timeStart).toBe(2);
|
expect((initResult.entries['3'] as OntimeEvent).timeStart).toBe(2);
|
||||||
});
|
});
|
||||||
@@ -264,7 +263,7 @@ describe('generate()', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const initResult = generate(rundown);
|
const initResult = generate(rundown, {});
|
||||||
expect(initResult.order.length).toBe(4);
|
expect(initResult.order.length).toBe(4);
|
||||||
expect(initResult.totalDuration).toBe(500 - 100);
|
expect(initResult.totalDuration).toBe(500 - 100);
|
||||||
});
|
});
|
||||||
@@ -280,7 +279,7 @@ describe('generate()', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const initResult = generate(rundown);
|
const initResult = generate(rundown, {});
|
||||||
expect(initResult.order.length).toBe(4);
|
expect(initResult.order.length).toBe(4);
|
||||||
expect(initResult.totalDuration).toBe(500 - 100);
|
expect(initResult.totalDuration).toBe(500 - 100);
|
||||||
});
|
});
|
||||||
@@ -310,7 +309,7 @@ describe('generate()', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const initResult = generate(rundown);
|
const initResult = generate(rundown, {});
|
||||||
expect(initResult.totalDuration).toBe((23 - 9 + 48) * MILLIS_PER_HOUR);
|
expect(initResult.totalDuration).toBe((23 - 9 + 48) * MILLIS_PER_HOUR);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -333,7 +332,7 @@ describe('generate()', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const initResult = generate(rundown);
|
const initResult = generate(rundown, {});
|
||||||
const expectedDuration = 8 * MILLIS_PER_HOUR + (dayInMs - 12 * MILLIS_PER_HOUR);
|
const expectedDuration = 8 * MILLIS_PER_HOUR + (dayInMs - 12 * MILLIS_PER_HOUR);
|
||||||
expect(initResult.totalDuration).toBe(expectedDuration);
|
expect(initResult.totalDuration).toBe(expectedDuration);
|
||||||
});
|
});
|
||||||
@@ -369,7 +368,7 @@ describe('generate()', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const initResult = generate(rundown);
|
const initResult = generate(rundown, {});
|
||||||
expect(initResult.entries).toMatchObject({
|
expect(initResult.entries).toMatchObject({
|
||||||
'1': {
|
'1': {
|
||||||
timeStart: 0,
|
timeStart: 0,
|
||||||
@@ -455,7 +454,7 @@ describe('generate() v4', () => {
|
|||||||
'300': makeOntimeEvent({ id: '300', timeStart: 300, timeEnd: 400, duration: 100 }),
|
'300': makeOntimeEvent({ id: '300', timeStart: 300, timeEnd: 400, duration: 100 }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const generatedRundown = generate(rundown);
|
const generatedRundown = generate(rundown, {});
|
||||||
|
|
||||||
expect(generatedRundown.order).toMatchObject(['1']);
|
expect(generatedRundown.order).toMatchObject(['1']);
|
||||||
expect(generatedRundown.totalDuration).toBe(300);
|
expect(generatedRundown.totalDuration).toBe(300);
|
||||||
@@ -495,7 +494,7 @@ describe('generate() v4', () => {
|
|||||||
'303': makeOntimeEvent({ id: '303', timeStart: 1100, timeEnd: 1200, duration: 100, linkStart: true }),
|
'303': makeOntimeEvent({ id: '303', timeStart: 1100, timeEnd: 1200, duration: 100, linkStart: true }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const generatedRundown = generate(rundown);
|
const generatedRundown = generate(rundown, {});
|
||||||
|
|
||||||
expect(generatedRundown.order).toMatchObject(['0', '1', '2', '3']);
|
expect(generatedRundown.order).toMatchObject(['0', '1', '2', '3']);
|
||||||
expect(generatedRundown.totalDuration).toBe(1200);
|
expect(generatedRundown.totalDuration).toBe(1200);
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import type { RundownMetadata } from './rundown.types.js';
|
|||||||
import { apply } from './delayUtils.js';
|
import { apply } from './delayUtils.js';
|
||||||
import { hasChanges, isDataStale, makeRundownMetadata, type ProcessedRundownMetadata } from './rundownCache.utils.js';
|
import { hasChanges, isDataStale, makeRundownMetadata, type ProcessedRundownMetadata } from './rundownCache.utils.js';
|
||||||
|
|
||||||
/** We hold the currently selected rundown and its metadata in memory */
|
|
||||||
let currentRundownId: EntryId = '';
|
let currentRundownId: EntryId = '';
|
||||||
let currentRundown: Rundown = {
|
let currentRundown: Rundown = {
|
||||||
id: '',
|
id: '',
|
||||||
@@ -78,16 +77,14 @@ export let customFieldChangelog: Record<string, string> = {};
|
|||||||
* Receives a rundown which will be processed and used as the new current rundown
|
* Receives a rundown which will be processed and used as the new current rundown
|
||||||
*/
|
*/
|
||||||
export async function init(initialRundown: Readonly<Rundown>, customFields: Readonly<CustomFields>) {
|
export async function init(initialRundown: Readonly<Rundown>, customFields: Readonly<CustomFields>) {
|
||||||
// TODO: do we need to clone?
|
// we clone this objects since we use mutating logic in the cache
|
||||||
currentRundown = structuredClone(initialRundown);
|
currentRundown = structuredClone(initialRundown);
|
||||||
currentRundownId = initialRundown.id;
|
currentRundownId = initialRundown.id;
|
||||||
projectCustomFields = structuredClone(customFields);
|
projectCustomFields = structuredClone(customFields);
|
||||||
generate();
|
|
||||||
|
|
||||||
// TODO: we may not need to persist this data since it should come from the database
|
updateCache();
|
||||||
// update the persisted data
|
|
||||||
await getDataProvider().setRundown(currentRundownId, currentRundown);
|
currentRundownId;
|
||||||
await getDataProvider().setCustomFields(customFields);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,14 +92,9 @@ export async function init(initialRundown: Readonly<Rundown>, customFields: Read
|
|||||||
* @private should not be called outside of `rundownCache.ts`, exported for testing
|
* @private should not be called outside of `rundownCache.ts`, exported for testing
|
||||||
*/
|
*/
|
||||||
export function generate(
|
export function generate(
|
||||||
initialRundown: Readonly<Rundown> = currentRundown,
|
initialRundown: Readonly<Rundown>,
|
||||||
customFields: Readonly<CustomFields> = projectCustomFields,
|
customFields: Readonly<CustomFields>,
|
||||||
): ProcessedRundownMetadata {
|
): ProcessedRundownMetadata {
|
||||||
// The stale state can only be cleared inside generate()
|
|
||||||
function clearIsStale() {
|
|
||||||
isStale = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { process, getMetadata } = makeRundownMetadata(customFields, customFieldChangelog);
|
const { process, getMetadata } = makeRundownMetadata(customFields, customFieldChangelog);
|
||||||
|
|
||||||
for (let i = 0; i < initialRundown.order.length; i++) {
|
for (let i = 0; i < initialRundown.order.length; i++) {
|
||||||
@@ -154,9 +146,18 @@ export function generate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const processedData = getMetadata();
|
return getMetadata();
|
||||||
clearIsStale();
|
}
|
||||||
customFieldChangelog = {};
|
|
||||||
|
/**
|
||||||
|
* Runs the generate function in the currently loaded rundown and updates caches
|
||||||
|
*/
|
||||||
|
export function updateCache() {
|
||||||
|
// The stale state can only be cleared inside updateCache()
|
||||||
|
function clearIsStale() {
|
||||||
|
isStale = false;
|
||||||
|
}
|
||||||
|
const processedData = generate(currentRundown, projectCustomFields);
|
||||||
|
|
||||||
// update the cache values
|
// update the cache values
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- we are not interested in the iteration data
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- we are not interested in the iteration data
|
||||||
@@ -164,15 +165,14 @@ export function generate(
|
|||||||
currentRundown.entries = entries;
|
currentRundown.entries = entries;
|
||||||
currentRundown.order = order;
|
currentRundown.order = order;
|
||||||
rundownMetadata = metadata;
|
rundownMetadata = metadata;
|
||||||
|
clearIsStale();
|
||||||
// The return value is used for testing
|
customFieldChangelog = {};
|
||||||
return processedData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns an ID guaranteed to be unique */
|
/** Returns an ID guaranteed to be unique */
|
||||||
export function getUniqueId(): string {
|
export function getUniqueId(): string {
|
||||||
if (isStale) {
|
if (isStale) {
|
||||||
generate();
|
updateCache();
|
||||||
}
|
}
|
||||||
let id = '';
|
let id = '';
|
||||||
do {
|
do {
|
||||||
@@ -184,7 +184,7 @@ export function getUniqueId(): string {
|
|||||||
/** Returns index of an event with a given id */
|
/** Returns index of an event with a given id */
|
||||||
export function getIndexOf(eventId: EntryId) {
|
export function getIndexOf(eventId: EntryId) {
|
||||||
if (isStale) {
|
if (isStale) {
|
||||||
generate();
|
updateCache();
|
||||||
}
|
}
|
||||||
return currentRundown.order.indexOf(eventId);
|
return currentRundown.order.indexOf(eventId);
|
||||||
}
|
}
|
||||||
@@ -192,7 +192,7 @@ export function getIndexOf(eventId: EntryId) {
|
|||||||
/** Returns id of an event at a given index */
|
/** Returns id of an event at a given index */
|
||||||
export function getIdOf(index: number) {
|
export function getIdOf(index: number) {
|
||||||
if (isStale) {
|
if (isStale) {
|
||||||
generate();
|
updateCache();
|
||||||
}
|
}
|
||||||
return currentRundown.order.at(index);
|
return currentRundown.order.at(index);
|
||||||
}
|
}
|
||||||
@@ -213,7 +213,7 @@ type RundownCache = {
|
|||||||
*/
|
*/
|
||||||
export function get(): Readonly<RundownCache> {
|
export function get(): Readonly<RundownCache> {
|
||||||
if (isStale) {
|
if (isStale) {
|
||||||
generate();
|
updateCache();
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: currentRundown.id,
|
id: currentRundown.id,
|
||||||
@@ -232,7 +232,7 @@ export function get(): Readonly<RundownCache> {
|
|||||||
*/
|
*/
|
||||||
export function getMetadata(): Readonly<RundownMetadata & { revision: number }> {
|
export function getMetadata(): Readonly<RundownMetadata & { revision: number }> {
|
||||||
if (isStale) {
|
if (isStale) {
|
||||||
generate();
|
updateCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -252,7 +252,7 @@ export type RundownOrder = {
|
|||||||
*/
|
*/
|
||||||
export function getEventOrder(): Readonly<RundownOrder> {
|
export function getEventOrder(): Readonly<RundownOrder> {
|
||||||
if (isStale) {
|
if (isStale) {
|
||||||
generate();
|
updateCache();
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
order: currentRundown.order,
|
order: currentRundown.order,
|
||||||
@@ -482,7 +482,7 @@ function invalidateIfUsed(label: CustomFieldLabel) {
|
|||||||
// ... and schedule a cache update
|
// ... and schedule a cache update
|
||||||
// schedule a non priority cache update
|
// schedule a non priority cache update
|
||||||
setImmediate(async () => {
|
setImmediate(async () => {
|
||||||
generate();
|
updateCache();
|
||||||
await getDataProvider().setRundown(currentRundownId, currentRundown);
|
await getDataProvider().setRundown(currentRundownId, currentRundown);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,10 +149,18 @@ export function filterTimedEvents(rundown: Rundown, timedEventOrder: EntryId[]):
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the first rundown in the project
|
* Gets the first rundown in the project
|
||||||
* We ensure that the projects always have a rundown
|
* We know that the project has at least one rundown
|
||||||
*/
|
*/
|
||||||
export function getFirstRundown(rundowns: ProjectRundowns): Rundown {
|
export function getFirstRundown(rundowns: ProjectRundowns): Rundown {
|
||||||
const firstKey = Object.keys(rundowns)[0];
|
const firstKey = Object.keys(rundowns)[0];
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-labels -- dev code path
|
||||||
|
DEV: {
|
||||||
|
if (!firstKey) {
|
||||||
|
throw new Error('rundownUtils.getFirstRundown() No rundowns found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return rundowns[firstKey];
|
return rundowns[firstKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -146,7 +146,6 @@ describe('test aliases import', () => {
|
|||||||
const testData = {
|
const testData = {
|
||||||
rundown: [],
|
rundown: [],
|
||||||
settings: {
|
settings: {
|
||||||
app: 'ontime',
|
|
||||||
version: '2.0.0',
|
version: '2.0.0',
|
||||||
},
|
},
|
||||||
urlPresets: [
|
urlPresets: [
|
||||||
@@ -171,7 +170,6 @@ describe('test views import', () => {
|
|||||||
const testData = {
|
const testData = {
|
||||||
rundown: [],
|
rundown: [],
|
||||||
settings: {
|
settings: {
|
||||||
app: 'ontime',
|
|
||||||
version: '2.0.0',
|
version: '2.0.0',
|
||||||
},
|
},
|
||||||
viewSettings: {
|
viewSettings: {
|
||||||
@@ -205,7 +203,6 @@ describe('test views import', () => {
|
|||||||
const testData = {
|
const testData = {
|
||||||
rundown: [],
|
rundown: [],
|
||||||
settings: {
|
settings: {
|
||||||
app: 'ontime',
|
|
||||||
version: '2.0.0',
|
version: '2.0.0',
|
||||||
},
|
},
|
||||||
} as unknown as DatabaseModel;
|
} as unknown as DatabaseModel;
|
||||||
|
|||||||
@@ -229,11 +229,10 @@ describe('parseSettings()', () => {
|
|||||||
expect(() => parseSettings({})).toThrow();
|
expect(() => parseSettings({})).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns an a base model as long as we have the app and version', () => {
|
it('returns an a base model as long as we have the app version', () => {
|
||||||
const result = parseSettings({ settings: { app: 'ontime', version: '1' } as Settings });
|
const result = parseSettings({ settings: { version: '1' } as Settings });
|
||||||
expect(result).toBeTypeOf('object');
|
expect(result).toBeTypeOf('object');
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
app: 'ontime',
|
|
||||||
version: expect.any(String),
|
version: expect.any(String),
|
||||||
serverPort: 4001,
|
serverPort: 4001,
|
||||||
editorKey: null,
|
editorKey: null,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { createEvent, type ErrorEmitter } from './parser.js';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a rundowns object along with the project custom fields
|
* Parse a rundowns object along with the project custom fields
|
||||||
|
* Returns a default rundown if none exists
|
||||||
*/
|
*/
|
||||||
export function parseRundowns(
|
export function parseRundowns(
|
||||||
data: Partial<DatabaseModel>,
|
data: Partial<DatabaseModel>,
|
||||||
@@ -32,6 +33,8 @@ export function parseRundowns(
|
|||||||
// check custom fields first
|
// check custom fields first
|
||||||
const parsedCustomFields = parseCustomFields(data, emitError);
|
const parsedCustomFields = parseCustomFields(data, emitError);
|
||||||
|
|
||||||
|
// ensure there is always a rundown to import
|
||||||
|
// this is important since the rest of the app assumes this exist
|
||||||
if (!data.rundowns || isObjectEmpty(data.rundowns)) {
|
if (!data.rundowns || isObjectEmpty(data.rundowns)) {
|
||||||
emitError?.('No data found to import');
|
emitError?.('No data found to import');
|
||||||
return {
|
return {
|
||||||
@@ -193,14 +196,14 @@ export function parseProject(data: Partial<DatabaseModel>, emitError?: ErrorEmit
|
|||||||
*/
|
*/
|
||||||
export function parseSettings(data: Partial<DatabaseModel>): Settings {
|
export function parseSettings(data: Partial<DatabaseModel>): Settings {
|
||||||
// skip if file definition is missing
|
// skip if file definition is missing
|
||||||
if (!data.settings || data.settings?.app !== 'ontime' || data.settings?.version == null) {
|
// TODO: skip parsing if the version is not correct
|
||||||
throw new Error('ERROR: unable to parse settings, missing app or version');
|
if (!data.settings || data.settings?.version == null) {
|
||||||
|
throw new Error('ERROR: unable to parse settings, missing or incorrect version');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Found settings, importing...');
|
console.log('Found settings, importing...');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
app: dbModel.settings.app,
|
|
||||||
version: dbModel.settings.version,
|
version: dbModel.settings.version,
|
||||||
serverPort: data.settings.serverPort ?? dbModel.settings.serverPort,
|
serverPort: data.settings.serverPort ?? dbModel.settings.serverPort,
|
||||||
editorKey: data.settings.editorKey ?? null,
|
editorKey: data.settings.editorKey ?? null,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { TimeFormat } from './TimeFormat.type.js';
|
import type { TimeFormat } from './TimeFormat.type.js';
|
||||||
|
|
||||||
export type Settings = {
|
export type Settings = {
|
||||||
app: 'ontime';
|
|
||||||
version: string;
|
version: string;
|
||||||
serverPort: number;
|
serverPort: number;
|
||||||
editorKey: null | string;
|
editorKey: null | string;
|
||||||
|
|||||||
Reference in New Issue
Block a user