chronik-cache
Version:
A cache helper for chronik-client
253 lines (252 loc) • 9.19 kB
JavaScript
"use strict";
// 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.
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const level_1 = require("level");
const failover_1 = __importDefault(require("./failover"));
const Logger_1 = __importDefault(require("./Logger"));
const BIGINT_TAG = '__chronikCacheBigint__';
function serializeValue(value) {
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 = {};
for (const [key, nestedValue] of Object.entries(value)) {
serialized[key] = serializeValue(nestedValue);
}
return serialized;
}
return value;
}
function deserializeValue(value) {
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 = {};
for (const [key, nestedValue] of Object.entries(value)) {
deserialized[key] = deserializeValue(nestedValue);
}
return deserialized;
}
return value;
}
function getSerializedSize(value) {
return Buffer.byteLength(JSON.stringify(serializeValue(value)), 'utf8');
}
class DbUtils {
/**
* @param cacheDir Database file path
* @param options Optional parameters
*/
constructor(cacheDir, options = {}) {
const { maxCacheSize = Infinity, enableLogging = false } = options;
this.db = new level_1.Level(cacheDir, {
valueEncoding: {
format: 'utf8',
encode: (obj) => JSON.stringify(serializeValue(obj)),
decode: (str) => deserializeValue(JSON.parse(str)),
}
});
this.maxCacheSize = maxCacheSize;
this.cacheDir = cacheDir;
this.failover = new failover_1.default(options.failoverOptions || {});
this.logger = new Logger_1.default(enableLogging);
}
/**
* Unified DB read operation handler
*/
async get(key, defaultValue = null) {
return await this.failover.handleDbOperation(async () => {
try {
const value = await this.db.get(key);
return typeof value === 'undefined' ? defaultValue : value;
}
catch (error) {
if (error.notFound) {
return defaultValue;
}
throw error;
}
}, `DB get operation for ${key}`);
}
/**
* Unified DB write operation handler
*/
async put(key, value) {
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) {
return await this.failover.handleDbOperation(async () => {
await this.db.del(key);
}, `DB delete operation for ${key}`);
}
/**
* Calculate cache size
*/
async calculateCacheSize() {
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;
}
/**
* Provide iterator interface for reading all key-value pairs
*/
async *iterator() {
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() {
return await this.failover.handleDbOperation(async () => {
await this.db.clear();
}, 'Clear database operation');
}
/**
* Clean least accessed entries in cache
*/
async cleanLeastAccessedCache() {
try {
let currentSize = await this.calculateCacheSize();
this.logger.log(`Initial cache size: ${currentSize} bytes, max allowed size: ${this.maxCacheSize}`);
const entries = [];
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() {
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) {
return await this.failover.handleDbOperation(async () => {
try {
const meta = 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) {
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, data) {
return await this.failover.handleDbOperation(async () => {
await this.db.put(`metadata:${key}`, data);
}, `DB update global metadata operation for ${key}`);
}
/**
* 添加用于获取全局元数据的方法
*/
async getGlobalMetadata(key, defaultValue = null) {
return await this.failover.handleDbOperation(async () => {
try {
const value = await this.db.get(`metadata:${key}`);
return typeof value === 'undefined' ? defaultValue : value;
}
catch (error) {
if (error.notFound) {
return defaultValue;
}
throw error;
}
}, `DB get global metadata operation for ${key}`);
}
}
exports.default = DbUtils;