Skip to main content
Glama
index-manager.ts7.15 kB
/** * IndexManager - Fast entity lookup with caching * * Manages index metadata for file-based storage: * - In-memory caching with TTL * - Atomic writes using shared file-utils.ts * - Query operations (find, findOne, etc.) * - Cache invalidation strategies */ import type { IndexMetadata, IndexFile, CacheEntry, CacheOptions, } from './types.js'; import { DEFAULT_CACHE_OPTIONS } from './types.js'; import { atomicWriteJSON, loadJSON } from './file-utils.js'; /** * IndexManager<TMetadata> * * Generic index manager for storing and querying entity metadata */ export class IndexManager<TMetadata extends IndexMetadata = IndexMetadata> { private readonly indexPath: string; private readonly cache = new Map<string, CacheEntry<TMetadata>>(); private readonly cacheOptions: CacheOptions; private readonly inMemoryIndex = new Map<string, TMetadata>(); private isDirty = false; private writeQueue: Promise<void> = Promise.resolve(); constructor( indexPath: string, cacheOptions?: Partial<CacheOptions> ) { this.indexPath = indexPath; // Use DEFAULT_CACHE_OPTIONS from types.ts as single source of truth this.cacheOptions = { ...DEFAULT_CACHE_OPTIONS, ...cacheOptions, }; } /** * Initialize index from disk or create new one * * Uses shared loadJSON from file-utils.ts. */ public async initialize(): Promise<void> { try { const indexFile = await loadJSON<IndexFile<TMetadata>>(this.indexPath); // Load entries into memory for (const entry of indexFile.entries) { this.inMemoryIndex.set(entry.id, entry); } } catch (error) { // Handle file not found - create empty index const nodeError = error as NodeJS.ErrnoException; if (nodeError.code === 'ENOENT') { await this.saveIndexFile(); } else { throw error; } } } /** * Add new entry to index */ public async add(entry: TMetadata): Promise<void> { this.inMemoryIndex.set(entry.id, entry); this.isDirty = true; this.invalidateCache(entry.id); await this.saveIndexFile(); } /** * Get entry by ID */ public get(id: string): Promise<TMetadata | undefined> { // Check cache first if (this.cacheOptions.enabled) { const cached = this.getCached(id); if (cached !== undefined) { return Promise.resolve(cached); } } const entry = this.inMemoryIndex.get(id); // Cache the result if (entry && this.cacheOptions.enabled) { this.setCached(id, entry); } return Promise.resolve(entry); } /** * Update existing entry */ public async update(entry: TMetadata): Promise<void> { if (!this.inMemoryIndex.has(entry.id)) { throw new Error(`Entry with ID '${entry.id}' not found`); } this.inMemoryIndex.set(entry.id, entry); this.isDirty = true; this.invalidateCache(entry.id); await this.saveIndexFile(); } /** * Delete entry by ID */ public async delete(id: string): Promise<void> { this.inMemoryIndex.delete(id); this.isDirty = true; this.invalidateCache(id); await this.saveIndexFile(); } /** * Get all entries */ public getAll(): Promise<TMetadata[]> { return Promise.resolve(Array.from(this.inMemoryIndex.values())); } /** * Clear all entries */ public async clear(): Promise<void> { this.inMemoryIndex.clear(); this.cache.clear(); this.isDirty = true; await this.saveIndexFile(); } /** * Find entries matching predicate */ public find(predicate: (entry: TMetadata) => boolean): Promise<TMetadata[]> { const results: TMetadata[] = []; for (const entry of this.inMemoryIndex.values()) { if (predicate(entry)) { results.push(entry); } } return Promise.resolve(results); } /** * Find first entry matching predicate */ public findOne(predicate: (entry: TMetadata) => boolean): Promise<TMetadata | undefined> { for (const entry of this.inMemoryIndex.values()) { if (predicate(entry)) { return Promise.resolve(entry); } } return Promise.resolve(undefined); } /** * Check if entry exists */ public has(id: string): Promise<boolean> { return Promise.resolve(this.inMemoryIndex.has(id)); } /** * Get index size */ public size(): Promise<number> { return Promise.resolve(this.inMemoryIndex.size); } /** * Save entire index (for batch updates) */ public async saveIndex(entries: TMetadata[]): Promise<void> { this.inMemoryIndex.clear(); for (const entry of entries) { this.inMemoryIndex.set(entry.id, entry); } this.cache.clear(); this.isDirty = true; await this.saveIndexFile(); } /** * Rebuild index (refresh from source) */ public async rebuild(): Promise<void> { // Rebuild keeps existing entries and forces save this.isDirty = true; await this.saveIndexFile(); } // ============================================================================ // Private Methods // ============================================================================ /** * Save index file to disk atomically (with queue to prevent concurrent writes) * * Delegates to shared atomicWriteJSON from file-utils.ts for Windows compatibility. */ private async saveIndexFile(): Promise<void> { // Queue writes to prevent concurrent atomicWriteJSON calls this.writeQueue = this.writeQueue.then(async () => { const indexFile: IndexFile<TMetadata> = { version: 1, indexType: 'entity-index', lastUpdated: new Date().toISOString(), entries: Array.from(this.inMemoryIndex.values()), stats: { totalEntries: this.inMemoryIndex.size, lastRebuild: new Date().toISOString(), }, }; await atomicWriteJSON(this.indexPath, indexFile); this.isDirty = false; }); await this.writeQueue; } /** * Get cached entry */ private getCached(id: string): TMetadata | undefined { const cached = this.cache.get(id); if (!cached) { return undefined; } // Check TTL if (this.cacheOptions.invalidation === 'ttl' && this.cacheOptions.ttl > 0) { const now = Date.now(); const age = now - cached.cachedAt; if (age > this.cacheOptions.ttl) { this.cache.delete(id); return undefined; } } return cached.value; } /** * Set cached entry */ private setCached(id: string, entry: TMetadata): void { // Enforce max cache size (LRU eviction) if (this.cache.size >= this.cacheOptions.maxSize) { // Remove oldest entry const firstKey = this.cache.keys().next().value; if (firstKey !== undefined && firstKey !== '') { this.cache.delete(firstKey); } } this.cache.set(id, { value: entry, cachedAt: Date.now(), ttl: this.cacheOptions.ttl, version: entry.version, }); } /** * Invalidate cache entry */ private invalidateCache(id: string): void { this.cache.delete(id); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cppmyjob/cpp-mcp-planner'

If you have feedback or need assistance with the MCP directory API, please join our Discord server