express-memfilestore
Version:
express-session full featured MemoryStore layer without leaks! Allows persisting sessions to disk.
362 lines (304 loc) • 8.58 kB
JavaScript
/* eslint-disable no-extra-parens */
/* eslint-disable consistent-this */
/* eslint-disable no-unused-expressions */
/*!
* Express-MemFileStore
* Copyright(c) 2020 Rocco Musolino <@roccomuso> (Developer of memorystore)
* Copyright(c) 2022 Nicholis du Toit <@TrueWinter> (Developer of Express-MemFileStore, a fork of memorystore)
* MIT Licensed
*/
var debug = require('debug')('MemFileStore');
var LRU = require('lru-cache');
var util = require('util');
var fs = require('fs');
/**
* One day in milliseconds.
*/
var oneDay = 86400000;
function getTTL (options, sess, sid) {
if (typeof options.ttl === 'number') return options.ttl;
if (typeof options.ttl === 'function') return options.ttl(options, sess, sid);
if (options.ttl) throw new TypeError('`options.ttl` must be a number or function.');
var maxAge = (sess && sess.cookie) ? (sess.cookie.maxAge || sess.cookie.originalMaxAge) : null;
return (typeof maxAge === 'number' ?
Math.floor(maxAge) : oneDay);
}
function prune (store) {
debug('Pruning expired entries');
store.forEach(function (value, key) {
store.get(key);
// The LRU cache will delete entries that are too old (TTL, not cookie expiry)
// (or when there are too many entries) when calling .get().
// So check if the entry exists before checking if it needs to be deleted.
if (store.has(key)) {
let date = new Date(JSON.parse(value).cookie.expires);
if (Date.now() > date.getTime()) {
debug(`Deleting ${key} as it expired ${Date.now() - date.getTime()}ms ago`);
store.del(key);
}
}
});
}
var defer = typeof setImmediate === 'function' ?
setImmediate :
function (fn) {
// eslint-disable-next-line prefer-spread
process.nextTick(fn.bind.apply(
// eslint-disable-next-line prefer-rest-params
fn, arguments
));
};
/**
* Return the `MemFileStore` extending `express`'s session Store.
*
* @param {object} express session
* @return {Function}
* @api public
*/
module.exports = function (session) {
/**
* Express's session Store.
*/
var Store = session.Store;
/**
* Initialize MemFileStore with the given `options`.
*
* @param {Object} options
* @api public
*/
function MemFileStore (options) {
if (!(this instanceof MemFileStore)) {
throw new TypeError('Cannot call MemFileStore constructor as a function');
}
options = options || {};
Store.call(this, options);
this.options = {};
this.options.checkPeriod = options.checkPeriod;
this.options.savePeriod = options.savePeriod;
this.options.saveFile = options.saveFile;
this.options.max = options.max || Infinity;
this.options.ttl = options.ttl;
this.options.dispose = options.dispose;
this.options.stale = options.stale;
this.options.noDisposeOnSet = options.noDisposeOnSet;
this.serializer = options.serializer || JSON;
this.store = LRU(this.options);
debug('Init MemFileStore');
if (fs.existsSync(this.options.saveFile)) {
let persistedData = JSON.parse(fs.readFileSync(this.options.saveFile, {
encoding: 'utf-8'
}));
for (let key in persistedData) {
this.store.set(key, this.serializer.stringify(persistedData[key]));
}
}
this.startInterval();
this.startSaveInterval();
}
/**
* Inherit from `Store`.
*/
util.inherits(MemFileStore, Store);
/**
* Attempt to fetch session by the given `sid`.
*
* @param {String} sid
* @param {Function} fn
* @api public
*/
MemFileStore.prototype.get = function (sid, fn) {
var store = this.store;
debug('GET "%s"', sid);
var data = store.get(sid);
if (!data) return fn();
debug('GOT %s', data);
var err = null;
var result;
try {
result = this.serializer.parse(data);
} catch (er) {
err = er;
}
fn && defer(fn, err, result);
};
/**
* Commit the given `sess` object associated with the given `sid`.
*
* @param {String} sid
* @param {Session} sess
* @param {Function} fn
* @api public
*/
MemFileStore.prototype.set = function (sid, sess, fn) {
var store = this.store;
var ttl = getTTL(this.options, sess, sid);
try {
var jsess = this.serializer.stringify(sess);
} catch (err) {
fn && defer(fn, err);
}
store.set(sid, jsess, ttl);
debug('SET "%s" %s ttl:%s', sid, jsess, ttl);
fn && defer(fn, null);
};
/**
* Destroy the session associated with the given `sid`.
*
* @param {String} sid
* @api public
*/
MemFileStore.prototype.destroy = function (sid, fn) {
var store = this.store;
if (Array.isArray(sid)) {
sid.forEach(function (s) {
debug('DEL "%s"', s);
store.del(s);
});
} else {
debug('DEL "%s"', sid);
store.del(sid);
}
fn && defer(fn, null);
};
/**
* Refresh the time-to-live for the session with the given `sid`.
*
* @param {String} sid
* @param {Session} sess
* @param {Function} fn
* @api public
*/
MemFileStore.prototype.touch = function (sid, sess, fn) {
var store = this.store;
var ttl = getTTL(this.options, sess, sid);
debug('EXPIRE "%s" ttl:%s', sid, ttl);
var err = null;
if (store.get(sid) !== undefined) {
try {
var s = this.serializer.parse(store.get(sid));
s.cookie = sess.cookie;
store.set(sid, this.serializer.stringify(s), ttl);
} catch (e) {
err = e;
}
}
fn && defer(fn, err);
};
/**
* Fetch all sessions' ids
*
* @param {Function} fn
* @api public
*/
MemFileStore.prototype.ids = function (fn) {
var store = this.store;
var Ids = store.keys();
debug('Getting IDs: %s', Ids);
fn && defer(fn, null, Ids);
};
/**
* Fetch all sessions
*
* @param {Function} fn
* @api public
*/
MemFileStore.prototype.all = function (fn) {
var store = this.store;
var self = this;
debug('Fetching all sessions');
var err = null;
var result = {};
try {
store.forEach(function (val, key) {
result[key] = self.serializer.parse(val);
});
} catch (e) {
err = e;
}
fn && defer(fn, err, result);
};
/**
* Delete all sessions from the store
*
* @param {Function} fn
* @api public
*/
MemFileStore.prototype.clear = function (fn) {
var store = this.store;
debug('delete all sessions from the store');
store.reset();
fn && defer(fn, null);
};
/**
* Get the count of all sessions in the store
*
* @param {Function} fn
* @api public
*/
MemFileStore.prototype.length = function (fn) {
var store = this.store;
debug('getting length', store.itemCount);
fn && defer(fn, null, store.itemCount);
};
/**
* Start the check interval
* @api public
*/
MemFileStore.prototype.startInterval = function () {
var self = this;
var ms = this.options.checkPeriod;
if (ms && typeof ms === 'number') {
clearInterval(this._checkInterval);
debug('starting periodic check for expired sessions');
this._checkInterval = setInterval(function () {
prune(self.store); // iterates over the entire cache proactively pruning old entries
}, Math.floor(ms)).unref();
}
};
/**
* Stop the check interval
* @api public
*/
MemFileStore.prototype.stopInterval = function () {
debug('stopping periodic check for expired sessions');
clearInterval(this._checkInterval);
};
/**
* Start the save interval
* @api public
*/
MemFileStore.prototype.startSaveInterval = function () {
var self = this;
var ms = this.options.savePeriod;
if (ms && typeof ms === 'number') {
clearInterval(this._saveInterval);
debug('starting periodic session saving');
this._saveInterval = setInterval(function () {
debug('Saving sessions to disk');
var results = {};
self.store.forEach(function (val, key) {
results[key] = self.serializer.parse(val);
});
fs.writeFileSync(self.options.saveFile, JSON.stringify(results), {
encoding: 'utf-8'
});
}, Math.floor(ms)).unref();
}
};
/**
* Stop the save interval
* @api public
*/
MemFileStore.prototype.stopSaveInterval = function () {
debug('stopping periodic session saving');
clearInterval(this._saveInterval);
};
/**
* Remove only expired entries from the store
* @api public
*/
MemFileStore.prototype.prune = function () {
prune(this.store);
};
return MemFileStore;
};