0.1.9•Updated 3 months ago
interface MemCacheEntry<T = unknown> {
cached_at: number
data: T
}
interface MemCacheOptions {
/**
* How long an entry should be kept in this cache.
*
* *Default*: 120 (2 minutes)
*/
expiration_period_seconds?: number
/**
* How frequently this cache should be checked for stale entries and cleaned up.
*
* *Default*: 30 (twice per minute)
*/
clean_interval_seconds?: number
/**
* When an item is pulled using `.get()`, should the cache expiration time reset?
* _Warning_: Items that are fetched frequently will never be renewed!
*
* *Default*: false
*/
renew_on_get?: boolean
}
export class MemCache<T = unknown> {
private static __all: MemCache[] = []
readonly expiration_period_seconds: number
readonly interval_ms: number
readonly renew_on_get: boolean
private interval_id!: number
constructor({
clean_interval_seconds = 30,
expiration_period_seconds = 120,
renew_on_get = false,
}: MemCacheOptions = {}) {
this.expiration_period_seconds = expiration_period_seconds * 1000;
this.interval_ms = clean_interval_seconds * 1000;
this.renew_on_get = renew_on_get;
MemCache.__all.push(this);
if(!Deno.args.includes('build')) this.queue();
}
private _entries: Map<string, MemCacheEntry<T>> = new Map<string, MemCacheEntry<T>>()
get size() {
return this._entries.size;
}
get snapshot() {
return this._entries.keys().filter((_, i) => i < 20).map(key => ({
key,
value: this.get(key) as T
}))
}
/**
* Get the value of a cached item
*/
get(key: string): T | null {
if(!this._entries.has(key)) return null;
const entry = this._entries.get(key)!;
if(this.renew_on_get) entry.cached_at = Date.now();
return entry.data as T;
}
/**
* Checks whether a cached item exists
*/
has(key: string) {
return this._entries.has(key);
}
/**
* If a value exists, return it. Otherwise, set it and return it.
*/
async passthrough(key: string, value: PassthroughValue<T> | PassthroughFunction<T>) {
if(this.has(key)) return this.get(key);
let result;
if(typeof value === 'function') {
result = await (value as PassthroughFunction<T>)();
} else {
result = value as T | undefined | null;
}
if(result !== null && result !== undefined) {
return this.set(key, result);
}
return null;
}
/**
* Sets the cached item and returns it
*/
set(key: string, value: T) {
this._entries.set(key, { data: value, cached_at: Date.now() });
return value;
}
/**
* Removes the cached item and returns it (if it exists)
*/
delete(key: string) {
if(!this._entries.has(key)) return null;
const value = this.get(key);
this._entries.delete(key);
return value;
}
/**
* Clears all entries from the cache
*/
clear() {
this._entries.clear();
}
/**
* Returns an iterable list of key, value pairs
*/
get entries() {
return this._entries.entries.bind(this._entries);
}
/**
* Returns an iterable list of keys
*/
get keys() {
return this._entries.keys.bind(this._entries);
}
/**
* Returns an iterable list of keys
*/
get values() {
return this._entries.values.bind(this._entries);
}
private queue() {
this.interval_id = setTimeout(this.clean.bind(this), this.interval_ms);
}
private clean() {
clearTimeout(this.interval_id);
const expiration_timestamp = Date.now();
let deleted = 0;
let oldest = Date.now();
for(const [key, { cached_at }] of this._entries.entries()) {
if(oldest < cached_at) oldest = cached_at;
if(cached_at + (this.expiration_period_seconds) < expiration_timestamp) {
this._entries.delete(key);
deleted++;
}
}
this.queue();
}
}
type PassthroughValueBasic<T> = T | null | undefined
export type PassthroughValue<T> = PassthroughValueBasic<T> | Promise<PassthroughValueBasic<T>>
export type PassthroughFunction<T> = (...params: unknown[]) => PassthroughValue<T>;