update API - part 1 (#709)

* api: `test-ontime` -> `version`

* api: `ontime-poll` -> `poll`

* api: `start-next` -> `startnext`

* api: `start-next` ->  `startnext`

---------

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
Co-authored-by: Carlos Valente <carlosvalente@pm.me>
This commit is contained in:
Alex Christoffer Rasmussen
2024-01-23 08:43:00 +01:00
committed by GitHub
parent cb8ba776e2
commit 0cd2ca4191
23 changed files with 432 additions and 393 deletions
+20 -20
View File
@@ -24,10 +24,10 @@ export const useOperator = () => {
export const useMessageControl = () => { export const useMessageControl = () => {
const featureSelector = (state: RuntimeStore) => ({ const featureSelector = (state: RuntimeStore) => ({
timerMessage: state.timerMessage, timer: state.message.timer,
publicMessage: state.publicMessage, public: state.message.public,
lowerMessage: state.lowerMessage, lower: state.message.lower,
externalMessage: state.externalMessage, external: state.message.external,
onAir: state.onAir, onAir: state.onAir,
}); });
@@ -35,17 +35,17 @@ export const useMessageControl = () => {
}; };
export const setMessage = { export const setMessage = {
presenterText: (payload: string) => socketSendJson('set-timer-message-text', payload), timerText: (payload: string) => socketSendJson('message', { timer: { text: payload } }),
presenterVisible: (payload: boolean) => socketSendJson('set-timer-message-visible', payload), timerVisible: (payload: boolean) => socketSendJson('message', { timer: { visible: payload } }),
publicText: (payload: string) => socketSendJson('set-public-message-text', payload), publicText: (payload: string) => socketSendJson('message', { public: { text: payload } }),
publicVisible: (payload: boolean) => socketSendJson('set-public-message-visible', payload), publicVisible: (payload: boolean) => socketSendJson('message', { public: { visible: payload } }),
lowerText: (payload: string) => socketSendJson('set-lower-message-text', payload), lowerText: (payload: string) => socketSendJson('message', { lower: { text: payload } }),
lowerVisible: (payload: boolean) => socketSendJson('set-lower-message-visible', payload), lowerVisible: (payload: boolean) => socketSendJson('message', { lower: { visible: payload } }),
externalText: (payload: string) => socketSendJson('set-external-message-text', payload), externalText: (payload: string) => socketSendJson('message', { external: { visible: payload } }),
externalVisible: (payload: boolean) => socketSendJson('set-external-message-visible', payload), externalVisible: (payload: boolean) => socketSendJson('message', { external: { visible: payload } }),
onAir: (payload: boolean) => socketSendJson('set-onAir', payload), onAir: (payload: boolean) => socketSendJson('onAir', payload),
timerBlink: (payload: boolean) => socketSendJson('set-timer-blink', payload), timerBlink: (payload: boolean) => socketSendJson('message', { timer: { blink: payload } }),
timerBlackout: (payload: boolean) => socketSendJson('set-timer-blackout', payload), timerBlackout: (payload: boolean) => socketSendJson('message', { timer: { blackout: payload } }),
}; };
export const usePlaybackControl = () => { export const usePlaybackControl = () => {
@@ -62,12 +62,12 @@ export const setPlayback = {
start: () => socketSendJson('start'), start: () => socketSendJson('start'),
pause: () => socketSendJson('pause'), pause: () => socketSendJson('pause'),
roll: () => socketSendJson('roll'), roll: () => socketSendJson('roll'),
startNext: () => socketSendJson('start-next'), startNext: () => socketSendJson('start', { next: true }),
previous: () => { previous: () => {
socketSendJson('previous'); socketSendJson('load', { previous: true });
}, },
next: () => { next: () => {
socketSendJson('next'); socketSendJson('load', { next: true });
}, },
stop: () => { stop: () => {
socketSendJson('stop'); socketSendJson('stop');
@@ -93,8 +93,8 @@ export const useCuesheet = () => {
}; };
export const setEventPlayback = { export const setEventPlayback = {
loadEvent: (eventId: string) => socketSendJson('loadid', eventId), loadEvent: (id: string) => socketSendJson('load', { id }),
startEvent: (eventId: string) => socketSendJson('startid', eventId), startEvent: (id: string) => socketSendJson('start', { id }),
start: () => socketSendJson('start'), start: () => socketSendJson('start'),
pause: () => socketSendJson('pause'), pause: () => socketSendJson('pause'),
}; };
+19 -17
View File
@@ -19,23 +19,25 @@ export const runtimeStorePlaceholder: RuntimeStore = {
timeDanger: null, timeDanger: null,
}, },
playback: Playback.Stop, playback: Playback.Stop,
timerMessage: { message: {
text: '', timer: {
visible: false, text: '',
timerBlink: false, visible: false,
timerBlackout: false, blink: false,
}, blackout: false,
publicMessage: { },
text: '', public: {
visible: false, text: '',
}, visible: false,
lowerMessage: { },
text: '', lower: {
visible: false, text: '',
}, visible: false,
externalMessage: { },
text: '', external: {
visible: false, text: '',
visible: false,
},
}, },
onAir: false, onAir: false,
loaded: { loaded: {
+2 -14
View File
@@ -87,21 +87,9 @@ export const connectSocket = (preferredClientName?: string) => {
runtime.setState(state); runtime.setState(state);
break; break;
} }
case 'ontime-timerMessage': { case 'ontime-message': {
const state = runtime.getState(); const state = runtime.getState();
state.timerMessage = payload; state.message = payload;
runtime.setState(state);
break;
}
case 'ontime-publicMessage': {
const state = runtime.getState();
state.publicMessage = payload;
runtime.setState(state);
break;
}
case 'ontime-lowerMessage': {
const state = runtime.getState();
state.lowerMessage = payload;
runtime.setState(state); runtime.setState(state);
break; break;
} }
@@ -11,41 +11,42 @@ import InputRow from './InputRow';
import style from './MessageControl.module.scss'; import style from './MessageControl.module.scss';
export default function MessageControl() { export default function MessageControl() {
const data = useMessageControl(); const message = useMessageControl();
const blink = message.timer.blink;
const blackout = message.timer.blackout;
return ( return (
<div className={style.messageContainer}> <div className={style.messageContainer}>
<InputRow <InputRow
label='Public / Backstage screen message' label='Public / Backstage screen message'
placeholder='Shown in public and backstage screens' placeholder='Shown in public and backstage screens'
text={data.publicMessage.text || ''} text={message.public.text || ''}
visible={data.publicMessage.visible || false} visible={message.public.visible || false}
changeHandler={(newValue) => setMessage.publicText(newValue)} changeHandler={(newValue) => setMessage.publicText(newValue)}
actionHandler={() => setMessage.publicVisible(!data.publicMessage.visible)} actionHandler={() => setMessage.publicVisible(!message.public.visible)}
/> />
<InputRow <InputRow
label='Lower third message' label='Lower third message'
placeholder='Shown in lower third' placeholder='Shown in lower third'
text={data.lowerMessage.text || ''} text={message.lower.text || ''}
visible={data.lowerMessage.visible || false} visible={message.lower.visible || false}
changeHandler={(newValue) => setMessage.lowerText(newValue)} changeHandler={(newValue) => setMessage.lowerText(newValue)}
actionHandler={() => setMessage.lowerVisible(!data.lowerMessage.visible)} actionHandler={() => setMessage.lowerVisible(!message.lower.visible)}
/> />
<InputRow <InputRow
label='Timer' label='Timer'
placeholder='Message shown in stage timer' placeholder='Message shown in stage timer'
text={data.timerMessage.text || ''} text={message.timer.text || ''}
visible={data.timerMessage.visible || false} visible={message.timer.visible || false}
changeHandler={(newValue) => setMessage.presenterText(newValue)} changeHandler={(newValue) => setMessage.timerText(newValue)}
actionHandler={() => setMessage.presenterVisible(!data.timerMessage.visible)} actionHandler={() => setMessage.timerVisible(!message.timer.visible)}
/> />
<div className={style.buttonSection}> <div className={style.buttonSection}>
<Button <Button
size='sm' size='sm'
className={`${data.timerMessage.timerBlink ? style.blink : ''}`} className={`${blink ? style.blink : ''}`}
variant={data.timerMessage.timerBlink ? 'ontime-filled' : 'ontime-subtle'} variant={blink ? 'ontime-filled' : 'ontime-subtle'}
leftIcon={data.timerMessage.timerBlink ? <IoSunny size='1rem' /> : <IoSunnyOutline size='1rem' />} leftIcon={blink ? <IoSunny size='1rem' /> : <IoSunnyOutline size='1rem' />}
onClick={() => setMessage.timerBlink(!data.timerMessage.timerBlink)} onClick={() => setMessage.timerBlink(!blink)}
data-testid='toggle timer blink' data-testid='toggle timer blink'
> >
Blink message Blink message
@@ -53,9 +54,9 @@ export default function MessageControl() {
<Button <Button
size='sm' size='sm'
className={style.blackoutButton} className={style.blackoutButton}
variant={data.timerMessage.timerBlackout ? 'ontime-filled' : 'ontime-subtle'} variant={blackout ? 'ontime-filled' : 'ontime-subtle'}
leftIcon={data.timerMessage.timerBlackout ? <IoEye size='1rem' /> : <IoEyeOffOutline size='1rem' />} leftIcon={blackout ? <IoEye size='1rem' /> : <IoEyeOffOutline size='1rem' />}
onClick={() => setMessage.timerBlackout(!data.timerMessage.timerBlackout)} onClick={() => setMessage.timerBlackout(!blackout)}
data-testid='toggle timer blackout' data-testid='toggle timer blackout'
> >
Blackout screen Blackout screen
@@ -65,8 +66,8 @@ export default function MessageControl() {
label='External Message' label='External Message'
placeholder='-' placeholder='-'
readonly readonly
text={data.externalMessage.text || ''} text={message.external.text || ''}
visible={data.externalMessage.visible || false} visible={message.external.visible || false}
changeHandler={() => undefined} changeHandler={() => undefined}
actionHandler={() => undefined} actionHandler={() => undefined}
/> />
@@ -55,20 +55,8 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
}, [rundownData]); }, [rundownData]);
// websocket data // websocket data
const { const { timer, message, playback, onAir, eventNext, publicEventNext, publicEventNow, eventNow, loaded } =
timer, useStore(runtime);
publicMessage,
timerMessage,
lowerMessage,
externalMessage,
playback,
onAir,
eventNext,
publicEventNext,
publicEventNow,
eventNow,
loaded,
} = useStore(runtime);
const publicSelectedId = loaded.selectedPublicEventId; const publicSelectedId = loaded.selectedPublicEventId;
const selectedId = loaded.selectedEventId; const selectedId = loaded.selectedEventId;
const nextId = loaded.nextEventId; const nextId = loaded.nextEventId;
@@ -93,10 +81,10 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
<Component <Component
{...props} {...props}
isMirrored={isMirrored} isMirrored={isMirrored}
pres={timerMessage} pres={message.timer}
publ={publicMessage} publ={message.public}
lower={lowerMessage} lower={message.lower}
external={externalMessage} external={message.external}
eventNow={eventNow} eventNow={eventNow}
publicEventNow={publicEventNow} publicEventNow={publicEventNow}
eventNext={eventNext} eventNext={eventNext}
@@ -141,8 +141,8 @@ export default function MinimalTimer(props: MinimalTimerProps) {
const showProgress = time.playback !== Playback.Stop; const showProgress = time.playback !== Playback.Stop;
const showWarning = (time.current ?? 1) < (time.timeWarning ?? 0); const showWarning = (time.current ?? 1) < (time.timeWarning ?? 0);
const showDanger = (time.current ?? 1) < (time.timeDanger ?? 0); const showDanger = (time.current ?? 1) < (time.timeDanger ?? 0);
const showBlinking = pres.timerBlink; const showBlinking = pres.blink;
const showBlackout = pres.timerBlackout; const showBlackout = pres.blackout;
const timerColor = userOptions.textColour const timerColor = userOptions.textColour
? userOptions.textColour ? userOptions.textColour
@@ -102,8 +102,8 @@ export default function Timer(props: TimerProps) {
const showFinished = finished && (time.timerType !== TimerType.Clock || showEndMessage); const showFinished = finished && (time.timerType !== TimerType.Clock || showEndMessage);
const showWarning = (time.current ?? 1) < (eventNow?.timeWarning ?? 0); const showWarning = (time.current ?? 1) < (eventNow?.timeWarning ?? 0);
const showDanger = (time.current ?? 1) < (eventNow?.timeDanger ?? 0); const showDanger = (time.current ?? 1) < (eventNow?.timeDanger ?? 0);
const showBlinking = pres.timerBlink; const showBlinking = pres.blink;
const showBlackout = pres.timerBlackout; const showBlackout = pres.blackout;
const showClock = time.timerType !== TimerType.Clock; const showClock = time.timerType !== TimerType.Clock;
const showExternal = external.visible && external.text; const showExternal = external.visible && external.text;
+5 -2
View File
@@ -5,6 +5,7 @@ import { Server } from 'node-osc';
import { IAdapter } from './IAdapter.js'; import { IAdapter } from './IAdapter.js';
import { dispatchFromAdapter, type ChangeOptions } from '../controllers/integrationController.js'; import { dispatchFromAdapter, type ChangeOptions } from '../controllers/integrationController.js';
import { logger } from '../classes/Logger.js'; import { logger } from '../classes/Logger.js';
import { objectFromPath } from './utils/parse.js';
export class OscServer implements IAdapter { export class OscServer implements IAdapter {
private readonly osc: Server; private readonly osc: Server;
@@ -18,6 +19,7 @@ export class OscServer implements IAdapter {
// message should look like /ontime/{path}/{params?} {args} where // message should look like /ontime/{path}/{params?} {args} where
// ontime: fixed message for app // ontime: fixed message for app
// path: command to be called // path: command to be called
// params: used to create a nested object to patch with
// args: extra data, only used on some API entries (delay, goto) // args: extra data, only used on some API entries (delay, goto)
// split message // split message
@@ -38,7 +40,7 @@ export class OscServer implements IAdapter {
let transformedPayload: unknown = args; let transformedPayload: unknown = args;
// we need to transform the params for the change endpoint // we need to transform the params for the change endpoint
// OSC: ontime/change/{eventID}/{propertyName} value // OSC: /ontime/change/{eventID}/{propertyName} value
if (path === 'change') { if (path === 'change') {
if (params.length < 2) { if (params.length < 2) {
logger.error(LogOrigin.Rx, 'OSC IN: No params provided for change'); logger.error(LogOrigin.Rx, 'OSC IN: No params provided for change');
@@ -59,10 +61,11 @@ export class OscServer implements IAdapter {
property, property,
value, value,
} satisfies ChangeOptions; } satisfies ChangeOptions;
} else if (params.length) {
transformedPayload = objectFromPath(params, args);
} }
try { try {
// we dont reply on OSC
dispatchFromAdapter( dispatchFromAdapter(
path, path,
{ {
@@ -0,0 +1,44 @@
import { objectFromPath } from '../parse.js';
describe('objectFromPath()', () => {
it('start index', () => {
const arr = ['index'];
const value = 1;
const objExpected = { index: 1 };
const obj = objectFromPath(arr, value);
expect(obj).toStrictEqual(objExpected);
});
it('set timer message text', () => {
const arr = ['timer', 'text'];
const value = 'hello';
const objExpected = { timer: { text: value } };
const obj = objectFromPath(arr, value);
expect(obj).toStrictEqual(objExpected);
});
it('set timer message text and visible', () => {
const arr = ['timer'];
const value = { text: 'hello', visible: true };
const objExpected = { timer: value };
const obj = objectFromPath(arr, value);
expect(obj).toStrictEqual(objExpected);
});
it('nests object with undefined value', () => {
const arr = ['a', 'b', 'c', 'd'];
const objExpected = { a: { b: { c: { d: undefined } } } };
const obj = objectFromPath(arr);
expect(obj).toStrictEqual(objExpected);
});
it('empty array creates undefinde object', () => {
const arr = [];
const value = '1234567890';
const objExpected = null;
const obj = objectFromPath(arr, value);
expect(obj).toStrictEqual(objExpected);
});
});
+11
View File
@@ -0,0 +1,11 @@
/**
* @description Creates a nested object with keys from an array and assigns the `value` to the last key
* @param {array} path - array to be nested
* @param {string} value - value to assign
* @returns {object | null} nested object or null if no object was created
*/
export const objectFromPath = (path: string[], value?: unknown): object | null => {
const obj = path.reduceRight((result, key) => ({ [key]: result }), value);
if (typeof obj === 'object') return obj;
return null;
};
+1 -4
View File
@@ -174,10 +174,7 @@ export const startServer = async () => {
* - Runtime Service publicEventNext * - Runtime Service publicEventNext
*/ */
eventStore.init({ eventStore.init({
timerMessage: messageService.timerMessage, message: messageService,
publicMessage: messageService.publicMessage,
lowerMessage: messageService.lowerMessage,
externalMessage: messageService.externalMessage,
onAir: state.playback !== Playback.Stop, onAir: state.playback !== Playback.Stop,
timer: state.timer, timer: state.timer,
playback: state.playback, playback: state.playback,
@@ -1,11 +1,14 @@
import { DeepPartial, MessageState } from 'ontime-types';
// skipcq: JS-C1003 - we like the API // skipcq: JS-C1003 - we like the API
import * as assert from '../utils/assert.js'; import * as assert from '../utils/assert.js';
import { ONTIME_VERSION } from '../ONTIME_VERSION.js'; import { ONTIME_VERSION } from '../ONTIME_VERSION.js';
import { isPartialTimerMessage, messageService } from '../services/message-service/MessageService.js'; import { messageService } from '../services/message-service/MessageService.js';
import { runtimeService } from '../services/runtime-service/RuntimeService.js'; import { runtimeService } from '../services/runtime-service/RuntimeService.js';
import { eventStore } from '../stores/EventStore.js'; import { eventStore } from '../stores/EventStore.js';
import { parse, updateEvent } from './integrationController.config.js'; import { parse, updateEvent } from './integrationController.config.js';
import { validateMessage, validateTimerMessage } from '../services/message-service/messageUtils.js';
export type ChangeOptions = { export type ChangeOptions = {
eventId: string; eventId: string;
@@ -32,11 +35,10 @@ export function dispatchFromAdapter(
type ActionHandler = (payload: unknown) => { payload: unknown }; type ActionHandler = (payload: unknown) => { payload: unknown };
// TODO: add data to missing returns once available
const actionHandlers: Record<string, ActionHandler> = { const actionHandlers: Record<string, ActionHandler> = {
/* General */ /* General */
'test-ontime': () => ({ payload: `Hello from Ontime version ${ONTIME_VERSION}` }), version: () => ({ payload: ONTIME_VERSION }),
'ontime-poll': () => ({ poll: () => ({
payload: eventStore.poll(), payload: eventStore.poll(),
}), }),
change: (payload) => { change: (payload) => {
@@ -48,123 +50,56 @@ const actionHandlers: Record<string, ActionHandler> = {
return { payload: updatedEvent }; return { payload: updatedEvent };
}, },
/* Message Service */ /* Message Service */
'set-timer-message': (payload) => { message: (payload) => {
if (!isPartialTimerMessage(payload)) { assert.isObject(payload);
throw new Error('Payload is not a valid timer message');
} const patch: DeepPartial<MessageState> = {
const newState = messageService.setTimerMessage(payload); timer: 'timer' in payload ? validateTimerMessage(payload.timer) : undefined,
return { payload: newState.timerMessage }; public: 'public' in payload ? validateMessage(payload.public) : undefined,
}, lower: 'lower' in payload ? validateMessage(payload.lower) : undefined,
'set-timer-blink': (payload) => { external: 'external' in payload ? validateMessage(payload.external) : undefined,
assert.isDefined(payload); };
const newState = messageService.setTimerBlink(Boolean(payload));
return { payload: newState.timerMessage }; const newMessage = messageService.patch(patch);
}, return { payload: newMessage };
'set-timer-blackout': (payload) => {
assert.isDefined(payload);
const newState = messageService.setTimerBlackout(Boolean(payload));
return { payload: newState.timerMessage };
},
'set-timer-message-text': (payload) => {
assert.isString(payload);
const newState = messageService.setTimerText(payload);
return { payload: newState.timerMessage };
},
'set-timer-message-visible': (payload) => {
assert.isDefined(payload);
const newState = messageService.setTimerVisibility(Boolean(payload));
return { payload: newState.timerMessage };
},
'set-public-message-text': (payload) => {
assert.isString(payload);
const newState = messageService.setPublicText(payload);
return { payload: newState.publicMessage };
},
'set-public-message-visible': (payload) => {
assert.isDefined(payload);
const newState = messageService.setPublicVisibility(Boolean(payload));
return { payload: newState.publicMessage };
},
'set-lower-message-text': (payload) => {
assert.isString(payload);
const newState = messageService.setLowerText(payload);
return { payload: newState.lowerMessage };
},
'set-lower-message-visible': (payload) => {
assert.isDefined(payload);
const newState = messageService.setLowerVisibility(Boolean(payload));
return { payload: newState.lowerMessage };
},
'set-external-message-text': (payload) => {
assert.isString(payload);
const newState = messageService.setExternalText(payload);
return { payload: newState.externalMessage };
},
'set-external-message-visible': (payload) => {
assert.isDefined(payload);
const newState = messageService.setExternalVisibility(Boolean(payload));
return { payload: newState.externalMessage };
}, },
/* Playback */ /* Playback */
start: (payload) => { start: (payload) => {
if (payload && typeof payload === 'object') { if (payload && typeof payload === 'object') {
if ('next' in payload) {
runtimeService.startNext();
return { payload: 'start' };
}
if ('index' in payload) { if ('index' in payload) {
const reply = actionHandlers.startindex(payload.index); const eventIndex = numberOrError(payload.index);
return reply; if (eventIndex <= 0) {
throw new Error(`Event index out of range ${eventIndex}`);
}
// Indexes in frontend are 1 based
const success = runtimeService.startByIndex(eventIndex - 1);
if (!success) {
throw new Error(`Event index not recognised or out of range ${eventIndex}`);
}
return { payload: 'success' };
} }
if ('id' in payload) { if ('id' in payload) {
const reply = actionHandlers.startid(payload.id); assert.isString(payload);
return reply; runtimeService.startById(payload);
return { payload: 'success' };
} }
if ('cue' in payload) { if ('cue' in payload) {
const reply = actionHandlers.startcue(payload.cue); assert.isString(payload);
return reply; runtimeService.startByCue(payload);
return { payload: 'success' };
} }
} }
runtimeService.start(); runtimeService.start();
return { payload: 'start' }; return { payload: 'start' };
}, },
'start-next': () => {
runtimeService.startNext();
return { payload: 'start' };
},
startindex: (payload) => {
const eventIndex = numberOrError(payload);
if (eventIndex <= 0) {
throw new Error(`Event index out of range ${eventIndex}`);
}
// Indexes in frontend are 1 based
const success = runtimeService.startByIndex(eventIndex - 1);
if (!success) {
throw new Error(`Event index not recognised or out of range ${eventIndex}`);
}
return { payload: 'success' };
},
startid: (payload) => {
assert.isString(payload);
runtimeService.startById(payload);
return { payload: 'success' };
},
startcue: (payload) => {
assert.isString(payload);
runtimeService.startByCue(payload);
return { payload: 'success' };
},
pause: () => { pause: () => {
runtimeService.pause(); runtimeService.pause();
return { payload: 'success' }; return { payload: 'success' };
}, },
previous: () => {
runtimeService.loadPrevious();
return { payload: 'success' };
},
next: () => {
runtimeService.loadNext();
return { payload: 'success' };
},
stop: () => { stop: () => {
runtimeService.stop(); runtimeService.stop();
return { payload: 'success' }; return { payload: 'success' };
@@ -177,24 +112,37 @@ const actionHandlers: Record<string, ActionHandler> = {
runtimeService.roll(); runtimeService.roll();
return { payload: 'success' }; return { payload: 'success' };
}, },
loadindex: (payload) => { load: (payload) => {
const eventIndex = numberOrError(payload); if (payload && typeof payload === 'object') {
if (eventIndex <= 0) { if ('index' in payload) {
throw new Error(`Event index out of range ${eventIndex}`); const eventIndex = numberOrError(payload.index);
if (eventIndex <= 0) {
throw new Error(`Event index out of range ${eventIndex}`);
}
// Indexes in frontend are 1 based
runtimeService.loadByIndex(eventIndex - 1);
return { payload: 'success' };
}
if ('id' in payload) {
assert.isDefined(payload.id);
runtimeService.loadById(payload.id.toString().toLowerCase());
return { payload: 'success' };
}
if ('cue' in payload) {
assert.isString(payload.cue);
runtimeService.loadByCue(payload.cue);
return { payload: 'success' };
}
if ('next' in payload) {
runtimeService.loadNext();
return { payload: 'success' };
}
if ('previous' in payload) {
runtimeService.loadPrevious();
return { payload: 'success' };
}
} }
// Indexes in frontend are 1 based throw new Error('No load method provided');
runtimeService.loadByIndex(eventIndex - 1);
return { payload: 'success' };
},
loadid: (payload) => {
assert.isDefined(payload);
runtimeService.loadById(payload.toString().toLowerCase());
return { payload: 'success' };
},
loadcue: (payload) => {
assert.isString(payload);
runtimeService.loadByCue(payload);
return { payload: 'success' };
}, },
addtime: (payload) => { addtime: (payload) => {
const time = numberOrError(payload); const time = numberOrError(payload);
+10 -2
View File
@@ -2,6 +2,7 @@ import express from 'express';
import { dispatchFromAdapter } from '../controllers/integrationController.js'; import { dispatchFromAdapter } from '../controllers/integrationController.js';
import { logger } from '../classes/Logger.js'; import { logger } from '../classes/Logger.js';
import { LogOrigin } from 'ontime-types'; import { LogOrigin } from 'ontime-types';
import { objectFromPath } from '../adapters/utils/parse.js';
export const router = express.Router(); export const router = express.Router();
@@ -14,8 +15,15 @@ router.get('/', (_req, res) => {
// any GET request in /api is sent to the integration controller // any GET request in /api is sent to the integration controller
router.get('/*', (req, res) => { router.get('/*', (req, res) => {
const action = req.path.substring(1); let action = req.path.substring(1);
const params = { payload: req.query }; const actionArray = action.split('/');
const params = { payload: req.query as object };
if (actionArray.length > 1) {
action = actionArray.shift();
params.payload = objectFromPath(actionArray, params.payload);
}
try { try {
const reply = dispatchFromAdapter(action, params, 'http'); const reply = dispatchFromAdapter(action, params, 'http');
@@ -1,4 +1,4 @@
import { Message, TimerMessage } from 'ontime-types'; import { DeepPartial, Message, TimerMessage, MessageState } from 'ontime-types';
import { throttle } from '../../utils/throttle.js'; import { throttle } from '../../utils/throttle.js';
@@ -7,10 +7,10 @@ import type { PublishFn } from '../../stores/EventStore.js';
let instance; let instance;
class MessageService { class MessageService {
timerMessage: TimerMessage; timer: TimerMessage;
publicMessage: Message; public: Message;
lowerMessage: Message; lower: Message;
externalMessage: Message; external: Message;
private throttledSet: PublishFn; private throttledSet: PublishFn;
private publish: PublishFn | null; private publish: PublishFn | null;
@@ -23,31 +23,35 @@ class MessageService {
// eslint-disable-next-line @typescript-eslint/no-this-alias -- this logic is used to ensure singleton // eslint-disable-next-line @typescript-eslint/no-this-alias -- this logic is used to ensure singleton
instance = this; instance = this;
this.timerMessage = {
text: '',
visible: false,
timerBlink: false,
timerBlackout: false,
};
this.publicMessage = {
text: '',
visible: false,
};
this.lowerMessage = {
text: '',
visible: false,
};
this.externalMessage = {
text: '',
visible: false,
};
this.throttledSet = () => { this.throttledSet = () => {
throw new Error('Published called before initialisation'); throw new Error('Published called before initialisation');
}; };
this.clear();
}
clear() {
this.timer = {
text: '',
visible: false,
blink: false,
blackout: false,
};
this.public = {
text: '',
visible: false,
};
this.lower = {
text: '',
visible: false,
};
this.external = {
text: '',
visible: false,
};
} }
init(publish: PublishFn) { init(publish: PublishFn) {
@@ -55,144 +59,26 @@ class MessageService {
this.throttledSet = throttle((key, value) => this.publish(key, value), 100); this.throttledSet = throttle((key, value) => this.publish(key, value), 100);
} }
/** getState(): MessageState {
* @description sets message on stage timer screen
*/
setExternalText(payload: string) {
if (this.externalMessage.text !== payload) {
this.externalMessage.text = payload;
this.throttledSet('externalMessage', this.externalMessage);
}
return this.getAll();
}
/**
* @description sets message visibility on stage timer screen
*/
setExternalVisibility(status: boolean) {
this.externalMessage.visible = status;
this.throttledSet('externalMessage', this.externalMessage);
return this.getAll();
}
/**
* @description patches the TimerMessage object
*/
setTimerMessage(payload: Partial<TimerMessage>) {
this.timerMessage = { ...this.timerMessage, ...payload };
this.throttledSet('timerMessage', this.timerMessage);
return this.getAll();
}
/**
* @description sets message on stage timer screen
*/
setTimerText(payload: string) {
this.timerMessage.text = payload;
this.throttledSet('timerMessage', this.timerMessage);
return this.getAll();
}
/**
* @description sets message visibility on stage timer screen
*/
setTimerVisibility(status: boolean) {
this.timerMessage.visible = status;
this.throttledSet('timerMessage', this.timerMessage);
return this.getAll();
}
/**
* @description sets message on public screen
*/
setPublicText(payload: string) {
this.publicMessage.text = payload;
this.throttledSet('publicMessage', this.publicMessage);
return this.getAll();
}
/**
* @description sets message visibility on public screen
*/
setPublicVisibility(status: boolean) {
this.publicMessage.visible = status;
this.throttledSet('publicMessage', this.publicMessage);
return this.getAll();
}
/**
* @description sets message on lower third screen
*/
setLowerText(payload: string) {
this.lowerMessage.text = payload;
this.throttledSet('lowerMessage', this.lowerMessage);
return this.getAll();
}
/**
* @description sets message visibility on lower third screen
*/
setLowerVisibility(status: boolean) {
this.lowerMessage.visible = status;
this.throttledSet('lowerMessage', this.lowerMessage);
return this.getAll();
}
/**
* @description set state of timer blink, toggles if parameters are offered
*/
setTimerBlink(status?: boolean) {
if (typeof status === 'undefined') {
this.timerMessage.timerBlink = !this.timerMessage.timerBlink;
} else {
this.timerMessage.timerBlink = status;
}
this.throttledSet('timerMessage', this.timerMessage);
return this.getAll();
}
/**
* @description set state of timer blackout, toggles if parameters are offered
*/
setTimerBlackout(status?: boolean) {
if (typeof status === 'undefined') {
this.timerMessage.timerBlackout = !this.timerMessage.timerBlackout;
} else {
this.timerMessage.timerBlackout = status;
}
this.throttledSet('timerMessage', this.timerMessage);
return this.getAll();
}
/**
* @description Returns feature data
*/
getAll() {
return { return {
timerMessage: this.timerMessage, timer: this.timer,
publicMessage: this.publicMessage, public: this.public,
lowerMessage: this.lowerMessage, lower: this.lower,
externalMessage: this.externalMessage, external: this.external,
}; };
} }
patch(message: DeepPartial<MessageState>) {
if (message.timer) this.timer = { ...this.timer, ...message.timer };
if (message.public) this.public = { ...this.public, ...message.public };
if (message.lower) this.lower = { ...this.lower, ...message.lower };
if (message.external) this.external = { ...this.external, ...message.external };
const newState = this.getState();
this.throttledSet('message', newState);
return newState;
}
} }
export const messageService = new MessageService(); export const messageService = new MessageService();
/**
* Asserts whether an object is a valid TimerMessage patch
* @param obj - object to evaluate
* @returns boolean
*/
export function isPartialTimerMessage(obj: any): obj is Partial<TimerMessage> {
return (
obj &&
typeof obj === 'object' &&
(typeof obj.text === 'string' || obj.text === undefined) &&
(typeof obj.visible === 'boolean' || obj.visible === undefined) &&
(typeof obj.timerBlink === 'boolean' || obj.timerBlink === undefined) &&
(typeof obj.timerBlackout === 'boolean' || obj.timerBlackout === undefined)
);
}
@@ -0,0 +1,59 @@
import { messageService } from '../MessageService.js';
describe('MessageService', () => {
const publishFunction = () => {};
beforeAll(() => {
messageService.init(publishFunction);
});
beforeEach(() => {
messageService.clear();
});
it('should patch the message state', () => {
const message = {
timer: { text: 'new text', visible: true },
public: { text: 'public text', visible: false },
lower: { text: 'lower text' },
external: { visible: true },
};
const newState = messageService.patch(message);
expect(newState).toEqual({
timer: { text: 'new text', visible: true, blackout: false, blink: false },
public: { text: 'public text', visible: false },
lower: {
text: 'lower text',
visible: false,
},
external: {
text: '',
visible: true,
},
});
});
it('should not affect other properties when patching', () => {
const initialMessage = {
timer: { text: 'initial text', visible: true },
public: { text: 'public text', visible: false },
};
const newState = messageService.patch(initialMessage);
expect(newState).toEqual({
timer: { text: 'initial text', visible: true, blackout: false, blink: false },
public: { text: 'public text', visible: false },
lower: {
text: '',
visible: false,
},
external: {
text: '',
visible: false,
},
});
});
});
@@ -0,0 +1,55 @@
import { validateMessage, validateTimerMessage } from '../messageUtils.js';
describe('validateMessage()', () => {
it('returns a valid Message object', () => {
const payload = {
text: '12312',
visible: 'true',
};
const expected = {
text: '12312',
visible: true,
};
expect(validateMessage(payload)).toEqual(expected);
});
it('skips keys not given', () => {
const payload = {
visible: 'true',
};
const expected = {
visible: true,
};
expect(validateMessage(payload)).toStrictEqual(expected);
});
});
describe('validateTimerMessage()', () => {
it('returns a valid Timer Message object', () => {
const payload = {
text: '12312',
visible: 'true',
blink: 'true',
blackout: 'true',
};
const expected = {
text: '12312',
visible: true,
blink: true,
blackout: true,
};
expect(validateTimerMessage(payload)).toEqual(expected);
});
it('skips keys not given', () => {
const payload = {
visible: 'true',
};
const expected = {
visible: true,
};
expect(validateTimerMessage(payload)).toStrictEqual(expected);
});
});
@@ -0,0 +1,35 @@
import { Message, TimerMessage } from 'ontime-types';
import * as assert from '../../utils/assert.js';
import { coerceBoolean, coerceString } from '../../utils/coerceType.js';
/**
* Creates a valid Message object from a payload
* @throws if the payload is not an object
*/
export function validateMessage(message: unknown): Partial<Message> {
assert.isObject(message);
const result: Partial<Message> = {};
if ('text' in message) result.text = coerceString(message.text);
if ('visible' in message) result.visible = coerceBoolean(message.visible);
return result;
}
/**
* Creates a valid Timer Message object from a payload
* @throws if the payload is not an object
*/
export function validateTimerMessage(message: unknown): Partial<TimerMessage> {
assert.isObject(message);
const result: Partial<TimerMessage> = {};
if ('text' in message) result.text = coerceString(message.text);
if ('visible' in message) result.visible = coerceBoolean(message.visible);
if ('blink' in message) result.blink = coerceBoolean(message.blink);
if ('blackout' in message) result.blackout = coerceBoolean(message.blackout);
return result;
}
+6
View File
@@ -15,3 +15,9 @@ export function isNumber(value: unknown): asserts value is number {
throw new Error(`Unexpected payload type: ${value}`); throw new Error(`Unexpected payload type: ${value}`);
} }
} }
export function isObject(value: unknown): asserts value is object {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new Error(`Unexpected payload type: ${value}`);
}
}
@@ -4,6 +4,13 @@ export type Message = {
}; };
export type TimerMessage = Message & { export type TimerMessage = Message & {
timerBlink: boolean; blink: boolean;
timerBlackout: boolean; blackout: boolean;
};
export type MessageState = {
timer: TimerMessage;
public: Message;
lower: Message;
external: Message;
}; };
@@ -1,5 +1,5 @@
import { Playback } from './Playback.type.js'; import { Playback } from './Playback.type.js';
import { Message, TimerMessage } from './MessageControl.type.js'; import { MessageState } from './MessageControl.type.js';
import { TimerState } from './TimerState.type.js'; import { TimerState } from './TimerState.type.js';
import { Runtime } from './Runtime.type.js'; import { Runtime } from './Runtime.type.js';
import { OntimeEvent } from '../core/OntimeEvent.type.js'; import { OntimeEvent } from '../core/OntimeEvent.type.js';
@@ -10,10 +10,7 @@ export type RuntimeStore = {
playback: Playback; playback: Playback;
// messages service // messages service
timerMessage: TimerMessage; message: MessageState;
publicMessage: Message;
lowerMessage: Message;
externalMessage: Message;
onAir: boolean; onAir: boolean;
// event loader // event loader
+2 -2
View File
@@ -53,7 +53,7 @@ export type { GetRundownCached } from './api/rundown-controller/BackendResponse.
export { type Log, LogLevel, type LogMessage, LogOrigin } from './definitions/runtime/Logger.type.js'; export { type Log, LogLevel, type LogMessage, LogOrigin } from './definitions/runtime/Logger.type.js';
export { Playback } from './definitions/runtime/Playback.type.js'; export { Playback } from './definitions/runtime/Playback.type.js';
export { TimerLifeCycle } from './definitions/core/TimerLifecycle.type.js'; export { TimerLifeCycle } from './definitions/core/TimerLifecycle.type.js';
export type { Message, TimerMessage } from './definitions/runtime/MessageControl.type.js'; export type { Message, TimerMessage, MessageState } from './definitions/runtime/MessageControl.type.js';
export type { Runtime } from './definitions/runtime/Runtime.type.js'; export type { Runtime } from './definitions/runtime/Runtime.type.js';
export type { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js'; export type { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js';
@@ -63,4 +63,4 @@ export type { TimerState } from './definitions/runtime/TimerState.type.js';
// TYPE UTILITIES // TYPE UTILITIES
export { isOntimeBlock, isOntimeDelay, isOntimeEvent, isKeyOfType } from './utils/guards.js'; export { isOntimeBlock, isOntimeDelay, isOntimeEvent, isKeyOfType } from './utils/guards.js';
export type { MaybeNumber } from './utils/utils.type.js'; export type { MaybeNumber, DeepPartial } from './utils/utils.type.js';
+4
View File
@@ -1 +1,5 @@
export type MaybeNumber = number | null; export type MaybeNumber = number | null;
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};