element-portable/src/utils/LruCache.ts
David Langley 491f0cd08a
Change license (#13)
* Copyright headers 1

* Licence headers 2

* Copyright Headers 3

* Copyright Headers 4

* Copyright Headers 5

* Copyright Headers 6

* Copyright headers 7

* Add copyright headers for html and config file

* Replace license files and update package.json

* Update with CLA

* lint
2024-09-09 13:57:16 +00:00

234 lines
5.8 KiB
TypeScript

/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { logger } from "matrix-js-sdk/src/logger";
interface CacheItem<K, V> {
key: K;
value: V;
/** Next item in the list */
next: CacheItem<K, V> | null;
/** Previous item in the list */
prev: CacheItem<K, V> | null;
}
/**
* Least Recently Used cache.
* Can be initialised with a capacity and drops the least recently used items.
* This cache should be error robust: Cache miss on error.
*
* Implemented via a key lookup map and a double linked list:
* head tail
* a next → b next → c → next null
* null ← prev a ← prev b ← prev c
*
* @template K - Type of the key used to look up the values inside the cache
* @template V - Type of the values inside the cache
*/
export class LruCache<K, V> {
/** Head of the list. */
private head: CacheItem<K, V> | null = null;
/** Tail of the list */
private tail: CacheItem<K, V> | null = null;
/** Key lookup map */
private map: Map<K, CacheItem<K, V>>;
/**
* @param capacity - Cache capcity.
* @throws {Error} - Raises an error if the cache capacity is less than 1.
*/
public constructor(private capacity: number) {
if (this.capacity < 1) {
throw new Error("Cache capacity must be at least 1");
}
this.map = new Map();
}
/**
* Whether the cache contains an item under this key.
* Marks the item as most recently used.
*
* @param key - Key of the item
* @returns true: item in cache, else false
*/
public has(key: K): boolean {
try {
return this.getItem(key) !== undefined;
} catch (e) {
// Should not happen but makes it more robust to the unknown.
this.onError(e);
return false;
}
}
/**
* Returns an item from the cache.
* Marks the item as most recently used.
*
* @param key - Key of the item
* @returns The value if found, else undefined
*/
public get(key: K): V | undefined {
try {
return this.getItem(key)?.value;
} catch (e) {
// Should not happen but makes it more robust to the unknown.
this.onError(e);
return undefined;
}
}
/**
* Adds an item to the cache.
* A newly added item will be the set as the most recently used.
*
* @param key - Key of the item
* @param value - Item value
*/
public set(key: K, value: V): void {
try {
this.safeSet(key, value);
} catch (e) {
// Should not happen but makes it more robust to the unknown.
this.onError(e);
}
}
/**
* Deletes an item from the cache.
*
* @param key - Key of the item to be removed
*/
public delete(key: K): void {
const item = this.map.get(key);
// Unknown item.
if (!item) return;
try {
this.removeItemFromList(item);
this.map.delete(key);
} catch (e) {
// Should not happen but makes it more robust to the unknown.
this.onError(e);
}
}
/**
* Clears the cache.
*/
public clear(): void {
this.map = new Map();
this.head = null;
this.tail = null;
}
/**
* Returns an iterator over the cached values.
*/
public *values(): IterableIterator<V> {
for (const item of this.map.values()) {
yield item.value;
}
}
private safeSet(key: K, value: V): void {
const item = this.getItem(key);
if (item) {
// The item is already stored under this key. Update the value.
item.value = value;
return;
}
const newItem: CacheItem<K, V> = {
key,
value,
next: null,
prev: null,
};
if (this.head) {
// Put item in front of the list.
this.head.prev = newItem;
newItem.next = this.head;
}
this.setHeadTail(newItem);
// Store item in lookup map.
this.map.set(key, newItem);
if (this.tail && this.map.size > this.capacity) {
// Map size exceeded cache capcity. Drop tail item.
this.delete(this.tail.key);
}
}
private onError(e: unknown): void {
logger.warn("LruCache error", e);
this.clear();
}
private getItem(key: K): CacheItem<K, V> | undefined {
const item = this.map.get(key);
// Not in cache.
if (!item) return undefined;
// Item is already at the head of the list.
// No update required.
if (item === this.head) return item;
this.removeItemFromList(item);
// Put item to the front.
if (this.head) {
this.head.prev = item;
}
item.prev = null;
item.next = this.head;
this.setHeadTail(item);
return item;
}
private setHeadTail(item: CacheItem<K, V>): void {
if (item.prev === null) {
// Item has no previous item → head
this.head = item;
}
if (item.next === null) {
// Item has no next item → tail
this.tail = item;
}
}
private removeItemFromList(item: CacheItem<K, V>): void {
if (item === this.head) {
this.head = item.next;
}
if (item === this.tail) {
this.tail = item.prev;
}
if (item.prev) {
item.prev.next = item.next;
}
if (item.next) {
item.next.prev = item.prev;
}
}
}