-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #92 from mcpower/from-string-cache
Add cache for fromString
- Loading branch information
Showing
2 changed files
with
206 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
/** | ||
* A LRU cache intended for caching pure functions. | ||
*/ | ||
export class LRUCache<K, V> { | ||
private map = new Map<K, ListNode<K, V>>(); | ||
// Invariant: Exactly one of the below is true before and after calling a | ||
// LRUCache method: | ||
// - first and last are both undefined, and map.size() is 0. | ||
// - first and last are the same object, and map.size() is 1. | ||
// - first and last are different objects, and map.size() is greater than 1. | ||
private first: ListNode<K, V> | undefined = undefined; | ||
private last: ListNode<K, V> | undefined = undefined; | ||
maxSize: number; | ||
|
||
/** | ||
* @param maxSize The maximum size for this cache. We recommend setting this | ||
* to be one less than a power of 2, as most hashtables - including V8's | ||
* Object hashtable (https://crsrc.org/c/v8/src/objects/ordered-hash-table.cc) | ||
* - uses powers of two for hashtable sizes. It can't exactly be a power of | ||
* two, as a .set() call could temporarily set the size of the map to be | ||
* maxSize + 1. | ||
*/ | ||
constructor(maxSize: number) { | ||
this.maxSize = maxSize; | ||
} | ||
|
||
get size(): number { | ||
return this.map.size; | ||
} | ||
|
||
/** | ||
* Gets the specified key from the cache, or undefined if it is not in the | ||
* cache. | ||
* @param key The key to get. | ||
* @returns The cached value, or undefined if key is not in the cache. | ||
*/ | ||
get(key: K): V | undefined { | ||
const node = this.map.get(key); | ||
if (node === undefined) { | ||
return undefined; | ||
} | ||
// It is guaranteed that there is at least one item in the cache. | ||
// Therefore, first and last are guaranteed to be a ListNode... | ||
// but if there is only one item, they might be the same. | ||
|
||
// Update the order of the list to make this node the first node in the | ||
// list. | ||
// This isn't needed if this node is already the first node in the list. | ||
if (node !== this.first) { | ||
// As this node is DIFFERENT from the first node, it is guaranteed that | ||
// there are at least two items in the cache. | ||
// However, this node could possibly be the last item. | ||
if (node === this.last) { | ||
// This node IS the last node. | ||
this.last = node.prev; | ||
// From the invariants, there must be at least two items in the cache, | ||
// so node - which is the original "last node" - must have a defined | ||
// previous node. Therefore, this.last - set above - must be defined | ||
// here. | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
this.last!.next = undefined; | ||
} else { | ||
// This node is somewhere in the middle of the list, so there must be at | ||
// least THREE items in the list, and this node's prev and next must be | ||
// defined here. | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
node.prev!.next = node.next; | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
node.next!.prev = node.prev; | ||
} | ||
node.next = this.first; | ||
// From the invariants, there must be at least two items in the cache, so | ||
// this.first must be a valid ListNode. | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
this.first!.prev = node; | ||
this.first = node; | ||
} | ||
return node.value; | ||
} | ||
|
||
/** | ||
* Sets an entry in the cache. | ||
* | ||
* @param key The key of the entry. | ||
* @param value The value of the entry. | ||
* @throws Error, if the map already contains the key. | ||
*/ | ||
set(key: K, value: V): void { | ||
// Ensure that this.maxSize >= 1. | ||
if (this.maxSize < 1) { | ||
return; | ||
} | ||
if (this.map.has(key)) { | ||
throw new Error("Cannot update existing keys in the cache"); | ||
} | ||
const node = new ListNode(key, value); | ||
// Move node to the front of the list. | ||
if (this.first === undefined) { | ||
// If the first is undefined, the last is undefined too. | ||
// Therefore, this cache has no items in it. | ||
this.first = node; | ||
this.last = node; | ||
} else { | ||
// This cache has at least one item in it. | ||
node.next = this.first; | ||
this.first.prev = node; | ||
this.first = node; | ||
} | ||
this.map.set(key, node); | ||
|
||
while (this.map.size > this.maxSize) { | ||
// We are guaranteed that this.maxSize >= 1, | ||
// so this.map.size is guaranteed to be >= 2, | ||
// so this.first and this.last must be different valid ListNodes, | ||
// and this.last.prev must also be a valid ListNode (possibly this.first). | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
const last = this.last!; | ||
this.map.delete(last.key); | ||
this.last = last.prev; | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
this.last!.next = undefined; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* A node in a doubly linked list. | ||
*/ | ||
class ListNode<K, V> { | ||
key: K; | ||
value: V; | ||
next: ListNode<K, V> | undefined = undefined; | ||
prev: ListNode<K, V> | undefined = undefined; | ||
|
||
constructor(key: K, value: V) { | ||
this.key = key; | ||
this.value = value; | ||
} | ||
} |