Files
deployd/lib/session.js
2012-10-24 10:25:30 -07:00

282 lines
7.0 KiB
JavaScript

var Store = require('./db').Store
, util = require('util')
, uuid = require('./util/uuid')
, Cookies = require('cookies')
, EventEmitter = require('events').EventEmitter
, debug = require('debug')('session')
, address = require('./util/address');
/*!
* A simple index for storing sesssions in memory.
*/
var sessionIndex = {}
, userSessionIndex = {};
/**
* A store for persisting sessions inbetween connection / disconnection.
* Automatically creates session IDs on inserted objects.
*/
function SessionStore(namespace, db, sockets) {
this.sockets = sockets;
// socket queue
var socketQueue = this.socketQueue = new EventEmitter()
, socketIndex = this.socketIndex = {};
if(sockets) {
sockets.on('connection', function (socket) {
// NOTE: do not use set here ever, the `Cookies` api is meant to get a req, res
// but we are just using it for a cookie parser
var cookies = new Cookies(socket.handshake)
, sid = cookies.get('sid');
if(sid) {
// index sockets against their session id
socketIndex[sid] = socket;
socketQueue.emit(sid, socket);
}
});
}
Store.apply(this, arguments);
}
util.inherits(SessionStore, Store);
exports.SessionStore = SessionStore;
SessionStore.prototype.createUniqueIdentifier = function() {
return uuid.create(128);
};
/**
* Create a new `Session` based on an optional `sid` (session id).
*
* @param {String} sid
* @param {Function} callback(err, session)
*/
SessionStore.prototype.createSession = function(sid, fn) {
var socketIndex = this.socketIndex
, store = this;
if(typeof sid == 'function') {
fn = sid;
sid = undefined;
}
if(sid) {
debug('building existing %s', sid);
this.find({id: sid}, function(err, s) {
if(err) return fn(err);
if(!s) {
store.insert({id: sid}, function(err, s) {
if(err) console.error(err);
});
}
var sess = sessionIndex[sid] || new Session(s, store, socketIndex, store.sockets);
sessionIndex[sid] = sess;
// index sessions by user
if(s && s.uid) {
userSessionIndex[s.uid] = sess;
} else {
debug('not indexing user session', s);
}
fn(err, sess);
});
} else {
sid = this.createUniqueIdentifier();
debug('creating new %s', sid);
var sess = sessionIndex[sid] = new Session({id: sid}, this, socketIndex, store.sockets);
fn(null, sess);
this.insert({id: sid}, function(err, s) {
if(err) console.error(err);
});
}
};
/**
* Emit data to all the sockets listening to the given event.
*
* @param {String} ev
* @param {Object} data
*/
SessionStore.prototype.emitToAll = function (ev, data) {
this.sockets.emit(ev, data);
};
/**
* Emit data to all the sockets bound to the given array of user ids.
*
* @param {Array} uids
* @param {String} event
* @param {Object} data
*/
SessionStore.prototype.emitToSessions = function (sids, ev, data) {
sids.forEach(function (sid) {
var session = sessionIndex[sid]
, socket = session && session.socket;
if(socket) {
socket.emit(ev, data);
}
});
};
/**
* An in memory representation of a client or user connection that can be saved to disk.
* Data will be passed around via a `Context` to resources.
*
* Example:
*
* var session = new Session({id: 'my-sid', new SessionStore('sessions', db)});
*
* session.set({uid: 'my-uid'}).save();
*
* @param {Object} data
* @param {Store} store
* @param {Socket} socket
*/
function Session(data, store, sockets, rawSockets) {
var sess = this;
var sid;
this.data = data;
if(data && data.id) this.sid = sid = data.id;
this.store = store;
this.emitToUsers = function(collection, query, event, data) {
collection.get(query, function(users) {
var userSession;
var notConnectedUsers = [];
if(users && users.id) {
var u = users;
users = [u];
}
debug('emit to users:');
debug(' query: %j', query);
debug(' event: %s', event);
debug(' data: %j', data);
// process.server.cluster.emitToUsers(users, event, data);
users.forEach(function(u) {
userSession = userSessionIndex[u.id];
// emit to sessions online
if(userSession && userSession.socket) {
debug('using a connected socket', userSession.socket);
userSession.socket.emit(event, data);
} else {
debug('user is offline: %j - because: %s', u, userSession ? 'user session did not have a socket' : 'no user session was found');
debug('all user sessions:', userSessionIndex);
notConnectedUsers.push(u.id);
}
});
debug('not connected users: %j', notConnectedUsers);
process.server.cluster.emitToUsers(notConnectedUsers, event, data);
});
};
this.emitToAll = function(ev, data) {
// emit to other servers in the cluster
process.server.cluster.emitToAll(ev, data);
// emit to the current server
store.emitToAll(ev, data);
};
store.socketQueue.on(this.sid, function (socket) {
address(function (err, add) {
debug('setting host %s for %s', add, sess.id);
sess.set({host: add + ':' + process.server.options.port}).save();
});
debug('bound socket to', sess);
sess.socket = socket;
socket.on('disconnect', function (argument) {
delete sess.store.socketIndex[sess.data.id];
delete sess.socket;
});
});
}
/**
* Set properties on the in memory representation of a session.
*
* @param {Object} changes
* @return {Session} this for chaining
*/
Session.prototype.set = function(object) {
var session = this
, data = session.data || (session.data = {});
Object.keys(object).forEach(function(key) {
data[key] = object[key];
});
return this;
};
/**
* Save the in memory representation of a session to its store.
*
* @param {Function} callback(err, data)
* @return {Session} this for chaining
*/
Session.prototype.save = function(fn) {
var session = this
, data = this.data
, query = {id: data.id};
debug('saving %s', data.id);
session.store.remove({id: data.id}, function (err) {
if(err) return fn(err);
session.store.insert(data, function (err, res) {
fn && fn(err, res);
});
});
return this;
};
/**
* Reset the session using the data in its store.
*
* @param {Function} callback(err, data)
* @return {Session} this for chaining
*/
Session.prototype.fetch = function(fn) {
var session = this;
this.store.first({id: this.data.id}, function (err, data) {
session.set(data);
fn(err, data);
});
return this;
};
/**
* Remove the session.
*
* @param {Function} callback(err, data)
* @return {Session} this for chaining
*/
Session.prototype.remove = function(fn) {
var session = this;
debug('removing %s', this.data.id);
delete sessionIndex[this.data.id];
delete userSessionIndex[this.data.uid]; // TODO: Don't delete all of a user's sessions
delete session.store.socketIndex[this.data.id];
this.store.remove({id: this.data.id}, fn);
return this;
};