mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-19 14:14:17 +00:00
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:
committed by
GitHub
parent
cb8ba776e2
commit
0cd2ca4191
@@ -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,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: {
|
||||||
|
|||||||
@@ -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,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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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];
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user