tyo-mq
Version:
Distributed Message Pub/Sub Service with socket.io
868 lines (702 loc) • 31.7 kB
JavaScript
/**
* Messaging Server
*/
'esversion: 6';
const os = require('os');
const pak = require('../package.json');
// info
var eventManager = require('./events');
const Constants = require('./constants');
function Server(options) {
this.options = options || {};
var server = this;
this.logger = console;
this.logger.debug = this.logger.debug || console.log;
var app = require('http').createServer((req, res) => {
res.writeHead(403);
});
var io = require('socket.io')/* .listen */(app, options);
// app.listen(this.options.port || Constants.DEFAULT_PORT);
/**
*
*/
var subscriptions = {};
/**
*
*/
var producers = {};
/**
*
*/
var consumers = {};
/**
*
* @param {*} eventStr
* @returns
*/
var getEventSubscriptions = function (eventStr) {
subscriptions[eventStr] = subscriptions[eventStr] || {};
return subscriptions[eventStr];
};
/**
*
* @param {*} eventStr
* @param {*} consumer
* @returns
*/
var getEventSubscriber = function (eventStr, consumer) {
consumer = consumer || Constants.ANONYMOUS;
var subscriptions = getEventSubscriptions(eventStr);
subscriptions[consumer] = subscriptions[consumer] || {subscribeTos: new Set()};
return subscriptions[consumer];
};
/**
* @todo
* Each event may be unqiue, deal with later
*
* @param {*} producer
*/
var getProducerMetaInfo = function (producer) {
producer = producer || Constants.ANONYMOUS;
producers[producer] = producers[producer] || {subscribers: new Set()};
return producers[producer];
};
/**
*
* @param {*} consumer
*/
var getConsumerMetaInfo = function (consumer) {
consumer = consumer || Constants.ANONYMOUS;
consumers[consumer] = consumers[consumer] || {name: consumer, subscribeTos: new Set()};
return consumers[consumer];
};
/**
*
* @param {*} consumer
*/
var deleteConsumerFromSubscriptions = function (consumer) {
for (var event in subscriptions) {
if (subscriptions[event][consumer])
delete subscriptions[event][consumer];
}
};
/**
* Actually we don't need to delete the subscription if the consumer lost connection, do we?
*/
var deleteProducerFromSubscriptions = function (producer) {
for (var event in subscriptions) {
for (var consumer in subscriptions[event]) {
var subscription = getEventSubscriber(event, consumer);
subscription.subscribeTos.forEach(function (name) {
if (name == producer) {
delete subscriptions[event][consumer];
var size = 0;
for (var key in subscriptions[event])
++size;
if (size === 0)
delete subscriptions[event];
}
});
}
}
};
/**
*
* @param {*} producer
*/
var getSubscribersByProducer = function (producer) {
var ids = {};
for (var key in subscriptions) {
if (typeof subscriptions[key] === 'string') {
var event = key;
for (var consumer in subscriptions[event]) {
var subscription = getEventSubscriber(event, consumer);
if (subscription.subscribeTos)
subscription.subscribeTos.forEach(function (name) {
if (name === producer) {
if (!ids[subscription.id]) {
ids[subscription.id] = {};
ids[subscription.id].events = new Set();
ids[subscription.id].name = subscription.name;
}
}
});
}
/**
* What is the purpose of this?
*/
for (var id in ids) {
ids[id].events = ids[id].events || new Set();
ids[id].events.add(event);
}
}
}
return ids;
};
/**
*
* @param {*} p
*/
this.start = function (p) {
var self = this;
var port = p || this.options.port || Constants.DEFAULT_PORT;
if (self.logger) {
self.logger.log(" ");
self.logger.log(" | |_ _ _ ___ _ __ ___ __ _ ");
self.logger.log(" | __| | | |/ _ \ _____| '_ ` _ \ / _` |");
self.logger.log(" | |_| |_| | (_) |_____| | | | | | (_| |");
self.logger.log(" \__|\__, |\___/ |_| |_| |_|\__, |");
self.logger.log(" |___/ |_|");
self.logger.log(" ");
self.logger.log('Message server version: ' + pak.version);
self.logger.log('Now listening on localhost:' + port);
}
this.create(io);
// creating the message server
app.listen(port);
if (process.send && typeof process.send === 'function')
process.send("Server started");
};
/**
* Create the queue
*/
this.create = function (io) {
//
var self = this;
// maintain a request table for whom is requesting what
// 1 success,
// creating a new websocket then wait for connection
io.sockets.on('connection', function(socket) {
var setupConsumer = function (consumer, consumerId) {
// unlike producer, consumer doesn't need to know the status of the producer
var consumerMeta = getConsumerMetaInfo(consumer);
consumerMeta.id = consumerId || consumerMeta.id;
consumerMeta.socket = socket.id;
consumerMeta.online = true;
return consumerMeta;
};
var setupProducer = function (producer, producerId) {
// unlike producer, producer doesn't need to know the status of the producer
var producerMeta = getProducerMetaInfo(producer);
producerMeta.id = producerId || producerMeta.id;
producerMeta.socket = socket.id;
producerMeta.online = true;
return producerMeta;
};
function sendErrorMessage (msg) {
sendErrorMessageById(socket.id, msg);
}
function sendErrorMessageById (id, msg) {
server.send(id, 'ERROR', msg);
}
/**
* system message all CAPS
*
* @param {*} event
* @param {*} producer
* @param {*} consumer
* @param {*} consumerId
*/
function subscribeMessage (event, producer, consumer, consumerId, scope) {
var eventStr;
server.logger.error("Consumer (name: " + consumer + ", id: " + socket.id + " subscribe: " + event + " from: " + producer);
eventStr = eventManager.toConsumerEvent(event, producer, scope != Constants.SCOPE_ALL);
// id is the message subscriber's id
// id = socket.id;
// register consumer if it hasn't done so
var consumerMeta = getConsumerMetaInfo(consumer);
if (!consumerMeta.socket)
setupConsumer(consumer, consumerId);
var subscription = getEventSubscriber(eventStr, consumer);
// the subscription is neither confirmed or authorized
subscription.id = consumerMeta.id;
subscription.acked = false;
subscription.name = consumer;
var subscribeTos;
subscribeTos = consumerMeta.subscribeTos || new Set();
if (!(producer in subscribeTos)) {
subscribeTos.add(producer);
subscription.subscribeTos.add(producer);
}
var producerMeta;
var status = true;
if (producer !== Constants.ALL_PUBLISHERS) {
producerMeta = getProducerMetaInfo(producer);
sendSubscriptionMessageToProducer(eventStr, producerMeta, consumerMeta, status);
}
else {
for (var name in producers) {
if (name === consumer)
continue;
producerMeta = getProducerMetaInfo(name);
sendSubscriptionMessageToProducer(eventStr, producerMeta, consumerMeta, status);
}
}
}
/**
*
* @param {*} eventStr
* @param {*} producerMeta
* @param {*} consumerMeta
* @param {*} status
*/
function sendSubscriptionMessageToProducer(eventStr, producerMeta, consumerMeta, status) {
var consumer = consumerMeta.name;
if (producerMeta.online) {
if (!producerMeta.subscribers.has(consumer))
producerMeta.subscribers.add(consumer);
sendSubscriptionMessageWithConsumerInfo(producerMeta, [eventStr], consumerMeta, status);
var subscription = getEventSubscriber(eventStr, consumer);
subscription.acked = true;
subscription.subscribeToId = producerMeta.id;
// @todo
// send subscription confirmation / rejection here
}
}
// send subscription message
function sendSubscriptionMessage(events, producerMeta, consumerMeta, status) {
if (producerMeta && producerMeta.id)
sendSubscriptionMessageWithConsumerInfo(producerMeta, events,consumerMeta, status);
}
// it seems the new updates weren't pushed to the remote repo
function sendSubscriptionMessageWithConsumerInfo(producerMeta, events, consumerMeta, status) {
if (status === null)
status = true;
var onSubscribeEvent = eventManager.toOnSubscribeEvent(producerMeta.id);
sendMessage(producerMeta.socket,
onSubscribeEvent,
{
name:consumerMeta.name,
id:consumerMeta.id,
socket:consumerMeta.socket,
events:events, online:status
},
consumerMeta.name
);
}
// subscribe message
socket.on('SUBSCRIBE', function (event) {
if (typeof event === "string")
try {
event = JSON.parse(event);
}
catch (e) {
server.logger.error("unknown subscribe event: " + e);
return;
}
if ((typeof event === 'object' && event.event)) {
var targetEvent;
var producer;
var consumer;
var id;
producer = event.producer;
consumer = event.consumer;
id = event.id || socket.id;
targetEvent = event.event;
producer = producer || Constants.ANONYMOUS;
consumer = consumer || Constants.ANONYMOUS;
scope = event.scope || Constants.SCOPE_DEFAULT;
subscribeMessage(targetEvent, producer, consumer, id, scope);
// can't do it in this scope, hasn't figured out why
// socket.on(event, function (data) {
// self.logger.log('Received subscribed message: ' + event + ', data: ' + data);
// for (var key in subscriptions[event]) {
// if (subscriptions[event][key])
// self.send(key, event, data);
// }
// });
}
else {
var msg = "Message name should be a object";
if (server.logger) {
server.logger.error("Incorrect subcription message name: " + event);
server.logger.error(msg);
}
sendErrorMessage(msg);
}
});
/**
*
*/
socket.on('UNSUBSCRIBE', function (data) {
if (subscriptions[data] && subscriptions[data][socket.id]) {
delete subscriptions[data][socket.id];
}
});
/**ƒ
*
*/
socket.on('DEBUG', function (data) {
if (server.logger)
server.logger.log('Received DEBUG message: ' + data);
});
/**
* DISCONNECT from server
*/
socket.on('QUIT', function (id) {
if (id === socket.id)
socket.disconnect();
});
/**
* Send the message for subscriber(s)' consumption
*
* we need to send two sets of messages one for single event receiver and one for all-event receivers
*
*/
function sendConsumeMessage (socketId, event, message, producer) {
var eventStr = eventManager.toConsumeEvent(event);
// var eventAll = eventManager.toConsumeEventAll(producer);
sendMessage(socketId, eventStr, message, producer);
// sendVolatileMessage(socketId, eventAll, msgObj);
}
/**
* we need to send two sets of messages one for single event receiver and one for all-event receivers
*
* @param {*} event
* @param {*} message
* @param {*} producer
*/
function broadcastConsumeMessage (event, message, producer) {
var eventStr = eventManager.toConsumeEvent(event);
var msgObj = {event:event, message:message, from:producer};
var eventAll = eventManager.toConsumeEventAll(producer);
// setTimeout(function () {
broadcastVolatileMessage(eventAll, msgObj);
//}, 400);
broadcastMessage(eventStr, msgObj);
}
/**
* Send socket message
*
*/
function sendMessage (id, event, message, from) {
if (!id) {
server.logger.error("Error: null socket");
server.logger.error("Event: " + event);
server.logger.error("Message: " + JSON.stringify(message));
if (from)
server.logger.error("From: " + JSON.stringify(from));
return;
}
server.logger.debug("sending message to " + id + ", event:" + event + ", message: " + JSON.stringify(message));
server.send(id, event, message);
}
/**
* Send socket volatile message
*
*/
function sendVolatileMessage (id, event, message) {
if (!id) {
server.logger.error("Error: null socket");
server.logger.error("Event: " + event);
server.logger.error("Message: " + JSON.stringify(message));
return;
}
server.logger.debug("sending volatile message to " + id + ", event:" + event + ", message: " + JSON.stringify(message));
server.volatile(event, message, id);
}
/**
* Send volatile message
*
*/
function broadcastVolatileMessage (event, message) {
server.logger.debug("broadcasting volatile message to everyone" + ", event:" + event + ", message: " + JSON.stringify(message));
server.volatile(event, message);
}
/**
*
* @param {*} event
* @param {*} message
*/
function broadcastMessage (event, message) {
server.logger.debug("broadcasting message to everyone" + ", event:" + event + ", message: " + JSON.stringify(message));
server.broadcast(event, message);
}
/**
*
*/
function generateMessage (event, message, producer) {
producer = producer || Constants.ANONYMOUS;
var msgObj = {event:event, message:message, from:producer};
let subscriptionEvent = eventManager.toConsumerEvent(event, producer);
let subscriptionEventAll = eventManager.toConsumerEventAll(producer);
[subscriptionEvent, subscriptionEventAll].forEach(function (eventStr) {
var subscriptions = getEventSubscriptions(eventStr);
for (var consumer in subscriptions) {
var subscription = subscriptions[consumer];
var consumerMeta = getConsumerMetaInfo(subscription.name);
var ar = Array.from(consumerMeta.subscribeTos);
for (var i = 0; i < ar.length; ++i) {
var name = ar[i];
if (name === Constants.ALL_PUBLISHERS || name === producer) {
sendConsumeMessage(consumerMeta.socket, eventStr, msgObj, producer);
break;
}
}
}
});
}
/**
* Relay message from producer to consumer
* the message has to an object containing event, message and producer name
*/
socket.on('PRODUCE', function (msg) {
let obj;
if (typeof msg === "string")
try {
obj = JSON.parse(msg);
}
catch (e) {
server.logger.error("Error parsing message: " + msg);
return;
}
else
obj = msg;
var producerName = obj.from;
// var event = eventManager.toConsumerEvent(obj.event, producerName);
var event = obj.event;
var message = obj.message;
var producerMeta = getProducerMetaInfo(producerName, obj.id);
if (!producerMeta.socket)
setupProducer(producerName, obj.id);
if (obj.method && obj.method === 'broadcast')
broadcastConsumeMessage(event, message, producerName);
else
generateMessage(event, message, producerName);
});
/**
* On a consumer is ready
*/
socket.on('CONSUMER', function (consumer) {
if (typeof consumer === "string")
try {
consumer = JSON.parse(consumer);
}
catch (e) {
server.logger.error("Error parsing consumer: " + consumer);
return;
}
if (typeof consumer.name !== "string") {
server.logger.error("Received incorrect consumer information");
sendErrorMessage({message: "Incorrect consumer's name", code: -1});
return;
}
server.logger.error("A consumer (name: " + consumer.name + ", socket: " + socket.id + ") has joined.");
var event;
var message = {event: 'CONNECT', socket: socket.id};
let consumerMeta = setupConsumer(consumer.name, consumer.id || socket.id);
let subscribedTos = consumerMeta.subscribeTos;
if (subscribedTos && subscribedTos.size > 0) {
let producers = Array.from(subscribedTos);
producers.forEach(function (producerName) {
var producerMeta = getProducerMetaInfo(producerName);
if (producerMeta && producerMeta.socket) {
message.consumer = consumer.name;
event = eventManager.toOnConnectEvent(producerMeta.id);
server.logger.log("Triggering event: " + event);
sendMessage(producerMeta.socket, event, message, consumer.name);
}
});
}
});
/**
* On producer is ready
*/
socket.on('PING', function (msg, callback) {
console.log("received PING message: " + msg);
if (callback)
callback("PONG");
});
socket.on('PRODUCER', function (msg) {
let producer;
if (typeof msg === "string")
try {
producer = JSON.parse(msg);
}
catch (e) {
server.logger.error("Unknown producer: " + msg);
// server.logger.error(e.message)
return;
}
else
producer = msg;
if (typeof producer.name !== "string") {
server.logger.error("Received incorrect producer information");
sendErrorMessage({message: "Incorrect producer's name", code: -1});
return;
}
var producerName = producer.name;
var producerId = producer.id || socket.id;
server.logger.log("A producer (name: " + producer.name + ", socket: " + socket.id + ", id: " + producerId + ") has joined.");
// var producerMeta = getProducerMetaInfo(producerName);
/**
* @todo
*
* Already a producer with such a name exists
*/
// if (producerMeta) {
// // @todo
// socket.disconnect();
// return;
// }
var producerMeta = getProducerMetaInfo(producerName); // producers[producerName] = producers[producerName] || {subscribers: new Set()};
var resendSubscriptionMessage = false;
if (producerMeta.id && producerMeta.id !== producerId) {
sendErrorMessageById(producerMeta.id, "The same producer newly joined");
// try {
// if (producerMeta.socket)
// producerMeta.socket.disconnect();
// }
// catch (err) {
// server.logger.error("Failed to disconnect previous joind producer: " + producerMeta.id);
// }
resendSubscriptionMessage = true;
}
setupProducer(producerName, producerId);
// in case the consumer connect before producer is ready
var ids = getSubscribersByProducer(producerName);
var event;
var message = {event: 'CONNECT', socket: socket.id};
for (var id in ids) {
var obj = ids[id];
var consumer = obj.name;
var events = [];
var consumerMeta = getConsumerMetaInfo(consumer);
if (!producerMeta.subscribers.has(consumer))
producerMeta.subscribers.add(consumer);
for (var event in obj.events) {
var subscription = getEventSubscriber(event, consumer);
if (resendSubscriptionMessage || !subscription.acked) {
events.push(event);
subscription.acked = true;
subscription.subscribeToId = socket.id;
}
}
if (events.length > 0) {
sendSubscriptionMessage(obj.events, producerMeta, consumerMeta, consumerMeta.online);
}
// now we send the producer online message to the consumer(s)
message.producer = producerName;
event = eventManager.toOnConnectEvent(consumerMeta.id);
server.logger.log("Triggering event: " + event);
server.logger.log("Producer is online (" + producerName + ")");
if (consumerMeta.socket)
sendMessage(consumerMeta.socket, event, message, producerName);
else
server.logger.debug("Trying to inform consumer (" + consumerMeta.name + ") that producer (" + producerName + ") is online but it seems the consumer is offline");
}
});
/**
* On HELLO
*/
socket.on('HELLO', function (message) {
server.logger.log("Received greetings from client who claims his/her name is " + message.name + " and a " + message.type);
});
/**
* @todo
* On Authentication
*/
socket.on('AUTHENTICATION', function (message) {
});
/**
* On Disconnect
*
* Please be noted losing connection doesn't mean unsubscribing / stop producing
*
* for unsubscribe / unpublish, see #UNSUBSCRIBE, #UNPUBLISH messages
*/
socket.on('disconnect', function () {
var event;
var message = {event: 'DISCONNECT', socket: socket.id};
// var id = socket.id;
/**
* @todo
*
* update the registration information
*
*/
// var isProducer = false;
// check if it is a producer
for (var name in producers) {
var producerMeta = producers[name];
if (producerMeta.socket && producerMeta.socket === socket.id) {
// isProducer = true;
producerMeta.online = false;
producerMeta.subscribers.forEach (function (consumerName) {
var consumerMeta = getConsumerMetaInfo(consumerName);
if (consumerMeta) {
// consumerMeta.subscribeTos.delete(name);
message.producer = name;
event = eventManager.toOnDisconnectEvent(consumerMeta.id);
server.logger.log("Triggering event: " + event);
server.logger.log("Lost connection to a producer (" + name + ")");
if (consumerMeta.socket)
sendMessage(consumerMeta.socket, event, message, name);
else
server.logger.debug("Trying to inform consumer that producer is offline but it seems the consumer is offline too");
}
});
producerMeta.socket = null;
// deleteProducerFromSubscriptions(name);
// no we are not gonna do it, as consumer still can wait for producer to come back online
// for whatever reasons it loses connection
// delete producers[name];
/**
* @todo
*
* send disconn info to subscribers
*/
break;
}
}
// check if it is a consumer
// a producer could be also a consumer
//if (!isProducer) {
for (var consumerName in consumers) {
var consumerMeta = consumers[consumerName];
if (consumerMeta.socket && consumerMeta.socket === socket.id) {
server.logger.log("Lost connection to a consumer (" + consumerName + ")");
consumerMeta.online = false;
consumerMeta.subscribeTos.forEach( function (producerName) {
var producerMeta = getProducerMetaInfo(producerName);
if (producerMeta && producerMeta.socket) {
message.consumer = consumerName;
event = eventManager.toOnDisconnectEvent(producerMeta.id);
server.logger.log("Triggering event: " + event);
sendMessage(producerMeta.socket, event, message, consumerName);
// producerMeta.subscribers.delete(consumerName);
}
});
consumerMeta.socket = null;
// deleteConsumerFromSubscriptions(consumerName);
// delete consumers[consumerName];
break;
}
}
// }
});
});
/**
* broadcast message to all connected sockets
*/
this.broadcast = function (event, message) {
io.emit(event, message);
};
/**
* sending the volatile message to all connected sockets
*/
this.volatile = function (event, message, id) {
if (id)
io.to(id).volatile.emit(event, message);
else
io.volatile.emit(event, message);
};
/**
* Send event / message to a particular endpoint
*/
this.send = function (socketId, event, message) {
io.to(socketId).emit(event, message);
};
};
}
module.exports = Server;