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}
/>
-
-
- : }
- onClick={() => setMessage.onAir(!data.onAir)}
- data-testid='toggle on air'
- >
- {data?.onAir ? 'Ontime is On Air' : 'Ontime is Off Air'}
-
-
);
}
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);
});