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
JavaScript
/**
* 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