UNPKG

@codai/cbd

Version:

Codai Better Database - High-Performance Vector Memory System with HPKV-inspired architecture and MCP server

422 lines 13.1 kB
/** * Key-Value Storage Engine - Redis-compatible key-value store * Part of CBD Universal Database Phase 3 */ import { EventEmitter } from 'events'; export class KeyValueStorageEngine extends EventEmitter { store = new Map(); expirationIndex = new Map(); // timestamp -> keys stats = { hits: 0, misses: 0, expiredKeys: 0 }; constructor() { super(); this.startExpirationCleanup(); } async initialize() { // Initialize the key-value storage engine this.emit('engine:initialized', { type: 'keyvalue' }); } /** * Set a key-value pair */ async set(key, value, options = {}) { try { const { ttl, nx, xx } = options; const exists = this.store.has(key); // Check conditions if (nx && exists) { return false; // Key exists, but NX specified } if (xx && !exists) { return false; // Key doesn't exist, but XX specified } // Remove old expiration if exists if (exists) { const oldPair = this.store.get(key); if (oldPair.expiresAt) { this.removeFromExpirationIndex(oldPair.expiresAt.getTime(), key); } } const now = new Date(); const pair = { key, value, type: this.getValueType(value), createdAt: exists ? this.store.get(key).createdAt : now, updatedAt: now }; if (ttl) { pair.ttl = ttl; pair.expiresAt = new Date(now.getTime() + ttl); } this.store.set(key, pair); // Add to expiration index if TTL is set if (pair.expiresAt) { this.addToExpirationIndex(pair.expiresAt.getTime(), key); } this.emit('key:set', { key, type: pair.type, ttl }); return true; } catch (error) { this.emit('keyvalue:error', { operation: 'set', key, error }); throw error; } } /** * Get a value by key */ async get(key) { try { const pair = this.store.get(key); if (!pair) { this.stats.misses++; return null; } // Check if expired if (pair.expiresAt && pair.expiresAt <= new Date()) { await this.delete(key); this.stats.misses++; this.stats.expiredKeys++; return null; } this.stats.hits++; this.emit('key:accessed', { key, type: pair.type }); return pair.value; } catch (error) { this.emit('keyvalue:error', { operation: 'get', key, error }); throw error; } } /** * Delete a key */ async delete(key) { try { const pair = this.store.get(key); if (!pair) { return false; } // Remove from expiration index if (pair.expiresAt) { this.removeFromExpirationIndex(pair.expiresAt.getTime(), key); } const deleted = this.store.delete(key); if (deleted) { this.emit('key:deleted', { key }); } return deleted; } catch (error) { this.emit('keyvalue:error', { operation: 'delete', key, error }); throw error; } } /** * Check if key exists */ async exists(key) { const pair = this.store.get(key); if (!pair) { return false; } // Check if expired if (pair.expiresAt && pair.expiresAt <= new Date()) { await this.delete(key); return false; } return true; } /** * Set TTL for a key */ async expire(key, ttl) { try { const pair = this.store.get(key); if (!pair) { return false; } // Remove old expiration if (pair.expiresAt) { this.removeFromExpirationIndex(pair.expiresAt.getTime(), key); } // Set new expiration const expiresAt = new Date(Date.now() + ttl); pair.expiresAt = expiresAt; pair.ttl = ttl; pair.updatedAt = new Date(); this.addToExpirationIndex(expiresAt.getTime(), key); this.emit('key:expired', { key, ttl }); return true; } catch (error) { this.emit('keyvalue:error', { operation: 'expire', key, error }); throw error; } } /** * Get TTL for a key */ async ttl(key) { const pair = this.store.get(key); if (!pair) { return -2; // Key doesn't exist } if (!pair.expiresAt) { return -1; // Key exists but has no TTL } const remaining = pair.expiresAt.getTime() - Date.now(); return remaining > 0 ? Math.ceil(remaining / 1000) : -2; // Return seconds } /** * Get multiple keys */ async mget(keys) { const results = []; for (const key of keys) { results.push(await this.get(key)); } return results; } /** * Set multiple key-value pairs */ async mset(pairs) { try { for (const { key, value, options = {} } of pairs) { await this.set(key, value, options); } this.emit('keys:mset', { count: pairs.length }); return true; } catch (error) { this.emit('keyvalue:error', { operation: 'mset', pairs, error }); throw error; } } /** * Increment a numeric value */ async incr(key, by = 1) { try { const current = await this.get(key); if (current === null) { await this.set(key, by); return by; } if (typeof current !== 'number') { throw new Error(`Value at key ${key} is not a number`); } const newValue = current + by; await this.set(key, newValue); this.emit('key:incremented', { key, by, newValue }); return newValue; } catch (error) { this.emit('keyvalue:error', { operation: 'incr', key, error }); throw error; } } /** * Decrement a numeric value */ async decr(key, by = 1) { return this.incr(key, -by); } /** * Scan keys with pattern matching */ async scan(cursor = 0, pattern, count = 10) { const keys = Array.from(this.store.keys()); const start = cursor; const end = Math.min(start + count, keys.length); let matchingKeys = keys.slice(start, end); // Apply pattern filter if (pattern) { const regex = this.patternToRegex(pattern); matchingKeys = matchingKeys.filter(key => regex.test(key)); } const nextCursor = end >= keys.length ? 0 : end; return { cursor: nextCursor, keys: matchingKeys }; } /** * Get all keys matching a pattern */ async keys(pattern = '*') { const regex = this.patternToRegex(pattern); return Array.from(this.store.keys()).filter(key => regex.test(key)); } /** * Get random key */ async randomKey() { const keys = Array.from(this.store.keys()); if (keys.length === 0) { return null; } const randomIndex = Math.floor(Math.random() * keys.length); return keys[randomIndex] || null; } /** * Rename a key */ async rename(oldKey, newKey) { try { const pair = this.store.get(oldKey); if (!pair) { return false; } // Copy to new key const remainingTTL = this.getRemainingTTL(pair); const setOptions = {}; if (remainingTTL) { setOptions.ttl = remainingTTL; } await this.set(newKey, pair.value, setOptions); // Delete old key await this.delete(oldKey); this.emit('key:renamed', { oldKey, newKey }); return true; } catch (error) { this.emit('keyvalue:error', { operation: 'rename', oldKey, newKey, error }); throw error; } } /** * Get type of value at key */ async type(key) { const pair = this.store.get(key); return pair ? pair.type : null; } /** * Flush all keys */ async flushAll() { const keyCount = this.store.size; this.store.clear(); this.expirationIndex.clear(); this.emit('database:flushed', { deletedKeys: keyCount }); } /** * Get database statistics */ async getKeyValueStats() { const keyTypes = new Map(); let totalKeySize = 0; let totalValueSize = 0; for (const [key, pair] of this.store) { // Count types keyTypes.set(pair.type, (keyTypes.get(pair.type) || 0) + 1); // Estimate sizes totalKeySize += key.length * 2; // UTF-16 totalValueSize += this.estimateValueSize(pair.value); } return { totalKeys: this.store.size, memoryUsage: totalKeySize + totalValueSize, hitRate: this.stats.hits / (this.stats.hits + this.stats.misses) || 0, missRate: this.stats.misses / (this.stats.hits + this.stats.misses) || 0, expiredKeys: this.stats.expiredKeys, keyTypes, averageKeySize: this.store.size > 0 ? totalKeySize / this.store.size : 0, averageValueSize: this.store.size > 0 ? totalValueSize / this.store.size : 0 }; } /** * Get all keys (use with caution on large datasets) */ async getAllKeys() { return Array.from(this.store.keys()); } /** * Get key-value pair info */ async info(key) { return this.store.get(key) || null; } // Private helper methods getValueType(value) { if (Array.isArray(value)) return 'array'; return typeof value; } addToExpirationIndex(timestamp, key) { if (!this.expirationIndex.has(timestamp)) { this.expirationIndex.set(timestamp, new Set()); } this.expirationIndex.get(timestamp).add(key); } removeFromExpirationIndex(timestamp, key) { const keySet = this.expirationIndex.get(timestamp); if (keySet) { keySet.delete(key); if (keySet.size === 0) { this.expirationIndex.delete(timestamp); } } } getRemainingTTL(pair) { if (!pair.expiresAt) { return undefined; } const remaining = pair.expiresAt.getTime() - Date.now(); return remaining > 0 ? remaining : undefined; } patternToRegex(pattern) { // Convert Redis-style patterns to regex const regexPattern = pattern .replace(/\*/g, '.*') // * matches any number of characters .replace(/\?/g, '.') // ? matches single character .replace(/\[([^\]]+)\]/g, '[$1]'); // [abc] character class return new RegExp(`^${regexPattern}$`); } estimateValueSize(value) { if (typeof value === 'string') { return value.length * 2; // UTF-16 } else if (typeof value === 'number') { return 8; // 64-bit number } else if (typeof value === 'boolean') { return 1; } else { return JSON.stringify(value).length * 2; // Rough estimate } } startExpirationCleanup() { // Clean up expired keys every 10 seconds setInterval(() => { this.cleanupExpiredKeys(); }, 10000); } cleanupExpiredKeys() { const now = Date.now(); const expiredTimestamps = []; for (const [timestamp, keys] of this.expirationIndex) { if (timestamp <= now) { for (const key of keys) { this.store.delete(key); this.stats.expiredKeys++; } expiredTimestamps.push(timestamp); } } // Clean up expiration index for (const timestamp of expiredTimestamps) { this.expirationIndex.delete(timestamp); } if (expiredTimestamps.length > 0) { this.emit('keys:expired', { count: expiredTimestamps.length }); } } } //# sourceMappingURL=KeyValueStorageEngine.js.map