safe-memory-cache
Version:
Secure and size-limited in-memory cache for Node.js and browsers
128 lines (113 loc) • 3.86 kB
JavaScript
const { create, freeze, defineProperties } = Object
const { bind } = Function.prototype
const uncurryThis = bind.bind(bind.call)
const { setTimeout, setInterval, clearInterval } = globalThis
const arrayUnshift = uncurryThis(Array.prototype.unshift)
const arrayPush = uncurryThis(Array.prototype.push)
const arrayPop = uncurryThis(Array.prototype.pop)
const mapHas = uncurryThis(Map.prototype.has)
const mapGet = uncurryThis(Map.prototype.get)
const mapSet = uncurryThis(Map.prototype.set)
const private = new WeakMap()
const getPriv = private.get.bind(private)
const setPriv = private.set.bind(private)
function spawnBucket(buckets) {
arrayUnshift(buckets, new Map())
}
function rotateBuckets(buckets, rotationHook) {
let i = 0
while (i<buckets.length && buckets[i].size === 0) {
i++
}
if (i === buckets.length) {
// nothing to do, all buckets empty
return
}
const dropped = arrayPop(buckets)
spawnBucket(buckets)
if (rotationHook) {
rotationHook(dropped)
}
}
var bucketsProto = {
clear: function clear() {
const buckets = []
setPriv(this, buckets)
for (var i = 0; i < this.N; i++) {
spawnBucket(buckets)
}
},
set: function set(key, value) {
const buckets = getPriv(this)
if (!(mapHas(buckets[0], key))) {
if (this.max && buckets[0].size >= Math.ceil(this.max / this.N)) {
rotateBuckets(buckets, this.rotationHook)
}
}
mapSet(buckets[0], key, value)
return value
},
get: function get(key) {
const buckets = getPriv(this)
const retain = this.retainUsed
for (var i = 0; i < buckets.length; i++) {
if (mapHas(buckets[i], key)) {
const value = mapGet(buckets[i], key)
if (i && retain) {
//put a reference in the newest bucket to retain most used refs longer
return this.set(key, value)
}
return value
}
}
},
_get_buckets: function () { return getPriv(this) },
}
const weakRotate = (memWeakRef) => {
const mem = memWeakRef.deref()
if (mem) {
rotateBuckets(getPriv(mem), mem.rotationHook)
return true
} else {
return false
}
}
function rotateBucketsPeriodically(memWeakRef, interval) {
const handle = setInterval(() => {
if (false === weakRotate(memWeakRef)) {
clearInterval(handle)
}
}, interval)
// don't keep node live
if (handle.unref) {
handle.unref()
}
}
module.exports = {
/**
*
* @param {object} opts
* @param {number} [opts.buckets] - number of buckets to use (default 2)
* @param {number} [opts.limit] - max number of items to store (default unlimited)
* @param {number} [opts.maxTTL] - max time to live for an item
* @param {boolean} [opts.retainUsed] - retain most used items longer if possible
* @param {function} [opts.cleanupListener] - callback to call when a bucket is rotated
*/
safeMemoryCache(opts) {
const number = ~~(opts.buckets) || 2;
const mem = create(bucketsProto)
defineProperties(mem, {
N: { value: number, enumerable: false },
max: { value: opts.limit, enumerable: false },
rotationHook: { value: opts.cleanupListener || null, enumerable: false },
retainUsed: { value: opts.retainUsed || false, enumerable: false }
})
mem.clear()
if (opts.maxTTL) {
// use a weakref to not retain mem while accessing rotateBucket
const memWeakRef = new WeakRef(mem)
rotateBucketsPeriodically(memWeakRef, ~~(opts.maxTTL / number))
}
return freeze(mem)
}
}