298 lines
7.2 KiB
TypeScript
298 lines
7.2 KiB
TypeScript
/**
|
|
* Cache system similar to Python's CacheDict from Tryton
|
|
* Implements LRU (Least Recently Used) cache using JavaScript Map
|
|
* TypeScript version
|
|
*/
|
|
|
|
export type CacheFactory<T> = () => T;
|
|
|
|
export class CacheDict<K = any, V = any> extends Map<K, V> {
|
|
private cacheLen: number;
|
|
private defaultFactory: CacheFactory<V> | null;
|
|
|
|
/**
|
|
* Create a new CacheDict
|
|
*/
|
|
constructor(
|
|
cacheLen: number = 10,
|
|
defaultFactory: CacheFactory<V> | null = null
|
|
) {
|
|
super();
|
|
this.cacheLen = cacheLen;
|
|
this.defaultFactory = defaultFactory;
|
|
}
|
|
|
|
/**
|
|
* Set a key-value pair and maintain LRU order
|
|
*/
|
|
override set(key: K, value: V): this {
|
|
// If key exists, delete it first to move to end
|
|
if (this.has(key)) {
|
|
this.delete(key);
|
|
}
|
|
|
|
super.set(key, value);
|
|
|
|
// Remove oldest entries if cache is full
|
|
while (this.size > this.cacheLen) {
|
|
const firstKey = this.keys().next().value;
|
|
if (firstKey !== undefined) {
|
|
this.delete(firstKey);
|
|
}
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Get a value and move it to end (most recently used)
|
|
*/
|
|
override get(key: K): V | undefined {
|
|
if (this.has(key)) {
|
|
const value = super.get(key)!;
|
|
// Move to end by re-setting
|
|
this.delete(key);
|
|
super.set(key, value);
|
|
return value;
|
|
}
|
|
|
|
// Handle missing key with default factory
|
|
if (this.defaultFactory) {
|
|
const value = this.defaultFactory();
|
|
this.set(key, value);
|
|
return value;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Override has() to update LRU order on access
|
|
*/
|
|
override has(key: K): boolean {
|
|
const exists = super.has(key);
|
|
if (exists) {
|
|
// Move to end on access
|
|
const value = super.get(key)!;
|
|
this.delete(key);
|
|
super.set(key, value);
|
|
}
|
|
return exists;
|
|
}
|
|
|
|
/**
|
|
* Get current cache size
|
|
*/
|
|
get length(): number {
|
|
return this.size;
|
|
}
|
|
|
|
/**
|
|
* Clear all items from cache
|
|
*/
|
|
override clear(): void {
|
|
super.clear();
|
|
}
|
|
|
|
/**
|
|
* Convert cache to array for debugging
|
|
*/
|
|
toArray(): Array<[K, V]> {
|
|
return Array.from(this.entries());
|
|
}
|
|
}
|
|
|
|
export interface CacheEntry<T = any> {
|
|
expire: Date;
|
|
value: T;
|
|
}
|
|
|
|
export interface CacheStats {
|
|
totalPrefixes: number;
|
|
totalEntries: number;
|
|
prefixes: Record<string, number>;
|
|
}
|
|
|
|
/**
|
|
* Advanced cache for Tryton RPC with expiration support
|
|
*/
|
|
export class TrytonCache {
|
|
private store: CacheDict<string, CacheDict<string, CacheEntry>>;
|
|
private cacheLen: number;
|
|
|
|
constructor(cacheLen: number = 1024) {
|
|
this.cacheLen = cacheLen;
|
|
this.store = new CacheDict<string, CacheDict<string, CacheEntry>>(
|
|
cacheLen,
|
|
() => new CacheDict<string, CacheEntry>(cacheLen)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if a prefix is cached
|
|
*/
|
|
cached(prefix: string): boolean {
|
|
return this.store.has(prefix);
|
|
}
|
|
|
|
/**
|
|
* Set cache entry with expiration
|
|
*/
|
|
set(prefix: string, key: string, expire: number | Date, value: any): void {
|
|
let expiration: Date;
|
|
|
|
if (typeof expire === "number") {
|
|
// Assume seconds, convert to Date
|
|
expiration = new Date(Date.now() + expire * 1000);
|
|
} else if (expire instanceof Date) {
|
|
expiration = expire;
|
|
} else {
|
|
throw new Error("Invalid expiration type");
|
|
}
|
|
|
|
// Deep copy value to avoid mutations
|
|
const cachedValue = this._deepCopy(value);
|
|
|
|
this.store.get(prefix)!.set(key, {
|
|
expire: expiration,
|
|
value: cachedValue,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get cached value if not expired
|
|
*/
|
|
get(prefix: string, key: string): any {
|
|
const now = new Date();
|
|
|
|
if (!this.store.has(prefix)) {
|
|
throw new Error("Key not found");
|
|
}
|
|
|
|
const prefixCache = this.store.get(prefix)!;
|
|
if (!prefixCache.has(key)) {
|
|
throw new Error("Key not found");
|
|
}
|
|
|
|
const entry = prefixCache.get(key)!;
|
|
|
|
if (entry.expire < now) {
|
|
prefixCache.delete(key);
|
|
throw new Error("Key expired");
|
|
}
|
|
|
|
console.log(`(cached) ${prefix} ${key}`);
|
|
return this._deepCopy(entry.value);
|
|
}
|
|
|
|
/**
|
|
* Clear cache for a specific prefix or all
|
|
*/
|
|
clear(prefix?: string): void {
|
|
if (prefix) {
|
|
if (this.store.has(prefix)) {
|
|
this.store.get(prefix)!.clear();
|
|
}
|
|
} else {
|
|
this.store.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deep copy objects to prevent mutations
|
|
*/
|
|
private _deepCopy<T>(obj: T): T {
|
|
if (obj === null || typeof obj !== "object") {
|
|
return obj;
|
|
}
|
|
|
|
if (obj instanceof Date) {
|
|
return new Date(obj.getTime()) as T;
|
|
}
|
|
|
|
if (obj instanceof Array) {
|
|
return obj.map((item) => this._deepCopy(item)) as T;
|
|
}
|
|
|
|
// Handle Buffer in Node.js environment
|
|
if (typeof Buffer !== "undefined" && obj instanceof Buffer) {
|
|
return Buffer.from(obj) as T;
|
|
}
|
|
|
|
if (typeof obj === "object") {
|
|
const copy: any = {};
|
|
for (const key in obj) {
|
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
copy[key] = this._deepCopy((obj as any)[key]);
|
|
}
|
|
}
|
|
return copy as T;
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
/**
|
|
* Get cache statistics
|
|
*/
|
|
getStats(): CacheStats {
|
|
const stats: CacheStats = {
|
|
totalPrefixes: this.store.size,
|
|
totalEntries: 0,
|
|
prefixes: {},
|
|
};
|
|
|
|
for (const [prefix, prefixCache] of this.store.entries()) {
|
|
const count = prefixCache.size;
|
|
stats.totalEntries += count;
|
|
stats.prefixes[prefix] = count;
|
|
}
|
|
|
|
return stats;
|
|
}
|
|
|
|
/**
|
|
* Remove expired entries
|
|
*/
|
|
cleanupExpired(): number {
|
|
const now = new Date();
|
|
let removedCount = 0;
|
|
|
|
for (const [prefix, prefixCache] of this.store.entries()) {
|
|
const keysToRemove: string[] = [];
|
|
|
|
for (const [key, entry] of prefixCache.entries()) {
|
|
if (entry.expire < now) {
|
|
keysToRemove.push(key);
|
|
}
|
|
}
|
|
|
|
keysToRemove.forEach((key) => {
|
|
prefixCache.delete(key);
|
|
removedCount++;
|
|
});
|
|
}
|
|
|
|
return removedCount;
|
|
}
|
|
|
|
/**
|
|
* Get cache size in bytes (approximate)
|
|
*/
|
|
getSizeEstimate(): number {
|
|
let size = 0;
|
|
|
|
for (const [prefix, prefixCache] of this.store.entries()) {
|
|
size += prefix.length * 2; // UTF-16 encoding
|
|
|
|
for (const [key, entry] of prefixCache.entries()) {
|
|
size += key.length * 2;
|
|
size += JSON.stringify(entry.value).length * 2;
|
|
size += 24; // Date object overhead
|
|
}
|
|
}
|
|
|
|
return size;
|
|
}
|
|
}
|