UNPKG

least-recent

Version:

A cache object that deletes the least-recently-used items

460 lines (459 loc) 13.5 kB
import { test } from "uvu"; import * as assert from "uvu/assert"; import { LRUCache } from "./most-recent.js"; const INVALID_CAPACITIES = [undefined, {}, -1, true, 1.01, Infinity]; for (const capacity of INVALID_CAPACITIES) { test(`invalid capacity ${capacity}`, () => { assert.throws(() => { // @ts-expect-error new LRUCache(capacity); }); }); } test("should be possible to create a LRU cache.", () => { const cache = new LRUCache(3); assert.equal(cache.capacity, 3); cache.set("one", 1); cache.set("two", 2); assert.equal(cache.size, 2); assert.equal(Array.from(cache.entries()), [ ["two", 2], ["one", 1], ]); cache.set("three", 3); assert.equal(cache.size, 3); assert.equal(Array.from(cache.entries()), [ ["three", 3], ["two", 2], ["one", 1], ]); cache.set("four", 4); assert.equal(cache.size, 3); assert.equal(Array.from(cache.entries()), [ ["four", 4], ["three", 3], ["two", 2], ]); cache.set("two", 5); assert.equal(Array.from(cache.entries()), [ ["two", 5], ["four", 4], ["three", 3], ]); assert.equal(cache.has("four"), true); assert.equal(cache.has("one"), false); assert.equal(cache.get("one"), undefined); assert.equal(cache.get("four"), 4); assert.equal(Array.from(cache.entries()), [ ["four", 4], ["two", 5], ["three", 3], ]); assert.equal(cache.get("three"), 3); assert.equal(Array.from(cache.entries()), [ ["three", 3], ["four", 4], ["two", 5], ]); assert.equal(cache.get("three"), 3); assert.equal(Array.from(cache.entries()), [ ["three", 3], ["four", 4], ["two", 5], ]); assert.equal(cache.peek("two"), 5); assert.equal(Array.from(cache.entries()), [ ["three", 3], ["four", 4], ["two", 5], ]); }); test("should be possible to clear a LRU cache.", () => { const cache = new LRUCache(3); cache.set("one", 1); cache.set("two", 2); cache.set("one", 3); assert.equal(Array.from(cache.entries()), [ ["one", 3], ["two", 2], ]); assert.equal(cache.get("two"), 2); assert.equal(Array.from(cache.entries()), [ ["two", 2], ["one", 3], ]); cache.clear(); assert.equal(cache.capacity, 3); assert.equal(cache.size, 0); assert.equal(cache.has("two"), false); cache.set("one", 1); cache.set("two", 2); cache.set("three", 3); cache.set("two", 6); cache.set("four", 4); assert.equal(Array.from(cache.entries()), [ ["four", 4], ["two", 6], ["three", 3], ]); }); test("should be possible to create an iterator over the cache's keys.", () => { const cache = new LRUCache(3); cache.set("one", 1); cache.set("two", 2); cache.set("three", 3); assert.equal(Array.from(cache.keys()), ["three", "two", "one"]); }); test("should be possible to create an iterator over the cache's values.", () => { const cache = new LRUCache(3); cache.set("one", 1); cache.set("two", 2); cache.set("three", 3); assert.equal(Array.from(cache.values()), [3, 2, 1]); }); test("should be possible to pop an evicted value when items are evicted from cache", () => { const cache = new LRUCache(3); cache.set("one", 1); cache.set("two", 2); cache.set("three", 3); const popResult = cache.setpop("four", 4); assert.equal(popResult, { evicted: true, key: "one", value: 1 }); assert.equal(Array.from(cache.values()), [4, 3, 2]); }); test("should return null when setting an item does not overwrite or evict", () => { const cache = new LRUCache(3); cache.set("one", 1); cache.set("two", 2); const popResult = cache.setpop("three", 3); assert.equal(popResult, null); }); test("should be possible to pop an overwritten value when items are overwritten from cache", () => { const cache = new LRUCache(3); cache.set("one", 1); cache.set("two", 2); cache.set("three", 3); const popResult = cache.setpop("three", 10); assert.equal(popResult, { evicted: false, key: "three", value: 3 }); assert.equal(Array.from(cache.values()), [10, 2, 1]); }); test("should work with capacity = 1.", () => { const cache = new LRUCache(1); cache.set("one", 1); cache.set("two", 2); cache.set("three", 3); assert.equal(Array.from(cache.entries()), [["three", 3]]); assert.equal(cache.get("one"), undefined); assert.equal(cache.get("three"), 3); assert.equal(cache.get("three"), 3); assert.equal(Array.from(cache.entries()), [["three", 3]]); }); test("should be possible to create a cache from an arbitrary iterable.", () => { const cache = LRUCache.from(new Map([ ["one", 1], ["two", 2], ])); assert.equal(cache.capacity, 2); assert.equal(Array.from(cache.entries()), [ ["two", 2], ["one", 1], ]); }); // test("should be possible to create a specialized cache.", function () { // var cache = new Cache(Uint8Array, Float64Array, 3); // // cache.set(3, 5.6); // cache.set(12, 6.464); // cache.set(23, 0.45); // cache.set(59, -0.464); // // assert.equal(Array.from(cache.entries()), [ // [59, -0.464], // [23, 0.45], // [12, 6.464], // ]); // // var cacheFrom = Cache.from([], Uint8Array, Float64Array, 3); // // cacheFrom.set(3, 5.6); // cacheFrom.set(12, 6.464); // cacheFrom.set(23, 0.45); // cacheFrom.set(59, -0.464); // // assert.equal(Array.from(cacheFrom.entries()), [ // [59, -0.464], // [23, 0.45], // [12, 6.464], // ]); // }); // test("should be possible to iterate over the cache using a callback.", function () { // var cache = new Cache(1); // // cache.set("one", 1); // cache.set("two", 2); // cache.set("three", 3); // // var entries = []; // // cache.forEach(function (value, key) { // entries.push([key, value]); // }); // // assert.equal(entries, Array.from(cache.entries())); // }); test("should be possible to iterate over the cache.", () => { const cache = new LRUCache(1); cache.set("one", 1); cache.set("two", 2); cache.set("three", 3); const entries = []; for (const entry of cache) { entries.push(entry); } assert.equal(entries, Array.from(cache.entries())); }); test("emit event on eviction", () => { const cache = new LRUCache(1); const events = []; cache.on("evicted", (key, value) => { events.push([key, value]); }); cache.set("one", 1); cache.set("two", 2); cache.set("three", 3); assert.equal(events, [ ["one", 1], ["two", 2], ]); }); test("should be possible to delete keys from a LRU cache.", function () { const cache = new LRUCache(3); assert.equal(cache.capacity, 3); // Delete when nothing has ever been added assert.equal(cache.delete("one"), false); cache.set("one", "uno"); cache.set("two", "dos"); cache.set("three", "tres"); assert.equal(Array.from(cache.entries()), [ ["three", "tres"], ["two", "dos"], ["one", "uno"], ]); // Delete a key that has never been seen assert.equal(cache.delete("NEVER SEEN EM"), false); // Delete head assert.equal(cache.delete("three"), true); assert.equal(Array.from(cache.entries()), [ ["two", "dos"], ["one", "uno"], ]); assert.equal(cache.delete("three"), false); cache.set("three", "trois"); assert.equal(Array.from(cache.entries()), [ ["three", "trois"], ["two", "dos"], ["one", "uno"], ]); // Delete node which is neither head or tail assert.equal(cache.delete("two"), true); assert.equal(Array.from(cache.entries()), [ ["three", "trois"], ["one", "uno"], ]); assert.equal(cache.delete("two"), false); // Delete tail assert.equal(cache.delete("one"), true); assert.equal(Array.from(cache.entries()), [["three", "trois"]]); // Delete the only key assert.equal(cache.delete("three"), true); assert.equal(cache.capacity, 3); assert.equal(cache.size, 0); // Delete from an emptied LRU assert.equal(cache.delete("three"), false); cache.set("one", "uno"); cache.set("two", "dos"); cache.set("three", "tres"); cache.set("two", "deux"); cache.set("four", "cuatro"); assert.equal(Array.from(cache.entries()), [ ["four", "cuatro"], ["two", "deux"], ["three", "tres"], ]); }); test("maintains LRU order regardless of deletions", function () { const cache = new LRUCache(5); let wasDeleted; cache.set("one", "uno"); cache.set("two", "dos"); cache.set("three", "tres"); cache.set("four", "cuatro"); cache.set("five", "cinco"); cache.get("one"); // order is [ one // five four three two ] <-- two will be removed cache.set("six", "seis"); assert.equal(Array.from(cache.entries()), [ ["six", "seis"], ["one", "uno"], ["five", "cinco"], ["four", "cuatro"], ["three", "tres"], ]); wasDeleted = cache.delete("five"); assert.equal(wasDeleted, true); wasDeleted = cache.delete("not_here"); assert.equal(wasDeleted, false); cache.set("one", "rast"); assert.equal(Array.from(cache.entries()), [ ["one", "rast"], ["six", "seis"], ["four", "cuatro"], ["three", "tres"], ]); cache.set("seven", "siete"); assert.equal(Array.from(cache.entries()), [ ["seven", "siete"], ["one", "rast"], ["six", "seis"], ["four", "cuatro"], ["three", "tres"], ]); cache.set("eight", "ocho"); assert.equal(Array.from(cache.entries()), [ ["eight", "ocho"], ["seven", "siete"], ["one", "rast"], ["six", "seis"], ["four", "cuatro"], ]); wasDeleted = cache.delete("five"); assert.equal(wasDeleted, false); assert.equal(Array.from(cache.entries()), [ ["eight", "ocho"], ["seven", "siete"], ["one", "rast"], ["six", "seis"], ["four", "cuatro"], ]); }); test("maintains LRU order regardless of removals", function () { const cache = new LRUCache(5); let dead; cache.set("one", "uno"); cache.set("two", "dos"); cache.set("three", "tres"); cache.set("four", "cuatro"); cache.set("five", "cinco"); cache.get("one"); // order is [ one // five four three two ] <-- two will be removed cache.set("six", "seis"); assert.equal(Array.from(cache.entries()), [ ["six", "seis"], ["one", "uno"], ["five", "cinco"], ["four", "cuatro"], ["three", "tres"], ]); assert.equal(cache.delete("five"), true); assert.equal(cache.delete("not_here"), false); cache.set("one", "rast"); assert.equal(Array.from(cache.entries()), [ ["one", "rast"], ["six", "seis"], ["four", "cuatro"], ["three", "tres"], ]); cache.set("seven", "siete"); assert.equal(Array.from(cache.entries()), [ ["seven", "siete"], ["one", "rast"], ["six", "seis"], ["four", "cuatro"], ["three", "tres"], ]); cache.set("eight", "ocho"); assert.equal(Array.from(cache.entries()), [ ["eight", "ocho"], ["seven", "siete"], ["one", "rast"], ["six", "seis"], ["four", "cuatro"], ]); assert.equal(cache.delete("five"), false); assert.equal(Array.from(cache.entries()), [ ["eight", "ocho"], ["seven", "siete"], ["one", "rast"], ["six", "seis"], ["four", "cuatro"], ]); }); test("enjoys a healthy workout of deletions", function () { var cache = new LRUCache(4); cache.set(0, "cero"); cache.set(1, "uno"); cache.set(2, "dos"); cache.delete(1); cache.set(3, "tres"); cache.set(4, "cuatro"); cache.get(2); assert.equal(Array.from(cache.entries()), [ [2, "dos"], [4, "cuatro"], [3, "tres"], [0, "cero"], ]); cache.set(5, "cinco"); cache.set(6, "seis"); cache.delete(1); cache.delete(2); cache.set(5, "cinq"); assert.equal(Array.from(cache.entries()), [ [5, "cinq"], [6, "seis"], [4, "cuatro"], ]); cache.set(7, "siete"); cache.set(8, "ocho"); cache.set(9, "nueve"); cache.delete(8); cache.set(10, "diez"); assert.equal(Array.from(cache.entries()), [ [10, "diez"], [9, "nueve"], [7, "siete"], [5, "cinq"], ]); cache.set(7, "sept"); cache.get(5); cache.set(8, "huit"); cache.set(9, "neuf"); cache.set(10, "dix"); assert.equal(Array.from(cache.entries()), [ [10, "dix"], [9, "neuf"], [8, "huit"], [5, "cinq"], ]); cache.get(8); cache.delete(10); cache.set(1, "rast"); cache.set(2, "deux"); cache.get(8); assert.equal(Array.from(cache.entries()), [ [8, "huit"], [2, "deux"], [1, "rast"], [9, "neuf"], ]); cache.delete(2); cache.delete(9); cache.get(1); cache.set(2, "dva"); cache.get(1); cache.set(3, "tri"); assert.equal(Array.from(cache.entries()), [ [3, "tri"], [1, "rast"], [2, "dva"], [8, "huit"], ]); }); test.run();