V2 integration (#293)

* feat: integrations and lifecycles

* refactor: prevent issues with start order
This commit is contained in:
Carlos Valente
2023-02-23 20:22:38 +01:00
committed by GitHub
parent d8a84ac88d
commit 70cc071425
61 changed files with 1510 additions and 687 deletions
@@ -3,6 +3,7 @@
* and adds logic specific to ontime data
*/
import { data, db } from '../../modules/loadDb.js';
import { safeMerge } from './DataProvider.utils.js';
export class DataProvider {
static getData() {
@@ -39,6 +40,7 @@ export class DataProvider {
}
static async deleteEvent(eventId) {
// @ts-expect-error -- this will go away once we type db
data.rundown = Array.from(data.rundown).filter((e) => e.id !== eventId);
await this.persist();
}
@@ -49,6 +51,7 @@ export class DataProvider {
static async clearRundown() {
data.rundown = [];
// @ts-expect-error -- not sure how to type, this is library side
await db.write();
}
@@ -149,11 +152,12 @@ export class DataProvider {
}
static async persist() {
// @ts-expect-error -- not sure how to type, this is library side
await db.write();
}
static async mergeIntoData(newData) {
const mergedData = DataProvider.safeMerge(data, newData);
const mergedData = safeMerge(data, newData);
data.event = mergedData.event;
data.settings = mergedData.settings;
data.osc = mergedData.osc;
@@ -163,36 +167,4 @@ export class DataProvider {
data.rundown = mergedData.rundown;
await this.persist();
}
/**
* Merges two data objects
* @param {object} existing
* @param {object} newData
*/
static safeMerge(existing, newData) {
const mergedData = { ...existing };
if (typeof newData?.rundown !== 'undefined') {
mergedData.rundown = newData.rundown;
}
if (typeof newData?.event !== 'undefined') {
mergedData.event = { ...newData.event };
}
if (typeof newData?.settings !== 'undefined') {
mergedData.settings = { ...newData.settings };
}
if (typeof newData?.osc !== 'undefined') {
mergedData.osc = { ...newData.osc };
}
if (typeof newData?.http !== 'undefined') {
mergedData.http = { ...newData.http };
}
if (typeof newData?.aliases !== 'undefined') {
mergedData.aliases = [...newData.aliases];
}
if (typeof newData?.userFields !== 'undefined') {
mergedData.userFields = { ...existing.userFields, ...newData.userFields };
}
return mergedData;
}
}
@@ -0,0 +1,39 @@
/**
* Merges two data objects
* @param {object} existing
* @param {object} newData
*/
export function safeMerge(existing, newData) {
const { rundown, event, settings, osc, http, aliases, userFields } = newData || {};
return {
...existing,
rundown: rundown ?? existing.rundown,
event: { ...existing.event, ...event },
settings: { ...existing.settings, ...settings },
views: {
overrideStyles: false,
},
aliases: aliases ?? existing.aliases,
userFields: {
...existing.userFields,
...(userFields && Object.fromEntries(Object.entries(userFields).filter(([_, value]) => value !== null))),
},
osc: {
...existing.osc,
...osc,
subscriptions: {
...existing.osc?.subscriptions,
...(newData?.osc?.subscriptions || {}),
...(existing.osc?.subscriptions && newData?.osc?.subscriptions
? Object.keys(existing.osc.subscriptions).reduce((acc, key) => {
if (!(key in newData.osc.subscriptions)) {
acc[key] = existing.osc.subscriptions[key];
}
return acc;
}, {})
: {}),
},
},
http: { ...existing.http, ...http },
};
}
@@ -0,0 +1,302 @@
import { safeMerge } from '../DataProvider.utils.js';
describe('safeMerge', () => {
const existing = {
rundown: [],
event: {
title: 'existing title',
publicUrl: 'existing public URL',
backstageUrl: 'existing backstageUrl',
backstageInfo: 'existing backstageInfo',
endMessage: 'existing endMessage',
},
settings: {
app: 'ontime',
lock: true,
pinCode: null,
timeFormat: '24',
version: 2,
serverPort: 4001,
},
views: {
overrideStyles: false,
},
aliases: [],
userFields: {
user0: 'existing user0',
user1: 'existing user1',
},
osc: {
portIn: 8888,
portOut: 9999,
targetIP: '127.0.0.1',
enabledIn: false,
enabledOut: false,
subscriptions: {
onLoad: {
message: '',
enabled: false,
},
onStart: {
message: '',
enabled: false,
},
onPause: {
message: '',
enabled: false,
},
onStop: {
message: '',
enabled: false,
},
onUpdate: {
message: '',
enabled: false,
},
onFinish: {
message: '',
enabled: false,
},
},
},
http: {
enabled: true,
user: null,
pwd: null,
},
};
it('returns existing data if new data is not provided', () => {
const mergedData = safeMerge(existing, undefined);
expect(mergedData).toEqual(existing);
});
it('merges the rundown key', () => {
const newData = {
rundown: [{ name: 'item 1' }, { name: 'item 2' }],
};
const mergedData = safeMerge(existing, newData);
expect(mergedData.rundown).toEqual(newData.rundown);
});
it('merges the event key', () => {
const newData = {
event: {
title: 'new title',
publicInfo: 'new public info',
},
};
const mergedData = safeMerge(existing, newData);
expect(mergedData.event).toEqual({
title: 'new title',
publicUrl: 'existing public URL',
publicInfo: 'new public info',
backstageUrl: 'existing backstageUrl',
backstageInfo: 'existing backstageInfo',
endMessage: 'existing endMessage',
});
});
it('merges the settings key', () => {
const newData = {
settings: {
serverPort: 3000,
lock: '1234',
},
};
const mergedData = safeMerge(existing, newData);
expect(mergedData.settings).toEqual({
app: 'ontime',
version: 2,
serverPort: 3000,
lock: '1234',
pinCode: null,
timeFormat: '24',
});
});
it('merges the osc key', () => {
const newData = {
osc: {
portIn: 7777,
subscriptions: {
onStart: {
message: 'new message',
enabled: true,
},
},
},
};
const mergedData = safeMerge(existing, newData);
expect(mergedData.osc).toEqual({
portIn: 7777,
portOut: 9999,
targetIP: '127.0.0.1',
enabledIn: false,
enabledOut: false,
subscriptions: {
onLoad: {
message: '',
enabled: false,
},
onStart: {
message: 'new message',
enabled: true,
},
onPause: {
message: '',
enabled: false,
},
onStop: {
message: '',
enabled: false,
},
onUpdate: {
message: '',
enabled: false,
},
onFinish: {
message: '',
enabled: false,
},
},
});
});
it('should merge the aliases key when present', () => {
const existingData = {
rundown: [],
event: {
title: '',
publicUrl: '',
publicInfo: '',
backstageUrl: '',
backstageInfo: '',
endMessage: '',
},
settings: {
app: 'ontime',
version: 2,
serverPort: 4001,
lock: null,
pinCode: null,
timeFormat: '24',
},
views: {
overrideStyles: false,
},
aliases: [],
userFields: {
user0: 'user0',
user1: 'user1',
user2: 'user2',
user3: 'user3',
user4: 'user4',
user5: 'user5',
user6: 'user6',
user7: 'user7',
user8: 'user8',
user9: 'user9',
},
osc: {
portIn: 8888,
portOut: 9999,
targetIP: '127.0.0.1',
enabledIn: false,
enabledOut: false,
subscriptions: {
onLoad: {
message: '',
enabled: false,
},
onStart: {
message: '',
enabled: false,
},
onPause: {
message: '',
enabled: false,
},
onStop: {
message: '',
enabled: false,
},
onUpdate: {
message: '',
enabled: false,
},
onFinish: {
message: '',
enabled: false,
},
},
},
http: {
user: null,
pwd: null,
messages: {
onLoad: {
url: '',
enabled: false,
},
onStart: {
url: '',
enabled: false,
},
onUpdate: {
url: '',
enabled: false,
},
onPause: {
url: '',
enabled: false,
},
onStop: {
url: '',
enabled: false,
},
onFinish: {
url: '',
enabled: false,
},
},
enabled: true,
},
};
const newData = {
aliases: ['alias1', 'alias2'],
};
const mergedData = safeMerge(existingData, newData);
expect(mergedData.aliases).toEqual(newData.aliases);
});
it('merges userFields into existing object', () => {
const existing = {
userFields: {
user0: 'Alice',
user1: 'Bob',
},
};
const newData = {
userFields: {
user2: 'Charlie',
user3: 'David',
user4: null,
},
};
const expected = {
user0: 'Alice',
user1: 'Bob',
user2: 'Charlie',
user3: 'David',
};
const result = safeMerge(existing, newData);
expect(result.userFields).toEqual(expected);
});
});
@@ -1,4 +1,4 @@
import { DataProvider } from '../data-provider/DataProvider.js';
import { DataProvider } from '../data-provider/DataProvider.ts';
import { getRollTimers } from '../../services/rollUtils.js';
let instance;
@@ -1,53 +0,0 @@
import * as http from 'http';
/**
* @description Class contains logic towards outgoing HTTP communications
* @class
*/
export class HTTPIntegration {
constructor() {
// nothing to do here
}
/**
* @description Initializes oscClient
* @param {object} httpConfig - Http configurations options
*/
init(httpConfig) {}
/**
* @description Sends http get request from predefined messages
* @param {string} path - complete http path
*/
async send(path) {
if (path == null) {
console.log('HTTP ERROR: Message undefined');
return;
}
const options = new URL(path);
let str = '';
const req = http.request(options, (res) => {
console.log(`statusCode: ${res.statusCode}`);
res.on('data', function (chunk) {
str += chunk;
});
res.on('end', function () {
console.log(str);
});
});
req.on('error', (error) => {
console.error(error);
});
req.end();
}
shutdown() {
/* Nothing to shutdown */
}
}
-169
View File
@@ -1,169 +0,0 @@
import { Client, Message } from 'node-osc';
/**
* @description Class contains logic towards outgoing OSC communications
* @class
*/
export class OSCIntegration {
constructor() {
// OSC Client
this.ADDRESS = '/ontime';
this.oscClient = null;
}
/**
* @description Returns list of implemented messages
* @returns {object} implemented messages
*/
get implemented() {
return {
play: 'play',
pause: 'pause',
stop: 'stop',
previous: 'prev',
next: 'next',
reload: 'reload',
finished: 'finished',
time: 'time',
overtime: 'overtime',
title: 'title',
eventNumber: 'eventNumber',
presenter: 'presenter',
};
}
/**
* @description Initializes oscClient
* @param {object} oscConfig - oscClient configuration options
* @param {string} oscConfig.ip - oscClient object
* @param {number} oscConfig.port - OSC Destination Port
*/
init(oscConfig) {
const { ip, port } = oscConfig;
const validateType = typeof ip !== 'string' || typeof port !== 'number';
const validateNull = ip == null || port == null;
if (validateType || validateNull) {
return {
success: false,
message: `Config options incorrect`,
};
}
try {
this.oscClient = new Client(ip, port);
return {
success: true,
message: `Initialised OSC Client at ${ip}:${port}`,
};
} catch (error) {
this.oscClient = null;
return {
success: false,
message: `Failed initialising OSC Client: ${error}`,
};
}
}
/**
* @description Sends osc from predefined messages
* @param {string} messageType - message to be sent
* @param {string} [payload] - optional payload required in some message types
*/
async send(messageType, payload) {
const reply = {
success: true,
message: 'OSC Message sent',
};
if (this.oscClient == null) {
reply.success = false;
reply.message = 'Client not initialised';
return reply;
}
if (messageType == null) {
reply.success = false;
reply.message = 'Message undefined';
return reply;
}
// only specify special cases
switch (payload) {
case 'overtime': {
// Whether timer is negative
this.oscClient.send(`${this.ADDRESS}/overtime`, payload, (err) => {
if (err) {
reply.success = false;
reply.message = err;
}
});
break;
}
case 'title': {
if (payload != null && payload !== '') {
// Send Title of current event
this.oscClient.send(`${this.ADDRESS}/title`, payload, (err) => {
if (err) {
reply.success = false;
reply.message = err;
}
});
} else {
reply.success = false;
reply.message = 'Missing message data';
}
break;
}
case 'eventNumber': {
if (payload != null && payload !== '') {
// Send event number of current event
this.oscClient.send(`${this.ADDRESS}/eventNumber`, payload, (err) => {
if (err) {
reply.success = false;
reply.message = err;
}
});
} else {
reply.success = false;
reply.message = 'Missing message data';
}
break;
}
case 'presenter': {
if (payload != null && payload !== '') {
// Send timer data on current event
this.oscClient.send(`${this.ADDRESS}/presenter`, payload, (err) => {
if (err) {
reply.success = false;
reply.message = err;
}
});
} else {
reply.success = false;
reply.message = 'Missing message data';
}
break;
}
default: {
// catch all for messages, allows to add new messages
// but should be used with the integrations definition
const message = new Message(`${this.ADDRESS}/${messageType}`);
if (payload != null) message.append(payload);
this.oscClient.send(message, (err) => {
if (err) {
reply.success = false;
reply.message = err;
}
});
break;
}
}
return reply;
}
shutdown() {
// Shutdown client object
this.oscClient.close();
this.oscClient = null;
}
}
@@ -1,163 +0,0 @@
import { OSCIntegration } from '../Osc';
import { Server } from 'node-osc';
test('Class initialises correctly', () => {
const osc = new OSCIntegration();
expect(osc.ADDRESS).toBe('/ontime');
expect(osc.oscClient).toBe(null);
// defined objects
expect(osc.implemented.play).toBeDefined();
expect(osc.implemented.pause).toBeDefined();
expect(osc.implemented.stop).toBeDefined();
expect(osc.implemented.previous).toBeDefined();
expect(osc.implemented.next).toBeDefined();
expect(osc.implemented.reload).toBeDefined();
expect(osc.implemented.finished).toBeDefined();
expect(osc.implemented.time).toBeDefined();
expect(osc.implemented.overtime).toBeDefined();
expect(osc.implemented.title).toBeDefined();
expect(osc.implemented.eventNumber).toBeDefined();
expect(osc.implemented.presenter).toBeDefined();
// initialise client succeeds
const { ip, port } = { ip: '127.0.0.1', port: 12345 };
const init = osc.init({ ip, port });
expect(init.message).toBe(`Initialised OSC Client at ${ip}:${port}`);
expect(init.success).toBe(true);
expect(osc.oscClient).not.toBe(null);
// object shutdown as expected
osc.shutdown();
expect(osc.oscClient).toBe(null);
});
describe('OSC fails to initialise when incorrect data is given', () => {
it('IP of wrong type', () => {
const osc = new OSCIntegration();
const init = osc.init({ ip: 123, port: 8888 });
expect(init.message).toBe('Config options incorrect');
expect(init.success).toBe(false);
expect(osc.oscClient).toBe(null);
});
it('IP is null', () => {
const osc = new OSCIntegration();
const init = osc.init({ ip: null, port: 8888 });
expect(init.message).toBe('Config options incorrect');
expect(init.success).toBe(false);
expect(osc.oscClient).toBe(null);
});
it('Port of wrong type', () => {
const osc = new OSCIntegration();
const init = osc.init({ ip: 'localhost', port: 'test' });
expect(init.message).toBe('Config options incorrect');
expect(init.success).toBe(false);
expect(osc.oscClient).toBe(null);
});
it('Port is null', () => {
const osc = new OSCIntegration();
const init = osc.init({ ip: 'localhost', port: null });
expect(init.message).toBe('Config options incorrect');
expect(init.success).toBe(false);
expect(osc.oscClient).toBe(null);
});
});
test('Test messages sending', async () => {
const testPort = 9999;
const testIP = 'localhost';
const testPayload = 'test';
const osc = new OSCIntegration();
const messages = [];
// prepare dummy server to receive messages
const oscServer = new Server(testPort, testIP);
oscServer.on('message', (m) => {
messages.push({ yay: m });
});
// try and send a message before initialising
const test = await osc.send('test');
expect(test.success).toBe(false);
expect(test.message).toBe('Client not initialised');
// initialise osc
osc.init({ ip: testIP, port: testPort });
// try and send unrecognised message
const test2 = await osc.send('test');
expect(test2.success).toBe(true);
// send play message
const playAddress = osc.implemented.play;
const playSent = await osc.send(playAddress);
expect(playSent.success).toBe(true);
// send pause message
const pauseAddress = osc.implemented.pause;
const pauseSent = await osc.send(pauseAddress);
expect(pauseSent.success).toBe(true);
// send stop message
const stopAddress = osc.implemented.stop;
const stopSent = await osc.send(stopAddress);
expect(stopSent.success).toBe(true);
// send previous message
const previousAddress = osc.implemented.previous;
const previousSent = await osc.send(previousAddress);
expect(previousSent.success).toBe(true);
// send next message
const nextAddress = osc.implemented.next;
const nextSent = await osc.send(nextAddress);
expect(nextSent.success).toBe(true);
// send reload message
const reloadAddress = osc.implemented.reload;
const reloadSent = await osc.send(reloadAddress);
expect(reloadSent.success).toBe(true);
// send finished message
const finishedAddress = osc.implemented.finished;
const finishedSent = await osc.send(finishedAddress);
expect(finishedSent.success).toBe(true);
// send time message
const timeAddress = osc.implemented.time;
const timeSent = await osc.send(timeAddress);
expect(timeSent.success).toBe(true);
// send overtime message
const overtimeAddress = osc.implemented.overtime;
const overtimeSent = await osc.send(overtimeAddress, testPayload);
expect(overtimeSent.success).toBe(true);
// send title message
const titleAddress = osc.implemented.title;
const titleSent = await osc.send(titleAddress, testPayload);
expect(titleSent.success).toBe(true);
// send eventNumber message
const eventNumberAddress = osc.implemented.eventNumber;
const eventNumberSent = await osc.send(eventNumberAddress, testPayload);
expect(eventNumberSent.success).toBe(true);
// send timer message
const presenterAddress = osc.implemented.presenter;
const presenterSent = await osc.send(presenterAddress, testPayload);
expect(presenterSent.success).toBe(true);
// cleanup
await osc.shutdown();
await oscServer.close();
// see messagesObject
// expect(messages.length).toBe(5);
});
@@ -7,7 +7,7 @@ import { messageManager } from '../message-manager/MessageManager.js';
import { PlaybackService } from '../../services/PlaybackService.js';
import { ADDRESS_MESSAGE_CONTROL } from './socketConfig.js';
import { eventTimer, TimerService } from '../../services/TimerService.js';
import { eventTimer, TimerService } from '../../services/TimerService.ts';
import { EventLoader, eventLoader } from '../event-loader/EventLoader.js';
class SocketController {