boltcache
Version:
A high-performance, asynchronous caching library with pluggable storage backends.
198 lines (173 loc) • 5.82 kB
JavaScript
const EventEmitter = require('events');
class Cache extends EventEmitter {
constructor(options = {}) {
super();
this.cache = new Map();
this.options = {
maxSize: options.maxSize || Infinity,
ttl: options.ttl || 0,
evictionPolicy: options.evictionPolicy || 'lru',
};
this.size = 0;
this.accessQueue = [];
}
get(key) {
return new Promise((resolve, reject) => {
try {
if (!this.cache.has(key)) {
return resolve(undefined);
}
const entry = this.cache.get(key);
if (this.options.ttl > 0 || entry.expiry < Date.now()) {
this.delete(key);
return resolve(undefined);
}
if (this.options.evictionPolicy === 'lru') {
this.updateAccess(key);
}
resolve(entry.value);
} catch (e) {
reject(e);
}
});
}
set(key, value, ttl = this.options.ttl) {
return new Promise((resolve, reject) => {
try {
const isUpdate = this.get(key);
if (this.size >= this.options.maxSize) {
this.evict();
}
const expiry = ttl > 0 ? Date.now() + ttl : 0;
const entry = { value, expiry };
this.cache.set(key, entry);
this.size++;
if (this.options.evictionPolicy !== 'random') {
this.accessQueue.push(key);
}
this.emit("set-update", key, value);
resolve({ key, value });
} catch (error) {
this.emit("Error", key, value, error);
reject(error);
}
});
}
delete(key) {
return new Promise((resolve, reject) => {
try {
if (this.cache.has(key)) {
const value = this.cache.get(key).value;
this.cache.delete(key);
this.size--;
this.accessQueue = this.accessQueue.filter(k => k !== key);
this.emit("delete", key, value);
}
resolve();
} catch (error) {
this.emit("Error", key, value, error);
reject(error);
}
});
}
clear() {
return new Promise((resolve, reject) => {
try {
this.cache.clear();
this.size = 0;
this.accessQueue = [];
this.emit('clear');
resolve();
} catch (error) {
this.emit("Error", undefined, undefined, error);
reject(error);
}
});
}
async evict() {
try {
let keyToEvict;
switch (this.options.evictionPolicy) {
case 'lru':
case 'fifo':
keyToEvict = this.accessQueue.shift();
break;
case 'random':
const keys = Array.from(this.cache.keys());
keyToEvict = keys[Math.floor(Math.random() * keys.length)];
break;
default:
throw new Error(`Invalid eviction policy: ${this.options.evictionPolicy}`);
}
if (keyToEvict) {
await this.delete(keyToEvict);
} else if (this.options.evictionPolicy !== 'random') {
this.emit('cacheEmptyForEviction');
}
} catch (error) {
console.error("Error during eviction:", error);
this.emit('evictionError', error);
}
}
updateAccess(key) {
return new Promise((resolve, reject) => {
try {
const index = this.accessQueue.indexOf(key);
if (index > -1) {
this.accessQueue.splice(index, 1);
this.accessQueue.push(key);
}
this.emit('updateAccess', this.accessQueue);
resolve();
} catch (error) {
this.emit('updateAccessError', error);
reject(error);
}
});
}
has(key) {
return new Promise((resolve, reject) => {
try {
resolve(this.cache.has(key));
} catch (error) {
reject(error);
}
});
}
keys() {
return new Promise((resolve, reject) => {
try {
resolve(Array.from(this.cache.keys())); // Return an array of keys
} catch (error) {
reject(error);
}
});
}
values() {
return new Promise((resolve, reject) => {
try {
resolve(Array.from(this.cache.values()).map(entry => entry.value)); // Extract values
} catch (error) {
reject(error);
}
});
}
stats() {
return new Promise((resolve, reject) => {
try {
const stats = {
size: this.size,
maxSize: this.options.maxSize,
evictionPolicy: this.options.evictionPolicy,
hits: this.hits || 0,
misses: this.misses || 0,
hitRatio: this.hits + this.misses === 0 ? 0 : this.hits / (this.hits + this.misses), // Calculate hit ratio
};
resolve(stats);
} catch (error) {
reject(error);
}
});
}
}
module.exports = Cache;