UNPKG

okta-mcp-server

Version:

Model Context Protocol (MCP) server for Okta API operations with support for bulk operations and caching

271 lines (269 loc) 9.25 kB
/** * SQLite-based cache implementation for high-performance caching */ import Database from 'better-sqlite3'; import path from 'path'; export class SqliteCache { db; eventBus; cleanupTimer; maxSize; preparedStatements = {}; constructor(options = {}) { const { filePath = path.join(process.cwd(), 'cache.db'), inMemory = false, maxSize = 10000, eventBus, cleanupInterval = 60000, // 1 minute walMode = true, } = options; this.maxSize = maxSize; this.eventBus = eventBus; // Initialize database this.db = new Database(inMemory ? ':memory:' : filePath); // Enable WAL mode for better concurrency if not in-memory if (!inMemory && walMode) { this.db.pragma('journal_mode = WAL'); } // Optimize for performance this.db.pragma('synchronous = NORMAL'); this.db.pragma('cache_size = 10000'); this.db.pragma('temp_store = MEMORY'); this.db.pragma('mmap_size = 30000000000'); // 30GB mmap // Initialize schema this.initializeSchema(); // Prepare statements for better performance this.prepareStatements(); // Start cleanup timer if (cleanupInterval > 0) { this.cleanupTimer = setInterval(() => { this.cleanupExpired(); }, cleanupInterval); } } initializeSchema() { // Create cache entries table this.db.exec(` CREATE TABLE IF NOT EXISTS cache_entries ( key TEXT PRIMARY KEY, value TEXT NOT NULL, expires_at INTEGER NOT NULL, created_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_expires_at ON cache_entries(expires_at); `); // Create tags table for tag-based invalidation this.db.exec(` CREATE TABLE IF NOT EXISTS cache_tags ( entry_key TEXT NOT NULL, tag TEXT NOT NULL, PRIMARY KEY (entry_key, tag), FOREIGN KEY (entry_key) REFERENCES cache_entries(key) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_tag ON cache_tags(tag); `); } prepareStatements() { // Prepare frequently used statements this.preparedStatements.get = this.db.prepare('SELECT value, expires_at FROM cache_entries WHERE key = ? AND expires_at > ?'); this.preparedStatements.set = this.db.prepare('INSERT OR REPLACE INTO cache_entries (key, value, expires_at, created_at) VALUES (?, ?, ?, ?)'); this.preparedStatements.delete = this.db.prepare('DELETE FROM cache_entries WHERE key = ?'); this.preparedStatements.has = this.db.prepare('SELECT 1 FROM cache_entries WHERE key = ? AND expires_at > ? LIMIT 1'); this.preparedStatements.deleteExpired = this.db.prepare('DELETE FROM cache_entries WHERE expires_at <= ?'); this.preparedStatements.getTags = this.db.prepare('SELECT tag FROM cache_tags WHERE entry_key = ?'); this.preparedStatements.setTag = this.db.prepare('INSERT OR IGNORE INTO cache_tags (entry_key, tag) VALUES (?, ?)'); this.preparedStatements.deleteByTag = this.db.prepare(` DELETE FROM cache_entries WHERE key IN (SELECT entry_key FROM cache_tags WHERE tag = ?) `); this.preparedStatements.deleteTags = this.db.prepare('DELETE FROM cache_tags WHERE entry_key = ?'); this.preparedStatements.count = this.db.prepare('SELECT COUNT(*) as count FROM cache_entries WHERE expires_at > ?'); this.preparedStatements.getAllKeys = this.db.prepare('SELECT key FROM cache_entries WHERE expires_at > ?'); } async get(key) { try { const now = Date.now(); const row = this.preparedStatements.get.get(key, now); if (!row) { this.eventBus?.emit('cache:miss', { key }); return undefined; } this.eventBus?.emit('cache:hit', { key }); return JSON.parse(row.value); } catch (error) { this.eventBus?.emit('cache:error', { key, error }); return undefined; } } async set(key, value, options) { const ttl = options?.ttl || 300; // Default 5 minutes const now = Date.now(); const expiresAt = now + ttl * 1000; // Check if we need to evict entries await this.evictIfNeeded(); const transaction = this.db.transaction(() => { // Insert or update the cache entry this.preparedStatements.set.run(key, JSON.stringify(value), expiresAt, now); // Handle tags if provided if (options?.tags && options.tags.length > 0) { // Delete existing tags for this key this.preparedStatements.deleteTags.run(key); // Insert new tags const setTag = this.preparedStatements.setTag; for (const tag of options.tags) { setTag.run(key, tag); } } }); try { transaction(); this.eventBus?.emit('cache:set', { key, ttl }); } catch (error) { this.eventBus?.emit('cache:error', { key, error }); throw error; } } async delete(key) { try { const result = this.preparedStatements.delete.run(key); return result.changes > 0; } catch (error) { this.eventBus?.emit('cache:error', { key, error }); return false; } } async has(key) { try { const now = Date.now(); const row = this.preparedStatements.has.get(key, now); return row !== undefined; } catch (error) { this.eventBus?.emit('cache:error', { key, error }); return false; } } async clear() { try { this.db.exec('DELETE FROM cache_entries'); this.eventBus?.emit('cache:clear', {}); } catch (error) { this.eventBus?.emit('cache:error', { error }); throw error; } } async clearByTag(tag) { try { this.preparedStatements.deleteByTag.run(tag); this.eventBus?.emit('cache:clear', { pattern: `tag:${tag}` }); } catch (error) { this.eventBus?.emit('cache:error', { tag, error }); throw error; } } async size() { try { const now = Date.now(); const row = this.preparedStatements.count.get(now); return row.count; } catch (error) { this.eventBus?.emit('cache:error', { error }); return 0; } } /** * Get all keys (for debugging) */ async keys() { try { const now = Date.now(); const rows = this.preparedStatements.getAllKeys.all(now); return rows.map((row) => row.key); } catch (error) { this.eventBus?.emit('cache:error', { error }); return []; } } /** * Clean up expired entries */ cleanupExpired() { try { const now = Date.now(); const result = this.preparedStatements.deleteExpired.run(now); if (result.changes > 0) { this.eventBus?.emit('cache:cleanup', { deleted: result.changes }); } } catch (error) { this.eventBus?.emit('cache:error', { error }); } } /** * Evict oldest entries if cache is full */ async evictIfNeeded() { const currentSize = await this.size(); if (currentSize >= this.maxSize) { // Evict 10% of oldest entries const toEvict = Math.ceil(this.maxSize * 0.1); this.db .prepare(` DELETE FROM cache_entries WHERE key IN ( SELECT key FROM cache_entries ORDER BY created_at ASC LIMIT ? ) `) .run(toEvict); } } /** * Get cache statistics */ getStats() { const now = Date.now(); const stats = this.db .prepare(` SELECT COUNT(*) as total, COUNT(CASE WHEN expires_at <= ? THEN 1 END) as expired FROM cache_entries `) .get(now); // Get database file size const dbStats = this.db .prepare('SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()') .get(); return { size: stats.total - stats.expired, expired: stats.expired, dbSize: dbStats.size, }; } /** * Close the database connection */ close() { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); } this.db.close(); } /** * Optimize the database (VACUUM) */ async optimize() { try { this.db.exec('VACUUM'); this.db.exec('ANALYZE'); } catch (error) { this.eventBus?.emit('cache:error', { error }); } } } //# sourceMappingURL=sqlite-cache.js.map