<|start_filename|>package.json<|end_filename|>
{
  "name": "refcache",
  "version": "1.1.0",
  "description": "Small cache abstraction that auto GCs unref'ed objects after a max size has been reached",
  "main": "index.js",
  "dependencies": {
    "time-ordered-set": "^1.0.2"
  },
  "devDependencies": {
    "standard": "^14.3.4",
    "tape": "^5.0.1"
  },
  "scripts": {
    "test": "standard && tape test.js"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/mafintosh/refcache.git"
  },
  "author": "<NAME> (@mafintosh)",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/mafintosh/refcache/issues"
  },
  "homepage": "https://github.com/mafintosh/refcache"
}


<|start_filename|>example.js<|end_filename|>
const Nanocache = require('./')

const n = new Nanocache({
  maxSize: 10,
  open (key) {
    console.log('opening', key)
    return key
  },
  close (key) {
    console.log('closing', key)
  }
})

const checkout = n.checkout(Buffer.from('hi'))

n.checkout(Buffer.from('foo'), { weak: true })
checkout.checkin()

let i = 0
n.checkout(Buffer.from('hi' + i++))
n.checkout(Buffer.from('hi' + i++))
n.checkout(Buffer.from('hi' + i++))
n.checkout(Buffer.from('hi' + i++))
n.checkout(Buffer.from('hi' + i++))
n.checkout(Buffer.from('hi' + i++))
n.checkout(Buffer.from('hi' + i++))
n.checkout(Buffer.from('hi' + i++))
n.checkout(Buffer.from('hi' + i++))
n.checkout(Buffer.from('hi' + i++))
n.checkout(Buffer.from('hi' + i++))


<|start_filename|>test.js<|end_filename|>
const tape = require('tape')
const Refcache = require('./')

tape('basic', function (t) {
  t.plan(3)

  let opened = 0
  let closed = 0

  const rc = new Refcache({
    maxSize: 2,
    open () {
      return opened++
    },
    close (id) {
      t.same(id, 1)
      closed++
    }
  })

  const a = rc.checkout('a')
  const b = rc.checkout('b')
  const c = rc.checkout('c')

  b.checkin()
  a.checkin()
  c.checkin()

  t.same(opened, 3)
  t.same(closed, 1)
})

tape('force remove', function (t) {
  t.plan(2)

  let opened = 0
  let closed = 0

  const rc = new Refcache({
    maxSize: 2,
    open () {
      return opened++
    },
    close () {
      closed++
    }
  })

  const a = rc.checkout('a')
  const b = rc.checkout('b')
  const c = rc.checkout('c')

  b.remove()
  a.remove()
  c.remove()

  t.same(opened, 3)
  t.same(closed, 3)
})

tape('gc weak refs', function (t) {
  t.plan(4)

  let opened = 0
  let closed = 0

  const rc = new Refcache({
    maxSize: 2,
    open () {
      return opened++
    },
    close () {
      closed++
    }
  })

  rc.checkout('a')
  rc.checkout('b', { weak: true })
  rc.checkout('c', { weak: true })

  t.same(opened, 3)
  t.same(closed, 1)

  rc.checkout('d')

  t.same(opened, 4)
  t.same(closed, 2)
})

tape('multi lookup on same key', function (t) {
  let opened = 0
  let closed = 0

  const rc = new Refcache({
    maxSize: 2,
    open () {
      return opened++
    },
    close () {
      closed++
    }
  })

  const a = rc.checkout('a')
  const b = rc.checkout('a', { weak: true })
  rc.checkout('a', { weak: true })

  t.same(opened, 1)
  t.same(closed, 0)

  rc.checkout('b')
  rc.checkout('c')

  t.same(opened, 3)
  t.same(closed, 0)

  b.checkin()

  t.same(opened, 3)
  t.same(closed, 0)

  a.checkin()

  t.same(opened, 3)
  t.same(closed, 1)
  t.same(b.closed, true)

  t.end()
})


<|start_filename|>index.js<|end_filename|>
const TOS = require('time-ordered-set')

class Entry {
  constructor (id, val) {
    this.id = id
    this.value = val
    this.refs = 0
    this.closed = false
    this.next = null
    this.prev = null
  }
}

class Checkout {
  constructor (cache, entry, external) {
    this.entry = entry
    this.value = entry.value
    this.cache = cache
    this.external = external
  }

  get closed () {
    return this.entry.closed
  }

  bump () {
    if (this.entry.refs > 0) return
    this.cache.gcable.add(this.entry)
  }

  remove () {
    if (!this.cache.opened.has(this.entry.id)) return
    this.cache.gcable.remove(this.entry)
    this.cache.opened.delete(this.id)
    this.entry.closed = true
    this.cache.close(this.entry.value)
  }

  checkin () {
    if (this.external) {
      if (!--this.entry.refs) {
        this.cache.gcable.add(this.entry)
        this.cache.gc(false)
      }
    }
  }
}

module.exports = class Refcache {
  constructor ({ maxSize = 0, open, close }) {
    this.maxSize = maxSize
    this.open = open
    this.close = close
    this.opened = new Map()
    this.gcable = new TOS()
  }

  gc (force = false) {
    if (!this.gcable.length) return
    if (!force && this.opened.size <= this.maxSize) return
    const oldest = this.gcable.remove(this.gcable.oldest)
    this.opened.delete(oldest.id)
    oldest.closed = true
    this.close(oldest.value)
  }

  has (key) {
    const id = key.toString('hex')
    return this.opened.has(id)
  }

  checkout (key, opts = {}) {
    const id = key.toString('hex')
    const external = !opts.weak

    let entry = this.opened.get(id)
    if (!entry) {
      entry = new Entry(id, this.open(key, opts))
      this.opened.set(id, entry)
      this.gc(false)
    }

    if (external) entry.refs++

    if (entry.refs === 0) {
      this.gcable.add(entry)
    }

    return new Checkout(this, entry, external)
  }
}
