Refactor/socket controller (#212)

* refactor(socket): dictionary cleanup
* refactor(socket): detangle socket from EventTimer
* refactor(socket): poll object
* refactor(socket): wip, extract data responsibilities to provider
* fix: issue with onAir
* refactor: create data provider and validation utils
* refactor: remove deprecated endpoint
* refactor: detangle and validate ontime controller
* refactor: detangle and validate events controller
* refactor: validate routers
* refactor: handle post failure in modals
This commit is contained in:
Carlos Valente
2022-10-09 19:10:48 +02:00
committed by GitHub
parent cd0577bf99
commit c2ed9d7634
28 changed files with 1145 additions and 898 deletions
-6
View File
@@ -165,12 +165,6 @@ export const getInfo = async () => {
return res.data;
};
/**
* @description HTTP request to mutate application info
* @return {Promise}
*/
export const postInfo = async (data) => axios.post(`${ontimeURL}/info`, data);
/**
* @description HTTP request to retrieve view settings
* @return {Promise}
@@ -58,6 +58,7 @@ export default function AppSettingsModal() {
setSubmitting(true);
// set context
// TODO: add fast-equals here and check if event settings have changed
saveEventSettings(formSettings);
const validation = { isValid: false };
@@ -83,12 +84,17 @@ export default function AppSettingsModal() {
if (!validation.isValid) {
emitError(`Invalid Input: ${validation.message}`);
} else {
await postSettings(formData);
await refetch();
try {
await postSettings(formData);
} catch (error) {
emitError(`Error saving settings: ${error}`)
} finally {
await refetch();
setChanged(false);
}
validation?.message && emitWarning(validation.message);
}
setSubmitting(false);
setChanged(false);
};
/**
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useContext, useEffect, useState } from 'react';
import { ModalBody } from '@chakra-ui/modal';
import { FormLabel, Input, Textarea } from '@chakra-ui/react';
import { EVENT_TABLE } from 'common/api/apiConstants';
@@ -6,6 +6,7 @@ import { fetchEvent, postEvent } from 'common/api/eventApi';
import { useFetch } from 'common/hooks/useFetch';
import { eventPlaceholderSettings } from '../../common/api/ontimeApi';
import { LoggingContext } from '../../common/context/LoggingContext';
import { inputProps } from './modalHelper';
import SubmitContainer from './SubmitContainer';
@@ -14,6 +15,7 @@ import style from './Modals.module.scss';
export default function SettingsModal() {
const { data, status, refetch } = useFetch(EVENT_TABLE, fetchEvent);
const { emitError } = useContext(LoggingContext);
const [formData, setFormData] = useState(eventPlaceholderSettings);
const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false);
@@ -42,13 +44,18 @@ export default function SettingsModal() {
event.preventDefault();
setSubmitting(true);
await postEvent(formData);
await refetch();
try {
await postEvent(formData);
} catch (error) {
emitError(`Error saving event settings: ${error}`)
} finally {
await refetch();
setChanged(false);
}
setChanged(false);
setSubmitting(false);
},
[formData, refetch]
[emitError, formData, refetch]
);
/**
@@ -3,7 +3,7 @@ import { ModalBody } from '@chakra-ui/modal';
import { FormControl, FormLabel, Input, Switch } from '@chakra-ui/react';
import { FiInfo } from '@react-icons/all-files/fi/FiInfo';
import { APP_TABLE } from 'common/api/apiConstants';
import { getInfo, httpPlaceholder, ontimeVars, postInfo } from 'common/api/ontimeApi';
import { getInfo, httpPlaceholder, ontimeVars } from 'common/api/ontimeApi';
import { useFetch } from 'common/hooks/useFetch';
import { LoggingContext } from '../../common/context/LoggingContext';
@@ -50,7 +50,7 @@ export default function IntegrationSettingsModal() {
if (e.status) {
emitError(`Invalid Input: ${e.message}`);
} else {
await postInfo(f);
// call API endpoint here with value of f
setChanged(false);
setSubmitting(false);
}
@@ -122,10 +122,14 @@ export default function OscSettingsModal() {
if (e.status) {
emitError(`Invalid Input: ${e.message}`);
} else {
// Post here
await postOSC(formData);
await refetch();
setChanged(false);
try {
await postOSC(formData);
} catch (error){
emitError(`Error setting OSC: ${error}`)
} finally {
await refetch();
setChanged(false);
}
}
setSubmitting(false);
},
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useContext, useEffect, useState } from 'react';
import { ModalBody } from '@chakra-ui/modal';
import { Input } from '@chakra-ui/react';
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
@@ -6,6 +6,7 @@ import { USERFIELDS } from 'common/api/apiConstants';
import { useFetch } from 'common/hooks/useFetch';
import { getUserFields, postUserFields, userFieldsPlaceholder } from '../../common/api/ontimeApi';
import { LoggingContext } from '../../common/context/LoggingContext';
import { handleLinks, host } from '../../common/utils/linkUtils';
import SubmitContainer from './SubmitContainer';
@@ -14,6 +15,7 @@ import style from './Modals.module.scss';
export default function TableOptionsModal() {
const { data, status, refetch } = useFetch(USERFIELDS, getUserFields);
const { emitError } = useContext(LoggingContext);
const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [userFields, setUserFields] = useState(userFieldsPlaceholder);
@@ -43,13 +45,17 @@ export default function TableOptionsModal() {
}
if (!errors) {
await postUserFields(validatedFields);
try {
await postUserFields(validatedFields);
} catch (error) {
emitError(`Error saving table options: ${error}`)
}
await refetch();
setChanged(false);
}
setSubmitting(false);
},[refetch, userFields]);
},[emitError, refetch, userFields]);
/**
* Reverts local state equals to server state
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useContext, useEffect, useState } from 'react';
import { ModalBody } from '@chakra-ui/modal';
import { FormControl, FormLabel } from '@chakra-ui/react';
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
@@ -6,6 +6,7 @@ import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInforma
import { VIEW_SETTINGS } from '../../common/api/apiConstants';
import { getView, postView, viewsPlaceholder } from '../../common/api/ontimeApi';
import EnableBtn from '../../common/components/buttons/EnableBtn';
import { LoggingContext } from '../../common/context/LoggingContext';
import { useFetch } from '../../common/hooks/useFetch';
import { openLink } from '../../common/utils/linkUtils';
@@ -15,6 +16,7 @@ import style from './Modals.module.scss';
export default function ViewsSettingsModal() {
const { data, status, refetch } = useFetch(VIEW_SETTINGS, getView);
const { emitError } = useContext(LoggingContext);
const [formData, setFormData] = useState(viewsPlaceholder);
const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false);
@@ -35,12 +37,17 @@ export default function ViewsSettingsModal() {
async (event) => {
event.preventDefault();
setSubmitting(true);
await postView(formData);
await refetch();
setChanged(false);
try {
await postView(formData);
} catch (error) {
emitError(`Error view settings: ${error}`)
} finally{
await refetch();
setChanged(false);
}
setSubmitting(false);
},
[formData, refetch]
[emitError, formData, refetch]
);
/**
+12
View File
@@ -0,0 +1,12 @@
module.exports = {
appIni: {
mainWindowWait: 2000,
},
reactAppUrl: {
development: 'http://localhost:3000/editor',
production: 'http://localhost:4001/editor',
},
externalUrls: {
help: 'https://cpvalente.gitbook.io/ontime/',
},
};
+15 -14
View File
@@ -10,19 +10,18 @@ const {
Notification,
} = require('electron');
const path = require('path');
const electronConfig = require('./electron.config');
if (process.env.NODE_ENV === undefined) {
process.env.NODE_ENV = 'production';
}
const env = process.env.NODE_ENV;
const isProduction = process.env.NODE_ENV === 'production';
let loaded = 'Nothing loaded';
let isQuitting = false;
const nodePath =
env !== 'production'
? path.join('file://', __dirname, 'src/app.js')
: path.join('file://', __dirname, '../', 'extraResources', 'src/app.js');
const nodePath = isProduction
? path.join('file://', __dirname, '../', 'extraResources', 'src/app.js')
: path.join('file://', __dirname, 'src/app.js');
(async () => {
try {
@@ -144,22 +143,25 @@ app.whenReady().then(() => {
// give the nodejs server some time
setTimeout(() => {
// Load page served by node
const reactApp =
env === 'development' ? 'http://localhost:3000/editor' : 'http://localhost:4001/editor';
const reactApp = isProduction
? electronConfig.reactAppUrl.production
: electronConfig.reactAppUrl.development;
win.loadURL(reactApp).then(() => {
win.webContents.setBackgroundThrottling(false);
// window stuff
win.show();
win.focus();
splash.destroy();
// tray stuff
tray.setToolTip(loaded);
if (typeof loaded === 'string') {
tray.setToolTip(loaded);
} else {
tray.setToolTip('Initialising error: please restart ontime');
}
});
}, 2000);
}, electronConfig.appIni.mainWindowWait);
// Hide on close
win.on('close', function (event) {
@@ -173,7 +175,6 @@ app.whenReady().then(() => {
});
// create tray
// TODO: Design better icon
tray = new Tray(trayIcon);
// Define context menu
@@ -269,7 +270,7 @@ ipcMain.on('send-to-link', (event, arg) => {
// send to help URL
if (arg === 'help') {
shell.openExternal('https://cpvalente.gitbook.io/ontime/');
shell.openExternal(electronConfig.externalUrls.help);
} else {
shell.openExternal(arg);
}
+24 -15
View File
@@ -21,9 +21,11 @@ import { router as playbackRouter } from './routes/playbackRouter.js';
// Global Objects
import { EventTimer } from './classes/timer/EventTimer.js';
import { socketProvider } from './classes/socket/SocketController.js';
// Start OSC server
import { initiateOSC, shutdownOSCServer } from './controllers/OscController.js';
import { fileURLToPath } from 'url';
import { DataProvider } from './classes/data-provider/DataProvider.js';
// get environment
const env = process.env.NODE_ENV || 'production';
@@ -31,9 +33,11 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export const { db, data } = await loadDb(__dirname);
console.log(`Starting ontime version ${process.env.npm_package_version}`);
// import socket provider
const socket = socketProvider;
// Create express APP
const app = express();
app.disable('x-powered-by');
@@ -80,22 +84,22 @@ app.use((err, req, res, next) => {
*
*/
const osc = data.osc;
const { osc, settings } = DataProvider.getData();
const oscIP = osc?.targetIP || config.osc.targetIP;
const oscOutPort = osc?.portOut || config.osc.portOut;
const oscInPort = osc?.port || config.osc.port;
const oscInEnabled = osc?.enabled !== undefined ? osc.enabled : config.osc.inputEnabled;
const serverPort = data.settings.serverPort || config.server.port;
const serverPort = settings.serverPort || config.server.port;
/**
* @description starts OSC server
* @description starts OSC server
* @param overrideConfig
* @return {Promise<void>}
*/
export const startOSCServer = async (overrideConfig = null) => {
if (!oscInEnabled) {
global.timer.info('RX', 'OSC Input Disabled');
socket.info('RX', 'OSC Input Disabled');
return;
}
@@ -105,7 +109,7 @@ export const startOSCServer = async (overrideConfig = null) => {
};
// Start OSC Server
global.timer.info('RX', `Starting OSC Server on port: ${oscInPort}`);
socket.info('RX', `Starting OSC Server on port: ${oscInPort}`);
initiateOSC(oscSettings);
};
@@ -119,11 +123,16 @@ const server = http.createServer(app);
*/
export const startServer = async (overrideConfig = null) => {
const port = 4001; // port hardcoded
const { events, http } = DataProvider.getData();
// Start server
const returnMessage = `Ontime is listening on port ${port}`;
server.listen(port, '0.0.0.0');
// init socket controller
await socket.initServer(server);
socket.info('SERVER', 'Socket initialised');
// OSC Config
const oscConfig = {
ip: oscIP,
@@ -131,9 +140,11 @@ export const startServer = async (overrideConfig = null) => {
};
// init timer
global.timer = new EventTimer(server, config.timer, oscConfig, data.http);
global.timer.setupWithEventList(data.events);
global.timer.info('SERVER', returnMessage);
global.timer = new EventTimer(socket, config.timer, oscConfig, http);
global.timer.setupWithEventList(events);
socket.info('SERVER', returnMessage);
socket.startListener();
return returnMessage;
};
@@ -146,16 +157,14 @@ export const shutdown = async () => {
// shutdown express server
server.close();
// shutdown OSC Server
shutdownOSCServer();
// shutdown timer
global.timer.shutdown();
socket.shutdown();
};
// register shutdown signals
process.once('SIGHUP', shutdown)
process.once('SIGINT', shutdown)
process.once('SIGTERM', shutdown)
process.once('SIGHUP', shutdown);
process.once('SIGINT', shutdown);
process.once('SIGTERM', shutdown);
export { server, app };
@@ -1,7 +1,168 @@
/**
* Class Event Provider adds functions specific for handling event data
* Class Event Provider is a mediator for handling the local db
* and adds logic specific to ontime data
*/
import { data, db } from '../../app.js';
export class DataProvider {
static getData() {
return data;
}
static async setEventData(newData) {
data.event = { ...data.event, ...newData };
await this.persist();
return data.event;
}
static getEventData() {
return data.event;
}
static async setEvents(newData) {
data.events = [...newData];
await this.persist();
}
static getEventById(eventId) {
return data.events.find((e) => e.id === eventId);
}
static async updateEventById(eventId, newData) {
const eventIndex = data.events.findIndex((e) => e.id === eventId);
const e = data.events[eventIndex];
data.events[eventIndex] = { ...e, ...newData };
data.events[eventIndex].revision++;
await this.persist();
return data.events[eventIndex];
}
static async deleteEvent(eventId) {
data.events = Array.from(data.events).filter((e) => e.id !== eventId);
await this.persist();
}
static getNumEvents() {
return data.events.length;
}
static async deleteAllEvents() {
data.events = [];
await db.write();
}
/**
* Insets an event after a given index
* @param entry
* @param index
* @return {Promise<void>}
* @private
*/
static async insertEventAt(entry, index) {
// get events
const events = DataProvider.getEvents();
const count = events.length;
const order = entry.order;
// Remove order field from object
delete entry.order;
// Insert at beginning
if (order === 0) {
events.unshift(entry);
}
// insert at end
else if (order >= count) {
events.push(entry);
}
// insert in the middle
else {
events.splice(index, 0, entry);
}
// save events
await DataProvider.setEvents(events);
}
/**
* @description Inserts an entry after an element with given ID
* @param entry
* @param id
* @return {Promise<void>}
* @private
*/
static async insertEventAfterId(entry, id) {
const index = [...data.events].findIndex((event) => event.id === id);
await DataProvider.insertEventAt(entry, index + 1);
}
static getSettings() {
return data.settings;
}
static async setSettings(newData) {
data.settings = { ...newData };
await this.persist();
}
static getOsc() {
return data.osc;
}
static getAliases() {
return data.aliases;
}
static async setAliases(newData) {
data.aliases = newData;
await this.persist();
}
static getUserFields() {
return { ...data.userFields };
}
static getViews() {
return { ...data.views };
}
static async setViews(newData) {
data.views = { ...newData };
await this.persist();
}
static async setUserFields(newData) {
data.userFields = { ...newData };
await this.persist();
}
static async setOsc(newData) {
data.osc = { ...newData };
await this.persist();
}
static getEvents() {
return [...data.events];
}
static async persist() {
await db.write();
}
static async mergeIntoData(newData) {
const mergedData = DataProvider.safeMerge(data, newData);
data.event = mergedData.event;
data.settings = mergedData.settings;
data.osc = mergedData.osc;
data.http = mergedData.http;
data.aliases = mergedData.aliases;
data.userFields = mergedData.userFields;
data.events = mergedData.events;
await this.persist();
}
/**
* Merges two data objects
* @param {object} existing
@@ -0,0 +1,400 @@
import { Server } from 'socket.io';
import getRandomName from '../../utils/getRandomName.js';
import { generateId } from '../../utils/generate_id.js';
import { stringFromMillis } from '../../utils/time.js';
import { Timer } from '../timer/Timer.js';
class SocketController {
constructor() {
this.numClients = 0;
this.messageStack = [];
this._MAX_MESSAGES = 100;
this._clientNames = {};
this.socket = null;
}
initServer(httpServer) {
this.socket = new Server(httpServer, {
cors: {
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
preflightContinue: false,
optionsSuccessStatus: 204,
},
});
}
startListener() {
this._socketMessageHandler();
}
shutdown() {
this.info('SERVER', 'Shutting down ontime');
if (this.socket) {
this.info('TX', '... Closing socket server');
this.socket.close();
}
}
/**
* Handle socket io connections
* @private
*/
_socketMessageHandler() {
this.socket.on('connection', (socket) => {
/*******************************/
/*** HANDLE NEW CONNECTION ***/
/*** --------------------- ***/
/*******************************/
// keep track of connections
this.numClients++;
this._clientNames[socket.id] = getRandomName();
const message = `${this.numClients} Clients with new connection: ${
this._clientNames[socket.id]
}`;
this.info('CLIENT', message);
// send state
socket.emit('timer', global.timer.getTimeObject());
socket.emit('playstate', global.timer.state);
socket.emit('selected-id', global.timer.selectedEventId);
socket.emit('next-id', global.timer.nextEventId);
socket.emit('publicselected-id', global.timer.selectedPublicEventId);
socket.emit('publicnext-id', global.timer.nextPublicEventId);
/**
* @description handle disconnecting a user
*/
socket.on('disconnect', () => {
this.numClients--;
const message = `${this.numClients} Clients with disconnection: ${
this._clientNames[socket.id]
}`;
delete this._clientNames[socket.id];
this.info('CLIENT', message);
});
/**
* @description utility for renaming a user
*/
socket.on('rename-client', (newName) => {
if (newName) {
const previousName = this._clientNames[socket.id];
this._clientNames[socket.id] = newName;
this.info('CLIENT', `Client ${previousName} renamed to ${newName}`);
}
});
/***************************************/
/*** TIMER STATE GETTERS / SETTERS ***/
/*** ------- WEBSOCKET API ------- ***/
/*** ----------------------------- ***/
/***************************************/
/*******************************************/
socket.on('ontime-test', () => {
socket.emit('hello', socket.id);
});
socket.on('set-start', () => {
global.timer.trigger('start');
socket.emit('playstate', global.timer.state);
});
socket.on('set-startid', (data) => {
global.timer.trigger('startById', data);
socket.emit('playstate', global.timer.state);
});
socket.on('set-startindex', (data) => {
const eventIndex = Number(data);
if (isNaN(eventIndex)) {
return;
}
global.timer.trigger('startByIndex', data);
socket.emit('playstate', global.timer.state);
});
socket.on('set-loadid', (data) => {
global.timer.trigger('loadById', data);
socket.emit('playstate', global.timer.state);
});
socket.on('set-loadindex', (data) => {
const eventIndex = Number(data);
if (isNaN(eventIndex)) {
return;
}
global.timer.trigger('loadByIndex', data);
socket.emit('playstate', global.timer.state);
});
socket.on('set-pause', () => {
global.timer.trigger('pause');
socket.emit('playstate', global.timer.state);
});
socket.on('set-stop', () => {
global.timer.trigger('stop');
socket.emit('playstate', global.timer.state);
});
socket.on('set-reload', () => {
global.timer.trigger('reload');
socket.emit('playstate', global.timer.state);
});
socket.on('set-previous', () => {
global.timer.trigger('previous');
socket.emit('playstate', global.timer.state);
});
socket.on('set-next', () => {
global.timer.trigger('next');
socket.emit('playstate', global.timer.state);
});
socket.on('set-roll', () => {
global.timer.trigger('roll');
socket.emit('playstate', global.timer.state);
});
socket.on('set-delay', (data) => {
const delayTime = Number(data);
if (isNaN(delayTime)) {
return;
}
global.timer.increment(delayTime * 1000 * 60);
});
socket.on('set-onAir', (data) => {
try {
const d = JSON.parse(data);
d ? global.timer.trigger('onAir') : global.timer.trigger('offAir');
} catch (error) {
this.error('RX', `Failed to parse message ${data}`);
}
this.send('onAir', global.timer.onAir);
global.timer._broadcastFeatureMessageControl();
});
/*******************************************/
// general playback state, useful for external sync
socket.on('ontime-poll', () => {
const timerPoll = global.timer.poll();
const isDelayed = false;
const colour = '';
socket.emit('ontime-poll', { isDelayed, colour, ...timerPoll });
});
/*******************************************/
// ** TO BE DEPRECATED ** //
socket.on('get-timer', () => {
socket.emit('timer', global.timer.getTimeObject());
});
// ** TO BE DEPRECATED IN FAVOR OF DELAY ** //
socket.on('increment-timer', (data) => {
if (isNaN(parseInt(data, 10))) return;
if (data < -5 || data > 5) return;
global.timer.increment(data * 1000 * 60);
});
/*******************************************/
// playstate
socket.on('set-playstate', (data) => {
global.timer.trigger(data);
global.timer._broadcastFeaturePlaybackControl();
global.timer._broadcastFeatureInfo();
});
socket.on('get-playstate', () => {
socket.emit('playstate', global.timer.state);
});
socket.on('get-onAir', () => {
socket.emit('onAir', global.timer.onAir);
});
/*******************************************/
// selection data
socket.on('get-selected', () => {
socket.emit('selected', {
id: global.timer.selectedEventId,
index: global.timer.selectedEventIndex,
total: global.timer._eventlist.length,
});
});
socket.on('get-selected-id', () => {
socket.emit('selected-id', global.timer.selectedEventId);
});
socket.on('get-next-id', () => {
socket.emit('next-id', global.timer.nextEventId);
});
// title data
socket.on('get-titles', () => {
socket.emit('titles', global.timer.titles);
});
socket.on('get-publictitles', () => {
socket.emit('publictitles', global.timer.titlesPublic);
});
/***********************************/
/*** MESSAGE GETTERS / SETTERS ***/
/*** ------------------------- ***/
/***********************************/
/*******************************************/
// Presenter message
socket.on('set-timer-message-text', (data) => {
global.timer._setTitles('set-timer-text', data);
global.timer._broadcastFeatureMessageControl();
});
socket.on('set-timer-message-visible', (data) => {
global.timer._setTitles('set-timer-visible', data);
global.timer._broadcastFeatureMessageControl();
});
/*******************************************/
// Public message
socket.on('set-public-message-text', (data) => {
global.timer._setTitles('set-public-text', data);
global.timer._broadcastFeatureMessageControl();
});
socket.on('set-public-message-visible', (data) => {
global.timer._setTitles('set-public-visible', data);
global.timer._broadcastFeatureMessageControl();
});
/*******************************************/
// Lower third message
socket.on('set-lower-message-text', (data) => {
global.timer._setTitles('set-lower-text', data);
global.timer._broadcastFeatureMessageControl();
});
socket.on('set-lower-message-visible', (data) => {
global.timer._setTitles('set-lower-visible', data);
global.timer._broadcastFeatureMessageControl();
});
/* MOLECULAR ENDPOINTS
* =====================
* 1. EVENT LIST
* 2. MESSAGE CONTROL
* 3. PLAYBACK CONTROL
* 4. INFO
* 5. CUE SHEET
* 6. TIMER OBJECT
* */
// 1. EVENT LIST
socket.on('get-ontime-feat-eventlist', () => {
global.timer._broadcastFeatureEventList();
});
// 2. MESSAGE CONTROL
socket.on('get-ontime-feat-messagecontrol', () => {
global.timer._broadcastFeatureMessageControl();
});
// 3. PLAYBACK CONTROL
socket.on('get-ontime-feat-playbackcontrol', () => {
global.timer._broadcastFeaturePlaybackControl();
});
// 4. INFO
socket.on('get-ontime-feat-info', () => {
global.timer._broadcastFeatureInfo();
});
// 5. CUE SHEET
socket.on('get-ontime-feat-cuesheet', () => {
global.timer._broadcastFeatureCuesheet();
});
// 6. TIMER
socket.on('get-ontime-timer', () => {
global.timer._broadcastFeatureTimer();
});
});
}
send(topic, payload) {
this.socket.emit(topic, payload);
}
/****************************************************************************/
/**
* Logger logic
* -------------
*
* This should be separate of event timer, left here for convenience
*
*/
/**
* Utility method, sends message and pushes into stack
* @param {string} level
* @param {string} origin
* @param {string} text
*/
_push(level, origin, text) {
const logMessage = {
id: generateId(),
level,
origin,
text,
time: stringFromMillis(Timer.getCurrentTime() || 0),
};
this.messageStack.unshift(logMessage);
this.socket.emit('logger', logMessage);
if (process.env.NODE_ENV !== 'production') {
console.log(`[${logMessage.level}] \t ${logMessage.origin} \t ${logMessage.text}`);
}
if (this.messageStack.length > this._MAX_MESSAGES) {
this.messageStack.pop();
}
}
/**
* Sends a message with level LOG
* @param {string} origin
* @param {string} text
*/
info(origin, text) {
this._push('INFO', origin, text);
}
/**
* Sends a message with level WARN
* @param {string} origin
* @param {string} text
*/
warning(origin, text) {
this._push('WARN', origin, text);
}
/**
* Sends a message with level ERROR
* @param {string} origin
* @param {string} text
*/
error(origin, text) {
this._push('ERROR', origin, text);
}
}
export const socketProvider = new SocketController();
+66 -443
View File
@@ -1,12 +1,8 @@
import { Timer } from './Timer.js';
import { Server } from 'socket.io';
import { DAY_TO_MS, getSelectionByRoll, replacePlaceholder, updateRoll } from './classUtils.js';
import { OSCIntegration } from './integrations/Osc.js';
import { HTTPIntegration } from './integrations/Http.js';
import { cleanURL } from '../../utils/url.js';
import getRandomName from '../../utils/getRandomName.js';
import { generateId } from '../../utils/generate_id.js';
import { stringFromMillis } from '../../utils/time.js';
/*
* Class EventTimer adds functions specific to APP
@@ -16,12 +12,12 @@ import { stringFromMillis } from '../../utils/time.js';
export class EventTimer extends Timer {
/**
* Instantiates an event timer object
* @param {object} httpServer
* @param {object} socket
* @param {object} timerConfig
* @param {object} [oscConfig]
* @param {object} [httpConfig]
*/
constructor(httpServer, timerConfig, oscConfig, httpConfig) {
constructor(socket, timerConfig, oscConfig, httpConfig) {
// call super constructor
super();
@@ -42,13 +38,15 @@ export class EventTimer extends Timer {
this.ontimeCycle = 'idle';
this.prevCycle = null;
// Socket Object
this.socket = socket;
// OSC Object
this.osc = null;
// HTTP Client Object
this.http = null;
this._numClients = 0;
this._interval = null;
this.presenter = {
@@ -69,26 +67,9 @@ export class EventTimer extends Timer {
this._eventlist = [];
this.onAir = false;
// initialise socketIO server
this.messageStack = [];
this.MAX_MESSAGES = 100;
this._clientNames = {};
this.io = new Server(httpServer, {
cors: {
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
preflightContinue: false,
optionsSuccessStatus: 204,
},
});
// set recurrent emits
this._interval = setInterval(() => this.runCycle(), timerConfig?.refresh || 1000);
// listen to new connections
this._listenToConnections();
if (oscConfig != null) {
this._initOscClient(oscConfig);
}
@@ -103,17 +84,12 @@ export class EventTimer extends Timer {
*/
shutdown() {
clearInterval(this._interval);
this.info('SERVER', 'Shutting down ontime');
if (this.io != null) {
this.info('TX', '... Closing socket server');
this.io.close();
}
if (this.osc != null) {
this.info('TX', '... Closing OSC Client');
this.socket.info('TX', '... Closing OSC Client');
this.osc.shutdown();
}
if (this.http != null) {
this.info('TX', '... Closing HTTP Client');
this.socket.info('TX', '... Closing HTTP Client');
this.http.shutdown();
}
}
@@ -126,7 +102,7 @@ export class EventTimer extends Timer {
_initOscClient(oscConfig) {
this.osc = new OSCIntegration();
const r = this.osc.init(oscConfig);
r.success ? this.info('TX', r.message) : this.error('TX', r.message);
r.success ? this.socket.info('TX', r.message) : this.socket.error('TX', r.message);
}
/**
@@ -135,7 +111,7 @@ export class EventTimer extends Timer {
* @private
*/
_initHTTPClient(httpConfig) {
this.info('TX', `Initialise HTTP Client on port`);
this.socket.info('TX', `Initialise HTTP Client on port`);
this.http = new HTTPIntegration();
this.http.init(httpConfig);
this.httpMessages = httpConfig.messages;
@@ -146,7 +122,7 @@ export class EventTimer extends Timer {
*/
broadcastTimer() {
// through websockets
this.io.emit('timer', this.getTimeObject());
this.socket.send('timer', this.getTimeObject());
}
/**
@@ -162,7 +138,7 @@ export class EventTimer extends Timer {
expectedFinish: this._getExpectedFinish(),
startedAt: this._startedAt,
};
this.io.emit('ontime-timer', featureData);
this.socket.send('ontime-timer', featureData);
}
/**
@@ -174,7 +150,7 @@ export class EventTimer extends Timer {
selectedEventId: this.selectedEventId,
nextEventId: this.nextEventId,
};
this.io.emit('ontime-feat-eventlist', featureData);
this.socket.send('ontime-feat-eventlist', featureData);
}
/**
@@ -188,7 +164,7 @@ export class EventTimer extends Timer {
lower: this.lower,
onAir: this.onAir,
};
this.io.emit('ontime-feat-messagecontrol', featureData);
this.socket.send('ontime-feat-messagecontrol', featureData);
}
/**
@@ -201,7 +177,7 @@ export class EventTimer extends Timer {
selectedEventId: this.selectedEventId,
numEvents: this._eventlist.length,
};
this.io.emit('ontime-feat-playbackcontrol', featureData);
this.socket.send('ontime-feat-playbackcontrol', featureData);
}
/**
@@ -216,7 +192,7 @@ export class EventTimer extends Timer {
selectedEventIndex: this.selectedEventIndex,
numEvents: this._eventlist.length,
};
this.io.emit('ontime-feat-info', featureData);
this.socket.send('ontime-feat-info', featureData);
}
_broadcastFeatureCuesheet() {
@@ -227,7 +203,7 @@ export class EventTimer extends Timer {
numEvents: this._eventlist.length,
titleNow: this.titles.titleNow,
};
this.io.emit('ontime-feat-cuesheet', featureData);
this.socket.send('ontime-feat-cuesheet', featureData);
}
/**
@@ -244,29 +220,20 @@ export class EventTimer extends Timer {
const numEvents = this._eventlist.length;
this.broadcastTimer();
this.io.emit('playstate', this.state);
this.io.emit('selected', {
this.socket.send('playstate', this.state);
this.socket.send('selected', {
id: this.selectedEventId,
index: this.selectedEventIndex,
total: numEvents,
});
this.io.emit('selected-id', this.selectedEventId);
this.io.emit('next-id', this.nextEventId);
this.io.emit('numevents', numEvents);
this.io.emit('publicselected-id', this.selectedPublicEventId);
this.io.emit('publicnext-id', this.nextPublicEventId);
this.io.emit('titles', this.titles);
this.io.emit('publictitles', this.titlesPublic);
this.io.emit('onAir', this.onAir);
}
/**
* Broadcast given message
* @param {string} address - socket io address
* @param {any} payload - message body
*/
broadcastThis(address, payload) {
this.io.emit(address, payload);
this.socket.send('selected-id', this.selectedEventId);
this.socket.send('next-id', this.nextEventId);
this.socket.send('numevents', numEvents);
this.socket.send('publicselected-id', this.selectedPublicEventId);
this.socket.send('publicnext-id', this.nextPublicEventId);
this.socket.send('titles', this.titles);
this.socket.send('publictitles', this.titlesPublic);
this.socket.send('onAir', this.onAir);
}
/**
@@ -282,7 +249,7 @@ export class EventTimer extends Timer {
case 'start': {
if (!numEvents) return false;
// Call action and force update
this.info('PLAYBACK', 'Play Mode Start');
this.socket.info('PLAYBACK', 'Play Mode Start');
this.start();
break;
}
@@ -290,8 +257,8 @@ export class EventTimer extends Timer {
if (!numEvents) return false;
const loaded = this.loadEventById(payload);
if (loaded) {
this.info('PLAYBACK', `Loaded event with ID ${payload}`);
this.info('PLAYBACK', 'Play Mode Start');
this.socket.info('PLAYBACK', `Loaded event with ID ${payload}`);
this.socket.info('PLAYBACK', 'Play Mode Start');
this.start();
} else {
return false;
@@ -302,8 +269,8 @@ export class EventTimer extends Timer {
if (!numEvents) return false;
const loaded = this.loadEventByIndex(payload);
if (loaded) {
this.info('PLAYBACK', `Loaded event with index ${payload}`);
this.info('PLAYBACK', 'Play Mode Start');
this.socket.info('PLAYBACK', `Loaded event with index ${payload}`);
this.socket.info('PLAYBACK', 'Play Mode Start');
this.start();
} else {
return false;
@@ -313,35 +280,35 @@ export class EventTimer extends Timer {
case 'pause': {
if (!numEvents) return false;
// Call action and force update
this.info('PLAYBACK', 'Play Mode Pause');
this.socket.info('PLAYBACK', 'Play Mode Pause');
this.pause();
break;
}
case 'stop': {
if (!numEvents) return false;
// Call action and force update
this.info('PLAYBACK', 'Play Mode Stop');
this.socket.info('PLAYBACK', 'Play Mode Stop');
this.stop();
break;
}
case 'roll': {
if (!numEvents) return false;
// Call action and force update
this.info('PLAYBACK', 'Play Mode Roll');
this.socket.info('PLAYBACK', 'Play Mode Roll');
this.roll();
break;
}
case 'previous': {
if (!numEvents) return false;
// Call action and force update
this.info('PLAYBACK', 'Play Mode Previous');
this.socket.info('PLAYBACK', 'Play Mode Previous');
this.previous();
break;
}
case 'next': {
if (!numEvents) return false;
// Call action and force update
this.info('PLAYBACK', 'Play Mode Next');
this.socket.info('PLAYBACK', 'Play Mode Next');
this.next();
break;
}
@@ -349,7 +316,7 @@ export class EventTimer extends Timer {
if (!numEvents) return false;
const loaded = this.loadEventById(payload);
if (loaded) {
this.info('PLAYBACK', `Loaded event with ID ${payload}`);
this.socket.info('PLAYBACK', `Loaded event with ID ${payload}`);
} else {
return false;
}
@@ -359,7 +326,7 @@ export class EventTimer extends Timer {
if (!numEvents) return false;
const loaded = this.loadEventByIndex(payload);
if (loaded) {
this.info('PLAYBACK', `Loaded event with index ${payload}`);
this.socket.info('PLAYBACK', `Loaded event with index ${payload}`);
} else {
return false;
}
@@ -368,32 +335,32 @@ export class EventTimer extends Timer {
case 'unload': {
if (!numEvents) return false;
// Call action and force update
this.info('PLAYBACK', 'Events unloaded');
this.socket.info('PLAYBACK', 'Events unloaded');
this.unload();
break;
}
case 'reload': {
if (!numEvents) return false;
// Call action and force update
this.info('PLAYBACK', 'Reloaded event');
this.socket.info('PLAYBACK', 'Reloaded event');
this.reload();
break;
}
case 'onAir': {
// Call action
this.info('PLAYBACK', 'Going On Air');
this.socket.info('PLAYBACK', 'Going On Air');
this.setonAir(true);
break;
}
case 'offAir': {
// Call action and force update
this.info('PLAYBACK', 'Going Off Air');
this.socket.info('PLAYBACK', 'Going Off Air');
this.setonAir(false);
break;
}
default: {
// Error, disable flag
this.error('RX', `Unhandled action triggered ${action}`);
this.socket.error('RX', `Unhandled action triggered ${action}`);
success = false;
break;
}
@@ -518,7 +485,7 @@ export class EventTimer extends Timer {
this.ontimeCycle = this.cycleState.onUpdate;
break;
default:
this.error('SERVER', `Unhandled cycle: ${this.ontimeCycle}`);
this.socket.error('SERVER', `Unhandled cycle: ${this.ontimeCycle}`);
}
// send http message if any
@@ -546,13 +513,13 @@ export class EventTimer extends Timer {
update() {
// if there is nothing selected, update clock
this.clock = this._getCurrentTime();
this.clock = Timer.getCurrentTime();
this._broadcastFeatureTimer();
this.broadcastTimer();
// if we are not updating, send the timers
if (this.ontimeCycle !== this.cycleState.onUpdate) {
this.io.emit('timer', this.getTimeObject());
this.socket.send('timer', this.getTimeObject());
}
// Have we skipped onStart?
@@ -616,33 +583,33 @@ export class EventTimer extends Timer {
// Presenter message
case 'set-timer-text':
this.presenter.text = payload;
this.broadcastThis('messages-timer', this.presenter);
this.socket.send('messages-timer', this.presenter);
break;
case 'set-timer-visible':
this.presenter.visible = payload;
this.broadcastThis('messages-timer', this.presenter);
this.socket.send('messages-timer', this.presenter);
break;
/*******************************************/
// Public message
case 'set-public-text':
this.public.text = payload;
this.broadcastThis('messages-public', this.public);
this.socket.send('messages-public', this.public);
break;
case 'set-public-visible':
this.public.visible = payload;
this.broadcastThis('messages-public', this.public);
this.socket.send('messages-public', this.public);
break;
/*******************************************/
// Lower third message
case 'set-lower-text':
this.lower.text = payload;
this.broadcastThis('messages-lower', this.lower);
this.socket.send('messages-lower', this.lower);
break;
case 'set-lower-visible':
this.lower.visible = payload;
this.broadcastThis('messages-lower', this.lower);
this.socket.send('messages-lower', this.lower);
break;
default:
@@ -650,286 +617,6 @@ export class EventTimer extends Timer {
}
}
/**
* Handle socket io connections
* @private
*/
_listenToConnections() {
this.io.on('connection', (socket) => {
/*******************************/
/*** HANDLE NEW CONNECTION ***/
/*** --------------------- ***/
/*******************************/
// keep track of connections
this._numClients++;
this._clientNames[socket.id] = getRandomName();
const m = `${this._numClients} Clients with new connection: ${this._clientNames[socket.id]}`;
this.info('CLIENT', m);
// send state
socket.emit('timer', this.getTimeObject());
socket.emit('playstate', this.state);
socket.emit('selected-id', this.selectedEventId);
socket.emit('next-id', this.nextEventId);
socket.emit('publicselected-id', this.selectedPublicEventId);
socket.emit('publicnext-id', this.nextPublicEventId);
/********************************/
/*** HANDLE DISCONNECT USER ***/
/*** ---------------------- ***/
/********************************/
socket.on('disconnect', () => {
this._numClients--;
const m = `${this._numClients} Clients with disconnection: ${this._clientNames[socket.id]}`;
delete this._clientNames[socket.id];
this.info('CLIENT', m);
});
/***************************************/
/*** TIMER STATE GETTERS / SETTERS ***/
/*** ------- WEBSOCKET API ------- ***/
/*** ----------------------------- ***/
/***************************************/
/*******************************************/
socket.on('ontime-test', () => {
socket.emit('hello');
});
socket.on('set-start', () => {
this.trigger('start');
socket.emit('playstate', this.state);
});
socket.on('set-startid', (data) => {
this.trigger('startById', data);
socket.emit('playstate', this.state);
});
socket.on('set-startindex', (data) => {
const eventIndex = Number(data);
if (isNaN(eventIndex)) {
return;
}
this.trigger('startByIndex', data);
socket.emit('playstate', this.state);
});
socket.on('set-loadid', (data) => {
this.trigger('loadById', data);
socket.emit('playstate', this.state);
});
socket.on('set-loadindex', (data) => {
const eventIndex = Number(data);
if (isNaN(eventIndex)) {
return;
}
this.trigger('loadByIndex', data);
socket.emit('playstate', this.state);
});
socket.on('set-pause', () => {
this.trigger('pause');
socket.emit('playstate', this.state);
});
socket.on('set-stop', () => {
this.trigger('stop');
socket.emit('playstate', this.state);
});
socket.on('set-reload', () => {
this.trigger('reload');
socket.emit('playstate', this.state);
});
socket.on('set-previous', () => {
this.trigger('previous');
socket.emit('playstate', this.state);
});
socket.on('set-next', () => {
this.trigger('next');
socket.emit('playstate', this.state);
});
socket.on('set-roll', () => {
this.trigger('roll');
socket.emit('playstate', this.state);
});
socket.on('set-delay', (data) => {
const delayTime = Number(data);
if (isNaN(delayTime)) {
return;
}
this.increment(delayTime * 1000 * 60);
});
socket.on('set-onAir', (data) => {
try {
const d = JSON.parse(data);
this.onAir = !!d;
} catch (error) {
this.error('RX', `Failed to parse message ${data}`);
}
this.broadcastThis('onAir', this.onAir);
this._broadcastFeatureMessageControl();
});
/*******************************************/
// general playback state
socket.on('get-state', () => {
socket.emit('timer', this.getTimeObject());
socket.emit('playstate', this.state);
socket.emit('selected-id', this.selectedEventId);
socket.emit('next-id', this.nextEventId);
socket.emit('publicselected-id', this.selectedPublicEventId);
socket.emit('publicnext-id', this.this.nextPublicEventId);
});
/*******************************************/
// ** TO BE DEPRECATED ** //
socket.on('get-timer', () => {
socket.emit('timer', this.getTimeObject());
});
// ** TO BE DEPRECATED IN FAVOR OF DELAY ** //
socket.on('increment-timer', (data) => {
if (isNaN(parseInt(data, 10))) return;
if (data < -5 || data > 5) return;
this.increment(data * 1000 * 60);
});
/*******************************************/
// playstate
socket.on('set-playstate', (data) => {
this.trigger(data);
this._broadcastFeaturePlaybackControl();
this._broadcastFeatureInfo();
});
socket.on('get-playstate', () => {
socket.emit('playstate', this.state);
});
socket.on('get-onAir', () => {
socket.emit('onAir', this.onAir);
});
/*******************************************/
// selection data
socket.on('get-selected', () => {
socket.emit('selected', {
id: this.selectedEventId,
index: this.selectedEventIndex,
total: this._eventlist.length,
});
});
socket.on('get-selected-id', () => {
socket.emit('selected-id', this.selectedEventId);
});
socket.on('get-next-id', () => {
socket.emit('next-id', this.nextEventId);
});
// title data
socket.on('get-titles', () => {
socket.emit('titles', this.titles);
});
socket.on('get-publictitles', () => {
socket.emit('publictitles', this.titlesPublic);
});
/***********************************/
/*** MESSAGE GETTERS / SETTERS ***/
/*** ------------------------- ***/
/***********************************/
/*******************************************/
// Presenter message
socket.on('set-timer-message-text', (data) => {
this._setTitles('set-timer-text', data);
this._broadcastFeatureMessageControl();
});
socket.on('set-timer-message-visible', (data) => {
this._setTitles('set-timer-visible', data);
this._broadcastFeatureMessageControl();
});
/*******************************************/
// Public message
socket.on('set-public-message-text', (data) => {
this._setTitles('set-public-text', data);
this._broadcastFeatureMessageControl();
});
socket.on('set-public-message-visible', (data) => {
this._setTitles('set-public-visible', data);
this._broadcastFeatureMessageControl();
});
/*******************************************/
// Lower third message
socket.on('set-lower-message-text', (data) => {
this._setTitles('set-lower-text', data);
this._broadcastFeatureMessageControl();
});
socket.on('set-lower-message-visible', (data) => {
this._setTitles('set-lower-visible', data);
this._broadcastFeatureMessageControl();
});
/* MOLECULAR ENDPOINTS
* =====================
* 1. EVENT LIST
* 2. MESSAGE CONTROL
* 3. PLAYBACK CONTROL
* 4. INFO
* 5. CUESHEET
* 6. TIMER OBJECT
* */
// 1. EVENT LIST
socket.on('get-ontime-feat-eventlist', () => {
this._broadcastFeatureEventList();
});
// 2. MESSAGE CONTROL
socket.on('get-ontime-feat-messagecontrol', () => {
this._broadcastFeatureMessageControl();
});
// 3. PLAYBACK CONTROL
socket.on('get-ontime-feat-playbackcontrol', () => {
this._broadcastFeaturePlaybackControl();
});
// 4. INFO
socket.on('get-ontime-feat-info', () => {
this._broadcastFeatureInfo();
});
// 5. CUE SHEET
socket.on('get-ontime-feat-cuesheet', () => {
this._broadcastFeatureCuesheet();
});
// 6. TIMER
socket.on('get-ontime-timer', () => {
this._broadcastFeatureTimer();
});
});
}
/**
* Deletes running event list from object
*/
@@ -944,7 +631,7 @@ export class EventTimer extends Timer {
this.ontimeCycle = this.cycleState.onStop;
// update clients
this.broadcastThis('numevents', this._eventlist.length);
this.socket.send('numevents', this._eventlist.length);
}
/**
@@ -1030,7 +717,7 @@ export class EventTimer extends Timer {
// find object in events
const eventIndex = this._eventlist.findIndex((e) => e.id === id);
if (eventIndex === -1) {
throw 'Event not found';
throw new Error('Event not found');
}
// check if event is set to be skipped
@@ -1068,7 +755,7 @@ export class EventTimer extends Timer {
this._loadTitlesNow();
}
} catch (error) {
this.error('SERVER', error);
this.socket.error('SERVER', error);
}
// update clients
@@ -1117,7 +804,7 @@ export class EventTimer extends Timer {
this._loadTitlesNow();
}
} catch (error) {
this.error('SERVER', error);
this.socket.error('SERVER', error);
}
// update clients
@@ -1228,7 +915,7 @@ export class EventTimer extends Timer {
this.selectedEventIndex = eventIndex;
this.selectedEventId = e.id;
} else if (type === 'reload') {
const now = this._getCurrentTime();
const now = Timer.getCurrentTime();
const elapsed = this.getElapsed();
this.duration = end - start;
@@ -1441,7 +1128,7 @@ export class EventTimer extends Timer {
setonAir(onAir) {
this.onAir = onAir;
// broadcast change
this.broadcastThis('onAir', onAir);
this.socket.send('onAir', onAir);
}
/**
@@ -1509,7 +1196,7 @@ export class EventTimer extends Timer {
* @description Look for current event considering local clock
*/
rollLoad() {
const now = this._getCurrentTime();
const now = Timer.getCurrentTime();
const prevLoaded = this.selectedEventId;
// maybe roll has already been loaded
@@ -1524,7 +1211,7 @@ export class EventTimer extends Timer {
// nothing to play, unload
if (nowIndex === null && nextIndex === null) {
this.unload();
this.warning('SERVER', 'Roll: no events found');
this.socket.warning('SERVER', 'Roll: no events found');
return;
}
@@ -1551,7 +1238,7 @@ export class EventTimer extends Timer {
if (nowIndex === null) {
// only warn the first time
if (this.secondaryTimer === null) {
this.info('SERVER', 'Roll: waiting for event start');
this.socket.info('SERVER', 'Roll: waiting for event start');
}
// reset running timer
@@ -1685,70 +1372,6 @@ export class EventTimer extends Timer {
/****************************************************************************/
/**
* Logger logic
* -------------
*
* This should be separate of event timer, left here for convenience
*
*/
/**
* Utility method, sends message and pushes into stack
* @param {string} level
* @param {string} origin
* @param {string} text
*/
_push(level, origin, text) {
const m = {
id: generateId(),
level,
origin,
text,
time: stringFromMillis(this._getCurrentTime()),
};
this.messageStack.unshift(m);
this.io.emit('logger', m);
if (process.env.NODE_ENV !== 'production') {
console.log(`[${m.level}] \t ${m.origin} \t ${m.text}`);
}
if (this.messageStack.length > this.MAX_MESSAGES) {
this.messageStack.pop();
}
}
/**
* Sends a message with level LOG
* @param {string} origin
* @param {string} text
*/
info(origin, text) {
this._push('INFO', origin, text);
}
/**
* Sends a message with level WARN
* @param {string} origin
* @param {string} text
*/
warning(origin, text) {
this._push('WARN', origin, text);
}
/**
* Sends a message with level ERROR
* @param {string} origin
* @param {string} text
*/
error(origin, text) {
this._push('ERROR', origin, text);
}
/****************************************************************************/
/**
* Integrations
* -------------
@@ -1765,20 +1388,20 @@ export class EventTimer extends Timer {
async sendOsc(message, payload) {
const reply = await this.osc.send(message, payload);
if (!reply.success) {
this.error('TX', reply.message);
this.socket.error('TX', reply.message);
}
}
/**
* Builds sync object
* @returns {{running: number, timer: (null|string|*), presenter: null, playback: string, clock: null, title: null}}
* @description Builds sync object
*/
poll() {
return {
clock: this.clock,
running: Timer.toSeconds(this.current),
currentId: this.selectedEventId,
timer: this.timeTag,
clock: this.clock,
playback: this.state,
currentColour: null,
title: this.titles.titleNow,
presenter: this.titles.presenterNow,
};
+5 -42
View File
@@ -11,39 +11,12 @@ export class Timer {
this.state = 'stop';
}
/**
* @description initiates a timer with given seconds
* @param seconds
* @param autoStart
*/
setupWithSeconds(seconds, autoStart = false) {
// aux
const now = this._getCurrentTime();
this.clock = now;
// populate targets
this.duration = seconds * 1000;
this._finishAt = now + seconds * 1000;
// start counting
this._startedAt = now;
if (autoStart) {
this.state = 'start';
} else {
this._pausedAt = now;
this._pausedInterval = 0;
}
this._pausedTotal = 0;
this.update();
}
/**
* @description updates the running timer
*/
update() {
// get current time
const now = this._getCurrentTime();
const now = Timer.getCurrentTime();
this.clock = now;
let checkFinish = false;
@@ -106,7 +79,7 @@ export class Timer {
* @return {number}
* @private
*/
_getCurrentTime() {
static getCurrentTime() {
const now = new Date();
// extract milliseconds since midnight
@@ -178,16 +151,6 @@ export class Timer {
};
}
/**
* @description get current time in seconds
* @return {number|number}
*/
getCurrentInSeconds() {
// update timeStamp
this.update();
return Timer.toSeconds(this.current);
}
// playback
/**
* @description start current time
@@ -197,7 +160,7 @@ export class Timer {
if (this.state === 'start') return;
else if (this._startedAt == null) {
// it hasn't started yet
const now = this._getCurrentTime();
const now = Timer.getCurrentTime();
// set start time as now
this._startedAt = now;
// calculate expected finish time
@@ -230,7 +193,7 @@ export class Timer {
}
// set pause time
this._pausedAt = this._getCurrentTime();
this._pausedAt = Timer.getCurrentTime();
// change state
this.state = 'pause';
@@ -259,7 +222,7 @@ export class Timer {
if (amount < 0 && Math.abs(amount) > this.current) {
// if we will make the clock negative
if (this._finishedAt == null) this._finishedAt = this._getCurrentTime();
if (this._finishedAt == null) this._finishedAt = Timer.getCurrentTime();
} else if (this.current < 0 && this.current + amount > 0) {
// clock will go from negative to positive
this._finishedAt = null;
+15 -11
View File
@@ -1,24 +1,28 @@
// get database
import { db, data } from '../app.js';
import { removeUndefined } from '../utils/parserUtils.js';
import { failEmptyObjects } from '../utils/routerUtils.js';
import { DataProvider } from '../classes/data-provider/DataProvider.js';
// Create controller for GET request to 'event'
// Returns ACK message
export const getEvent = async (req, res) => {
res.json(data.event);
res.json(DataProvider.getEventData());
};
// Create controller for POST request to 'event'
// Returns ACK message
export const postEvent = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
if (failEmptyObjects(req.body, res)) {
return;
}
// TODO: validate data
try {
data.event = { ...data.event, ...req.body };
await db.write();
res.sendStatus(200);
const newEvent = removeUndefined({
title: req.body?.title,
url: req.body?.url,
publicInfo: req.body?.publicInfo,
backstageInfo: req.body?.backstageInfo,
endMessage: req.body?.endMessage,
});
const newData = await DataProvider.setEventData(newEvent);
res.status(200).send(newData);
} catch (error) {
res.status(400).send(error);
}
@@ -0,0 +1,14 @@
import { body, validationResult } from 'express-validator';
export const eventSanitizer = [
body('title').optional().isString().trim().escape(),
body('url').optional().isString().trim().escape(),
body('publicInfo').optional().isString().trim().escape(),
body('backstageInfo').optional().isString().trim().escape(),
body('endMessage').optional().isString().trim().escape(),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];
+44 -128
View File
@@ -1,7 +1,3 @@
// get database
import { data, db } from '../app.js';
// utils
import {
block as blockDef,
delay as delayDef,
@@ -10,87 +6,36 @@ import {
import { generateId } from '../utils/generate_id.js';
import { MAX_EVENTS } from '../settings.js';
import { getPreviousPlayable } from '../utils/eventUtils.js';
import { DataProvider } from '../classes/data-provider/DataProvider.js';
import { failEmptyObjects } from '../utils/routerUtils.js';
import { socketProvider } from '../classes/socket/SocketController.js';
// import socket provider
const socket = socketProvider;
async function _insertAndSync(newEvent) {
if (newEvent.order) {
const events = data.events;
await _insertAt(newEvent, newEvent.order);
const events = DataProvider.getEvents();
await DataProvider.insertEventAt(newEvent, newEvent.order);
const previousId = events?.[newEvent.order - 1]?.id;
_insertEventInTimerAfterId(newEvent, previousId);
} else if (newEvent.after) {
await _insertAfterId(newEvent, newEvent.after);
await DataProvider.insertEventAfterId(newEvent, newEvent.after);
_insertEventInTimerAfterId(newEvent, newEvent.after);
} else {
await _insertAt(newEvent, 0);
await DataProvider.insertEventAt(newEvent, 0);
_insertEventInTimerAfterId(newEvent);
}
}
/**
* Insets an event after a given index
* @param entry
* @param index
* @return {Promise<void>}
* @private
*/
async function _insertAt(entry, index) {
// get events
const events = data.events;
const count = events.length;
const order = entry.order;
// Remove order field from object
delete entry.order;
// Insert at beginning
if (order === 0) {
events.unshift(entry);
}
// insert at end
else if (order >= count) {
events.push(entry);
}
// insert in the middle
else {
events.splice(index, 0, entry);
}
// save events
data.events = events;
await db.write();
}
/**
* @description Inserts an entry after an element with given Id
* @param entry
* @param id
* @return {Promise<void>}
* @private
*/
async function _insertAfterId(entry, id) {
const index = [...data.events].findIndex((event) => event.id === id);
await _insertAt(entry, index + 1);
}
/**
* @description deletes an event from the db given its id
* @param eventId
* @return {Promise<void>}
*/
async function _removeById(eventId) {
data.events = Array.from(data.events).filter((e) => e.id !== eventId);
await db.write();
}
/**
* @description returns all events of type event
* @return {unknown[]}
*/
function getEventEvents() {
// return data.events.filter((e) => e.type === 'event');
return Array.from(data.events).filter((e) => e.type === 'event');
const events = DataProvider.getEvents();
return Array.from(events).filter((e) => e.type === 'event');
}
// Updates timer object
@@ -112,7 +57,7 @@ function _insertEventInTimerAfterId(event, previousId) {
try {
global.timer.insertEventAfterId(event, previousId);
} catch (error) {
global.timer.error('SERVER', `Unable to update object: ${error}`);
socket.error('SERVER', `Unable to update object: ${error}`);
}
}
}
@@ -135,7 +80,7 @@ function _deleteTimerId(entryId) {
// Create controller for GET request to '/events'
// Returns -
export const eventsGetAll = async (req, res) => {
res.json(data.events);
res.json(DataProvider.getEvents());
};
// Create controller for GET request to '/events/:eventId'
@@ -146,7 +91,7 @@ export const eventsGetById = async (req, res) => {
if (id == null) {
res.status(400).send(`No eventId found in request`);
} else {
const event = data.events.find((e) => e.id === id);
const event = DataProvider.getEventById(id);
res.json(event);
}
};
@@ -154,13 +99,12 @@ export const eventsGetById = async (req, res) => {
// Create controller for POST request to '/events/'
// Returns -
export const eventsPost = async (req, res) => {
// TODO: Validate event
if (!req.body) {
res.status(400).send(`No object found in request`);
if (failEmptyObjects(req.body, res)) {
return;
}
if (data.events.length > MAX_EVENTS) {
const numEvents = DataProvider.getNumEvents();
if (numEvents > MAX_EVENTS) {
const error = `ERROR: Reached limit number of ${MAX_EVENTS} events`;
res.status(400).send(error);
return;
@@ -187,7 +131,7 @@ export const eventsPost = async (req, res) => {
}
try {
_insertAndSync(newEvent);
await _insertAndSync(newEvent);
res.sendStatus(201);
} catch (error) {
res.status(400).send(error);
@@ -197,41 +141,33 @@ export const eventsPost = async (req, res) => {
// Create controller for PUT request to '/events/'
// Returns -
export const eventsPut = async (req, res) => {
// no valid params
if (!req.body) {
res.status(400).send(`No object found`);
if (failEmptyObjects(req.body, res)) {
return;
}
const eventId = req.body.id;
if (!eventId) {
res.status(400).send(`Object malformed: id missing`);
return;
}
const eventIndex = data.events.findIndex((e) => e.id === eventId);
if (eventIndex === -1) {
res.status(400).send(`No Id found found`);
const eventDataFromRequest = req.body;
const eventId = eventDataFromRequest.id;
const event = DataProvider.getEventById(eventId);
if (typeof event === 'undefined') {
res.status(400).send(`No event with ID found`);
return;
}
try {
const e = data.events[eventIndex];
data.events[eventIndex] = { ...e, ...req.body };
data.events[eventIndex].revision++;
await db.write();
const newData = DataProvider.updateEventById(eventId, eventDataFromRequest);
if (data.events[eventIndex].skip) {
if (newData.skip) {
_deleteTimerId(eventId);
// if it is a skip, i make sure it is deleted from timer
// if it is a skip, make sure it is deleted from timer
// event id might already not exist
} else {
try {
_updateTimersSingle(eventId, req.body);
_updateTimersSingle(newData.id, eventDataFromRequest);
} catch (error) {
if (error === 'Event not found') {
const { id: previousId } = getPreviousPlayable(data.events, e.id);
_insertEventInTimerAfterId(data.events[eventIndex], previousId);
const events = DataProvider.getEvents();
const { id: previousId } = getPreviousPlayable(events, newData.id);
_insertEventInTimerAfterId(newData, previousId);
}
}
}
@@ -250,16 +186,14 @@ export const eventsPatch = async (req, res) => {
};
export const eventsReorder = async (req, res) => {
// TODO: Validate event
if (!req.body) {
res.status(400).send(`No object found in request`);
if (failEmptyObjects(req.body, res)) {
return;
}
const { index, from, to } = req.body;
// get events
const events = data.events;
const events = DataProvider.getEvents();
const idx = events.findIndex((e) => e.id === index, from);
// Check if item is at given index
@@ -276,8 +210,7 @@ export const eventsReorder = async (req, res) => {
events.splice(to, 0, reorderedItem);
// save events
data.events = events;
await db.write();
await DataProvider.setEventData(events);
// update timer
_updateTimers();
@@ -291,15 +224,9 @@ export const eventsReorder = async (req, res) => {
// Create controller for PATCH request to '/events/applydelay/:eventId'
// Returns -
export const eventsApplyDelay = async (req, res) => {
// no valid params
if (!req.params.eventId) {
res.status(400).send(`No id found in request`);
return;
}
try {
// get events
const events = data.events;
const events = DataProvider.getEvents();
// AUX
let delayIndex = null;
@@ -340,8 +267,7 @@ export const eventsApplyDelay = async (req, res) => {
if (blockIndex) events.splice(blockIndex - 1, 1);
// update events
data.events = events;
await db.write();
await DataProvider.setEvents(events);
// update timer
_updateTimers();
@@ -355,20 +281,15 @@ export const eventsApplyDelay = async (req, res) => {
// Create controller for DELETE request to '/events/:eventId'
// Returns -
export const eventsDelete = async (req, res) => {
// no valid params
if (!req.params.eventId) {
res.status(400).send(`No id found in request`);
return;
}
try {
const eventId = req.params.eventId;
// remove new event
await _removeById(req.params.eventId);
await DataProvider.deleteEvent(eventId);
// update timer
_deleteTimerId(req.params.eventId);
_deleteTimerId(eventId);
res.sendStatus(201);
res.sendStatus(204);
} catch (error) {
res.status(400).send(error);
}
@@ -378,14 +299,9 @@ export const eventsDelete = async (req, res) => {
// Returns -
export const eventsDeleteAll = async (req, res) => {
try {
// set with nothing
data.events = [];
await db.write();
// update timer object
await DataProvider.deleteAllEvents();
global.timer.clearEventList();
res.sendStatus(201);
res.sendStatus(204);
} catch (error) {
res.status(400).send(error);
}
@@ -0,0 +1,19 @@
import { body, param, validationResult } from 'express-validator';
export const eventsPutValidator = [
body('id').isString().exists(),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];
export const paramsMustHaveEventId = [
param('eventId').exists(),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];
+50 -94
View File
@@ -1,10 +1,11 @@
import fs from 'fs';
import { data, db } from '../app.js';
import { networkInterfaces } from 'os';
import { fileHandler } from '../utils/parser.js';
import { generateId } from '../utils/generate_id.js';
import { resolveDbPath } from '../modules/loadDb.js';
import { DataProvider } from '../classes/data-provider/DataProvider.js';
import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
import { mergeObject } from '../utils/parserUtils.js';
// Create controller for GET request to '/ontime/poll'
// Returns data for current state
@@ -22,7 +23,8 @@ export const poll = async (req, res) => {
// Create controller for GET request to '/ontime/db'
// Returns -
export const dbDownload = async (req, res) => {
const fileTitle = data?.event?.title || 'ontime events';
const { title } = DataProvider.getEventData();
const fileTitle = title || 'ontime events';
const dbInDisk = resolveDbPath();
res.download(dbInDisk, `${fileTitle}.json`, (err) => {
@@ -48,18 +50,13 @@ const uploadAndParse = async (file, req, res, options) => {
} else if (result.message === 'success') {
// explicitly write objects
if (typeof result !== 'undefined') {
if (!options.onlyEvents) {
const mergedData = DataProvider.safeMerge(data, result.data);
data.event = mergedData.event;
data.settings = mergedData.settings;
data.osc = mergedData.osc;
data.http = mergedData.http;
data.aliases = mergedData.aliases;
data.userFields = mergedData.userFields;
const newEvents = result.data.events || [];
if (options.onlyEvents) {
await DataProvider.setEvents(newEvents);
} else {
await DataProvider.mergeIntoData(result.data);
}
data.events = result.data.events || [];
global.timer.setupWithEventList(result.data.events || []);
await db.write();
global.timer.setupWithEventList(newEvents);
}
res.sendStatus(200);
} else {
@@ -71,7 +68,7 @@ const uploadAndParse = async (file, req, res, options) => {
};
/**
* @description Gets information on IPV4 non internal interfaces
* @description Gets information on IPV4 non-internal interfaces
* @returns {array} - Array of objects {name: ip}
*/
const getNetworkInterfaces = () => {
@@ -96,15 +93,8 @@ const getNetworkInterfaces = () => {
// Create controller for POST request to '/ontime/info'
// Returns -
export const getInfo = async (req, res) => {
const version = data.settings.version;
const serverPort = data.settings.serverPort;
const osc = {
port: data.osc.port,
portOut: data.osc.portOut,
targetIP: data.osc.targetIP,
enabled: data.osc.enabled,
};
const { version, serverPort } = DataProvider.getSettings();
const osc = DataProvider.getOsc();
// get nif and inject localhost
const ni = getNetworkInterfaces();
@@ -122,18 +112,16 @@ export const getInfo = async (req, res) => {
// Create controller for POST request to '/ontime/aliases'
// Returns -
export const getAliases = async (req, res) => {
// send aliases array
res.status(200).send(data.aliases);
const aliases = DataProvider.getAliases();
res.status(200).send(aliases);
};
// Create controller for POST request to '/ontime/aliases'
// Returns ACK message
export const postAliases = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
if (failIsNotArray()) {
return;
}
// TODO: validate data
try {
const newAliases = [];
req.body.forEach((a) => {
@@ -144,9 +132,8 @@ export const postAliases = async (req, res) => {
pathAndParams: a.pathAndParams,
});
});
data.aliases = newAliases;
await db.write();
res.sendStatus(200);
await DataProvider.setAliases(newAliases);
res.status(200).send(newAliases);
} catch (error) {
res.status(400).send(error);
}
@@ -155,27 +142,21 @@ export const postAliases = async (req, res) => {
// Create controller for GET request to '/ontime/userfields'
// Returns -
export const getUserFields = async (req, res) => {
// send userFields array
res.status(200).send(data.userFields);
const userFields = DataProvider.getUserFields();
res.status(200).send(userFields);
};
// Create controller for POST request to '/ontime/userfields'
// Returns ACK message
export const postUserFields = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
if (failEmptyObjects(req.body, res)) {
return;
}
try {
const newUserFields = { ...data.userFields };
for (const field in newUserFields) {
if (typeof req.body[field] !== 'undefined') {
newUserFields[field] = req.body[field];
}
}
data.userFields = newUserFields;
await db.write();
res.sendStatus(200);
const persistedData = DataProvider.getUserFields();
const newData = mergeObject(persistedData, req.body);
await DataProvider.setUserFields(newData);
res.status(200).send(newData);
} catch (error) {
res.status(400).send(error);
}
@@ -184,12 +165,8 @@ export const postUserFields = async (req, res) => {
// Create controller for POST request to '/ontime/settings'
// Returns -
export const getSettings = async (req, res) => {
const version = data.settings.version;
const serverPort = data.settings.serverPort;
const pinCode = data.settings.pinCode;
const timeFormat = data.settings.timeFormat;
const { version, serverPort, pinCode, timeFormat } = DataProvider.getSettings();
// send object with network information
res.status(200).send({
version,
serverPort,
@@ -201,12 +178,12 @@ export const getSettings = async (req, res) => {
// Create controller for POST request to '/ontime/settings'
// Returns ACK message
export const postSettings = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
if (failEmptyObjects(req.body, res)) {
return;
}
try {
let pin = data.settings.pinCode;
const settings = DataProvider.getSettings();
let pin = settings.pinCode;
if (typeof req.body?.pinCode === 'string') {
if (req.body?.pinCode.length === 0) {
pin = null;
@@ -215,20 +192,20 @@ export const postSettings = async (req, res) => {
}
}
let timeFormat = data.settings.timeFormat;
let format = settings.timeFormat;
if (typeof req.body?.timeFormat === 'string') {
if (req.body?.timeFormat === '12' || req.body?.timeFormat === '24') {
timeFormat = req.body.timeFormat;
format = req.body.timeFormat;
}
}
data.settings = {
...data.settings,
const newData = {
...settings,
pinCode: pin,
timeFormat: timeFormat,
timeFormat: format,
};
await db.write();
res.sendStatus(200);
DataProvider.setSettings(newData);
res.status(200).send(newData);
} catch (error) {
res.status(400).send(error);
}
@@ -239,7 +216,8 @@ export const postSettings = async (req, res) => {
* @method GET
*/
export const getViewSettings = async (req, res) => {
res.status(200).send({ ...data.views });
const views = DataProvider.getViews();
res.status(200).send(views);
};
/**
@@ -247,33 +225,14 @@ export const getViewSettings = async (req, res) => {
* @method POST
*/
export const postViewSettings = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
if (failEmptyObjects(req.body, res)) {
return;
}
try {
data.views = {
overrideStyles: req.body?.overrideStyles ?? data.views.overrideStyles,
};
await db.write();
res.sendStatus(200);
} catch (error) {
res.status(400).send(error);
}
};
// Create controller for POST request to '/ontime/info'
// Returns ACK message
export const postInfo = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
return;
}
// TODO: validate data
try {
data.settings = { ...data.settings, ...req.body };
await db.write();
res.sendStatus(200);
const newData = { overrideStyles: req.body.overrideStyles };
DataProvider.setViews(newData);
res.status(200).send(newData);
} catch (error) {
res.status(400).send(error);
}
@@ -282,25 +241,22 @@ export const postInfo = async (req, res) => {
// Create controller for POST request to '/ontime/osc'
// Returns -
export const getOSC = async (req, res) => {
// send object with network information
res.status(200).send(data.osc);
const osc = DataProvider.getOsc();
res.status(200).send(osc);
};
// Create controller for POST request to '/ontime/osc'
// Returns ACK message
export const postOSC = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
if (failEmptyObjects(req.body, res)) {
return;
}
// TODO: validate data
try {
data.osc = { ...data.osc, ...req.body };
await db.write();
res.sendStatus(200);
await DataProvider.setOsc(req.body);
res.send(req.body).status(200);
} catch (error) {
res.status(400).send(error);
console.log(error);
}
};
@@ -313,7 +269,7 @@ export const dbUpload = async (req, res) => {
}
const options = req.query;
const file = req.file.path;
uploadAndParse(file, req, res, options);
await uploadAndParse(file, req, res, options);
};
// Create controller for POST request to '/ontime/dbpath'
@@ -323,5 +279,5 @@ export const dbPathToUpload = async (req, res) => {
res.status(400).send({ message: 'Path to file not found' });
return;
}
uploadAndParse(req.body.path, req, res);
await uploadAndParse(req.body.path, req, res);
};
@@ -0,0 +1,77 @@
import { body, check, validationResult } from 'express-validator';
/**
* @description Validates object for POST /ontime/views
*/
export const viewValidator = [
check('overrideStyles').isBoolean().withMessage('overrideStyles value must be boolean'),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];
/**
* @description Validates object for POST /ontime/aliases
*/
export const validateAliases = [
body().isArray(),
body('*.enabled').isBoolean(),
body('*.alias').isString().trim(),
body('*.pathAndParams').isString().trim(),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];
/**
* @description Validates object for POST /ontime/userfields
*/
export const validateUserFields = [
body('user0').exists().isString().trim(),
body('user1').exists().isString().trim(),
body('user2').exists().isString().trim(),
body('user3').exists().isString().trim(),
body('user4').exists().isString().trim(),
body('user5').exists().isString().trim(),
body('user6').exists().isString().trim(),
body('user7').exists().isString().trim(),
body('user8').exists().isString().trim(),
body('user9').exists().isString().trim(),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];
/**
* @description Validates object for POST /ontime/settings
*/
export const validateSettings = [
body('pinCode').isString().isLength({ min: 0, max: 4 }).optional({ nullable: true }),
body('timeFormat').isString().isIn(['12', '24']),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];
/**
* @description Validates object for POST /ontime/osc
*/
export const validateOSC = [
body('port').exists().isInt({ min: 0, max: 65353 }),
body('portOut').exists().isInt({ min: 0, max: 65353 }),
body('targetIP').exists().isIP(),
body('enabled').exists().isBoolean(),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];
+2 -1
View File
@@ -3,9 +3,10 @@ export const router = express.Router();
// import event controller
import { getEvent, postEvent } from '../controllers/eventController.js';
import { eventSanitizer } from '../controllers/eventController.validate.js';
// create route between controller and 'GET /event' endpoint
router.get('/', getEvent);
// create route between controller and 'POST /event' endpoint
router.post('/', postEvent);
router.post('/', eventSanitizer, postEvent);
+7 -3
View File
@@ -13,6 +13,10 @@ import {
eventsDeleteAll,
eventsDelete,
} from '../controllers/eventsController.js';
import {
eventsPutValidator,
paramsMustHaveEventId,
} from '../controllers/eventsController.validate.js';
// create route between controller and '/events/' endpoint
router.get('/', eventsGetAll);
@@ -24,7 +28,7 @@ router.get('/:eventId', eventsGetById);
router.post('/', eventsPost);
// create route between controller and '/events/' endpoint
router.put('/', eventsPut);
router.put('/', eventsPutValidator, eventsPut);
// create route between controller and '/events/' endpoint
// DEPRECATED
@@ -34,10 +38,10 @@ router.patch('/', eventsPatch);
router.patch('/reorder/', eventsReorder);
// create route between controller and '/events/applydelay/:eventId' endpoint
router.patch('/applydelay/:eventId', eventsApplyDelay);
router.patch('/applydelay/:eventId', paramsMustHaveEventId, eventsApplyDelay);
// create route between controller and '/events/all' endpoint
router.delete('/all', eventsDeleteAll);
// create route between controller and '/events/:eventId' endpoint
router.delete('/:eventId', eventsDelete);
router.delete('/:eventId', paramsMustHaveEventId, eventsDelete);
+11 -9
View File
@@ -12,14 +12,19 @@ import {
getViewSettings,
poll,
postAliases,
postInfo,
postOSC,
postSettings,
postUserFields,
postViewSettings,
} from '../controllers/ontimeController.js';
import { viewValidator } from '../validation/ontimeValidator.js';
import {
viewValidator,
validateAliases,
validateUserFields,
validateSettings,
validateOSC,
} from '../controllers/ontimeController.validate.js';
export const router = express.Router();
@@ -36,7 +41,7 @@ router.post('/db', uploadFile, dbUpload);
router.get('/settings', getSettings);
// create route between controller and '/ontime/settings' endpoint
router.post('/settings', postSettings);
router.post('/settings', validateSettings, postSettings);
// create route between controller and '/ontime/views' endpoint
router.get('/views', getViewSettings);
@@ -48,25 +53,22 @@ router.post('/views', viewValidator, postViewSettings);
router.get('/aliases', getAliases);
// create route between controller and '/ontime/aliases' endpoint
router.post('/aliases', postAliases);
router.post('/aliases', validateAliases, postAliases);
// create route between controller and '/ontime/aliases' endpoint
router.get('/userfields', getUserFields);
// create route between controller and '/ontime/aliases' endpoint
router.post('/userfields', postUserFields);
router.post('/userfields', validateUserFields, postUserFields);
// create route between controller and '/ontime/info' endpoint
router.get('/info', getInfo);
// create route between controller and '/ontime/info' endpoint
router.post('/info', postInfo);
// create route between controller and '/ontime/osc' endpoint
router.get('/osc', getOSC);
// create route between controller and '/ontime/osc' endpoint
router.post('/osc', postOSC);
router.post('/osc', validateOSC, postOSC);
// create route between controller and '/ontime/dbpath' endpoint
router.post('/dbpath', dbPathToUpload);
@@ -0,0 +1,92 @@
import { isEmptyObject, mergeObject, removeUndefined } from '../parserUtils.js';
describe('isEmptyObject()', () => {
test('finds an empty object', () => {
const isEmpty = isEmptyObject({});
expect(isEmpty).toBe(true);
});
test('throws on other types', () => {
expect(() => isEmptyObject(12)).toThrow();
});
test('resolves an object with methods', () => {
const isEmpty = isEmptyObject({ test: 'yes' });
expect(isEmpty).toBe(false);
});
});
describe('mergeObject()', () => {
test('it suppresses undefined keys', () => {
const a = {
first: 'yes',
second: 'yes',
};
const b = {
first: undefined,
second: 'no',
};
const merged = mergeObject(a, b);
expect(merged).toStrictEqual({
first: 'yes',
second: 'no',
});
});
test('it handles falsy values', () => {
const a = {
first: 'yes',
second: 'yes',
third: 'yes',
};
const b = {
first: 0,
second: null,
third: '',
};
const merged = mergeObject(a, b);
expect(merged).toStrictEqual({
first: 0,
second: null,
third: '',
});
});
test.skip('it only merges fields of the first object', () => {
const a = {
first: 'yes',
second: 'yes',
third: 'yes',
};
const b = {
first: 0,
second: null,
third: '',
forth: 'not-this',
};
const merged = mergeObject(a, b);
expect(merged).toStrictEqual({
first: 0,
second: null,
third: '',
});
});
});
describe('removeUndefined()', () => {
test('it removes undefined keys from object', () => {
const obj = {
first: 'yes',
second: undefined,
third: 'yes',
};
expect(removeUndefined(obj)).toStrictEqual({
first: 'yes',
third: 'yes',
});
});
test('it handles falsy values object', () => {
const obj = {
first: '',
second: 0,
third: 'null',
};
expect(removeUndefined(obj)).toStrictEqual(obj);
});
});
+3 -96
View File
@@ -1,19 +1,13 @@
const adjective = [
'abandoned',
'able',
'absolute',
'adorable',
'adventurous',
'academic',
'acceptable',
'acclaimed',
'accomplished',
'accurate',
'aching',
'acidic',
'acrobatic',
'active',
'actual',
'adaptable',
'adorable',
'adventurous',
'adept',
'admirable',
'admired',
@@ -23,16 +17,11 @@ const adjective = [
'advanced',
'afraid',
'affectionate',
'aged',
'aggravating',
'aggressive',
'agile',
'agitated',
'agonizing',
'agreeable',
'ajar',
'alarmed',
'alarming',
'alert',
'alienated',
'alive',
@@ -52,13 +41,11 @@ const adjective = [
'annual',
'another',
'antique',
'anxious',
'any',
'apprehensive',
'appropriate',
'apt',
'arctic',
'arid',
'aromatic',
'artistic',
'ashamed',
@@ -73,18 +60,12 @@ const adjective = [
'authorized',
'automatic',
'avaricious',
'average',
'aware',
'awesome',
'awful',
'awkward',
'babyish',
'bad',
'back',
'baggy',
'bare',
'barren',
'basic',
'beautiful',
'belated',
'beloved',
@@ -112,7 +93,6 @@ const adjective = [
'boiling',
'bold',
'bony',
'boring',
'bossy',
'both',
'bouncy',
@@ -127,12 +107,10 @@ const adjective = [
'broken',
'bronze',
'brown',
'bruised',
'bubbly',
'bulky',
'bumpy',
'buoyant',
'burdensome',
'burly',
'bustling',
'busy',
@@ -195,7 +173,6 @@ const adjective = [
'cooperative',
'coordinated',
'corny',
'corrupt',
'costly',
'courageous',
'courteous',
@@ -203,8 +180,6 @@ const adjective = [
'crazy',
'creamy',
'creative',
'creepy',
'criminal',
'crisp',
'critical',
'crooked',
@@ -219,7 +194,6 @@ const adjective = [
'curvy',
'cute',
'cylindrical',
'damaged',
'damp',
'dangerous',
'dapper',
@@ -236,10 +210,8 @@ const adjective = [
'decimal',
'decisive',
'deep',
'defenseless',
'defensive',
'defiant',
'deficient',
'definite',
'definitive',
'delayed',
@@ -249,7 +221,6 @@ const adjective = [
'delirious',
'demanding',
'dense',
'dental',
'dependable',
'dependent',
'descriptive',
@@ -258,29 +229,19 @@ const adjective = [
'determined',
'devoted',
'different',
'difficult',
'digital',
'diligent',
'dim',
'dimpled',
'dimwitted',
'direct',
'disastrous',
'discrete',
'disfigured',
'disgusting',
'disloyal',
'dismal',
'distant',
'downright',
'dreary',
'dirty',
'disguised',
'dishonest',
'dismal',
'distant',
'distinct',
'distorted',
'dizzy',
'dopey',
'doting',
@@ -293,7 +254,6 @@ const adjective = [
'droopy',
'dry',
'dual',
'dull',
'dutiful',
'each',
'eager',
@@ -474,20 +434,16 @@ const adjective = [
'great',
'greedy',
'green',
'gregarious',
'grim',
'grimy',
'gripping',
'grizzled',
'gross',
'grotesque',
'grouchy',
'grounded',
'growing',
'growling',
'grown',
'grubby',
'gruesome',
'grumpy',
'guilty',
'gullible',
@@ -506,7 +462,6 @@ const adjective = [
'harmonious',
'harsh',
'hasty',
'hateful',
'haunting',
'healthy',
'heartfelt',
@@ -528,16 +483,13 @@ const adjective = [
'honorable',
'honored',
'hopeful',
'horrible',
'hospitable',
'hot',
'huge',
'humble',
'humiliating',
'humming',
'humongous',
'hungry',
'hurtful',
'husky',
'icky',
'icy',
@@ -548,10 +500,7 @@ const adjective = [
'idiotic',
'idolized',
'ignorant',
'ill',
'illegal',
'ill-fated',
'ill-informed',
'illiterate',
'illustrious',
'imaginary',
@@ -566,15 +515,11 @@ const adjective = [
'imperfect',
'imperturbable',
'impish',
'impolite',
'important',
'impossible',
'impractical',
'impressionable',
'impressive',
'improbable',
'impure',
'inborn',
'incomparable',
'incompatible',
'incomplete',
@@ -592,7 +537,6 @@ const adjective = [
'innocent',
'insecure',
'insidious',
'insignificant',
'insistent',
'instructive',
'insubstantial',
@@ -639,7 +583,6 @@ const adjective = [
'known',
'kooky',
'kosher',
'lame',
'lanky',
'large',
'last',
@@ -722,9 +665,7 @@ const adjective = [
'miniature',
'minor',
'minty',
'miserable',
'miserly',
'misguided',
'misty',
'mixed',
'modern',
@@ -777,18 +718,15 @@ const adjective = [
'noteworthy',
'novel',
'noxious',
'numb',
'nutritious',
'nutty',
'obedient',
'obese',
'oblong',
'oily',
'oblong',
'obvious',
'occasional',
'odd',
'oddball',
'offbeat',
'offensive',
'official',
@@ -836,7 +774,6 @@ const adjective = [
'personal',
'pertinent',
'pesky',
'pessimistic',
'petty',
'phony',
'physical',
@@ -858,7 +795,6 @@ const adjective = [
'pointed',
'pointless',
'poised',
'poor',
'popular',
'portly',
'posh',
@@ -903,7 +839,6 @@ const adjective = [
'quarterly',
'queasy',
'querulous',
'questionable',
'quick',
'quick-witted',
'quiet',
@@ -931,7 +866,6 @@ const adjective = [
'reliable',
'relieved',
'remarkable',
'remorseful',
'remote',
'repentant',
'required',
@@ -949,7 +883,6 @@ const adjective = [
'robust',
'rosy',
'rotating',
'rotten',
'rough',
'round',
'rowdy',
@@ -963,7 +896,6 @@ const adjective = [
'rusty',
'sad',
'safe',
'salty',
'same',
'sandy',
'sane',
@@ -998,7 +930,6 @@ const adjective = [
'shadowy',
'shady',
'shallow',
'shameful',
'shameless',
'sharp',
'shimmering',
@@ -1011,7 +942,6 @@ const adjective = [
'showy',
'shrill',
'shy',
'sick',
'silent',
'silky',
'silly',
@@ -1027,7 +957,6 @@ const adjective = [
'sleepy',
'slight',
'slim',
'slimy',
'slippery',
'slow',
'slushy',
@@ -1136,7 +1065,6 @@ const adjective = [
'tender',
'tense',
'tepid',
'terrible',
'terrific',
'testy',
'thankful',
@@ -1183,7 +1111,6 @@ const adjective = [
'tubby',
'turbulent',
'twin',
'ugly',
'ultimate',
'unacceptable',
'unaware',
@@ -1198,14 +1125,12 @@ const adjective = [
'unfolded',
'unfortunate',
'unhappy',
'unhealthy',
'uniform',
'unimportant',
'unique',
'united',
'unkempt',
'unknown',
'unlawful',
'unlined',
'unlucky',
'unnatural',
@@ -1235,7 +1160,6 @@ const adjective = [
'usable',
'used',
'useful',
'useless',
'utilized',
'utter',
'vacant',
@@ -1271,7 +1195,6 @@ const adjective = [
'warmhearted',
'warped',
'wary',
'wasteful',
'watchful',
'waterlogged',
'watery',
@@ -1611,7 +1534,6 @@ const object = [
'classroom',
'delivery',
'device',
'difficulty',
'drama',
'election',
'engine',
@@ -1625,7 +1547,6 @@ const object = [
'suggestion',
'tension',
'variation',
'anxiety',
'atmosphere',
'awareness',
'bread',
@@ -1761,7 +1682,6 @@ const object = [
'drawer',
'establishment',
'examination',
'garbage',
'grocery',
'honey',
'impression',
@@ -1886,7 +1806,6 @@ const object = [
'group',
'risk',
'word',
'fat',
'force',
'key',
'light',
@@ -1934,7 +1853,6 @@ const object = [
'coast',
'action',
'age',
'bad',
'boat',
'record',
'result',
@@ -2223,7 +2141,6 @@ const object = [
'pleasure',
'score',
'screw',
'sex',
'shop',
'shower',
'suit',
@@ -2416,7 +2333,6 @@ const object = [
'counter',
'flower',
'grandfather',
'harm',
'knee',
'lawyer',
'leather',
@@ -2747,7 +2663,6 @@ const object = [
'crazy',
'escape',
'gather',
'hate',
'prior',
'repair',
'rough',
@@ -2757,19 +2672,15 @@ const object = [
'strike',
'employ',
'external',
'hurt',
'illegal',
'laugh',
'lay',
'mobile',
'nasty',
'ordinary',
'respond',
'royal',
'senior',
'split',
'strain',
'struggle',
'swim',
'train',
'upper',
@@ -2808,7 +2719,6 @@ const object = [
'prompt',
'quiet',
'refuse',
'regret',
'reveal',
'rush',
'shake',
@@ -2848,9 +2758,6 @@ const object = [
'wake',
'wrap',
'yesterday',
'Thomas',
'Tom',
'Lieuwe',
];
/**
+36
View File
@@ -49,3 +49,39 @@ export const validateFile = (file) => {
return false;
}
};
/**
* @description Verifies if object is empty
* @param {object} obj
*/
export const isEmptyObject = (obj) => {
if (typeof obj === 'object' && obj !== null && !Array.isArray(obj)) {
return Object.keys(obj).length === 0;
}
throw new Error('Variable is not an object');
};
/**
* @description Merges two objects, suppressing undefined keys
* @param {object} a
* @param {object} b
*/
export const mergeObject = (a, b) => {
const merged = {};
Object.keys({ ...a, ...b }).map((key) => {
merged[key] = typeof b[key] === 'undefined' ? a[key] : b[key];
});
return merged;
};
/**
* @description Removes undefined
* @param {object} obj
*/
export const removeUndefined = (obj) => {
const patched = {};
Object.keys({ ...obj })
.filter((key) => typeof obj[key] !== 'undefined')
.map((key) => (patched[key] = obj[key]));
return patched;
};
+39
View File
@@ -0,0 +1,39 @@
import { isEmptyObject } from './parserUtils.js';
/**
* @description initial checks for an empty of malformed request object
* @param obj
* @param res
*/
export const failEmptyObjects = (obj, res) => {
let failed = false;
try {
if (isEmptyObject(obj)) {
res.status(400).send('No object found in request');
failed = true;
}
} catch (error) {
res.status(400).send(error);
failed = true;
}
return failed;
};
/**
* @description initial checks for an empty of malformed request object
* @param obj
* @param res
*/
export const failIsNotArray = (obj, res) => {
let failed = false;
try {
if (!Array.isArray(obj)) {
res.status(400).send('No array found in request');
failed = true;
}
} catch (error) {
res.status(400).send(error);
failed = true;
}
return failed;
};
-13
View File
@@ -1,13 +0,0 @@
import { check, validationResult } from 'express-validator';
/**
* @description Validates object for POST /ontime/views
*/
export const viewValidator = [
check('overrideStyles').isBoolean().withMessage('overrideStyles value must be boolean'),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];