Compare commits

..

1 Commits

Author SHA1 Message Date
alex-arc 96c8228a19 refactor: use branded types in runtimestate 2026-07-29 23:15:08 +02:00
25 changed files with 542 additions and 650 deletions
-1
View File
@@ -15,7 +15,6 @@ export const RUNDOWN = ['rundown'];
export const CURRENT_RUNDOWN_QUERY_KEY = ['rundown', 'current']; export const CURRENT_RUNDOWN_QUERY_KEY = ['rundown', 'current'];
export const getRundownQueryKey = (rundownId: string) => ['rundown', rundownId]; export const getRundownQueryKey = (rundownId: string) => ['rundown', rundownId];
export const RUNTIME = ['runtimeStore']; export const RUNTIME = ['runtimeStore'];
export const SESSION_STATS = ['sessionStats'];
export const URL_PRESETS = ['urlpresets']; export const URL_PRESETS = ['urlpresets'];
export const VIEW_SETTINGS = ['viewSettings']; export const VIEW_SETTINGS = ['viewSettings'];
export const CSS_OVERRIDE = ['cssOverride']; export const CSS_OVERRIDE = ['cssOverride'];
+1 -9
View File
@@ -1,19 +1,11 @@
import axios from 'axios'; import axios from 'axios';
import { GetInfo, LinkOptions, SessionStats } from 'ontime-types'; import { GetInfo, LinkOptions } from 'ontime-types';
import { apiEntryUrl } from './constants'; import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions'; import type { RequestOptions } from './requestOptions';
const sessionPath = `${apiEntryUrl}/session`; const sessionPath = `${apiEntryUrl}/session`;
/**
* HTTP request to retrieve statistics of the running session
*/
export async function getSessionStats(options?: RequestOptions): Promise<SessionStats> {
const res = await axios.get(sessionPath, { signal: options?.signal });
return res.data;
}
/** /**
* HTTP request to retrieve application info * HTTP request to retrieve application info
*/ */
@@ -13,9 +13,6 @@ import Select from '../select/Select';
import style from './RedirectClientModal.module.scss'; import style from './RedirectClientModal.module.scss';
/** the first navigator view, used as the initial value of the view select */
const defaultView = `/${navigatorConstants[0].url}`;
interface RedirectClientModalProps { interface RedirectClientModalProps {
id: string; id: string;
name: string; name: string;
@@ -28,7 +25,7 @@ interface RedirectClientModalProps {
export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onClose }: RedirectClientModalProps) { export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onClose }: RedirectClientModalProps) {
const { data } = useUrlPresets(); const { data } = useUrlPresets();
const [path, setPath] = useState(currentPath); const [path, setPath] = useState(currentPath);
const [selected, setSelected] = useState(defaultView); const [selected, setSelected] = useState('/');
const { setRedirect } = setClientRemote; const { setRedirect } = setClientRemote;
@@ -100,18 +97,19 @@ export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onC
<Select <Select
fluid fluid
options={viewOptions} options={viewOptions}
defaultValue={defaultView} defaultValue={viewOptions[0].value}
onValueChange={(value) => { onValueChange={(value) => {
if (value === null) return; if (value === null) return;
setSelected(value); setSelected(value);
}} }}
disabled={enabledPresets.length === 0}
/> />
</label> </label>
<Button <Button
variant='primary' variant='primary'
aria-label='Redirect to preset' aria-label='Redirect to preset'
className={style.redirect} className={style.redirect}
disabled={selected === currentPath} disabled={enabledPresets.length === 0 || selected === '/'}
onClick={() => handleRedirect(selected)} onClick={() => handleRedirect(selected)}
> >
Redirect <IoArrowForward /> Redirect <IoArrowForward />
@@ -2,7 +2,6 @@ import { useState } from 'react';
import Button from '../../../common/components/buttons/Button'; import Button from '../../../common/components/buttons/Button';
import { setClientRemote } from '../../hooks/useSocket'; import { setClientRemote } from '../../hooks/useSocket';
import { isKeyEnter } from '../../utils/keyEvent';
import Dialog from '../dialog/Dialog'; import Dialog from '../dialog/Dialog';
import Input from '../input/input/Input'; import Input from '../input/input/Input';
@@ -34,18 +33,7 @@ export function RenameClientModal({ id, name: currentName = '', isOpen, onClose
showCloseButton showCloseButton
onClose={onClose} onClose={onClose}
bodyElements={ bodyElements={
<Input <Input height='large' placeholder='New name' value={name} onChange={(event) => setName(event.target.value)} />
height='large'
placeholder='New name'
autoFocus
value={name}
onChange={(event) => setName(event.target.value)}
onKeyDown={(event) => {
if (isKeyEnter(event) && canSubmit) {
handleRename();
}
}}
/>
} }
footerElements={ footerElements={
<> <>
@@ -1,17 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { SessionStats } from 'ontime-types';
import { queryRefetchInterval } from '../../ontimeConfig';
import { SESSION_STATS } from '../api/constants';
import { getSessionStats } from '../api/session';
export default function useSessionStats() {
const { data, status, isError, refetch } = useQuery<SessionStats>({
queryKey: SESSION_STATS,
queryFn: ({ signal }) => getSessionStats({ signal }),
placeholderData: (previousData, _previousQuery) => previousData,
refetchInterval: queryRefetchInterval,
});
return { data, status, isError, refetch };
}
+1 -4
View File
@@ -15,14 +15,11 @@ const logger = createStore<LogStore>(() => ({
logs: [], logs: [],
})); }));
/** the log is kept in memory, we cap it to avoid it growing through a long show */
const maxLogEntries = 500;
export const useLogData = () => useStore(logger); export const useLogData = () => useStore(logger);
export const addLog = (log: Log) => export const addLog = (log: Log) =>
logger.setState((state) => ({ logger.setState((state) => ({
logs: [log, ...state.logs].slice(0, maxLogEntries), logs: [log, ...state.logs],
})); }));
export const clearLogs = () => logger.setState({ logs: [] }); export const clearLogs = () => logger.setState({ logs: [] });
@@ -117,10 +117,6 @@ $inner-padding: 1rem;
td[data-warn='true'] { td[data-warn='true'] {
background-color: $orange-1300; background-color: $orange-1300;
} }
tr[data-highlight='true'] {
background-color: $blue-1100;
}
} }
.listGroup { .listGroup {
@@ -19,12 +19,10 @@ export default function InfoNif() {
// interfaces outside localhost wont have access // interfaces outside localhost wont have access
if (nif.name === 'localhost' && !isLocalhost) return null; if (nif.name === 'localhost' && !isLocalhost) return null;
const address = linkToOtherHost(nif.address); const address = linkToOtherHost(nif.address);
// we show the address as it would be typed in a browser, including the port
const label = new URL(address).host;
return ( return (
<CopyTag key={`${nif.name}-${nif.address}`} copyValue={address} onClick={() => handleClick(address)}> <CopyTag key={nif.name} copyValue={address} onClick={() => handleClick(address)}>
{`${nif.name} - ${label}`} <IoArrowUp className={style.goIcon} /> {`${nif.name} - ${nif.address}`} <IoArrowUp className={style.goIcon} />
</CopyTag> </CopyTag>
); );
})} })}
@@ -19,13 +19,10 @@ export default function LogExport() {
<Panel.SubHeader> <Panel.SubHeader>
Event log Event log
<Button onClick={extract}> <Button onClick={extract}>
Open in new window <IoArrowUp className={style.iconRotate} /> Extract <IoArrowUp className={style.iconRotate} />
</Button> </Button>
</Panel.SubHeader> </Panel.SubHeader>
<Panel.Divider /> <Panel.Divider />
<Panel.Paragraph>
Activity in this session. The log is not saved with the project and is cleared when Ontime restarts.
</Panel.Paragraph>
<Log /> <Log />
</Panel.Card> </Panel.Card>
</Panel.Section> </Panel.Section>
@@ -1,38 +1,26 @@
import { MessageTag } from 'ontime-types';
import { useEffect } from 'react';
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView'; import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
import { isOntimeCloud } from '../../../../externals'; import { usePing } from '../../../../common/hooks/useSocket';
import { sendSocket } from '../../../../common/utils/socket';
import { isDocker } from '../../../../externals';
import type { PanelBaseProps } from '../../panel-list/PanelList'; import type { PanelBaseProps } from '../../panel-list/PanelList';
import * as Panel from '../../panel-utils/PanelUtils'; import * as Panel from '../../panel-utils/PanelUtils';
import ClientControlPanel from './client-control/ClientControlPanel'; import ClientControlPanel from './client-control/ClientControlPanel';
import InfoNif from './NetworkInterfaces';
import LogExport from './NetworkLogExport'; import LogExport from './NetworkLogExport';
import NetworkStatus from './NetworkStatus';
export default function NetworkLogPanel({ location }: PanelBaseProps) { export default function NetworkLogPanel({ location }: PanelBaseProps) {
const statusRef = useScrollIntoView<HTMLDivElement>('status', location);
const interfacesRef = useScrollIntoView<HTMLDivElement>('interfaces', location);
const clientsRef = useScrollIntoView<HTMLDivElement>('clients', location); const clientsRef = useScrollIntoView<HTMLDivElement>('clients', location);
const logRef = useScrollIntoView<HTMLDivElement>('log', location); const logRef = useScrollIntoView<HTMLDivElement>('log', location);
return ( return (
<> <>
<Panel.Header>Network</Panel.Header> <Panel.Header>Network</Panel.Header>
<div ref={statusRef}> {isDocker && (
<NetworkStatus /> <Panel.Section>
</div> <OntimeCloudStats />
{!isOntimeCloud && ( </Panel.Section>
<div ref={interfacesRef}>
<Panel.Section>
<Panel.Card>
<Panel.SubHeader>Network interfaces</Panel.SubHeader>
<Panel.Divider />
<Panel.Paragraph>
Addresses which can be used to reach Ontime from other devices in the network. <br />
Click an address to open it in a new window, or use the icon to copy it to the clipboard.
</Panel.Paragraph>
<InfoNif />
</Panel.Card>
</Panel.Section>
</div>
)} )}
<div ref={logRef}> <div ref={logRef}>
<LogExport /> <LogExport />
@@ -43,3 +31,29 @@ export default function NetworkLogPanel({ location }: PanelBaseProps) {
</> </>
); );
} }
function OntimeCloudStats() {
const ping = usePing();
/**
* Send immediate ping request, and keep sending on an interval
*/
useEffect(() => {
sendSocket(MessageTag.Ping, new Date());
const doPing = setInterval(() => {
sendSocket(MessageTag.Ping, new Date());
}, 5000);
return () => {
clearInterval(doPing);
};
}, []);
return (
<Panel.SubHeader>
Ontime cloud
<Panel.Description>Current ping: {ping}ms</Panel.Description>
</Panel.SubHeader>
);
}
@@ -1,109 +0,0 @@
import { MaybeString, MessageTag } from 'ontime-types';
import { MILLIS_PER_MINUTE } from 'ontime-utils';
import { useEffect } from 'react';
import Tag from '../../../../common/components/tag/Tag';
import useSessionStats from '../../../../common/hooks-query/useSessionStats';
import { usePing } from '../../../../common/hooks/useSocket';
import { useClientStore } from '../../../../common/stores/clientStore';
import { formatDuration } from '../../../../common/utils/time';
import { sendSocket } from '../../../../common/utils/socket';
import { isDocker } from '../../../../externals';
import * as Panel from '../../panel-utils/PanelUtils';
/** ping values above this are flagged to the user (ms) */
const slowPingThreshold = 100;
/** how often we ask the server for a ping (ms) */
const pingInterval = 5000;
/**
* Presents a date as the time elapsed since it happened
*/
function timeSince(date: MaybeString): string {
if (date === null) {
return 'Never';
}
const elapsed = Date.now() - new Date(date).getTime();
if (elapsed < MILLIS_PER_MINUTE) {
return 'Just now';
}
return `${formatDuration(elapsed)} ago`;
}
export default function NetworkStatus() {
const { data, status, isError } = useSessionStats();
// the client list is kept up to date over websocket, unlike the polled session stats
const clients = useClientStore((store) => store.clients);
return (
<Panel.Section>
<Panel.Card>
<Panel.SubHeader>Server status</Panel.SubHeader>
<Panel.Loader isLoading={status === 'pending'} />
{isError && <Panel.Error>Failed to load session data</Panel.Error>}
<Panel.Divider />
<Panel.ListGroup>
<Panel.ListItem>
<Panel.Field title='Connected clients' description='Clients currently connected to this server' />
<Tag>{Object.keys(clients).length}</Tag>
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field title='Server running for' description='Time since the Ontime server was started' />
<Tag>{data ? formatDuration(Date.now() - new Date(data.startedAt).getTime()) : '...'}</Tag>
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field title='Last client connection' description='When a client last connected to the server' />
<Tag>{data ? timeSince(data.lastConnection) : '...'}</Tag>
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field
title='Last integration request'
description='When the server last received a request from an integration'
/>
<Tag>{data ? timeSince(data.lastRequest) : '...'}</Tag>
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field title='Server time zone' description='Time zone used by the machine running Ontime' />
<Tag>{data ? data.timezone : '...'}</Tag>
</Panel.ListItem>
{isDocker && <CloudPing />}
</Panel.ListGroup>
</Panel.Card>
</Panel.Section>
);
}
/**
* Ontime cloud users are not in the same network as the server
* we show the round trip time to help them qualify the connection
*/
function CloudPing() {
const ping = usePing();
/**
* Send immediate ping request, and keep sending on an interval
*/
useEffect(() => {
sendSocket(MessageTag.Ping, new Date());
const doPing = setInterval(() => {
sendSocket(MessageTag.Ping, new Date());
}, pingInterval);
return () => {
clearInterval(doPing);
};
}, []);
return (
<Panel.ListItem>
<Panel.Field
title='Ontime cloud latency'
description='Time for a message to travel to the server and back. Lower is better'
/>
<Tag variant={ping > slowPingThreshold ? 'warning' : 'default'}>{ping}ms</Tag>
</Panel.ListItem>
);
}
@@ -1,3 +1,11 @@
.fullWidth {
width: 100%;
}
.halfWidth {
width: 50%;
}
.halfWidthNoWrap { .halfWidthNoWrap {
width: 50%; width: 50%;
white-space: nowrap; white-space: nowrap;
@@ -8,11 +16,6 @@
user-select: text; user-select: text;
} }
.muted { .self {
color: $label-gray; background-color: $blue-1100;
}
// align section notes with the table contents
.note {
padding: 0 2rem;
} }
@@ -5,42 +5,14 @@ import { useState } from 'react';
import Button from '../../../../../common/components/buttons/Button'; import Button from '../../../../../common/components/buttons/Button';
import { RedirectClientModal } from '../../../../../common/components/client-modal/RedirectClientModal'; import { RedirectClientModal } from '../../../../../common/components/client-modal/RedirectClientModal';
import { RenameClientModal } from '../../../../../common/components/client-modal/RenameClientModal'; import { RenameClientModal } from '../../../../../common/components/client-modal/RenameClientModal';
import CopyTag from '../../../../../common/components/copy-tag/CopyTag';
import Tag from '../../../../../common/components/tag/Tag'; import Tag from '../../../../../common/components/tag/Tag';
import Tooltip from '../../../../../common/components/tooltip/Tooltip';
import { setClientRemote } from '../../../../../common/hooks/useSocket'; import { setClientRemote } from '../../../../../common/hooks/useSocket';
import { useClientStore } from '../../../../../common/stores/clientStore'; import { useClientStore } from '../../../../../common/stores/clientStore';
import { openLink } from '../../../../../common/utils/linkUtils'; import { cx } from '../../../../../common/utils/styleUtils';
import * as Panel from '../../../panel-utils/PanelUtils'; import * as Panel from '../../../panel-utils/PanelUtils';
import style from './ClientControlPanel.module.scss'; import style from './ClientControlPanel.module.scss';
/**
* Shows the path a client is showing
* The full URL is available for copying or opening in a new window
*/
function ClientPath({ origin, path }: { origin: string; path: string }) {
// clients report their path on connection, we may not have it yet
if (!path) {
return <span className={style.muted}>unknown</span>;
}
// origin is empty for clients which have not reported their location
if (!origin) {
return <span className={style.copiable}>{path}</span>;
}
const fullUrl = `${origin}${path}`;
return (
<Tooltip text={fullUrl} render={<span />}>
<CopyTag copyValue={fullUrl} size='small' onClick={() => openLink(fullUrl)}>
{path}
</CopyTag>
</Tooltip>
);
}
export default function ClientList() { export default function ClientList() {
const id = useClientStore((store) => store.id); const id = useClientStore((store) => store.id);
const clients = useClientStore((store) => store.clients); const clients = useClientStore((store) => store.clients);
@@ -60,19 +32,8 @@ export default function ClientList() {
redirectHandler.open(); redirectHandler.open();
}; };
/** const ontimeClients = Object.entries(clients).filter(([_, { type }]) => type === 'ontime');
* Clients are given by the server in connection order, which means rows move const otherClients = Object.entries(clients).filter(([_, { type }]) => type !== 'ontime');
* as clients come and go. We keep a stable order: self first, then by name
*/
const sortClients = (clientEntries: [string, Client][]) =>
clientEntries.sort(([keyA, clientA], [keyB, clientB]) => {
if (keyA === id) return -1;
if (keyB === id) return 1;
return clientA.name.localeCompare(clientB.name);
});
const ontimeClients = sortClients(Object.entries(clients).filter(([_, { type }]) => type === 'ontime'));
const otherClients = sortClients(Object.entries(clients).filter(([_, { type }]) => type !== 'ontime'));
const targetClient: Client | undefined = clients[targetId]; const targetClient: Client | undefined = clients[targetId];
@@ -98,37 +59,29 @@ export default function ClientList() {
)} )}
<Panel.Section> <Panel.Section>
<Panel.Title>Ontime Clients ({ontimeClients.length})</Panel.Title> <Panel.Title>Ontime Clients ({ontimeClients.length})</Panel.Title>
<div className={style.note}>
<Panel.Description>
Ontime views connected to this server. Identify shows a marker in the client screen until you turn it off,
redirect sends the client to a different view.
</Panel.Description>
</div>
<Panel.Table> <Panel.Table>
<thead> <thead>
<tr> <tr>
<th style={{ width: '20%' }}>Client Name</th> <td style={{ width: '20%' }}>Client Name</td>
<th>Path</th> <td>Path</td>
<th /> <td />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ontimeClients.length === 0 && <Panel.TableEmpty label='No Ontime clients connected' />}
{ontimeClients.map(([key, client]) => { {ontimeClients.map(([key, client]) => {
const { identify, name, path } = client; const { identify, name, path } = client;
const isCurrent = id === key; const isCurrent = id === key;
return ( return (
<tr key={key} data-highlight={isCurrent}> <tr key={key} className={cx([isCurrent && style.self])}>
<Panel.InlineElements relation='inner' as='td'> <Panel.InlineElements relation='inner' as='td'>
{isCurrent && <Tag>SELF</Tag>} {isCurrent && <Tag>SELF</Tag>}
{name} {name}
</Panel.InlineElements> </Panel.InlineElements>
<td> <td className={style.copiable}>{path}</td>
<ClientPath origin={client.origin} path={path} />
</td>
<Panel.InlineElements relation='inner' as='td'> <Panel.InlineElements relation='inner' as='td'>
<Button <Button
size='small' size='small'
className={`${identify ? style.blink : ''}`}
disabled={isCurrent} disabled={isCurrent}
variant={identify ? 'primary' : 'subtle'} variant={identify ? 'primary' : 'subtle'}
data-testid={isCurrent ? '' : 'not-self-identify'} data-testid={isCurrent ? '' : 'not-self-identify'}
@@ -164,21 +117,14 @@ export default function ClientList() {
<Panel.Divider /> <Panel.Divider />
<Panel.Section> <Panel.Section>
<Panel.Title>Other Clients ({otherClients.length})</Panel.Title> <Panel.Title>Other Clients ({otherClients.length})</Panel.Title>
<div className={style.note}>
<Panel.Description>
Connections which are not Ontime views, such as integrations and custom clients. These cannot be controlled
from Ontime.
</Panel.Description>
</div>
<Panel.Table> <Panel.Table>
<thead> <thead>
<tr> <tr>
<th className={style.halfWidthNoWrap}>Client Name</th> <td className={style.halfWidthNoWrap}>Client Name</td>
<th className={style.halfWidthNoWrap}>Client type</th> <td className={style.halfWidthNoWrap}>Client type</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{otherClients.length === 0 && <Panel.TableEmpty label='No other clients connected' />}
{otherClients.map(([key, client]) => { {otherClients.map(([key, client]) => {
const { name, type } = client; const { name, type } = client;
@@ -67,14 +67,6 @@ const staticOptions = [
id: 'network', id: 'network',
label: 'Network', label: 'Network',
secondary: [ secondary: [
{
id: 'network__status',
label: 'Server status',
},
{
id: 'network__interfaces',
label: 'Network interfaces',
},
{ {
id: 'network__log', id: 'network__log',
label: 'Event log', label: 'Event log',
@@ -51,13 +51,3 @@ $info-hover: $section-white;
flex-wrap: wrap; flex-wrap: wrap;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
} }
.count {
color: $info-gray;
white-space: nowrap;
}
.empty {
color: $info-gray;
padding: 0.5rem 0;
}
+86 -64
View File
@@ -1,90 +1,115 @@
import { LogLevel, LogOrigin } from 'ontime-types'; import { LogOrigin } from 'ontime-types';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { IoClose } from 'react-icons/io5'; import { IoClose } from 'react-icons/io5';
import Button from '../../common/components/buttons/Button'; import Button from '../../common/components/buttons/Button';
import Input from '../../common/components/input/input/Input';
import { clearLogs, useLogData } from '../../common/stores/logger'; import { clearLogs, useLogData } from '../../common/stores/logger';
import * as Panel from '../app-settings/panel-utils/PanelUtils'; import * as Panel from '../app-settings/panel-utils/PanelUtils';
import style from './Log.module.scss'; import style from './Log.module.scss';
const allOrigins = [
LogOrigin.User,
LogOrigin.Client,
LogOrigin.Server,
LogOrigin.Playback,
LogOrigin.Rx,
LogOrigin.Tx,
];
export default function Log() { export default function Log() {
const { logs: logData } = useLogData(); const { logs: logData } = useLogData();
// log entries are not guaranteed to have an origin from the enum const [showClient, setShowClient] = useState(true);
const [origins, setOrigins] = useState<Set<string>>(() => new Set<string>(allOrigins)); const [showServer, setShowServer] = useState(true);
const [onlyProblems, setOnlyProblems] = useState(false); const [showRx, setShowRx] = useState(true);
const [search, setSearch] = useState(''); const [showTx, setShowTx] = useState(true);
const [showPlayback, setShowPlayback] = useState(true);
const [showUser, setShowUser] = useState(true);
const toggleOrigin = useCallback((origin: LogOrigin) => { const matchers: LogOrigin[] = [];
setOrigins((previous) => { if (showUser) {
const newOrigins = new Set(previous); matchers.push(LogOrigin.User);
if (newOrigins.has(origin)) { }
newOrigins.delete(origin); if (showClient) {
} else { matchers.push(LogOrigin.Client);
newOrigins.add(origin); }
} if (showServer) {
return newOrigins; matchers.push(LogOrigin.Server);
}); }
if (showRx) {
matchers.push(LogOrigin.Rx);
}
if (showTx) {
matchers.push(LogOrigin.Tx);
}
if (showPlayback) {
matchers.push(LogOrigin.Playback);
}
const filteredData = logData.filter((entry) => matchers.some((match) => entry.origin === match));
const disableOthers = useCallback((toEnable: LogOrigin) => {
setShowUser(toEnable === LogOrigin.User);
setShowClient(toEnable === LogOrigin.Client);
setShowServer(toEnable === LogOrigin.Server);
setShowRx(toEnable === LogOrigin.Rx);
setShowTx(toEnable === LogOrigin.Tx);
setShowPlayback(toEnable === LogOrigin.Playback);
}, []); }, []);
/** middle click on an origin shows only that origin */
const showOnlyOrigin = useCallback((origin: LogOrigin) => {
setOrigins(new Set([origin]));
}, []);
const searchTerm = search.toLowerCase();
const filteredData = logData.filter((entry) => {
if (!origins.has(entry.origin)) return false;
if (onlyProblems && entry.level === LogLevel.Info) return false;
if (searchTerm && !entry.text.toLowerCase().includes(searchTerm)) return false;
return true;
});
const isFiltered = filteredData.length !== logData.length;
return ( return (
<> <>
<Panel.InlineElements className={style.buttonBar}> <Panel.InlineElements className={style.buttonBar}>
{allOrigins.map((origin) => (
<Button
key={origin}
variant={origins.has(origin) ? 'primary' : 'subtle'}
size='small'
onClick={() => toggleOrigin(origin)}
onAuxClick={() => showOnlyOrigin(origin)}
onContextMenu={(event) => event.preventDefault()}
>
{origin}
</Button>
))}
<Button <Button
variant={onlyProblems ? 'primary' : 'subtle'} variant={showUser ? 'primary' : 'subtle'}
size='small' size='small'
onClick={() => setOnlyProblems((previous) => !previous)} onClick={() => setShowUser((s) => !s)}
onAuxClick={() => disableOthers(LogOrigin.User)}
onContextMenu={(e) => e.preventDefault()}
> >
Issues only {LogOrigin.User}
</Button>
<Button
variant={showClient ? 'primary' : 'subtle'}
size='small'
onClick={() => setShowClient((s) => !s)}
onAuxClick={() => disableOthers(LogOrigin.Client)}
onContextMenu={(e) => e.preventDefault()}
>
{LogOrigin.Client}
</Button>
<Button
variant={showServer ? 'primary' : 'subtle'}
size='small'
onClick={() => setShowServer((s) => !s)}
onAuxClick={() => disableOthers(LogOrigin.Server)}
onContextMenu={(e) => e.preventDefault()}
>
{LogOrigin.Server}
</Button>
<Button
variant={showPlayback ? 'primary' : 'subtle'}
size='small'
onClick={() => setShowPlayback((s) => !s)}
onAuxClick={() => disableOthers(LogOrigin.Playback)}
onContextMenu={(e) => e.preventDefault()}
>
{LogOrigin.Playback}
</Button>
<Button
variant={showRx ? 'primary' : 'subtle'}
size='small'
onClick={() => setShowRx((s) => !s)}
onAuxClick={() => disableOthers(LogOrigin.Rx)}
onContextMenu={(e) => e.preventDefault()}
>
{LogOrigin.Rx}
</Button>
<Button
variant={showTx ? 'primary' : 'subtle'}
size='small'
onClick={() => setShowTx((s) => !s)}
onAuxClick={() => disableOthers(LogOrigin.Tx)}
onContextMenu={(e) => e.preventDefault()}
>
{LogOrigin.Tx}
</Button> </Button>
<Button variant='subtle-destructive' size='small' onClick={clearLogs} className={style.apart}> <Button variant='subtle-destructive' size='small' onClick={clearLogs} className={style.apart}>
<IoClose /> Clear <IoClose /> Clear
</Button> </Button>
</Panel.InlineElements> </Panel.InlineElements>
<Panel.InlineElements className={style.buttonBar}>
<Input fluid placeholder='Filter messages' value={search} onChange={(event) => setSearch(event.target.value)} />
<span className={style.count}>
{isFiltered ? `${filteredData.length} of ${logData.length}` : logData.length} entries
</span>
</Panel.InlineElements>
<ul className={style.log}> <ul className={style.log}>
{filteredData.map((logEntry) => ( {filteredData.map((logEntry) => (
<li key={logEntry.id} className={`${style.logEntry} ${style[logEntry.level]} `}> <li key={logEntry.id} className={`${style.logEntry} ${style[logEntry.level]} `}>
@@ -93,9 +118,6 @@ export default function Log() {
<span className={style.msg}>{logEntry.text}</span> <span className={style.msg}>{logEntry.text}</span>
</li> </li>
))} ))}
{filteredData.length === 0 && (
<li className={style.empty}>{logData.length === 0 ? 'No activity yet' : 'No entries match the filters'}</li>
)}
</ul> </ul>
</> </>
); );
+17 -1
View File
@@ -1,5 +1,5 @@
import { Day, Duration, Instant, TimeOfDay } from 'ontime-types'; import { Day, Duration, Instant, TimeOfDay } from 'ontime-types';
import { MILLIS_PER_MINUTE, dayInMs } from 'ontime-utils'; import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND, dayInMs } from 'ontime-utils';
/** Returns the current instant */ /** Returns the current instant */
export function now(): Instant { export function now(): Instant {
@@ -59,6 +59,10 @@ export function addDuration(instant: Instant, duration: Duration | Duration[]):
return (instant + totalDuration) as Instant; return (instant + totalDuration) as Instant;
} }
export function combineValues<T extends Duration | Day>(...duration: T[]): T {
return Array.isArray(duration) ? (duration.reduce<number>((total, current) => total + current, 0) as T) : duration;
}
/** /**
* Calculates elapsed time on the clock from a starting time to the current time * Calculates elapsed time on the clock from a starting time to the current time
* Handles overnight crossing (when current < start, assumes we've crossed midnight) * Handles overnight crossing (when current < start, assumes we've crossed midnight)
@@ -80,3 +84,15 @@ export function daysSinceStart(startEpoch: Instant, currentEpoch: Instant): Day
return (currentDaySerial - startDaySerial) as Day; return (currentDaySerial - startDaySerial) as Day;
} }
export const ZERO_DURATION = 0 as Duration;
/**
* utility function to generate Instant times
*/
export function generateInstanceFromClock(hour: number, minutes: number, second: number, ms = 0) {
return toInstant(
(hour * MILLIS_PER_HOUR + minutes * MILLIS_PER_MINUTE + second * MILLIS_PER_SECOND + ms) as TimeOfDay,
now(),
);
}
@@ -1,7 +1,8 @@
import { EndAction, Playback, TimeOfDay, TimeStrategy, TimerPhase, TimerType } from 'ontime-types'; import { Duration, EndAction, Playback, TimeOfDay, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND, dayInMs, millisToString } from 'ontime-utils'; import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND, dayInMs, millisToString } from 'ontime-utils';
import type { RuntimeState } from '../../stores/runtimeState.js'; import * as timeCore from '../../lib/time-core/timeCore.js';
import type { InternalRuntimeState, RuntimeState } from '../../stores/runtimeState.js';
import { import {
findDayOffset, findDayOffset,
getCurrent, getCurrent,
@@ -19,48 +20,48 @@ const asTimeOfDay = (value: number): RuntimeState['clock'] => value as RuntimeSt
describe('getElapsed()', () => { describe('getElapsed()', () => {
it('returns active elapsed time from startedAt without add-time adjustments', () => { it('returns active elapsed time from startedAt without add-time adjustments', () => {
const state = { const state = {
clock: 5 * MILLIS_PER_MINUTE, _now: timeCore.generateInstanceFromClock(0, 5, 0),
timer: { timer: {
addedTime: -10 * MILLIS_PER_MINUTE, addedTime: -10 * MILLIS_PER_MINUTE,
current: 15 * MILLIS_PER_MINUTE, current: 15 * MILLIS_PER_MINUTE,
duration: 20 * MILLIS_PER_MINUTE, duration: 20 * MILLIS_PER_MINUTE,
startedAt: 2 * MILLIS_PER_MINUTE, startedAt: timeCore.generateInstanceFromClock(0, 2, 0),
}, },
_timer: { _timer: {
pausedAt: null, pausedAt: null,
pausedDuration: 0, pausedDuration: 0,
}, },
} as RuntimeState; } as InternalRuntimeState;
expect(getElapsed(state)).toBe(3 * MILLIS_PER_MINUTE); expect(getElapsed(state)).toBe(3 * MILLIS_PER_MINUTE);
}); });
it('subtracts accumulated pause time', () => { it('subtracts accumulated pause time', () => {
const state = { const state = {
clock: 10 * MILLIS_PER_MINUTE, _now: timeCore.generateInstanceFromClock(0, 10, 0),
timer: { timer: {
startedAt: 2 * MILLIS_PER_MINUTE, startedAt: timeCore.generateInstanceFromClock(0, 2, 0),
}, },
_timer: { _timer: {
pausedAt: null, pausedAt: null,
pausedDuration: 5 * MILLIS_PER_MINUTE, pausedDuration: 5 * MILLIS_PER_MINUTE,
}, },
} as RuntimeState; } as InternalRuntimeState;
expect(getElapsed(state)).toBe(3 * MILLIS_PER_MINUTE); expect(getElapsed(state)).toBe(3 * MILLIS_PER_MINUTE);
}); });
it('uses the current pause start while paused', () => { it('uses the current pause start while paused', () => {
const state = { const state = {
clock: 10 * MILLIS_PER_MINUTE, _now: timeCore.generateInstanceFromClock(0, 10, 0),
timer: { timer: {
startedAt: 2 * MILLIS_PER_MINUTE, startedAt: timeCore.generateInstanceFromClock(0, 2, 0),
}, },
_timer: { _timer: {
pausedAt: 7 * MILLIS_PER_MINUTE, pausedAt: 7 * MILLIS_PER_MINUTE,
pausedDuration: 1 * MILLIS_PER_MINUTE, pausedDuration: 1 * MILLIS_PER_MINUTE,
}, },
} as RuntimeState; } as InternalRuntimeState;
expect(getElapsed(state)).toBe(4 * MILLIS_PER_MINUTE); expect(getElapsed(state)).toBe(4 * MILLIS_PER_MINUTE);
}); });
@@ -82,7 +83,7 @@ describe('getExpectedFinish()', () => {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
const calculatedFinish = getExpectedFinish(state); const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(null); expect(calculatedFinish).toBe(null);
}); });
@@ -95,13 +96,13 @@ describe('getExpectedFinish()', () => {
timer: { timer: {
addedTime: 0, addedTime: 0,
duration: 10, duration: 10,
startedAt: 10, startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
}, },
_timer: { _timer: {
pausedAt: null, pausedAt: null,
hasFinished: true, hasFinished: true,
}, },
} as RuntimeState; } as InternalRuntimeState;
const calculatedFinish = getExpectedFinish(state); const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(20); expect(calculatedFinish).toBe(20);
}); });
@@ -114,13 +115,13 @@ describe('getExpectedFinish()', () => {
timer: { timer: {
addedTime: 0, addedTime: 0,
duration: 10, duration: 10,
startedAt: 1, startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 1),
}, },
_timer: { _timer: {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
const calculatedFinish = getExpectedFinish(state); const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(11); expect(calculatedFinish).toBe(11);
}); });
@@ -133,13 +134,13 @@ describe('getExpectedFinish()', () => {
timer: { timer: {
addedTime: 20, addedTime: 20,
duration: 10, duration: 10,
startedAt: 1, startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 1),
}, },
_timer: { _timer: {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
const calculatedFinish = getExpectedFinish(state); const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(31); expect(calculatedFinish).toBe(31);
@@ -153,13 +154,13 @@ describe('getExpectedFinish()', () => {
timer: { timer: {
addedTime: -10, addedTime: -10,
duration: 10, duration: 10,
startedAt: 1, startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 1),
}, },
_timer: { _timer: {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
const calculatedFinish = getExpectedFinish(state); const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(1); expect(calculatedFinish).toBe(1);
@@ -173,13 +174,13 @@ describe('getExpectedFinish()', () => {
timer: { timer: {
addedTime: -100, addedTime: -100,
duration: 10, duration: 10,
startedAt: 1, startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 1),
}, },
_timer: { _timer: {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
const calculatedFinish = getExpectedFinish(state); const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(1); expect(calculatedFinish).toBe(1);
@@ -193,13 +194,13 @@ describe('getExpectedFinish()', () => {
timer: { timer: {
addedTime: 0, addedTime: 0,
duration: 0, duration: 0,
startedAt: 1, startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 1),
}, },
_timer: { _timer: {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
const calculatedFinish = getExpectedFinish(state); const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(1); expect(calculatedFinish).toBe(1);
@@ -213,13 +214,13 @@ describe('getExpectedFinish()', () => {
timer: { timer: {
addedTime: 0, addedTime: 0,
duration: dayInMs, duration: dayInMs,
startedAt: 10, startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
}, },
_timer: { _timer: {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
const calculatedFinish = getExpectedFinish(state); const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(10); expect(calculatedFinish).toBe(10);
@@ -234,13 +235,13 @@ describe('getExpectedFinish()', () => {
timer: { timer: {
addedTime: 10, addedTime: 10,
duration: dayInMs, duration: dayInMs,
startedAt: 10, startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
}, },
_timer: { _timer: {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
const calculatedFinish = getExpectedFinish(state); const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(40); expect(calculatedFinish).toBe(40);
@@ -253,7 +254,7 @@ describe('getExpectedFinish()', () => {
}, },
timer: { timer: {
addedTime: 0, addedTime: 0,
startedAt: 79200000, // 22:00:00 startedAt: timeCore.generateInstanceFromClock(22, 0, 0),
}, },
_timer: { _timer: {
pausedAt: null, pausedAt: null,
@@ -263,7 +264,7 @@ describe('getExpectedFinish()', () => {
actualStart: 79200000, actualStart: 79200000,
plannedEnd: 600000, plannedEnd: 600000,
}, },
} as RuntimeState; } as InternalRuntimeState;
const calculatedFinish = getExpectedFinish(state); const calculatedFinish = getExpectedFinish(state);
// expected finish is not a duration but a point in time // expected finish is not a duration but a point in time
@@ -279,7 +280,7 @@ describe('getCurrent()', () => {
timeEnd: 30, timeEnd: 30,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
}, },
clock: 0, _now: timeCore.generateInstanceFromClock(0, 0, 0),
timer: { timer: {
addedTime: 10, addedTime: 10,
duration: 111, // <-- we take the duration value duration: 111, // <-- we take the duration value
@@ -289,7 +290,7 @@ describe('getCurrent()', () => {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
const current = getCurrent(state); const current = getCurrent(state);
expect(current).toBe(111); expect(current).toBe(111);
@@ -300,17 +301,17 @@ describe('getCurrent()', () => {
timeEnd: 10, timeEnd: 10,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
}, },
clock: 1, _now: timeCore.generateInstanceFromClock(0, 0, 0, 1),
timer: { timer: {
addedTime: 0, addedTime: 0,
duration: 10, duration: 10,
startedAt: 0, startedAt: timeCore.generateInstanceFromClock(0, 0, 0),
}, },
_timer: { _timer: {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
const current = getCurrent(state); const current = getCurrent(state);
expect(current).toBe(9); expect(current).toBe(9);
@@ -321,17 +322,17 @@ describe('getCurrent()', () => {
timeEnd: 10, timeEnd: 10,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
}, },
clock: 1, _now: timeCore.generateInstanceFromClock(0, 0, 0, 1),
timer: { timer: {
addedTime: 10, addedTime: 10,
duration: 10, duration: 10,
startedAt: 0, startedAt: timeCore.generateInstanceFromClock(0, 0, 0),
}, },
_timer: { _timer: {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
const current = getCurrent(state); const current = getCurrent(state);
expect(current).toBe(19); expect(current).toBe(19);
@@ -342,17 +343,17 @@ describe('getCurrent()', () => {
timeEnd: 20, timeEnd: 20,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
}, },
clock: 10, _now: timeCore.generateInstanceFromClock(0, 0, 0, 10),
timer: { timer: {
addedTime: 0, addedTime: 0,
duration: dayInMs + 10, duration: dayInMs + 10,
startedAt: 10, startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
}, },
_timer: { _timer: {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
const current = getCurrent(state); const current = getCurrent(state);
expect(current).toBe(dayInMs + 10); expect(current).toBe(dayInMs + 10);
@@ -363,17 +364,17 @@ describe('getCurrent()', () => {
timeEnd: 20, timeEnd: 20,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
}, },
clock: 5, _now: timeCore.generateInstanceFromClock(0, 0, 0, 5),
timer: { timer: {
addedTime: 0, addedTime: 0,
duration: dayInMs + 10, duration: dayInMs + 10,
startedAt: 10, startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
}, },
_timer: { _timer: {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
const current = getCurrent(state); const current = getCurrent(state);
expect(current).toBe(15); expect(current).toBe(15);
@@ -384,17 +385,17 @@ describe('getCurrent()', () => {
timeEnd: 20, timeEnd: 20,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
}, },
clock: 5, _now: timeCore.generateInstanceFromClock(0, 0, 0, 5),
timer: { timer: {
addedTime: 20, addedTime: 20,
duration: dayInMs + 10, duration: dayInMs + 10,
startedAt: 10, startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
}, },
_timer: { _timer: {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
const current = getCurrent(state); const current = getCurrent(state);
expect(current).toBe(35); expect(current).toBe(35);
@@ -407,7 +408,7 @@ describe('getCurrent()', () => {
timeEnd: 100, timeEnd: 100,
countToEnd: true, countToEnd: true,
}, },
clock: 30, _now: timeCore.generateInstanceFromClock(0, 0, 0, 30),
timer: { timer: {
addedTime: 0, addedTime: 0,
duration: 100, duration: 100,
@@ -420,7 +421,7 @@ describe('getCurrent()', () => {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
const current = getCurrent(state); const current = getCurrent(state);
expect(current).toBe(70); expect(current).toBe(70);
@@ -432,11 +433,11 @@ describe('getCurrent()', () => {
timeEnd: 100, timeEnd: 100,
countToEnd: true, countToEnd: true,
}, },
clock: 30, _now: timeCore.generateInstanceFromClock(0, 0, 0, 30),
timer: { timer: {
addedTime: 0, addedTime: 0,
duration: 100, duration: 100,
startedAt: 10, startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
}, },
rundown: { rundown: {
plannedEnd: 100, plannedEnd: 100,
@@ -445,7 +446,7 @@ describe('getCurrent()', () => {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
const current = getCurrent(state); const current = getCurrent(state);
expect(current).toBe(70); expect(current).toBe(70);
@@ -457,11 +458,11 @@ describe('getCurrent()', () => {
timeEnd: 100, timeEnd: 100,
countToEnd: true, countToEnd: true,
}, },
clock: 30, _now: timeCore.generateInstanceFromClock(0, 0, 0, 30),
timer: { timer: {
addedTime: 7, addedTime: 7,
duration: 100, duration: 100,
startedAt: 10, startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
}, },
rundown: { rundown: {
plannedEnd: 100, plannedEnd: 100,
@@ -470,7 +471,7 @@ describe('getCurrent()', () => {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
const current = getCurrent(state); const current = getCurrent(state);
expect(current).toBe(77); expect(current).toBe(77);
@@ -483,11 +484,11 @@ describe('getCurrent()', () => {
timeEnd: 600000, // 00:10:00 timeEnd: 600000, // 00:10:00
countToEnd: true, countToEnd: true,
}, },
clock: 79500000, // 22:05:00 _now: timeCore.generateInstanceFromClock(22, 5, 0),
timer: { timer: {
addedTime: 0, addedTime: 0,
duration: Infinity, // not relevant, duration: Infinity, // not relevant,
startedAt: 79200000, // 22:00:00 startedAt: timeCore.generateInstanceFromClock(22, 0, 0),
}, },
rundown: { rundown: {
actualStart: 79200000, actualStart: 79200000,
@@ -497,7 +498,7 @@ describe('getCurrent()', () => {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
const current = getCurrent(state); const current = getCurrent(state);
expect(current).toBe(dayInMs - 79500000 + 600000); expect(current).toBe(dayInMs - 79500000 + 600000);
@@ -505,7 +506,7 @@ describe('getCurrent()', () => {
it('handles events that were started late', () => { it('handles events that were started late', () => {
const state = { const state = {
clock: 82000000, // 22:46:40 <--- starting 16 min after the scheduled end _now: timeCore.generateInstanceFromClock(22, 46, 40), // 22:46:40 <--- starting 16 min after the scheduled end
eventNow: { eventNow: {
timeStart: 77400000, // 21:30:00 timeStart: 77400000, // 21:30:00
timeEnd: 81000000, // 22:30:00 timeEnd: 81000000, // 22:30:00
@@ -515,7 +516,7 @@ describe('getCurrent()', () => {
timer: { timer: {
addedTime: 0, addedTime: 0,
duration: Infinity, // not relevant, duration: Infinity, // not relevant,
startedAt: 79200000, // 22:00:00 startedAt: timeCore.generateInstanceFromClock(22, 0, 0),
}, },
rundown: { rundown: {
actualStart: 82000000, // 22:46:40 <--- started now actualStart: 82000000, // 22:46:40 <--- started now
@@ -525,7 +526,7 @@ describe('getCurrent()', () => {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
const current = getCurrent(state); const current = getCurrent(state);
expect(current).toBe(81000000 - 82000000); // <-- planned end - now expect(current).toBe(81000000 - 82000000); // <-- planned end - now
@@ -541,17 +542,17 @@ describe('getExpectedFinish() and getCurrentTime() combined', () => {
timeEnd: 10, timeEnd: 10,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
}, },
clock: 0, _now: timeCore.generateInstanceFromClock(0, 0, 0),
timer: { timer: {
addedTime: 0, addedTime: 0,
duration, duration,
startedAt: 0, startedAt: timeCore.generateInstanceFromClock(0, 0, 0),
}, },
_timer: { _timer: {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
const expectedFinish = getExpectedFinish(state); const expectedFinish = getExpectedFinish(state);
const current = getCurrent(state); const current = getCurrent(state);
@@ -569,25 +570,25 @@ describe('getExpectedFinish() and getCurrentTime() combined', () => {
timeEnd: 10, timeEnd: 10,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
}, },
clock: 5, _now: timeCore.generateInstanceFromClock(0, 0, 0, 5),
timer: { timer: {
addedTime: 3, addedTime: 3,
duration, duration,
startedAt: 0, startedAt: timeCore.generateInstanceFromClock(0, 0, 0),
}, },
_timer: { _timer: {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
const expectedFinish = getExpectedFinish(state); const expectedFinish = getExpectedFinish(state);
const current = getCurrent(state); const current = getCurrent(state);
const elapsed = duration - current; const elapsed = duration - current;
expect(expectedFinish).toBe(13); expect(expectedFinish).toBe(13);
expect(elapsed).toBe(2);
expect(current).toBe(8); expect(current).toBe(8);
expect(elapsed).toBe(2);
}); });
}); });
@@ -871,7 +872,7 @@ describe('getRuntimeOffset()', () => {
dayOffset: 0, dayOffset: 0,
}, },
timer: { timer: {
startedAt: 150, startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 150),
addedTime: 0, addedTime: 0,
current: 0, current: 0,
}, },
@@ -883,9 +884,9 @@ describe('getRuntimeOffset()', () => {
plannedStart: 100, plannedStart: 100,
currentDay: 0, currentDay: 0,
}, },
clock: 150, _now: timeCore.generateInstanceFromClock(0, 0, 0, 150),
_startDayOffset: 0, _startDayOffset: 0,
} as RuntimeState; } as InternalRuntimeState;
const { absolute } = getRuntimeOffset(state); const { absolute } = getRuntimeOffset(state);
expect(absolute).toBe(50); expect(absolute).toBe(50);
@@ -899,7 +900,7 @@ describe('getRuntimeOffset()', () => {
dayOffset: 0, dayOffset: 0,
}, },
timer: { timer: {
startedAt: 150, // we started 50ms delayed startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 150), // we started 50ms delayed
addedTime: 10, // we compensated with 10ms addedTime: 10, // we compensated with 10ms
current: 10, // we are 10ms into the timer current: 10, // we are 10ms into the timer
}, },
@@ -912,7 +913,7 @@ describe('getRuntimeOffset()', () => {
currentDay: 0, currentDay: 0,
}, },
_startDayOffset: 0, _startDayOffset: 0,
} as RuntimeState; } as InternalRuntimeState;
const { absolute } = getRuntimeOffset(state); const { absolute } = getRuntimeOffset(state);
expect(absolute).toBe(60); expect(absolute).toBe(60);
@@ -927,7 +928,7 @@ describe('getRuntimeOffset()', () => {
dayOffset: 0, dayOffset: 0,
}, },
timer: { timer: {
startedAt: 100, // we started ontime startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 100), // we started ontime
current: -10, // we are 10 seconds over current: -10, // we are 10 seconds over
addedTime: 0, // we have not compensated with added time addedTime: 0, // we have not compensated with added time
}, },
@@ -940,7 +941,7 @@ describe('getRuntimeOffset()', () => {
currentDay: 0, currentDay: 0,
}, },
_startDayOffset: 0, _startDayOffset: 0,
} as RuntimeState; } as InternalRuntimeState;
const { absolute } = getRuntimeOffset(state); const { absolute } = getRuntimeOffset(state);
expect(absolute).toBe(10); expect(absolute).toBe(10);
@@ -954,9 +955,9 @@ describe('getRuntimeOffset()', () => {
timeEnd: 150, timeEnd: 150,
dayOffset: 0, dayOffset: 0,
}, },
clock: 150, _now: timeCore.generateInstanceFromClock(0, 0, 0, 150),
timer: { timer: {
startedAt: 100, // started on time startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 100), // started on time
current: 25, // are 25ms into it current: 25, // are 25ms into it
addedTime: 0, addedTime: 0,
}, },
@@ -969,7 +970,7 @@ describe('getRuntimeOffset()', () => {
currentDay: 0, currentDay: 0,
}, },
_startDayOffset: 0, _startDayOffset: 0,
} as RuntimeState; } as InternalRuntimeState;
const { absolute } = getRuntimeOffset(state); const { absolute } = getRuntimeOffset(state);
expect(absolute).toBe(25); expect(absolute).toBe(25);
@@ -977,7 +978,7 @@ describe('getRuntimeOffset()', () => {
it('offset doesnt exist if we havent started', () => { it('offset doesnt exist if we havent started', () => {
const state = { const state = {
clock: 78480789, _now: timeCore.generateInstanceFromClock(22, 48, 0), // 78480789
eventNow: { eventNow: {
id: 'd6a2ce', id: 'd6a2ce',
timeStart: 77400000, timeStart: 77400000,
@@ -1011,7 +1012,7 @@ describe('getRuntimeOffset()', () => {
}, },
_startDayOffset: 0, _startDayOffset: 0,
_timer: { pausedAt: null }, _timer: { pausedAt: null },
} as RuntimeState; } as InternalRuntimeState;
const { absolute } = getRuntimeOffset(state); const { absolute } = getRuntimeOffset(state);
expect(absolute).toBe(0); expect(absolute).toBe(0);
@@ -1019,7 +1020,7 @@ describe('getRuntimeOffset()', () => {
it('with time-to-end, offsets dont exist if we are not in overtime', () => { it('with time-to-end, offsets dont exist if we are not in overtime', () => {
const state = { const state = {
clock: 80000000, // 22:13:20 _now: timeCore.generateInstanceFromClock(22, 13, 20),
eventNow: { eventNow: {
id: 'd6a2ce', id: 'd6a2ce',
type: 'event', type: 'event',
@@ -1063,11 +1064,11 @@ describe('getRuntimeOffset()', () => {
expectedFinish: 81600000, expectedFinish: 81600000,
playback: Playback.Play, playback: Playback.Play,
secondaryTimer: null, secondaryTimer: null,
startedAt: 78000000, startedAt: timeCore.generateInstanceFromClock(21, 40, 0),
}, },
_startDayOffset: 0, _startDayOffset: 0,
_timer: { pausedAt: null }, _timer: { pausedAt: null },
} as RuntimeState; } as InternalRuntimeState;
const { absolute } = getRuntimeOffset(state); const { absolute } = getRuntimeOffset(state);
expect(absolute).toBe(0); expect(absolute).toBe(0);
@@ -1075,7 +1076,7 @@ describe('getRuntimeOffset()', () => {
it('with time-to-end, offset is the overtime', () => { it('with time-to-end, offset is the overtime', () => {
const state = { const state = {
clock: 82000000, // 22:46:40 _now: timeCore.generateInstanceFromClock(22, 46, 40),
eventNow: { eventNow: {
id: 'd6a2ce', id: 'd6a2ce',
type: 'event', type: 'event',
@@ -1119,11 +1120,11 @@ describe('getRuntimeOffset()', () => {
expectedFinish: 81600000, expectedFinish: 81600000,
playback: Playback.Play, playback: Playback.Play,
secondaryTimer: null, secondaryTimer: null,
startedAt: 78000000, startedAt: timeCore.generateInstanceFromClock(21, 40, 0),
}, },
_startDayOffset: 0, _startDayOffset: 0,
_timer: { pausedAt: null }, _timer: { pausedAt: null },
} as RuntimeState; } as InternalRuntimeState;
const { absolute } = getRuntimeOffset(state); const { absolute } = getRuntimeOffset(state);
expect(absolute).toBe(400000 - 200000); // <--- offset is always the overtime + added time expect(absolute).toBe(400000 - 200000); // <--- offset is always the overtime + added time
@@ -1131,7 +1132,7 @@ describe('getRuntimeOffset()', () => {
it('handles time-to-end started after the end time', () => { it('handles time-to-end started after the end time', () => {
const state = { const state = {
clock: 82000000, // 22:46:40 <--- starting 16m 40s after the scheduled end _now: timeCore.generateInstanceFromClock(22, 46, 40), // 22:46:40 <--- starting 16m 40s after the scheduled end
eventNow: { eventNow: {
id: 'd6a2ce', id: 'd6a2ce',
timeStart: 77400000, // 21:30:00 timeStart: 77400000, // 21:30:00
@@ -1164,14 +1165,14 @@ describe('getRuntimeOffset()', () => {
expectedFinish: 82000000 + 3600000, // <--- now + duration expectedFinish: 82000000 + 3600000, // <--- now + duration
playback: Playback.Play, playback: Playback.Play,
secondaryTimer: null, secondaryTimer: null,
startedAt: 82000000, // <--- started now startedAt: timeCore.generateInstanceFromClock(22, 46, 40),
}, },
_startDayOffset: 0, _startDayOffset: 0,
_timer: { pausedAt: null }, _timer: { pausedAt: null },
} as RuntimeState; } as InternalRuntimeState;
const updateCurrent = getCurrent(state); const updateCurrent = getCurrent(state);
state.timer.current = updateCurrent; state.timer.current = updateCurrent as Duration;
const { absolute } = getRuntimeOffset(state); const { absolute } = getRuntimeOffset(state);
expect(millisToString(absolute)).toBe('00:16:40'); expect(millisToString(absolute)).toBe('00:16:40');
expect(absolute).toBe(82000000 - 81000000); // <-- now - planned end expect(absolute).toBe(82000000 - 81000000); // <-- now - planned end
@@ -1187,7 +1188,7 @@ describe('getRuntimeOffset() relative', () => {
dayOffset: 0, dayOffset: 0,
}, },
timer: { timer: {
startedAt: 150, startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 150),
addedTime: 0, addedTime: 0,
current: 0, current: 0,
}, },
@@ -1200,7 +1201,7 @@ describe('getRuntimeOffset() relative', () => {
currentDay: 0, currentDay: 0,
}, },
_startDayOffset: 0, _startDayOffset: 0,
} as RuntimeState; } as InternalRuntimeState;
const { absolute, relative } = getRuntimeOffset(state); const { absolute, relative } = getRuntimeOffset(state);
expect(absolute).toBe(0); expect(absolute).toBe(0);
@@ -1214,7 +1215,7 @@ describe('getRuntimeOffset() relative', () => {
dayOffset: 0, dayOffset: 0,
}, },
timer: { timer: {
startedAt: 150, startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 150),
addedTime: 0, addedTime: 0,
current: 0, current: 0,
}, },
@@ -1227,7 +1228,7 @@ describe('getRuntimeOffset() relative', () => {
currentDay: 0, currentDay: 0,
}, },
_startDayOffset: 0, _startDayOffset: 0,
} as RuntimeState; } as InternalRuntimeState;
const { absolute, relative } = getRuntimeOffset(state); const { absolute, relative } = getRuntimeOffset(state);
expect(absolute).toBe(50); expect(absolute).toBe(50);
@@ -1241,7 +1242,7 @@ describe('getRuntimeOffset() relative', () => {
dayOffset: 0, dayOffset: 0,
}, },
timer: { timer: {
startedAt: 100, startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 100),
addedTime: 0, addedTime: 0,
current: 0, current: 0,
}, },
@@ -1254,7 +1255,7 @@ describe('getRuntimeOffset() relative', () => {
currentDay: 0, currentDay: 0,
}, },
_startDayOffset: 0, _startDayOffset: 0,
} as RuntimeState; } as InternalRuntimeState;
const { absolute, relative } = getRuntimeOffset(state); const { absolute, relative } = getRuntimeOffset(state);
expect(absolute).toBe(-50); expect(absolute).toBe(-50);
@@ -1276,7 +1277,7 @@ describe('getTimerPhase()', () => {
secondaryTimer: null, secondaryTimer: null,
startedAt: null, startedAt: null,
}, },
} as RuntimeState; } as InternalRuntimeState;
const phase = getTimerPhase(state); const phase = getTimerPhase(state);
expect(phase).toBe(TimerPhase.None); expect(phase).toBe(TimerPhase.None);
@@ -1294,7 +1295,7 @@ describe('getTimerPhase()', () => {
timeDanger: 100, timeDanger: 100,
timeWarning: 200, timeWarning: 200,
}, },
} as RuntimeState; } as InternalRuntimeState;
const phase = getTimerPhase(state); const phase = getTimerPhase(state);
expect(phase).toBe(TimerPhase.Overtime); expect(phase).toBe(TimerPhase.Overtime);
@@ -1312,7 +1313,7 @@ describe('getTimerPhase()', () => {
timeDanger: 100, timeDanger: 100,
timeWarning: 200, timeWarning: 200,
}, },
} as RuntimeState; } as InternalRuntimeState;
const phase = getTimerPhase(state); const phase = getTimerPhase(state);
expect(phase).toBe(TimerPhase.Danger); expect(phase).toBe(TimerPhase.Danger);
@@ -1330,7 +1331,7 @@ describe('getTimerPhase()', () => {
timeDanger: 100, timeDanger: 100,
timeWarning: 200, timeWarning: 200,
}, },
} as RuntimeState; } as InternalRuntimeState;
const phase = getTimerPhase(state); const phase = getTimerPhase(state);
expect(phase).toBe(TimerPhase.Warning); expect(phase).toBe(TimerPhase.Warning);
@@ -1348,7 +1349,7 @@ describe('getTimerPhase()', () => {
timeDanger: 100, timeDanger: 100,
timeWarning: 200, timeWarning: 200,
}, },
} as RuntimeState; } as InternalRuntimeState;
const phase = getTimerPhase(state); const phase = getTimerPhase(state);
expect(phase).toBe(TimerPhase.Default); expect(phase).toBe(TimerPhase.Default);
@@ -1356,7 +1357,7 @@ describe('getTimerPhase()', () => {
it('#1042 identifies waiting to roll', () => { it('#1042 identifies waiting to roll', () => {
const state = { const state = {
clock: 55691050, _now: timeCore.generateInstanceFromClock(16, 28, 11), //55691050
eventNow: null, eventNow: null,
eventNext: null, eventNext: null,
rundown: { rundown: {
@@ -1382,13 +1383,13 @@ describe('getTimerPhase()', () => {
startedAt: null, startedAt: null,
}, },
_timer: { _timer: {
forceFinish: null, forceFinish: false,
pausedAt: null, pausedAt: null,
}, },
_rundown: { _rundown: {
totalDelay: 0, totalDelay: 0,
}, },
} as RuntimeState; } as InternalRuntimeState;
const phase = getTimerPhase(state); const phase = getTimerPhase(state);
expect(phase).toBe(TimerPhase.Pending); expect(phase).toBe(TimerPhase.Pending);
@@ -1396,7 +1397,7 @@ describe('getTimerPhase()', () => {
it('#1042 identifies waiting to roll', () => { it('#1042 identifies waiting to roll', () => {
const state = { const state = {
clock: 55691050, _now: timeCore.generateInstanceFromClock(16, 28, 11), //55691050
eventNow: null, eventNow: null,
eventNext: null, eventNext: null,
rundown: { rundown: {
@@ -1422,13 +1423,13 @@ describe('getTimerPhase()', () => {
startedAt: null, startedAt: null,
}, },
_timer: { _timer: {
forceFinish: null, forceFinish: false,
pausedAt: null, pausedAt: null,
}, },
_rundown: { _rundown: {
totalDelay: 0, totalDelay: 0,
}, },
} as RuntimeState; } as InternalRuntimeState;
const phase = getTimerPhase(state); const phase = getTimerPhase(state);
expect(phase).toBe(TimerPhase.Pending); expect(phase).toBe(TimerPhase.Pending);
+2
View File
@@ -5,6 +5,8 @@ import { RundownMetadata } from '../api-data/rundown/rundown.types.js';
import { getTimedIndexFromPlayableIndex } from '../api-data/rundown/rundown.utils.js'; import { getTimedIndexFromPlayableIndex } from '../api-data/rundown/rundown.utils.js';
import { normaliseEndTime } from './timerUtils.js'; import { normaliseEndTime } from './timerUtils.js';
// import * as timeCore from '../lib/time-core/timeCore.js';
/** /**
* Finds current event in a rolling rundown * Finds current event in a rolling rundown
*/ */
@@ -760,16 +760,17 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
// save the restore state // save the restore state
if (hasImmediateChanges) { if (hasImmediateChanges) {
const internalState = runtimeState.getInternalState();
restoreService restoreService
.save({ .save({
playback: state.timer.playback, playback: state.timer.playback,
selectedEventId: state.eventNow?.id ?? null, selectedEventId: state.eventNow?.id ?? null,
startedAt: state.timer.startedAt, startedAt: state.timer.startedAt,
addedTime: state.timer.addedTime, addedTime: state.timer.addedTime,
pausedAt: state._timer.pausedAt, pausedAt: internalState._timer.pausedAt,
pausedDuration: state._timer.pausedDuration, pausedDuration: internalState._timer.pausedDuration,
firstStart: state.rundown.actualStart, firstStart: state.rundown.actualStart,
startEpoch: state._startEpoch, startEpoch: internalState._startEpoch,
currentDay: state.rundown.currentDay, currentDay: state.rundown.currentDay,
}) })
.catch((_e) => { .catch((_e) => {
+39 -36
View File
@@ -1,7 +1,9 @@
import { Day, MaybeNumber, TimeOfDay, TimerPhase } from 'ontime-types'; import { Day, Duration, Maybe, TimeOfDay, TimerPhase } from 'ontime-types';
import { MILLIS_PER_HOUR, checkIsNow, dayInMs, isPlaybackActive } from 'ontime-utils'; import { MILLIS_PER_HOUR, checkIsNow, dayInMs, isPlaybackActive } from 'ontime-utils';
import type { RuntimeState } from '../stores/runtimeState.js'; import * as timeCore from '../lib/time-core/timeCore.js';
import { ZERO_DURATION } from '../lib/time-core/timeCore.js';
import type { InternalRuntimeState, RuntimeState } from '../stores/runtimeState.js';
/** /**
* handle events that span over midnight * handle events that span over midnight
@@ -20,48 +22,46 @@ export function hasCrossedMidnight(previous: TimeOfDay, current: TimeOfDay): boo
/** /**
* Calculates expected finish time of a running timer * Calculates expected finish time of a running timer
* @param {RuntimeState} state runtime state * @param {InternalRuntimeState} state runtime state
* @returns {number | null} new current time or null if nothing is running * @returns {number | null} new current time or null if nothing is running
*/ */
export function getExpectedFinish(state: RuntimeState): MaybeNumber { export function getExpectedFinish(state: InternalRuntimeState): Maybe<TimeOfDay> {
const { startedAt, duration, addedTime } = state.timer; const { startedAt, duration, addedTime } = state.timer;
if (state.eventNow === null) { if (state.eventNow === null) {
return null; return null;
} }
const { countToEnd, timeEnd } = state.eventNow; const { countToEnd, timeEnd } = state.eventNow;
const { pausedAt } = state._timer; const { pausedAt } = state._timer;
const { clock } = state; const { _now } = state;
if (startedAt === null) { if (startedAt === null) {
return null; return null;
} }
const pausedTime = pausedAt != null ? clock - pausedAt : 0; const pausedTime = pausedAt !== null ? timeCore.toTimeOfDay(_now) - pausedAt : 0;
if (countToEnd) { if (countToEnd) {
return timeEnd + addedTime + pausedTime; return (timeEnd + addedTime + pausedTime) as TimeOfDay;
} }
// handle events that finish the day after // handle events that finish the day after
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- duration exists if ther eis a timer // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- duration exists if there is a timer
const expectedFinish = startedAt + duration! + addedTime + pausedTime; const expectedFinish = timeCore.toTimeOfDay(startedAt) + duration! + addedTime + pausedTime;
if (expectedFinish > dayInMs) { if (expectedFinish > dayInMs) {
return expectedFinish - dayInMs; return (expectedFinish - dayInMs) as TimeOfDay;
} }
// an event cannot finish before it started (user added too much negative time) // an event cannot finish before it started (user added too much negative time)
return Math.max(expectedFinish, startedAt); return Math.max(expectedFinish, timeCore.toTimeOfDay(startedAt)) as TimeOfDay;
} }
/** /**
* Calculates running countdown * Calculates running countdown
* @param {RuntimeState} state runtime state * @param {InternalRuntimeState} state runtime state
* @returns {number} current time for timer * @returns {number} current time for timer
*/ */
export function getCurrent(state: InternalRuntimeState): Duration {
export function getCurrent(state: RuntimeState): number {
// eslint-disable-next-line no-unused-labels -- dev code path // eslint-disable-next-line no-unused-labels -- dev code path
DEV: { DEV: {
if (state.eventNow === null || state.timer.duration === null) { if (state.eventNow === null || state.timer.duration === null) {
@@ -71,12 +71,12 @@ export function getCurrent(state: RuntimeState): number {
const { startedAt, duration, addedTime } = state.timer; const { startedAt, duration, addedTime } = state.timer;
const { countToEnd, timeStart, timeEnd } = state.eventNow; const { countToEnd, timeStart, timeEnd } = state.eventNow;
const { pausedAt } = state._timer; const { pausedAt } = state._timer;
const { clock } = state; const { _now } = state;
if (countToEnd) { if (countToEnd) {
const isEventOverMidnight = timeStart > timeEnd; const isEventOverMidnight = timeStart > timeEnd;
const correctDay = isEventOverMidnight ? dayInMs : 0; const correctDay = isEventOverMidnight ? dayInMs : 0;
return correctDay - clock + timeEnd + addedTime; return (correctDay - timeCore.toTimeOfDay(_now) + timeEnd + addedTime) as Duration;
} }
if (startedAt === null) { if (startedAt === null) {
@@ -84,19 +84,19 @@ export function getCurrent(state: RuntimeState): number {
} }
if (pausedAt != null) { if (pausedAt != null) {
return startedAt + duration + addedTime - pausedAt; return (timeCore.toTimeOfDay(startedAt) + duration + addedTime - pausedAt) as Duration;
} }
const hasPassedMidnight = startedAt > clock; const hasPassedMidnight = timeCore.toTimeOfDay(startedAt) > timeCore.toTimeOfDay(_now);
const correctDay = hasPassedMidnight ? dayInMs : 0; const correctDay = hasPassedMidnight ? dayInMs : 0;
return startedAt + duration + addedTime - clock - correctDay; return (timeCore.toTimeOfDay(startedAt) + duration + addedTime - timeCore.toTimeOfDay(_now) - correctDay) as Duration;
} }
/** /**
* Calculates active time elapsed since the timer started. * Calculates active time elapsed since the timer started.
*/ */
export function getElapsed(state: RuntimeState): MaybeNumber { export function getElapsed(state: InternalRuntimeState): Maybe<Duration> {
const { clock } = state; const { _now } = state;
const { startedAt } = state.timer; const { startedAt } = state.timer;
const { pausedAt, pausedDuration } = state._timer; const { pausedAt, pausedDuration } = state._timer;
@@ -104,11 +104,11 @@ export function getElapsed(state: RuntimeState): MaybeNumber {
return null; return null;
} }
const referenceClock = pausedAt ?? clock; const referenceClock = pausedAt ?? timeCore.toTimeOfDay(_now);
const elapsedSinceStart = getTimeSinceStart(referenceClock, startedAt); const elapsedSinceStart = getTimeSinceStart(referenceClock, timeCore.toTimeOfDay(startedAt));
const activeElapsed = elapsedSinceStart - pausedDuration; const activeElapsed = elapsedSinceStart - pausedDuration;
return Math.max(0, activeElapsed); return Math.max(0, activeElapsed) as Duration;
} }
function getTimeSinceStart(clock: TimeOfDay, startedAt: number): number { function getTimeSinceStart(clock: TimeOfDay, startedAt: number): number {
@@ -127,7 +127,7 @@ function getTimeSinceStart(clock: TimeOfDay, startedAt: number): number {
* @returns {boolean} * @returns {boolean}
*/ */
export function skippedOutOfEvent(state: RuntimeState, previousTime: number, skipLimit: number): boolean { export function skippedOutOfEvent(state: RuntimeState, previousTime: number, skipLimit: number): boolean {
// we cant have skipped if we havent started // we cant have skipped if we haven't started
if (state.timer.expectedFinish === null || state.timer.startedAt === null) { if (state.timer.expectedFinish === null || state.timer.startedAt === null) {
return false; return false;
} }
@@ -151,12 +151,12 @@ export function skippedOutOfEvent(state: RuntimeState, previousTime: number, ski
* Positive offset is over time / behind schedule * Positive offset is over time / behind schedule
* Negative offset is under time / ahead of schedule * Negative offset is under time / ahead of schedule
*/ */
export function getRuntimeOffset(state: RuntimeState): { absolute: number; relative: number } { export function getRuntimeOffset(state: InternalRuntimeState): { absolute: Duration; relative: Duration } {
const { eventNow, clock, _startDayOffset } = state; const { eventNow, _now, _startDayOffset } = state;
const { addedTime, current, startedAt } = state.timer; const { addedTime, current, startedAt } = state.timer;
// nothing to calculate if there are no loaded events or if we havent started // nothing to calculate if there are no loaded events or if we havent started
if (eventNow === null || startedAt === null || _startDayOffset === null) { if (eventNow === null || startedAt === null || _startDayOffset === null) {
return { absolute: 0, relative: 0 }; return { absolute: ZERO_DURATION, relative: ZERO_DURATION };
} }
const { countToEnd, timeStart, dayOffset } = eventNow; const { countToEnd, timeStart, dayOffset } = eventNow;
@@ -171,20 +171,23 @@ export function getRuntimeOffset(state: RuntimeState): { absolute: number; relat
} }
// difference between planned event start and actual event start (will be positive if we started behind) // difference between planned event start and actual event start (will be positive if we started behind)
const eventStartOffset = startedAt + _startDayOffset * dayInMs - (timeStart + dayOffset * dayInMs); const eventStartOffset =
timeCore.toTimeOfDay(startedAt) + _startDayOffset * dayInMs - (timeStart + dayOffset * dayInMs);
// how long has the event been running over (is a negative number when in over timer so inverted before adding to offset) // how long has the event been running over (is a negative number when in over timer so inverted before adding to offset)
const overtime = Math.abs(Math.min(current, 0)); const overtime = Math.abs(Math.min(current, 0));
// time the playback was paused, the different from now to when we paused is added to the offset TODO: brakes when crossing midnight // time the playback was paused, the different from now to when we paused is added to the offset TODO: brakes when crossing midnight
const pausedTime = state._timer.pausedAt === null ? 0 : clock - state._timer.pausedAt; const pausedTime = state._timer.pausedAt === null ? 0 : timeCore.toTimeOfDay(_now) - state._timer.pausedAt;
// absolute offset is difference between schedule and playback time // absolute offset is difference between schedule and playback time
// in case of count to end, the absolute offset is overtime and added time // in case of count to end, the absolute offset is overtime and added time
const absolute = countToEnd ? overtime + addedTime : eventStartOffset + overtime + pausedTime + addedTime; const absolute = (
countToEnd ? overtime + addedTime : eventStartOffset + overtime + pausedTime + addedTime
) as Duration;
// the relative offset is the same as the absolute but adjusted relative to the actual start time // the relative offset is the same as the absolute but adjusted relative to the actual start time
const relative = absolute + plannedStart - actualStart - _startDayOffset * dayInMs; const relative = (absolute + plannedStart - actualStart - _startDayOffset * dayInMs) as Duration;
return { absolute, relative }; return { absolute, relative };
} }
@@ -193,7 +196,7 @@ export function getRuntimeOffset(state: RuntimeState): { absolute: number; relat
* Checks running timer to see which phase it currently is in * Checks running timer to see which phase it currently is in
* @param state * @param state
*/ */
export function getTimerPhase(state: RuntimeState): TimerPhase { export function getTimerPhase(state: InternalRuntimeState): TimerPhase {
if (!isPlaybackActive(state.timer.playback)) { if (!isPlaybackActive(state.timer.playback)) {
return TimerPhase.None; return TimerPhase.None;
} }
@@ -223,7 +226,7 @@ export function getTimerPhase(state: RuntimeState): TimerPhase {
/** /**
* Finds the day offset relative to an event start * Finds the day offset relative to an event start
* used byt the runtimeState on first start to get correct offsets * used by the RuntimeState on first start to get correct offsets
*/ */
export function findDayOffset(plannedStart: number, clock: number): Day { export function findDayOffset(plannedStart: number, clock: number): Day {
const distance = clock - plannedStart; const distance = clock - plannedStart;
@@ -1,33 +1,12 @@
import { OffsetMode, Playback, type TimeOfDay, TimerPhase } from 'ontime-types'; import { Day, type Duration, type Instant, OffsetMode, Playback, TimeOfDay, TimerPhase } from 'ontime-types';
import { deepmerge } from 'ontime-utils'; import { deepmerge } from 'ontime-utils';
import type { RuntimeState } from '../runtimeState.js'; import { ZERO_DURATION } from '../../lib/time-core/timeCore.js';
import type { InternalRuntimeState, RuntimeState } from '../runtimeState.js';
const baseState: RuntimeState = { export const runtimePlaceholder = {
clock: 0 as TimeOfDay,
eventNow: null,
eventNext: null,
eventFlag: null,
groupNow: null,
rundown: {
selectedEventIndex: null,
numEvents: 0,
plannedStart: 0,
plannedEnd: 0,
actualStart: null,
actualGroupStart: null,
currentDay: 0,
},
offset: {
absolute: 0,
relative: 0,
mode: OffsetMode.Absolute,
expectedRundownEnd: null,
expectedGroupEnd: null,
expectedFlagStart: null,
},
timer: { timer: {
addedTime: 0, addedTime: ZERO_DURATION,
current: null, current: null,
duration: null, duration: null,
elapsed: null, elapsed: null,
@@ -37,10 +16,43 @@ const baseState: RuntimeState = {
secondaryTimer: null, secondaryTimer: null,
startedAt: null, startedAt: null,
}, },
offset: {
absolute: ZERO_DURATION,
relative: ZERO_DURATION,
mode: OffsetMode.Absolute,
expectedGroupEnd: null,
expectedRundownEnd: null,
expectedFlagStart: null,
},
rundown: {
selectedEventIndex: null,
numEvents: 0,
plannedStart: 0 as TimeOfDay,
plannedEnd: 0 as TimeOfDay,
actualStart: null,
actualGroupStart: null,
currentDay: 0 as Day,
},
} as Readonly<InternalRuntimeState>;
const baseState: InternalRuntimeState = {
_now: 0 as Instant,
eventNow: null,
eventNext: null,
eventFlag: null,
groupNow: null,
rundown: {
...runtimePlaceholder.rundown,
},
offset: {
...runtimePlaceholder.offset,
},
timer: { ...runtimePlaceholder.timer },
_timer: { _timer: {
forceFinish: null, forceFinish: false,
pausedAt: null, pausedAt: null,
pausedDuration: 0, pausedDuration: 0 as Duration,
secondaryTarget: null, secondaryTarget: null,
hasFinished: false, hasFinished: false,
}, },
@@ -6,9 +6,10 @@ import { rundownCache } from '../../api-data/rundown/rundown.dao.js';
import { initRundown } from '../../api-data/rundown/rundown.service.js'; import { initRundown } from '../../api-data/rundown/rundown.service.js';
import { RundownMetadata } from '../../api-data/rundown/rundown.types.js'; import { RundownMetadata } from '../../api-data/rundown/rundown.types.js';
import { import {
type RuntimeState, type InternalRuntimeState,
addTime, addTime,
clearState, clearState,
getInternalState,
getState, getState,
load, load,
loadGroupFlagAndEnd, loadGroupFlagAndEnd,
@@ -32,7 +33,7 @@ const mockEvent = {
} as PlayableEvent; } as PlayableEvent;
const mockState = { const mockState = {
clock: 666, _now: 666,
eventNow: null, eventNow: null,
eventNext: null, eventNext: null,
rundown: { rundown: {
@@ -53,7 +54,7 @@ const mockState = {
pausedAt: null, pausedAt: null,
hasFinished: false, hasFinished: false,
}, },
} as RuntimeState; } as InternalRuntimeState;
vi.mock('../../classes/data-provider/DataProvider.js', () => { vi.mock('../../classes/data-provider/DataProvider.js', () => {
return { return {
@@ -108,34 +109,34 @@ describe('mutation on runtimeState', () => {
const { metadata, rundown } = rundownCache.get(); const { metadata, rundown } = rundownCache.get();
load(mockEvent, rundown, metadata); load(mockEvent, rundown, metadata);
let newState = getState(); let newState = getInternalState();
expect(newState.eventNow?.id).toBe(mockEvent.id); expect(newState.eventNow?.id).toBe(mockEvent.id);
expect(newState.eventNext?.id).toBe('event2'); expect(newState.eventNext?.id).toBe('event2');
expect(newState.timer.playback).toBe(Playback.Armed); expect(newState.timer.playback).toBe(Playback.Armed);
expect(newState.clock).not.toBe(666); expect(getState().clock).not.toBe(666);
expect(newState.groupNow).toBeNull(); expect(newState.groupNow).toBeNull();
// 2. Start event // 2. Start event
vi.setSystemTime('jan 1 00:02'); vi.setSystemTime('jan 1 00:02');
let success = start(); let success = start();
newState = getState(); newState = getInternalState();
expect(success).toBe(true); expect(success).toBe(true);
expect(newState.timer).toMatchObject({ expect(newState.timer).toMatchObject({
playback: Playback.Play, playback: Playback.Play,
}); });
expect(newState.rundown.actualStart).toBe(newState.clock); expect(newState.rundown.actualStart).toBe(getState().clock);
// 3. Pause event // 3. Pause event
vi.setSystemTime('jan 1 00:03'); vi.setSystemTime('jan 1 00:03');
success = pause(); success = pause();
newState = getState(); newState = getInternalState();
expect(success).toBe(true); expect(success).toBe(true);
expect(newState.clock).not.toBe(666); expect(getState().clock).not.toBe(666);
expect(newState.timer).toMatchObject({ expect(newState.timer).toMatchObject({
playback: Playback.Pause, playback: Playback.Pause,
addedTime: 0, addedTime: 0,
}); });
expect(newState._timer.pausedAt).toEqual(newState.clock); expect(newState._timer.pausedAt).toEqual(getState().clock);
success = pause(); success = pause();
expect(success).toBe(false); expect(success).toBe(false);
@@ -143,7 +144,7 @@ describe('mutation on runtimeState', () => {
// 4. Restart event // 4. Restart event
vi.setSystemTime('jan 1 00:04'); vi.setSystemTime('jan 1 00:04');
success = start(); success = start();
newState = getState(); newState = getInternalState();
expect(success).toBe(true); expect(success).toBe(true);
expect(newState.timer).toMatchObject({ expect(newState.timer).toMatchObject({
playback: Playback.Play, playback: Playback.Play,
@@ -163,7 +164,7 @@ describe('mutation on runtimeState', () => {
// 5. Stop event // 5. Stop event
vi.setSystemTime('jan 1 00:05'); vi.setSystemTime('jan 1 00:05');
success = stop(); success = stop();
newState = getState(); newState = getInternalState();
expect(success).toBe(true); expect(success).toBe(true);
expect(newState.eventNow).toBe(null); expect(newState.eventNow).toBe(null);
expect(newState.timer).toMatchObject({ expect(newState.timer).toMatchObject({
@@ -825,7 +826,7 @@ describe('roll mode', () => {
test('pending event', () => { test('pending event', () => {
const { rundown, metadata } = rundownCache.get(); const { rundown, metadata } = rundownCache.get();
const { eventId, didStart } = roll(rundown, metadata); const { eventId, didStart } = roll(rundown, metadata);
const state = getState(); const state = getInternalState();
expect(eventId).toBe('1'); expect(eventId).toBe('1');
expect(didStart).toBe(false); expect(didStart).toBe(false);
@@ -944,7 +945,7 @@ describe('loadGroupFlagAndEnd()', () => {
groupNow: null, groupNow: null,
eventNow: rundown.entries[11], eventNow: rundown.entries[11],
rundown: { actualGroupStart: null }, rundown: { actualGroupStart: null },
} as RuntimeState; } as InternalRuntimeState;
const metadata = { playableEventOrder: ['0', '11', '3'], flags: ['1'] } as RundownMetadata; const metadata = { playableEventOrder: ['0', '11', '3'], flags: ['1'] } as RundownMetadata;
@@ -972,7 +973,7 @@ describe('loadGroupFlagAndEnd()', () => {
groupNow: rundown.entries[1], groupNow: rundown.entries[1],
eventNow: rundown.entries[22], eventNow: rundown.entries[22],
rundown: { actualGroupStart: null }, rundown: { actualGroupStart: null },
} as RuntimeState; } as InternalRuntimeState;
const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata; const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata;
@@ -1000,7 +1001,7 @@ describe('loadGroupFlagAndEnd()', () => {
groupNow: rundown.entries[1], groupNow: rundown.entries[1],
eventNow: rundown.entries[0], eventNow: rundown.entries[0],
rundown: { actualGroupStart: null }, rundown: { actualGroupStart: null },
} as RuntimeState; } as InternalRuntimeState;
const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata; const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata;
@@ -1025,7 +1026,7 @@ describe('loadGroupFlagAndEnd()', () => {
groupNow: null, groupNow: null,
eventNow: rundown.entries[0], eventNow: rundown.entries[0],
rundown: { actualGroupStart: null }, rundown: { actualGroupStart: null },
} as RuntimeState; } as InternalRuntimeState;
const metadata = { playableEventOrder: ['0', '1'], flags: ['1'] } as RundownMetadata; const metadata = { playableEventOrder: ['0', '1'], flags: ['1'] } as RundownMetadata;
@@ -1087,7 +1088,7 @@ describe('loadGroupFlagAndEnd()', () => {
groupNow: null, groupNow: null,
eventNow: rundown.entries[0], eventNow: rundown.entries[0],
rundown: { actualGroupStart: null }, rundown: { actualGroupStart: null },
} as RuntimeState; } as InternalRuntimeState;
const metadata = { playableEventOrder: ['0', '1', '2', '3'], flags: ['3'] } as RundownMetadata; const metadata = { playableEventOrder: ['0', '1', '2', '3'], flags: ['3'] } as RundownMetadata;
@@ -1169,7 +1170,7 @@ describe('loadGroupFlagAndEnd()', () => {
groupNow: null, groupNow: null,
eventNow: rundown.entries[0], eventNow: rundown.entries[0],
rundown: { actualGroupStart: null }, rundown: { actualGroupStart: null },
} as RuntimeState; } as InternalRuntimeState;
const metadata = { playableEventOrder: ['0', '1', '2', '3'], flags: ['3'] } as RundownMetadata; const metadata = { playableEventOrder: ['0', '1', '2', '3'], flags: ['3'] } as RundownMetadata;
+154 -105
View File
@@ -17,7 +17,6 @@ import {
TimerPhase, TimerPhase,
TimerState, TimerState,
isOntimeEvent, isOntimeEvent,
runtimeStorePlaceholder,
} from 'ontime-types'; } from 'ontime-types';
import { import {
calculateDuration, calculateDuration,
@@ -44,14 +43,18 @@ import {
hasCrossedMidnight, hasCrossedMidnight,
} from '../services/timerUtils.js'; } from '../services/timerUtils.js';
import { timerConfig } from '../setup/config.js'; import { timerConfig } from '../setup/config.js';
import { runtimePlaceholder } from './__mocks__/runtimeState.mocks.js';
type ExpectedMetadata = { type ExpectedMetadata = {
event: OntimeEvent; event: OntimeEvent;
accumulatedGap: number; accumulatedGap: Duration;
isLinkedToLoaded: boolean; isLinkedToLoaded: boolean;
} | null; } | null;
export type RuntimeState = { export type RuntimeState = {
/**
* @deprecated we want to phase out the 24h clock
*/
clock: TimeOfDay; clock: TimeOfDay;
groupNow: OntimeGroup | null; groupNow: OntimeGroup | null;
eventNow: PlayableEvent | null; eventNow: PlayableEvent | null;
@@ -61,36 +64,72 @@ export type RuntimeState = {
timer: TimerState; timer: TimerState;
rundown: RundownState; rundown: RundownState;
// private properties of the timer calculations // private properties of the timer calculations
};
export type InternalRuntimeState = Omit<RuntimeState, 'clock' | 'timer' | 'offset' | 'rundown'> & {
timer: {
addedTime: Duration;
current: Maybe<Duration>;
duration: Maybe<Duration>;
elapsed: Maybe<Duration>;
expectedFinish: Maybe<TimeOfDay>;
phase: TimerPhase;
playback: Playback;
secondaryTimer: Maybe<Duration>;
startedAt: Maybe<Instant>;
};
offset: {
absolute: Duration;
relative: Duration;
mode: OffsetMode;
expectedGroupEnd: Maybe<TimeOfDay>;
expectedRundownEnd: Maybe<TimeOfDay>;
expectedFlagStart: Maybe<TimeOfDay>;
};
rundown: {
selectedEventIndex: Maybe<number>;
numEvents: number;
plannedStart: Maybe<TimeOfDay>;
plannedEnd: Maybe<TimeOfDay>;
actualStart: Maybe<TimeOfDay>;
currentDay: Maybe<Day>;
actualGroupStart: Maybe<TimeOfDay>;
};
_now: Instant;
_timer: { _timer: {
forceFinish: Maybe<TimeOfDay>; // whether we should declare an event as finished, will contain the finish time forceFinish: boolean; // whether we should declare an event as finished, will contain the finish time
pausedAt: Maybe<TimeOfDay>; pausedAt: Maybe<TimeOfDay>;
pausedDuration: number; pausedDuration: Duration;
secondaryTarget: Maybe<TimeOfDay>; secondaryTarget: Maybe<TimeOfDay>;
hasFinished: boolean; hasFinished: boolean;
}; };
_rundown: { _rundown: {
totalDelay: number; // this value comes from rundown service totalDelay: number; // this value comes from rundown service
}; };
_startDayOffset: Maybe<Day>;
_startEpoch: Maybe<Instant>;
_group: ExpectedMetadata; _group: ExpectedMetadata;
_flag: ExpectedMetadata; _flag: ExpectedMetadata;
_end: ExpectedMetadata; _end: ExpectedMetadata;
_startEpoch: Maybe<Instant>;
_startDayOffset: Maybe<Day>;
}; };
const runtimeState: RuntimeState = { const runtimeState: InternalRuntimeState = {
clock: timeCore.timeOfDayNow(), _now: timeCore.now(),
groupNow: null, groupNow: null,
eventNow: null, eventNow: null,
eventNext: null, eventNext: null,
eventFlag: null, eventFlag: null,
offset: { ...runtimeStorePlaceholder.offset }, offset: { ...runtimePlaceholder.offset },
timer: { ...runtimeStorePlaceholder.timer }, timer: {
rundown: { ...runtimeStorePlaceholder.rundown }, ...runtimePlaceholder.timer,
},
rundown: {
...runtimePlaceholder.rundown,
},
_timer: { _timer: {
forceFinish: null, forceFinish: false,
pausedAt: null, pausedAt: null,
pausedDuration: 0, pausedDuration: 0 as Duration,
secondaryTarget: null, secondaryTarget: null,
hasFinished: false, hasFinished: false,
}, },
@@ -104,19 +143,32 @@ const runtimeState: RuntimeState = {
_startDayOffset: null, _startDayOffset: null,
}; };
export function getInternalState(): Readonly<InternalRuntimeState> {
return runtimeState;
}
export function getState(): Readonly<RuntimeState> { export function getState(): Readonly<RuntimeState> {
// create a shallow copy of the state // create a shallow copy of the state
return { return {
...runtimeState, ...runtimeState,
clock: timeCore.toTimeOfDay(runtimeState._now),
eventNow: runtimeState.eventNow ? { ...runtimeState.eventNow } : null, eventNow: runtimeState.eventNow ? { ...runtimeState.eventNow } : null,
eventNext: runtimeState.eventNext ? { ...runtimeState.eventNext } : null, eventNext: runtimeState.eventNext ? { ...runtimeState.eventNext } : null,
eventFlag: runtimeState.eventFlag ? { ...runtimeState.eventFlag } : null, eventFlag: runtimeState.eventFlag ? { ...runtimeState.eventFlag } : null,
groupNow: runtimeState.groupNow ? { ...runtimeState.groupNow } : null, groupNow: runtimeState.groupNow ? { ...runtimeState.groupNow } : null,
offset: { ...runtimeState.offset }, offset: { ...runtimeState.offset },
rundown: { ...runtimeState.rundown }, rundown: { ...runtimeState.rundown },
timer: { ...runtimeState.timer }, timer: {
_timer: { ...runtimeState._timer }, addedTime: runtimeState.timer.addedTime,
_rundown: { ...runtimeState._rundown }, current: runtimeState.timer.current,
duration: runtimeState.timer.duration,
elapsed: runtimeState.timer.elapsed,
expectedFinish: runtimeState.timer.expectedFinish,
phase: runtimeState.timer.phase,
playback: runtimeState.timer.playback,
secondaryTimer: runtimeState.timer.secondaryTimer,
startedAt: runtimeState.timer.startedAt ? timeCore.toTimeOfDay(runtimeState.timer.startedAt) : null,
},
}; };
} }
@@ -127,22 +179,18 @@ export function clearEventData() {
runtimeState.eventNow = null; runtimeState.eventNow = null;
runtimeState.eventNext = null; runtimeState.eventNext = null;
runtimeState.offset.absolute = 0; runtimeState.offset = { ...runtimePlaceholder.offset };
runtimeState.offset.relative = 0;
runtimeState.offset.expectedFlagStart = null;
runtimeState.offset.expectedGroupEnd = null;
runtimeState.offset.expectedRundownEnd = null;
runtimeState.rundown.selectedEventIndex = null; runtimeState.rundown.selectedEventIndex = null;
runtimeState.timer.playback = Playback.Stop; runtimeState.timer.playback = Playback.Stop;
runtimeState.clock = timeCore.timeOfDayNow(); runtimeState._now = timeCore.now();
runtimeState.timer = { ...runtimeStorePlaceholder.timer }; runtimeState.timer = { ...runtimePlaceholder.timer };
// when clearing, we maintain the total delay from the rundown // when clearing, we maintain the total delay from the rundown
runtimeState._timer.forceFinish = null; runtimeState._timer.forceFinish = false;
runtimeState._timer.pausedAt = null; runtimeState._timer.pausedAt = null;
runtimeState._timer.pausedDuration = 0; runtimeState._timer.pausedDuration = 0 as Duration;
runtimeState._timer.secondaryTarget = null; runtimeState._timer.secondaryTarget = null;
runtimeState._timer.hasFinished = false; runtimeState._timer.hasFinished = false;
} }
@@ -160,22 +208,18 @@ export function clearState() {
runtimeState.rundown.actualStart = null; runtimeState.rundown.actualStart = null;
runtimeState.rundown.selectedEventIndex = null; runtimeState.rundown.selectedEventIndex = null;
runtimeState.offset.absolute = 0; runtimeState.offset = { ...runtimePlaceholder.offset };
runtimeState.offset.relative = 0;
runtimeState.offset.expectedRundownEnd = null;
runtimeState.offset.expectedGroupEnd = null;
runtimeState.offset.expectedFlagStart = null;
runtimeState._end = null; runtimeState._end = null;
runtimeState.timer.playback = Playback.Stop; runtimeState.timer.playback = Playback.Stop;
runtimeState.clock = timeCore.timeOfDayNow(); runtimeState._now = timeCore.now();
runtimeState.timer = { ...runtimeStorePlaceholder.timer }; runtimeState.timer = { ...runtimePlaceholder.timer };
// when clearing, we maintain the total delay from the rundown // when clearing, we maintain the total delay from the rundown
runtimeState._timer.forceFinish = null; runtimeState._timer.forceFinish = false;
runtimeState._timer.pausedAt = null; runtimeState._timer.pausedAt = null;
runtimeState._timer.pausedDuration = 0; runtimeState._timer.pausedDuration = 0 as Duration;
runtimeState._timer.secondaryTarget = null; runtimeState._timer.secondaryTarget = null;
runtimeState._timer.hasFinished = false; runtimeState._timer.hasFinished = false;
@@ -217,9 +261,9 @@ export function updateRundownData(rundownData: {
runtimeState._rundown.totalDelay = rundownData.totalDelay; runtimeState._rundown.totalDelay = rundownData.totalDelay;
runtimeState.rundown.numEvents = rundownData.numEvents; runtimeState.rundown.numEvents = rundownData.numEvents;
runtimeState.rundown.plannedStart = rundownData.firstStart; runtimeState.rundown.plannedStart = rundownData.firstStart as TimeOfDay;
runtimeState.rundown.plannedEnd = runtimeState.rundown.plannedEnd =
rundownData.firstStart === null ? null : rundownData.firstStart + rundownData.totalDuration; rundownData.firstStart === null ? null : ((rundownData.firstStart + rundownData.totalDuration) as TimeOfDay);
if (isPlaybackActive(runtimeState.timer.playback)) getExpectedTimes(); if (isPlaybackActive(runtimeState.timer.playback)) getExpectedTimes();
} }
@@ -253,7 +297,7 @@ export function load(
// update state // update state
runtimeState.timer.playback = Playback.Armed; runtimeState.timer.playback = Playback.Armed;
runtimeState.timer.duration = calculateDuration(event.timeStart, event.timeEnd); runtimeState.timer.duration = event.duration as Duration;
runtimeState.timer.current = getCurrent(runtimeState); runtimeState.timer.current = getCurrent(runtimeState);
runtimeState.rundown.numEvents = metadata.timedEventOrder.length; runtimeState.rundown.numEvents = metadata.timedEventOrder.length;
@@ -267,13 +311,13 @@ export function load(
(firstStart === null || typeof firstStart === 'number') && (firstStart === null || typeof firstStart === 'number') &&
(startEpoch === null || typeof startEpoch === 'number') (startEpoch === null || typeof startEpoch === 'number')
) { ) {
runtimeState.rundown.actualStart = firstStart; runtimeState.rundown.actualStart = firstStart as TimeOfDay;
runtimeState._startEpoch = startEpoch; runtimeState._startEpoch = startEpoch;
if (firstStart !== null && runtimeState.rundown.plannedStart !== null) { if (firstStart !== null && runtimeState.rundown.plannedStart !== null) {
runtimeState._startDayOffset = findDayOffset(runtimeState.rundown.plannedStart, firstStart); runtimeState._startDayOffset = findDayOffset(runtimeState.rundown.plannedStart, firstStart);
} }
if (currentDay !== undefined) { if (currentDay !== undefined) {
runtimeState.rundown.currentDay = currentDay; runtimeState.rundown.currentDay = currentDay as Day;
} }
const { absolute, relative } = getRuntimeOffset(runtimeState); const { absolute, relative } = getRuntimeOffset(runtimeState);
runtimeState.offset.absolute = absolute; runtimeState.offset.absolute = absolute;
@@ -365,16 +409,16 @@ export function updateLoaded(event?: PlayableEvent): string | undefined {
// handle edge cases with roll // handle edge cases with roll
if (runtimeState.timer.playback === Playback.Roll) { if (runtimeState.timer.playback === Playback.Roll) {
const offsetClock = runtimeState.clock - runtimeState.offset.absolute; const offsetClock = timeCore.toTimeOfDay(runtimeState._now) - runtimeState.offset.absolute;
// if waiting to roll, we update the targets and potentially start the timer // if waiting to roll, we update the targets and potentially start the timer
if (runtimeState._timer.secondaryTarget !== null) { if (runtimeState._timer.secondaryTarget !== null) {
if (runtimeState.eventNow.timeStart < offsetClock && offsetClock < runtimeState.eventNow.timeEnd) { if (runtimeState.eventNow.timeStart < offsetClock && offsetClock < runtimeState.eventNow.timeEnd) {
// if the event is now, we queue a start // if the event is now, we queue a start
runtimeState._timer.secondaryTarget = runtimeState.eventNow.timeStart as TimeOfDay; runtimeState._timer.secondaryTarget = runtimeState.eventNow.timeStart as TimeOfDay;
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock; runtimeState.timer.secondaryTimer = (runtimeState._timer.secondaryTarget - offsetClock) as Duration;
} else { } else {
runtimeState._timer.secondaryTarget = normaliseRollStart( runtimeState._timer.secondaryTarget = normaliseRollStart(
runtimeState.eventNow.timeStart, runtimeState.eventNow.timeStart as TimeOfDay,
offsetClock, offsetClock,
) as TimeOfDay; ) as TimeOfDay;
} }
@@ -391,7 +435,7 @@ export function updateLoaded(event?: PlayableEvent): string | undefined {
runtimeState.timer.startedAt = null; runtimeState.timer.startedAt = null;
runtimeState._timer.hasFinished = false; runtimeState._timer.hasFinished = false;
runtimeState.timer.addedTime = 0; runtimeState.timer.addedTime = 0 as Duration;
runtimeState._timer.pausedAt = null; runtimeState._timer.pausedAt = null;
// this could be looked after by the timer // this could be looked after by the timer
@@ -414,7 +458,7 @@ export function updateAll(rundown: Rundown, metadata: RundownMetadata) {
loadGroupFlagAndEnd(rundown, metadata, eventNowIndex); loadGroupFlagAndEnd(rundown, metadata, eventNowIndex);
} }
export function start(state: RuntimeState = runtimeState): boolean { export function start(state: InternalRuntimeState = runtimeState): boolean {
if (state.eventNow === null) { if (state.eventNow === null) {
return false; return false;
} }
@@ -422,22 +466,19 @@ export function start(state: RuntimeState = runtimeState): boolean {
return false; return false;
} }
const epoch = timeCore.now(); state._now = timeCore.now();
const now = timeCore.toTimeOfDay(epoch);
state.clock = now;
state.timer.secondaryTimer = null; state.timer.secondaryTimer = null;
// add paused time if it exists // add paused time if it exists
if (state._timer.pausedAt) { if (state._timer.pausedAt) {
const timeToAdd = state.clock - state._timer.pausedAt; const timeToAdd = (timeCore.toTimeOfDay(state._now) - state._timer.pausedAt) as Duration;
state.timer.addedTime += timeToAdd; state.timer.addedTime = timeCore.combineValues(state.timer.addedTime, timeToAdd);
state._timer.pausedDuration += timeToAdd; state._timer.pausedDuration = timeCore.combineValues(state._timer.pausedDuration, timeToAdd);
state._timer.pausedAt = null; state._timer.pausedAt = null;
} }
if (state.timer.startedAt === null) { if (state.timer.startedAt === null) {
state.timer.startedAt = state.clock; state.timer.startedAt = state._now;
} }
state.timer.playback = Playback.Play; state.timer.playback = Playback.Play;
@@ -445,14 +486,15 @@ export function start(state: RuntimeState = runtimeState): boolean {
state.timer.elapsed = getElapsed(state); state.timer.elapsed = getElapsed(state);
if (state.rundown.actualStart === null) { if (state.rundown.actualStart === null) {
state._startDayOffset = (findDayOffset(state.eventNow.timeStart, state.clock) + state.eventNow.dayOffset) as Day; state._startDayOffset = (findDayOffset(state.eventNow.timeStart, timeCore.toTimeOfDay(state._now)) +
state.eventNow.dayOffset) as Day;
state.rundown.currentDay = state._startDayOffset; state.rundown.currentDay = state._startDayOffset;
state._startEpoch = epoch; state._startEpoch = state._now;
state.rundown.actualStart = state.clock; state.rundown.actualStart = timeCore.toTimeOfDay(state._now);
} }
if (state.groupNow !== null && state.rundown.actualGroupStart === null) { if (state.groupNow !== null && state.rundown.actualGroupStart === null) {
state.rundown.actualGroupStart = state.clock; state.rundown.actualGroupStart = timeCore.toTimeOfDay(state._now);
} }
// update timer phase // update timer phase
@@ -475,18 +517,18 @@ export function start(state: RuntimeState = runtimeState): boolean {
return true; return true;
} }
export function pause(state: RuntimeState = runtimeState): boolean { export function pause(state: InternalRuntimeState = runtimeState): boolean {
if (state.timer.playback !== Playback.Play) { if (state.timer.playback !== Playback.Play) {
return false; return false;
} }
state.timer.playback = Playback.Pause; state.timer.playback = Playback.Pause;
state.clock = timeCore.timeOfDayNow(); state._now = timeCore.now();
state._timer.pausedAt = state.clock; state._timer.pausedAt = timeCore.toTimeOfDay(state._now);
return true; return true;
} }
export function stop(state: RuntimeState = runtimeState): boolean { export function stop(state: InternalRuntimeState = runtimeState): boolean {
if (state.timer.playback === Playback.Stop) { if (state.timer.playback === Playback.Stop) {
return false; return false;
} }
@@ -516,7 +558,7 @@ export function addTime(amount: number) {
if (willGoNegative && !runtimeState._timer.hasFinished) { if (willGoNegative && !runtimeState._timer.hasFinished) {
// set finished time so side effects are triggered // set finished time so side effects are triggered
runtimeState._timer.forceFinish = timeCore.timeOfDayNow(); runtimeState._timer.forceFinish = true;
} else { } else {
const willGoPositive = runtimeState.timer.current < 0 && runtimeState.timer.current + amount > 0; const willGoPositive = runtimeState.timer.current < 0 && runtimeState.timer.current + amount > 0;
if (willGoPositive) { if (willGoPositive) {
@@ -525,8 +567,8 @@ export function addTime(amount: number) {
} }
// we can update the state after handling the side effects // we can update the state after handling the side effects
runtimeState.timer.addedTime += amount; runtimeState.timer.addedTime = timeCore.combineValues(runtimeState.timer.addedTime, amount as Duration);
runtimeState.timer.current += amount; runtimeState.timer.current = timeCore.combineValues(runtimeState.timer.current, amount as Duration);
runtimeState.timer.elapsed = getElapsed(runtimeState); runtimeState.timer.elapsed = getElapsed(runtimeState);
// update runtime delays: over - under // update runtime delays: over - under
@@ -546,10 +588,8 @@ export type UpdateResult = {
export function update(): UpdateResult { export function update(): UpdateResult {
// 0. there are some things we always do // 0. there are some things we always do
const previousClock = runtimeState.clock; const previousClock = timeCore.toTimeOfDay(runtimeState._now);
const epoch = timeCore.now(); runtimeState._now = timeCore.now(); // we update the clock on every update call
const now = timeCore.toTimeOfDay(epoch);
runtimeState.clock = now; // we update the clock on every update call
// 1. is playback idle? // 1. is playback idle?
if (!isPlaybackActive(runtimeState.timer.playback)) { if (!isPlaybackActive(runtimeState.timer.playback)) {
@@ -558,13 +598,13 @@ export function update(): UpdateResult {
// calculate currentDay from epoch (days elapsed since playback was started) // calculate currentDay from epoch (days elapsed since playback was started)
if (runtimeState._startEpoch !== null && runtimeState._startDayOffset !== null) { if (runtimeState._startEpoch !== null && runtimeState._startDayOffset !== null) {
const daysSinceStart = timeCore.daysSinceStart(runtimeState._startEpoch, epoch); const daysSinceStart = timeCore.daysSinceStart(runtimeState._startEpoch, runtimeState._now);
runtimeState.rundown.currentDay = runtimeState._startDayOffset + daysSinceStart; runtimeState.rundown.currentDay = timeCore.combineValues(runtimeState._startDayOffset, daysSinceStart);
} }
// 2. are we waiting to roll? // 2. are we waiting to roll?
if (runtimeState.timer.playback === Playback.Roll && runtimeState.timer.secondaryTimer !== null) { if (runtimeState.timer.playback === Playback.Roll && runtimeState.timer.secondaryTimer !== null) {
const clockHasCrossedMidnight = hasCrossedMidnight(previousClock, now); const clockHasCrossedMidnight = hasCrossedMidnight(previousClock, timeCore.toTimeOfDay(runtimeState._now));
return updateIfWaitingToRoll(clockHasCrossedMidnight); return updateIfWaitingToRoll(clockHasCrossedMidnight);
} }
@@ -618,22 +658,24 @@ export function update(): UpdateResult {
} }
// account for offset // account for offset
const offsetClock = runtimeState.clock + runtimeState.offset.absolute; const offsetClock = timeCore.toTimeOfDay(
timeCore.addDuration(runtimeState._now, runtimeState.offset.absolute as Duration),
);
runtimeState.timer.phase = TimerPhase.Pending; runtimeState.timer.phase = TimerPhase.Pending;
if (hasCrossedMidnight) { if (hasCrossedMidnight) {
// if we crossed midnight, we need to update the target // if we crossed midnight, we need to update the target
// this is the same logic from the roll function // this is the same logic from the roll function
runtimeState._timer.secondaryTarget = normaliseRollStart( runtimeState._timer.secondaryTarget = normaliseRollStart(
runtimeState.eventNow.timeStart, runtimeState.eventNow.timeStart as TimeOfDay,
offsetClock, offsetClock,
) as TimeOfDay; ) as TimeOfDay;
} }
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget! - offsetClock; runtimeState.timer.secondaryTimer = (runtimeState._timer.secondaryTarget! - offsetClock) as Duration;
return { return {
hasTimerFinished: false, hasTimerFinished: false,
hasSecondaryTimerFinished: runtimeState.timer.secondaryTimer <= 0, hasSecondaryTimerFinished: runtimeState.timer.secondaryTimer! <= 0,
}; };
} }
} }
@@ -650,9 +692,7 @@ export function roll(
} }
// we will need to do some calculations, update the time first // we will need to do some calculations, update the time first
const epoch = timeCore.now(); runtimeState._now = timeCore.now();
const now = timeCore.toTimeOfDay(epoch);
runtimeState.clock = now;
// 2. if there is an event armed, we use it // 2. if there is an event armed, we use it
if (runtimeState.timer.playback === Playback.Armed || runtimeState.timer.phase === TimerPhase.Pending) { if (runtimeState.timer.playback === Playback.Armed || runtimeState.timer.phase === TimerPhase.Pending) {
@@ -664,7 +704,9 @@ export function roll(
} }
if (offset) { if (offset) {
runtimeState.offset = { ...offset }; runtimeState.offset = {
...offset,
} as InternalRuntimeState['offset'];
} }
runtimeState.timer.playback = Playback.Roll; runtimeState.timer.playback = Playback.Roll;
@@ -673,15 +715,16 @@ export function roll(
runtimeState.eventNow.timeEnd < runtimeState.eventNow.timeStart runtimeState.eventNow.timeEnd < runtimeState.eventNow.timeStart
? runtimeState.eventNow.timeEnd + dayInMs ? runtimeState.eventNow.timeEnd + dayInMs
: runtimeState.eventNow.timeEnd; : runtimeState.eventNow.timeEnd;
runtimeState.timer.expectedFinish = normalisedEndTime; runtimeState.timer.expectedFinish = normalisedEndTime as TimeOfDay;
// account for offset // account for offset
const offsetClock = runtimeState.clock - runtimeState.offset.absolute; // const offsetClock = timeCore.addDuration(runtimeState._now, -runtimeState.offset.absolute as Duration);
const offsetClock = timeCore.toTimeOfDay(runtimeState._now) - runtimeState.offset.absolute;
// state catch up // state catch up
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, normalisedEndTime); runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, normalisedEndTime);
runtimeState.timer.current = runtimeState.timer.duration; runtimeState.timer.current = runtimeState.timer.duration;
runtimeState.timer.elapsed = 0; runtimeState.timer.elapsed = 0 as Duration;
// check if the event is ready to start or if needs to be pending // check if the event is ready to start or if needs to be pending
const isNow = checkIsNow(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd, offsetClock); const isNow = checkIsNow(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd, offsetClock);
@@ -690,8 +733,8 @@ export function roll(
* If we are starting an event in roll mode * If we are starting an event in roll mode
* we backtrace all the start times to the supposed start time of the event * we backtrace all the start times to the supposed start time of the event
*/ */
const plannedStart = runtimeState.eventNow.timeStart; const plannedStart = runtimeState.eventNow.timeStart as TimeOfDay;
runtimeState.timer.startedAt = plannedStart; runtimeState.timer.startedAt = timeCore.toInstant(plannedStart, runtimeState._now);
// reset the secondary timer to cancel any countdowns // reset the secondary timer to cancel any countdowns
runtimeState.timer.secondaryTimer = null; runtimeState.timer.secondaryTimer = null;
@@ -709,18 +752,20 @@ export function roll(
runtimeState._startDayOffset = (findDayOffset(runtimeState.eventNow.timeStart, plannedStart) + runtimeState._startDayOffset = (findDayOffset(runtimeState.eventNow.timeStart, plannedStart) +
runtimeState.eventNow.dayOffset) as Day; runtimeState.eventNow.dayOffset) as Day;
// backdate _startEpoch to when the event conceptually started // backdate _startEpoch to when the event conceptually started
const timeElapsed = timeCore.elapsedTime(runtimeState.clock, plannedStart as TimeOfDay); const timeElapsed = timeCore.elapsedTime(timeCore.toTimeOfDay(runtimeState._now), plannedStart as TimeOfDay);
runtimeState._startEpoch = timeCore.addDuration(epoch, -timeElapsed as Duration); runtimeState._startEpoch = timeCore.addDuration(runtimeState._now, -timeElapsed as Duration);
// calculate currentDay from the backdated epoch // calculate currentDay from the backdated epoch
runtimeState.rundown.currentDay = runtimeState.rundown.currentDay = timeCore.combineValues(
runtimeState._startDayOffset + timeCore.daysSinceStart(runtimeState._startEpoch, epoch); runtimeState._startDayOffset,
timeCore.daysSinceStart(runtimeState._startEpoch, runtimeState._now),
);
} }
} else { } else {
runtimeState._timer.secondaryTarget = normaliseRollStart( runtimeState._timer.secondaryTarget = normaliseRollStart(
runtimeState.eventNow.timeStart, runtimeState.eventNow.timeStart as TimeOfDay,
offsetClock, offsetClock,
) as TimeOfDay; ) as TimeOfDay;
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock; runtimeState.timer.secondaryTimer = (runtimeState._timer.secondaryTarget - offsetClock) as Duration;
runtimeState.timer.phase = TimerPhase.Pending; runtimeState.timer.phase = TimerPhase.Pending;
} }
@@ -737,9 +782,9 @@ export function roll(
// account for offset but we only keep it if passed to us // account for offset but we only keep it if passed to us
if (offset) { if (offset) {
runtimeState.offset = { ...offset }; runtimeState.offset = { ...offset } as InternalRuntimeState['offset'];
} }
const offsetClock = runtimeState.clock - runtimeState.offset.absolute; const offsetClock = timeCore.toTimeOfDay(runtimeState._now) - runtimeState.offset.absolute;
const { index, isPending } = loadRoll(rundown, metadata, offsetClock); const { index, isPending } = loadRoll(rundown, metadata, offsetClock);
@@ -766,7 +811,7 @@ export function roll(
runtimeState.timer.phase = TimerPhase.Pending; runtimeState.timer.phase = TimerPhase.Pending;
// we need to normalise start time in case it is the day after // we need to normalise start time in case it is the day after
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, offsetClock) as TimeOfDay; runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, offsetClock) as TimeOfDay;
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock; runtimeState.timer.secondaryTimer = (runtimeState._timer.secondaryTarget - offsetClock) as Duration;
// preload timer properties // preload timer properties
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd); runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd);
@@ -789,18 +834,18 @@ export function roll(
* when we start in roll mode * when we start in roll mode
* we need to backtrace all times to the supposed start time of the event * we need to backtrace all times to the supposed start time of the event
*/ */
const plannedStart = runtimeState.eventNow.timeStart; const plannedStart = runtimeState.eventNow.timeStart as TimeOfDay;
const endTime = const endTime =
runtimeState.eventNow.timeEnd < runtimeState.eventNow.timeStart runtimeState.eventNow.timeEnd < runtimeState.eventNow.timeStart
? runtimeState.eventNow.timeEnd + dayInMs ? runtimeState.eventNow.timeEnd + dayInMs
: runtimeState.eventNow.timeEnd; : runtimeState.eventNow.timeEnd;
runtimeState.timer.startedAt = plannedStart; runtimeState.timer.startedAt = timeCore.toInstant(plannedStart, runtimeState._now);
runtimeState.timer.expectedFinish = endTime; runtimeState.timer.expectedFinish = endTime as TimeOfDay;
// state catch up // state catch up
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, endTime); runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, endTime);
runtimeState.timer.current = getCurrent(runtimeState); runtimeState.timer.current = getCurrent(runtimeState);
runtimeState.timer.elapsed = 0; runtimeState.timer.elapsed = 0 as Duration;
// update runtime // update runtime
runtimeState.rundown.actualStart = plannedStart; runtimeState.rundown.actualStart = plannedStart;
@@ -813,11 +858,11 @@ export function roll(
runtimeState._startDayOffset = (findDayOffset(runtimeState.eventNow.timeStart, plannedStart) + runtimeState._startDayOffset = (findDayOffset(runtimeState.eventNow.timeStart, plannedStart) +
runtimeState.eventNow.dayOffset) as Day; runtimeState.eventNow.dayOffset) as Day;
// backdate _startEpoch to when the event conceptually started // backdate _startEpoch to when the event conceptually started
const timeElapsed = timeCore.elapsedTime(runtimeState.clock, plannedStart as TimeOfDay); const timeElapsed = timeCore.elapsedTime(timeCore.toTimeOfDay(runtimeState._now), plannedStart as TimeOfDay);
runtimeState._startEpoch = timeCore.addDuration(epoch, -timeElapsed as Duration); runtimeState._startEpoch = timeCore.addDuration(runtimeState._now, -timeElapsed as Duration);
// calculate currentDay from the backdated epoch // calculate currentDay from the backdated epoch
runtimeState.rundown.currentDay = (runtimeState._startDayOffset + runtimeState.rundown.currentDay = (runtimeState._startDayOffset +
timeCore.daysSinceStart(runtimeState._startEpoch, epoch)) as Day; timeCore.daysSinceStart(runtimeState._startEpoch, runtimeState._now)) as Day;
return { eventId: runtimeState.eventNow.id, didStart: true }; return { eventId: runtimeState.eventNow.id, didStart: true };
} }
@@ -852,7 +897,11 @@ function getExpectedTimes(state = runtimeState) {
plannedStart, plannedStart,
actualStart, actualStart,
}); });
state.offset.expectedGroupEnd = getExpectedEnd(lastEvent, lastEventExpectedStart, state.rundown.currentDay!); state.offset.expectedGroupEnd = getExpectedEnd(
lastEvent,
lastEventExpectedStart,
state.rundown.currentDay!,
) as TimeOfDay;
} }
} }
@@ -869,7 +918,7 @@ function getExpectedTimes(state = runtimeState) {
plannedStart, plannedStart,
actualStart, actualStart,
}); });
state.offset.expectedFlagStart = expectedStart; state.offset.expectedFlagStart = expectedStart as TimeOfDay;
} }
} }
@@ -884,7 +933,7 @@ function getExpectedTimes(state = runtimeState) {
plannedStart, plannedStart,
actualStart, actualStart,
}); });
state.offset.expectedRundownEnd = getExpectedEnd(event, expectedStart, state.rundown.currentDay!); state.offset.expectedRundownEnd = getExpectedEnd(event, expectedStart, state.rundown.currentDay!) as TimeOfDay;
} }
} }
@@ -957,13 +1006,13 @@ export function loadGroupFlagAndEnd(
if (!foundFlag && metadata.flags.includes(entry.id)) { if (!foundFlag && metadata.flags.includes(entry.id)) {
foundFlag = true; foundFlag = true;
state.eventFlag = entry as PlayableEvent; // we know it is playable as it is coming from the playableEventOrder list state.eventFlag = entry as PlayableEvent; // we know it is playable as it is coming from the playableEventOrder list
state._flag = { event: entry, isLinkedToLoaded, accumulatedGap }; state._flag = { event: entry, isLinkedToLoaded, accumulatedGap: accumulatedGap as Duration };
} }
} }
if (!foundGroupEnd && entry.id === lastEventInGroup?.id) { if (!foundGroupEnd && entry.id === lastEventInGroup?.id) {
foundGroupEnd = true; foundGroupEnd = true;
state._group = { event: lastEventInGroup, isLinkedToLoaded, accumulatedGap }; state._group = { event: lastEventInGroup, isLinkedToLoaded, accumulatedGap: accumulatedGap as Duration };
} }
} }
} }
@@ -971,7 +1020,7 @@ export function loadGroupFlagAndEnd(
const lastID = playableEventOrder.at(-1); const lastID = playableEventOrder.at(-1);
const lastEvent = lastID ? (entries[lastID] as OntimeEvent) : null; const lastEvent = lastID ? (entries[lastID] as OntimeEvent) : null;
if (lastEvent) { if (lastEvent) {
state._end = { event: lastEvent, isLinkedToLoaded, accumulatedGap }; state._end = { event: lastEvent, isLinkedToLoaded, accumulatedGap: accumulatedGap as Duration };
} }
} }
@@ -1,4 +1,4 @@
import type { OntimeEvent } from 'ontime-types'; import type { Duration, OntimeEvent } from 'ontime-types';
import { TimeStrategy } from 'ontime-types'; import { TimeStrategy } from 'ontime-types';
import { dayInMs } from '../date-utils/conversionUtils.js'; import { dayInMs } from '../date-utils/conversionUtils.js';
@@ -103,16 +103,17 @@ export function validateTimes(
/** /**
* @description calculates event duration considering midnight * @description calculates event duration considering midnight
* @deprecated - duration is the more useful value from the event data, prefer using that directly
* @param {number} timeStart * @param {number} timeStart
* @param {number} timeEnd * @param {number} timeEnd
* @returns {number} * @returns {number}
*/ */
export function calculateDuration(timeStart: number, timeEnd: number): number { export function calculateDuration(timeStart: number, timeEnd: number): Duration {
// Durations must be positive // Durations must be positive
if (timeEnd < timeStart) { if (timeEnd < timeStart) {
return timeEnd + dayInMs - timeStart; return (timeEnd + dayInMs - timeStart) as Duration;
} }
return timeEnd - timeStart; return (timeEnd - timeStart) as Duration;
} }
export function calculateEnd(timeStart: number, duration: number): number { export function calculateEnd(timeStart: number, duration: number): number {