claude-playwright
Version:
Seamless integration between Claude Code and Playwright MCP for efficient browser automation and testing
1 lines • 13.9 kB
Source Map (JSON)
{"version":3,"sources":["../../src/core/cache-manager.ts"],"sourcesContent":["import Database from 'better-sqlite3';\nimport * as path from 'path';\nimport * as os from 'os';\nimport * as fs from 'fs';\nimport crypto from 'crypto';\n\ninterface CacheEntry {\n id?: number;\n cache_key: string;\n cache_type: 'selector' | 'state' | 'snapshot';\n url: string;\n data: string;\n ttl: number;\n created_at: number;\n accessed_at: number;\n hit_count: number;\n profile?: string;\n}\n\ninterface CacheOptions {\n maxSizeMB?: number;\n selectorTTL?: number;\n stateTTL?: number;\n snapshotTTL?: number;\n cleanupInterval?: number;\n}\n\nexport class CacheManager {\n private db: Database.Database;\n private cleanupTimer?: NodeJS.Timeout;\n private options: Required<CacheOptions>;\n private cacheDir: string;\n\n constructor(options: CacheOptions = {}) {\n this.options = {\n maxSizeMB: options.maxSizeMB ?? 50,\n selectorTTL: options.selectorTTL ?? 300000, // 5 minutes\n stateTTL: options.stateTTL ?? 2000, // 2 seconds\n snapshotTTL: options.snapshotTTL ?? 1800000, // 30 minutes\n cleanupInterval: options.cleanupInterval ?? 60000 // 1 minute\n };\n\n // Create cache directory\n this.cacheDir = path.join(os.homedir(), '.claude-playwright', 'cache');\n if (!fs.existsSync(this.cacheDir)) {\n fs.mkdirSync(this.cacheDir, { recursive: true });\n }\n\n // Initialize database\n const dbPath = path.join(this.cacheDir, 'selector-cache.db');\n this.db = new Database(dbPath);\n this.db.pragma('journal_mode = WAL');\n this.db.pragma('synchronous = NORMAL');\n \n this.initializeDatabase();\n this.startCleanupTimer();\n }\n\n private initializeDatabase(): void {\n // Create cache table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS cache (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n cache_key TEXT NOT NULL,\n cache_type TEXT NOT NULL,\n url TEXT NOT NULL,\n data TEXT NOT NULL,\n ttl INTEGER NOT NULL,\n created_at INTEGER NOT NULL,\n accessed_at INTEGER NOT NULL,\n hit_count INTEGER DEFAULT 0,\n profile TEXT,\n UNIQUE(cache_key, cache_type, profile)\n );\n\n CREATE INDEX IF NOT EXISTS idx_cache_key ON cache(cache_key);\n CREATE INDEX IF NOT EXISTS idx_cache_type ON cache(cache_type);\n CREATE INDEX IF NOT EXISTS idx_url ON cache(url);\n CREATE INDEX IF NOT EXISTS idx_ttl ON cache(created_at, ttl);\n CREATE INDEX IF NOT EXISTS idx_profile ON cache(profile);\n\n -- Metrics table for performance tracking\n CREATE TABLE IF NOT EXISTS cache_metrics (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n cache_type TEXT NOT NULL,\n hits INTEGER DEFAULT 0,\n misses INTEGER DEFAULT 0,\n evictions INTEGER DEFAULT 0,\n timestamp INTEGER NOT NULL\n );\n `);\n }\n\n private generateCacheKey(input: string | object): string {\n const data = typeof input === 'string' ? input : JSON.stringify(input);\n return crypto.createHash('md5').update(data).digest('hex');\n }\n\n async get(\n key: string | object,\n type: 'selector' | 'state' | 'snapshot',\n profile?: string\n ): Promise<any | null> {\n const cacheKey = this.generateCacheKey(key);\n const now = Date.now();\n\n try {\n const stmt = this.db.prepare(`\n SELECT * FROM cache \n WHERE cache_key = ? \n AND cache_type = ? \n AND (profile = ? OR (profile IS NULL AND ? IS NULL))\n AND (created_at + ttl) > ?\n `);\n\n const entry = stmt.get(cacheKey, type, profile, profile, now) as CacheEntry | undefined;\n\n if (entry) {\n // Update hit count and access time\n const updateStmt = this.db.prepare(`\n UPDATE cache \n SET hit_count = hit_count + 1, accessed_at = ?\n WHERE id = ?\n `);\n updateStmt.run(now, entry.id);\n\n this.recordHit(type);\n return JSON.parse(entry.data);\n }\n\n this.recordMiss(type);\n return null;\n } catch (error) {\n console.error('Cache get error:', error);\n return null;\n }\n }\n\n async set(\n key: string | object,\n value: any,\n type: 'selector' | 'state' | 'snapshot',\n options: { url?: string; profile?: string; ttl?: number } = {}\n ): Promise<void> {\n const cacheKey = this.generateCacheKey(key);\n const now = Date.now();\n const ttl = options.ttl ?? this.getTTLForType(type);\n\n try {\n // Check cache size and evict if necessary\n await this.evictIfNeeded();\n\n const stmt = this.db.prepare(`\n INSERT OR REPLACE INTO cache \n (cache_key, cache_type, url, data, ttl, created_at, accessed_at, hit_count, profile)\n VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)\n `);\n\n stmt.run(\n cacheKey,\n type,\n options.url || '',\n JSON.stringify(value),\n ttl,\n now,\n now,\n options.profile\n );\n } catch (error) {\n console.error('Cache set error:', error);\n }\n }\n\n async invalidate(options: { url?: string; type?: string; profile?: string } = {}): Promise<void> {\n let query = 'DELETE FROM cache WHERE 1=1';\n const params: any[] = [];\n\n if (options.url) {\n query += ' AND url = ?';\n params.push(options.url);\n }\n\n if (options.type) {\n query += ' AND cache_type = ?';\n params.push(options.type);\n }\n\n if (options.profile !== undefined) {\n query += ' AND (profile = ? OR (profile IS NULL AND ? IS NULL))';\n params.push(options.profile, options.profile);\n }\n\n try {\n const stmt = this.db.prepare(query);\n const result = stmt.run(...params);\n \n if (result.changes > 0) {\n this.recordEvictions(result.changes);\n }\n } catch (error) {\n console.error('Cache invalidate error:', error);\n }\n }\n\n async clear(): Promise<void> {\n try {\n this.db.exec('DELETE FROM cache');\n this.db.exec('DELETE FROM cache_metrics');\n } catch (error) {\n console.error('Cache clear error:', error);\n }\n }\n\n private getTTLForType(type: 'selector' | 'state' | 'snapshot'): number {\n switch (type) {\n case 'selector':\n return this.options.selectorTTL;\n case 'state':\n return this.options.stateTTL;\n case 'snapshot':\n return this.options.snapshotTTL;\n default:\n return this.options.selectorTTL;\n }\n }\n\n private async evictIfNeeded(): Promise<void> {\n try {\n // Get database size\n const stats = fs.statSync(path.join(this.cacheDir, 'selector-cache.db'));\n const sizeMB = stats.size / (1024 * 1024);\n\n if (sizeMB > this.options.maxSizeMB) {\n // Evict least recently used entries\n const deleteCount = Math.floor(this.db.prepare('SELECT COUNT(*) as count FROM cache').get().count * 0.2);\n \n const stmt = this.db.prepare(`\n DELETE FROM cache \n WHERE id IN (\n SELECT id FROM cache \n ORDER BY accessed_at ASC \n LIMIT ?\n )\n `);\n \n const result = stmt.run(deleteCount);\n if (result.changes > 0) {\n this.recordEvictions(result.changes);\n }\n }\n } catch (error) {\n console.error('Eviction error:', error);\n }\n }\n\n private cleanup(): void {\n try {\n const now = Date.now();\n const stmt = this.db.prepare('DELETE FROM cache WHERE (created_at + ttl) < ?');\n const result = stmt.run(now);\n \n if (result.changes > 0) {\n this.recordEvictions(result.changes);\n }\n } catch (error) {\n console.error('Cleanup error:', error);\n }\n }\n\n private startCleanupTimer(): void {\n this.cleanupTimer = setInterval(() => {\n this.cleanup();\n }, this.options.cleanupInterval);\n }\n\n private recordHit(type: string): void {\n this.updateMetrics(type, 'hits');\n }\n\n private recordMiss(type: string): void {\n this.updateMetrics(type, 'misses');\n }\n\n private recordEvictions(count: number): void {\n try {\n const stmt = this.db.prepare(`\n INSERT INTO cache_metrics (cache_type, evictions, timestamp)\n VALUES ('all', ?, ?)\n `);\n stmt.run(count, Date.now());\n } catch (error) {\n console.error('Metrics error:', error);\n }\n }\n\n private updateMetrics(type: string, metric: 'hits' | 'misses'): void {\n try {\n const stmt = this.db.prepare(`\n INSERT INTO cache_metrics (cache_type, ${metric}, timestamp)\n VALUES (?, 1, ?)\n `);\n stmt.run(type, Date.now());\n } catch (error) {\n console.error('Metrics error:', error);\n }\n }\n\n async getMetrics(): Promise<any> {\n try {\n const stmt = this.db.prepare(`\n SELECT \n cache_type,\n SUM(hits) as total_hits,\n SUM(misses) as total_misses,\n SUM(evictions) as total_evictions\n FROM cache_metrics\n GROUP BY cache_type\n `);\n \n return stmt.all();\n } catch (error) {\n console.error('Get metrics error:', error);\n return [];\n }\n }\n\n close(): void {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n }\n this.db.close();\n }\n}"],"mappings":"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","names":["Database","crypto"]}