:
}
- onClick={() => setMessage.timerBlink(!data.timerMessage.timerBlink)}
+ className={`${blink ? style.blink : ''}`}
+ variant={blink ? 'ontime-filled' : 'ontime-subtle'}
+ leftIcon={blink ?
:
}
+ onClick={() => setMessage.timerBlink(!blink)}
data-testid='toggle timer blink'
>
Blink message
@@ -53,9 +54,9 @@ export default function MessageControl() {
:
}
- onClick={() => setMessage.timerBlackout(!data.timerMessage.timerBlackout)}
+ variant={blackout ? 'ontime-filled' : 'ontime-subtle'}
+ leftIcon={blackout ?
:
}
+ onClick={() => setMessage.timerBlackout(!blackout)}
data-testid='toggle timer blackout'
>
Blackout screen
@@ -65,8 +66,8 @@ export default function MessageControl() {
label='External Message'
placeholder='-'
readonly
- text={data.externalMessage.text || ''}
- visible={data.externalMessage.visible || false}
+ text={message.external.text || ''}
+ visible={message.external.visible || false}
changeHandler={() => undefined}
actionHandler={() => undefined}
/>
diff --git a/apps/client/src/features/viewers/ViewWrapper.tsx b/apps/client/src/features/viewers/ViewWrapper.tsx
index 636c08250..be4659ed8 100644
--- a/apps/client/src/features/viewers/ViewWrapper.tsx
+++ b/apps/client/src/features/viewers/ViewWrapper.tsx
@@ -55,20 +55,8 @@ const withData =
(Component: ComponentType
) => {
}, [rundownData]);
// websocket data
- const {
- timer,
- publicMessage,
- timerMessage,
- lowerMessage,
- externalMessage,
- playback,
- onAir,
- eventNext,
- publicEventNext,
- publicEventNow,
- eventNow,
- loaded,
- } = useStore(runtime);
+ const { timer, message, playback, onAir, eventNext, publicEventNext, publicEventNow, eventNow, loaded } =
+ useStore(runtime);
const publicSelectedId = loaded.selectedPublicEventId;
const selectedId = loaded.selectedEventId;
const nextId = loaded.nextEventId;
@@ -93,10 +81,10 @@ const withData =
(Component: ComponentType
) => {
{
+ 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);
+ });
+});
diff --git a/apps/server/src/adapters/utils/parse.ts b/apps/server/src/adapters/utils/parse.ts
new file mode 100644
index 000000000..c52f334f4
--- /dev/null
+++ b/apps/server/src/adapters/utils/parse.ts
@@ -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;
+};
diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts
index 862d9baa7..fbdafb977 100644
--- a/apps/server/src/app.ts
+++ b/apps/server/src/app.ts
@@ -174,10 +174,7 @@ export const startServer = async () => {
* - Runtime Service publicEventNext
*/
eventStore.init({
- timerMessage: messageService.timerMessage,
- publicMessage: messageService.publicMessage,
- lowerMessage: messageService.lowerMessage,
- externalMessage: messageService.externalMessage,
+ message: messageService,
onAir: state.playback !== Playback.Stop,
timer: state.timer,
playback: state.playback,
diff --git a/apps/server/src/controllers/integrationController.ts b/apps/server/src/controllers/integrationController.ts
index 84a7e53ed..2a9a1236d 100644
--- a/apps/server/src/controllers/integrationController.ts
+++ b/apps/server/src/controllers/integrationController.ts
@@ -1,11 +1,14 @@
+import { DeepPartial, MessageState } from 'ontime-types';
+
// 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 { messageService } from '../services/message-service/MessageService.js';
import { runtimeService } from '../services/runtime-service/RuntimeService.js';
import { eventStore } from '../stores/EventStore.js';
import { parse, updateEvent } from './integrationController.config.js';
+import { validateMessage, validateTimerMessage } from '../services/message-service/messageUtils.js';
export type ChangeOptions = {
eventId: string;
@@ -32,11 +35,10 @@ export function dispatchFromAdapter(
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': () => ({
+ version: () => ({ payload: ONTIME_VERSION }),
+ poll: () => ({
payload: eventStore.poll(),
}),
change: (payload) => {
@@ -48,123 +50,56 @@ const actionHandlers: Record = {
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 };
+ message: (payload) => {
+ assert.isObject(payload);
+
+ const patch: DeepPartial = {
+ timer: 'timer' in payload ? validateTimerMessage(payload.timer) : undefined,
+ public: 'public' in payload ? validateMessage(payload.public) : undefined,
+ lower: 'lower' in payload ? validateMessage(payload.lower) : undefined,
+ external: 'external' in payload ? validateMessage(payload.external) : undefined,
+ };
+
+ const newMessage = messageService.patch(patch);
+ return { payload: newMessage };
},
/* Playback */
start: (payload) => {
if (payload && typeof payload === 'object') {
+ if ('next' in payload) {
+ runtimeService.startNext();
+ return { payload: 'start' };
+ }
if ('index' in payload) {
- const reply = actionHandlers.startindex(payload.index);
- return reply;
+ const eventIndex = numberOrError(payload.index);
+ 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) {
- const reply = actionHandlers.startid(payload.id);
- return reply;
+ assert.isString(payload);
+ runtimeService.startById(payload);
+ return { payload: 'success' };
}
if ('cue' in payload) {
- const reply = actionHandlers.startcue(payload.cue);
- return reply;
+ assert.isString(payload);
+ runtimeService.startByCue(payload);
+ return { payload: 'success' };
}
}
-
runtimeService.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: () => {
runtimeService.pause();
return { payload: 'success' };
},
- previous: () => {
- runtimeService.loadPrevious();
- return { payload: 'success' };
- },
- next: () => {
- runtimeService.loadNext();
- return { payload: 'success' };
- },
stop: () => {
runtimeService.stop();
return { payload: 'success' };
@@ -177,24 +112,37 @@ const actionHandlers: Record = {
runtimeService.roll();
return { payload: 'success' };
},
- loadindex: (payload) => {
- const eventIndex = numberOrError(payload);
- if (eventIndex <= 0) {
- throw new Error(`Event index out of range ${eventIndex}`);
+ load: (payload) => {
+ if (payload && typeof payload === 'object') {
+ if ('index' in payload) {
+ 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
- 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' };
+ throw new Error('No load method provided');
},
addtime: (payload) => {
const time = numberOrError(payload);
diff --git a/apps/server/src/routes/apiRouter.ts b/apps/server/src/routes/apiRouter.ts
index fd7f7c281..2b08a819c 100644
--- a/apps/server/src/routes/apiRouter.ts
+++ b/apps/server/src/routes/apiRouter.ts
@@ -2,6 +2,7 @@ import express from 'express';
import { dispatchFromAdapter } from '../controllers/integrationController.js';
import { logger } from '../classes/Logger.js';
import { LogOrigin } from 'ontime-types';
+import { objectFromPath } from '../adapters/utils/parse.js';
export const router = express.Router();
@@ -14,8 +15,15 @@ router.get('/', (_req, res) => {
// 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 };
+ let action = req.path.substring(1);
+ 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 {
const reply = dispatchFromAdapter(action, params, 'http');
diff --git a/apps/server/src/services/message-service/MessageService.ts b/apps/server/src/services/message-service/MessageService.ts
index f590c08db..f114058e6 100644
--- a/apps/server/src/services/message-service/MessageService.ts
+++ b/apps/server/src/services/message-service/MessageService.ts
@@ -1,4 +1,4 @@
-import { Message, TimerMessage } from 'ontime-types';
+import { DeepPartial, Message, TimerMessage, MessageState } from 'ontime-types';
import { throttle } from '../../utils/throttle.js';
@@ -7,10 +7,10 @@ import type { PublishFn } from '../../stores/EventStore.js';
let instance;
class MessageService {
- timerMessage: TimerMessage;
- publicMessage: Message;
- lowerMessage: Message;
- externalMessage: Message;
+ timer: TimerMessage;
+ public: Message;
+ lower: Message;
+ external: Message;
private throttledSet: PublishFn;
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
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 = () => {
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) {
@@ -55,144 +59,26 @@ class MessageService {
this.throttledSet = throttle((key, value) => this.publish(key, value), 100);
}
- /**
- * @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) {
- 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() {
+ getState(): MessageState {
return {
- timerMessage: this.timerMessage,
- publicMessage: this.publicMessage,
- lowerMessage: this.lowerMessage,
- externalMessage: this.externalMessage,
+ timer: this.timer,
+ public: this.public,
+ lower: this.lower,
+ external: this.external,
};
}
+
+ patch(message: DeepPartial) {
+ 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();
-
-/**
- * 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/services/message-service/__tests__/MessageService.test.ts b/apps/server/src/services/message-service/__tests__/MessageService.test.ts
new file mode 100644
index 000000000..4faff0a4b
--- /dev/null
+++ b/apps/server/src/services/message-service/__tests__/MessageService.test.ts
@@ -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,
+ },
+ });
+ });
+});
diff --git a/apps/server/src/services/message-service/__tests__/messageUtils.test.ts b/apps/server/src/services/message-service/__tests__/messageUtils.test.ts
new file mode 100644
index 000000000..efe40d688
--- /dev/null
+++ b/apps/server/src/services/message-service/__tests__/messageUtils.test.ts
@@ -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);
+ });
+});
diff --git a/apps/server/src/services/message-service/messageUtils.ts b/apps/server/src/services/message-service/messageUtils.ts
new file mode 100644
index 000000000..9c2f12601
--- /dev/null
+++ b/apps/server/src/services/message-service/messageUtils.ts
@@ -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 {
+ assert.isObject(message);
+
+ const result: Partial = {};
+ 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 {
+ assert.isObject(message);
+
+ const result: Partial = {};
+
+ 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;
+}
diff --git a/apps/server/src/utils/__tests__/arrayUtils.tests.ts b/apps/server/src/utils/__tests__/arrayUtils.test.ts
similarity index 100%
rename from apps/server/src/utils/__tests__/arrayUtils.tests.ts
rename to apps/server/src/utils/__tests__/arrayUtils.test.ts
diff --git a/apps/server/src/utils/assert.ts b/apps/server/src/utils/assert.ts
index 0381c0e6e..2dedcbc82 100644
--- a/apps/server/src/utils/assert.ts
+++ b/apps/server/src/utils/assert.ts
@@ -15,3 +15,9 @@ export function isNumber(value: unknown): asserts value is number {
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}`);
+ }
+}
diff --git a/packages/types/src/definitions/runtime/MessageControl.type.ts b/packages/types/src/definitions/runtime/MessageControl.type.ts
index a08537b29..23b6caa4a 100644
--- a/packages/types/src/definitions/runtime/MessageControl.type.ts
+++ b/packages/types/src/definitions/runtime/MessageControl.type.ts
@@ -4,6 +4,13 @@ export type Message = {
};
export type TimerMessage = Message & {
- timerBlink: boolean;
- timerBlackout: boolean;
+ blink: boolean;
+ blackout: boolean;
+};
+
+export type MessageState = {
+ timer: TimerMessage;
+ public: Message;
+ lower: Message;
+ external: Message;
};
diff --git a/packages/types/src/definitions/runtime/RuntimeStore.type.ts b/packages/types/src/definitions/runtime/RuntimeStore.type.ts
index 12be4a8d4..8756ddd8f 100644
--- a/packages/types/src/definitions/runtime/RuntimeStore.type.ts
+++ b/packages/types/src/definitions/runtime/RuntimeStore.type.ts
@@ -1,5 +1,5 @@
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 { Runtime } from './Runtime.type.js';
import { OntimeEvent } from '../core/OntimeEvent.type.js';
@@ -10,10 +10,7 @@ export type RuntimeStore = {
playback: Playback;
// messages service
- timerMessage: TimerMessage;
- publicMessage: Message;
- lowerMessage: Message;
- externalMessage: Message;
+ message: MessageState;
onAir: boolean;
// event loader
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index 118c67f7f..6f0bcfded 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -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 { Playback } from './definitions/runtime/Playback.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 { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js';
@@ -63,4 +63,4 @@ export type { TimerState } from './definitions/runtime/TimerState.type.js';
// TYPE UTILITIES
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';
diff --git a/packages/types/src/utils/utils.type.ts b/packages/types/src/utils/utils.type.ts
index 1a22c2055..c67ec8c2d 100644
--- a/packages/types/src/utils/utils.type.ts
+++ b/packages/types/src/utils/utils.type.ts
@@ -1 +1,5 @@
export type MaybeNumber = number | null;
+
+export type DeepPartial = {
+ [P in keyof T]?: T[P] extends object ? DeepPartial : T[P];
+};