First commit: Tryton client using TypeScript
This commit is contained in:
297
cache.ts
Normal file
297
cache.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user