import * as api from "@/lib/backend/api"; import type { MetadataCacheInvalidation } from "./metadataResultCache"; import type { ObjectDdlRequest } from "./objectDdlCache"; const OBJECT_METADATA_CACHE_PREFIX = "object-meta:v1"; const MAX_PERSISTED_METADATA_CHARS = 5 * 1024 * 1024; interface ObjectMetadataCacheEnvelope { version: 1; cachedAt: string; value: T; } interface InFlightLoad { force: boolean; invalidated: boolean; promise: Promise; } const inFlightLoads = new Map>(); const pendingInvalidations = new Map>(); async function loadSchemaCacheSafe(cacheKey: string): Promise { try { return await api.loadSchemaCache(cacheKey); } catch { return null; } } async function saveSchemaCacheSafe(cacheKey: string, payload: unknown): Promise { try { await api.saveSchemaCache(cacheKey, payload); } catch { // Cache persistence is best effort and must not block metadata rendering. } } async function deleteSchemaCachePrefixSafe(prefix: string): Promise { try { await api.deleteSchemaCachePrefix(prefix); } catch { // Cache invalidation is best effort when running with a reduced backend. } } export type ObjectMetadataFacet = "columns" | "indexes" | "foreign-keys" | "triggers" | "comment"; function cacheSegment(value: string | undefined): string { return encodeURIComponent(value ?? ""); } function facetKey(request: ObjectDdlRequest, facet: ObjectMetadataFacet): string { return `${[OBJECT_METADATA_CACHE_PREFIX, cacheSegment(request.connectionId), cacheSegment(request.database), cacheSegment(request.schema), cacheSegment(request.tableName), cacheSegment(request.catalog), facet].join(":")}:`; } function invalidationPrefix(match: MetadataCacheInvalidation): string { const parts = [OBJECT_METADATA_CACHE_PREFIX]; if (!match.connectionId) return `${OBJECT_METADATA_CACHE_PREFIX}:`; parts.push(cacheSegment(match.connectionId)); if (!match.database) return `${parts.join(":")}:`; parts.push(cacheSegment(match.database)); if (!match.schema) return `${parts.join(":")}:`; parts.push(cacheSegment(match.schema)); if (!match.tableName) return `${parts.join(":")}:`; parts.push(cacheSegment(match.tableName)); return `${parts.join(":")}:`; } function decodeEnvelope(payload: unknown): T | null { if (!payload || typeof payload !== "object") return null; const envelope = payload as Partial>; if (envelope.version !== 1 || typeof envelope.cachedAt !== "string" || !Number.isFinite(Date.parse(envelope.cachedAt)) || !("value" in envelope)) return null; return envelope.value === undefined ? null : envelope.value; } async function waitForPendingInvalidations(cacheKey: string): Promise { const pending = [...pendingInvalidations.entries()].filter(([prefix]) => cacheKey.startsWith(prefix)).map(([, promise]) => promise); if (pending.length) await Promise.all(pending); } export async function loadObjectMetadataFacet(request: ObjectDdlRequest, facet: ObjectMetadataFacet, loader: () => Promise, options?: { force?: boolean }): Promise<{ value: T; cacheStatus: "disk" | "remote" }> { const cacheKey = facetKey(request, facet); await waitForPendingInvalidations(cacheKey); if (!options?.force) { const cached = decodeEnvelope(await loadSchemaCacheSafe(cacheKey)); if (cached !== null) return { value: cached, cacheStatus: "disk" }; } const existing = inFlightLoads.get(cacheKey); if (existing && (!options?.force || existing.force)) return { value: (await existing.promise) as T, cacheStatus: "remote" }; if (existing) { existing.invalidated = true; inFlightLoads.delete(cacheKey); } const entry: InFlightLoad = { force: options?.force === true, invalidated: false, promise: Promise.resolve(undefined as T) }; entry.promise = loader() .then(async (value) => { const serialized = JSON.stringify(value); if (!entry.invalidated && serialized.length <= MAX_PERSISTED_METADATA_CHARS) { const envelope: ObjectMetadataCacheEnvelope = { version: 1, cachedAt: new Date().toISOString(), value }; await saveSchemaCacheSafe(cacheKey, envelope); } return value; }) .finally(() => { if (inFlightLoads.get(cacheKey) === entry) inFlightLoads.delete(cacheKey); }); inFlightLoads.set(cacheKey, entry as InFlightLoad); return { value: await entry.promise, cacheStatus: "remote" }; } export async function invalidateObjectMetadataCache(match: MetadataCacheInvalidation): Promise { const prefix = invalidationPrefix(match); for (const [cacheKey, entry] of inFlightLoads) { if (!cacheKey.startsWith(prefix)) continue; entry.invalidated = true; inFlightLoads.delete(cacheKey); } const existing = pendingInvalidations.get(prefix); if (existing) return existing; const deletion = deleteSchemaCachePrefixSafe(prefix).finally(() => { if (pendingInvalidations.get(prefix) === deletion) pendingInvalidations.delete(prefix); }); pendingInvalidations.set(prefix, deletion); return deletion; }