Files

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;
}
}