Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LRU cache implementation in base JS #164

Open
LTLA opened this issue Mar 15, 2023 · 0 comments
Open

LRU cache implementation in base JS #164

LTLA opened this issue Mar 15, 2023 · 0 comments

Comments

@LTLA
Copy link
Collaborator

LTLA commented Mar 15, 2023

As promised. As JS doesn't have a base ordered map, we instead use a history array that cross-references a key-value store. Touching the store will update the history, and when we hit the limit, we search the history to evict old entries from the store. This requires some semi-regular garbage collection on the history array to avoid accumulating the access records.

class LeastRecentlyUsedCache {
    #store;
    #history;
    #limit;
    #callback;

    constructor(limit, { callback = null } = {}) {
        this.#store = new Map;
        this.#history = [];
        this.#limit = limit;
        this.#callback = (callback == null ? x => {} : callback);
    }

    #gc() {
        if (this.#history > this.#limit * 5) {
            let replacement = [];
            for (const x of this.#history) {
                if (x !== null) {
                    this.#store.get(x).last_access = replacement.length;
                    replacement.push(x);
                }
            }
            this.#history = replacement;
        }
    }

    #touch(key, exists) {
        // Don't bother updating it if it's already the last-accessed.
        if (exists.last_access !== this.#history - 1) {
            this.#history[exists.last_access] = null;
            exists.last_access = this.#history.length;
        }
        this.#gc();
        this.#history.push(key);
    }

    set(key, value) {
        let exists = this.#store.get(key);
        if (typeof exists !== "undefined") {
            this.#touch(key, exists);
            exists.value = value;
        } else {
            // Flushing.
            if (this.#store.size == this.#limit) {
                let i = 0;
                while (i < this.#history.length) {
                    let x = this.#history[i];
                    if (x !== null) {
                        this.#history[i] = null;
                        this.#callback(this.#store.get(x).value);
                        this.#store.delete(x);
                        break;
                    }
                    i++;
                }
            }

            this.#gc();
            this.#store.set(key, { value: value, last_access: this.#history.length });
            this.#history.push(key);
        }
    }

    get(key) {
        let exists = this.#store.get(key);
        if (typeof exists === "undefined") {
            throw new Error("failed to find key '" + key + "' in the cache");
        }
        this.#touch(key, exists);
        return exists.value;
    }

    delete(key) {
        let exists = this.#store.get(key);
        if (typeof exists === "undefined") {
            throw new Error("failed to find key '" + key + "' in the cache");
        }
        this.#history[exists.last_access] = null;
        this.#callback(exists.value);
        this.#store.delete(key);
        return;
    }
};

Usage is something like:

let cache = new LeastRecentlyUsedCache(1);
cache.set("A", "FOO");
console.log(cache.get("A"));
cache.set("B", "WHEE");
console.log(cache.get("B"));
console.log(cache.get("A"));

Which gives the expected eviction:

FOO
WHEE
/home/luna/Downloads/lru.js:67
            throw new Error("failed to find key '" + key + "' in the cache");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant