Skip to content

Commit

Permalink
implement ttl (aka maxAge)
Browse files Browse the repository at this point in the history
  • Loading branch information
isaacs committed Jan 20, 2022
1 parent 16e8687 commit 95a2423
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 14 deletions.
77 changes: 63 additions & 14 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,73 @@
// not a global until after the supported versions
// use Date.now() if not in a Node env or not available
const tryRequire = mod => {
try {
return require(mod)
} catch (e)
/* istanbul ignore next - no easy way to make require() throw */
{ return {} }
}
const { performance = Date } = tryRequire('perf_hooks')
const { now } = performance

class LRUEntry {
constructor (value, size) {
constructor (value, size, start, ttl) {
if (ttl) {
return new LRUEntryTTL(value, size, start, ttl)
}
this.value = value
this.size = size
}
}

class LRUEntryTTL extends LRUEntry {
constructor (value, size, start, ttl) {
super(value, size)
this.start = start
this.ttl = ttl
}
get age () {
return now() - this.start
}
get stale () {
return this.age > this.ttl
}
}

const asInt = n => ~~n
const asPosInt = n => typeof n === 'number' && n > 0 ? asInt(n) : null
const ifFunc = n => typeof n === 'function' ? n : null
const naiveLength = () => 1

class LRUCache {
constructor (options) {
if (!options || typeof options !== 'object') {
throw new Error('invalid options object')
}
const maxOk = options.max === asInt(options.max) &&
options.max > 0
const maxOk = options.max && (options.max === asPosInt(options.max))
if (!maxOk) {
throw new Error('options.max must be integer >0')
}
this.max = options.max
this.ttl = asPosInt(options.ttl)
this.allowStale = this.ttl && !!options.allowStale
this.updateAgeOnGet = this.ttl && !!options.updateAgeOnGet
this.old = new Map()
this.current = new Map()
this.oldSize = 0
this.currentSize = 0
this.sizeCalculation = ifFunc(options.sizeCalculation) ||
ifFunc(options.length) ||
naiveLength
ifFunc(options.length)
this.dispose = ifFunc(options.dispose)
}
get size () {
return this.oldSize + this.currentSize
}
set (key, value) {
const entry = new LRUEntry(value, this.sizeCalculation(value, key))
set (key, value, ttl = this.ttl) {
const { sizeCalculation } = this
const n = ttl ? now() : 0
const s = sizeCalculation ? sizeCalculation(value, key) : 1
const entry = new LRUEntry(value, s, n, ttl)

const replace = this.current.get(key)
this.currentSize += entry.size - (replace ? replace.size : 0)
const { dispose } = this
Expand All @@ -51,10 +85,24 @@ class LRUCache {
get (key) {
const fromCurrent = this.current.get(key)
if (fromCurrent) {
if (this.ttl && fromCurrent.stale) {
this.delete(key)
return this.allowStale ? fromCurrent.value : undefined
}
if (this.updateAgeOnGet) {
fromCurrent.start = now()
}
return fromCurrent.value
} else {
const fromOld = this.old.get(key)
if (fromOld) {
if (this.ttl && fromOld.stale) {
this.delete(key)
return this.allowStale ? fromOld.value : undefined
}
if (this.updateAgeOnGet) {
fromOld.start = now()
}
this.promote(key, fromOld)
return fromOld.value
}
Expand Down Expand Up @@ -82,14 +130,15 @@ class LRUCache {
}
}
has (key, updateRecency) {
if (this.current.has(key)) {
return true
const fromCurrent = this.current.get(key)
if (fromCurrent) {
return this.ttl ? !fromCurrent.stale : true
}
const oldHas = this.old.get(key)
if (oldHas && updateRecency) {
this.promote(key, oldHas)
const fromOld = this.old.get(key)
if (fromOld && updateRecency) {
this.promote(key, fromOld)
}
return !!oldHas
return !!fromOld && (this.ttl ? !fromOld.stale : true)
}
reset () {
this.swap()
Expand Down
138 changes: 138 additions & 0 deletions test/ttl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
const t = require('tap')

const clock = {
performance: {
now: () => clock.now,
},
now: 0,
advance: n => clock.now += n,
}

const runTests = (LRU, t) => {
t.test('ttl tests defaults', t => {
const c = new LRU({ max: 5, ttl: 10 })
c.set(1, 1)
t.equal(c.get(1), 1)
clock.advance(5)
t.equal(c.get(1), 1)
clock.advance(5)
t.equal(c.get(1), 1)
clock.advance(1)
t.equal(c.has(1), false)
t.equal(c.get(1), undefined)
t.equal(c.size, 0)

c.set(2, 2, 100)
clock.advance(50)
t.equal(c.has(2), true)
t.equal(c.get(2), 2)
clock.advance(51)
t.equal(c.has(2), false)
t.equal(c.get(2), undefined)

c.reset()
for (let i = 0; i < 9; i++) {
c.set(i, i)
}
// now we have 9 items
// get an expired item from old set
clock.advance(11)
t.equal(c.has(4), false)
t.equal(c.get(4), undefined)

t.end()
})

t.test('ttl with allowStale', t => {
const c = new LRU({ max: 5, ttl: 10, allowStale: true })
c.set(1, 1)
t.equal(c.get(1), 1)
clock.advance(5)
t.equal(c.get(1), 1)
clock.advance(5)
t.equal(c.get(1), 1)
clock.advance(1)
t.equal(c.has(1), false)
t.equal(c.get(1), 1)
t.equal(c.get(1), undefined)
t.equal(c.size, 0)

c.set(2, 2, 100)
clock.advance(50)
t.equal(c.has(2), true)
t.equal(c.get(2), 2)
clock.advance(51)
t.equal(c.has(2), false)
t.equal(c.get(2), 2)
t.equal(c.get(2), undefined)

c.reset()
for (let i = 0; i < 9; i++) {
c.set(i, i)
}
// now we have 9 items
// get an expired item from old set
clock.advance(11)
t.equal(c.has(4), false)
t.equal(c.get(4), 4)
t.equal(c.get(4), undefined)

t.end()
})

t.test('ttl with updateAgeOnGet', t => {
const c = new LRU({ max: 5, ttl: 10, updateAgeOnGet: true })
c.set(1, 1)
t.equal(c.get(1), 1)
clock.advance(5)
t.equal(c.get(1), 1)
clock.advance(5)
t.equal(c.get(1), 1)
clock.advance(1)
t.equal(c.has(1), true)
t.equal(c.get(1), 1)
t.equal(c.size, 1)
c.reset()

c.set(2, 2, 100)
for (let i = 0; i < 10; i++) {
clock.advance(50)
t.equal(c.has(2), true)
t.equal(c.get(2), 2)
}
clock.advance(101)
t.equal(c.has(2), false)
t.equal(c.get(2), undefined)

c.reset()
for (let i = 0; i < 9; i++) {
c.set(i, i)
}
// now we have 9 items
// get an expired item from old set
t.equal(c.has(3), true)
t.equal(c.get(3), 3)
clock.advance(11)
t.equal(c.has(4), false)
t.equal(c.get(4), undefined)

t.end()
})

t.end()
}

t.test('tests with perf_hooks.performance.now()', t => {
const LRU = t.mock('../', { perf_hooks: clock })
runTests(LRU, t)
})

t.test('tests using Date.now()', t => {
const Date_ = global.Date
t.teardown(() => global.Date = Date_)
global.Date = clock.performance
const LRU = t.mock('../', { perf_hooks: {} })
runTests(LRU, t)
})


0 comments on commit 95a2423

Please sign in to comment.