diff --git a/README.md b/README.md index 5b093ddd..6c10ae96 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,10 @@ This is the secret used to sign the session ID cookie. The session store instance, defaults to a new `MemoryStore` instance. +##### cacheLimit + +The limit on the size of the default `MemoryStore` instance, defaults to 1000 objects. + ##### unset Control the result of unsetting `req.session` (through `delete`, setting to `null`, diff --git a/index.js b/index.js index 43d70e16..f22f852b 100644 --- a/index.js +++ b/index.js @@ -85,7 +85,7 @@ function session(options){ var options = options || {} // name - previously "options.key" , name = options.name || options.key || 'connect.sid' - , store = options.store || new MemoryStore + , store = options.store || new MemoryStore(options) , cookie = options.cookie || {} , trustProxy = options.proxy , storeReady = true diff --git a/session/memory.js b/session/memory.js index 9887680d..9bba1d10 100644 --- a/session/memory.js +++ b/session/memory.js @@ -35,9 +35,13 @@ module.exports = MemoryStore * @public */ -function MemoryStore() { +function MemoryStore(options) { Store.call(this) + this.cacheLimit = options && options.cacheLimit || 1000 this.sessions = Object.create(null) + this.first = null + this.last = null + this.l = 0 } /** @@ -62,7 +66,7 @@ MemoryStore.prototype.all = function all(callback) { var session = getSession.call(this, sessionId) if (session) { - sessions[sessionId] = session; + sessions[sessionId] = session } } @@ -78,6 +82,9 @@ MemoryStore.prototype.all = function all(callback) { MemoryStore.prototype.clear = function clear(callback) { this.sessions = Object.create(null) + this.last = null + this.first = null + this.l = 0 callback && defer(callback) } @@ -89,7 +96,24 @@ MemoryStore.prototype.clear = function clear(callback) { */ MemoryStore.prototype.destroy = function destroy(sessionId, callback) { + var sess = this.sessions[sessionId] delete this.sessions[sessionId] + + if(typeof sess !== 'undefined'){ + if(sess.next !== null) sess.next.prev = sess.prev + if(sess.prev !== null) sess.prev.next = sess.next + + if(this.last !== null && this.last.id === sessionId){ + this.last = this.last.next + } + + if(this.first !== null && this.first.id === sessionId){ + this.first = this.first.prev + } + + this.l-- + } + callback && defer(callback) } @@ -122,14 +146,31 @@ MemoryStore.prototype.get = function get(sessionId, callback) { */ MemoryStore.prototype.length = function length(callback) { - this.all(function (err, sessions) { - if (err) return callback(err) - callback(null, Object.keys(sessions).length) - }) + callback(null, this.l) } MemoryStore.prototype.set = function set(sessionId, session, callback) { - this.sessions[sessionId] = JSON.stringify(session) + + var firstSession = this.first + + if (this.l >= this.cacheLimit) { + this.destroy(this.last.id) + } + + if (typeof this.sessions[sessionId] === 'undefined'){ + this.l++ + } + + this.first = this.sessions[sessionId] = {id: sessionId, data: JSON.stringify(session), prev: firstSession, next: null} + + if(firstSession !== null){ + firstSession.next = this.first + } + + if(this.last === null) { + this.last = this.first + } + callback && defer(callback) } @@ -148,7 +189,7 @@ MemoryStore.prototype.touch = function touch(sessionId, session, callback) { if (currentSession) { // update expiration currentSession.cookie = session.cookie - this.sessions[sessionId] = JSON.stringify(currentSession) + this.sessions[sessionId].data = JSON.stringify(currentSession) } callback && defer(callback) @@ -167,7 +208,7 @@ function getSession(sessionId) { } // parse - sess = JSON.parse(sess) + sess = JSON.parse(sess.data) var expires = typeof sess.cookie.expires === 'string' ? new Date(sess.cookie.expires) @@ -175,7 +216,7 @@ function getSession(sessionId) { // destroy expired session if (expires && expires <= Date.now()) { - delete this.sessions[sessionId] + this.destroy(sessionId) return } diff --git a/test/session.js b/test/session.js index b88e047a..9beaf224 100644 --- a/test/session.js +++ b/test/session.js @@ -1878,6 +1878,68 @@ describe('session()', function(){ }) }) +describe('memoryStore', function(done){ + + it('should only contain 10 items', function(done){ + var store = new session.MemoryStore({cacheLimit: 10}) + + for (var i = 0; i < 15; i++){ + store.set(i, {cookie: { expires: new Date((new Date()).valueOf() + 60*10*1000) }}) + } + + store.length(function (err, length) { + if (err) return done(err) + assert.equal(length, 10) + done() + }) + }) + + it('delete the first item', function(){ + var store = new session.MemoryStore({cacheLimit: 10}) + + for (var i = 0; i < 15; i++){ + store.set(i, {cookie: { expires: new Date((new Date()).valueOf() + 60*10*1000) }}) + } + + store.destroy(14) + + store.length(function (err, length) { + if (err) return done(err) + assert.equal(length, 9) + done + }) + }) + + it('delete the last item', function(){ + var store = new session.MemoryStore({cacheLimit: 10}) + + for (var i = 0; i < 10; i++){ + store.set(i, {cookie: { expires: new Date((new Date()).valueOf() + 60*10*1000) }}) + } + + store.destroy(0) + store.destroy(1) + + store.length(function (err, length) { + if (err) return done(err) + assert.equal(length, 8) + done + }) + + for (var i = 10; i < 12; i++){ + store.set(i, {cookie: { expires: new Date((new Date()).valueOf() + 60*10*1000) }}) + } + + store.length(function (err, length) { + if (err) return done(err) + assert.equal(length, 10) + done + }) + }) + + +}) + function cookie(res) { var setCookie = res.headers['set-cookie']; return (setCookie && setCookie[0]) || undefined;