Source: lru.js

  1. /**
  2. * A doubly linked list-based Least Recently Used (LRU) cache. Will keep most
  3. * recently used items while discarding least recently used items when its limit
  4. * is reached.
  5. *
  6. * Licensed under MIT. Copyright (c) 2010 Rasmus Andersson <http://hunch.se/>
  7. * See README.md for details.
  8. *
  9. * Illustration of the design:
  10. *
  11. * entry entry entry entry
  12. * ______ ______ ______ ______
  13. * | head |.newer => | |.newer => | |.newer => | tail |
  14. * | A | | B | | C | | D |
  15. * |______| <= older.|______| <= older.|______| <= older.|______|
  16. *
  17. * removed <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- added
  18. */
  19. function LRUCache (limit) {
  20. // Current size of the cache. (Read-only).
  21. this.size = 0;
  22. // Maximum number of items this cache can hold.
  23. this.limit = limit;
  24. this._keymap = {};
  25. }
  26. /**
  27. * Put <value> into the cache associated with <key>. Returns the entry which was
  28. * removed to make room for the new entry. Otherwise undefined is returned
  29. * (i.e. if there was enough room already).
  30. */
  31. LRUCache.prototype.put = function(key, value) {
  32. var entry = {key:key, value:value};
  33. // Note: No protection agains replacing, and thus orphan entries. By design.
  34. this._keymap[key] = entry;
  35. if (this.tail) {
  36. // link previous tail to the new tail (entry)
  37. this.tail.newer = entry;
  38. entry.older = this.tail;
  39. } else {
  40. // we're first in -- yay
  41. this.head = entry;
  42. }
  43. // add new entry to the end of the linked list -- it's now the freshest entry.
  44. this.tail = entry;
  45. if (this.size === this.limit) {
  46. // we hit the limit -- remove the head
  47. return this.shift();
  48. } else {
  49. // increase the size counter
  50. this.size++;
  51. }
  52. return null;
  53. };
  54. /**
  55. * Purge the least recently used (oldest) entry from the cache. Returns the
  56. * removed entry or undefined if the cache was empty.
  57. *
  58. * If you need to perform any form of finalization of purged items, this is a
  59. * good place to do it. Simply override/replace this function:
  60. *
  61. * var c = new LRUCache(123);
  62. * c.shift = function() {
  63. * var entry = LRUCache.prototype.shift.call(this);
  64. * doSomethingWith(entry);
  65. * return entry;
  66. * }
  67. */
  68. LRUCache.prototype.shift = function() {
  69. // todo: handle special case when limit == 1
  70. var entry = this.head;
  71. if (entry) {
  72. if (this.head.newer) {
  73. this.head = this.head.newer;
  74. this.head.older = undefined;
  75. } else {
  76. this.head = undefined;
  77. }
  78. // Remove last strong reference to <entry> and remove links from the purged
  79. // entry being returned:
  80. entry.newer = entry.older = undefined;
  81. // delete is slow, but we need to do this to avoid uncontrollable growth:
  82. delete this._keymap[entry.key];
  83. }
  84. return entry;
  85. };
  86. /**
  87. * Get and register recent use of <key>. Returns the value associated with <key>
  88. * or undefined if not in cache.
  89. */
  90. LRUCache.prototype.get = function(key, returnEntry) {
  91. // First, find our cache entry
  92. var entry = this._keymap[key];
  93. if (entry === undefined) return null; // Not cached. Sorry.
  94. // As <key> was found in the cache, register it as being requested recently
  95. if (entry === this.tail) {
  96. // Already the most recenlty used entry, so no need to update the list
  97. return returnEntry ? entry : entry.value;
  98. }
  99. // HEAD--------------TAIL
  100. // <.older .newer>
  101. // <--- add direction --
  102. // A B C <D> E
  103. if (entry.newer) {
  104. if (entry === this.head)
  105. this.head = entry.newer;
  106. entry.newer.older = entry.older; // C <-- E.
  107. }
  108. if (entry.older)
  109. entry.older.newer = entry.newer; // C. --> E
  110. entry.newer = undefined; // D --x
  111. entry.older = this.tail; // D. --> E
  112. if (this.tail)
  113. this.tail.newer = entry; // E. <-- D
  114. this.tail = entry;
  115. return returnEntry ? entry : entry.value;
  116. };
  117. // ----------------------------------------------------------------------------
  118. // Following code is optional and can be removed without breaking the core
  119. // functionality.
  120. /**
  121. * Check if <key> is in the cache without registering recent use. Feasible if
  122. * you do not want to chage the state of the cache, but only "peek" at it.
  123. * Returns the entry associated with <key> if found, or undefined if not found.
  124. */
  125. LRUCache.prototype.find = function(key) {
  126. return this._keymap[key];
  127. };
  128. /**
  129. * Update the value of entry with <key>. Returns the old value, or undefined if
  130. * entry was not in the cache.
  131. */
  132. LRUCache.prototype.set = function(key, value) {
  133. var oldvalue, entry = this.get(key, true);
  134. if (entry) {
  135. oldvalue = entry.value;
  136. entry.value = value;
  137. } else {
  138. oldvalue = this.put(key, value);
  139. if (oldvalue) oldvalue = oldvalue.value;
  140. }
  141. return oldvalue;
  142. };
  143. /**
  144. * Remove entry <key> from cache and return its value. Returns undefined if not
  145. * found.
  146. */
  147. LRUCache.prototype.remove = function(key) {
  148. var entry = this._keymap[key];
  149. if (!entry) return null;
  150. delete this._keymap[entry.key]; // need to do delete unfortunately
  151. if (entry.newer && entry.older) {
  152. // relink the older entry with the newer entry
  153. entry.older.newer = entry.newer;
  154. entry.newer.older = entry.older;
  155. } else if (entry.newer) {
  156. // remove the link to us
  157. entry.newer.older = undefined;
  158. // link the newer entry to head
  159. this.head = entry.newer;
  160. } else if (entry.older) {
  161. // remove the link to us
  162. entry.older.newer = undefined;
  163. // link the newer entry to head
  164. this.tail = entry.older;
  165. } else { // if(entry.older === undefined && entry.newer === undefined) {
  166. this.head = this.tail = undefined;
  167. }
  168. this.size--;
  169. return entry.value;
  170. };
  171. /** Removes all entries */
  172. LRUCache.prototype.removeAll = function() {
  173. // This should be safe, as we never expose strong refrences to the outside
  174. this.head = this.tail = undefined;
  175. this.size = 0;
  176. this._keymap = {};
  177. };
  178. /**
  179. * Return an array containing all keys of entries stored in the cache object, in
  180. * arbitrary order.
  181. */
  182. if (typeof Object.keys === 'function') {
  183. LRUCache.prototype.keys = function() { return Object.keys(this._keymap); };
  184. } else {
  185. LRUCache.prototype.keys = function() {
  186. var keys = [];
  187. for (var k in this._keymap) keys.push(k);
  188. return keys;
  189. };
  190. }
  191. /**
  192. * Call `fun` for each entry. Starting with the newest entry if `desc` is a true
  193. * value, otherwise starts with the oldest (head) enrty and moves towards the
  194. * tail.
  195. *
  196. * `fun` is called with 3 arguments in the context `context`:
  197. * `fun.call(context, Object key, Object value, LRUCache self)`
  198. */
  199. LRUCache.prototype.forEach = function(fun, context, desc) {
  200. var entry;
  201. if (context === true) { desc = true; context = undefined; }
  202. else if (typeof context !== 'object') context = this;
  203. if (desc) {
  204. entry = this.tail;
  205. while (entry) {
  206. fun.call(context, entry.key, entry.value, this);
  207. entry = entry.older;
  208. }
  209. } else {
  210. entry = this.head;
  211. while (entry) {
  212. fun.call(context, entry.key, entry.value, this);
  213. entry = entry.newer;
  214. }
  215. }
  216. };
  217. /** Returns a JSON (array) representation */
  218. LRUCache.prototype.toJSON = function() {
  219. var s = [], entry = this.head;
  220. while (entry) {
  221. s.push({key:entry.key.toJSON(), value:entry.value.toJSON()});
  222. entry = entry.newer;
  223. }
  224. return s;
  225. };
  226. /** Returns a String representation */
  227. LRUCache.prototype.toString = function() {
  228. var s = '', entry = this.head;
  229. while (entry) {
  230. s += String(entry.key)+':'+entry.value;
  231. entry = entry.newer;
  232. if (entry)
  233. s += ' < ';
  234. }
  235. return s;
  236. };
  237. module.exports = LRUCache;