bigbasealpha
Version:
Enterprise-Grade NoSQL Database System with Modular Logger & Offline HSM Security - Complete database platform with professional text-based logging, encryption, caching, indexing, JWT authentication, auto-generated REST API, real-time dashboard, and maste
1,213 lines (949 loc) • 33.2 kB
JavaScript
import { EventEmitter } from 'events';
import { promises as fs } from 'fs';
import { join } from 'path';
/**
* Redis-like Cache Layer for BigBaseAlpha
* High-performance in-memory caching with Redis-compatible operations
*/
export class RedisLikeCache extends EventEmitter {
constructor(config = {}) {
super();
// Logger setup (fallback to default if not provided)
this.logger = config.logger || {
info: (...args) => console.log('[INFO] [REDIS]', ...args),
warn: (...args) => console.warn('[WARN] [REDIS]', ...args),
error: (...args) => console.error('[ERROR] [REDIS]', ...args),
success: (...args) => console.log('[SUCCESS] [REDIS]', ...args),
debug: (...args) => console.log('[DEBUG] [REDIS]', ...args),
process: (...args) => console.log('[PROCESS] [REDIS]', ...args)
};
this.config = {
enabled: config.redis?.enabled !== false,
maxMemory: config.redis?.maxMemory || '256MB',
evictionPolicy: config.redis?.evictionPolicy || 'lru', // lru, lfu, random, volatile-lru
persistence: config.redis?.persistence !== false,
persistenceFile: config.redis?.persistenceFile || 'redis-cache.json',
snapshotInterval: config.redis?.snapshotInterval || 300000, // 5 minutes
maxKeys: config.redis?.maxKeys || 1000000,
defaultTTL: config.redis?.defaultTTL || null,
keyPrefix: config.redis?.keyPrefix || 'bb:',
...(config.redis || {})
};
this.database = null;
this.isInitialized = false;
// In-memory storage
this.store = new Map();
this.expires = new Map(); // TTL tracking
this.accessLog = new Map(); // LRU tracking
this.accessCount = new Map(); // LFU tracking
// Data structures for Redis-like types
this.lists = new Map();
this.sets = new Map();
this.hashes = new Map();
this.sortedSets = new Map();
// Statistics
this.stats = {
hits: 0,
misses: 0,
evictions: 0,
expires: 0,
operations: 0,
memoryUsage: 0,
keyCount: 0,
lastSnapshot: null,
uptime: Date.now()
};
// Background tasks
this.expirationTimer = null;
this.snapshotTimer = null;
this.memoryCheckTimer = null;
// Commands registry (Redis-like) - will be initialized in init()
this.commands = new Map();
}
async init() {
if (this.isInitialized) return;
this.logger.process('Initializing Redis-like Cache Layer...'); if (!this.config.enabled) {
console.log('[WARN] Redis-like cache is disabled in configuration');
return;
}
try {
// Initialize commands first
this._initializeCommands();
// Load persistent data if enabled
if (this.config.persistence) {
await this._loadPersistentData();
}
// Start background tasks
this._startExpirationTask();
this._startSnapshotTask();
this._startMemoryMonitoring();
this.isInitialized = true;
this.stats.uptime = Date.now();
console.log(`[SUCCESS] Redis-like Cache Layer initialized`);
console.log(` - Max Memory: ${this.config.maxMemory}`);
console.log(` - Eviction Policy: ${this.config.evictionPolicy}`);
console.log(` - Persistence: ${this.config.persistence ? 'Enabled' : 'Disabled'}`);
this.emit('cacheInitialized', {
maxMemory: this.config.maxMemory,
evictionPolicy: this.config.evictionPolicy,
persistence: this.config.persistence
});
} catch (error) {
console.error('[ERROR] Failed to initialize Redis-like Cache:', error.message);
throw error;
}
}
setDatabase(database) {
this.database = database;
}
_initializeCommands() {
// String commands
this.commands.set('GET', (...args) => this.get(...args));
this.commands.set('SET', (...args) => this.set(...args));
this.commands.set('DEL', (...args) => this.del(...args));
this.commands.set('EXISTS', (...args) => this.exists(...args));
this.commands.set('EXPIRE', (...args) => this.expire(...args));
this.commands.set('TTL', (...args) => this.ttl(...args));
this.commands.set('INCR', (...args) => this.incr(...args));
this.commands.set('DECR', (...args) => this.decr(...args));
this.commands.set('INCRBY', (...args) => this.incrby(...args));
this.commands.set('DECRBY', (...args) => this.decrby(...args));
this.commands.set('APPEND', (...args) => this.append(...args));
this.commands.set('STRLEN', (...args) => this.strlen(...args));
// List commands
this.commands.set('LPUSH', (...args) => this.lpush(...args));
this.commands.set('RPUSH', (...args) => this.rpush(...args));
this.commands.set('LPOP', (...args) => this.lpop(...args));
this.commands.set('RPOP', (...args) => this.rpop(...args));
this.commands.set('LLEN', (...args) => this.llen(...args));
this.commands.set('LRANGE', (...args) => this.lrange(...args));
this.commands.set('LINDEX', (...args) => this.lindex(...args));
this.commands.set('LSET', (...args) => this.lset(...args));
// Set commands
this.commands.set('SADD', (...args) => this.sadd(...args));
this.commands.set('SREM', (...args) => this.srem(...args));
this.commands.set('SMEMBERS', (...args) => this.smembers(...args));
this.commands.set('SISMEMBER', (...args) => this.sismember(...args));
this.commands.set('SCARD', (...args) => this.scard(...args));
this.commands.set('SUNION', (...args) => this.sunion(...args));
this.commands.set('SINTER', (...args) => this.sinter(...args));
// Hash commands
this.commands.set('HSET', (...args) => this.hset(...args));
this.commands.set('HGET', (...args) => this.hget(...args));
this.commands.set('HDEL', (...args) => this.hdel(...args));
this.commands.set('HGETALL', (...args) => this.hgetall(...args));
this.commands.set('HKEYS', (...args) => this.hkeys(...args));
this.commands.set('HVALS', (...args) => this.hvals(...args));
this.commands.set('HEXISTS', (...args) => this.hexists(...args));
this.commands.set('HLEN', (...args) => this.hlen(...args));
// Sorted Set commands
this.commands.set('ZADD', (...args) => this.zadd(...args));
this.commands.set('ZREM', (...args) => this.zrem(...args));
this.commands.set('ZRANGE', (...args) => this.zrange(...args));
this.commands.set('ZRANK', (...args) => this.zrank(...args));
this.commands.set('ZSCORE', (...args) => this.zscore(...args));
this.commands.set('ZCARD', (...args) => this.zcard(...args));
// Utility commands
this.commands.set('KEYS', (...args) => this.keys(...args));
this.commands.set('FLUSHALL', (...args) => this.flushall(...args));
this.commands.set('INFO', (...args) => this.info(...args));
this.commands.set('PING', (...args) => this.ping(...args));
this.commands.set('SAVE', (...args) => this.save(...args));
}
// =============================================================================
// STRING COMMANDS
// =============================================================================
async get(key) {
this._updateStats('operation');
if (this._isExpired(key)) {
this._expireKey(key);
this._updateStats('miss');
return null;
}
if (this.store.has(key)) {
this._updateAccess(key);
this._updateStats('hit');
return this.store.get(key);
}
this._updateStats('miss');
return null;
}
async set(key, value, options = {}) {
this._updateStats('operation');
const { ttl, nx, xx } = options;
// NX: Only set if key doesn't exist
if (nx && this.store.has(key)) {
return 0;
}
// XX: Only set if key exists
if (xx && !this.store.has(key)) {
return 0;
}
// Check memory limits before setting
if (this._shouldEvict()) {
this._evictKeys();
}
this.store.set(key, value);
this._updateAccess(key);
// Set TTL if provided
if (ttl || this.config.defaultTTL) {
this.expires.set(key, Date.now() + (ttl || this.config.defaultTTL));
}
this._updateStats('keyCount');
this.emit('keySet', { key, value, ttl });
return 1;
}
async del(...keys) {
this._updateStats('operation');
let deleted = 0;
for (const key of keys) {
if (this.store.has(key)) {
this.store.delete(key);
this.expires.delete(key);
this.accessLog.delete(key);
this.accessCount.delete(key);
this._cleanupDataStructures(key);
deleted++;
}
}
this._updateStats('keyCount');
this.emit('keysDeleted', { keys, deleted });
return deleted;
}
async exists(...keys) {
this._updateStats('operation');
let count = 0;
for (const key of keys) {
if (!this._isExpired(key) && this.store.has(key)) {
count++;
}
}
return count;
}
async expire(key, seconds) {
this._updateStats('operation');
if (!this.store.has(key)) {
return 0;
}
this.expires.set(key, Date.now() + (seconds * 1000));
return 1;
}
async ttl(key) {
this._updateStats('operation');
if (!this.store.has(key)) {
return -2; // Key doesn't exist
}
if (!this.expires.has(key)) {
return -1; // Key exists but no TTL
}
const expireTime = this.expires.get(key);
const remaining = Math.ceil((expireTime - Date.now()) / 1000);
return remaining > 0 ? remaining : -2;
}
async incr(key) {
return this.incrby(key, 1);
}
async decr(key) {
return this.decrby(key, 1);
}
async incrby(key, increment) {
this._updateStats('operation');
const current = this.store.get(key);
const value = current ? parseInt(current) : 0;
if (isNaN(value)) {
throw new Error('Value is not an integer');
}
const newValue = value + increment;
await this.set(key, newValue.toString());
return newValue;
}
async decrby(key, decrement) {
return this.incrby(key, -decrement);
}
async append(key, value) {
this._updateStats('operation');
const current = this.store.get(key) || '';
const newValue = current + value;
await this.set(key, newValue);
return newValue.length;
}
async strlen(key) {
this._updateStats('operation');
const value = await this.get(key);
return value ? value.length : 0;
}
// =============================================================================
// LIST COMMANDS
// =============================================================================
async lpush(key, ...elements) {
this._updateStats('operation');
if (!this.lists.has(key)) {
this.lists.set(key, []);
}
const list = this.lists.get(key);
list.unshift(...elements);
this._updateAccess(key);
return list.length;
}
async rpush(key, ...elements) {
this._updateStats('operation');
if (!this.lists.has(key)) {
this.lists.set(key, []);
}
const list = this.lists.get(key);
list.push(...elements);
this._updateAccess(key);
return list.length;
}
async lpop(key) {
this._updateStats('operation');
const list = this.lists.get(key);
if (!list || list.length === 0) {
return null;
}
this._updateAccess(key);
return list.shift();
}
async rpop(key) {
this._updateStats('operation');
const list = this.lists.get(key);
if (!list || list.length === 0) {
return null;
}
this._updateAccess(key);
return list.pop();
}
async llen(key) {
this._updateStats('operation');
const list = this.lists.get(key);
return list ? list.length : 0;
}
async lrange(key, start, stop) {
this._updateStats('operation');
const list = this.lists.get(key);
if (!list) return [];
this._updateAccess(key);
// Handle negative indices
const len = list.length;
start = start < 0 ? Math.max(0, len + start) : Math.min(start, len);
stop = stop < 0 ? Math.max(-1, len + stop) : Math.min(stop, len - 1);
return list.slice(start, stop + 1);
}
async lindex(key, index) {
this._updateStats('operation');
const list = this.lists.get(key);
if (!list) return null;
this._updateAccess(key);
const len = list.length;
const actualIndex = index < 0 ? len + index : index;
return actualIndex >= 0 && actualIndex < len ? list[actualIndex] : null;
}
async lset(key, index, element) {
this._updateStats('operation');
const list = this.lists.get(key);
if (!list) {
throw new Error('No such key');
}
const len = list.length;
const actualIndex = index < 0 ? len + index : index;
if (actualIndex < 0 || actualIndex >= len) {
throw new Error('Index out of range');
}
list[actualIndex] = element;
this._updateAccess(key);
return 'OK';
}
// =============================================================================
// SET COMMANDS
// =============================================================================
async sadd(key, ...members) {
this._updateStats('operation');
if (!this.sets.has(key)) {
this.sets.set(key, new Set());
}
const set = this.sets.get(key);
let added = 0;
for (const member of members) {
if (!set.has(member)) {
set.add(member);
added++;
}
}
this._updateAccess(key);
return added;
}
async srem(key, ...members) {
this._updateStats('operation');
const set = this.sets.get(key);
if (!set) return 0;
let removed = 0;
for (const member of members) {
if (set.delete(member)) {
removed++;
}
}
this._updateAccess(key);
return removed;
}
async smembers(key) {
this._updateStats('operation');
const set = this.sets.get(key);
if (!set) return [];
this._updateAccess(key);
return Array.from(set);
}
async sismember(key, member) {
this._updateStats('operation');
const set = this.sets.get(key);
if (!set) return 0;
this._updateAccess(key);
return set.has(member) ? 1 : 0;
}
async scard(key) {
this._updateStats('operation');
const set = this.sets.get(key);
return set ? set.size : 0;
}
async sunion(...keys) {
this._updateStats('operation');
const result = new Set();
for (const key of keys) {
const set = this.sets.get(key);
if (set) {
this._updateAccess(key);
for (const member of set) {
result.add(member);
}
}
}
return Array.from(result);
}
async sinter(...keys) {
this._updateStats('operation');
if (keys.length === 0) return [];
let result = this.sets.get(keys[0]);
if (!result) return [];
result = new Set(result);
for (let i = 1; i < keys.length; i++) {
const set = this.sets.get(keys[i]);
if (!set) return [];
this._updateAccess(keys[i]);
const intersection = new Set();
for (const member of result) {
if (set.has(member)) {
intersection.add(member);
}
}
result = intersection;
}
this._updateAccess(keys[0]);
return Array.from(result);
}
// =============================================================================
// HASH COMMANDS
// =============================================================================
async hset(key, field, value) {
this._updateStats('operation');
if (!this.hashes.has(key)) {
this.hashes.set(key, new Map());
}
const hash = this.hashes.get(key);
const isNew = !hash.has(field);
hash.set(field, value);
this._updateAccess(key);
return isNew ? 1 : 0;
}
async hget(key, field) {
this._updateStats('operation');
const hash = this.hashes.get(key);
if (!hash) return null;
this._updateAccess(key);
return hash.get(field) || null;
}
async hdel(key, ...fields) {
this._updateStats('operation');
const hash = this.hashes.get(key);
if (!hash) return 0;
let deleted = 0;
for (const field of fields) {
if (hash.delete(field)) {
deleted++;
}
}
this._updateAccess(key);
return deleted;
}
async hgetall(key) {
this._updateStats('operation');
const hash = this.hashes.get(key);
if (!hash) return {};
this._updateAccess(key);
const result = {};
for (const [field, value] of hash) {
result[field] = value;
}
return result;
}
async hkeys(key) {
this._updateStats('operation');
const hash = this.hashes.get(key);
if (!hash) return [];
this._updateAccess(key);
return Array.from(hash.keys());
}
async hvals(key) {
this._updateStats('operation');
const hash = this.hashes.get(key);
if (!hash) return [];
this._updateAccess(key);
return Array.from(hash.values());
}
async hexists(key, field) {
this._updateStats('operation');
const hash = this.hashes.get(key);
if (!hash) return 0;
this._updateAccess(key);
return hash.has(field) ? 1 : 0;
}
async hlen(key) {
this._updateStats('operation');
const hash = this.hashes.get(key);
return hash ? hash.size : 0;
}
// =============================================================================
// SORTED SET COMMANDS (Simplified)
// =============================================================================
async zadd(key, score, member) {
this._updateStats('operation');
if (!this.sortedSets.has(key)) {
this.sortedSets.set(key, new Map());
}
const zset = this.sortedSets.get(key);
const isNew = !zset.has(member);
zset.set(member, score);
this._updateAccess(key);
return isNew ? 1 : 0;
}
async zrem(key, ...members) {
this._updateStats('operation');
const zset = this.sortedSets.get(key);
if (!zset) return 0;
let removed = 0;
for (const member of members) {
if (zset.delete(member)) {
removed++;
}
}
this._updateAccess(key);
return removed;
}
async zrange(key, start, stop) {
this._updateStats('operation');
const zset = this.sortedSets.get(key);
if (!zset) return [];
this._updateAccess(key);
// Sort by score
const sorted = Array.from(zset.entries()).sort((a, b) => a[1] - b[1]);
const members = sorted.map(entry => entry[0]);
// Handle range
const len = members.length;
start = start < 0 ? Math.max(0, len + start) : Math.min(start, len);
stop = stop < 0 ? Math.max(-1, len + stop) : Math.min(stop, len - 1);
return members.slice(start, stop + 1);
}
async zscore(key, member) {
this._updateStats('operation');
const zset = this.sortedSets.get(key);
if (!zset) return null;
this._updateAccess(key);
return zset.get(member) || null;
}
async zcard(key) {
this._updateStats('operation');
const zset = this.sortedSets.get(key);
return zset ? zset.size : 0;
}
async zrank(key, member) {
this._updateStats('operation');
const zset = this.sortedSets.get(key);
if (!zset) return null;
this._updateAccess(key);
// Sort by score to get rank
const sorted = Array.from(zset.entries()).sort((a, b) => a[1] - b[1]);
const index = sorted.findIndex(entry => entry[0] === member);
return index >= 0 ? index : null;
}
// =============================================================================
// UTILITY COMMANDS
// =============================================================================
async keys(pattern = '*') {
this._updateStats('operation');
const allKeys = Array.from(this.store.keys());
if (pattern === '*') {
return allKeys;
}
// Simple pattern matching (*, ?)
const regex = new RegExp(
pattern.replace(/\*/g, '.*').replace(/\?/g, '.')
);
return allKeys.filter(key => regex.test(key));
}
async flushall() {
this._updateStats('operation');
this.store.clear();
this.expires.clear();
this.accessLog.clear();
this.accessCount.clear();
this.lists.clear();
this.sets.clear();
this.hashes.clear();
this.sortedSets.clear();
this.stats.keyCount = 0;
this.emit('flushed');
return 'OK';
}
async ping(message = 'PONG') {
this._updateStats('operation');
return message;
}
async info(section = 'all') {
this._updateStats('operation');
const uptime = Math.floor((Date.now() - this.stats.uptime) / 1000);
const hitRate = this.stats.hits + this.stats.misses > 0
? (this.stats.hits / (this.stats.hits + this.stats.misses) * 100).toFixed(2)
: '0.00';
const info = {
server: {
version: '1.0.0',
uptime_in_seconds: uptime,
uptime_in_days: Math.floor(uptime / 86400)
},
memory: {
used_memory: this.stats.memoryUsage,
max_memory: this.config.maxMemory,
eviction_policy: this.config.evictionPolicy
},
stats: {
total_operations: this.stats.operations,
keyspace_hits: this.stats.hits,
keyspace_misses: this.stats.misses,
hit_rate: `${hitRate}%`,
evicted_keys: this.stats.evictions,
expired_keys: this.stats.expires,
current_keys: this.stats.keyCount
},
keyspace: {
keys: this.store.size,
lists: this.lists.size,
sets: this.sets.size,
hashes: this.hashes.size,
sorted_sets: this.sortedSets.size
}
};
if (section === 'all') {
return info;
}
return info[section] || {};
}
async save() {
this._updateStats('operation');
if (!this.config.persistence) {
return 'Persistence is disabled';
}
await this._saveSnapshot();
return 'OK';
}
// =============================================================================
// COMMAND EXECUTION
// =============================================================================
async executeCommand(command, ...args) {
const cmd = command.toUpperCase();
if (!this.commands.has(cmd)) {
throw new Error(`Unknown command: ${command}`);
}
try {
const result = await this.commands.get(cmd)(...args);
this.emit('commandExecuted', {
command: cmd,
args,
result,
timestamp: Date.now()
});
return result;
} catch (error) {
this.emit('commandError', {
command: cmd,
args,
error: error.message,
timestamp: Date.now()
});
throw error;
}
}
// =============================================================================
// PRIVATE HELPER METHODS
// =============================================================================
_isExpired(key) {
if (!this.expires.has(key)) {
return false;
}
return Date.now() > this.expires.get(key);
}
_expireKey(key) {
this.store.delete(key);
this.expires.delete(key);
this.accessLog.delete(key);
this.accessCount.delete(key);
this._cleanupDataStructures(key);
this.stats.expires++;
this.emit('keyExpired', { key });
}
_cleanupDataStructures(key) {
this.lists.delete(key);
this.sets.delete(key);
this.hashes.delete(key);
this.sortedSets.delete(key);
}
_updateAccess(key) {
this.accessLog.set(key, Date.now());
this.accessCount.set(key, (this.accessCount.get(key) || 0) + 1);
}
_updateStats(type) {
switch (type) {
case 'hit':
this.stats.hits++;
break;
case 'miss':
this.stats.misses++;
break;
case 'operation':
this.stats.operations++;
break;
case 'keyCount':
this.stats.keyCount = this.store.size;
break;
}
}
_shouldEvict() {
const maxMemoryBytes = this._parseMemorySize(this.config.maxMemory);
const currentMemory = this._estimateMemoryUsage();
return currentMemory > maxMemoryBytes || this.store.size > this.config.maxKeys;
}
_evictKeys() {
const toEvict = Math.max(1, Math.floor(this.store.size * 0.1)); // Evict 10%
let evicted = 0;
const keys = Array.from(this.store.keys());
switch (this.config.evictionPolicy) {
case 'lru':
this._evictLRU(toEvict);
break;
case 'lfu':
this._evictLFU(toEvict);
break;
case 'random':
this._evictRandom(toEvict);
break;
case 'volatile-lru':
this._evictVolatileLRU(toEvict);
break;
default:
this._evictLRU(toEvict);
}
}
_evictLRU(count) {
const sortedByAccess = Array.from(this.accessLog.entries())
.sort((a, b) => a[1] - b[1]);
for (let i = 0; i < Math.min(count, sortedByAccess.length); i++) {
const key = sortedByAccess[i][0];
this.del(key);
this.stats.evictions++;
}
}
_evictLFU(count) {
const sortedByCount = Array.from(this.accessCount.entries())
.sort((a, b) => a[1] - b[1]);
for (let i = 0; i < Math.min(count, sortedByCount.length); i++) {
const key = sortedByCount[i][0];
this.del(key);
this.stats.evictions++;
}
}
_evictRandom(count) {
const keys = Array.from(this.store.keys());
for (let i = 0; i < Math.min(count, keys.length); i++) {
const randomIndex = Math.floor(Math.random() * keys.length);
const key = keys[randomIndex];
this.del(key);
this.stats.evictions++;
}
}
_evictVolatileLRU(count) {
const volatileKeys = Array.from(this.expires.keys());
const sortedByAccess = volatileKeys
.map(key => [key, this.accessLog.get(key) || 0])
.sort((a, b) => a[1] - b[1]);
for (let i = 0; i < Math.min(count, sortedByAccess.length); i++) {
const key = sortedByAccess[i][0];
this.del(key);
this.stats.evictions++;
}
}
_parseMemorySize(size) {
const units = { B: 1, KB: 1024, MB: 1024 * 1024, GB: 1024 * 1024 * 1024 };
const match = size.match(/^(\d+)(B|KB|MB|GB)$/i);
if (!match) return 256 * 1024 * 1024; // Default 256MB
const [, number, unit] = match;
return parseInt(number) * units[unit.toUpperCase()];
}
_estimateMemoryUsage() {
let size = 0;
// Estimate string storage
for (const [key, value] of this.store) {
size += key.length * 2; // UTF-16
size += typeof value === 'string' ? value.length * 2 : 8;
}
// Estimate data structures
size += this.lists.size * 100; // Rough estimate
size += this.sets.size * 100;
size += this.hashes.size * 100;
size += this.sortedSets.size * 100;
this.stats.memoryUsage = size;
return size;
}
_startExpirationTask() {
this.expirationTimer = setInterval(() => {
const expiredKeys = [];
for (const [key, expireTime] of this.expires) {
if (Date.now() > expireTime) {
expiredKeys.push(key);
}
}
for (const key of expiredKeys) {
this._expireKey(key);
}
}, 10000); // Check every 10 seconds
}
_startSnapshotTask() {
if (!this.config.persistence || !this.config.snapshotInterval) return;
this.snapshotTimer = setInterval(async () => {
try {
await this._saveSnapshot();
} catch (error) {
console.error('[ERROR] Failed to save snapshot:', error.message);
}
}, this.config.snapshotInterval);
}
_startMemoryMonitoring() {
this.memoryCheckTimer = setInterval(() => {
this._estimateMemoryUsage();
if (this._shouldEvict()) {
this._evictKeys();
}
}, 30000); // Check every 30 seconds
}
async _saveSnapshot() {
if (!this.config.persistence) return;
const snapshot = {
timestamp: Date.now(),
version: '1.0.0',
data: {
store: Object.fromEntries(this.store),
expires: Object.fromEntries(this.expires),
lists: Object.fromEntries(
Array.from(this.lists.entries()).map(([k, v]) => [k, Array.from(v)])
),
sets: Object.fromEntries(
Array.from(this.sets.entries()).map(([k, v]) => [k, Array.from(v)])
),
hashes: Object.fromEntries(
Array.from(this.hashes.entries()).map(([k, v]) => [k, Object.fromEntries(v)])
),
sortedSets: Object.fromEntries(
Array.from(this.sortedSets.entries()).map(([k, v]) => [k, Object.fromEntries(v)])
)
},
stats: this.stats
};
await fs.writeFile(this.config.persistenceFile, JSON.stringify(snapshot, null, 2));
this.stats.lastSnapshot = Date.now();
console.log(`[STORAGE] Cache snapshot saved: ${this.config.persistenceFile}`);
}
async _loadPersistentData() {
try {
if (!await fs.access(this.config.persistenceFile).then(() => true).catch(() => false)) {
this.logger.info('No existing cache snapshot found');
return;
}
const data = await fs.readFile(this.config.persistenceFile, 'utf8');
const snapshot = JSON.parse(data);
if (snapshot.data) {
// Restore store
this.store = new Map(Object.entries(snapshot.data.store || {}));
this.expires = new Map(Object.entries(snapshot.data.expires || {}).map(([k, v]) => [k, Number(v)]));
// Restore data structures
this.lists = new Map(Object.entries(snapshot.data.lists || {}));
this.sets = new Map(Object.entries(snapshot.data.sets || {}).map(([k, v]) => [k, new Set(v)]));
this.hashes = new Map(Object.entries(snapshot.data.hashes || {}).map(([k, v]) => [k, new Map(Object.entries(v))]));
this.sortedSets = new Map(Object.entries(snapshot.data.sortedSets || {}).map(([k, v]) => [k, new Map(Object.entries(v))]));
// Update stats
if (snapshot.stats) {
this.stats = { ...this.stats, ...snapshot.stats };
}
console.log(`📂 Loaded cache snapshot: ${this.store.size} keys restored`);
}
} catch (error) {
console.warn('[WARN] Failed to load cache snapshot:', error.message);
}
}
// Public API methods
getStats() {
const uptime = Math.floor((Date.now() - this.stats.uptime) / 1000);
const hitRate = this.stats.hits + this.stats.misses > 0
? (this.stats.hits / (this.stats.hits + this.stats.misses) * 100).toFixed(2)
: '0.00';
return {
...this.stats,
uptime,
hitRate: parseFloat(hitRate),
memoryUsage: this._estimateMemoryUsage(),
keyspaceInfo: {
keys: this.store.size,
lists: this.lists.size,
sets: this.sets.size,
hashes: this.hashes.size,
sortedSets: this.sortedSets.size
},
config: {
maxMemory: this.config.maxMemory,
evictionPolicy: this.config.evictionPolicy,
persistence: this.config.persistence
}
};
}
async shutdown() {
console.log('[PROCESS] Shutting down Redis-like Cache...');
// Clear timers
if (this.expirationTimer) clearInterval(this.expirationTimer);
if (this.snapshotTimer) clearInterval(this.snapshotTimer);
if (this.memoryCheckTimer) clearInterval(this.memoryCheckTimer);
// Save final snapshot
if (this.config.persistence) {
try {
await this._saveSnapshot();
} catch (error) {
console.error('[ERROR] Failed to save final snapshot:', error.message);
}
}
// Clear all data
this.store.clear();
this.expires.clear();
this.accessLog.clear();
this.accessCount.clear();
this.lists.clear();
this.sets.clear();
this.hashes.clear();
this.sortedSets.clear();
this.logger.success('Redis-like Cache shutdown complete');
}
}
export default RedisLikeCache;