UNPKG

chronik-cache

Version:

A cache helper for chronik-client

325 lines (288 loc) 10.3 kB
// Copyright (c) 2024 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. import { Level } from 'level'; import FailoverHandler from './failover'; import Logger from './Logger'; const BIGINT_TAG = '__chronikCacheBigint__'; function serializeValue(value: any): any { if (typeof value === 'bigint') { return { [BIGINT_TAG]: value.toString() }; } if (Array.isArray(value)) { return value.map(serializeValue); } if (value !== null && typeof value === 'object') { const serialized: Record<string, any> = {}; for (const [key, nestedValue] of Object.entries(value)) { serialized[key] = serializeValue(nestedValue); } return serialized; } return value; } function deserializeValue(value: any): any { if (Array.isArray(value)) { return value.map(deserializeValue); } if (value !== null && typeof value === 'object') { const keys = Object.keys(value); if ( keys.length === 1 && keys[0] === BIGINT_TAG && typeof value[BIGINT_TAG] === 'string' ) { return BigInt(value[BIGINT_TAG]); } const deserialized: Record<string, any> = {}; for (const [key, nestedValue] of Object.entries(value)) { deserialized[key] = deserializeValue(nestedValue); } return deserialized; } return value; } function getSerializedSize(value: any): number { return Buffer.byteLength(JSON.stringify(serializeValue(value)), 'utf8'); } interface DbUtilsOptions { valueEncoding?: string; maxCacheSize?: number; enableLogging?: boolean; failoverOptions?: any; } interface CacheEntry { identifier: string; accessCount: number; size: number; } interface MetaData { pageCount: number; } export default class DbUtils { public db: Level<string, any>; private maxCacheSize: number; public cacheDir: string; private failover: FailoverHandler; private logger: Logger; /** * @param cacheDir Database file path * @param options Optional parameters */ constructor(cacheDir: string, options: DbUtilsOptions = {}) { const { maxCacheSize = Infinity, enableLogging = false } = options; this.db = new Level(cacheDir, { valueEncoding: { format: 'utf8', encode: (obj: any) => JSON.stringify(serializeValue(obj)), decode: (str: string) => deserializeValue(JSON.parse(str)), } }); this.maxCacheSize = maxCacheSize; this.cacheDir = cacheDir; this.failover = new FailoverHandler(options.failoverOptions || {}); this.logger = new Logger(enableLogging); } /** * Unified DB read operation handler */ async get(key: string, defaultValue: any = null): Promise<any> { return await this.failover.handleDbOperation( async () => { try { const value = await this.db.get(key); return typeof value === 'undefined' ? defaultValue : value; } catch (error: any) { if (error.notFound) { return defaultValue; } throw error; } }, `DB get operation for ${key}` ); } /** * Unified DB write operation handler */ async put(key: string, value: any): Promise<void> { return await this.failover.handleDbOperation( async () => { await this.db.put(key, value); }, `DB put operation for ${key}` ); } /** * Unified DB delete operation handler */ async del(key: string): Promise<void> { return await this.failover.handleDbOperation( async () => { await this.db.del(key); }, `DB delete operation for ${key}` ); } /** * Calculate cache size */ async calculateCacheSize(): Promise<number> { const result = await this.failover.handleDbOperation( async () => { let totalSize = 0; for await (const [key, value] of this.db.iterator()) { // 同时计算key和value的字节大小 totalSize += Buffer.byteLength(key, 'utf8'); totalSize += getSerializedSize(value); } return totalSize; }, 'Calculate cache size operation' ); return result as number; } /** * Provide iterator interface for reading all key-value pairs */ async *iterator(): AsyncGenerator<[string, any], void, unknown> { try { for await (const [key, value] of this.db.iterator()) { yield [key, value]; } } catch (error) { this.logger.error('Error in iterator:', error); throw error; } } /** * Clear database */ async clear(): Promise<void> { return await this.failover.handleDbOperation( async () => { await this.db.clear(); }, 'Clear database operation' ); } /** * Clean least accessed entries in cache */ async cleanLeastAccessedCache(): Promise<void> { try { let currentSize = await this.calculateCacheSize(); this.logger.log(`Initial cache size: ${currentSize} bytes, max allowed size: ${this.maxCacheSize}`); const entries: CacheEntry[] = []; for await (const [key, value] of this.db.iterator()) { const size = getSerializedSize(value); // 获取全局元数据中的访问计数 const metadata = await this.getGlobalMetadata(key); const accessCount = metadata?.accessCount || 0; this.logger.log(`[Debug] Found entry: ${key}, accessCount: ${accessCount}, size: ${size}`); entries.push({ identifier: key, accessCount, size }); } entries.sort((a, b) => a.accessCount - b.accessCount); let i = 0; while (currentSize > this.maxCacheSize && i < entries.length) { const entry = entries[i]; this.logger.log(`[Debug] Attempting to remove entry: ${entry.identifier}, currentSize: ${currentSize}`); await this.del(entry.identifier); // 同时删除对应的全局元数据 await this.del(`metadata:${entry.identifier}`); currentSize -= entry.size; this.logger.log(`Cleaned cache for ${entry.identifier}, access count: ${entry.accessCount}`); i++; } if (currentSize > this.maxCacheSize) { this.logger.log('[Debug] Cache is still above limit after cleanup. Aborting update.'); throw new Error('Cache size remains above the limit, abort update to avoid concurrent removal and updating.'); } this.logger.log(`Cache cleanup completed, removed ${i} entries, final size: ${currentSize} bytes`); } catch (error) { this.logger.error('Error cleaning cache:', error); throw error; } } /** * Clear all cache */ async clearAll(): Promise<void> { try { await this.db.clear(); } catch (error) { this.logger.error('Error clearing all keys:', error); } } /** * Delete data stored in a paginated manner. */ async deletePaginated(keyBase: string): Promise<void> { return await this.failover.handleDbOperation( async () => { try { const meta: MetaData = await this.db.get(`${keyBase}:meta`); if (meta) { const { pageCount } = meta; for (let i = 0; i < pageCount; i++) { await this.del(`${keyBase}:${i}`); } await this.del(`${keyBase}:meta`); return; } } catch { // If meta not found, fall through. } await this.del(keyBase); }, `DB delete paginated operation for ${keyBase}` ); } /** * Unified DB token cache delete operation handler with pagination support */ async clearTokenCache(tokenId: string): Promise<void> { return await this.failover.handleDbOperation( async () => { await this.deletePaginated(`${tokenId}:txMap`); await this.deletePaginated(`${tokenId}:txOrder`); await this.del(`metadata:token:${tokenId}`); }, `DB clear token cache operation for ${tokenId}` ); } /** * 添加用于存储全局元数据的方法 */ async updateGlobalMetadata(key: string, data: any): Promise<void> { return await this.failover.handleDbOperation( async () => { await this.db.put(`metadata:${key}`, data); }, `DB update global metadata operation for ${key}` ); } /** * 添加用于获取全局元数据的方法 */ async getGlobalMetadata(key: string, defaultValue: any = null): Promise<any> { return await this.failover.handleDbOperation( async () => { try { const value = await this.db.get(`metadata:${key}`); return typeof value === 'undefined' ? defaultValue : value; } catch (error: any) { if (error.notFound) { return defaultValue; } throw error; } }, `DB get global metadata operation for ${key}` ); } }