transitory
Version:
In-memory cache with high hit rates via LFU eviction. Supports time-based expiration, automatic loading and metrics.
199 lines • 7.09 kB
JavaScript
;
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.TimerNode = exports.TimerWheel = void 0;
var CacheNode_1 = require("../CacheNode");
/**
* Helper function to calculate the closest power of two to N.
*
* @param n -
* input
* @returns
* closest power of two to `n`
*/
function toPowerOfN(n) {
return Math.pow(2, Math.ceil(Math.log(n) / Math.LN2));
}
var LAYERS = [64, 64, 32, 4, 1];
var SPANS = [toPowerOfN(1000), toPowerOfN(60000), toPowerOfN(3600000), toPowerOfN(86400000), LAYERS[3] * toPowerOfN(86400000), LAYERS[3] * toPowerOfN(86400000)];
var SHIFTS = SPANS.slice(0, SPANS.length - 1).map(function (span) { return 1 + Math.floor(Math.log(span - 1) * Math.LOG2E); });
/**
* A timer wheel for variable expiration of items in a cache. Stores items in
* layers that are circular buffers that represent a time span.
*
* This implementation takes some extra care to work with Number as they are
* actually doubles and shifting turns them into 32-bit ints. To represent
* time we need more than 32-bits so to fully support things this implementation
* uses a base which is removed from all of the numbers to make them fit into
* 32-bits.
*
* Based on an idea by Ben Manes implemented in Caffeine.
*/
var TimerWheel = /** @class */ (function () {
function TimerWheel(evict) {
var _this = this;
this.evict = evict;
this.base = Date.now();
this.layers = LAYERS.map(function (b) {
var result = new Array(b);
for (var i = 0; i < b; i++) {
result[i] = new TimerNode(_this, null, null);
}
return result;
});
this.time = 0;
}
Object.defineProperty(TimerWheel.prototype, "localTime", {
get: function () {
return Date.now() - this.base;
},
enumerable: false,
configurable: true
});
TimerWheel.prototype.findBucket = function (node) {
var d = node.time - this.time;
if (d <= 0)
return null;
var layers = this.layers;
for (var i = 0, n = layers.length - 1; i < n; i++) {
if (d >= SPANS[i + 1])
continue;
var ticks = node.time >>> SHIFTS[i];
var index = ticks & (layers[i].length - 1);
return layers[i][index];
}
return layers[layers.length - 1][0];
};
TimerWheel.prototype.advance = function (localTime) {
var previous = this.time;
var time = localTime || this.localTime;
this.time = time;
var layers = this.layers;
// Holder for expired keys
var expired = null;
/*
* Go through all of the layers on the wheel, evict things and move
* other stuff around.
*/
for (var i = 0, n = SHIFTS.length; i < n; i++) {
var previousTicks = previous >>> SHIFTS[i];
var timeTicks = time >>> SHIFTS[i];
// At the same tick, no need to keep working down the layers
if (timeTicks <= previousTicks)
break;
var wheel = layers[i];
// Figure out the actual buckets to use
var start = void 0;
var end = void 0;
if (time - previous >= SPANS[i + 1]) {
start = 0;
end = wheel.length - 1;
}
else {
start = previousTicks & (SPANS[i] - 1);
end = timeTicks & (SPANS[i] - 1);
}
// Go through all of the buckets and move stuff around
for (var j = start; j <= end; j++) {
var head = wheel[j & (wheel.length - 1)];
var node = head.next;
head.previous = head;
head.next = head;
while (node !== head) {
var next = node.next;
node.remove();
if (node.time <= time) {
// This node has expired, add it to the queue
if (!expired)
expired = [];
expired.push(node.key);
}
else {
// Find a new bucket to put this node in
var b = this.findBucket(node);
if (b) {
node.appendToTail(b);
}
}
node = next;
}
}
}
if (expired) {
this.evict(expired);
}
};
/**
* Create a node that that helps with tracking when a key and value
* should be evicted.
*
* @param key -
* key to set
* @param value -
* value to set
* @returns
* node
*/
TimerWheel.prototype.node = function (key, value) {
return new TimerNode(this, key, value);
};
/**
* Schedule eviction of the given node at the given timestamp.
*
* @param node -
* node to reschedule
* @param time -
* new expiration time
* @returns
* if the node was rescheduled
*/
TimerWheel.prototype.schedule = function (node, time) {
node.remove();
if (time <= 0)
return false;
node.time = this.localTime + time;
var parent = this.findBucket(node);
if (!parent)
return false;
node.appendToTail(parent);
return true;
};
/*
* Remove the given node from the wheel.
*/
TimerWheel.prototype.deschedule = function (node) {
node.remove();
};
return TimerWheel;
}());
exports.TimerWheel = TimerWheel;
/* Node in a doubly linked list. More or less the same as used in BoundedCache */
var TimerNode = /** @class */ (function (_super) {
__extends(TimerNode, _super);
function TimerNode(wheel, key, value) {
var _this = _super.call(this, key, value) || this;
_this.time = Number.MAX_SAFE_INTEGER;
_this.wheel = wheel;
return _this;
}
TimerNode.prototype.isExpired = function () {
return this.wheel.localTime > this.time;
};
return TimerNode;
}(CacheNode_1.CacheNode));
exports.TimerNode = TimerNode;
//# sourceMappingURL=TimerWheel.js.map