Skip to content

Instantly share code, notes, and snippets.

@sohamkamani
Created July 21, 2020 16:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sohamkamani/3ce4725647ee28b30b1477c03920eb3d to your computer and use it in GitHub Desktop.
Save sohamkamani/3ce4725647ee28b30b1477c03920eb3d to your computer and use it in GitHub Desktop.
An example of how to implement LRU cache with eviction using the HTML5 localStorage API
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LocalStorage With LRU Eviction</title>
</head>
<body>
<button id="btn-set">Set</button><br />
Key: <input id="input-set-key" /><br />
Value: <input id="input-set-val" /> <br /><br />
<button id="btn-get">Get</button><br />
Key: <input id="input-get-key" /><br />
<div>Value: <span id="value"></span></div>
<div>
<p>Keys (Ordered in ascending recency of use)</p>
<p id="current-keys"></p>
</div>
<script>
const headKeyName = "_nodelist_head"
class NodeList {
constructor() {
this.head = null
this.tail = null
this.count = 0
}
add(key) {
const node = { key }
return this.addNode(node)
}
setHead(node) {
this.head = node
if (!node) {
localStorage.removeItem(headKeyName)
}
localStorage.setItem(headKeyName, node.key)
}
addNode(node) {
this.count += 1
if (!this.head && !this.tail) {
this.setHead(node)
this.tail = node
return node
}
this.tail.next = node
node.prev = this.tail
node.next = null
this.tail = node
return node
}
remove(node) {
if (!node) {
return
}
this.count -= 1
if (this.tail === node) {
this.tail = node.prev
}
if (this.head === node) {
this.setHead(node.next)
}
if (node.prev) {
node.prev.next = node.next
}
if (node.next) {
node.next.prev = node.prev
}
}
pop() {
if (!this.head) {
return null
}
const node = this.head
this.remove(this.head)
return node
}
moveToTail(node) {
this.remove(node)
return this.addNode(node)
}
load() {
const headKey = localStorage.getItem(headKeyName)
if (!headKey) {
return
}
const headNode = JSON.parse(localStorage.getItem(headKey))
let currentNode = headNode
let currentKey = headKey
while (currentNode) {
this.add(currentKey)
if (!currentNode.next) {
return
}
currentKey = currentNode.next
currentNode = JSON.parse(
localStorage.getItem(currentKey)
)
}
}
toString() {
const values = []
let current = this.head
while (current) {
values.push(current.key)
current = current.next
}
return values.join(",")
}
}
function serializeNode(node, value) {
return JSON.stringify({
value: value,
next: node.next && node.next.key,
prev: node.prev && node.prev.key,
})
}
function getNodeValue(nodeStr) {
const node = JSON.parse(nodeStr)
return node.value
}
class LocalStorageLRU {
constructor(maxKeys) {
this.maxKeys = maxKeys
this.keys = {}
this.nodes = new NodeList()
this.nodes.load()
}
set(key, value) {
const existingNode = this.keys[key]
if (existingNode) {
this.nodes.remove(existingNode)
}
if (this.nodes.count >= this.maxKeys) {
this.removeLeastUsedNode()
}
const node = this.nodes.add(key)
this.keys[key] = node
localStorage.setItem(key, serializeNode(node, value))
}
get(key) {
if (!this.keys[key]) {
return
}
const value = getNodeValue(localStorage.getItem(key))
const node = this.nodes.moveToTail(this.keys[key])
localStorage.setItem(key, serializeNode(node, value))
return value
}
removeLeastUsedNode() {
const node = this.nodes.pop()
delete this.keys[node.key]
localStorage.removeItem(node.key)
}
}
const btnSet = document.getElementById("btn-set")
const btnGet = document.getElementById("btn-get")
const inputSetKey = document.getElementById("input-set-key")
const inputSetVal = document.getElementById("input-set-val")
const inputGetKey = document.getElementById("input-get-key")
const valueDisplay = document.getElementById("value")
const currentKeyDisplay = document.getElementById("current-keys")
const lruStorage = new LocalStorageLRU(5)
btnSet.addEventListener("click", () => {
lruStorage.set(inputSetKey.value, inputSetVal.value)
currentKeyDisplay.innerHTML = lruStorage.nodes.toString()
})
btnGet.addEventListener("click", () => {
const value = lruStorage.get(inputGetKey.value)
valueDisplay.innerHTML = value
currentKeyDisplay.innerHTML = lruStorage.nodes.toString()
})
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment