UNPKG

jay-code

Version:

Streamlined AI CLI orchestration engine with mathematical rigor and enterprise-grade reliability

791 lines (664 loc) 20.6 kB
/** * SharedMemory - Unified memory persistence module for ruv-swarm * Supports both .swarm/ and .hive-mind/ directories with SQLite backend * * @module shared-memory */ import Database from 'better-sqlite3'; import path from 'path'; import fs from 'fs/promises'; import { EventEmitter } from 'events'; import { performance } from 'perf_hooks'; /** * Migration definitions for schema evolution */ const MIGRATIONS = [ { version: 1, description: 'Initial schema', sql: ` -- Memory store table CREATE TABLE IF NOT EXISTS memory_store ( id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT NOT NULL, namespace TEXT NOT NULL DEFAULT 'default', value TEXT NOT NULL, type TEXT NOT NULL DEFAULT 'json', metadata TEXT, created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), accessed_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), access_count INTEGER NOT NULL DEFAULT 0, ttl INTEGER, expires_at INTEGER, compressed INTEGER DEFAULT 0, size INTEGER NOT NULL DEFAULT 0, UNIQUE(key, namespace) ); -- Metadata table for system information CREATE TABLE IF NOT EXISTS metadata ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) ); -- Migrations tracking table CREATE TABLE IF NOT EXISTS migrations ( version INTEGER PRIMARY KEY, description TEXT, applied_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) ); -- Performance indexes CREATE INDEX IF NOT EXISTS idx_memory_namespace ON memory_store(namespace); CREATE INDEX IF NOT EXISTS idx_memory_expires ON memory_store(expires_at) WHERE expires_at IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_memory_accessed ON memory_store(accessed_at); CREATE INDEX IF NOT EXISTS idx_memory_key_namespace ON memory_store(key, namespace); -- Insert initial metadata INSERT OR IGNORE INTO metadata (key, value) VALUES ('version', '1.0.0'), ('created_at', strftime('%s', 'now')); `, }, { version: 2, description: 'Add tags and search capabilities', sql: ` -- Add tags column ALTER TABLE memory_store ADD COLUMN tags TEXT; -- Create tags index for faster searching CREATE INDEX IF NOT EXISTS idx_memory_tags ON memory_store(tags) WHERE tags IS NOT NULL; -- Update version UPDATE metadata SET value = '1.1.0', updated_at = strftime('%s', 'now') WHERE key = 'version'; `, }, ]; /** * High-performance LRU cache implementation */ class LRUCache { constructor(maxSize = 1000, maxMemoryMB = 50) { this.maxSize = maxSize; this.maxMemory = maxMemoryMB * 1024 * 1024; this.cache = new Map(); this.currentMemory = 0; this.hits = 0; this.misses = 0; this.evictions = 0; } get(key) { if (this.cache.has(key)) { const value = this.cache.get(key); // Move to end (most recently used) this.cache.delete(key); this.cache.set(key, value); this.hits++; return value.data; } this.misses++; return null; } set(key, data, size = 0) { // Estimate size if not provided if (!size) { size = this._estimateSize(data); } // Handle memory pressure while (this.currentMemory + size > this.maxMemory && this.cache.size > 0) { this._evictLRU(); } // Handle size limit while (this.cache.size >= this.maxSize) { this._evictLRU(); } this.cache.set(key, { data, size, timestamp: Date.now() }); this.currentMemory += size; } delete(key) { const entry = this.cache.get(key); if (entry) { this.currentMemory -= entry.size; return this.cache.delete(key); } return false; } clear() { this.cache.clear(); this.currentMemory = 0; this.hits = 0; this.misses = 0; this.evictions = 0; } getStats() { const total = this.hits + this.misses; return { size: this.cache.size, memoryUsage: this.currentMemory, memoryUsageMB: this.currentMemory / (1024 * 1024), hitRate: total > 0 ? (this.hits / total) * 100 : 0, evictions: this.evictions, utilizationPercent: (this.currentMemory / this.maxMemory) * 100, }; } _estimateSize(data) { try { return JSON.stringify(data).length * 2; // UTF-16 estimate } catch { return 1000; // Default for non-serializable } } _evictLRU() { const firstKey = this.cache.keys().next().value; if (firstKey !== undefined) { const entry = this.cache.get(firstKey); this.cache.delete(firstKey); this.currentMemory -= entry.size; this.evictions++; } } } /** * SharedMemory class - Core implementation */ export class SharedMemory extends EventEmitter { constructor(options = {}) { super(); this.options = { directory: options.directory || '.hive-mind', filename: options.filename || 'memory.db', cacheSize: options.cacheSize || 1000, cacheMemoryMB: options.cacheMemoryMB || 50, compressionThreshold: options.compressionThreshold || 10240, // 10KB gcInterval: options.gcInterval || 300000, // 5 minutes enableWAL: options.enableWAL !== false, enableVacuum: options.enableVacuum !== false, ...options, }; this.db = null; this.cache = new LRUCache(this.options.cacheSize, this.options.cacheMemoryMB); this.statements = new Map(); this.gcTimer = null; this.isInitialized = false; // Performance tracking this.metrics = { operations: new Map(), lastGC: Date.now(), totalOperations: 0, }; } /** * Initialize the database and run migrations */ async initialize() { if (this.isInitialized) return; const startTime = performance.now(); try { // Ensure directory exists await fs.mkdir(path.join(process.cwd(), this.options.directory), { recursive: true }); // Open database const dbPath = path.join(process.cwd(), this.options.directory, this.options.filename); this.db = new Database(dbPath); // Configure for performance this._configureDatabase(); // Run migrations await this._runMigrations(); // Prepare statements this._prepareStatements(); // Start garbage collection this._startGarbageCollection(); this.isInitialized = true; const duration = performance.now() - startTime; this._recordMetric('initialize', duration); this.emit('initialized', { dbPath, duration }); } catch (error) { this.emit('error', error); throw new Error(`Failed to initialize SharedMemory: ${error.message}`); } } /** * Store a value in memory */ async store(key, value, options = {}) { this._ensureInitialized(); const startTime = performance.now(); try { const namespace = options.namespace || 'default'; const ttl = options.ttl; const tags = options.tags ? JSON.stringify(options.tags) : null; const metadata = options.metadata ? JSON.stringify(options.metadata) : null; // Serialize value let serialized = value; let type = 'string'; let compressed = 0; if (typeof value !== 'string') { serialized = JSON.stringify(value); type = 'json'; } const size = Buffer.byteLength(serialized); // Compress if needed if (size > this.options.compressionThreshold) { // In production, use proper compression compressed = 1; } // Calculate expiry const expiresAt = ttl ? Math.floor(Date.now() / 1000) + ttl : null; // Store in database this.statements .get('upsert') .run(key, namespace, serialized, type, metadata, tags, ttl, expiresAt, compressed, size); // Update cache const cacheKey = this._getCacheKey(key, namespace); this.cache.set(cacheKey, value, size); const duration = performance.now() - startTime; this._recordMetric('store', duration); this.emit('stored', { key, namespace, size, compressed: !!compressed }); return { success: true, key, namespace, size }; } catch (error) { this.emit('error', error); throw error; } } /** * Retrieve a value from memory */ async retrieve(key, namespace = 'default') { this._ensureInitialized(); const startTime = performance.now(); try { // Check cache first const cacheKey = this._getCacheKey(key, namespace); const cached = this.cache.get(cacheKey); if (cached !== null) { this._recordMetric('retrieve_cache', performance.now() - startTime); return cached; } // Get from database const row = this.statements.get('select').get(key, namespace); if (!row) { this._recordMetric('retrieve_miss', performance.now() - startTime); return null; } // Check expiry if (row.expires_at && row.expires_at < Math.floor(Date.now() / 1000)) { // Delete expired entry this.statements.get('delete').run(key, namespace); this._recordMetric('retrieve_expired', performance.now() - startTime); return null; } // Update access stats this.statements.get('updateAccess').run(key, namespace); // Deserialize value let value = row.value; if (row.type === 'json') { value = JSON.parse(value); } // Update cache this.cache.set(cacheKey, value, row.size); const duration = performance.now() - startTime; this._recordMetric('retrieve_db', duration); return value; } catch (error) { this.emit('error', error); throw error; } } /** * List entries in a namespace */ async list(namespace = 'default', options = {}) { this._ensureInitialized(); const limit = options.limit || 100; const offset = options.offset || 0; try { const rows = this.statements.get('list').all(namespace, limit, offset); return rows.map((row) => ({ key: row.key, namespace: row.namespace, type: row.type, size: row.size, compressed: !!row.compressed, tags: row.tags ? JSON.parse(row.tags) : [], createdAt: new Date(row.created_at * 1000), updatedAt: new Date(row.updated_at * 1000), accessedAt: new Date(row.accessed_at * 1000), accessCount: row.access_count, expiresAt: row.expires_at ? new Date(row.expires_at * 1000) : null, })); } catch (error) { this.emit('error', error); throw error; } } /** * Delete an entry */ async delete(key, namespace = 'default') { this._ensureInitialized(); try { // Remove from cache const cacheKey = this._getCacheKey(key, namespace); this.cache.delete(cacheKey); // Remove from database const result = this.statements.get('delete').run(key, namespace); if (result.changes > 0) { this.emit('deleted', { key, namespace }); return true; } return false; } catch (error) { this.emit('error', error); throw error; } } /** * Clear all entries in a namespace */ async clear(namespace = 'default') { this._ensureInitialized(); try { // Clear cache entries for namespace for (const [key] of this.cache.cache) { if (key.startsWith(`${namespace}:`)) { this.cache.delete(key); } } // Clear database entries const result = this.statements.get('clearNamespace').run(namespace); this.emit('cleared', { namespace, count: result.changes }); return { cleared: result.changes }; } catch (error) { this.emit('error', error); throw error; } } /** * Get statistics */ async getStats() { this._ensureInitialized(); try { const dbStats = this.statements.get('stats').all(); const cacheStats = this.cache.getStats(); // Transform database stats const namespaceStats = {}; for (const row of dbStats) { namespaceStats[row.namespace] = { count: row.count, totalSize: row.total_size, avgSize: row.avg_size, compressed: row.compressed_count, }; } return { namespaces: namespaceStats, cache: cacheStats, metrics: this._getMetricsSummary(), database: { totalEntries: Object.values(namespaceStats).reduce((sum, ns) => sum + ns.count, 0), totalSize: Object.values(namespaceStats).reduce((sum, ns) => sum + ns.totalSize, 0), }, }; } catch (error) { this.emit('error', error); throw error; } } /** * Search entries by pattern or tags */ async search(options = {}) { this._ensureInitialized(); const { pattern, namespace, tags, limit = 50, offset = 0 } = options; try { let query = 'SELECT * FROM memory_store WHERE 1=1'; const params = []; if (namespace) { query += ' AND namespace = ?'; params.push(namespace); } if (pattern) { query += ' AND key LIKE ?'; params.push(`%${pattern}%`); } if (tags && tags.length > 0) { // Simple tag search - in production, use JSON functions query += ' AND tags IS NOT NULL'; } query += ' ORDER BY accessed_at DESC LIMIT ? OFFSET ?'; params.push(limit, offset); const stmt = this.db.prepare(query); const rows = stmt.all(...params); return rows.map((row) => ({ key: row.key, namespace: row.namespace, value: row.type === 'json' ? JSON.parse(row.value) : row.value, metadata: row.metadata ? JSON.parse(row.metadata) : null, tags: row.tags ? JSON.parse(row.tags) : [], })); } catch (error) { this.emit('error', error); throw error; } } /** * Backup the database */ async backup(filepath) { this._ensureInitialized(); try { await this.db.backup(filepath); this.emit('backup', { filepath }); return { success: true, filepath }; } catch (error) { this.emit('error', error); throw error; } } /** * Close the database connection */ async close() { if (!this.isInitialized) return; try { // Stop garbage collection if (this.gcTimer) { clearInterval(this.gcTimer); this.gcTimer = null; } // Final optimization if (this.options.enableVacuum) { this.db.pragma('optimize'); } // Close statements for (const stmt of this.statements.values()) { stmt.finalize(); } this.statements.clear(); // Close database this.db.close(); this.db = null; // Clear cache this.cache.clear(); this.isInitialized = false; this.emit('closed'); } catch (error) { this.emit('error', error); throw error; } } /** * Private helper methods */ _ensureInitialized() { if (!this.isInitialized) { throw new Error('SharedMemory not initialized. Call initialize() first.'); } } _configureDatabase() { // Performance optimizations if (this.options.enableWAL) { this.db.pragma('journal_mode = WAL'); } this.db.pragma('synchronous = NORMAL'); this.db.pragma('cache_size = -64000'); // 64MB this.db.pragma('temp_store = MEMORY'); this.db.pragma('mmap_size = 268435456'); // 256MB } _runMigrations() { // Create migrations table if needed this.db.exec(` CREATE TABLE IF NOT EXISTS migrations ( version INTEGER PRIMARY KEY, description TEXT, applied_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) ) `); // Get current version const currentVersion = this.db.prepare('SELECT MAX(version) as version FROM migrations').get().version || 0; // Run pending migrations const pending = MIGRATIONS.filter((m) => m.version > currentVersion); if (pending.length > 0) { const transaction = this.db.transaction((migrations) => { for (const migration of migrations) { this.db.exec(migration.sql); this.db .prepare('INSERT INTO migrations (version, description) VALUES (?, ?)') .run(migration.version, migration.description); } }); transaction(pending); this.emit('migrated', { from: currentVersion, to: pending[pending.length - 1].version }); } } _prepareStatements() { // Upsert statement this.statements.set( 'upsert', this.db.prepare(` INSERT INTO memory_store (key, namespace, value, type, metadata, tags, ttl, expires_at, compressed, size) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(key, namespace) DO UPDATE SET value = excluded.value, type = excluded.type, metadata = excluded.metadata, tags = excluded.tags, ttl = excluded.ttl, expires_at = excluded.expires_at, compressed = excluded.compressed, size = excluded.size, updated_at = strftime('%s', 'now'), access_count = memory_store.access_count + 1 `), ); // Select statement this.statements.set( 'select', this.db.prepare(` SELECT * FROM memory_store WHERE key = ? AND namespace = ? `), ); // Update access statement this.statements.set( 'updateAccess', this.db.prepare(` UPDATE memory_store SET accessed_at = strftime('%s', 'now'), access_count = access_count + 1 WHERE key = ? AND namespace = ? `), ); // Delete statement this.statements.set( 'delete', this.db.prepare(` DELETE FROM memory_store WHERE key = ? AND namespace = ? `), ); // List statement this.statements.set( 'list', this.db.prepare(` SELECT * FROM memory_store WHERE namespace = ? ORDER BY accessed_at DESC LIMIT ? OFFSET ? `), ); // Clear namespace statement this.statements.set( 'clearNamespace', this.db.prepare(` DELETE FROM memory_store WHERE namespace = ? `), ); // Stats statement this.statements.set( 'stats', this.db.prepare(` SELECT namespace, COUNT(*) as count, SUM(size) as total_size, AVG(size) as avg_size, SUM(compressed) as compressed_count FROM memory_store GROUP BY namespace `), ); // Garbage collection statement this.statements.set( 'gc', this.db.prepare(` DELETE FROM memory_store WHERE expires_at IS NOT NULL AND expires_at < strftime('%s', 'now') `), ); } _startGarbageCollection() { this.gcTimer = setInterval(() => { this._runGarbageCollection(); }, this.options.gcInterval); } _runGarbageCollection() { try { const result = this.statements.get('gc').run(); if (result.changes > 0) { this.emit('gc', { expired: result.changes }); } this.metrics.lastGC = Date.now(); } catch (error) { this.emit('error', error); } } _getCacheKey(key, namespace) { return `${namespace}:${key}`; } _recordMetric(operation, duration) { if (!this.metrics.operations.has(operation)) { this.metrics.operations.set(operation, []); } const metrics = this.metrics.operations.get(operation); metrics.push(duration); // Keep only last 100 measurements if (metrics.length > 100) { metrics.shift(); } this.metrics.totalOperations++; } _getMetricsSummary() { const summary = {}; for (const [operation, durations] of this.metrics.operations) { if (durations.length > 0) { summary[operation] = { count: durations.length, avg: durations.reduce((a, b) => a + b, 0) / durations.length, min: Math.min(...durations), max: Math.max(...durations), }; } } summary.totalOperations = this.metrics.totalOperations; summary.lastGC = new Date(this.metrics.lastGC).toISOString(); return summary; } } // Export for backwards compatibility export default SharedMemory;