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