refactor: remove unused and legacy code

- remove legacy migrations
- remove unused server code
- remove unused UI code
This commit is contained in:
Carlos Valente
2025-03-07 23:02:32 +01:00
parent 73afbacdd7
commit 7958d49153
49 changed files with 64 additions and 1083 deletions
+2 -6
View File
@@ -10,7 +10,6 @@ export const PROJECT_DATA = ['project'];
export const PROJECT_LIST = ['projectList'];
export const RUNDOWN = ['rundown'];
export const RUNTIME = ['runtimeStore'];
export const SHEET_STATE = ['sheetState'];
export const URL_PRESETS = ['urlpresets'];
export const VIEW_SETTINGS = ['viewSettings'];
export const CLIENT_LIST = ['clientList'];
@@ -19,11 +18,8 @@ export const REPORT = ['report'];
// API URLs
export const apiEntryUrl = `${serverURL}/data`;
export const projectDataURL = `${serverURL}/project`;
export const rundownURL = `${serverURL}/events`;
export const ontimeURL = `${serverURL}/ontime`;
const userAssetsPath = 'user';
const cssOverridePath = 'styles/override.css';
export const userAssetsPath = 'user';
export const cssOverridePath = 'styles/override.css';
export const overrideStylesURL = `${serverURL}/${userAssetsPath}/${cssOverridePath}`;
export const projectLogoPath = `${serverURL}/${userAssetsPath}/logo`;
@@ -23,7 +23,7 @@ export type OptionWithoutGroup = {
withDivider?: boolean;
};
export type OptionWithGroup = {
type OptionWithGroup = {
label: string;
group: Omit<OptionWithoutGroup, 'isGroup'>[];
};
@@ -3,8 +3,8 @@ import React from 'react';
// skipcq: JS-C1003 - sentry does not expose itself as an ES Module.
import * as Sentry from '@sentry/react';
import { runtimeStore } from '@/common/stores/runtime';
import { hasConnected, reconnectAttempts, shouldReconnect } from '@/common/utils/socket';
import { hasConnected, reconnectAttempts } from '../../../common/utils/socket';
import { runtimeStore } from '../../stores/runtime';
import style from './ErrorBoundary.module.scss';
@@ -37,7 +37,7 @@ class ErrorBoundary extends React.Component {
scope.setExtras({
error,
store: appState,
hasSocket: { hasConnected, shouldReconnect, reconnectAttempts },
hasSocket: { hasConnected, reconnectAttempts },
});
const eventId = Sentry.captureException(error);
this.setState({ eventId, info });
@@ -1,45 +0,0 @@
$loader-size: 4rem;
.overlay {
position: absolute;
z-index: 10;
width: 100%;
height: 100%;
display: grid;
place-content: center;
background-color: $black-10;
backdrop-filter: blur(5px);
}
.loader {
width: $loader-size;
height: $loader-size;
background: $blue-500;
display: inline-block;
border-radius: 50%;
box-sizing: border-box;
animation: animloader 1s ease-in infinite;
}
@keyframes animloader {
0% {
transform: scale(0);
opacity: 0.6;
}
100% {
transform: scale(1);
opacity: 0;
}
}
@keyframes animloader {
0% {
transform: scale(0);
opacity: 0.6;
}
100% {
transform: scale(1);
opacity: 0;
}
}
@@ -1,9 +0,0 @@
import style from './LoaderOverlay.module.scss';
export default function LoaderOverlay() {
return (
<div className={style.overlay}>
<span className={style.loader} />
</div>
);
}
@@ -1,16 +0,0 @@
import { memo } from 'react';
import NavigationMenu from './NavigationMenu';
interface ProductionNavigationMenuProps {
isMenuOpen: boolean;
onMenuClose: () => void;
}
function ProductionNavigationMenu(props: ProductionNavigationMenuProps) {
const { isMenuOpen, onMenuClose } = props;
return <NavigationMenu isOpen={isMenuOpen} onClose={onMenuClose} />;
}
export default memo(ProductionNavigationMenu);
@@ -12,7 +12,7 @@ type OptionsField = {
defaultValue?: string;
};
export type MultiselectOption = { value: string; label: string; colour: string };
type MultiselectOption = { value: string; label: string; colour: string };
export type MultiselectOptions = Record<string, MultiselectOption>;
type MultiOptionsField = {
type: 'multi-option';
@@ -1,32 +0,0 @@
// roughly from https://github.com/juliencrn/usehooks-ts/blob/master/packages/usehooks-ts/src/useMediaQuery/useMediaQuery.ts
import { useCallback, useEffect, useState } from 'react';
function getMatches(query: string): boolean {
return window.matchMedia(query).matches;
}
// TODO: debounce handleChange
export default function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState<boolean>(getMatches(query));
const handleChange = useCallback(() => {
setMatches(getMatches(query));
}, [query]);
useEffect(() => {
const matchMedia = window.matchMedia(query);
// Triggered at the first client-side load and if query changes
handleChange();
// Listen matchMedia
matchMedia.addEventListener('change', handleChange);
return () => {
matchMedia.removeEventListener('change', handleChange);
};
}, [handleChange, query]);
return matches;
}
@@ -1,11 +1,9 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { editAutomationSettings, getAutomationSettings } from '../api/automation';
import { getAutomationSettings } from '../api/automation';
import { AUTOMATION } from '../api/constants';
import { logAxiosError } from '../api/utils';
import { automationPlaceholderSettings } from '../models/AutomationSettings';
import { ontimeQueryClient } from '../queryClient';
export default function useAutomationSettings() {
const { data, status, isFetching, isError, refetch } = useQuery({
@@ -20,15 +18,3 @@ export default function useAutomationSettings() {
return { data: data ?? automationPlaceholderSettings, status, isFetching, isError, refetch };
}
export function useAutomationSettingsMutation() {
const { isPending, mutateAsync } = useMutation({
mutationFn: editAutomationSettings,
onError: (error) => logAxiosError('Error saving Automation settings', error),
onSuccess: (data) => {
ontimeQueryClient.setQueryData(AUTOMATION, data);
},
onSettled: () => ontimeQueryClient.invalidateQueries({ queryKey: AUTOMATION }),
});
return { isPending, mutateAsync };
}
@@ -11,7 +11,7 @@ const placeholderProjectList: ProjectFileListResponse = {
lastLoadedProject: '',
};
export function useProjectList() {
function useProjectList() {
const { data, status, refetch } = useQuery({
queryKey: PROJECT_LIST,
queryFn: getProjects,
@@ -12,7 +12,7 @@ type noop = (this: any, ...args: any[]) => any;
type PickFunction<T extends noop> = (this: ThisParameterType<T>, ...args: Parameters<T>) => ReturnType<T>;
export const isFunction = (value: unknown): value is (...args: any) => any => typeof value === 'function';
const isFunction = (value: unknown): value is (...args: any) => any => typeof value === 'function';
export default function useMemoisedFn<T extends noop>(fn: T) {
if (isDev) {
-10
View File
@@ -91,14 +91,6 @@ export const setPlayback = {
},
};
export const useInfoPanel = createSelector((state: RuntimeStore) => ({
eventNow: state.eventNow,
eventNext: state.eventNext,
playback: state.timer.playback,
selectedEventIndex: state.runtime.selectedEventIndex,
numEvents: state.runtime.numEvents,
}));
export const useAuxTimerTime = createSelector((state: RuntimeStore) => state.auxtimer1.current);
export const useAuxTimerControl = createSelector((state: RuntimeStore) => ({
@@ -145,8 +137,6 @@ export const useProgressData = createSelector((state: RuntimeStore) => ({
timeDanger: state.eventNow?.timeDanger ?? null,
}));
export const setClientName = (newName: string) => socketSendJson('set-client-name', newName);
export const useRuntimeOverview = createSelector((state: RuntimeStore) => ({
plannedStart: state.runtime.plannedStart,
actualStart: state.runtime.actualStart,
+1 -1
View File
@@ -11,7 +11,7 @@ type LogStore = {
logs: Log[];
};
export const logger = createStore<LogStore>(() => ({
const logger = createStore<LogStore>(() => ({
logs: [],
}));
+9 -16
View File
@@ -16,11 +16,10 @@ import { addDialog } from '../stores/dialogStore';
import { addLog } from '../stores/logger';
import { addToBatchUpdates, flushBatchUpdates, patchRuntime, patchRuntimeProperty } from '../stores/runtime';
export let websocket: WebSocket | null = null;
let websocket: WebSocket | null = null;
let reconnectTimeout: NodeJS.Timeout | null = null;
const reconnectInterval = 1000;
export let shouldReconnect = true;
export let hasConnected = false;
export let reconnectAttempts = 0;
@@ -50,15 +49,14 @@ export const connectSocket = () => {
console.warn('WebSocket disconnected');
setOnlineStatus(false);
if (shouldReconnect) {
reconnectTimeout = setTimeout(() => {
console.warn('WebSocket: attempting reconnect');
if (websocket && websocket.readyState === WebSocket.CLOSED) {
reconnectAttempts += 1;
connectSocket();
}
}, reconnectInterval);
}
// we decide to allows reconnect
reconnectTimeout = setTimeout(() => {
console.warn('WebSocket: attempting reconnect');
if (websocket && websocket.readyState === WebSocket.CLOSED) {
reconnectAttempts += 1;
connectSocket();
}
}, reconnectInterval);
};
websocket.onerror = (error) => {
@@ -224,11 +222,6 @@ export const connectSocket = () => {
};
};
export const disconnectSocket = () => {
shouldReconnect = false;
websocket?.close();
};
export const socketSend = (message: any) => {
if (websocket && websocket.readyState === WebSocket.OPEN) {
websocket.send(message);
+1 -1
View File
@@ -35,7 +35,7 @@ function getFormatFromParams() {
* Gets the format options from the applicaton settings
* @returns a string equivalent to the format, ie: hh:mm:ss a or HH:mm:ss
*/
export function getFormatFromSettings(): TimeFormat {
function getFormatFromSettings(): TimeFormat {
const settings: Settings | undefined = ontimeQueryClient.getQueryData(APP_SETTINGS);
return settings?.timeFormat ?? '24';
}
-11
View File
@@ -1,11 +0,0 @@
import { TestingLibraryMatchers } from '@testing-library/jest-dom/matchers';
import 'vitest';
// ugly hack because vite and pnpm are not playing ball with jest
// https://github.com/testing-library/jest-dom/issues/123
declare global {
namespace Vi {
type Assertion<T = any> = TestingLibraryMatchers<T, void>;
}
}
@@ -13,10 +13,6 @@ import * as Panel from '../../panel-utils/PanelUtils';
import GeneralPinInput from './GeneralPinInput';
export type GeneralPanelFormValues = {
filename: string;
};
export default function GeneralPanelForm() {
const { data, status, refetch } = useSettings();
const {
@@ -3,7 +3,7 @@ import { ImportCustom, ImportMap } from 'ontime-utils';
export type NamedImportMap = typeof namedImportMap;
// Record of label and import name
export const namedImportMap = {
const namedImportMap = {
Worksheet: 'event schedule',
Start: 'time start',
'Link start': 'link start',
@@ -16,8 +16,6 @@ import EventEditorEmpty from './EventEditorEmpty';
import style from './EventEditor.module.scss';
export type EventEditorSubmitActions = keyof OntimeEvent;
export type EditorUpdateFields = 'cue' | 'title' | 'note' | 'colour' | CustomFieldLabel;
interface EventEditorProps {
@@ -6,7 +6,7 @@ import { RUNDOWN } from '../../common/api/constants';
import { ontimeQueryClient } from '../../common/queryClient';
import { isMacOS } from '../../common/utils/deviceUtils';
export type SelectionMode = 'shift' | 'click' | 'ctrl';
type SelectionMode = 'shift' | 'click' | 'ctrl';
interface EventSelectionStore {
selectedEvents: Set<string>;
@@ -1,15 +0,0 @@
// used in both sm and public views
export const titleVariants = {
hidden: {
x: -1500,
},
visible: {
x: 0,
transition: {
duration: 1,
},
},
exit: {
x: -1500,
},
};
@@ -32,7 +32,7 @@ interface TranslationContextValue {
getLocalizedString: (key: keyof typeof langEn, lang?: string) => string;
}
export const TranslationContext = createContext<TranslationContextValue>({
const TranslationContext = createContext<TranslationContextValue>({
getLocalizedString: () => '',
});
@@ -12,7 +12,7 @@ interface SectionProps {
export default memo(Section);
export function Section(props: SectionProps) {
function Section(props: SectionProps) {
const { category, content, title, status } = props;
const sectionClasses = cx(['section', category === 'now' && 'section--now']);
@@ -11,7 +11,6 @@ import {
MILLIS_PER_HOUR,
} from 'ontime-utils';
import { clamp } from '../../common/utils/math';
import { formatDuration } from '../../common/utils/time';
import { isStringBoolean } from '../../features/viewers/common/viewUtils';
@@ -22,13 +21,6 @@ type CSSPosition = {
width: number;
};
/**
* Calculates the position (in %) of an element relative to a schedule
*/
export function getRelativePositionX(scheduleStart: number, scheduleEnd: number, now: number): number {
return clamp(((now - scheduleStart) / (scheduleEnd - scheduleStart)) * 100, 0, 100);
}
/**
* Calculates an absolute position of an element based on a schedule
*/
+1 -1
View File
@@ -29,7 +29,7 @@ import { authenticateSocket } from '../middleware/authenticate.js';
let instance: SocketServer | null = null;
export class SocketServer implements IAdapter {
class SocketServer implements IAdapter {
private readonly MAX_PAYLOAD = 1024 * 256; // 256Kb
private wss: WebSocketServer | null;
@@ -12,5 +12,3 @@ export const router = express.Router();
router.post('/upload', uploadExcel, validateFileExists, postExcel);
router.get('/worksheets', getWorksheets);
router.post('/preview', validateImportMapOptions, previewExcel);
// TODO: validate import map
@@ -1,11 +1,4 @@
import {
ErrorResponse,
MessageResponse,
OntimeRundown,
OntimeRundownEntry,
RundownCached,
RundownPaginated,
} from 'ontime-types';
import { ErrorResponse, MessageResponse, OntimeRundown, OntimeRundownEntry, RundownCached } from 'ontime-types';
import { getErrorMessage } from 'ontime-utils';
import type { Request, Response } from 'express';
@@ -21,12 +14,7 @@ import {
reorderEvent,
swapEvents,
} from '../../services/rundown-service/RundownService.js';
import {
getEventWithId,
getNormalisedRundown,
getPaginated,
getRundown,
} from '../../services/rundown-service/rundownUtils.js';
import { getEventWithId, getNormalisedRundown, getRundown } from '../../services/rundown-service/rundownUtils.js';
export async function rundownGetAll(_req: Request, res: Response<OntimeRundown>) {
const rundown = getRundown();
@@ -55,34 +43,6 @@ export async function rundownGetById(req: Request, res: Response<OntimeRundownEn
}
}
export async function rundownGetPaginated(req: Request, res: Response<RundownPaginated | ErrorResponse>) {
const { limit, offset } = req.query;
if (limit == null && offset == null) {
return res.json({
rundown: getRundown(),
total: getRundown().length,
});
}
try {
let parsedOffset = Number(offset);
if (Number.isNaN(parsedOffset)) {
parsedOffset = 0;
}
let parsedLimit = Number(limit);
if (Number.isNaN(parsedLimit)) {
parsedLimit = Infinity;
}
const paginatedRundown = getPaginated(parsedOffset, parsedLimit);
res.status(200).json(paginatedRundown);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).json({ message });
}
}
export async function rundownPost(req: Request, res: Response<OntimeRundownEntry | ErrorResponse>) {
if (failEmptyObjects(req.body, res)) {
return;
@@ -8,7 +8,6 @@ import {
rundownGetAll,
rundownGetById,
rundownGetNormalised,
rundownGetPaginated,
rundownPost,
rundownPut,
rundownReorder,
@@ -18,7 +17,6 @@ import {
paramsMustHaveEventId,
rundownArrayOfIds,
rundownBatchPutValidator,
rundownGetPaginatedQueryParams,
rundownPostValidator,
rundownPutValidator,
rundownReorderValidator,
@@ -28,7 +26,6 @@ import {
export const router = express.Router();
router.get('/', rundownGetAll); // not used in Ontime frontend
router.get('/paginated', rundownGetPaginatedQueryParams, rundownGetPaginated); // not used in Ontime frontend
router.get('/normalised', rundownGetNormalised);
router.get('/:eventId', paramsMustHaveEventId, rundownGetById); // not used in Ontime frontend
@@ -1,4 +1,4 @@
import { body, param, query, validationResult } from 'express-validator';
import { body, param, validationResult } from 'express-validator';
import type { Request, Response, NextFunction } from 'express';
export const rundownPostValidator = [
@@ -77,14 +77,3 @@ export const rundownArrayOfIds = [
next();
},
];
export const rundownGetPaginatedQueryParams = [
query('offset').isNumeric().optional(),
query('limit').isNumeric().optional(),
(req: Request, res: Response, next: NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];
@@ -1,36 +0,0 @@
import { handleLegacyMessageConversion } from '../integration.legacy.js';
describe('handleLegacyConversion', () => {
it('should return the payload as is if it is not a legacy message', () => {
expect(handleLegacyMessageConversion({})).toEqual({});
const newPayload = {
timer: {
text: 'text',
visible: true,
blink: true,
blackout: true,
},
external: 'text',
};
expect(handleLegacyMessageConversion(newPayload)).toEqual(newPayload);
});
it('should convert a legacy payload with external message', () => {
expect(handleLegacyMessageConversion({ external: { text: 'text', visible: true } })).toEqual({
external: 'text',
timer: {
secondarySource: 'external',
},
});
expect(handleLegacyMessageConversion({ external: { visible: true } })).toEqual({
timer: {
secondarySource: 'external',
},
});
expect(handleLegacyMessageConversion({ external: { text: 'text' } })).toEqual({
external: 'text',
});
});
});
@@ -16,8 +16,6 @@ import { socket } from '../adapters/WebsocketAdapter.js';
import { throttle } from '../utils/throttle.js';
import { willCauseRegeneration } from '../services/rundown-service/rundownCacheUtils.js';
import { handleLegacyMessageConversion } from './integration.legacy.js';
const throttledUpdateEvent = throttle(updateEvent, 20);
let lastRequest: Date | null = null;
@@ -89,12 +87,9 @@ const actionHandlers: Record<string, ActionHandler> = {
message: (payload) => {
assert.isObject(payload);
// TODO: remove this once we feel its been enough time, ontime 3.6.0, 20/09/2024
const migratedPayload = handleLegacyMessageConversion(payload);
const patch: DeepPartial<MessageState> = {
timer: 'timer' in migratedPayload ? validateTimerMessage(migratedPayload.timer) : undefined,
external: 'external' in migratedPayload ? validateMessage(migratedPayload.external) : undefined,
timer: 'timer' in payload ? validateTimerMessage(payload.timer) : undefined,
external: 'external' in payload ? validateMessage(payload.external) : undefined,
};
const newMessage = messageService.patch(patch);
@@ -1,67 +0,0 @@
import { MessageState } from 'ontime-types';
import { DeepPartial } from 'ts-essentials';
export type LegacyMessageState = DeepPartial<{
timer: {
text: string;
visible: boolean;
blink: boolean;
blackout: boolean;
};
external: {
text: string;
visible: boolean;
};
}>;
function isLegacyMessageState(value: object): value is LegacyMessageState {
// @ts-expect-error -- good enough here
return value?.external?.text !== undefined || value?.external?.visible !== undefined;
}
/**
* This function is used to maintain support for legacy data in the /message endpoint
* The previous message endpoint expected a patch of the message state
* @example {
* timer: { blink: boolean, blackout: boolean, text: string, visible: boolean },
* external: { visible: boolean, text: string }
* }
*
* This change is introduced in version 3.6.0
*/
export function handleLegacyMessageConversion(payload: object): object | Partial<MessageState> {
// if it is not a legacy message, we pass it as is
if (!isLegacyMessageState(payload)) {
return payload;
}
/**
* The current migration only needs to handle the cases
* for the deprecated external message controls
*/
// Migrate external message
// 2.1 the user gives us the text and a visible flag
if (payload?.external?.text !== undefined && payload.external.visible !== undefined) {
return {
timer: { secondarySource: payload.external.visible ? 'external' : null },
external: payload.external.text,
} as Partial<MessageState>;
}
// 2.2 the user gives us the text
else if (payload?.external?.text !== undefined) {
return {
external: payload.external.text,
} as Partial<MessageState>;
}
// 2.3 the user gives us the visible flag
else if (payload?.external?.visible !== undefined) {
return {
timer: { secondarySource: payload.external.visible ? 'external' : null },
} as Partial<MessageState>;
}
// there should be no case for us to reach this since
// the type guard would have ensured one of the above states
return payload;
}
+1 -1
View File
@@ -245,7 +245,7 @@ export const startIntegrations = async () => {
* @param {number} exitCode
* @return {Promise<void>}
*/
export const shutdown = async (exitCode = 0) => {
const shutdown = async (exitCode = 0) => {
consoleHighlight(`Ontime shutting down with code ${exitCode}`);
// clear the restore file if it was a normal exit
+2 -2
View File
@@ -1,9 +1,9 @@
import { Log, LogLevel } from 'ontime-types';
import { generateId, millisToString } from 'ontime-utils';
import { clock } from '../services/Clock.js';
import { socket } from '../adapters/WebsocketAdapter.js';
import { consoleSubdued, consoleError } from '../utils/console.js';
import { timeNow } from '../utils/time.js';
import { isProduction } from '../externals.js';
class Logger {
@@ -75,7 +75,7 @@ class Logger {
level,
origin,
text,
time: millisToString(clock.getSystemTime() || 0),
time: millisToString(timeNow()),
};
this._push(log);
}
-60
View File
@@ -1,60 +0,0 @@
enum Source {
System = 'system',
MIDI = 'MIDI',
}
/**
* Service manages retrieving current time from a managed time source
*/
class Clock {
private static instance: Clock;
private readonly source: Source;
constructor(source?: Source) {
if (Clock.instance) {
return Clock.instance;
}
Clock.instance = this;
this.source = source || Source.System;
}
/**
* Get current time from source
*/
timeNow(): number {
switch (this.source) {
case Source.System:
return this.getSystemTime();
case Source.MIDI:
// @ts-expect-error -- not implemented
return this.getMidiTime();
default:
throw new Error('Invalid time source');
}
}
/**
* Get current time from system
*/
getSystemTime() {
const now = new Date();
// extract milliseconds since midnight
let elapsed = now.getHours() * 3600000;
elapsed += now.getMinutes() * 60000;
elapsed += now.getSeconds() * 1000;
elapsed += now.getMilliseconds();
return elapsed;
}
/**
* Get current time from MIDI
*/
getMidiTime() {
throw new Error('Not implemented');
}
}
export const clock = new Clock();
@@ -4,8 +4,8 @@ import { SimpleTimer } from '../../classes/simple-timer/SimpleTimer.js';
import { eventStore } from '../../stores/EventStore.js';
import { timerConfig } from '../../config/config.js';
export type EmitFn = (state: SimpleTimerState) => void;
export type GetTimeFn = () => number;
type EmitFn = (state: SimpleTimerState) => void;
type GetTimeFn = () => number;
export class AuxTimerService {
private timer: SimpleTimer;
@@ -1,6 +1,5 @@
import {
CustomFields,
EndAction,
EventCustomFields,
OntimeBlock,
OntimeDelay,
@@ -8,11 +7,9 @@ import {
OntimeRundown,
SupportedEvent,
TimeStrategy,
TimerType,
} from 'ontime-types';
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, dayInMs } from 'ontime-utils';
import { calculateRuntimeDelays, getDelayAt, calculateRuntimeDelaysFrom } from '../delayUtils.js';
import {
add,
batchEdit,
@@ -557,410 +554,6 @@ describe('swap() mutation', () => {
});
});
describe('calculateRuntimeDelays', () => {
it('calculates all delays in a given rundown', () => {
const rundown: OntimeRundown = [
{
title: '',
note: '',
endAction: EndAction.None,
timerType: TimerType.CountDown,
countToEnd: false,
timeStrategy: TimeStrategy.LockEnd,
linkStart: null,
timeStart: 600000,
timeEnd: 1200000,
duration: 600000,
isPublic: true,
skip: false,
colour: '',
type: SupportedEvent.Event,
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: '659e1',
cue: '1',
custom: {},
},
{
duration: 600000,
type: SupportedEvent.Delay,
id: '07986',
},
{
title: '',
note: '',
endAction: EndAction.None,
timerType: TimerType.CountDown,
countToEnd: false,
timeStrategy: TimeStrategy.LockEnd,
linkStart: null,
timeStart: 1200000,
timeEnd: 1200000,
duration: 0,
isPublic: true,
skip: false,
colour: '',
type: SupportedEvent.Event,
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: '1c48f',
cue: '2',
custom: {},
},
{
duration: 1200000,
type: SupportedEvent.Delay,
id: '7db42',
},
{
title: '',
note: '',
endAction: EndAction.None,
timerType: TimerType.CountDown,
countToEnd: false,
timeStrategy: TimeStrategy.LockEnd,
linkStart: null,
timeStart: 600000,
timeEnd: 1200000,
duration: 600000,
isPublic: true,
skip: false,
colour: '',
type: SupportedEvent.Event,
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: 'd48c2',
cue: '3',
custom: {},
},
{
title: '',
type: SupportedEvent.Block,
id: '9870d',
},
{
title: '',
note: '',
endAction: EndAction.None,
timerType: TimerType.CountDown,
countToEnd: false,
timeStrategy: TimeStrategy.LockEnd,
linkStart: null,
timeStart: 1200000,
timeEnd: 1800000,
duration: 600000,
isPublic: true,
skip: false,
colour: '',
type: SupportedEvent.Event,
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: '2f185',
cue: '4',
custom: {},
},
];
const updatedRundown = calculateRuntimeDelays(rundown);
expect(rundown.length).toBe(updatedRundown.length);
expect((updatedRundown[0] as OntimeEvent).delay).toBe(0);
expect((updatedRundown[2] as OntimeEvent).delay).toBe(600000);
expect((updatedRundown[4] as OntimeEvent).delay).toBe(600000 + 1200000);
expect((updatedRundown[6] as OntimeEvent).delay).toBe(0);
});
});
describe('getDelayAt()', () => {
const delayedRundown: OntimeRundown = [
{
title: '',
note: '',
endAction: EndAction.None,
timerType: TimerType.CountDown,
countToEnd: false,
timeStrategy: TimeStrategy.LockEnd,
linkStart: null,
timeStart: 600000,
timeEnd: 1200000,
duration: 600000,
isPublic: true,
skip: false,
colour: '',
type: SupportedEvent.Event,
revision: 0,
timeWarning: 120000,
timeDanger: 60000,
id: '659e1',
delay: 0,
dayOffset: 0,
gap: 0,
cue: '1',
custom: {},
},
{
duration: 600000,
type: SupportedEvent.Delay,
id: '07986',
},
{
title: '',
note: '',
endAction: EndAction.None,
timerType: TimerType.CountDown,
countToEnd: false,
timeStrategy: TimeStrategy.LockEnd,
linkStart: null,
timeStart: 1200000,
timeEnd: 1200000,
duration: 0,
isPublic: true,
skip: false,
colour: '',
type: SupportedEvent.Event,
revision: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: '1c48f',
delay: 600000,
cue: '2',
custom: {},
},
{
duration: 1200000,
type: SupportedEvent.Delay,
id: '7db42',
},
{
title: '',
note: '',
endAction: EndAction.None,
timerType: TimerType.CountDown,
countToEnd: false,
timeStrategy: TimeStrategy.LockEnd,
linkStart: null,
timeStart: 600000,
timeEnd: 1200000,
duration: 600000,
isPublic: true,
skip: false,
colour: '',
type: SupportedEvent.Event,
revision: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: 'd48c2',
delay: 1800000,
cue: '3',
custom: {},
},
{
title: '',
type: SupportedEvent.Block,
id: '9870d',
},
{
title: '',
note: '',
endAction: EndAction.None,
timerType: TimerType.CountDown,
countToEnd: false,
timeStrategy: TimeStrategy.LockEnd,
linkStart: null,
timeStart: 1200000,
timeEnd: 1800000,
duration: 600000,
isPublic: true,
skip: false,
colour: '',
type: SupportedEvent.Event,
revision: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: '2f185',
delay: 0,
cue: '4',
custom: {},
},
];
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: '',
note: '',
endAction: EndAction.None,
timerType: TimerType.CountDown,
countToEnd: false,
timeStrategy: TimeStrategy.LockEnd,
linkStart: null,
timeStart: 600000,
timeEnd: 1200000,
duration: 600000,
isPublic: true,
skip: false,
colour: '',
type: SupportedEvent.Event,
revision: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: '659e1',
delay: 0,
cue: '1',
custom: {},
},
{
duration: 600000,
type: SupportedEvent.Delay,
id: '07986',
},
{
title: '',
note: '',
endAction: EndAction.None,
timerType: TimerType.CountDown,
countToEnd: false,
timeStrategy: TimeStrategy.LockEnd,
linkStart: null,
timeStart: 1200000,
timeEnd: 1200000,
duration: 0,
isPublic: true,
skip: false,
colour: '',
type: SupportedEvent.Event,
revision: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: '1c48f',
delay: 0,
cue: '2',
custom: {},
},
{
duration: 1200000,
type: SupportedEvent.Delay,
id: '7db42',
},
{
title: '',
note: '',
endAction: EndAction.None,
timerType: TimerType.CountDown,
countToEnd: false,
timeStrategy: TimeStrategy.LockEnd,
linkStart: null,
timeStart: 600000,
timeEnd: 1200000,
duration: 600000,
isPublic: true,
skip: false,
colour: '',
type: SupportedEvent.Event,
revision: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: 'd48c2',
delay: 1800000,
cue: '3',
custom: {},
},
{
title: '',
type: SupportedEvent.Block,
id: '9870d',
},
{
title: '',
note: '',
endAction: EndAction.None,
timerType: TimerType.CountDown,
countToEnd: false,
timeStrategy: TimeStrategy.LockEnd,
linkStart: null,
timeStart: 1200000,
timeEnd: 1800000,
duration: 600000,
isPublic: true,
skip: false,
colour: '',
type: SupportedEvent.Event,
revision: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: '2f185',
delay: 0,
cue: '4',
custom: {},
},
];
const updatedRundown = calculateRuntimeDelaysFrom('07986', delayedRundown);
// we only update from the 4th on
expect((updatedRundown[0] as OntimeEvent).delay).toBe(0);
// 1 + 3
expect((updatedRundown[4] as OntimeEvent).delay).toBe(600000 + 1200000);
});
});
describe('custom fields', () => {
describe('createCustomField()', () => {
it('creates a field from given parameters', () => {
@@ -1,39 +0,0 @@
import { OntimeRundown } from 'ontime-types';
import { getPaginated } from '../rundownUtils.js';
describe('getPaginated', () => {
// mock cache so we dont run data functions
beforeAll(() => {
vi.mock('../rundownCache.js', () => ({}));
});
// @ts-expect-error -- we know this is not correct, but good enough for the test
const getData = () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as OntimeRundown;
it('should return the correct paginated rundown', () => {
const offset = 0;
const limit = 1;
const result = getPaginated(offset, limit, getData);
expect(result.rundown).toHaveLength(1);
expect(result.total).toBe(10);
});
it('should handle overflows', () => {
const offset = 0;
const limit = 20;
const result = getPaginated(offset, limit, getData);
expect(result.rundown).toHaveLength(10);
expect(result.total).toBe(10);
});
it('should handle out of range', () => {
const offset = 11;
const limit = Infinity;
const result = getPaginated(offset, limit, getData);
expect(result.rundown).toHaveLength(0);
expect(result.total).toBe(10);
});
});
@@ -1,94 +1,6 @@
import { OntimeRundown, isOntimeDelay, isOntimeBlock, isOntimeEvent, OntimeEvent } from 'ontime-types';
import { OntimeRundown, isOntimeDelay, isOntimeEvent, OntimeEvent } from 'ontime-types';
import { deleteAtIndex } from 'ontime-utils';
/**
* 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 (isOntimeDelay(event)) {
accumulatedDelay += event.duration;
} else if (isOntimeBlock(event)) {
accumulatedDelay = 0;
} else if (isOntimeEvent(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 (isOntimeDelay(event)) {
accumulatedDelay += event.duration;
} else if (isOntimeBlock(event)) {
if (i === eventIndex) {
accumulatedDelay = 0;
} else {
break;
}
} else if (isOntimeEvent(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 (isOntimeDelay(event)) {
return event.duration + getDelayAt(eventIndex - 1, rundown);
} else if (isOntimeBlock(event)) {
return 0;
} else if (isOntimeEvent(event)) {
return event.delay ?? 0;
}
return 0;
}
/**
* Applies delay from given event ID, deletes the delay event after
* @throws {Error} if event ID not found or is not a delay
@@ -105,7 +105,7 @@ export function handleCustomField(
}
/** List of event properties which do not need the rundown to be regenerated */
export enum regenerateWhitelist {
enum RegenerateWhitelist {
'id',
'cue',
'title',
@@ -125,7 +125,7 @@ export enum regenerateWhitelist {
* @param path
*/
export function isDataStale(patch: Partial<OntimeRundownEntry>): boolean {
return Object.keys(patch).some((key) => !(key in regenerateWhitelist));
return Object.keys(patch).some((key) => !(key in RegenerateWhitelist));
}
/**
@@ -133,7 +133,7 @@ export function isDataStale(patch: Partial<OntimeRundownEntry>): boolean {
* @param path
*/
export function willCauseRegeneration(key: keyof OntimeEvent): boolean {
return !(key in regenerateWhitelist);
return !(key in RegenerateWhitelist);
}
/**
@@ -101,19 +101,3 @@ export function findNext(currentEventId?: string): PlayableEvent | null {
const nextEvent = playableEvents.at(newIndex);
return nextEvent ?? null;
}
/**
* Returns a paginated rundown
* Exposes a getter function for the rundown for testing
*/
export function getPaginated(
offset: number,
limit: number,
source = getRundown,
): { rundown: OntimeRundownEntry[]; total: number } {
const rundown = source();
return {
rundown: rundown.slice(Math.min(offset, rundown.length), Math.min(offset + limit, rundown.length)),
total: rundown.length,
};
}
+7 -7
View File
@@ -21,7 +21,7 @@ import {
isPlaybackActive,
} from 'ontime-utils';
import { clock } from '../services/Clock.js';
import { timeNow } from '../utils/time.js';
import type { RestorePoint } from '../services/RestoreService.js';
import {
getCurrent,
@@ -52,7 +52,7 @@ export type RuntimeState = {
};
const runtimeState: RuntimeState = {
clock: clock.timeNow(),
clock: timeNow(),
currentBlock: { ...runtimeStorePlaceholder.currentBlock },
eventNow: null,
publicEventNow: null,
@@ -98,7 +98,7 @@ export function clear() {
runtimeState.runtime.selectedEventIndex = null;
runtimeState.timer.playback = Playback.Stop;
runtimeState.clock = clock.timeNow();
runtimeState.clock = timeNow();
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
// when clearing, we maintain the total delay from the rundown
@@ -357,7 +357,7 @@ export function start(state: RuntimeState = runtimeState): boolean {
if (state.timer.playback === Playback.Play) {
return false;
}
state.clock = clock.timeNow();
state.clock = timeNow();
state.timer.secondaryTimer = null;
// add paused time if it exists
@@ -400,7 +400,7 @@ export function pause(state: RuntimeState = runtimeState): boolean {
}
state.timer.playback = Playback.Pause;
state.clock = clock.timeNow();
state.clock = timeNow();
state._timer.pausedAt = state.clock;
return true;
}
@@ -438,7 +438,7 @@ export function addTime(amount: number) {
if (willGoNegative && !hasFinished) {
// set finished time so side effects are triggered
runtimeState._timer.forceFinish = clock.timeNow();
runtimeState._timer.forceFinish = timeNow();
} else {
const willGoPositive = runtimeState.timer.current < 0 && runtimeState.timer.current + amount > 0;
if (willGoPositive) {
@@ -466,7 +466,7 @@ export type UpdateResult = {
export function update(): UpdateResult {
// 0. there are some things we always do
const previousClock = runtimeState.clock;
runtimeState.clock = clock.timeNow(); // we update the clock on every update call
runtimeState.clock = timeNow(); // we update the clock on every update call
// 1. is playback idle?
if (!isPlaybackActive(runtimeState.timer.playback)) {
@@ -1,13 +1,10 @@
import {
CustomFields,
DatabaseModel,
EndAction,
OntimeEvent,
OntimeRundown,
Settings,
SupportedEvent,
TimeStrategy,
TimerType,
URLPreset,
} from 'ontime-types';
@@ -360,46 +357,3 @@ describe('parseRundown() linking', () => {
});
});
});
describe('parseRundown() migrations', () => {
const legacyEvent = {
id: '1',
type: SupportedEvent.Event,
cue: '',
title: '',
note: '',
endAction: EndAction.None,
timerType: 'time-to-end',
linkStart: null,
timeStrategy: TimeStrategy.LockDuration,
timeStart: 0,
timeEnd: 0,
duration: 0,
isPublic: false,
skip: false,
colour: '',
revision: 0,
timeWarning: 120000,
timeDanger: 60000,
custom: {},
};
it('migrates an event with time-to-end', () => {
const result = parseRundown({ rundown: [legacyEvent] as OntimeRundown });
expect(result.rundown[0]).toMatchObject({
id: '1',
timerType: TimerType.CountDown,
countToEnd: true,
});
});
it('migrates an event without time-to-end', () => {
const countdownEvent = { ...legacyEvent, timerType: TimerType.CountDown };
const result = parseRundown({ rundown: [countdownEvent] as OntimeRundown });
expect(result.rundown[0]).toMatchObject({
id: '1',
timerType: TimerType.CountDown,
countToEnd: false,
});
});
});
+1 -1
View File
@@ -303,7 +303,7 @@ export const parseExcel = (
};
};
export type ParsingError = {
type ParsingError = {
context: string;
message: string;
};
+1 -21
View File
@@ -8,7 +8,6 @@ import {
OntimeRundown,
ProjectData,
Settings,
TimerType,
URLPreset,
ViewSettings,
isOntimeBlock,
@@ -53,7 +52,7 @@ export function parseRundown(
let newEvent: OntimeEvent | OntimeDelay | OntimeBlock | null;
if (isOntimeEvent(event)) {
const maybeEvent = runEventMigrations({ ...event, id });
const maybeEvent = { ...event, id };
if (event.linkStart) {
maybeEvent.linkStart = previousId;
@@ -246,22 +245,3 @@ export function sanitiseCustomFields(data: object): CustomFields {
return newCustomFields;
}
/**
* Time to end was moved from a TimerType to a standalone boolean named count to end
* Released as part of v3.10.0
*/
function migrateTimeToEnd(event: any): OntimeEvent {
if (event.timerType === 'time-to-end') {
event.timerType = TimerType.CountDown;
event.countToEnd = true;
}
return event;
}
/**
* Mutating function migrates event data entries
*/
function runEventMigrations(event: any): OntimeEvent {
return migrateTimeToEnd(event);
}
@@ -1,9 +0,0 @@
import { parse } from 'path';
/**
* @description Takes a filename and removes the extension
* @param {string} filename - filename with extension
*/
export const removeFileExtension = (filename: string): string => {
return parse(filename).name;
};
+14
View File
@@ -60,3 +60,17 @@ export function getTimezoneLabel(date: Date): string {
return `GMT ${sign}${pad(hours)}:${pad(minutes)} ${tzName}`;
}
/**
* Get current time from system
*/
export function timeNow() {
const now = new Date();
// extract milliseconds since midnight
let elapsed = now.getHours() * 3600000;
elapsed += now.getMinutes() * 60000;
elapsed += now.getSeconds() * 1000;
elapsed += now.getMilliseconds();
return elapsed;
}
@@ -1,4 +1,3 @@
import type { OntimeRundown } from '../../definitions/core/Rundown.type.js';
import type { Playback } from '../../definitions/runtime/Playback.type.js';
import type { MaybeString } from '../../utils/utils.type.js';
@@ -51,8 +50,3 @@ export type ProjectLogoResponse = {
export type ErrorResponse = MessageResponse;
export type AuthenticationStatus = 'authenticated' | 'not_authenticated' | 'pending';
export type RundownPaginated = {
rundown: OntimeRundown;
total: number;
};
-1
View File
@@ -67,7 +67,6 @@ export type {
ErrorResponse,
ProjectFileListResponse,
MessageResponse,
RundownPaginated,
SessionStats,
ProjectLogoResponse,
} from './api/ontime-controller/BackendResponse.type.js';