import { appendDebugLog } from "@/lib/backend/debugLog"; export const METADATA_CACHE_DEFAULT_MEMORY_MB = 64; export const METADATA_CACHE_MIN_MEMORY_MB = 16; export const METADATA_CACHE_RECOMMENDED_MAX_MEMORY_MB = 256; export const METADATA_CACHE_HARD_MAX_MEMORY_MB = 512; export const METADATA_CACHE_MAX_ENTRY_BYTES = 1024 * 1024; const METADATA_CACHE_CONNECTION_SHARE = 0.25; const METADATA_CACHE_KEY_PREFIX = "dbx-metadata-cache:"; interface RuntimeCacheEntry { key: string; connectionId: string; value: T; sizeBytes: number; touchedAt: number; } interface MetadataCacheLogContext { connectionId: string; database: string; facet: string; } export interface MetadataRuntimeCacheHit { value: T; sizeBytes: number; } export interface MetadataRuntimeCacheDiagnostics { entries: number; bytes: number; maxMemoryBytes: number; maxEntryBytes: number; configuredMemoryMb: number; l1Hits: number; l1Misses: number; l2Hits: number; remoteMisses: number; evictions: number; connectionClears: number; } const entries = new Map(); let configuredMemoryMb = METADATA_CACHE_DEFAULT_MEMORY_MB; let totalBytes = 0; let l1Hits = 0; let l1Misses = 0; let l2Hits = 0; let remoteMisses = 0; let evictions = 0; let connectionClears = 0; function now(): number { return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now(); } function memoryBudgetBytes(): number { return configuredMemoryMb * 1024 * 1024; } function connectionBudgetBytes(): number { return Math.floor(memoryBudgetBytes() * METADATA_CACHE_CONNECTION_SHARE); } function keyConnectionId(key: string, connectionId?: string): string { if (connectionId) return connectionId; const marker = `${METADATA_CACHE_KEY_PREFIX}`; if (!key.startsWith(marker)) return ""; return key.slice(marker.length).split(":", 1)[0] ?? ""; } function estimateValueBytes(value: unknown): number { let serialized = ""; try { serialized = JSON.stringify(value) ?? "null"; } catch { serialized = String(value); } // JSON byte length is a lower bound for V8 heap usage. Account for UTF-16 // strings and object/array/Map entry overhead with a conservative multiplier. const utf8Bytes = typeof TextEncoder !== "undefined" ? new TextEncoder().encode(serialized).byteLength : serialized.length; return Math.max(256, utf8Bytes * 2 + 512); } function decodeCacheSegment(value: string | undefined): string { if (!value) return ""; try { return decodeURIComponent(value); } catch { return value; } } function cacheLogContext(key: string, fallbackConnectionId = ""): MetadataCacheLogContext { const parts = key.split(":"); if ((parts[0] === "object-ddl" || parts[0] === "object-meta") && parts[1] === "v1") { return { connectionId: decodeCacheSegment(parts[2]) || fallbackConnectionId, database: decodeCacheSegment(parts[3]), facet: parts[0] === "object-ddl" ? "ddl" : decodeCacheSegment(parts[8]), }; } return { connectionId: fallbackConnectionId, database: "", facet: "" }; } function logCacheMetric(metric: string, key: string, details: Record = {}, fallbackConnectionId = ""): void { appendDebugLog("debug", "[DBX][metadata-cache]", { metric, ...cacheLogContext(key, fallbackConnectionId), ...details, cacheBytes: totalBytes, cacheEntries: entries.size, }); } function removeEntry(key: string, evictionReason?: string): boolean { const entry = entries.get(key); if (!entry) return false; entries.delete(key); totalBytes = Math.max(0, totalBytes - entry.sizeBytes); if (evictionReason) { evictions++; logCacheMetric("metadata_cache_eviction", key, { entrySizeBytes: entry.sizeBytes, evictionReason }, entry.connectionId); } return true; } function connectionBytes(connectionId: string): number { let bytes = 0; for (const entry of entries.values()) { if (entry.connectionId === connectionId) bytes += entry.sizeBytes; } return bytes; } function evictOldest(predicate: (entry: RuntimeCacheEntry) => boolean, reason: string): boolean { let oldest: RuntimeCacheEntry | undefined; for (const entry of entries.values()) { if (!predicate(entry)) continue; if (!oldest || entry.touchedAt < oldest.touchedAt) oldest = entry; } return oldest ? removeEntry(oldest.key, reason) : false; } function evictForInsert(connectionId: string, sizeBytes: number): boolean { const budget = memoryBudgetBytes(); const perConnectionBudget = connectionBudgetBytes(); while (connectionBytes(connectionId) + sizeBytes > perConnectionBudget) { if (!evictOldest((entry) => entry.connectionId === connectionId, "connection-budget")) return false; } while (totalBytes + sizeBytes > budget) { if (!evictOldest(() => true, "total-budget")) return false; } return true; } export function normalizeMetadataCacheMemoryMb(value: unknown): number { if (typeof value !== "number" || !Number.isFinite(value)) return METADATA_CACHE_DEFAULT_MEMORY_MB; const rounded = Math.round(value); if (rounded > METADATA_CACHE_HARD_MAX_MEMORY_MB) return METADATA_CACHE_DEFAULT_MEMORY_MB; return Math.max(METADATA_CACHE_MIN_MEMORY_MB, rounded); } export function configureMetadataRuntimeCache(memoryMb: unknown): number { if (typeof memoryMb === "number" && Number.isFinite(memoryMb)) { const rounded = Math.round(memoryMb); if (rounded > METADATA_CACHE_HARD_MAX_MEMORY_MB) { console.warn(`[metadata-cache] memory limit ${rounded} MB exceeds the hard limit; falling back to ${METADATA_CACHE_DEFAULT_MEMORY_MB} MB`); } else if (rounded > METADATA_CACHE_RECOMMENDED_MAX_MEMORY_MB) { console.warn(`[metadata-cache] memory limit ${rounded} MB exceeds the recommended ${METADATA_CACHE_RECOMMENDED_MAX_MEMORY_MB} MB`); } } configuredMemoryMb = normalizeMetadataCacheMemoryMb(memoryMb); const connectionIds = new Set([...entries.values()].map((entry) => entry.connectionId)); for (const connectionId of connectionIds) { while (connectionBytes(connectionId) > connectionBudgetBytes()) { if (!evictOldest((entry) => entry.connectionId === connectionId, "connection-budget-reconfigured")) break; } } while (totalBytes > memoryBudgetBytes()) { if (!evictOldest(() => true, "total-budget-reconfigured")) break; } return configuredMemoryMb; } export function getConfiguredMetadataCacheMemoryMb(): number { return configuredMemoryMb; } export function getMetadataRuntimeCache(key: string): MetadataRuntimeCacheHit | undefined { const entry = entries.get(key) as RuntimeCacheEntry | undefined; if (!entry) { l1Misses++; return undefined; } entries.delete(key); entry.touchedAt = now(); entries.set(key, entry); l1Hits++; logCacheMetric("metadata_cache_l1_hit", key, { entrySizeBytes: entry.sizeBytes }, entry.connectionId); return { value: entry.value, sizeBytes: entry.sizeBytes }; } export function setMetadataRuntimeCache(key: string, value: T, connectionId?: string): boolean { const sizeBytes = estimateValueBytes(value); if (sizeBytes > METADATA_CACHE_MAX_ENTRY_BYTES) { logCacheMetric("metadata_cache_l1_skip", key, { entrySizeBytes: sizeBytes, evictionReason: "entry-too-large" }, connectionId); return false; } removeEntry(key); const owner = keyConnectionId(key, connectionId); if (!evictForInsert(owner, sizeBytes)) return false; entries.set(key, { key, connectionId: owner, value, sizeBytes, touchedAt: now() }); totalBytes += sizeBytes; logCacheMetric("metadata_cache_store", key, { entrySizeBytes: sizeBytes }, owner); return true; } export function invalidateMetadataRuntimeCachePrefix(prefix: string): number { let removed = 0; for (const key of [...entries.keys()]) { if (key === prefix || key.startsWith(prefix)) removed += removeEntry(key) ? 1 : 0; } return removed; } export function clearMetadataRuntimeCacheForConnection(connectionId: string): number { let removed = 0; for (const [key, entry] of [...entries.entries()]) { if (entry.connectionId === connectionId) removed += removeEntry(key) ? 1 : 0; } connectionClears++; logCacheMetric("metadata_cache_connection_clear", "", { connectionId, removedEntries: removed }, connectionId); return removed; } export function clearMetadataRuntimeCacheForDatabase(connectionId: string, database: string): number { let removed = 0; for (const [key, entry] of [...entries.entries()]) { const context = cacheLogContext(key, entry.connectionId); if (entry.connectionId === connectionId && context.database === database) removed += removeEntry(key) ? 1 : 0; } logCacheMetric("metadata_cache_database_clear", "", { connectionId, database, removedEntries: removed }, connectionId); return removed; } export function clearMetadataRuntimeCache(): void { entries.clear(); totalBytes = 0; l1Hits = 0; l1Misses = 0; l2Hits = 0; remoteMisses = 0; evictions = 0; connectionClears = 0; } export function recordMetadataCacheL2Hit(key: string, value: unknown): void { l2Hits++; logCacheMetric("metadata_cache_l2_hit", key, { entrySizeBytes: estimateValueBytes(value) }); } export function recordMetadataCacheRemoteMiss(key: string): void { remoteMisses++; logCacheMetric("metadata_cache_remote_miss", key); } export function getMetadataRuntimeCacheDiagnostics(): MetadataRuntimeCacheDiagnostics { return { entries: entries.size, bytes: totalBytes, maxMemoryBytes: memoryBudgetBytes(), maxEntryBytes: METADATA_CACHE_MAX_ENTRY_BYTES, configuredMemoryMb, l1Hits, l1Misses, l2Hits, remoteMisses, evictions, connectionClears, }; }