Files
deployd/lib/session.js
2012-09-06 10:50:53 -07:00

255 lines
6.4 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');
/*!
* 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 = {};
// TODO - sockets.on('connection', ...) - map to a session id based on socket.handshake.headers
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) {
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;
}
fn(err, sess);
});
} else {
sid = this.createUniqueIdentifier();
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);
});
}
};
/**
* 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) {
this.data = data;
if(data && data.id) this.sid = data.id;
this.store = store;
// create faux socket, to queue any events until
// a real socket is available
var socketWrapper = this.socket = {
on: function () {
// if we have a real socket, use it
if(this._socket) {
this._socket.apply(this._socket, arguments);
} else {
// otherwise add to bind queue
var queue = this._bindQueue = this._bindQueue || [];
queue.push(arguments);
}
},
emit: function () {
// if we have a real socket, use it
if(this._socket) {
this._socket.emit.apply(this._socket, arguments);
} else {
// otherwise add to bind queue
var queue = this._emitQueue = this._bindQueue || [];
queue.push(arguments);
}
},
_socket: sockets[this.sid]
};
this.emitToUsers = function(collection, query, event, data) {
collection.get(query, function(users) {
var userSession;
// TODO: arguments in weird order
if(users && users.id) {
userSession = userSessionIndex[err.id];
console.info(userSession);
if(userSession && userSession.socket) {
userSession.socket.emit(event, data);
}
return;
}
users.forEach(function(u) {
userSession = userSessionIndex[u.id];
// emit to sessions online
if(userSession && userSession.socket) {
userSession.socket.emit(event, data);
}
});
});
};
this.emitToAll = function() {
rawSockets.emit.apply(rawSockets, arguments);
};
// resolve queue once a socket is ready
store.socketQueue.once(this.sid, function (socket) {
socketWrapper._socket = socket;
// drain bind queue
if(socketWrapper._bindQueue && socketWrapper._bindQueue.length) {
socketWrapper._bindQueue.forEach(function (args) {
socket.on.apply(socket, args);
});
}
// drain emit queue
if(socketWrapper._emitQueue && socketWrapper._emitQueue.length) {
socketWrapper._emitQueue.forEach(function (args) {
socket.emit.apply(socket, args);
});
}
});
}
/**
* 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};
session.remove(function (err) {
if(err) return fn(err);
session.store.insert(data, function (err, res) {
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;
};