I am trying to create a multi-tenant app (saas), where each client has its own database.
My situation is:
I created a middleware that would determine who the client is based on a subdomain, then retrieve the client's database connection info from a general database. I don't know how to establish a connection object for this client so as to be able to use in my controllers. And should I do this in the middleware or in a controller? And if it's in the model, how do i pass the connection string and parameters (I could use session, but I don't know how to access session from within model).
How do i do the following?
Organisation: where do I create the db connection for client dynamically?
Inject/pass connection parameters to controller or model (where connection definition is made)
After dynamic connection has been made, how do i access it globally for that client?
This is an example of my middleware, and i would like to create a mongoose connection which i would like to make dynamic (pass in client's connection info):
function clientlistener() {
return function (req, res, next) {
console.dir('look at my sub domain ' + req.subdomains[0]);
// console.log(req.session.Client.name);
if (req.session.Client && req.session.Client.name === req.subdomains[0]) {
var options = session.Client.options;
var url = session.Client.url
var conn = mongoose.createConnection(url, options);
next();
}
}
}
How do I access this connection object from inside the controller? Or from the model?
Thank you.
This is to help others who may find themselves in similar situation as I did. I hope that it could be standardized. I dont think we should have to reinvent the wheel every time someone needs to make a multi-tenant application.
This example describes a multi-tenant structure with each client having its own database.
Like i said there might be a better way of doing this, but because i didn't get help myself, this was my solution.
So here are the goals this solution targets:
each client is identified by subdomain e.g client1.application.com,
application checks if subdomain is valid,
application looks up and obtains connection information (database url, credentials, etc) from master database,
application connects to client database ( pretty much hands over to client),
application takes measures to ensure integrity and resource management (e.g use the same database connection for members of the same client, rather than make new connection).
Here is the code
in your app.js file
app.use(clientListener()); // checks and identify valid clients
app.use(setclientdb());// sets db for valid clients
I've created two middlewares :
clientListener - to identify the client connecting,
setclientdb - gets client details from Master database, after client is identified, and then establishes connection to client database.
clientListener middleware
I check who the client is by checking the subdomain from the request object. I do a bunch of checks to be sure the client is valid (I know the code is messy, and can be made cleaner). After ensuring the client is valid, I store the clients info in session. I also check that if the clients info is already stored in session, there is no need to query the database again. We just need to make sure that the request subdomain, matches that which is already stored in session.
var Clients = require('../models/clients');
var basedomain = dbConfig.baseDomain;
var allowedSubs = {'admin':true, 'www':true };
allowedSubs[basedomain] = true;
function clientlistener() {
return function(req, res, next) {
//console.dir('look at my sub domain ' + req.subdomains[0]);
// console.log(req.session.Client.name);
if( req.subdomains[0] in allowedSubs || typeof req.subdomains[0] === 'undefined' || req.session.Client && req.session.Client.name === req.subdomains[0] ){
//console.dir('look at the sub domain ' + req.subdomains[0]);
//console.dir('testing Session ' + req.session.Client);
console.log('did not search database for '+ req.subdomains[0]);
//console.log(JSON.stringify(req.session.Client, null, 4));
next();
}
else{
Clients.findOne({subdomain: req.subdomains[0]}, function (err, client) {
if(!err){
if(!client){
//res.send(client);
res.send(403, 'Sorry! you cant see that.');
}
else{
console.log('searched database for '+ req.subdomains[0]);
//console.log(JSON.stringify(client, null, 4));
//console.log(client);
// req.session.tester = "moyo cow";
req.session.Client = client;
return next();
}
}
else{
console.log(err);
return next(err)
}
});
}
}
}
module.exports = clientlistener;
setclientdb middleware:
I check everything again making sure that the client is valid. Then the connection to the client's database with the info retrieved from session is opened.
I also make sure to store all active connections into a global object, so as to prevent new connections to the database upon each request(we don't want to overload each clients mongodb server with connections).
var mongoose = require('mongoose');
//var dynamicConnection = require('../models/dynamicMongoose');
function setclientdb() {
return function(req, res, next){
//check if client has an existing db connection /*** Check if client db is connected and pooled *****/
if(/*typeof global.App.clientdbconn === 'undefined' && */ typeof(req.session.Client) !== 'undefined' && global.App.clients[req.session.Client.name] !== req.subdomains[0])
{
//check if client session, matches current client if it matches, establish new connection for client
if(req.session.Client && req.session.Client.name === req.subdomains[0] )
{
console.log('setting db for client ' + req.subdomains[0]+ ' and '+ req.session.Client.dbUrl);
client = mongoose.createConnection(req.session.Client.dbUrl /*, dbconfigoptions*/);
client.on('connected', function () {
console.log('Mongoose default connection open to ' + req.session.Client.name);
});
// When the connection is disconnected
client.on('disconnected', function () {
console.log('Mongoose '+ req.session.Client.name +' connection disconnected');
});
// If the Node process ends, close the Mongoose connection
process.on('SIGINT', function() {
client.close(function () {
console.log(req.session.Client.name +' connection disconnected through app termination');
process.exit(0);
});
});
//If pool has not been created, create it and Add new connection to the pool and set it as active connection
if(typeof(global.App.clients) === 'undefined' || typeof(global.App.clients[req.session.Client.name]) === 'undefined' && typeof(global.App.clientdbconn[req.session.Client.name]) === 'undefined')
{
clientname = req.session.Client.name;
global.App.clients[clientname] = req.session.Client.name;// Store name of client in the global clients array
activedb = global.App.clientdbconn[clientname] = client; //Store connection in the global connection array
console.log('I am now in the list of active clients ' + global.App.clients[clientname]);
}
global.App.activdb = activedb;
console.log('client connection established, and saved ' + req.session.Client.name);
next();
}
//if current client, does not match session client, then do not establish connection
else
{
delete req.session.Client;
client = false;
next();
}
}
else
{
if(typeof(req.session.Client) === 'undefined')
{
next();
}
//if client already has a connection make it active
else{
global.App.activdb = global.App.clientdbconn[req.session.Client.name];
console.log('did not make new connection for ' + req.session.Client.name);
return next();
}
}
}
}
module.exports = setclientdb;
Last but not the least
Since I am using a combination of mongoose and native mongo, We have to compile our models at run time. Please see below
Add this to your app.js
// require your models directory
var models = require('./models');
// Create models using mongoose connection for use in controllers
app.use(function db(req, res, next) {
req.db = {
User: global.App.activdb.model('User', models.agency_user, 'users')
//Post: global.App.activdb.model('Post', models.Post, 'posts')
};
return next();
});
Explanation:
Like I said earlier on I created a global object to store the active database connection object: global.App.activdb
Then I use this connection object to create (compile) mongoose model, after i store it in the db property of the req object: req.db. I do this so that i can access my models in my controller like this for example.
Example of my Users controller:
exports.list = function (req, res) {
req.db.User.find(function (err, users) {
res.send("respond with a resource" + users + 'and connections ' + JSON.stringify(global.App.clients, null, 4));
console.log('Worker ' + cluster.worker.id + ' running!');
});
};
I will come back and clean this up eventually. If anyone wants to help me, that be nice.
Hello everyone, here is a much more updated solution.
So here are the goals this solution targets:
each client is identified by subdomain e.g client1.application.com,
application checks if subdomain is valid,
application looks up and obtains connection information (database url, credentials, etc) from master database,
application connects to client database ( pretty much hands over to client),
application takes measures to ensure integrity and resource management (e.g use the same database connection for members of the same client, rather than make new connection).
updates
use of promises,
automatic import & compilation of models
New middleware ; modelsinit (used to automatically import and compile mongoose models)
Clean up of middlewares (setclientdb, clientlistener, modelsInit)
Please see below for some Explanations
**
modelsInit Middleware
**
features
tests if models are already compiled. If so, skip.
tests to see if request is not a tenant request; i.e (request to apps homepage, admin page, etc)
'use strict';
/**
* Created by moyofalaye on 3/17/14.
*/
var path = require('path');
var config = require('../../config/config');
// Globbing model files
config.getGlobbedFiles('./app/models/*.js').forEach(function (modelPath) {
require(path.resolve(modelPath));
});
function modelsInit() {
return function (req, res, next) {
//console.log(req.subdomains[0]);
switch (req.subdomains[0]) {
case 'www':
case undefined:
return next();
break;
case 'admin':
return next();
break;
// default:
// return
}
var clientname = req.session.Client.name;
// test if models are not already compiled if so, skip
if (/*typeof req.db === 'undefined' && */ typeof global.App.clientModel[clientname] === 'undefined') {
req.db = {};
//Get files from models directory
config.getGlobbedFiles('./app/models/clientmodels/**/*.js').forEach(function (modelPath) {
console.log('the filepath is ' + modelPath);
//Deduce/ extrapulate model names from the file names
//Im not very good with regxp but this is what i had to do, to get the names from the filename e.g users.server.models.js (this is my naming convention, so as not to get confused with server side models and client side models
var filename = modelPath.replace(/^.*[\\\/]/, '');
var fullname = filename.substr(0, filename.lastIndexOf('.'));
var endname = fullname.indexOf('.');
var name = fullname.substr(0, endname);
req.db[name] = require(path.resolve(modelPath))(global.App.activdb);
console.log('the filename is ' + name);
});
global.App.clientModel[clientname] = req.db;
console.log(global.App.clients);
return next();
}
// since models exist, pass it to request.db for easy consumption in controllers
req.db = global.App.clientModel[clientname];
return next();
};
}
module.exports = modelsInit;
Todo: Further Explanation
ClientListener.js
var config = require('../../config/config');
var Clients = require('../models/clients');
var basedomain = config.baseDomain;
var allowedSubs = {'admin': true, 'www': true};
allowedSubs[basedomain] = true;
//console.dir(allowedSubs);
function clientlistener() {
return function (req, res, next) {
//check if client has already been recognized
if (req.subdomains[0] in allowedSubs || typeof req.subdomains[0] == 'undefined' || req.session.Client && req.session.Client.name === req.subdomains[0]) {
console.log('did not search database for ' + req.subdomains[0]);
//console.log(JSON.stringify(req.session.Client, null, 4));
return next();
}
//look for client in database
else {
Clients.findOne({subdomain: req.subdomains[0]}, function (err, client) {
if (!err) {
//if client not found
if (!client) {
//res.send(client);
res.status(403).send('Sorry! you cant see that.');
console.log(client);
}
// client found, create session and add client
else {
console.log('searched database for ' + req.subdomains[0]);
req.session.Client = client;
return next();
}
}
else {
console.log(err);
return next(err)
}
});
}
}
}
module.exports = clientlistener;
setclientdb.js
var client;
var clientname;
var activedb;
var Promise = require("bluebird");
Promise.promisifyAll(require("mongoose"));
//mongoose = require('mongoose');
function setclientdb() {
return function (req, res, next) {
//check if client is not valid
if (typeof(req.session.Client) === 'undefined' || req.session.Client && req.session.Client.name !== req.subdomains[0]) {
delete req.session.Client;
client = false;
return next();
}
//if client already has an existing connection make it active
else if (global.App.clients.indexOf(req.session.Client.name) > -1) {
global.App.activdb = global.App.clientdbconn[req.session.Client.name]; //global.App.clientdbconnection is an array of or established connections
console.log('did not make new connection for ' + req.session.Client.name);
return next();
}
//make new db connection
else {
console.log('setting db for client ' + req.subdomains[0] + ' and ' + req.session.Client.dbUrl);
client = mongoose.createConnection(req.session.Client.dbUrl /*, dbconfigoptions*/);
client.on('connected', function () {
console.log('Mongoose default connection open to ' + req.session.Client.name);
//If pool has not been created, create it and Add new connection to the pool and set it as active connection
if (typeof(global.App.clients) === 'undefined' || typeof(global.App.clients[req.session.Client.name]) === 'undefined' && typeof(global.App.clientdbconn[req.session.Client.name]) === 'undefined') {
clientname = req.session.Client.name;
global.App.clients.push(req.session.Client.name);// Store name of client in the global clients array
activedb = global.App.clientdbconn[clientname] = client; //Store connection in the global connection array and set it as the current active database
console.log('I am now in the list of active clients ' + global.App.clients[clientname]);
global.App.activdb = activedb;
console.log('client connection established, and saved ' + req.session.Client.name);
return next();
}
});
// When the connection is disconnected
client.on('disconnected', function () {
console.log('Mongoose ' + req.session.Client.name + ' connection disconnected');
});
// If the Node process ends, close the Mongoose connection
process.on('SIGINT', function () {
client.close(function () {
console.log(req.session.Client.name + ' connection disconnected through app termination');
process.exit(0);
});
});
}
}
}
module.exports = setclientdb;
Further Explanations Coming
Related
I am building a simple VoIP app using asterisk-manager module on nodeJS. The asterisk server is installed on centos 7 (basic install) and is hosted on a virtual machine. The code below, listens for agent login event, and popups a url when it receives dtmf key:
var port = 5038,
host = 'asteriskIP',
username = 'popup',
password = 'popup',
open = require('open'),
mysql = require('mysql'),
ami = new require('asterisk-manager')(port, host, username, password, true);
ami.keepConnected();
//Mysql server connection pool
var pool = mysql.createPool({
host: host,
user: 'user',
password: 'password',
database: 'db'
});
ami.on('newstate', function (stateEvent) {
var channelState = stateEvent.channelstate;
if (channelState === '6') {
return false;
}
/*
Listen for new channel after agent login
*/
ami.on('newchannel', function (e) {
/* Check if caller id number is empty (This is necessary owning to new channel created as a result of
DTMF. If this returns true, return false else execute mysql query.
*/
if (e.calleridnum === '' && isNaN(e.calleridnum)) {
return false;
} else if (e.calleridnum !== '' && !isNaN(e.calleridnum)) {
var callerId = e.calleridnum;
sql = "INSERT INTO dtmf (caller_id) VALUES ?",
values = [[callerId]];
pool.query(sql, [values], function (error) {
if (error) throw error;
});
/*
Listen for DTMF on 'end' and update current caller table
*/
ami.on('dtmf', function (evt) {
var end = evt.end;
if (end === 'Yes') {
var digit = evt.digit;
sql = `UPDATE dtmf SET caller_lang = ${digit} WHERE caller_id = ?`,
values = [[callerId]];
pool.query(sql, [values], function (error) {
if (error) throw error;
});
/*
This piece of code retrieves DTMF code input and popsup
a url in the agents browser window.
*/
ami.on('bridge', function (evt) {
var state = evt.bridgestate;
if (state === 'Link') {
switch (digit) {
case '1':
open('http://someurl?' + digit);
break;
case '2':
open('http://someurl?' + digit);
break;
default:
}
}
})
}
});
}
return false;
});
});
Everything works fine when I run this code on my mac. However, when I deployed the code to the virtual machine, it inserts and updates the database normally, but no url pops up in my browser. Please is there a way nodeJS app deployed on a virtual machine, can open a window on a users local browser? Thanks.
For security reason there is no browser or OS that will let you pop up a browser window on someone else computer without first being connected.
For that i think you would have to build a client app for example a widget, service or browser extension that would be running on the person computer ... This client could use Socket.io to listen and react to event happening on the Node.js end.
That could be one solution.
I would like to get a multi-process node. Workers are listening clients connections. I need pass sockets to master process because master process emit message to clients. Workers also need socket to emit message to clients.
Socket is a circular object and can't pass to a master process.
My code:
const cluster = require('cluster');
const http = require('http');
var io = require('socket.io');
var users;
var clients = {};
if (cluster.isMaster) {
function messageHandler(msg) {
if (msg.usersarray) {
usersarray = msg.usersarray;
console.log(usersarray);
}else if(msg.socket){
clients[usersarray["manu"][0]] = msg.socket;
clients[usersarray["manu"][0]].emit("hola","hola");
}
}
// Start workers and listen for messages containing notifyRequest
const numCPUs = require('os').cpus().length;
for (var i = 0; i < numCPUs; i++) {
cluster.fork();
}
Object.keys(cluster.workers).forEach((id) => {
cluster.workers[id].on('message', messageHandler);
});
}else {
// Create server & socket
var server = http.createServer(function(req, res){
// Send HTML headers and message
res.writeHead(404, {'Content-Type': 'text/html'});
res.end('<h1>Aw, snap! 404</h1>');
});
server.listen(3000);
io = io.listen(server);
// Add a connect listener
io.sockets.on('connection', function(socket) {
var hs = socket.handshake;
console.log("socket connected");
if(users == undefined){
users = {};
}
if(hs.query.usuario != undefined){
if(users[hs.query.usuario] == undefined){
users[hs.query.usuario] = new Array();
}
users[hs.query.usuario].push(socket.id); // connected user with its socket.id
clients[socket.id] = socket; // add the client data to the hash
process.send({ usersarray: users});
process.send({ socket: socket});
}
// Disconnect listener
socket.on('disconnect', function() {
console.log('Client disconnected.');
});
});
}
in line process.send({ socket: socket}); Node js get error "TypeError: Converting circular structure to JSON"
-I used some module to transform circular object but don't working.
-I tried to pass socket id and then in master process, created new socket with this id but I didn't know to use it.
There is any posibility to pass socket from worker to master process?
Node js version: v5.5.0
Hm, I don't think it is possible what you are trying to do. When you create a cluster it means that you create separate processes (master + workers) which can only talk over the pipe.
Talking over the pipe means they can only send strings to each other. process.send tries to serialize a Javascript object as JSON (--> making a string out of it) using JSON.stringify. JSON for example cannot have functions, circles, etc. I just checked the socket object, it is very complex and contains functions (such as socket.emit()), so you cannot just serialize it and send it over the pipe.
Maybe you can check this or this on how to use clustered WebSockets.
It doesn't seem very trivial.. Maybe you can just pass CPU intensive tasks to some worker processes (via cluster or just spawning them yourself), send the results back to the master and let him do all the communication with the client?
I understand your purpose of broadcasting to all the node worker processes in a cluster, although you can not send socket component as such but there is a workaround for the purpose to be served. I will try an explain with an example :
Step 1: When a client action requires a broadcast :
Child.js (Process that has been forked) :
socket.on("BROADCAST_TO_ALL_WORKERS", function (data)
{
process.send({cmd : 'BROADCAST_TO_ALL_WORKERS', message :data.message});
})
Step 2: On the cluster creation side
Server.js (Place where cluster forking happens):
if (cluster.isMaster) {
for (var i = 0; i < numCPUs; i++) {
var worker = cluster.fork();
worker.on('message', function (data) {
if (data.cmd === "BROADCAST_TO_ALL_WORKERS") {
console.log(server_debug_prefix() + "Server Broadcast To All, Message : " + data.message + " , Reload : " + data.reload + " Player Id : " + data.player_id);
Object.keys(cluster.workers).forEach(function(id) {
cluster.workers[id].send({cmd : "BROADCAST_TO_WORKER", message : data.message});
});
}
});
}
cluster.on('exit', function (worker, code, signal) {
var newWorker = cluster.fork();
newWorker.on('message', function (data) {
console.log(data);
if (data.cmd === "BROADCAST_TO_ALL_WORKERS") {
console.log(data.cmd,data);
Object.keys(cluster.workers).forEach(function(id) {
cluster.workers[id].send({cmd : "BROADCAST_TO_WORKER", message : data.message});
});
}
});
});
}
else {
//Node Js App Entry
require("./Child.js");
}
Step 3: To Broadcast in the child process
-> Put this before io.on("connection") in Child.js
process.on("message", function(data){
if(data.cmd === "BROADCAST_TO_WORKER"){
io.sockets.emit("SERVER_MESSAGE", { message: data.message, reload: data.reload, player_id : data.player_id });
}
});
I hope its clear. Please comment if its confusing ... I will try and make it clear.
after some struggling I have managed to run socket.io with express version > v4 and now I experience some weird behaviour which I can't figure out.
First let me explain my example.
I have built a test backend in which a user can login.
Right after login the user only sees a list with currently online users.
How do I get the currently online users?
Well. For this example every user, that has logged into the backend section is "online" and therefore should be displayed in that list.
Now to the code.
Backend router with io function end emits:
router.get('/', function(req, res, next) {
// console.log("socket io req response: ",req.app.get('io') );
if(!req.session.user[0].io){
req.app.get('io').on('connection', function(socket){ // Gets executed when user connects to /backend/
var currentUser = req.session.user[0].name; // save username according to session
req.session.user[0].io = true;
console.log("IO SESSION SET : "+ req.session.user[0].io +" for user " + currentUser);
// console.log('User connected : ' + currentUser); // Log user to server
req.app.get('io').emit('cameOnline', currentUser); // emit function cameOnline to client js
socket.on('disconnect', function(){
// console.log('User: '+req.session.user[0].name+' disconnected.'); // Log disconnected user to server
req.session.user[0].io = false;
console.log("IO SESSION SET : "+ req.session.user[0].io +" for user " + currentUser);
req.app.get('io').emit('wentOffline', currentUser); // emit wentOffline to client js
});
});
}
// res.render('backend', { title: 'Backend' });
// console.log(req.session.user[0].name);
/*
-------------------------
#########################
IF EVERYTHING FAILS - THE CODE BELOW IS THE NORMAL RENDER WITHOUT SOCKET IO
#########################
-------------------------
*/
if (req.session && req.session.user) { // Check if session exists
// lookup the user in the DB by pulling their email from the session
User.getUser({ name: req.session.user.name }, function (err, user) {
if (!user) {
console.log("No User found");
// if the user isn't found in the DB, reset the session info and
// redirect the user to the login page
// console.log(user);
req.session.reset();
res.redirect('/login');
} else {
// expose the user to the template
res.locals.user = user;
// render the dashboard page
function passOnlineUsers(callback){ // Looks for All users who are online
User.getOnlineUsers({}, function(err, res, fields){
// console.log(res);
if (err) throw err;
callback(err, res);
});
}
passOnlineUsers(function(err, result){
res.render('backend', { title: 'Backend', viewer: req.session.user[0], online: result });
// res.render('backend', { title: 'Backend', viewer: "", online: result });
});
}
});
} else {
res.redirect('/login');
}
});
Before establishing the socket.io connection I check if the users session already says that he has one. If not -> then proceed to create a connection.
Then the function "cameOnline" gets emited to the client like so : req.app.get('io').emit('cameOnline', currentUser);
Now to the client side code:
socket.on('cameOnline', function(name){
console.log(name + " connected.");
var $li = $('<li/>');
$li.addClass(name);
if($('#onlineUsers .'+name).length == 0){
console.log(name + " is about to be added");
$li.text(" "+ name);
$('#onlineUsers').append($li);
}else{
console.log("no new user");
}
// $('#onlineUsers').append($("<span>").text(" " + name)).addClass(name);
});
socket.on('wentOffline', function(name){
console.log(name +" is about to go offline");
var toDel = $('#onlineUsers li.'+name);
toDel.remove();
});
The client side listens to the "cameOnline" emit and adds the user if he is not already in the list.
Similar behavior with the disconnect function. Pretty self explained.
Now to the issue:
When I log into the backend with user : "Noa" i see user "Noa" correctly displayed in the list.
Now when I open a separate browser and login with another user : "Albert" the user gets correctly added live to the online list on both clients.
Perfect.
BUT
When I disconnect with user : "Albert" the online user list for user "Noa" gets cleared completely, as if noone was online.
When I refresh the page as user "Noa" I suddenly see both users as online again although the second user isn't online any more.
When I check the console on my client I see duplicated logs of console.log(name + " is about to be added"); as if there is some sort of caching. The more I refresh the more user "copies" I have.
The "copies" of the user I am online with get incremented and the one weird copy of the user who WAS online stays there as well. All the time.
Any ideas what I am doing massively wrong here?
** EDIT **
bin/www io declaration:
/**
* Socket IO
*/
var io = require('socket.io')(server);
app.set('io',io);
This is more of a question regarding what to do in the scenario where you want to trigger a socket event for one user, that might be logged into another browser.
I've got a couple of functions that update a users' workstack real-time (in a queue of other workstacks that are assignable by other users); however, if the user is logged into another browser at the same time, and do an update in one browser, it doesn't update in the other (as they have a different socket.id).
I'm not sure what to do with this... I could do it based on the user ID of the person logged in, but at present my socket code does not have scope of any session variables and although there are modules such as session-socket - I'm not sure if that's the right path to go down.
Can anyone advise a way I might approach this please?
If you don't use any cluster you can follow the approach I had in my Miaou chat : I simply set the user as a property of the socket object and I iterate on sockets when necessary. This allow for a few utilitarian functions.
Here's the (simplified) related code.
io.on('connect', function(socket){
...
var userId = session.passport.user;
if (!userId) return die("no authenticated user in socket's session");
...
var shoe = new Shoe(socket, completeUser) // <=== bindind user socket here
// socket event binding here
The Shoe object :
// A shoe embeds a socket and is provided to controlers and plugins.
// It's kept in memory by the closures of the socket event handlers
function Shoe(socket, completeUser){
this.socket = socket;
this.completeUser = completeUser;
this.publicUser = {id:completeUser.id, name:completeUser.name};
this.room;
socket['publicUser'] = this.publicUser;
this.emit = socket.emit.bind(socket);
}
var Shoes = Shoe.prototype;
// emits something to all sockets of a given user. Returns the number of sockets
Shoes.emitToAllSocketsOfUser = function(key, args, onlyOtherSockets){
var currentUserId = this.publicUser.id,
nbs = 0;
for (var clientId in io.sockets.connected) {
var socket = io.sockets.connected[clientId];
if (onlyOtherSockets && socket === this.socket) continue;
if (socket && socket.publicUser && socket.publicUser.id===currentUserId) {
socket.emit(key, args);
nbs++;
}
}
return nbs;
}
// returns the socket of the passed user if he's in the same room
Shoes.userSocket = function(userIdOrName) {
var clients = io.sockets.adapter.rooms[this.room.id],
sockets = [];
for (var clientId in clients) {
var socket = io.sockets.connected[clientId];
if (socket && socket.publicUser && (socket.publicUser.id===userIdOrName||socket.publicUser.name===userIdOrName)) {
return socket;
}
}
}
// returns the ids of the rooms to which the user is currently connected
Shoes.userRooms = function(){
var rooms = [],
uid = this.publicUser.id;
iorooms = io.sockets.adapter.rooms;
for (var roomId in iorooms) {
if (+roomId!=roomId) continue;
var clients = io.sockets.adapter.rooms[roomId];
for (var clientId in clients) {
var socket = io.sockets.connected[clientId];
if (socket && socket.publicUser && socket.publicUser.id===uid) {
rooms.push(roomId);
break;
}
}
}
return rooms;
}
// returns the first found socket of the passed user (may be in another room)
function anyUserSocket(userIdOrName) {
for (var clientId in io.sockets.connected) {
var socket = io.sockets.connected[clientId];
if (socket.publicUser && (socket.publicUser.id===userIdOrName||socket.publicUser.name===userIdOrName)) {
return socket;
}
}
}
// closes all sockets from a user in a given room
exports.throwOut = function(userId, roomId, text){
var clients = io.sockets.adapter.rooms[roomId];;
for (var clientId in clients) {
var socket = io.sockets.connected[clientId];
if (socket.publicUser && socket.publicUser.id===userId) {
if (text) socket.emit('miaou.error', text);
socket.disconnect('unauthorized');
}
}
}
Real code
Now, with ES6 based node versions and WeakMap, I might implement a more direct mapping but the solution I described is robust and efficient enough.
Starting from the official chat example of Node.js.
The user is prompted to emit his name to the server through 'register' (client.html):
while (name == '') {
name = prompt("What's your name?","");
}
socket.emit('register', name );
The server receives the name. I want it to make the name as the identifier of the socket. So when I need to send a message to that user I send it to the socket having his name (Names are stored in a database for the info).
Changes will take place here (server.js):
socket.on('register', function (name) {
socket.set('nickname', name, function () {
// this kind of emit will send to all! :D
io.sockets.emit('chat', {
msg : "naay nag apil2! si " + name + '!',
msgr : "mr. server"
});
});
});
I'm struggling making this works, because I can't go any farer if I can't identify the socket. So any help will be really appreciated.
Update: I understand that nickname is a parameter for the socket, so the question is more specifically: how to get the socket that has "Kyle" as a nickname to emit it a message?
Store your sockets in a structure like this:
var allSockets = {
// A storage object to hold the sockets
sockets: {},
// Adds a socket to the storage object so it can be located by name
addSocket: function(socket, name) {
this.sockets[name] = socket;
},
// Removes a socket from the storage object based on its name
removeSocket: function(name) {
if (this.sockets[name] !== undefined) {
this.sockets[name] = null;
delete this.sockets[name];
}
},
// Returns a socket from the storage object based on its name
// Throws an exception if the name is not valid
getSocketByName: function(name) {
if (this.sockets[name] !== undefined) {
return this.sockets[name];
} else {
throw new Error("A socket with the name '"+name+"' does not exist");
}
}
};