index.js 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. 'use strict'
  2. module.exports = LRUCache
  3. // This will be a proper iterable 'Map' in engines that support it,
  4. // or a fakey-fake PseudoMap in older versions.
  5. var Map = require('pseudomap')
  6. var util = require('util')
  7. // A linked list to keep track of recently-used-ness
  8. var Yallist = require('yallist')
  9. // use symbols if possible, otherwise just _props
  10. var hasSymbol = typeof Symbol === 'function'
  11. var makeSymbol
  12. if (hasSymbol) {
  13. makeSymbol = function (key) {
  14. return Symbol(key)
  15. }
  16. } else {
  17. makeSymbol = function (key) {
  18. return '_' + key
  19. }
  20. }
  21. var MAX = makeSymbol('max')
  22. var LENGTH = makeSymbol('length')
  23. var LENGTH_CALCULATOR = makeSymbol('lengthCalculator')
  24. var ALLOW_STALE = makeSymbol('allowStale')
  25. var MAX_AGE = makeSymbol('maxAge')
  26. var DISPOSE = makeSymbol('dispose')
  27. var NO_DISPOSE_ON_SET = makeSymbol('noDisposeOnSet')
  28. var LRU_LIST = makeSymbol('lruList')
  29. var CACHE = makeSymbol('cache')
  30. function naiveLength () { return 1 }
  31. // lruList is a yallist where the head is the youngest
  32. // item, and the tail is the oldest. the list contains the Hit
  33. // objects as the entries.
  34. // Each Hit object has a reference to its Yallist.Node. This
  35. // never changes.
  36. //
  37. // cache is a Map (or PseudoMap) that matches the keys to
  38. // the Yallist.Node object.
  39. function LRUCache (options) {
  40. if (!(this instanceof LRUCache)) {
  41. return new LRUCache(options)
  42. }
  43. if (typeof options === 'number') {
  44. options = { max: options }
  45. }
  46. if (!options) {
  47. options = {}
  48. }
  49. var max = this[MAX] = options.max
  50. // Kind of weird to have a default max of Infinity, but oh well.
  51. if (!max ||
  52. !(typeof max === 'number') ||
  53. max <= 0) {
  54. this[MAX] = Infinity
  55. }
  56. var lc = options.length || naiveLength
  57. if (typeof lc !== 'function') {
  58. lc = naiveLength
  59. }
  60. this[LENGTH_CALCULATOR] = lc
  61. this[ALLOW_STALE] = options.stale || false
  62. this[MAX_AGE] = options.maxAge || 0
  63. this[DISPOSE] = options.dispose
  64. this[NO_DISPOSE_ON_SET] = options.noDisposeOnSet || false
  65. this.reset()
  66. }
  67. // resize the cache when the max changes.
  68. Object.defineProperty(LRUCache.prototype, 'max', {
  69. set: function (mL) {
  70. if (!mL || !(typeof mL === 'number') || mL <= 0) {
  71. mL = Infinity
  72. }
  73. this[MAX] = mL
  74. trim(this)
  75. },
  76. get: function () {
  77. return this[MAX]
  78. },
  79. enumerable: true
  80. })
  81. Object.defineProperty(LRUCache.prototype, 'allowStale', {
  82. set: function (allowStale) {
  83. this[ALLOW_STALE] = !!allowStale
  84. },
  85. get: function () {
  86. return this[ALLOW_STALE]
  87. },
  88. enumerable: true
  89. })
  90. Object.defineProperty(LRUCache.prototype, 'maxAge', {
  91. set: function (mA) {
  92. if (!mA || !(typeof mA === 'number') || mA < 0) {
  93. mA = 0
  94. }
  95. this[MAX_AGE] = mA
  96. trim(this)
  97. },
  98. get: function () {
  99. return this[MAX_AGE]
  100. },
  101. enumerable: true
  102. })
  103. // resize the cache when the lengthCalculator changes.
  104. Object.defineProperty(LRUCache.prototype, 'lengthCalculator', {
  105. set: function (lC) {
  106. if (typeof lC !== 'function') {
  107. lC = naiveLength
  108. }
  109. if (lC !== this[LENGTH_CALCULATOR]) {
  110. this[LENGTH_CALCULATOR] = lC
  111. this[LENGTH] = 0
  112. this[LRU_LIST].forEach(function (hit) {
  113. hit.length = this[LENGTH_CALCULATOR](hit.value, hit.key)
  114. this[LENGTH] += hit.length
  115. }, this)
  116. }
  117. trim(this)
  118. },
  119. get: function () { return this[LENGTH_CALCULATOR] },
  120. enumerable: true
  121. })
  122. Object.defineProperty(LRUCache.prototype, 'length', {
  123. get: function () { return this[LENGTH] },
  124. enumerable: true
  125. })
  126. Object.defineProperty(LRUCache.prototype, 'itemCount', {
  127. get: function () { return this[LRU_LIST].length },
  128. enumerable: true
  129. })
  130. LRUCache.prototype.rforEach = function (fn, thisp) {
  131. thisp = thisp || this
  132. for (var walker = this[LRU_LIST].tail; walker !== null;) {
  133. var prev = walker.prev
  134. forEachStep(this, fn, walker, thisp)
  135. walker = prev
  136. }
  137. }
  138. function forEachStep (self, fn, node, thisp) {
  139. var hit = node.value
  140. if (isStale(self, hit)) {
  141. del(self, node)
  142. if (!self[ALLOW_STALE]) {
  143. hit = undefined
  144. }
  145. }
  146. if (hit) {
  147. fn.call(thisp, hit.value, hit.key, self)
  148. }
  149. }
  150. LRUCache.prototype.forEach = function (fn, thisp) {
  151. thisp = thisp || this
  152. for (var walker = this[LRU_LIST].head; walker !== null;) {
  153. var next = walker.next
  154. forEachStep(this, fn, walker, thisp)
  155. walker = next
  156. }
  157. }
  158. LRUCache.prototype.keys = function () {
  159. return this[LRU_LIST].toArray().map(function (k) {
  160. return k.key
  161. }, this)
  162. }
  163. LRUCache.prototype.values = function () {
  164. return this[LRU_LIST].toArray().map(function (k) {
  165. return k.value
  166. }, this)
  167. }
  168. LRUCache.prototype.reset = function () {
  169. if (this[DISPOSE] &&
  170. this[LRU_LIST] &&
  171. this[LRU_LIST].length) {
  172. this[LRU_LIST].forEach(function (hit) {
  173. this[DISPOSE](hit.key, hit.value)
  174. }, this)
  175. }
  176. this[CACHE] = new Map() // hash of items by key
  177. this[LRU_LIST] = new Yallist() // list of items in order of use recency
  178. this[LENGTH] = 0 // length of items in the list
  179. }
  180. LRUCache.prototype.dump = function () {
  181. return this[LRU_LIST].map(function (hit) {
  182. if (!isStale(this, hit)) {
  183. return {
  184. k: hit.key,
  185. v: hit.value,
  186. e: hit.now + (hit.maxAge || 0)
  187. }
  188. }
  189. }, this).toArray().filter(function (h) {
  190. return h
  191. })
  192. }
  193. LRUCache.prototype.dumpLru = function () {
  194. return this[LRU_LIST]
  195. }
  196. LRUCache.prototype.inspect = function (n, opts) {
  197. var str = 'LRUCache {'
  198. var extras = false
  199. var as = this[ALLOW_STALE]
  200. if (as) {
  201. str += '\n allowStale: true'
  202. extras = true
  203. }
  204. var max = this[MAX]
  205. if (max && max !== Infinity) {
  206. if (extras) {
  207. str += ','
  208. }
  209. str += '\n max: ' + util.inspect(max, opts)
  210. extras = true
  211. }
  212. var maxAge = this[MAX_AGE]
  213. if (maxAge) {
  214. if (extras) {
  215. str += ','
  216. }
  217. str += '\n maxAge: ' + util.inspect(maxAge, opts)
  218. extras = true
  219. }
  220. var lc = this[LENGTH_CALCULATOR]
  221. if (lc && lc !== naiveLength) {
  222. if (extras) {
  223. str += ','
  224. }
  225. str += '\n length: ' + util.inspect(this[LENGTH], opts)
  226. extras = true
  227. }
  228. var didFirst = false
  229. this[LRU_LIST].forEach(function (item) {
  230. if (didFirst) {
  231. str += ',\n '
  232. } else {
  233. if (extras) {
  234. str += ',\n'
  235. }
  236. didFirst = true
  237. str += '\n '
  238. }
  239. var key = util.inspect(item.key).split('\n').join('\n ')
  240. var val = { value: item.value }
  241. if (item.maxAge !== maxAge) {
  242. val.maxAge = item.maxAge
  243. }
  244. if (lc !== naiveLength) {
  245. val.length = item.length
  246. }
  247. if (isStale(this, item)) {
  248. val.stale = true
  249. }
  250. val = util.inspect(val, opts).split('\n').join('\n ')
  251. str += key + ' => ' + val
  252. })
  253. if (didFirst || extras) {
  254. str += '\n'
  255. }
  256. str += '}'
  257. return str
  258. }
  259. LRUCache.prototype.set = function (key, value, maxAge) {
  260. maxAge = maxAge || this[MAX_AGE]
  261. var now = maxAge ? Date.now() : 0
  262. var len = this[LENGTH_CALCULATOR](value, key)
  263. if (this[CACHE].has(key)) {
  264. if (len > this[MAX]) {
  265. del(this, this[CACHE].get(key))
  266. return false
  267. }
  268. var node = this[CACHE].get(key)
  269. var item = node.value
  270. // dispose of the old one before overwriting
  271. // split out into 2 ifs for better coverage tracking
  272. if (this[DISPOSE]) {
  273. if (!this[NO_DISPOSE_ON_SET]) {
  274. this[DISPOSE](key, item.value)
  275. }
  276. }
  277. item.now = now
  278. item.maxAge = maxAge
  279. item.value = value
  280. this[LENGTH] += len - item.length
  281. item.length = len
  282. this.get(key)
  283. trim(this)
  284. return true
  285. }
  286. var hit = new Entry(key, value, len, now, maxAge)
  287. // oversized objects fall out of cache automatically.
  288. if (hit.length > this[MAX]) {
  289. if (this[DISPOSE]) {
  290. this[DISPOSE](key, value)
  291. }
  292. return false
  293. }
  294. this[LENGTH] += hit.length
  295. this[LRU_LIST].unshift(hit)
  296. this[CACHE].set(key, this[LRU_LIST].head)
  297. trim(this)
  298. return true
  299. }
  300. LRUCache.prototype.has = function (key) {
  301. if (!this[CACHE].has(key)) return false
  302. var hit = this[CACHE].get(key).value
  303. if (isStale(this, hit)) {
  304. return false
  305. }
  306. return true
  307. }
  308. LRUCache.prototype.get = function (key) {
  309. return get(this, key, true)
  310. }
  311. LRUCache.prototype.peek = function (key) {
  312. return get(this, key, false)
  313. }
  314. LRUCache.prototype.pop = function () {
  315. var node = this[LRU_LIST].tail
  316. if (!node) return null
  317. del(this, node)
  318. return node.value
  319. }
  320. LRUCache.prototype.del = function (key) {
  321. del(this, this[CACHE].get(key))
  322. }
  323. LRUCache.prototype.load = function (arr) {
  324. // reset the cache
  325. this.reset()
  326. var now = Date.now()
  327. // A previous serialized cache has the most recent items first
  328. for (var l = arr.length - 1; l >= 0; l--) {
  329. var hit = arr[l]
  330. var expiresAt = hit.e || 0
  331. if (expiresAt === 0) {
  332. // the item was created without expiration in a non aged cache
  333. this.set(hit.k, hit.v)
  334. } else {
  335. var maxAge = expiresAt - now
  336. // dont add already expired items
  337. if (maxAge > 0) {
  338. this.set(hit.k, hit.v, maxAge)
  339. }
  340. }
  341. }
  342. }
  343. LRUCache.prototype.prune = function () {
  344. var self = this
  345. this[CACHE].forEach(function (value, key) {
  346. get(self, key, false)
  347. })
  348. }
  349. function get (self, key, doUse) {
  350. var node = self[CACHE].get(key)
  351. if (node) {
  352. var hit = node.value
  353. if (isStale(self, hit)) {
  354. del(self, node)
  355. if (!self[ALLOW_STALE]) hit = undefined
  356. } else {
  357. if (doUse) {
  358. self[LRU_LIST].unshiftNode(node)
  359. }
  360. }
  361. if (hit) hit = hit.value
  362. }
  363. return hit
  364. }
  365. function isStale (self, hit) {
  366. if (!hit || (!hit.maxAge && !self[MAX_AGE])) {
  367. return false
  368. }
  369. var stale = false
  370. var diff = Date.now() - hit.now
  371. if (hit.maxAge) {
  372. stale = diff > hit.maxAge
  373. } else {
  374. stale = self[MAX_AGE] && (diff > self[MAX_AGE])
  375. }
  376. return stale
  377. }
  378. function trim (self) {
  379. if (self[LENGTH] > self[MAX]) {
  380. for (var walker = self[LRU_LIST].tail;
  381. self[LENGTH] > self[MAX] && walker !== null;) {
  382. // We know that we're about to delete this one, and also
  383. // what the next least recently used key will be, so just
  384. // go ahead and set it now.
  385. var prev = walker.prev
  386. del(self, walker)
  387. walker = prev
  388. }
  389. }
  390. }
  391. function del (self, node) {
  392. if (node) {
  393. var hit = node.value
  394. if (self[DISPOSE]) {
  395. self[DISPOSE](hit.key, hit.value)
  396. }
  397. self[LENGTH] -= hit.length
  398. self[CACHE].delete(hit.key)
  399. self[LRU_LIST].removeNode(node)
  400. }
  401. }
  402. // classy, since V8 prefers predictable objects.
  403. function Entry (key, value, length, now, maxAge) {
  404. this.key = key
  405. this.value = value
  406. this.length = length
  407. this.now = now
  408. this.maxAge = maxAge || 0
  409. }