@mos-connection/connector
Version:
MOS compliant TCP/IP Socket connection.
523 lines • 24.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MosConnection = void 0;
const connectionConfig_1 = require("./config/connectionConfig");
const mosSocketServer_1 = require("./connection/mosSocketServer");
const model_1 = require("@mos-connection/model");
const MosDevice_1 = require("./MosDevice");
const socketConnection_1 = require("./connection/socketConnection");
const NCSServerConnection_1 = require("./connection/NCSServerConnection");
const helper_1 = require("@mos-connection/helper");
const eventemitter3_1 = require("eventemitter3");
const iconv = require("iconv-lite");
const mosMessageParser_1 = require("./connection/mosMessageParser");
const lib_1 = require("./lib");
class MosConnection extends eventemitter3_1.EventEmitter {
/** */
constructor(configOptions) {
super();
this._debug = false;
this._disposed = false;
this._scheduleCheckProfileValidnessTimeout = null;
this._incomingSockets = {};
this._ncsConnections = {};
this._mosDevices = {};
this._initialized = false;
this._isListening = false;
this._isOpenMediaHotStandby = false;
this._conf = new connectionConfig_1.ConnectionConfig(configOptions);
if (this._conf.debug) {
this._debug = this._conf.debug;
}
// Setup utility functions for handling MosTypes:
this.mosTypes = (0, model_1.getMosTypes)(configOptions.strict ?? false);
if (this._conf.strict) {
const orgStack = new Error();
this._scheduleCheckProfileValidness(orgStack);
}
}
/**
* Initiate the MosConnection, start accepting connections
*/
async init() {
this.emit('info', `Initializing MOS-Connection, id: ${this._conf.mosID}`);
this._initialized = true;
if (this._conf.acceptsConnections) {
await this._initiateIncomingConnections();
this._isListening = true;
return true;
}
return false;
}
/**
* Establish a new connection to a MOS-device (NCS-server). When established, the new MOS-device will be emitted to this.onConnection()
* @param connectionOptions Connection options
*/
async connect(connectionOptions) {
if (!this._initialized)
throw Error('Not initialized, run .init() first!');
this._isOpenMediaHotStandby = connectionOptions.secondary?.openMediaHotStandby ?? false;
// Connect to MOS-device:
const primary = new NCSServerConnection_1.NCSServerConnection(connectionOptions.primary.id, connectionOptions.primary.host, this._conf.mosID, connectionOptions.primary.timeout, connectionOptions.primary.heartbeatInterval, this._debug, this.mosTypes.strict, this._isOpenMediaHotStandby);
const secondary = connectionOptions.secondary
? new NCSServerConnection_1.NCSServerConnection(connectionOptions.secondary.id, connectionOptions.secondary.host, this._conf.mosID, connectionOptions.secondary.timeout, connectionOptions.secondary.heartbeatInterval, this._debug, this.mosTypes.strict, this._isOpenMediaHotStandby)
: null;
this._ncsConnections[connectionOptions.primary.host] = primary;
primary.on('rawMessage', (type, message) => {
this.emit('rawMessage', 'primary', type, message);
});
primary.on('warning', (str) => {
this.emit('warning', 'primary: ' + str);
});
primary.on('error', (error) => {
error.message = `primary: ${error.message}`;
this.emit('error', error);
});
primary.on('info', (str) => {
this.emit('info', 'primary: ' + str);
});
primary.createClient(MosConnection.nextSocketID, connectionOptions.primary.ports?.lower ?? MosConnection.CONNECTION_PORT_LOWER, 'lower', true);
primary.createClient(MosConnection.nextSocketID, connectionOptions.primary.ports?.upper ?? MosConnection.CONNECTION_PORT_UPPER, 'upper', true);
if (!connectionOptions.primary.dontUseQueryPort) {
primary.createClient(MosConnection.nextSocketID, connectionOptions.primary.ports?.query ?? MosConnection.CONNECTION_PORT_QUERY, 'query', false);
}
if (secondary && connectionOptions.secondary) {
this._ncsConnections[connectionOptions.secondary.host] = secondary;
secondary.on('rawMessage', (type, message) => {
this.emit('rawMessage', 'secondary', type, message);
});
secondary.on('warning', (str) => {
this.emit('warning', 'secondary: ' + str);
});
secondary.on('error', (error) => {
error.message = `secondary: ${error.message}`;
this.emit('error', error);
});
secondary.on('info', (str) => {
this.emit('info', 'secondary: ' + str);
});
secondary.createClient(MosConnection.nextSocketID, connectionOptions.secondary.ports?.lower ?? MosConnection.CONNECTION_PORT_LOWER, 'lower', !connectionOptions.secondary.openMediaHotStandby);
secondary.createClient(MosConnection.nextSocketID, connectionOptions.secondary.ports?.upper ?? MosConnection.CONNECTION_PORT_UPPER, 'upper', !connectionOptions.secondary.openMediaHotStandby);
if (!connectionOptions.primary.dontUseQueryPort) {
secondary.createClient(MosConnection.nextSocketID, connectionOptions.secondary.ports?.query ?? MosConnection.CONNECTION_PORT_QUERY, 'query', false);
}
// Handle that .openMediaHotStandby should not check for heartbeats on
// the secondary connection when the primary is connected
// And disable heartbeats on the primary when the primary is disconnected
if (connectionOptions.secondary?.openMediaHotStandby) {
// Initially disable heartbeats on secondary since primary should be attempted first
secondary.disableHeartbeats();
primary.enableHeartbeats();
primary.on('connectionChanged', () => {
if (primary.connected) {
secondary?.disableHeartbeats();
primary.enableHeartbeats();
}
else {
secondary?.enableHeartbeats();
primary.disableHeartbeats();
}
});
// Handle secondary connection changes
setTimeout(() => {
secondary?.on('connectionChanged', () => {
if (!primary.connected) {
// Secondary is active
if (secondary?.connected) {
secondary.enableHeartbeats();
primary.disableHeartbeats();
}
else {
// Secondary disconnected - try to re-enable primary
primary.enableHeartbeats();
secondary?.disableHeartbeats();
}
}
});
}, 50);
}
}
return this._registerMosDevice(this._conf.mosID, connectionOptions.primary.id, connectionOptions.secondary ? connectionOptions.secondary.id : null, primary, secondary);
}
/** Callback is called when a new connection is established */
onConnection(cb) {
this._onConnection = cb;
}
/** True if mosConnection is listening for connections */
get isListening() {
return this._isListening;
}
/** TO BE IMPLEMENTED: True if mosConnection is mos-compliant */
get isCompliant() {
return false;
}
/** True if mosConnection is configured to accept connections */
get acceptsConnections() {
return this._conf.acceptsConnections;
}
/** A list of the profiles mosConnection is currently configured to use */
get profiles() {
return this._conf.profiles;
}
/** Close all connections and clear all data */
async dispose() {
this._disposed = true;
const sockets = [];
for (const socketID in this._incomingSockets) {
const e = this._incomingSockets[socketID];
if (e) {
sockets.push(e.socket);
}
}
const disposePromises0 = sockets.map(async (socket) => {
return new Promise((resolve) => {
socket.on('close', resolve);
socket.end();
socket.destroy();
});
});
const disposePromises1 = [
this._lowerSocketServer ? this._lowerSocketServer.dispose([]) : Promise.resolve(),
this._upperSocketServer ? this._upperSocketServer.dispose([]) : Promise.resolve(),
this._querySocketServer ? this._querySocketServer.dispose([]) : Promise.resolve(),
];
const disposePromises2 = [];
for (const deviceId of Object.keys(this._mosDevices)) {
const device = this._mosDevices[deviceId];
disposePromises2.push(this.disposeMosDevice(device));
}
await Promise.all(disposePromises0);
await Promise.all(disposePromises1);
await Promise.all(disposePromises2);
}
/** Return a specific MOS-device */
getDevice(id) {
return this._mosDevices[id];
}
/** Get a list of all MOS-devices */
getDevices() {
return Object.keys(this._mosDevices).map((id) => {
return this._mosDevices[id];
});
}
async disposeMosDevice(myMosIDOrMosDevice, theirMosId0, theirMosId1) {
let id0;
let id1;
if (myMosIDOrMosDevice && myMosIDOrMosDevice instanceof MosDevice_1.MosDevice) {
// myMosID = myMosIDOrMosDevice
const mosDevice = myMosIDOrMosDevice;
id0 = mosDevice.idPrimary;
id1 = mosDevice.idSecondary;
}
else {
const myMosID = myMosIDOrMosDevice;
id0 = myMosID + '_' + theirMosId0;
id1 = theirMosId1 ? myMosID + '_' + theirMosId1 : null;
}
if (this._mosDevices[id0]) {
await this._mosDevices[id0].dispose();
delete this._mosDevices[id0];
}
else if (id1 && this._mosDevices[id1]) {
await this._mosDevices[id1].dispose();
delete this._mosDevices[id1];
}
else {
throw new Error(`Device not found ("${id0}", "${id1}")`);
}
}
/**
* Do a check if the profile is valid. Throws if not.
* Optionally called after a mosConnection has been set up to ensure that all callbacks have been set up properly.
*/
checkProfileValidness() {
if (this._scheduleCheckProfileValidnessTimeout) {
clearTimeout(this._scheduleCheckProfileValidnessTimeout);
this._scheduleCheckProfileValidnessTimeout = null;
}
const orgStack = new Error();
this._checkProfileValidness(orgStack);
}
/** TO BE IMPLEMENTED */
get complianceText() {
if (this.isCompliant) {
const profiles = [];
for (const nextSocketID in this._conf.profiles) {
if (this._conf.profiles[nextSocketID] === true) {
profiles.push(nextSocketID);
}
}
return `MOS Compatible - Profiles ${profiles.join(',')}`;
}
return 'Warning: Not MOS compatible';
}
setDebug(debug) {
this._debug = debug;
this.getDevices().forEach((device) => {
device.setDebug(debug);
});
Object.keys(this._ncsConnections).forEach((host) => {
const conn = this._ncsConnections[host];
if (conn) {
conn.setDebug(debug);
}
});
if (this._lowerSocketServer)
this._lowerSocketServer.setDebug(debug);
if (this._upperSocketServer)
this._upperSocketServer.setDebug(debug);
if (this._querySocketServer)
this._querySocketServer.setDebug(debug);
}
_registerMosDevice(myMosID, theirMosId0, theirMosId1, primary, secondary) {
const id0 = myMosID + '_' + theirMosId0;
const id1 = theirMosId1 ? myMosID + '_' + theirMosId1 : null;
const mosDevice = new MosDevice_1.MosDevice(id0, id1, this._conf, primary, secondary, this._conf.offspecFailover, this.mosTypes.strict);
mosDevice.setDebug(this._debug);
// Add mosDevice to register:
if (this._mosDevices[id0]) {
throw new Error('Unable to register MosDevice "' + id0 + '": The device already exists!');
}
if (id1 && this._mosDevices[id1]) {
throw new Error('Unable to register MosDevice "' + id1 + '": The device already exists!');
}
this._mosDevices[id0] = mosDevice;
if (id1)
this._mosDevices[id1] = mosDevice;
mosDevice.connect();
// emit to .onConnection:
if (this._onConnection)
this._onConnection(mosDevice);
return mosDevice;
}
/** Set up TCP-server */
async _initiateIncomingConnections() {
if (!this._conf.acceptsConnections) {
return Promise.reject(new Error('Not configured for accepting connections'));
}
const initSocket = (port, portType) => {
const socketServer = new mosSocketServer_1.MosSocketServer(port, portType, this._debug);
socketServer.on(socketConnection_1.SocketServerEvent.CLIENT_CONNECTED, (e) => this._registerIncomingClient(e));
socketServer.on(socketConnection_1.SocketServerEvent.ERROR, (err) => {
// handle error
this.emit('error', err);
});
return socketServer;
};
this._lowerSocketServer = initSocket(this._conf.ports?.lower ?? MosConnection.CONNECTION_PORT_LOWER, 'lower');
this._upperSocketServer = initSocket(this._conf.ports?.upper ?? MosConnection.CONNECTION_PORT_UPPER, 'upper');
this._querySocketServer = initSocket(this._conf.ports?.query ?? MosConnection.CONNECTION_PORT_QUERY, 'query');
const handleListen = async (socketServer) => {
await socketServer.listen();
this.emit('info', 'Listening on port ' + socketServer.port + ' (' + socketServer.portDescription + ')');
};
await Promise.all([
handleListen(this._lowerSocketServer),
handleListen(this._upperSocketServer),
handleListen(this._querySocketServer),
]);
// All sockets are open and listening at this point
}
/** */
_registerIncomingClient(client) {
const socketID = MosConnection.nextSocketID;
this.emit('rawMessage', 'incoming_' + socketID, 'newConnection', 'From ' + client.socket.remoteAddress + ':' + client.socket.remotePort);
const messageParser = new mosMessageParser_1.MosMessageParser(`${socketID}, ${client.socket.remoteAddress}, ${client.portDescription}`);
// messageParser.debug = this._debug
messageParser.on('message', (message, messageString) => {
// Handle incoming data
handleMessage(message, messageString).catch((err) => this.emit('error', err));
});
// handles socket listeners
client.socket.on('close', ( /*hadError: boolean*/) => {
this._disposeIncomingSocket(socketID);
this.emit('rawMessage', 'incoming_' + socketID, 'closedConnection', '');
});
client.socket.on('end', () => {
this.debugTrace('Socket End');
});
client.socket.on('drain', () => {
this.debugTrace('Socket Drain');
});
client.socket.on('data', (data) => {
const messageString = iconv.decode(data, 'utf16-be');
this.emit('rawMessage', 'incoming', 'recieved', messageString);
try {
messageParser.debug = this._debug;
messageParser.parseMessage(messageString);
}
catch (err) {
this.emit('error', err instanceof Error ? err : new Error(`${err}`));
}
});
const handleMessage = async (parsed, _messageString) => {
const remoteAddressContent = client.socket.remoteAddress
? client.socket.remoteAddress.split(':')
: undefined;
const remoteAddress = remoteAddressContent ? remoteAddressContent[remoteAddressContent.length - 1] : '';
const ncsID = parsed.mos.ncsID;
const mosID = parsed.mos.mosID;
const mosMessageId = parsed.mos.messageID ? parseInt(parsed.mos.messageID, 10) : undefined;
let mosDevice = this._mosDevices[ncsID + '_' + mosID] || this._mosDevices[mosID + '_' + ncsID];
const sendReply = (message) => {
message.ncsID = ncsID;
message.mosID = mosID;
message.prepare(mosMessageId);
const sendMessageString = message.toString();
const buf = iconv.encode(sendMessageString, 'utf16-be');
client.socket.write(buf);
this.emit('rawMessage', 'incoming_' + socketID, 'sent', sendMessageString);
};
if (!mosDevice && this._conf.openRelay) {
// No MOS-device found in the register
// Register a new mosDevice to use for this connection:
if (ncsID === this._conf.mosID) {
// Setup a "primary" connection back to the mos-device, so that we can automatically
// send commands to it through the mosDevice
const primary = new NCSServerConnection_1.NCSServerConnection(mosID, remoteAddress, this._conf.mosID, undefined, undefined, this._debug, this.mosTypes.strict, this._isOpenMediaHotStandby);
this._ncsConnections[remoteAddress] = primary;
primary.on('rawMessage', (type, message) => {
this.emit('rawMessage', 'primary', type, message);
});
primary.on('warning', (str) => {
this.emit('warning', 'primary: ' + str);
});
primary.on('error', (err) => {
err.message = `primary: ${err.message}`;
this.emit('error', err);
});
const openRelayOptions = typeof this._conf.openRelay === 'object' ? this._conf.openRelay.options : undefined;
primary.createClient(MosConnection.nextSocketID, openRelayOptions?.ports?.lower ?? MosConnection.CONNECTION_PORT_LOWER, 'lower', true);
primary.createClient(MosConnection.nextSocketID, openRelayOptions?.ports?.upper ?? MosConnection.CONNECTION_PORT_UPPER, 'upper', true);
mosDevice = this._registerMosDevice(this._conf.mosID, mosID, null, primary, null);
}
else if (mosID === this._conf.mosID) {
mosDevice = await this.connect({
primary: {
id: ncsID,
host: remoteAddress,
},
});
}
}
if (mosDevice) {
mosDevice
.routeData(parsed.mos, client.portDescription)
.catch((err) => {
if (helper_1.MosModel.ParseError.isParseError(err)) {
// Try to add the main mos message key to the error:
const breadcrumbs = Object.keys(parsed.mos).filter((key) => !['messageID', 'mosID', 'ncsID'].includes(key) &&
typeof parsed.mos[key] === 'object');
throw helper_1.MosModel.ParseError.handleCaughtError(breadcrumbs.join('/'), err);
}
else
throw err;
})
.then((message) => {
sendReply(message);
})
.catch((err) => {
// Something went wrong
if (err instanceof helper_1.MosModel.MosMessage) {
sendReply(err);
}
else {
// Internal/parsing error
// Log error:
this.emit('warning', `Error when handling incoming data: ${err} ${err?.stack}`);
// reply with NACK:
// TODO: implement ACK
// https://mosprotocol.com/wp-content/MOS-Protocol-Documents/MOS_Protocol_Version_2.8.5_Final.htm#mosAck
const msg = new helper_1.MosModel.MOSAck({
ID: this.mosTypes.mosString128.create('0'),
Revision: 0,
Description: this.mosTypes.mosString128.create(`Internal error: ${err}`),
Status: model_1.IMOSAckStatus.NACK,
}, this.mosTypes.strict);
sendReply(msg); // TODO: Need tests
}
});
}
else {
// No MOS-device found in the register
// We can't handle the message, reply with a NACK:
const msg = new helper_1.MosModel.MOSAck({
ID: this.mosTypes.mosString128.create('0'),
Revision: 0,
Description: this.mosTypes.mosString128.create('MosDevice not found'),
Status: model_1.IMOSAckStatus.NACK,
}, this.mosTypes.strict);
sendReply(msg); // TODO: Need tests
}
};
client.socket.on('error', (e) => {
e.message = `Socket had error (${socketID}, ${client.socket.remoteAddress}, ${client.portDescription}): ${e.message}`;
this.emit('error', e);
this.debugTrace(`Socket had error (${socketID}, ${client.socket.remoteAddress}, ${client.portDescription}): ${e}`);
});
// Register this socket:
this._incomingSockets[socketID + ''] = client;
this.debugTrace('Added socket: ', socketID);
}
/** Close socket and clean up */
_disposeIncomingSocket(socketID) {
const e = this._incomingSockets[socketID + ''];
if (e) {
e.socket.removeAllListeners();
e.socket.destroy();
}
delete this._incomingSockets[socketID + ''];
this.debugTrace('removed: ', socketID, '\n');
}
/** Get new unique id */
static get nextSocketID() {
return this._nextSocketID++ + '';
}
debugTrace(...strs) {
// eslint-disable-next-line no-console
if (this._debug)
console.log(...strs);
}
_scheduleCheckProfileValidness(orgStack) {
if (this._scheduleCheckProfileValidnessTimeout)
return;
this._scheduleCheckProfileValidnessTimeout = setTimeout(() => {
this._scheduleCheckProfileValidnessTimeout = null;
if (this._disposed)
return;
try {
this._checkProfileValidness(orgStack);
}
catch (e) {
// eslint-disable-next-line no-console
console.error(e);
}
}, lib_1.PROFILE_VALIDNESS_CHECK_WAIT_TIME);
}
/**
* Checks that all callbacks have been set up properly, according to which MOS-profile have been set in the options.
* throws if something's wrong
*/
_checkProfileValidness(orgStack) {
if (!this._conf.strict)
return;
const fixError = (message) => {
// Change the stack of the error, so that it points to the original call to the MosDevice:
const err = new Error(message);
err.stack = message + orgStack.stack;
return err;
};
if (this.listenerCount('error') === 0) {
throw fixError(`Error: no listener for the "error" event has been set up for MosConnection!`);
}
if (this.listenerCount('warning') === 0) {
throw fixError(`Error: no listener for the "warning" event has been set up for MosConnection!`);
}
}
}
exports.MosConnection = MosConnection;
MosConnection.CONNECTION_PORT_LOWER = 10540;
MosConnection.CONNECTION_PORT_UPPER = 10541;
MosConnection.CONNECTION_PORT_QUERY = 10542;
MosConnection._nextSocketID = 0;
//# sourceMappingURL=MosConnection.js.map