Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 22 additions & 16 deletions internal/lrucache.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,46 @@
'use strict'

// Two generations, with no reordering on read. A hit in the old generation is
// promoted back into the live one. When the live generation fills up it becomes
// the old one and the previous old generation is dropped, so at most 2 * max
// entries are retained.
class LRUCache {
constructor () {
this.max = 1000
this.map = new Map()
this.old = new Map()
}

get (key) {
const value = this.map.get(key)
if (value === undefined) {
return undefined
} else {
// Remove the key from the map and add it to the end
this.map.delete(key)
this.map.set(key, value)
if (value !== undefined) {
return value
}
const stale = this.old.get(key)
if (stale !== undefined) {
this.set(key, stale)
return stale
}
return undefined
}

delete (key) {
return this.map.delete(key)
const inMap = this.map.delete(key)
const inOld = this.old.delete(key)
return inMap || inOld
}

set (key, value) {
const deleted = this.delete(key)

if (!deleted && value !== undefined) {
// If cache is full, delete the least recently used item
if (this.map.size >= this.max) {
const firstKey = this.map.keys().next().value
this.delete(firstKey)
}
if (value === undefined) {
return this
}

this.map.set(key, value)
if (!this.map.has(key) && this.map.size >= this.max) {
this.old = this.map
this.map = new Map()
}

this.map.set(key, value)
return this
}
}
Expand Down
53 changes: 51 additions & 2 deletions test/internal/lrucache.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,57 @@ test('basic cache operation', t => {
t.equal(c.get(i), i)
}
c.set(1001, 1001)
// lru item should be gone
t.equal(c.get(1001), 1001)
// setting undefined is a no-op and does not clobber an existing value
t.equal(c.set(42, undefined), c)
t.equal(c.get(42), 42)
t.equal(c.get('not-in-the-cache'), undefined)
t.end()
})

test('delete removes the key from both generations', t => {
const c = new LRUCache()
const max = 1000

for (let i = 0; i < max; i++) {
c.set(i, i)
}
// fills the live generation, demoting everything above into the old one
c.set('rotate', 1)
c.set(7, 'live')

t.equal(c.delete(7), true)
t.equal(c.get(7), undefined)
t.equal(c.delete(7), false)
t.end()
})

test('entries are evicted once the cache is full', t => {
const c = new LRUCache()

for (let i = 0; i < 10 * c.max; i++) {
c.set(i, i)
}
t.equal(c.get(0), undefined)
c.set(42, undefined)
t.ok(c.map.size + c.old.size <= 2 * c.max)
t.end()
})

test('promoting from the old generation stays within the bound', t => {
const c = new LRUCache()
const max = c.max

for (let i = 0; i < max; i++) {
c.set(`a${i}`, i)
}
c.set('rotate', 1)
for (let i = 0; i < max - 2; i++) {
c.set(`b${i}`, i)
}
for (let i = 0; i < max; i++) {
c.get(`a${i}`)
}

t.ok(c.map.size + c.old.size <= 2 * max)
t.end()
})