UNPKG

transitory

Version:

In-memory cache with high hit rates via LFU eviction. Supports time-based expiration, automatic loading and metrics.

170 lines 5.55 kB
import { CacheNode } from '../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)); } const LAYERS = [64, 64, 32, 4, 1]; const SPANS = [toPowerOfN(1000), toPowerOfN(60000), toPowerOfN(3600000), toPowerOfN(86400000), LAYERS[3] * toPowerOfN(86400000), LAYERS[3] * toPowerOfN(86400000)]; const SHIFTS = SPANS.slice(0, SPANS.length - 1).map(span => 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. */ export class TimerWheel { constructor(evict) { this.evict = evict; this.base = Date.now(); this.layers = LAYERS.map(b => { const result = new Array(b); for (let i = 0; i < b; i++) { result[i] = new TimerNode(this, null, null); } return result; }); this.time = 0; } get localTime() { return Date.now() - this.base; } findBucket(node) { const d = node.time - this.time; if (d <= 0) return null; const layers = this.layers; for (let i = 0, n = layers.length - 1; i < n; i++) { if (d >= SPANS[i + 1]) continue; const ticks = node.time >>> SHIFTS[i]; const index = ticks & (layers[i].length - 1); return layers[i][index]; } return layers[layers.length - 1][0]; } advance(localTime) { const previous = this.time; const time = localTime || this.localTime; this.time = time; const layers = this.layers; // Holder for expired keys let expired = null; /* * Go through all of the layers on the wheel, evict things and move * other stuff around. */ for (let i = 0, n = SHIFTS.length; i < n; i++) { const previousTicks = previous >>> SHIFTS[i]; const timeTicks = time >>> SHIFTS[i]; // At the same tick, no need to keep working down the layers if (timeTicks <= previousTicks) break; const wheel = layers[i]; // Figure out the actual buckets to use let start; let end; 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 (let j = start; j <= end; j++) { const head = wheel[j & (wheel.length - 1)]; let node = head.next; head.previous = head; head.next = head; while (node !== head) { const 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 const 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 */ node(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 */ schedule(node, time) { node.remove(); if (time <= 0) return false; node.time = this.localTime + time; const parent = this.findBucket(node); if (!parent) return false; node.appendToTail(parent); return true; } /* * Remove the given node from the wheel. */ deschedule(node) { node.remove(); } } /* Node in a doubly linked list. More or less the same as used in BoundedCache */ export class TimerNode extends CacheNode { constructor(wheel, key, value) { super(key, value); this.time = Number.MAX_SAFE_INTEGER; this.wheel = wheel; } isExpired() { return this.wheel.localTime > this.time; } } //# sourceMappingURL=TimerWheel.js.map