connect-redis.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. /*!
  2. * Connect - Redis
  3. * Copyright(c) 2012 TJ Holowaychuk <tj@vision-media.ca>
  4. * MIT Licensed
  5. */
  6. var debug = require('debug')('connect:redis');
  7. var redis = require('redis');
  8. var util = require('util');
  9. var noop = function(){};
  10. /**
  11. * One day in seconds.
  12. */
  13. var oneDay = 86400;
  14. function getTTL(store, sess, sid) {
  15. if (typeof store.ttl === 'number' || typeof store.ttl === 'string') return store.ttl;
  16. if (typeof store.ttl === 'function') return store.ttl(store, sess, sid);
  17. if (store.ttl) throw new TypeError('`store.ttl` must be a number or function.');
  18. var maxAge = sess.cookie.maxAge;
  19. return (typeof maxAge === 'number'
  20. ? Math.floor(maxAge / 1000)
  21. : oneDay);
  22. }
  23. /**
  24. * Return the `RedisStore` extending `express`'s session Store.
  25. *
  26. * @param {object} express session
  27. * @return {Function}
  28. * @api public
  29. */
  30. module.exports = function (session) {
  31. /**
  32. * Express's session Store.
  33. */
  34. var Store = session.Store;
  35. /**
  36. * Initialize RedisStore with the given `options`.
  37. *
  38. * @param {Object} options
  39. * @api public
  40. */
  41. function RedisStore (options) {
  42. if (!(this instanceof RedisStore)) {
  43. throw new TypeError('Cannot call RedisStore constructor as a function');
  44. }
  45. var self = this;
  46. options = options || {};
  47. Store.call(this, options);
  48. this.prefix = options.prefix == null
  49. ? 'sess:'
  50. : options.prefix;
  51. delete options.prefix;
  52. this.scanCount = Number(options.scanCount) || 100;
  53. delete options.scanCount;
  54. this.serializer = options.serializer || JSON;
  55. if (options.url) {
  56. options.socket = options.url;
  57. }
  58. // convert to redis connect params
  59. if (options.client) {
  60. this.client = options.client;
  61. }
  62. else if (options.socket) {
  63. this.client = redis.createClient(options.socket, options);
  64. }
  65. else {
  66. this.client = redis.createClient(options);
  67. }
  68. // logErrors
  69. if(options.logErrors){
  70. // if options.logErrors is function, allow it to override. else provide default logger. useful for large scale deployment
  71. // which may need to write to a distributed log
  72. if(typeof options.logErrors != 'function'){
  73. options.logErrors = function (err) {
  74. console.error('Warning: connect-redis reported a client error: ' + err);
  75. };
  76. }
  77. this.client.on('error', options.logErrors);
  78. }
  79. if (options.pass) {
  80. this.client.auth(options.pass, function (err) {
  81. if (err) {
  82. throw err;
  83. }
  84. });
  85. }
  86. this.ttl = options.ttl;
  87. this.disableTTL = options.disableTTL;
  88. if (options.unref) this.client.unref();
  89. if ('db' in options) {
  90. if (typeof options.db !== 'number') {
  91. console.error('Warning: connect-redis expects a number for the "db" option');
  92. }
  93. self.client.select(options.db);
  94. self.client.on('connect', function () {
  95. self.client.select(options.db);
  96. });
  97. }
  98. self.client.on('error', function (er) {
  99. debug('Redis returned err', er);
  100. self.emit('disconnect', er);
  101. });
  102. self.client.on('connect', function () {
  103. self.emit('connect');
  104. });
  105. }
  106. /**
  107. * Inherit from `Store`.
  108. */
  109. util.inherits(RedisStore, Store);
  110. /**
  111. * Attempt to fetch session by the given `sid`.
  112. *
  113. * @param {String} sid
  114. * @param {Function} fn
  115. * @api public
  116. */
  117. RedisStore.prototype.get = function (sid, fn) {
  118. var store = this;
  119. var psid = store.prefix + sid;
  120. if (!fn) fn = noop;
  121. debug('GET "%s"', sid);
  122. store.client.get(psid, function (er, data) {
  123. if (er) return fn(er);
  124. if (!data) return fn();
  125. var result;
  126. data = data.toString();
  127. debug('GOT %s', data);
  128. try {
  129. result = store.serializer.parse(data);
  130. }
  131. catch (er) {
  132. return fn(er);
  133. }
  134. return fn(null, result);
  135. });
  136. };
  137. /**
  138. * Commit the given `sess` object associated with the given `sid`.
  139. *
  140. * @param {String} sid
  141. * @param {Session} sess
  142. * @param {Function} fn
  143. * @api public
  144. */
  145. RedisStore.prototype.set = function (sid, sess, fn) {
  146. var store = this;
  147. var args = [store.prefix + sid];
  148. if (!fn) fn = noop;
  149. try {
  150. var jsess = store.serializer.stringify(sess);
  151. }
  152. catch (er) {
  153. return fn(er);
  154. }
  155. args.push(jsess);
  156. if (!store.disableTTL) {
  157. var ttl = getTTL(store, sess, sid);
  158. args.push('EX', ttl);
  159. debug('SET "%s" %s ttl:%s', sid, jsess, ttl);
  160. } else {
  161. debug('SET "%s" %s', sid, jsess);
  162. }
  163. store.client.set(args, function (er) {
  164. if (er) return fn(er);
  165. debug('SET complete');
  166. fn.apply(null, arguments);
  167. });
  168. };
  169. /**
  170. * Destroy the session associated with the given `sid`.
  171. *
  172. * @param {String} sid
  173. * @api public
  174. */
  175. RedisStore.prototype.destroy = function (sid, fn) {
  176. debug('DEL "%s"', sid);
  177. if (Array.isArray(sid)) {
  178. var multi = this.client.multi();
  179. var prefix = this.prefix;
  180. sid.forEach(function (s) {
  181. multi.del(prefix + s);
  182. });
  183. multi.exec(fn);
  184. } else {
  185. sid = this.prefix + sid;
  186. this.client.del(sid, fn);
  187. }
  188. };
  189. /**
  190. * Refresh the time-to-live for the session with the given `sid`.
  191. *
  192. * @param {String} sid
  193. * @param {Session} sess
  194. * @param {Function} fn
  195. * @api public
  196. */
  197. RedisStore.prototype.touch = function (sid, sess, fn) {
  198. var store = this;
  199. var psid = store.prefix + sid;
  200. if (!fn) fn = noop;
  201. if (store.disableTTL) return fn();
  202. var ttl = getTTL(store, sess);
  203. debug('EXPIRE "%s" ttl:%s', sid, ttl);
  204. store.client.expire(psid, ttl, function (er) {
  205. if (er) return fn(er);
  206. debug('EXPIRE complete');
  207. fn.apply(this, arguments);
  208. });
  209. };
  210. /**
  211. * Fetch all sessions' Redis keys using non-blocking SCAN command
  212. *
  213. * @param {Function} fn
  214. * @api private
  215. */
  216. function allKeys (store, cb) {
  217. var keysObj = {}; // Use an object to dedupe as scan can return duplicates
  218. var pattern = store.prefix + '*';
  219. var scanCount = store.scanCount;
  220. debug('SCAN "%s"', pattern);
  221. (function nextBatch (cursorId) {
  222. store.client.scan(cursorId, 'match', pattern, 'count', scanCount, function (err, result) {
  223. if (err) return cb(err);
  224. var nextCursorId = result[0];
  225. var keys = result[1];
  226. debug('SCAN complete (next cursor = "%s")', nextCursorId);
  227. keys.forEach(function (key) {
  228. keysObj[key] = 1;
  229. });
  230. if (nextCursorId != 0) {
  231. // next batch
  232. return nextBatch(nextCursorId);
  233. }
  234. // end of cursor
  235. return cb(null, Object.keys(keysObj));
  236. });
  237. })(0);
  238. }
  239. /**
  240. * Fetch all sessions' ids
  241. *
  242. * @param {Function} fn
  243. * @api public
  244. */
  245. RedisStore.prototype.ids = function (fn) {
  246. var store = this;
  247. var prefixLength = store.prefix.length;
  248. if (!fn) fn = noop;
  249. allKeys(store, function (err, keys) {
  250. if (err) return fn(err);
  251. keys = keys.map(function (key) {
  252. return key.substr(prefixLength);
  253. });
  254. return fn(null, keys);
  255. });
  256. };
  257. /**
  258. * Fetch all sessions
  259. *
  260. * @param {Function} fn
  261. * @api public
  262. */
  263. RedisStore.prototype.all = function (fn) {
  264. var store = this;
  265. var prefixLength = store.prefix.length;
  266. if (!fn) fn = noop;
  267. allKeys(store, function (err, keys) {
  268. if (err) return fn(err);
  269. if (keys.length === 0) return fn(null,[]);
  270. store.client.mget(keys, function (err, sessions) {
  271. if (err) return fn(err);
  272. var result;
  273. try {
  274. result = sessions.map(function (data, index) {
  275. data = data.toString();
  276. data = store.serializer.parse(data);
  277. data.id = keys[index].substr(prefixLength);
  278. return data;
  279. });
  280. } catch (e) {
  281. err = e;
  282. }
  283. return fn(err, result);
  284. });
  285. });
  286. };
  287. return RedisStore;
  288. };