From 062dc0c914e5504cfcf66e3ca6ba629387dd826c Mon Sep 17 00:00:00 2001 From: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Date: Tue, 19 Dec 2023 21:44:43 +0100 Subject: [PATCH] refactor: pass HTTP to integration controller (#652) * refactor: pass HTTP to integration controller * refactor: deprecate onair control * refactor: remove playback router --- .../message/MessageControl.module.scss | 15 - .../control/message/MessageControl.tsx | 14 - apps/server/src/adapters/OscAdapter.ts | 7 +- apps/server/src/adapters/WebsocketAdapter.ts | 4 +- apps/server/src/app.ts | 4 +- .../src/classes/event-loader/EventLoader.ts | 2 +- .../src/controllers/integrationController.ts | 460 ++++++++---------- .../src/controllers/playbackController.ts | 98 ---- apps/server/src/routes/apiRouter.ts | 27 + apps/server/src/routes/playbackRouter.ts | 47 -- .../message-service/MessageService.ts | 46 +- apps/server/src/stores/EventStore.ts | 6 +- apps/server/src/utils/assert.ts | 17 + .../features/201-message-control.spec.ts | 10 +- 14 files changed, 280 insertions(+), 477 deletions(-) delete mode 100644 apps/server/src/controllers/playbackController.ts create mode 100644 apps/server/src/routes/apiRouter.ts delete mode 100644 apps/server/src/routes/playbackRouter.ts create mode 100644 apps/server/src/utils/assert.ts diff --git a/apps/client/src/features/control/message/MessageControl.module.scss b/apps/client/src/features/control/message/MessageControl.module.scss index 5f3d0e46f..10d9fcc12 100644 --- a/apps/client/src/features/control/message/MessageControl.module.scss +++ b/apps/client/src/features/control/message/MessageControl.module.scss @@ -6,24 +6,9 @@ gap: $section-spacing; } -.onAirSection { - display: flex; - flex-direction: column; - gap: $element-spacing; -} - .buttonSection { display: grid; grid-template-columns: 1fr 1fr; gap: $element-spacing; margin-top: -0.5rem; } - -.label { - font-size: $inner-section-text-size; - color: $label-gray; - - &.active { - color: $action-text-color; - } -} diff --git a/apps/client/src/features/control/message/MessageControl.tsx b/apps/client/src/features/control/message/MessageControl.tsx index 8feb66cc9..459ff867c 100644 --- a/apps/client/src/features/control/message/MessageControl.tsx +++ b/apps/client/src/features/control/message/MessageControl.tsx @@ -1,8 +1,6 @@ import { Button } from '@chakra-ui/react'; import { IoEye } from '@react-icons/all-files/io5/IoEye'; import { IoEyeOffOutline } from '@react-icons/all-files/io5/IoEyeOffOutline'; -import { IoMicOffOutline } from '@react-icons/all-files/io5/IoMicOffOutline'; -import { IoMicSharp } from '@react-icons/all-files/io5/IoMicSharp'; import { IoSunny } from '@react-icons/all-files/io5/IoSunny'; import { IoSunnyOutline } from '@react-icons/all-files/io5/IoSunnyOutline'; @@ -72,18 +70,6 @@ export default function MessageControl() { changeHandler={() => undefined} actionHandler={() => undefined} /> -
- - -
); } diff --git a/apps/server/src/adapters/OscAdapter.ts b/apps/server/src/adapters/OscAdapter.ts index d07896983..92c9d4b54 100644 --- a/apps/server/src/adapters/OscAdapter.ts +++ b/apps/server/src/adapters/OscAdapter.ts @@ -62,17 +62,14 @@ export class OscServer implements IAdapter { } try { - const reply = dispatchFromAdapter( + // we dont reply on OSC + dispatchFromAdapter( path, { payload: transformedPayload, }, 'osc', ); - if (reply) { - const { topic, payload } = reply; - this.osc.emit(topic, payload); - } } catch (error) { logger.error(LogOrigin.Rx, `OSC IN: ${error}`); } diff --git a/apps/server/src/adapters/WebsocketAdapter.ts b/apps/server/src/adapters/WebsocketAdapter.ts index 0383a1bf6..b35eb6ecc 100644 --- a/apps/server/src/adapters/WebsocketAdapter.ts +++ b/apps/server/src/adapters/WebsocketAdapter.ts @@ -132,8 +132,8 @@ export class SocketServer implements IAdapter { 'ws', ); if (reply) { - const { topic, payload } = reply; - ws.send(topic, payload); + const { payload } = reply; + ws.send(type, payload); } } catch (error) { logger.error(LogOrigin.Rx, `WS IN: ${error}`); diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 98a90b0e6..2a3e3683c 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -15,7 +15,7 @@ import { ONTIME_VERSION } from './ONTIME_VERSION.js'; import { router as rundownRouter } from './routes/rundownRouter.js'; import { router as projectRouter } from './routes/projectRouter.js'; import { router as ontimeRouter } from './routes/ontimeRouter.js'; -import { router as playbackRouter } from './routes/playbackRouter.js'; +import { router as apiRouter } from './routes/apiRouter.js'; // Import adapters import { OscServer } from './adapters/OscAdapter.js'; @@ -61,7 +61,7 @@ app.use(express.json({ limit: '1mb' })); app.use('/events', rundownRouter); app.use('/project', projectRouter); app.use('/ontime', ontimeRouter); -app.use('/playback', playbackRouter); +app.use('/api', apiRouter); // serve static - css app.use('/external', express.static(externalsStartDirectory)); diff --git a/apps/server/src/classes/event-loader/EventLoader.ts b/apps/server/src/classes/event-loader/EventLoader.ts index a371f3ff5..f567472fa 100644 --- a/apps/server/src/classes/event-loader/EventLoader.ts +++ b/apps/server/src/classes/event-loader/EventLoader.ts @@ -95,7 +95,7 @@ export class EventLoader { */ static getEventWithCue(cue) { const timedEvents = EventLoader.getTimedEvents(); - return timedEvents.find((event) => event.cue === cue); + return timedEvents.find((event) => event.cue.toLowerCase() === cue.toLowerCase()); } /** diff --git a/apps/server/src/controllers/integrationController.ts b/apps/server/src/controllers/integrationController.ts index e95acab7b..a129cf6b0 100644 --- a/apps/server/src/controllers/integrationController.ts +++ b/apps/server/src/controllers/integrationController.ts @@ -1,4 +1,8 @@ -import { messageService } from '../services/message-service/MessageService.js'; +// skipcq: JS-C1003 - we like the API +import * as assert from '../utils/assert.js'; +import { ONTIME_VERSION } from '../ONTIME_VERSION.js'; + +import { isPartialTimerMessage, messageService } from '../services/message-service/MessageService.js'; import { PlaybackService } from '../services/PlaybackService.js'; import { eventStore } from '../stores/EventStore.js'; import { parse, updateEvent } from './integrationController.config.js'; @@ -9,273 +13,205 @@ export type ChangeOptions = { value: unknown; }; -//TODO: re-throwing the error does not add any extra information or value export function dispatchFromAdapter( type: string, args: { payload: unknown; }, - _source?: 'osc' | 'ws', + _source?: 'osc' | 'ws' | 'http', ) { const payload = args.payload; - const typeComponents = type.toLowerCase().split('/'); - const mainType = typeComponents[0]; - - switch (mainType) { - case 'test-ontime': { - return { topic: 'hello' }; - } - - case 'ontime-poll': { - return { - topic: 'poll', - payload: eventStore.poll(), - }; - } - - case 'set-onair': { - if (typeof payload !== 'undefined') { - messageService.setOnAir(Boolean(payload)); - } - break; - } - - case 'onair': { - messageService.setOnAir(true); - break; - } - - case 'offair': { - messageService.setOnAir(false); - break; - } - - case 'set-timer-blink': { - if (typeof payload !== 'undefined') { - messageService.setTimerBlink(Boolean(payload)); - } - break; - } - - case 'set-timer-blackout': { - if (typeof payload !== 'undefined') { - messageService.setTimerBlackout(Boolean(payload)); - } - break; - } - - case 'set-timer-message-text': { - if (typeof payload !== 'string') { - throw new Error(`Unable to parse payload: ${payload}`); - } - messageService.setTimerText(payload); - break; - } - case 'set-timer-message-visible': { - if (typeof payload === 'undefined') { - throw new Error(`Unable to parse payload: ${payload}`); - } - messageService.setTimerVisibility(Boolean(payload)); - break; - } - - case 'set-public-message-text': { - if (typeof payload !== 'string') { - throw new Error(`Unable to parse payload: ${payload}`); - } - messageService.setPublicText(payload); - break; - } - case 'set-public-message-visible': { - if (typeof payload === 'undefined') { - throw new Error(`Unable to parse payload: ${payload}`); - } - messageService.setPublicVisibility(Boolean(payload)); - break; - } - - case 'set-lower-message-text': { - if (typeof payload !== 'string') { - throw new Error(`Unable to parse payload: ${payload}`); - } - messageService.setLowerText(payload); - break; - } - case 'set-lower-message-visible': { - if (typeof payload === 'undefined') { - throw new Error(`Unable to parse payload: ${payload}`); - } - messageService.setLowerVisibility(Boolean(payload)); - break; - } - - case 'set-external-message-text': { - if (typeof payload !== 'string') { - throw new Error(`Unable to parse payload: ${payload}`); - } - messageService.setExternalText(payload); - return; - } - case 'set-external-message-visible': { - if (typeof payload === 'undefined') { - throw new Error(`Unable to parse payload: ${payload}`); - } - messageService.setExternalVisibility(Boolean(payload)); - break; - } - case 'start': { - PlaybackService.start(); - break; - } - - case 'start-next': { - PlaybackService.startNext(); - break; - } - - case 'startindex': { - const eventIndex = Number(payload); - if (isNaN(eventIndex) || eventIndex <= 0) { - throw new Error(`Event index not recognised or out of range ${eventIndex}`); - } - - try { - // Indexes in frontend are 1 based - PlaybackService.startByIndex(eventIndex - 1); - } catch (error) { - throw new Error(`Error loading event:: ${error}`); - } - break; - } - - case 'startid': { - if (!payload || typeof payload !== 'string') { - throw new Error(`Event ID not recognised: ${payload}`); - } - PlaybackService.startById(payload); - break; - } - - case 'startcue': { - if (!payload || typeof payload !== 'string') { - throw new Error(`Event cue not recognised: ${payload}`); - } - PlaybackService.startByCue(payload); - break; - } - - case 'pause': { - PlaybackService.pause(); - break; - } - case 'previous': { - PlaybackService.loadPrevious(); - break; - } - case 'next': { - PlaybackService.loadNext(); - break; - } - case 'unload': - case 'stop': { - PlaybackService.stop(); - break; - } - case 'reload': { - PlaybackService.reload(); - break; - } - case 'roll': { - PlaybackService.roll(); - break; - } - case 'addtime': { - const time = Number(payload); - if (isNaN(time)) { - throw new Error(`Time not recognised ${payload}`); - } - try { - PlaybackService.addTime(time); - } catch (error) { - throw new Error(`Could not add time: ${error}`); - } - break; - } - //deprecated - case 'delay': { - const delayTime = Number(payload); - if (isNaN(delayTime)) { - throw new Error(`Delay time not recognised ${payload}`); - } - - try { - PlaybackService.setDelay(delayTime); - } catch (error) { - throw new Error(`Could not add delay: ${error}`); - } - break; - } - case 'gotoindex': - case 'loadindex': { - const eventIndex = Number(payload); - if (isNaN(eventIndex) || eventIndex <= 0) { - throw new Error(`Event index not recognised or out of range ${eventIndex}`); - } - - try { - // Indexes in frontend are 1 based - PlaybackService.loadByIndex(eventIndex - 1); - } catch (error) { - throw new Error(`Event index not recognised or out of range ${error}`); - } - break; - } - case 'gotoid': - case 'loadid': { - if (!payload) { - throw new Error(`Event ID not recognised: ${payload}`); - } - - try { - PlaybackService.loadById(payload.toString().toLowerCase()); - } catch (error) { - throw new Error(`OSC IN: error calling goto ${error}`); - } - break; - } - case 'gotocue': - case 'loadcue': { - if (!payload || typeof payload !== 'string') { - throw new Error(`Event cue not recognised: ${payload}`); - } - - try { - PlaybackService.loadByCue(payload); - } catch (error) { - throw new Error(`OSC IN: error calling goto ${error}`); - } - break; - } - - case 'get-playback': { - const playback = eventStore.get('playback'); - return { topic: 'playback', payload: playback }; - } - - case 'get-timer': { - const timer = eventStore.get('timer'); - return { topic: 'timer', payload: timer }; - } - - // WS: {type: 'change', payload: { eventId, property, value } } - case 'change': { - const { eventId, property, value } = payload as ChangeOptions; - const { parsedPayload, parsedProperty } = parse(property, value); - return updateEvent(eventId, parsedProperty, parsedPayload); - } - - default: { - throw new Error(`Unhandled message ${type}`); - } + const action = type.toLowerCase(); + const handler = actionHandlers[action]; + if (handler) { + return handler(payload); + } else { + throw new Error(`Unhandled message ${type}`); } } + +type ActionHandler = (payload: unknown) => { payload: unknown }; + +// TODO: add data to missing returns once available +const actionHandlers: Record = { + /* General */ + 'test-ontime': () => ({ payload: `Hello from Ontime version ${ONTIME_VERSION}` }), + 'ontime-poll': () => ({ + payload: eventStore.poll(), + }), + change: (payload) => { + // WS: {type: 'change', payload: { eventId, property, value } } + const { eventId, property, value } = payload as ChangeOptions; + const { parsedPayload, parsedProperty } = parse(property, value); + + const updatedEvent = updateEvent(eventId, parsedProperty, parsedPayload); + return { payload: updatedEvent }; + }, + /* Message Service */ + 'set-timer-message': (payload) => { + if (!isPartialTimerMessage(payload)) { + throw new Error('Payload is not a valid timer message'); + } + const newState = messageService.setTimerMessage(payload); + return { payload: newState.timerMessage }; + }, + 'set-timer-blink': (payload) => { + assert.isDefined(payload); + const newState = messageService.setTimerBlink(Boolean(payload)); + return { payload: newState.timerMessage }; + }, + '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 */ + start: (payload) => { + if (payload && typeof payload === 'object') { + if ('index' in payload) { + const reply = actionHandlers.startindex(payload.index); + return reply; + } + if ('id' in payload) { + const reply = actionHandlers.startid(payload.id); + return reply; + } + if ('cue' in payload) { + const reply = actionHandlers.startcue(payload.cue); + return reply; + } + } + + PlaybackService.start(); + + return { payload: 'start' }; + }, + 'start-next': () => { + PlaybackService.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 + PlaybackService.startByIndex(eventIndex - 1); + return { payload: 'success' }; + }, + startid: (payload) => { + assert.isString(payload); + PlaybackService.startById(payload); + return { payload: 'success' }; + }, + startcue: (payload) => { + assert.isString(payload); + PlaybackService.startByCue(payload); + return { payload: 'success' }; + }, + pause: () => { + PlaybackService.pause(); + return { payload: 'success' }; + }, + previous: () => { + PlaybackService.loadPrevious(); + return { payload: 'success' }; + }, + next: () => { + PlaybackService.loadNext(); + return { payload: 'success' }; + }, + stop: () => { + PlaybackService.stop(); + return { payload: 'success' }; + }, + reload: () => { + PlaybackService.reload(); + return { payload: 'success' }; + }, + roll: () => { + PlaybackService.roll(); + return { payload: 'success' }; + }, + loadindex: (payload) => { + const eventIndex = numberOrError(payload); + if (eventIndex <= 0) { + throw new Error(`Event index out of range ${eventIndex}`); + } + + // Indexes in frontend are 1 based + PlaybackService.loadByIndex(eventIndex - 1); + return { payload: 'success' }; + }, + loadid: (payload) => { + assert.isDefined(payload); + PlaybackService.loadById(payload.toString().toLowerCase()); + return { payload: 'success' }; + }, + loadcue: (payload) => { + assert.isString(payload); + PlaybackService.loadByCue(payload); + return { payload: 'success' }; + }, + addtime: (payload) => { + const time = numberOrError(payload); + PlaybackService.addTime(time); + return { payload: 'success' }; + }, +}; + +/** + * Returns a value of type number, converting if necessary + * Otherwise throws + * @param value + * @returns number + * @throws + */ +function numberOrError(value: unknown) { + const converted = Number(value); + if (isNaN(converted)) { + throw new Error('Payload is not a valid number'); + } + return converted; +} diff --git a/apps/server/src/controllers/playbackController.ts b/apps/server/src/controllers/playbackController.ts deleted file mode 100644 index fa1fc80e8..000000000 --- a/apps/server/src/controllers/playbackController.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { PlaybackService } from '../services/PlaybackService.js'; -import { eventStore } from '../stores/EventStore.js'; - -// Create controller for POST request to '/playback' -// Returns playback state -export const pbGet = async (req, res) => { - res.send({ playback: eventStore.get('playback') }); -}; - -// Create controller for POST request to '/playback/start' -// Starts timer object -export const pbStart = async (req, res) => { - const { eventId, eventIndex } = req.query; - if (eventId) { - const success = PlaybackService.startById(eventId); - success ? res.sendStatus(202) : res.status(400).send({ message: 'Invalid event ID' }); - } else if (eventIndex) { - const index = Number(eventIndex); - if (!isNaN(index)) { - const success = PlaybackService.startByIndex(eventIndex - 1); - success ? res.sendStatus(202) : res.status(400).send({ message: 'Invalid event index' }); - } else { - res.status(400).send({ message: 'Invalid event index' }); - } - } else { - PlaybackService.start(); - res.sendStatus(202); - } -}; - -// Create controller for POST request to '/playback/pause' -// Pauses timer object -export const pbPause = async (req, res) => { - PlaybackService.pause(); - res.sendStatus(202); -}; - -// Create controller for POST request to '/playback/stop' -// Stops timer object -export const pbStop = async (req, res) => { - PlaybackService.stop(); - res.sendStatus(202); -}; - -// Create controller for POST request to '/playback/roll' -// Sets timer object to roll mode -export const pbRoll = async (req, res) => { - PlaybackService.roll(); - res.sendStatus(202); -}; - -// Create controller for POST request to '/playback/previous' -// Loads previous event -export const pbPrevious = async (req, res) => { - PlaybackService.loadPrevious(); - res.sendStatus(202); -}; - -// Create controller for POST request to '/playback/next' -// Loads Next event -export const pbNext = async (req, res) => { - PlaybackService.loadNext(); - res.sendStatus(202); -}; - -// Create controller for POST request to '/playback/load' -// Load requested event -export const pbLoad = async (req, res) => { - const { eventId, eventIndex } = req.query; - if (eventId) { - const success = PlaybackService.loadById(eventId); - success ? res.sendStatus(202) : res.status(400).send({ message: 'Invalid event ID' }); - } else if (eventIndex) { - const index = Number(eventIndex); - if (!isNaN(index)) { - const success = PlaybackService.loadByIndex(eventIndex - 1); - success ? res.sendStatus(202) : res.status(400).send({ message: 'Invalid event index' }); - } else { - res.status(400).send({ message: 'Invalid event index' }); - } - } else { - res.status(400).send({ message: 'No event given' }); - } -}; - -// Create controller for POST request to '/playback/unload' -// Unloads any events -export const pbUnload = async (req, res) => { - PlaybackService.stop(); - res.sendStatus(202); -}; - -// Create controller for POST request to '/playback/reload' -// Reloads current event -export const pbReload = async (req, res) => { - PlaybackService.reload(); - res.sendStatus(202); -}; diff --git a/apps/server/src/routes/apiRouter.ts b/apps/server/src/routes/apiRouter.ts new file mode 100644 index 000000000..fd7f7c281 --- /dev/null +++ b/apps/server/src/routes/apiRouter.ts @@ -0,0 +1,27 @@ +import express from 'express'; +import { dispatchFromAdapter } from '../controllers/integrationController.js'; +import { logger } from '../classes/Logger.js'; +import { LogOrigin } from 'ontime-types'; + +export const router = express.Router(); + +const helloMessage = 'You have reached Ontime API server'; + +// create route between controller and '/api/' endpoint +router.get('/', (_req, res) => { + res.status(200).json({ message: helloMessage }); +}); + +// any GET request in /api is sent to the integration controller +router.get('/*', (req, res) => { + const action = req.path.substring(1); + const params = { payload: req.query }; + + try { + const reply = dispatchFromAdapter(action, params, 'http'); + res.status(202).json(reply); + } catch (error) { + logger.error(LogOrigin.Rx, `HTTP IN: ${error}`); + res.status(500).json({ error: error.message }); + } +}); diff --git a/apps/server/src/routes/playbackRouter.ts b/apps/server/src/routes/playbackRouter.ts deleted file mode 100644 index 25fa904c6..000000000 --- a/apps/server/src/routes/playbackRouter.ts +++ /dev/null @@ -1,47 +0,0 @@ -import express from 'express'; -import { - pbGet, - pbLoad, - pbNext, - pbPause, - pbPrevious, - pbReload, - pbRoll, - pbStart, - pbStop, - pbUnload, -} from '../controllers/playbackController.js'; - -export const router = express.Router(); - -// create route between controller and '/playback/' endpoint -router.get('/', pbGet); - -// create route between controller and '/playback/start' endpoint -router.post('/start', pbStart); - -// create route between controller and '/playback/pause' endpoint -router.post('/pause', pbPause); - -// create route between controller and '/playback/stop' endpoint -router.post('/stop', pbStop); - -// create route between controller and '/playback/roll' endpoint -router.post('/roll', pbRoll); - -// create route between controller and '/playback/previous' endpoint -router.post('/previous', pbPrevious); - -// create route between controller and '/playback/next' endpoint -router.post('/next', pbNext); - -// create route between controller and '/playback/load' endpoint -router.post('/load', pbLoad); - -// create route between controller and '/playback/unload' endpoint -router.post('/unload', pbUnload); - -// create route between controller and '/playback/reload' endpoint -router.post('/reload', pbReload); - -// router.post('*', (req, res) => res.return(404)) diff --git a/apps/server/src/services/message-service/MessageService.ts b/apps/server/src/services/message-service/MessageService.ts index 5ab3bf048..a1f251820 100644 --- a/apps/server/src/services/message-service/MessageService.ts +++ b/apps/server/src/services/message-service/MessageService.ts @@ -1,6 +1,4 @@ -import { Message } from 'ontime-types'; - -import { TimerMessage } from 'ontime-types/src/definitions/runtime/MessageControl.type.js'; +import { Message, TimerMessage } from 'ontime-types'; import { throttle } from '../../utils/throttle.js'; import type { PublishFn } from '../../stores/EventStore.js'; @@ -12,7 +10,6 @@ class MessageService { publicMessage: Message; lowerMessage: Message; externalMessage: Message; - onAir: boolean; private throttledSet: PublishFn; private publish: PublishFn | null; @@ -47,7 +44,6 @@ class MessageService { visible: false, }; - this.onAir = false; this.throttledSet = () => { throw new Error('Published called before initialisation'); }; @@ -78,6 +74,15 @@ class MessageService { return this.getAll(); } + /** + * @description patches the TimerMessage object + */ + setTimerMessage(payload: Partial) { + this.timerMessage = { ...this.timerMessage, ...payload }; + this.throttledSet('timerMessage', this.timerMessage); + return this.getAll(); + } + /** * @description sets message on stage timer screen */ @@ -132,19 +137,6 @@ class MessageService { return this.getAll(); } - /** - * @description set state of onAir, toggles if parameters are offered - */ - setOnAir(status?: boolean) { - if (typeof status === 'undefined') { - this.onAir = !this.onAir; - } else { - this.onAir = status; - } - this.throttledSet('onAir', this.onAir); - return this.getAll(); - } - /** * @description set state of timer blink, toggles if parameters are offered */ @@ -181,9 +173,25 @@ class MessageService { timerMessage: this.timerMessage, publicMessage: this.publicMessage, lowerMessage: this.lowerMessage, - onAir: this.onAir, + externalMessage: this.externalMessage, }; } } 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 { + 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) + ); +} diff --git a/apps/server/src/stores/EventStore.ts b/apps/server/src/stores/EventStore.ts index a6410b0bf..bcee1c0bd 100644 --- a/apps/server/src/stores/EventStore.ts +++ b/apps/server/src/stores/EventStore.ts @@ -1,4 +1,4 @@ -import { RuntimeStore } from 'ontime-types'; +import { Playback, RuntimeStore } from 'ontime-types'; import { socket } from '../adapters/WebsocketAdapter.js'; import { eventTimer } from '../services/TimerService.js'; import { messageService } from '../services/message-service/MessageService.js'; @@ -53,10 +53,10 @@ export const eventStore = { * Currently registered objects in store * - Timer Service timer * - Timer Service playback + * - Timer Service onAir * - Message Service timerMessage * - Message Service publicMessage * - Message Service lowerMessage - * - Message Service onAir * - Event Loader loaded * - Event Loader eventNow * - Event Loader publicEventNow @@ -67,11 +67,11 @@ export const eventStore = { export const getInitialPayload = () => ({ timer: eventTimer.timer, playback: eventTimer.playback, + onAir: eventTimer.playback !== Playback.Stop, timerMessage: messageService.timerMessage, publicMessage: messageService.publicMessage, lowerMessage: messageService.lowerMessage, externalMessage: messageService.externalMessage, - onAir: messageService.onAir, loaded: eventLoader.loaded, eventNow: eventLoader.eventNow, publicEventNow: eventLoader.publicEventNow, diff --git a/apps/server/src/utils/assert.ts b/apps/server/src/utils/assert.ts new file mode 100644 index 000000000..0381c0e6e --- /dev/null +++ b/apps/server/src/utils/assert.ts @@ -0,0 +1,17 @@ +export function isString(value: unknown): asserts value is string { + if (typeof value !== 'string') { + throw new Error(`Unexpected payload type: ${value}`); + } +} + +export function isDefined(value: T | undefined): asserts value is T { + if (value === undefined) { + throw new Error('Payload not found'); + } +} + +export function isNumber(value: unknown): asserts value is number { + if (typeof value !== 'string') { + throw new Error(`Unexpected payload type: ${value}`); + } +} diff --git a/e2e/tests/features/201-message-control.spec.ts b/e2e/tests/features/201-message-control.spec.ts index dbe55e417..59734f6d1 100644 --- a/e2e/tests/features/201-message-control.spec.ts +++ b/e2e/tests/features/201-message-control.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { test } from '@playwright/test'; test('message control sends messages to screens', async ({ context }) => { const editorPage = await context.newPage(); @@ -32,12 +32,4 @@ test('message control sends messages to screens', async ({ context }) => { await featurePage.goto('http://localhost:4001/timer'); await featurePage.waitForLoadState('load', { timeout: 5000 }); await featurePage.getByText('testing stage').click(); - - // on air state - await editorPage.getByTestId('toggle on air').click(); - - await featurePage.goto('http://localhost:4001/studio'); - await featurePage.getByText('ON AIR').click(); - const onAirActive = await featurePage.locator('data-testid=on-air-enabled'); - await expect(onAirActive).toHaveText(/on air/i); });