UNPKG

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
"use strict"; 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