add LRU Cache utility
we'll use it to cache members later on
This commit is contained in:
parent
7af22da587
commit
f0c0c3e084
1 changed files with 146 additions and 0 deletions
146
src/utils/LRUCache.js
Normal file
146
src/utils/LRUCache.js
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Very simple least-recently-used cache implementation
|
||||||
|
* that should be fast enough for very small cache sizes
|
||||||
|
*/
|
||||||
|
export class BaseLRUCache {
|
||||||
|
constructor(limit) {
|
||||||
|
this._limit = limit;
|
||||||
|
this._entries = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_get(findEntryFn) {
|
||||||
|
const idx = this._entries.findIndex(findEntryFn);
|
||||||
|
if (idx !== -1) {
|
||||||
|
const entry = this._entries[idx];
|
||||||
|
// move to top
|
||||||
|
if (idx > 0) {
|
||||||
|
this._entries.splice(idx, 1);
|
||||||
|
this._entries.unshift(entry);
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_set(value, findEntryFn) {
|
||||||
|
let indexToRemove = this._entries.findIndex(findEntryFn);
|
||||||
|
this._entries.unshift(value);
|
||||||
|
if (indexToRemove === -1) {
|
||||||
|
if (this._entries.length > this._limit) {
|
||||||
|
indexToRemove = this._entries.length - 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// we added the entry at the start since we looked for the index
|
||||||
|
indexToRemove += 1;
|
||||||
|
}
|
||||||
|
if (indexToRemove !== -1) {
|
||||||
|
this._onEvictEntry(this._entries[indexToRemove]);
|
||||||
|
this._entries.splice(indexToRemove, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onEvictEntry() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LRUCache extends BaseLRUCache {
|
||||||
|
constructor(limit, keyFn) {
|
||||||
|
super(limit);
|
||||||
|
this._keyFn = keyFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key) {
|
||||||
|
return this._get(e => this._keyFn(e) === key);
|
||||||
|
}
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
const key = this._keyFn(value);
|
||||||
|
this._set(value, e => this._keyFn(e) === key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tests() {
|
||||||
|
return {
|
||||||
|
"can retrieve added entries": assert => {
|
||||||
|
const cache = new LRUCache(2, e => e.id);
|
||||||
|
cache.set({id: 1, name: "Alice"});
|
||||||
|
cache.set({id: 2, name: "Bob"});
|
||||||
|
assert.equal(cache.get(1).name, "Alice");
|
||||||
|
assert.equal(cache.get(2).name, "Bob");
|
||||||
|
},
|
||||||
|
"first entry is evicted first": assert => {
|
||||||
|
const cache = new LRUCache(2, e => e.id);
|
||||||
|
cache.set({id: 1, name: "Alice"});
|
||||||
|
cache.set({id: 2, name: "Bob"});
|
||||||
|
cache.set({id: 3, name: "Charly"});
|
||||||
|
assert.equal(cache.get(1), undefined);
|
||||||
|
assert.equal(cache.get(2).name, "Bob");
|
||||||
|
assert.equal(cache.get(3).name, "Charly");
|
||||||
|
assert.equal(cache._entries.length, 2);
|
||||||
|
},
|
||||||
|
"second entry is evicted if first is requested": assert => {
|
||||||
|
const cache = new LRUCache(2, e => e.id);
|
||||||
|
cache.set({id: 1, name: "Alice"});
|
||||||
|
cache.set({id: 2, name: "Bob"});
|
||||||
|
cache.get(1);
|
||||||
|
cache.set({id: 3, name: "Charly"});
|
||||||
|
assert.equal(cache.get(1).name, "Alice");
|
||||||
|
assert.equal(cache.get(2), undefined);
|
||||||
|
assert.equal(cache.get(3).name, "Charly");
|
||||||
|
assert.equal(cache._entries.length, 2);
|
||||||
|
},
|
||||||
|
"setting an entry twice removes the first": assert => {
|
||||||
|
const cache = new LRUCache(2, e => e.id);
|
||||||
|
cache.set({id: 1, name: "Alice"});
|
||||||
|
cache.set({id: 2, name: "Bob"});
|
||||||
|
cache.set({id: 1, name: "Al Ice"});
|
||||||
|
cache.set({id: 3, name: "Charly"});
|
||||||
|
assert.equal(cache.get(1).name, "Al Ice");
|
||||||
|
assert.equal(cache.get(2), undefined);
|
||||||
|
assert.equal(cache.get(3).name, "Charly");
|
||||||
|
assert.equal(cache._entries.length, 2);
|
||||||
|
},
|
||||||
|
"evict callback is called": assert => {
|
||||||
|
let evictions = 0;
|
||||||
|
class CustomCache extends LRUCache {
|
||||||
|
_onEvictEntry(entry) {
|
||||||
|
assert.equal(entry.name, "Alice");
|
||||||
|
evictions += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cache = new CustomCache(2, e => e.id);
|
||||||
|
cache.set({id: 1, name: "Alice"});
|
||||||
|
cache.set({id: 2, name: "Bob"});
|
||||||
|
cache.set({id: 3, name: "Charly"});
|
||||||
|
assert.equal(evictions, 1);
|
||||||
|
},
|
||||||
|
"evict callback is called when replacing entry with same identity": assert => {
|
||||||
|
let evictions = 0;
|
||||||
|
class CustomCache extends LRUCache {
|
||||||
|
_onEvictEntry(entry) {
|
||||||
|
assert.equal(entry.name, "Alice");
|
||||||
|
evictions += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cache = new CustomCache(2, e => e.id);
|
||||||
|
cache.set({id: 1, name: "Alice"});
|
||||||
|
cache.set({id: 1, name: "Bob"});
|
||||||
|
assert.equal(evictions, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
Reference in a new issue