connect-databank
Version:
Use any databank driver as a store for session sessions
327 lines (287 loc) • 9.18 kB
JavaScript
// connect-databank.js
//
// Store connect session in a databank
//
// Copyright 2012-2013 E14N Inc. http://e14n.com/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
require("set-immediate");
var util = require("util"),
_ = require("underscore"),
uuid = require("node-uuid"),
async = require("async");
module.exports = function(sessionModule) {
var Store = sessionModule.Store;
var DatabankStore = function(bank, parentLog, cleanupInterval) {
var store = this,
log,
isActive = function(session) {
var expires;
if ('string' == typeof session.cookie.expires) {
expires = new Date(session.cookie.expires);
} else {
expires = session.cookie.expires;
}
if (!expires || Date.now() < expires) {
return true;
} else {
return false;
}
};
if (parentLog) {
log = parentLog.child({component: "connect-databank"});
}
store.get = function(sid, callback) {
async.waterfall([
function(callback) {
bank.read("session", sid, function(err, session) {
if (err && err.name == "NoSuchThingError") {
if (log) log.debug({sid: sid}, "No session in get()");
callback(null, null);
} else if (err) {
callback(err, null);
} else {
callback(null, session);
}
});
},
function(session, callback) {
if (!session) {
callback(null, session);
} else if (!isActive(session)) {
// delete it later
// XXX: should we just leave it for cleanup()?
setImmediate(function() {
bank.del("session", sid, function(err) {
if (err) {
if (log) log.error({err: err, sid: sid}, "Error deleting inactive session");
} else {
if (log) log.debug({sid: sid}, "Inactive session; deleted.");
}
});
});
callback(null, null);
} else {
callback(null, session);
}
}
], function(err, session) {
if (err) {
if (log) log.error(err);
callback(err, null);
} else {
if (log) log.debug({sid: sid, session: session}, "Got session.");
callback(null, session);
}
});
};
store.set = function(sid, sess, callback) {
// session sets this... usually.
if (!_.has(sess, "sid")) {
sess.sid = sid;
}
async.waterfall([
function(callback) {
bank.save("session", sid, sess, callback);
}
], function(err, saved) {
if (err) {
if (log) log.error(err);
callback(err);
} else {
if (log) log.debug({sid: sid, session: sess}, "Saved session.");
callback(null);
}
});
};
store.destroy = function(sid, callback) {
async.waterfall([
function(callback) {
bank.del("session", sid, callback);
}
], function(err) {
if (err && err.name == "NoSuchThingError") {
if (log) log.debug({sid: sid}, "Destroy for non-existent session; all good.");
callback(null);
} else if (err) {
if (log) log.error(err);
callback(err);
} else {
if (log) log.debug({sid: sid}, "Destroy for found session");
callback(null);
}
});
};
store.all = function(callback) {
async.waterfall([
function(callback) {
var sessions = [],
keepActive = function(session) {
if (isActive(session)) {
sessions.push(session);
}
};
bank.scan("session", keepActive, function(err) {
if (err) {
callback(err, null);
} else {
callback(null, sessions);
}
});
}
], function(err, sessions) {
if (err) {
if (log) log.error(err);
callback(err, null);
} else {
if (log) log.debug({count: sessions.length}, "Retrieved all sessions");
callback(null, sessions);
}
});
};
store.length = function(callback) {
async.waterfall([
function(callback) {
var lng = 0,
incr = function(session) { lng++; };
bank.scan("session", incr, function(err) {
if (err) {
callback(err, null);
} else {
callback(null, lng);
}
});
}
], function(err, lng) {
if (err) {
if (log) log.error(err);
callback(err, null);
} else {
if (log) log.debug({length: lng}, "Retrieved count of sessions");
callback(null, lng);
}
});
};
store.clear = function(callback) {
async.waterfall([
function(callback) {
var sids = [];
bank.scan("session", function(session) { sids.push(session.sid); }, function(err) {
if (err) {
callback(err, null);
} else {
callback(null, sids);
}
});
},
function(sids, callback) {
var delSid = function(sid, callback) {
bank.del("session", sid, callback);
};
async.eachLimit(sids, 16, delSid, callback);
}
], function(err) {
if (err) {
if (log) log.error(err);
callback(err);
} else {
if (log) log.debug("Cleared all sessions.");
callback(null);
}
});
};
store.close = function() {
var self = this;
clearInterval(self.interval);
clearTimeout(self.timeout);
};
// XXX: should this be public?
store.cleanup = function(callback) {
var cid = uuid.v4(),
q,
cleanupSession = function(sid, callback) {
if (log) log.debug({sid: sid, cid: cid}, "Deleting inactive session.");
bank.del("session", sid, function(err) {
if (err && err.name == "NoSuchThingError") {
if (log) log.debug({sid: sid, cid: cid}, "Missing inactive session on delete; ignoring.");
callback(null);
} else if (err) {
if (log) log.error({sid: sid, cid: cid, err: err}, "Error deleting this session.");
callback(err);
} else {
if (log) log.debug({sid: sid, cid: cid}, "Successfully deleted session.");
callback(null);
}
});
},
check = function(session) {
setImmediate(function() {
if (isActive(session)) {
if (log) log.debug({sid: session.sid, cid: cid}, "Ignoring active session.");
} else {
if (log) log.debug({sid: session.sid, cid: cid}, "Queuing inactive session for cleanup.");
q.push(session.sid);
}
});
},
scanDone = false;
if (log) log.debug({cid: cid}, "Starting cleanup.");
q = async.queue(cleanupSession, 16);
q.drain = function() {
if (log) log.debug({cid: cid}, "Queue is empty.");
if (scanDone) {
if (log) log.debug({cid: cid}, "Finished cleaning up.");
callback(null);
}
};
bank.scan("session", check, function(err) {
if (err) {
if (log) log.error({cid: cid, err: err}, "Error scanning sessions.");
callback(err);
} else {
if (log) log.debug({cid: cid}, "Finished scanning sessions; waiting for queue to drain.");
scanDone = true;
}
});
};
// Set up cleanup to happen every so often.
if (cleanupInterval) {
// Since there may be multiple processes trying to clean up,
// We stagger our cleanup randomly somewhere over the interval period
store.timeout = setTimeout(function() {
var doCleanup = function() {
store.cleanup(function(err) {
if (err) {
if (log) log.error(err, "Error cleaning up sessions");
} else {
if (log) log.debug("Finished cleaning up sessions.");
}
});
};
doCleanup();
store.interval = setInterval(function() {
doCleanup();
}, cleanupInterval);
}, Math.floor(Math.random()*cleanupInterval));
}
};
// This is weird
util.inherits(DatabankStore, Store);
// A schema, for those who want it
DatabankStore.schema = {
session: {
pkey: "sid"
}
};
return DatabankStore;
};