mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-09 17:33:55 +00:00
refactor: cuesheet v2 (#435)
* refactor: typescript migration * chore: remove prop-types package * refactor: file structure * refactor: migrate ontime table to tanstack table 8 * refactor: rundown controller uses service as data source * refactor: convert to typescript * feat: caching store * refactor: add delay values to rundown * feat: toggle past visibility * chore: update tests * refactor: add extra fields to CSV * style: show skipped events * chore: add route to navigation menu * style: allow jumping to bottom * chore: add tests
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { LogOrigin, OSCSettings } from 'ontime-types';
|
||||
|
||||
import { Server } from 'node-osc';
|
||||
import { OSCSettings } from 'ontime-types';
|
||||
|
||||
import { IAdapter } from './IAdapter.js';
|
||||
import { dispatchFromAdapter } from '../controllers/integrationController.js';
|
||||
@@ -25,13 +26,13 @@ export class OscServer implements IAdapter {
|
||||
|
||||
// get first part before (ontime)
|
||||
if (address !== 'ontime') {
|
||||
logger.error('RX', `OSC IN: OSC messages to ontime must start with /ontime/, received: ${msg}`);
|
||||
logger.error(LogOrigin.Rx, `OSC IN: OSC messages to ontime must start with /ontime/, received: ${msg}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// get second part (command)
|
||||
if (!path) {
|
||||
logger.error('RX', 'OSC IN: No path found');
|
||||
logger.error(LogOrigin.Rx, 'OSC IN: No path found');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -42,7 +43,7 @@ export class OscServer implements IAdapter {
|
||||
this.osc.emit(topic, payload);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('RX', `OSC IN: ${error}`);
|
||||
logger.error(LogOrigin.Rx, `OSC IN: ${error}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
* Payload: adds necessary payload for the request to be completed
|
||||
*/
|
||||
|
||||
import { LogOrigin } from 'ontime-types';
|
||||
|
||||
import { WebSocket, WebSocketServer } from 'ws';
|
||||
|
||||
import getRandomName from '../utils/getRandomName.js';
|
||||
@@ -47,7 +49,7 @@ export class SocketServer implements IAdapter {
|
||||
this.wss.on('connection', (ws) => {
|
||||
let clientId = getRandomName();
|
||||
this.clientIds.add(clientId);
|
||||
logger.info('CLIENT', `${this.wss.clients.size} Connections with new: ${clientId}`);
|
||||
logger.info(LogOrigin.Client, `${this.wss.clients.size} Connections with new: ${clientId}`);
|
||||
|
||||
// send store payload on connect
|
||||
ws.send(
|
||||
@@ -67,7 +69,7 @@ export class SocketServer implements IAdapter {
|
||||
ws.on('error', console.error);
|
||||
|
||||
ws.on('close', () => {
|
||||
logger.info('CLIENT', `${this.wss.clients.size} Connections with disconnected: ${clientId}`);
|
||||
logger.info(LogOrigin.Client, `${this.wss.clients.size} Connections with disconnected: ${clientId}`);
|
||||
this.clientIds.delete(clientId);
|
||||
});
|
||||
|
||||
@@ -96,7 +98,7 @@ export class SocketServer implements IAdapter {
|
||||
clientId = payload;
|
||||
this.clientIds.delete(previousName);
|
||||
this.clientIds.add(clientId);
|
||||
logger.info('CLIENT', `Client ${previousName} renamed to ${clientId}`);
|
||||
logger.info(LogOrigin.Client, `Client ${previousName} renamed to ${clientId}`);
|
||||
}
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
@@ -127,7 +129,7 @@ export class SocketServer implements IAdapter {
|
||||
ws.send(topic, payload);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('RX', `WS IN: ${error}`);
|
||||
logger.error(LogOrigin.Rx, `WS IN: ${error}`);
|
||||
}
|
||||
} catch (_) {
|
||||
// we ignore unknown
|
||||
|
||||
@@ -9,7 +9,7 @@ import { join, resolve } from 'path';
|
||||
|
||||
import { currentDirectory, environment, externalsStartDirectory, isProduction, resolvedPath } from './setup.js';
|
||||
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
|
||||
import { OSCSettings } from 'ontime-types';
|
||||
import { LogOrigin, OSCSettings } from 'ontime-types';
|
||||
|
||||
// Import Routes
|
||||
import { router as rundownRouter } from './routes/rundownRouter.js';
|
||||
@@ -159,7 +159,7 @@ export const startOSCServer = async (overrideConfig = null) => {
|
||||
const { osc } = DataProvider.getData();
|
||||
|
||||
if (!osc.enabledIn) {
|
||||
logger.info('RX', 'OSC Input Disabled');
|
||||
logger.info(LogOrigin.Rx, 'OSC Input Disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ export const startOSCServer = async (overrideConfig = null) => {
|
||||
};
|
||||
|
||||
// Start OSC Server
|
||||
logger.info('RX', `Starting OSC Server on port: ${oscSettings.portIn}`);
|
||||
logger.info(LogOrigin.Rx, `Starting OSC Server on port: ${oscSettings.portIn}`);
|
||||
oscServer = new OscServer(oscSettings);
|
||||
};
|
||||
|
||||
@@ -187,7 +187,7 @@ export const startIntegrations = async (config?: { osc: OSCSettings }) => {
|
||||
}
|
||||
|
||||
const { success, message } = oscIntegration.init(osc);
|
||||
logger.info('RX', message);
|
||||
logger.info(LogOrigin.Rx, message);
|
||||
|
||||
if (success) {
|
||||
integrationService.register(oscIntegration);
|
||||
@@ -214,12 +214,12 @@ export const shutdown = async (exitCode = 0) => {
|
||||
process.on('exit', (code) => console.log(`Ontime exited with code: ${code}`));
|
||||
|
||||
process.on('unhandledRejection', async (error) => {
|
||||
logger.error('SERVER', `Error: unhandled rejection ${error}`);
|
||||
logger.error(LogOrigin.Server, `Error: unhandled rejection ${error}`);
|
||||
await shutdown(1);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', async (error) => {
|
||||
logger.error('SERVER', `Error: uncaught exception ${error}`);
|
||||
logger.error(LogOrigin.Server, `Error: uncaught exception ${error}`);
|
||||
await shutdown(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Class Event Provider is a mediator for handling the local db
|
||||
* and adds logic specific to ontime data
|
||||
*/
|
||||
import { EventData, SupportedEvent, ViewSettings } from 'ontime-types';
|
||||
import { EventData, ViewSettings } from 'ontime-types';
|
||||
|
||||
import { data, db } from '../../modules/loadDb.js';
|
||||
import { safeMerge } from './DataProvider.utils.js';
|
||||
@@ -27,31 +27,14 @@ export class DataProvider {
|
||||
await this.persist();
|
||||
}
|
||||
|
||||
static getIndexOf(eventId) {
|
||||
return data.rundown.findIndex((e) => e.id === eventId);
|
||||
}
|
||||
|
||||
static getEventById(eventId) {
|
||||
return data.rundown.find((e) => e.id === eventId);
|
||||
}
|
||||
|
||||
static async updateEventById(eventId, newData) {
|
||||
const eventIndex = data.rundown.findIndex((e) => e.id === eventId);
|
||||
const persistedEvent = data.rundown[eventIndex];
|
||||
const newEvent = { ...persistedEvent, ...newData };
|
||||
if (newEvent.type === SupportedEvent.Event) {
|
||||
newEvent.revision++;
|
||||
}
|
||||
data.rundown[eventIndex] = newEvent;
|
||||
await this.persist();
|
||||
return data.rundown[eventIndex];
|
||||
}
|
||||
|
||||
static async deleteEvent(eventId) {
|
||||
const eventIndex = data.rundown.findIndex((e) => e.id === eventId);
|
||||
|
||||
if (eventIndex !== -1) {
|
||||
data.rundown.splice(eventIndex, 1);
|
||||
await this.persist();
|
||||
}
|
||||
}
|
||||
|
||||
static getRundownLength() {
|
||||
return data.rundown.length;
|
||||
}
|
||||
@@ -62,53 +45,6 @@ export class DataProvider {
|
||||
await db.write();
|
||||
}
|
||||
|
||||
/**
|
||||
* Insets an event after a given index
|
||||
* @param entry
|
||||
* @param index
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
static async insertEventAt(entry, index) {
|
||||
// get events
|
||||
const events = DataProvider.getRundown();
|
||||
const count = events.length;
|
||||
const order = entry.order;
|
||||
|
||||
// Remove order field from object
|
||||
delete entry.order;
|
||||
|
||||
// Insert at beginning
|
||||
if (order === 0) {
|
||||
events.unshift(entry);
|
||||
}
|
||||
|
||||
// insert at end
|
||||
else if (order >= count) {
|
||||
events.push(entry);
|
||||
}
|
||||
|
||||
// insert in the middle
|
||||
else {
|
||||
events.splice(index, 0, entry);
|
||||
}
|
||||
|
||||
// save events
|
||||
await DataProvider.setRundown(events);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Inserts an entry after an element with given ID
|
||||
* @param entry
|
||||
* @param id
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
static async insertEventAfterId(entry, id) {
|
||||
const index = [...data.rundown].findIndex((event) => event.id === id);
|
||||
// eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars -- we are just getting rid of after parameter
|
||||
const { after, ...sanitisedEvent } = entry;
|
||||
await DataProvider.insertEventAt(sanitisedEvent, index + 1);
|
||||
}
|
||||
|
||||
static getSettings() {
|
||||
return data.settings;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Loaded, OntimeEvent, TitleBlock } from 'ontime-types';
|
||||
import { Loaded, OntimeEvent, SupportedEvent, TitleBlock } from 'ontime-types';
|
||||
|
||||
import { DataProvider } from '../data-provider/DataProvider.js';
|
||||
import { getRollTimers } from '../../services/rollUtils.js';
|
||||
@@ -34,8 +34,7 @@ export class EventLoader {
|
||||
* @return {array}
|
||||
*/
|
||||
static getTimedEvents(): OntimeEvent[] {
|
||||
// return mockLoaderData.filter((event) => event.type === 'event');
|
||||
return DataProvider.getRundown().filter((event) => event.type === 'event');
|
||||
return DataProvider.getRundown().filter((event) => event.type === SupportedEvent.Event) as OntimeEvent[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,8 +42,9 @@ export class EventLoader {
|
||||
* @return {array}
|
||||
*/
|
||||
static getPlayableEvents(): OntimeEvent[] {
|
||||
// return mockLoaderData.filter((event) => event.type === 'event' && !event.skip);
|
||||
return DataProvider.getRundown().filter((event) => event.type === 'event' && !event.skip);
|
||||
return DataProvider.getRundown().filter(
|
||||
(event) => event.type === SupportedEvent.Event && !event.skip,
|
||||
) as OntimeEvent[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Alias, EventData, LogOrigin } from 'ontime-types';
|
||||
|
||||
import fs from 'fs';
|
||||
import type { Alias, EventData } from 'ontime-types';
|
||||
import { networkInterfaces } from 'os';
|
||||
|
||||
import { fileHandler } from '../utils/parser.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
|
||||
@@ -10,7 +12,7 @@ import { eventStore } from '../stores/EventStore.js';
|
||||
import { resolveDbPath } from '../setup.js';
|
||||
import { oscIntegration } from '../services/integration-service/OscIntegration.js';
|
||||
import { logger } from '../classes/Logger.js';
|
||||
import { deleteAllEvents, forceReset } from '../services/RundownService.js';
|
||||
import { deleteAllEvents, forceReset } from '../services/rundown-service/RundownService.js';
|
||||
|
||||
// Create controller for GET request to '/ontime/poll'
|
||||
// Returns data for current state
|
||||
@@ -281,7 +283,7 @@ export const postOscSubscriptions = async (req, res) => {
|
||||
|
||||
// TODO: this update could be more granular, checking that relevant data was changed
|
||||
const { message } = oscIntegration.init(oscSettings);
|
||||
logger.info('RX', message);
|
||||
logger.info(LogOrigin.Rx, message);
|
||||
|
||||
res.send(oscSettings).status(200);
|
||||
} catch (error) {
|
||||
@@ -302,7 +304,7 @@ export const postOSC = async (req, res) => {
|
||||
|
||||
// TODO: this update could be more granular, checking that relevant data was changed
|
||||
const { message } = oscIntegration.init(oscSettings);
|
||||
logger.info('RX', message);
|
||||
logger.info(LogOrigin.Rx, message);
|
||||
|
||||
res.send(oscSettings).status(200);
|
||||
} catch (error) {
|
||||
|
||||
+5
-9
@@ -1,4 +1,4 @@
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.ts';
|
||||
import { OntimeEvent } from 'ontime-types';
|
||||
import { failEmptyObjects } from '../utils/routerUtils.js';
|
||||
import {
|
||||
addEvent,
|
||||
@@ -7,18 +7,14 @@ import {
|
||||
deleteEvent,
|
||||
editEvent,
|
||||
reorderEvent,
|
||||
} from '../services/RundownService.ts';
|
||||
} from '../services/rundown-service/RundownService.js';
|
||||
import { getDelayedRundown } from '../services/rundown-service/delayedRundown.utils.js';
|
||||
|
||||
// Create controller for GET request to '/events'
|
||||
// Returns -
|
||||
export const rundownGetAll = async (req, res) => {
|
||||
res.json(DataProvider.getRundown());
|
||||
};
|
||||
|
||||
// Create controller for GET request to '/events/:eventId'
|
||||
// Returns -
|
||||
export const getEventById = async (req, res) => {
|
||||
res.json(DataProvider.getEventById(req.params?.eventId));
|
||||
const delayedRundown = getDelayedRundown();
|
||||
res.json(delayedRundown);
|
||||
};
|
||||
|
||||
// Create controller for POST request to '/events/'
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EndAction, OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent, TimerType } from 'ontime-types';
|
||||
|
||||
export const event: Omit<OntimeEvent, 'id'> = {
|
||||
export const event: Omit<OntimeEvent, 'id' | 'delay'> = {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import express from 'express';
|
||||
import {
|
||||
deleteEventById,
|
||||
getEventById,
|
||||
rundownApplyDelay,
|
||||
rundownDelete,
|
||||
rundownGetAll,
|
||||
@@ -21,9 +20,6 @@ export const router = express.Router();
|
||||
// create route between controller and '/events/' endpoint
|
||||
router.get('/', rundownGetAll);
|
||||
|
||||
// create route between controller and '/events/:eventId' endpoint
|
||||
router.get('/:eventId', paramsMustHaveEventId, getEventById);
|
||||
|
||||
// create route between controller and '/events/' endpoint
|
||||
router.post('/', rundownPostValidator, rundownPost);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OntimeEvent } from 'ontime-types';
|
||||
import { LogOrigin, OntimeEvent, Playback } from 'ontime-types';
|
||||
import { validatePlayback } from 'ontime-utils';
|
||||
|
||||
import { eventLoader, EventLoader } from '../classes/event-loader/EventLoader.js';
|
||||
@@ -20,9 +20,9 @@ export class PlaybackService {
|
||||
static loadEvent(event: OntimeEvent): boolean {
|
||||
let success = false;
|
||||
if (!event) {
|
||||
logger.error('PLAYBACK', 'No event found');
|
||||
logger.error(LogOrigin.Playback, 'No event found');
|
||||
} else if (event.skip) {
|
||||
logger.warning('PLAYBACK', `Refused playback of skipped event ID ${event.id}`);
|
||||
logger.warning(LogOrigin.Playback, `Refused playback of skipped event ID ${event.id}`);
|
||||
} else {
|
||||
eventLoader.loadEvent(event);
|
||||
eventTimer.load(event);
|
||||
@@ -41,7 +41,7 @@ export class PlaybackService {
|
||||
const event = EventLoader.getEventWithId(eventId);
|
||||
const success = PlaybackService.loadEvent(event);
|
||||
if (success) {
|
||||
logger.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
||||
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
|
||||
PlaybackService.start();
|
||||
}
|
||||
return success;
|
||||
@@ -56,7 +56,7 @@ export class PlaybackService {
|
||||
const event = EventLoader.getEventAtIndex(eventIndex);
|
||||
const success = PlaybackService.loadEvent(event);
|
||||
if (success) {
|
||||
logger.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
||||
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
|
||||
PlaybackService.start();
|
||||
}
|
||||
return success;
|
||||
@@ -71,7 +71,7 @@ export class PlaybackService {
|
||||
const event = EventLoader.getEventWithId(eventId);
|
||||
const success = PlaybackService.loadEvent(event);
|
||||
if (success) {
|
||||
logger.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
||||
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
@@ -85,7 +85,7 @@ export class PlaybackService {
|
||||
const event = EventLoader.getEventAtIndex(eventIndex);
|
||||
const success = PlaybackService.loadEvent(event);
|
||||
if (success) {
|
||||
logger.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
||||
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
@@ -98,7 +98,7 @@ export class PlaybackService {
|
||||
if (previousEvent) {
|
||||
const success = PlaybackService.loadEvent(previousEvent);
|
||||
if (success) {
|
||||
logger.info('PLAYBACK', `Loaded event with ID ${previousEvent.id}`);
|
||||
logger.info(LogOrigin.Playback, `Loaded event with ID ${previousEvent.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,19 +113,19 @@ export class PlaybackService {
|
||||
if (nextEvent) {
|
||||
const success = PlaybackService.loadEvent(nextEvent);
|
||||
if (success) {
|
||||
logger.info('PLAYBACK', `Loaded event with ID ${nextEvent.id}`);
|
||||
logger.info(LogOrigin.Playback, `Loaded event with ID ${nextEvent.id}`);
|
||||
return true;
|
||||
}
|
||||
} else if (fallbackAction === 'stop') {
|
||||
logger.info('PLAYBACK', 'No next event found! Stopping playback');
|
||||
logger.info(LogOrigin.Playback, 'No next event found! Stopping playback');
|
||||
PlaybackService.stop();
|
||||
return false;
|
||||
} else if (fallbackAction === 'pause') {
|
||||
logger.info('PLAYBACK', 'No next event found! Pausing playback');
|
||||
logger.info(LogOrigin.Playback, 'No next event found! Pausing playback');
|
||||
PlaybackService.pause();
|
||||
return false;
|
||||
} else {
|
||||
logger.info('PLAYBACK', 'No next event found! Continuing playback');
|
||||
logger.info(LogOrigin.Playback, 'No next event found! Continuing playback');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -137,7 +137,7 @@ export class PlaybackService {
|
||||
if (validatePlayback(eventTimer.playback).start) {
|
||||
eventTimer.start();
|
||||
const newState = eventTimer.playback;
|
||||
logger.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
||||
logger.info(LogOrigin.Playback, `Play Mode ${newState.toUpperCase()}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ export class PlaybackService {
|
||||
if (validatePlayback(eventTimer.playback).pause) {
|
||||
eventTimer.pause();
|
||||
const newState = eventTimer.playback;
|
||||
logger.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
||||
logger.info(LogOrigin.Playback, `Play Mode ${newState.toUpperCase()}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ export class PlaybackService {
|
||||
eventLoader.reset();
|
||||
eventTimer.stop();
|
||||
const newState = eventTimer.playback;
|
||||
logger.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
||||
logger.info(LogOrigin.Playback, `Play Mode ${newState.toUpperCase()}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,14 +193,14 @@ export class PlaybackService {
|
||||
|
||||
// nothing to play
|
||||
if (rollTimers === null) {
|
||||
logger.warning('SERVER', 'Roll: no events found');
|
||||
logger.warning(LogOrigin.Server, 'Roll: no events found');
|
||||
PlaybackService.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
const { currentEvent, nextEvent } = rollTimers;
|
||||
if (!currentEvent && !nextEvent) {
|
||||
logger.warning('SERVER', 'Roll: no events found');
|
||||
logger.warning(LogOrigin.Server, 'Roll: no events found');
|
||||
PlaybackService.stop();
|
||||
return;
|
||||
}
|
||||
@@ -208,7 +208,7 @@ export class PlaybackService {
|
||||
eventTimer.roll(currentEvent, nextEvent);
|
||||
|
||||
const newState = eventTimer.playback;
|
||||
logger.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
||||
logger.info(LogOrigin.Playback, `Play Mode ${newState.toUpperCase()}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,8 +221,8 @@ export class PlaybackService {
|
||||
const delayInMs = delayTime * 1000 * 60;
|
||||
eventTimer.delay(delayInMs);
|
||||
delayInMs > 0
|
||||
? logger.info('PLAYBACK', `Added ${delayTime} min delay`)
|
||||
: logger.info('PLAYBACK', `Removed ${delayTime} min delay`);
|
||||
? logger.info(LogOrigin.Playback, `Added ${delayTime} min delay`)
|
||||
: logger.info(LogOrigin.Playback, `Removed ${delayTime} min delay`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+125
-81
@@ -1,11 +1,30 @@
|
||||
import { OntimeBaseEvent, OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||
import {
|
||||
LogOrigin,
|
||||
OntimeBaseEvent,
|
||||
OntimeBlock,
|
||||
OntimeDelay,
|
||||
OntimeEvent,
|
||||
OntimeRundown,
|
||||
SupportedEvent,
|
||||
} from 'ontime-types';
|
||||
import { generateId } from 'ontime-utils';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { block as blockDef, delay as delayDef, event as eventDef } from '../models/eventsDefinition.js';
|
||||
import { MAX_EVENTS } from '../settings.js';
|
||||
import { EventLoader, eventLoader } from '../classes/event-loader/EventLoader.js';
|
||||
import { eventTimer } from './TimerService.js';
|
||||
import { sendRefetch } from '../adapters/websocketAux.js';
|
||||
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||
import { block as blockDef, delay, delay as delayDef, event as eventDef } from '../../models/eventsDefinition.js';
|
||||
import { MAX_EVENTS } from '../../settings.js';
|
||||
import { EventLoader, eventLoader } from '../../classes/event-loader/EventLoader.js';
|
||||
import { eventTimer } from '../TimerService.js';
|
||||
import { sendRefetch } from '../../adapters/websocketAux.js';
|
||||
import { runtimeCacheStore } from '../../stores/cachingStore.js';
|
||||
import {
|
||||
cachedAdd,
|
||||
cachedDelete,
|
||||
cachedEdit,
|
||||
cachedReorder,
|
||||
calculateRuntimeDelaysFrom,
|
||||
delayedRundownCacheKey,
|
||||
getDelayedRundown,
|
||||
} from './delayedRundown.utils.js';
|
||||
import { logger } from '../../classes/Logger.js';
|
||||
|
||||
/**
|
||||
* Forces rundown to be recalculated
|
||||
@@ -14,6 +33,7 @@ import { sendRefetch } from '../adapters/websocketAux.js';
|
||||
export function forceReset() {
|
||||
eventLoader.reset();
|
||||
sendRefetch();
|
||||
runtimeCacheStore.invalidate(delayedRundownCacheKey);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,7 +93,7 @@ const isNewNext = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates timer object
|
||||
* Updates timer service when a relevant piece of data changes
|
||||
*/
|
||||
export function updateTimer(affectedIds?: string[]) {
|
||||
const runningEventId = eventLoader.loaded.selectedEventId;
|
||||
@@ -130,44 +150,54 @@ export async function addEvent(eventData: Partial<OntimeEvent> | Partial<OntimeD
|
||||
let newEvent: Partial<OntimeBaseEvent> = {};
|
||||
const id = generateId();
|
||||
|
||||
// TODO: filter the parameters that exist in the event, use the parserUtils
|
||||
switch (eventData.type) {
|
||||
case 'event':
|
||||
newEvent = { ...eventDef, ...eventData, id } as Partial<OntimeEvent>;
|
||||
case SupportedEvent.Event:
|
||||
newEvent = { ...eventDef, ...eventData, id };
|
||||
break;
|
||||
case 'delay':
|
||||
newEvent = { ...delayDef, ...eventData, id } as Partial<OntimeDelay>;
|
||||
case SupportedEvent.Delay:
|
||||
newEvent = { ...delayDef, ...eventData, id };
|
||||
break;
|
||||
case 'block':
|
||||
newEvent = { ...blockDef, ...eventData, id } as Partial<OntimeBlock>;
|
||||
case SupportedEvent.Block:
|
||||
newEvent = { ...blockDef, ...eventData, id };
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const afterId = newEvent?.after;
|
||||
if (typeof afterId === 'undefined') {
|
||||
await DataProvider.insertEventAt(newEvent, 0);
|
||||
let insertIndex = 0;
|
||||
if (typeof newEvent?.after !== 'undefined') {
|
||||
const index = DataProvider.getIndexOf(newEvent.after);
|
||||
if (index < 0) {
|
||||
logger.warning(LogOrigin.Server, `Could not find event with id ${newEvent.after}`);
|
||||
} else {
|
||||
delete newEvent.after;
|
||||
await DataProvider.insertEventAfterId(newEvent, afterId);
|
||||
insertIndex = index + 1;
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
delete newEvent.after;
|
||||
}
|
||||
|
||||
// modify rundown
|
||||
await cachedAdd(insertIndex, newEvent as OntimeEvent | OntimeDelay | OntimeBlock);
|
||||
|
||||
// notify timer service of changed events
|
||||
updateTimer([id]);
|
||||
|
||||
// notify event loader that rundown size has changed
|
||||
updateChangeNumEvents();
|
||||
|
||||
// advice socket subscribers of change
|
||||
sendRefetch();
|
||||
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
export async function editEvent(eventData) {
|
||||
const eventId = eventData.id;
|
||||
const eventInMemory = DataProvider.getEventById(eventId);
|
||||
if (typeof eventInMemory === 'undefined') {
|
||||
throw new Error('No event with ID found');
|
||||
}
|
||||
const newEvent = await DataProvider.updateEventById(eventId, eventData);
|
||||
updateTimer([eventId]);
|
||||
export async function editEvent(eventData: Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>) {
|
||||
const newEvent = await cachedEdit(eventData.id, eventData);
|
||||
|
||||
// notify timer service of changed events
|
||||
updateTimer([newEvent.id]);
|
||||
|
||||
// advice socket subscribers of change
|
||||
sendRefetch();
|
||||
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
@@ -177,9 +207,18 @@ export async function editEvent(eventData) {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function deleteEvent(eventId) {
|
||||
await DataProvider.deleteEvent(eventId);
|
||||
await cachedDelete(eventId);
|
||||
|
||||
// notify timer service of changed events
|
||||
updateTimer([eventId]);
|
||||
|
||||
// notify event loader that rundown size has changed
|
||||
updateChangeNumEvents();
|
||||
|
||||
// invalidate cache
|
||||
runtimeCacheStore.invalidate(delayedRundownCacheKey);
|
||||
|
||||
// advice socket subscribers of change
|
||||
sendRefetch();
|
||||
}
|
||||
|
||||
@@ -190,78 +229,83 @@ export async function deleteEvent(eventId) {
|
||||
export async function deleteAllEvents() {
|
||||
await DataProvider.clearRundown();
|
||||
updateTimer();
|
||||
updateChangeNumEvents();
|
||||
sendRefetch();
|
||||
forceReset();
|
||||
}
|
||||
|
||||
/**
|
||||
* reorders a given event
|
||||
* @param {string} eventId
|
||||
* @param {number} from
|
||||
* @param {number} to
|
||||
* @param {string} eventId - ID of event from, for sanity check
|
||||
* @param {number} from - index of event from
|
||||
* @param {number} to - index of event to
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function reorderEvent(eventId, from, to) {
|
||||
const rundown = DataProvider.getRundown();
|
||||
const index = rundown.findIndex((event) => event.id === eventId);
|
||||
export async function reorderEvent(eventId: string, from: number, to: number) {
|
||||
const reorderedItem = await cachedReorder(eventId, from, to);
|
||||
|
||||
if (index !== from) {
|
||||
throw new Error('ID not found at index');
|
||||
}
|
||||
const [reorderedItem] = rundown.splice(from, 1);
|
||||
|
||||
// reinsert item at to
|
||||
rundown.splice(to, 0, reorderedItem);
|
||||
|
||||
// save rundown
|
||||
await DataProvider.setRundown(rundown);
|
||||
// notify timer service of changed events
|
||||
updateTimer();
|
||||
|
||||
// advice socket subscribers of change
|
||||
sendRefetch();
|
||||
return reorderedItem;
|
||||
}
|
||||
|
||||
export function _applyDelay(
|
||||
eventId: string,
|
||||
rundown: OntimeRundown,
|
||||
): {
|
||||
delayIndex: number | null;
|
||||
updatedRundown: OntimeRundown;
|
||||
} {
|
||||
const updatedRundown = [...rundown];
|
||||
let delayIndex = null;
|
||||
let delayValue = 0;
|
||||
|
||||
for (const [index, event] of updatedRundown.entries()) {
|
||||
// look for delay
|
||||
if (delayIndex === null) {
|
||||
if (event.type === SupportedEvent.Delay && event.id === eventId) {
|
||||
delayValue = event.duration;
|
||||
delayIndex = index;
|
||||
|
||||
if (delayValue === 0) {
|
||||
// nothing to apply
|
||||
break;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// once delay is found, apply delay value to all items until block or end
|
||||
if (event.type === SupportedEvent.Event) {
|
||||
updatedRundown[index] = {
|
||||
...event,
|
||||
timeStart: Math.max(0, event.timeStart + delayValue),
|
||||
timeEnd: Math.max(event.duration, event.timeEnd + delayValue),
|
||||
revision: event.revision + 1,
|
||||
};
|
||||
} else if (event.type === SupportedEvent.Block) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { delayIndex, updatedRundown };
|
||||
}
|
||||
|
||||
/**
|
||||
* applies delay value for given event
|
||||
* @param eventId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function applyDelay(eventId: string) {
|
||||
const rundown = DataProvider.getRundown();
|
||||
let delayIndex = null;
|
||||
let delayValue = 0;
|
||||
|
||||
for (const [index, event] of rundown.entries()) {
|
||||
// look for delay
|
||||
if (delayIndex === null) {
|
||||
if (event.id === eventId && event.type === SupportedEvent.Delay) {
|
||||
delayValue = event.duration;
|
||||
delayIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
// apply delay value to all items until block or end
|
||||
else {
|
||||
if (event.type === SupportedEvent.Event) {
|
||||
event.timeStart = Math.max(0, event.timeStart + delayValue);
|
||||
event.timeEnd = Math.max(event.duration, event.timeStart + delayValue);
|
||||
event.revision += 1;
|
||||
} else if (event.type === SupportedEvent.Block) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rundown: OntimeRundown = DataProvider.getRundown();
|
||||
const { delayIndex, updatedRundown } = _applyDelay(eventId, rundown);
|
||||
if (delayIndex === null) {
|
||||
throw new Error(`Delay event with ID ${eventId} not found`);
|
||||
}
|
||||
|
||||
// delete delay
|
||||
rundown.splice(delayIndex, 1);
|
||||
|
||||
// update rundown
|
||||
await DataProvider.setRundown(rundown);
|
||||
updateTimer();
|
||||
sendRefetch();
|
||||
await DataProvider.setRundown(updatedRundown);
|
||||
await deleteEvent(eventId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -0,0 +1,319 @@
|
||||
import { EndAction, OntimeRundown, SupportedEvent, TimerType } from 'ontime-types';
|
||||
import { _applyDelay } from '../RundownService.js';
|
||||
|
||||
describe('applyDelay()', () => {
|
||||
it('applies its duration to following events', () => {
|
||||
const rundown: OntimeRundown = [
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 4,
|
||||
id: '659e1',
|
||||
},
|
||||
{
|
||||
duration: 600000,
|
||||
type: SupportedEvent.Delay,
|
||||
revision: 0,
|
||||
id: '07986',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 4,
|
||||
id: 'd48c2',
|
||||
},
|
||||
];
|
||||
|
||||
const eventId = rundown[1].id;
|
||||
const { delayIndex, updatedRundown } = _applyDelay(eventId, rundown);
|
||||
|
||||
expect(delayIndex).toBe(1);
|
||||
// we do not delay delays anymore
|
||||
expect(updatedRundown.length).toBe(3);
|
||||
expect(rundown.length).toBe(3);
|
||||
expect(updatedRundown[0].timeStart).toBe(rundown[0].timeStart);
|
||||
expect(updatedRundown[2].timeStart).toBe(rundown[1].duration + rundown[2].timeStart);
|
||||
expect(updatedRundown[2].timeEnd).toBe(rundown[1].duration + rundown[2].timeEnd);
|
||||
});
|
||||
it('stops propagating on blocks', () => {
|
||||
const rundown: OntimeRundown = [
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: '659e1',
|
||||
},
|
||||
{
|
||||
duration: 600000,
|
||||
type: SupportedEvent.Delay,
|
||||
revision: 0,
|
||||
id: '07986',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: 'd48c2',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
type: SupportedEvent.Block,
|
||||
id: '9870d',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStart: 1200000,
|
||||
timeEnd: 1800000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 2,
|
||||
id: '2f185',
|
||||
},
|
||||
];
|
||||
|
||||
const eventId = rundown[1].id;
|
||||
const { updatedRundown } = _applyDelay(eventId, rundown);
|
||||
|
||||
expect(updatedRundown[0].timeStart).toBe(rundown[0].timeStart);
|
||||
expect(updatedRundown[2].timeStart).toBe(rundown[1].duration + rundown[2].timeStart);
|
||||
expect(updatedRundown[4].timeStart).toBe(rundown[4].timeStart);
|
||||
});
|
||||
it('only applies given delay', () => {
|
||||
const rundown: OntimeRundown = [
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: '659e1',
|
||||
},
|
||||
{
|
||||
duration: 600000,
|
||||
type: SupportedEvent.Delay,
|
||||
revision: 0,
|
||||
id: '07986',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStart: 1200000,
|
||||
timeEnd: 1200000,
|
||||
duration: 0,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: '1c48f',
|
||||
},
|
||||
{
|
||||
duration: 1200000,
|
||||
type: SupportedEvent.Delay,
|
||||
revision: 0,
|
||||
id: '7db42',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: 'd48c2',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
type: SupportedEvent.Block,
|
||||
id: '9870d',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStart: 1200000,
|
||||
timeEnd: 1800000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: '2f185',
|
||||
},
|
||||
];
|
||||
|
||||
const eventId = rundown[1].id;
|
||||
const { updatedRundown } = _applyDelay(eventId, rundown);
|
||||
|
||||
expect(updatedRundown[0].timeStart).toBe(rundown[0].timeStart);
|
||||
expect(updatedRundown[2].timeStart).toBe(rundown[1].duration + rundown[2].timeStart);
|
||||
expect(updatedRundown[4].timeStart).toBe(rundown[1].duration + rundown[4].timeStart);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,445 @@
|
||||
import { EndAction, OntimeRundown, SupportedEvent, TimerType } from 'ontime-types';
|
||||
|
||||
import { calculateRuntimeDelays, calculateRuntimeDelaysFrom, getDelayAt } from '../delayedRundown.utils.js';
|
||||
|
||||
describe('calculateRuntimeDelays', () => {
|
||||
it('calculates all delays in a given rundown', () => {
|
||||
const rundown: OntimeRundown = [
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: '659e1',
|
||||
},
|
||||
{
|
||||
duration: 600000,
|
||||
type: SupportedEvent.Delay,
|
||||
revision: 0,
|
||||
id: '07986',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStart: 1200000,
|
||||
timeEnd: 1200000,
|
||||
duration: 0,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: '1c48f',
|
||||
},
|
||||
{
|
||||
duration: 1200000,
|
||||
type: SupportedEvent.Delay,
|
||||
revision: 0,
|
||||
id: '7db42',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: 'd48c2',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
type: SupportedEvent.Block,
|
||||
id: '9870d',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStart: 1200000,
|
||||
timeEnd: 1800000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: '2f185',
|
||||
},
|
||||
];
|
||||
|
||||
const updatedRundown = calculateRuntimeDelays(rundown);
|
||||
|
||||
expect(rundown.length).toBe(updatedRundown.length);
|
||||
expect(updatedRundown[0].delay).toBe(0);
|
||||
expect(updatedRundown[2].delay).toBe(600000);
|
||||
expect(updatedRundown[4].delay).toBe(600000 + 1200000);
|
||||
expect(updatedRundown[6].delay).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDelayAt()', () => {
|
||||
const delayedRundown: OntimeRundown = [
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: '659e1',
|
||||
delay: 0,
|
||||
},
|
||||
{
|
||||
duration: 600000,
|
||||
type: SupportedEvent.Delay,
|
||||
revision: 0,
|
||||
id: '07986',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStart: 1200000,
|
||||
timeEnd: 1200000,
|
||||
duration: 0,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: '1c48f',
|
||||
delay: 600000,
|
||||
},
|
||||
{
|
||||
duration: 1200000,
|
||||
type: SupportedEvent.Delay,
|
||||
revision: 0,
|
||||
id: '7db42',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: 'd48c2',
|
||||
delay: 1800000,
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
type: SupportedEvent.Block,
|
||||
id: '9870d',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStart: 1200000,
|
||||
timeEnd: 1800000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: '2f185',
|
||||
delay: 0,
|
||||
},
|
||||
];
|
||||
|
||||
it('calculates delay in a rundown', () => {
|
||||
const delayAtStart = getDelayAt(0, delayedRundown);
|
||||
const delayOnFirstEvent = getDelayAt(2, delayedRundown);
|
||||
const delayOnSecondEvent = getDelayAt(4, delayedRundown);
|
||||
const delayOnBlockedEvent = getDelayAt(0, delayedRundown);
|
||||
|
||||
expect(delayAtStart).toBe(0);
|
||||
expect(delayOnFirstEvent).toBe(600000);
|
||||
expect(delayOnSecondEvent).toBe(600000 + 1200000);
|
||||
expect(delayOnBlockedEvent).toBe(0);
|
||||
});
|
||||
it('finds delay before a delay block', () => {
|
||||
const valueOnFirstDelayBlock = getDelayAt(1, delayedRundown);
|
||||
const valueOnSecondDelayBlock = getDelayAt(3, delayedRundown);
|
||||
const valueAfterSecondDelayBlock = getDelayAt(4, delayedRundown);
|
||||
|
||||
expect(valueOnFirstDelayBlock).toBe(0);
|
||||
expect(valueOnSecondDelayBlock).toBe(600000);
|
||||
expect(valueAfterSecondDelayBlock).toBe(600000 + 1200000);
|
||||
});
|
||||
it('returns 0 after blocks', () => {
|
||||
const valueOnBlock = getDelayAt(6, delayedRundown);
|
||||
expect(valueOnBlock).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateRuntimeDelaysFrom()', () => {
|
||||
it('updates delays from given id', () => {
|
||||
const delayedRundown: OntimeRundown = [
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: '659e1',
|
||||
delay: 0,
|
||||
},
|
||||
{
|
||||
duration: 600000,
|
||||
type: SupportedEvent.Delay,
|
||||
revision: 0,
|
||||
id: '07986',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStart: 1200000,
|
||||
timeEnd: 1200000,
|
||||
duration: 0,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: '1c48f',
|
||||
delay: 0,
|
||||
},
|
||||
{
|
||||
duration: 1200000,
|
||||
type: SupportedEvent.Delay,
|
||||
revision: 0,
|
||||
id: '7db42',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: 'd48c2',
|
||||
delay: 1800000,
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
type: SupportedEvent.Block,
|
||||
id: '9870d',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStart: 1200000,
|
||||
timeEnd: 1800000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: '2f185',
|
||||
delay: 0,
|
||||
},
|
||||
];
|
||||
|
||||
const updatedRundown = calculateRuntimeDelaysFrom('07986', delayedRundown);
|
||||
|
||||
// we only update from the 4th on
|
||||
expect(updatedRundown[0].delay).toBe(0);
|
||||
// 1 + 3
|
||||
expect(updatedRundown[4].delay).toBe(600000 + 1200000);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,261 @@
|
||||
import { OntimeBlock, OntimeDelay, OntimeEvent, OntimeRundown, SupportedEvent } from 'ontime-types';
|
||||
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||
import { getCached, runtimeCacheStore } from '../../stores/cachingStore.js';
|
||||
import { isProduction } from '../../setup.js';
|
||||
import { deleteAtIndex, insertAtIndex, reorderArray } from '../../utils/arrayUtils.js';
|
||||
|
||||
/**
|
||||
* Key of rundown in cache
|
||||
*/
|
||||
export const delayedRundownCacheKey = 'delayed-rundown';
|
||||
|
||||
/**
|
||||
* Invalidates the cached rundown when an inconsistency is found
|
||||
* will throw when not in production
|
||||
* @param errorMessage
|
||||
*/
|
||||
export function invalidateFromError(errorMessage = 'Found mismatch between store and cache') {
|
||||
if (isProduction) {
|
||||
runtimeCacheStore.invalidate(delayedRundownCacheKey);
|
||||
} else {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns rundown with calculated delays
|
||||
* Ensures request goes through the caching layer
|
||||
*/
|
||||
export function getDelayedRundown(): OntimeRundown {
|
||||
function calculateRundown() {
|
||||
const rundown = DataProvider.getRundown();
|
||||
return calculateRuntimeDelays(rundown);
|
||||
}
|
||||
|
||||
return getCached(delayedRundownCacheKey, calculateRundown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event in the rundown at given index, ensuring replication to delayed rundown cache
|
||||
* @param eventIndex
|
||||
* @param event
|
||||
*/
|
||||
export async function cachedAdd(eventIndex: number, event: OntimeEvent | OntimeDelay | OntimeBlock) {
|
||||
// TODO: create wrapper function
|
||||
const rundown = DataProvider.getRundown();
|
||||
const newRundown = insertAtIndex(eventIndex, event, rundown);
|
||||
|
||||
const delayedRundown = getDelayedRundown();
|
||||
let newDelayedRundown = insertAtIndex(eventIndex, event, delayedRundown);
|
||||
|
||||
// update delay cache
|
||||
if (event.type === SupportedEvent.Event) {
|
||||
// if it is an event, we need its delay
|
||||
(newDelayedRundown[eventIndex] as OntimeEvent).delay = getDelayAt(eventIndex, newDelayedRundown);
|
||||
} else {
|
||||
// if it is a block or delay, we invalidate from here
|
||||
newDelayedRundown = calculateRuntimeDelaysFromIndex(eventIndex, newDelayedRundown);
|
||||
}
|
||||
|
||||
runtimeCacheStore.setCached(delayedRundownCacheKey, newDelayedRundown);
|
||||
// we need to delay updating this to ensure add operation happens on same dataset
|
||||
await DataProvider.setRundown(newRundown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits an event in rundown, ensuring replication to delayed rundown cache
|
||||
* @param eventId
|
||||
* @param patchObject
|
||||
*/
|
||||
export async function cachedEdit(
|
||||
eventId: string,
|
||||
patchObject: Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>,
|
||||
) {
|
||||
const indexInMemory = DataProvider.getIndexOf(eventId);
|
||||
if (indexInMemory < 0) {
|
||||
throw new Error('No event with ID found');
|
||||
}
|
||||
|
||||
const updatedRundown = DataProvider.getRundown();
|
||||
const newEvent = { ...updatedRundown[indexInMemory], ...patchObject };
|
||||
if (newEvent.type === SupportedEvent.Event) {
|
||||
newEvent.revision++;
|
||||
}
|
||||
// @ts-expect-error -- this merge is safe
|
||||
updatedRundown[indexInMemory] = newEvent;
|
||||
|
||||
let newDelayedRundown = getDelayedRundown();
|
||||
if (newDelayedRundown?.[indexInMemory].id !== newEvent.id) {
|
||||
invalidateFromError();
|
||||
} else {
|
||||
// @ts-expect-error -- this merge is safe
|
||||
newDelayedRundown[indexInMemory] = newEvent;
|
||||
if (newEvent.type === SupportedEvent.Event) {
|
||||
(newDelayedRundown[indexInMemory] as OntimeEvent).delay = getDelayAt(indexInMemory, newDelayedRundown);
|
||||
} else if (newEvent.type === SupportedEvent.Delay) {
|
||||
// blocks have no reason to change the rundown, from delays we need to recalculate
|
||||
newDelayedRundown = calculateRuntimeDelaysFromIndex(indexInMemory, newDelayedRundown);
|
||||
}
|
||||
|
||||
runtimeCacheStore.setCached(delayedRundownCacheKey, newDelayedRundown);
|
||||
}
|
||||
|
||||
// we need to delay updating this to ensure edit operation happens on same dataset
|
||||
await DataProvider.setRundown(updatedRundown);
|
||||
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an event with given id from rundown, ensuring replication to delayed rundown cache
|
||||
* @param eventId
|
||||
*/
|
||||
export async function cachedDelete(eventId: string) {
|
||||
const eventIndex = DataProvider.getIndexOf(eventId);
|
||||
let delayedRundown = getDelayedRundown();
|
||||
|
||||
if (eventIndex < 0) {
|
||||
if (delayedRundown.findIndex((event) => event.id === eventId) >= 0) {
|
||||
invalidateFromError();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let updatedRundown = DataProvider.getRundown();
|
||||
const eventType = updatedRundown[eventIndex].type;
|
||||
updatedRundown = deleteAtIndex(eventIndex, updatedRundown);
|
||||
if (eventId !== delayedRundown[eventIndex].id) {
|
||||
invalidateFromError();
|
||||
} else {
|
||||
delayedRundown = deleteAtIndex(eventIndex, delayedRundown);
|
||||
if (eventType === SupportedEvent.Delay || eventType === SupportedEvent.Block) {
|
||||
// for events, we do not have to worry
|
||||
// the following event, would have taken the place of the deleted event by now
|
||||
delayedRundown = calculateRuntimeDelaysFromIndex(eventIndex, delayedRundown);
|
||||
}
|
||||
runtimeCacheStore.setCached(delayedRundownCacheKey, delayedRundown);
|
||||
}
|
||||
// we need to delay updating this to ensure edit operation happens on same dataset
|
||||
await DataProvider.setRundown(updatedRundown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorders an event in the rundown, ensuring replication to delayed rundown cache
|
||||
* @param eventId
|
||||
* @param from
|
||||
* @param to
|
||||
*/
|
||||
export async function cachedReorder(eventId: string, from: number, to: number) {
|
||||
const indexCheck = DataProvider.getIndexOf(eventId);
|
||||
if (indexCheck !== from) {
|
||||
invalidateFromError();
|
||||
throw new Error('ID not found at index');
|
||||
}
|
||||
|
||||
let updatedRundown = DataProvider.getRundown();
|
||||
const reorderedEvent = updatedRundown[from];
|
||||
updatedRundown = reorderArray(updatedRundown, from, to);
|
||||
|
||||
const delayedRundown = getDelayedRundown();
|
||||
if (eventId !== delayedRundown[from].id) {
|
||||
invalidateFromError();
|
||||
} else {
|
||||
// TODO: could we be more granular about updates
|
||||
// I fear we need to update both from and to, which could signify more iterations
|
||||
runtimeCacheStore.invalidate(delayedRundownCacheKey);
|
||||
}
|
||||
|
||||
// we need to delay updating this to ensure edit operation happens on same dataset
|
||||
await DataProvider.setRundown(updatedRundown);
|
||||
|
||||
return reorderedEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates all delays in a given rundown
|
||||
* @param rundown
|
||||
*/
|
||||
export function calculateRuntimeDelays(rundown: OntimeRundown) {
|
||||
let accumulatedDelay = 0;
|
||||
const updatedRundown = [...rundown];
|
||||
|
||||
for (const [index, event] of updatedRundown.entries()) {
|
||||
if (event.type === SupportedEvent.Delay) {
|
||||
accumulatedDelay += event.duration;
|
||||
} else if (event.type === SupportedEvent.Block) {
|
||||
accumulatedDelay = 0;
|
||||
} else if (event.type === SupportedEvent.Event) {
|
||||
updatedRundown[index] = {
|
||||
...event,
|
||||
delay: accumulatedDelay,
|
||||
};
|
||||
}
|
||||
}
|
||||
return updatedRundown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate delays in rundown from a given index
|
||||
* @param eventIndex
|
||||
* @param rundown
|
||||
*/
|
||||
export function calculateRuntimeDelaysFromIndex(eventIndex: number, rundown: OntimeRundown) {
|
||||
if (eventIndex === -1) {
|
||||
throw new Error('ID not found at index');
|
||||
}
|
||||
|
||||
let accumulatedDelay = getDelayAt(eventIndex, rundown);
|
||||
const updatedRundown = [...rundown];
|
||||
|
||||
for (let i = eventIndex; i < rundown.length; i++) {
|
||||
const event = rundown[i];
|
||||
if (event.type === SupportedEvent.Delay) {
|
||||
accumulatedDelay += event.duration;
|
||||
} else if (event.type === SupportedEvent.Block) {
|
||||
if (i === eventIndex) {
|
||||
accumulatedDelay = 0;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else if (event.type === SupportedEvent.Event) {
|
||||
updatedRundown[i] = {
|
||||
...event,
|
||||
delay: accumulatedDelay,
|
||||
};
|
||||
}
|
||||
}
|
||||
return updatedRundown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate delays in rundown from an event with given id
|
||||
* @param eventId
|
||||
* @param rundown
|
||||
*/
|
||||
export function calculateRuntimeDelaysFrom(eventId: string, rundown: OntimeRundown) {
|
||||
const index = rundown.findIndex((event) => event.id === eventId);
|
||||
return calculateRuntimeDelaysFromIndex(index, rundown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates delay to an event at a given index
|
||||
* @param eventIndex
|
||||
* @param rundown
|
||||
*/
|
||||
export function getDelayAt(eventIndex: number, rundown: OntimeRundown): number {
|
||||
if (eventIndex < 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// we need to check the event before
|
||||
const event = rundown[eventIndex - 1];
|
||||
|
||||
if (event.type === SupportedEvent.Delay) {
|
||||
return event.duration + getDelayAt(eventIndex - 1, rundown);
|
||||
} else if (event.type === SupportedEvent.Block) {
|
||||
return 0;
|
||||
} else if (event.type === SupportedEvent.Event) {
|
||||
return event.delay ?? 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { runtimeCacheStore } from '../cachingStore.js';
|
||||
|
||||
describe('cachingStore()', () => {
|
||||
beforeEach(() => {
|
||||
runtimeCacheStore.clear(); // Clear the cache before each test
|
||||
});
|
||||
|
||||
it('should check if an item is cached', () => {
|
||||
// Add an item to the cache
|
||||
runtimeCacheStore.setCached('key', 'value');
|
||||
|
||||
// Check if the item is cached
|
||||
expect(runtimeCacheStore.checkCached('key')).toBe(true);
|
||||
expect(runtimeCacheStore.checkCached('non-existent-key')).toBe(false);
|
||||
});
|
||||
|
||||
it('should get an item from the cache', () => {
|
||||
// Add an item to the cache
|
||||
runtimeCacheStore.setCached('key', 'value');
|
||||
|
||||
// Get the item from the cache
|
||||
const result = runtimeCacheStore.getCached('key', () => 'default-value');
|
||||
|
||||
// Check the returned value
|
||||
expect(result).toBe('value');
|
||||
});
|
||||
|
||||
it('should retrieve default value when item is not cached', () => {
|
||||
// Get an item that is not in the cache
|
||||
const result = runtimeCacheStore.getCached('non-existent-key', () => 'default-value');
|
||||
|
||||
// Check the returned value
|
||||
expect(result).toBe('default-value');
|
||||
});
|
||||
|
||||
it('should set an item in the cache', () => {
|
||||
// Set an item in the cache
|
||||
runtimeCacheStore.setCached('key', 'value');
|
||||
|
||||
// Check if the item is cached
|
||||
expect(runtimeCacheStore.checkCached('key')).toBe(true);
|
||||
});
|
||||
|
||||
it('should invalidate an item in the cache', () => {
|
||||
// Add an item to the cache
|
||||
runtimeCacheStore.setCached('key', 'value');
|
||||
|
||||
// Invalidate the item
|
||||
runtimeCacheStore.invalidate('key');
|
||||
|
||||
// Check if the item is no longer cached
|
||||
expect(runtimeCacheStore.checkCached('key')).toBe(false);
|
||||
});
|
||||
|
||||
it('should clear the cache', () => {
|
||||
// Add items to the cache
|
||||
runtimeCacheStore.setCached('key1', 'value1');
|
||||
runtimeCacheStore.setCached('key2', 'value2');
|
||||
|
||||
// Clear the cache
|
||||
runtimeCacheStore.clear();
|
||||
|
||||
// Check if the cache is empty
|
||||
expect(runtimeCacheStore.checkCached('key1')).toBe(false);
|
||||
expect(runtimeCacheStore.checkCached('key2')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
interface CacheData {
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
const runtimeCache: Map<string, CacheData> = new Map();
|
||||
|
||||
export function checkCached(key: string): boolean {
|
||||
return runtimeCache.has(key);
|
||||
}
|
||||
|
||||
export function getCached<T>(key: string, callback: () => T): T {
|
||||
if (!runtimeCache.has(key)) {
|
||||
try {
|
||||
const data = callback();
|
||||
runtimeCache.set(key, { data });
|
||||
} catch (error) {
|
||||
console.log(`Failed retrieving data from callback: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
return runtimeCache.get(key).data as T;
|
||||
}
|
||||
|
||||
export function setCached<T>(key: string, value: T): T {
|
||||
runtimeCache.set(key, { data: value });
|
||||
return runtimeCache.get(key).data as T;
|
||||
}
|
||||
|
||||
export function invalidate(key) {
|
||||
runtimeCache.delete(key);
|
||||
}
|
||||
|
||||
export function clear() {
|
||||
runtimeCache.clear();
|
||||
}
|
||||
|
||||
function createCacheStore() {
|
||||
return {
|
||||
checkCached,
|
||||
getCached,
|
||||
setCached,
|
||||
invalidate,
|
||||
clear,
|
||||
};
|
||||
}
|
||||
|
||||
export const runtimeCacheStore = createCacheStore();
|
||||
@@ -0,0 +1,54 @@
|
||||
import { insertAtIndex, reorderArray } from '../arrayUtils.js';
|
||||
|
||||
describe('insertAtIndex', () => {
|
||||
it('should insert an item at the beginning of the array', () => {
|
||||
const array = [2, 3, 4];
|
||||
const result = insertAtIndex(0, 1, array);
|
||||
expect(result).toEqual([1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
it('should insert an item at the end of the array', () => {
|
||||
const array = [1, 2, 3];
|
||||
const result = insertAtIndex(3, 4, array);
|
||||
expect(result).toEqual([1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
it('should insert an item in the middle of the array', () => {
|
||||
const array = [1, 2, 4];
|
||||
const result = insertAtIndex(2, 3, array);
|
||||
expect(result).toEqual([1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
it('should return a new array and not modify the original array', () => {
|
||||
const array = [1, 2, 3];
|
||||
const result = insertAtIndex(1, 5, array);
|
||||
expect(result).toEqual([1, 5, 2, 3]);
|
||||
expect(array).toEqual([1, 2, 3]); // Original array should remain unchanged
|
||||
});
|
||||
});
|
||||
|
||||
describe('reorderArray', () => {
|
||||
it('should reorder an item in the array', () => {
|
||||
const array = ['a', 'b', 'c', 'd'];
|
||||
const result = reorderArray(array, 1, 3);
|
||||
expect(result).toEqual(['a', 'c', 'd', 'b']);
|
||||
});
|
||||
|
||||
it('should return the original array if fromIndex and toIndex are the same', () => {
|
||||
const array = ['a', 'b', 'c'];
|
||||
const result = reorderArray(array, 1, 1);
|
||||
expect(result).toEqual(array);
|
||||
});
|
||||
|
||||
it('should handle reordering to the beginning of the array', () => {
|
||||
const array = ['a', 'b', 'c'];
|
||||
const result = reorderArray(array, 2, 0);
|
||||
expect(result).toEqual(['c', 'a', 'b']);
|
||||
});
|
||||
|
||||
it('should handle reordering to the end of the array', () => {
|
||||
const array = ['a', 'b', 'c'];
|
||||
const result = reorderArray(array, 0, 2);
|
||||
expect(result).toEqual(['b', 'c', 'a']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Inserts an item in an array at a given index
|
||||
* @param index
|
||||
* @param item
|
||||
* @param array
|
||||
*/
|
||||
export function insertAtIndex<T>(index: number, item: T, array: T[]): T[] {
|
||||
const modifiedArray = [...array];
|
||||
|
||||
// Insert at beginning
|
||||
if (index === 0) {
|
||||
modifiedArray.unshift(item);
|
||||
}
|
||||
|
||||
// insert at end
|
||||
else if (index >= modifiedArray.length) {
|
||||
modifiedArray.push(item);
|
||||
}
|
||||
|
||||
// insert in the middle
|
||||
else {
|
||||
modifiedArray.splice(index, 0, item);
|
||||
}
|
||||
|
||||
return modifiedArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes array element at a given index
|
||||
* @param index
|
||||
* @param array
|
||||
*/
|
||||
export function deleteAtIndex<T>(index: number, array: T[]) {
|
||||
return array.filter((_, i) => i !== index);
|
||||
}
|
||||
|
||||
export function reorderArray<T>(array: T[], fromIndex: number, toIndex: number) {
|
||||
if (fromIndex === toIndex) {
|
||||
return array; // No change needed, return the original array
|
||||
}
|
||||
|
||||
const modifiedArray = [...array];
|
||||
|
||||
// delete in from
|
||||
const [reorderedItem] = modifiedArray.splice(fromIndex, 1);
|
||||
|
||||
// reinsert item at to
|
||||
modifiedArray.splice(toIndex, 0, reorderedItem);
|
||||
return modifiedArray;
|
||||
}
|
||||
Reference in New Issue
Block a user