Spaces:
Build error
Build error
File size: 6,579 Bytes
0bfe2e3 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
import { createLogger } from './logger';
import { Env } from './env';
const logger = createLogger('cache');
function formatBytes(bytes: number, decimals: number = 2): string {
if (!+bytes) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
}
class CacheItem<T> {
constructor(
public value: T,
public lastAccessed: number,
public ttl: number // Time-To-Live in milliseconds
) {}
}
export class Cache<K, V> {
private static instances: Map<string, any> = new Map();
private cache: Map<K, CacheItem<V>>;
private maxSize: number;
private static isStatsLoopRunning: boolean = false;
private constructor(maxSize: number) {
this.cache = new Map<K, CacheItem<V>>();
this.maxSize = maxSize;
Cache.startStatsLoop();
}
private static startStatsLoop() {
if (Cache.isStatsLoopRunning) {
return;
}
Cache.isStatsLoopRunning = true;
const interval = Env.LOG_CACHE_STATS_INTERVAL * 60 * 1000; // Convert minutes to ms
const runAndReschedule = () => {
Cache.stats();
const delay = interval - (Date.now() % interval);
setTimeout(runAndReschedule, delay).unref();
};
const initialDelay = interval - (Date.now() % interval);
setTimeout(runAndReschedule, initialDelay).unref();
}
/**
* Get an instance of the cache with a specific name
* @param name Unique identifier for this cache instance
* @param maxSize Maximum size of the cache (only used when creating a new instance)
*/
public static getInstance<K, V>(
name: string,
maxSize: number = Env.DEFAULT_MAX_CACHE_SIZE
): Cache<K, V> {
if (!this.instances.has(name)) {
logger.debug(`Creating new cache instance: ${name}`);
this.instances.set(name, new Cache<K, V>(maxSize));
}
return this.instances.get(name) as Cache<K, V>;
}
/**
* Gets the statistics of the cache in use by the program. returns a formatted string containing a list of all cache instances
* and their currently held items, max items
*/
public static stats() {
if (!this.instances || this.instances.size === 0) {
return;
}
let grandTotalItems = 0;
let grandTotalSize = 0;
const header = [
'ββββββββββββββββββββββββ€βββββββββββ€ββββββββββββββββββ€ββββββββββββββββββ',
'β Cache Name β Items β Max Size β Estimated Size β',
'β βββββββββββββββββββββββͺβββββββββββͺββββββββββββββββββͺββββββββββββββββββ£',
];
const bodyLines = Array.from(this.instances.entries()).map(
([name, cache]) => {
let instanceSize = 0;
for (const item of cache.cache.values()) {
try {
// Estimate object size by getting the byte length of its JSON string representation.
// This is an approximation but is effective for many use cases.
instanceSize += Buffer.byteLength(JSON.stringify(item), 'utf8');
} catch (e) {
// Could fail on circular references. In that case, we add 0.
instanceSize += 0;
}
}
grandTotalItems += cache.cache.size;
grandTotalSize += instanceSize;
const nameStr = name.padEnd(20);
const itemsStr = String(cache.cache.size).padEnd(8);
const maxSizeStr = String(cache.maxSize ?? '-').padEnd(15);
const estSizeStr = formatBytes(instanceSize).padEnd(15);
return `β ${nameStr} β ${itemsStr} β ${maxSizeStr} β ${estSizeStr} β`;
}
);
const footer = [
'ββββββββββββββββββββββββ§βββββββββββ§ββββββββββββββββββ§ββββββββββββββββββ',
` Summary: ${this.instances.size} cache instance(s), ${grandTotalItems} total items, Est. Total Size: ${formatBytes(grandTotalSize)}`,
];
const lines = [...header, ...bodyLines, ...footer];
logger.verbose(lines.join('\n'));
}
/**
* Wrap a function with caching logic by immediately executing it with the provided arguments.
* @param fn The function to wrap
* @param key A unique key for caching
* @param ttl Time-To-Live in seconds for the cached value
* @param args The arguments to pass to the function
*/
async wrap<T extends (...args: any[]) => any>(
fn: T,
key: K,
ttl: number,
...args: Parameters<T>
): Promise<ReturnType<T>> {
const cachedValue = this.get(key);
if (cachedValue !== undefined) {
return cachedValue as ReturnType<T>;
}
const result = await fn(...args);
this.set(key, result, ttl);
return result;
}
get(key: K, updateTTL: boolean = true): V | undefined {
const item = this.cache.get(key);
if (item) {
const now = Date.now();
if (now - item.lastAccessed > item.ttl) {
this.cache.delete(key);
return undefined;
}
if (updateTTL) {
item.lastAccessed = now;
}
return item.value;
}
return undefined;
}
/**
* Set a value in the cache with a specific TTL
* @param key The key to set the value for
* @param value The value to set
* @param ttl The TTL in seconds
*/
set(key: K, value: V, ttl: number): void {
if (this.cache.size >= this.maxSize) {
this.evict();
}
this.cache.set(key, new CacheItem<V>(value, Date.now(), ttl * 1000));
}
/**
* Update the value of an existing key in the cache without changing the TTL
* @param key The key to update
* @param value The new value
*/
update(key: K, value: V): void {
const item = this.cache.get(key);
if (item) {
item.value = value;
}
}
clear(): void {
this.cache.clear();
}
private evict(): void {
let oldestKey: K | undefined;
let oldestTime = Infinity;
for (const [key, item] of this.cache.entries()) {
if (item.lastAccessed < oldestTime) {
oldestTime = item.lastAccessed;
oldestKey = key;
}
}
if (oldestKey !== undefined) {
this.cache.delete(oldestKey);
}
}
}
|