UNPKG

@yihuangdb/storage-object

Version:

A Node.js storage object layer library using Redis OM

742 lines • 29.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.OptimizedSchemaRegistry = void 0; exports.getOptimizedSchemaRegistry = getOptimizedSchemaRegistry; const storage_1 = require("./storage"); const connection_1 = require("./connection"); const performance_profiler_1 = require("./performance-profiler"); class OptimizedSchemaRegistry { static instance; registeredSchemas = new Map(); storageInstances = new Map(); client; options; registryKey = 'schema:registry'; // Performance optimizations cache = new Map(); cacheSize = 100; cacheTTL = 60000; // 60 seconds accessFrequency = new Map(); hotSchemas = new Set(); pendingOperations = new Map(); cleanupInterval; hotSchemaInterval; pendingPersistence; constructor(options = {}) { this.options = { enableCache: true, cacheSize: 100, cacheTTL: 60000, enableProfiling: true, lazyLoad: true, compressionEnabled: false, ...options, }; this.cacheSize = options.cacheSize || 100; this.cacheTTL = options.cacheTTL || 60000; if (options.persistMetadata !== false) { this.initializeRedis(); } if (options.enableProfiling) { performance_profiler_1.profiler.enable(); performance_profiler_1.profiler.setSlowThreshold(50); // 50ms for registry operations } // Periodic cache cleanup this.cleanupInterval = setInterval(() => this.cleanupCache(), 30000); // Periodic hot schema detection this.hotSchemaInterval = setInterval(() => this.detectHotSchemas(), 60000); } static getInstance(options) { if (!OptimizedSchemaRegistry.instance) { OptimizedSchemaRegistry.instance = new OptimizedSchemaRegistry(options); } return OptimizedSchemaRegistry.instance; } async initializeRedis() { await performance_profiler_1.profiler.measure('registry.initializeRedis', async () => { const connection = connection_1.RedisConnection.getInstance({ url: this.options.redisUrl, }); await connection.connect(); this.client = await connection.getClient(); if (!this.options.lazyLoad) { await this.loadPersistedSchemas(); } }); } async loadPersistedSchemas() { if (!this.client) return; await performance_profiler_1.profiler.measure('registry.loadPersistedSchemas', async () => { try { const schemas = await this.client.hGetAll(this.registryKey); // Load schemas in parallel for better performance const loadPromises = Object.entries(schemas).map(async ([name, metadataJson]) => { try { const metadata = JSON.parse(metadataJson); // Convert date strings back to Date objects metadata.createdAt = new Date(metadata.createdAt); metadata.updatedAt = new Date(metadata.updatedAt); metadata.lastAccessedAt = new Date(metadata.lastAccessedAt); if (metadata.performance?.slowestOperation?.timestamp) { metadata.performance.slowestOperation.timestamp = new Date(metadata.performance.slowestOperation.timestamp); } this.registeredSchemas.set(name, metadata); // Pre-cache frequently accessed schemas if (metadata.statistics.totalOperations > 100) { this.hotSchemas.add(name); this.cacheSchema(name, metadata); } } catch (error) { console.error(`Failed to parse schema metadata for ${name}:`, error); } }); await Promise.all(loadPromises); } catch (error) { console.error('Failed to load persisted schemas:', error); } }); } async register(name, schema, options) { return performance_profiler_1.profiler.measure('registry.register', async () => { // Check cache first const cached = this.getCached(`register:${name}`); if (cached && this.schemasEqual(cached.schema, schema)) { return cached; } // Check for pending registration to avoid duplicate work const pendingKey = `register:${name}`; if (this.pendingOperations.has(pendingKey)) { return this.pendingOperations.get(pendingKey); } const registrationPromise = this.performRegistration(name, schema, options); this.pendingOperations.set(pendingKey, registrationPromise); try { const result = await registrationPromise; return result; } finally { this.pendingOperations.delete(pendingKey); } }, { name, fieldCount: Object.keys(schema).length }); } async performRegistration(name, schema, options) { // Check if schema already exists let metadata = this.registeredSchemas.get(name); if (metadata) { // Check if schema has changed if (!this.schemasEqual(metadata.schema, schema)) { // Update version metadata.version++; metadata.schema = schema; metadata.updatedAt = new Date(); metadata.attributes = this.analyzeSchema(schema); metadata.options = options; } metadata.lastAccessedAt = new Date(); } else { // Create new metadata metadata = { name, version: 1, schema, options, createdAt: new Date(), updatedAt: new Date(), lastAccessedAt: new Date(), attributes: this.analyzeSchema(schema), statistics: { objectCount: 0, createCount: 0, readCount: 0, updateCount: 0, deleteCount: 0, totalOperations: 0, averageObjectSize: 0, }, performance: { averageCreateTime: 0, averageReadTime: 0, averageUpdateTime: 0, averageDeleteTime: 0, slowestOperation: { type: 'none', duration: 0, timestamp: new Date(), }, }, status: 'active', }; } this.registeredSchemas.set(name, metadata); // Cache the metadata this.cacheSchema(name, metadata); // Persist to Redis asynchronously to avoid blocking if (this.options.persistMetadata && this.client) { this.persistSchemaAsync(name, metadata); } return metadata; } analyzeSchema(schema) { return performance_profiler_1.profiler.measureSync('registry.analyzeSchema', () => { const fields = {}; let fieldCount = 0; let indexedFieldCount = 0; for (const [fieldName, fieldConfig] of Object.entries(schema)) { fieldCount++; const config = typeof fieldConfig === 'string' ? { type: fieldConfig, indexed: false } : fieldConfig; if (config.indexed) { indexedFieldCount++; } fields[fieldName] = { type: config.type, indexed: config.indexed || false, nullCount: 0, uniqueValues: 0, }; } return { fieldCount, indexedFieldCount, fields, }; }); } async persistSchemaAsync(name, metadata) { if (!this.client) return; // Use Promise to track persistence const persistPromise = (async () => { try { const metadataJson = this.options.compressionEnabled ? await this.compress(metadata) : JSON.stringify(metadata); await this.client.hSet(this.registryKey, name, metadataJson); } catch (error) { console.error(`Failed to persist schema ${name}:`, error); } })(); // Track the promise for cleanup if (!this.pendingPersistence) { this.pendingPersistence = new Set(); } this.pendingPersistence.add(persistPromise); persistPromise.finally(() => { this.pendingPersistence?.delete(persistPromise); }); // Don't await to keep it non-blocking return; } async getStorage(name) { return performance_profiler_1.profiler.measure('registry.getStorage', async () => { // Track access frequency for hot schema detection this.trackAccess(name); // Check cache first let storage = this.storageInstances.get(name); if (storage) { this.updateLastAccessed(name); return storage; } // Check for pending storage creation const pendingKey = `storage:${name}`; if (this.pendingOperations.has(pendingKey)) { return this.pendingOperations.get(pendingKey); } // Check if schema is registered let metadata = this.getCached(`metadata:${name}`) || this.registeredSchemas.get(name); if (!metadata && this.client) { // Lazy load from Redis if not in memory metadata = await this.lazyLoadSchema(name); } if (!metadata) { return null; } const creationPromise = this.createStorageInstance(name, metadata); this.pendingOperations.set(pendingKey, creationPromise); try { storage = await creationPromise; return storage; } finally { this.pendingOperations.delete(pendingKey); } }, { name }); } async lazyLoadSchema(name) { return performance_profiler_1.profiler.measure('registry.lazyLoadSchema', async () => { if (!this.client) return null; try { const metadataJson = await this.client.hGet(this.registryKey, name); if (!metadataJson) return null; const metadata = this.options.compressionEnabled ? await this.decompress(metadataJson) : JSON.parse(metadataJson); // Convert date strings back to Date objects metadata.createdAt = new Date(metadata.createdAt); metadata.updatedAt = new Date(metadata.updatedAt); metadata.lastAccessedAt = new Date(metadata.lastAccessedAt); if (metadata.performance?.slowestOperation?.timestamp) { metadata.performance.slowestOperation.timestamp = new Date(metadata.performance.slowestOperation.timestamp); } this.registeredSchemas.set(name, metadata); this.cacheSchema(name, metadata); return metadata; } catch (error) { console.error(`Failed to lazy load schema ${name}:`, error); return null; } }, { name }); } async registerStorageInstance(name, storage) { await performance_profiler_1.profiler.measure('registry.registerStorageInstance', async () => { // Wrap storage methods to track statistics this.wrapStorageMethods(storage, name); // Store the instance this.storageInstances.set(name, storage); this.updateLastAccessed(name); // Pre-warm cache for hot schemas if (this.hotSchemas.has(name)) { const metadata = this.registeredSchemas.get(name); if (metadata) { this.cacheSchema(name, metadata); } } }, { name }); } async createStorageInstance(name, metadata) { return performance_profiler_1.profiler.measure('registry.createStorageInstance', async () => { const storage = new storage_1.StorageObject(name, metadata.schema, metadata.options); await storage.initialize(); // Wrap storage methods to track statistics this.wrapStorageMethods(storage, name); this.storageInstances.set(name, storage); this.updateLastAccessed(name); return storage; }, { name }); } wrapStorageMethods(storage, name) { const metadata = this.registeredSchemas.get(name); if (!metadata) return; // Wrap create method const originalCreate = storage.create.bind(storage); storage.create = async (data) => { const startTime = Date.now(); try { const result = await originalCreate(data); const duration = Date.now() - startTime; this.updateStatistics(name, 'create', duration); // Update cache if this is a hot schema if (this.hotSchemas.has(name)) { this.invalidateCache(`storage:${name}:*`); } return result; } catch (error) { throw error; } }; // Wrap findById method with caching const originalFindById = storage.findById.bind(storage); storage.findById = async (id) => { const cacheKey = `storage:${name}:findById:${id}`; const cached = this.getCached(cacheKey); if (cached) { this.updateStatistics(name, 'read', 0); // Cache hit return cached; } const startTime = Date.now(); try { const result = await originalFindById(id); const duration = Date.now() - startTime; this.updateStatistics(name, 'read', duration); // Cache the result if this is a hot schema if (this.hotSchemas.has(name) && result) { this.setCached(cacheKey, result, this.cacheTTL / 2); // Shorter TTL for individual items } return result; } catch (error) { throw error; } }; // Wrap find method with query caching const originalFind = storage.find.bind(storage); storage.find = async (query, options) => { const cacheKey = `storage:${name}:find:${JSON.stringify({ query, options })}`; const cached = this.getCached(cacheKey); if (cached) { this.updateStatistics(name, 'read', 0); // Cache hit return cached; } const startTime = Date.now(); try { const result = await originalFind(query, options); const duration = Date.now() - startTime; this.updateStatistics(name, 'read', duration); // Cache query results for hot schemas if (this.hotSchemas.has(name) && result.length < 100) { this.setCached(cacheKey, result, this.cacheTTL / 4); // Even shorter TTL for queries } return result; } catch (error) { throw error; } }; // Wrap update method const originalUpdate = storage.update.bind(storage); storage.update = async (id, data, options) => { const startTime = Date.now(); try { const result = await originalUpdate(id, data, options); const duration = Date.now() - startTime; this.updateStatistics(name, 'update', duration); // Invalidate caches this.invalidateCache(`storage:${name}:findById:${id}`); this.invalidateCache(`storage:${name}:find:*`); return result; } catch (error) { throw error; } }; // Wrap delete method const originalDelete = storage.delete.bind(storage); storage.delete = async (id) => { const startTime = Date.now(); try { const result = await originalDelete(id); if (result) { const duration = Date.now() - startTime; this.updateStatistics(name, 'delete', duration); // Invalidate caches this.invalidateCache(`storage:${name}:findById:${id}`); this.invalidateCache(`storage:${name}:find:*`); } return result; } catch (error) { throw error; } }; } updateStatistics(name, operation, duration) { const metadata = this.registeredSchemas.get(name); if (!metadata) return; // Update operation counts metadata.statistics[`${operation}Count`]++; metadata.statistics.totalOperations++; // Update performance metrics with exponential moving average for smoothing const perfKey = `average${operation.charAt(0).toUpperCase() + operation.slice(1)}Time`; const alpha = 0.1; // Smoothing factor const currentAvg = metadata.performance[perfKey]; metadata.performance[perfKey] = currentAvg * (1 - alpha) + duration * alpha; // Track slowest operation if (duration > metadata.performance.slowestOperation.duration) { metadata.performance.slowestOperation = { type: operation, duration, timestamp: new Date(), }; } // Update last accessed time metadata.lastAccessedAt = new Date(); } updateLastAccessed(name) { const metadata = this.registeredSchemas.get(name); if (metadata) { metadata.lastAccessedAt = new Date(); } } schemasEqual(schema1, schema2) { const keys1 = Object.keys(schema1).sort(); const keys2 = Object.keys(schema2).sort(); if (keys1.length !== keys2.length) { return false; } for (let i = 0; i < keys1.length; i++) { if (keys1[i] !== keys2[i]) { return false; } const field1 = schema1[keys1[i]]; const field2 = schema2[keys2[i]]; if (JSON.stringify(field1) !== JSON.stringify(field2)) { return false; } } return true; } // Cache management methods getCached(key) { if (!this.options.enableCache) return null; const entry = this.cache.get(key); if (!entry) return null; // Check if cache is expired if (Date.now() - entry.timestamp > this.cacheTTL) { this.cache.delete(key); return null; } // Update hit count entry.hits++; return entry.data; } setCached(key, data, ttl) { if (!this.options.enableCache) return; // Enforce cache size limit if (this.cache.size >= this.cacheSize) { this.evictLRU(); } this.cache.set(key, { data, timestamp: Date.now(), hits: 0, }); } cacheSchema(name, metadata) { this.setCached(`metadata:${name}`, metadata); this.setCached(`register:${name}`, metadata); } invalidateCache(pattern) { if (pattern.includes('*')) { // Wildcard pattern const regex = new RegExp(pattern.replace(/\*/g, '.*')); for (const key of this.cache.keys()) { if (regex.test(key)) { this.cache.delete(key); } } } else { // Exact key this.cache.delete(pattern); } } evictLRU() { // Find least recently used cache entry let lruKey = null; let minHits = Infinity; let oldestTime = Date.now(); for (const [key, entry] of this.cache.entries()) { const score = entry.hits * 1000 + (Date.now() - entry.timestamp); if (score < minHits) { minHits = score; lruKey = key; } } if (lruKey) { this.cache.delete(lruKey); } } cleanupCache() { const now = Date.now(); for (const [key, entry] of this.cache.entries()) { if (now - entry.timestamp > this.cacheTTL) { this.cache.delete(key); } } } trackAccess(name) { const count = (this.accessFrequency.get(name) || 0) + 1; this.accessFrequency.set(name, count); } detectHotSchemas() { // Calculate access frequency threshold const frequencies = Array.from(this.accessFrequency.values()); if (frequencies.length === 0) return; frequencies.sort((a, b) => b - a); const threshold = frequencies[Math.min(5, Math.floor(frequencies.length * 0.2))]; // Top 20% or top 5 // Update hot schemas set this.hotSchemas.clear(); for (const [name, frequency] of this.accessFrequency.entries()) { if (frequency >= threshold) { this.hotSchemas.add(name); } } // Reset frequencies for next period this.accessFrequency.clear(); } async compress(data) { // Implement compression if needed // For now, just stringify return JSON.stringify(data); } async decompress(data) { // Implement decompression if needed // For now, just parse return JSON.parse(data); } // Public methods for monitoring and management async getSchemaMetadata(name) { return performance_profiler_1.profiler.measure('registry.getSchemaMetadata', async () => { const cached = this.getCached(`metadata:${name}`); if (cached) return cached; const metadata = this.registeredSchemas.get(name); if (metadata) { this.cacheSchema(name, metadata); return metadata; } // Try lazy loading if enabled if (this.options.lazyLoad && this.client) { return this.lazyLoadSchema(name); } return null; }, { name }); } async getAllSchemas() { return performance_profiler_1.profiler.measure('registry.getAllSchemas', async () => { // Load all schemas if using lazy loading if (this.options.lazyLoad && this.client) { await this.loadPersistedSchemas(); } return Array.from(this.registeredSchemas.values()); }); } getPerformanceReport() { let hits = 0; let totalAccess = 0; for (const entry of this.cache.values()) { hits += entry.hits; totalAccess += entry.hits + 1; } return { cacheStats: { size: this.cache.size, hits, misses: totalAccess - hits, hitRate: totalAccess > 0 ? hits / totalAccess : 0, }, hotSchemas: Array.from(this.hotSchemas), registryMetrics: performance_profiler_1.profiler.getMetrics().filter(m => m.operation.startsWith('registry.')), bottlenecks: performance_profiler_1.profiler.getBottlenecks(), }; } async updateObjectCount(name) { await performance_profiler_1.profiler.measure('registry.updateObjectCount', async () => { const storage = await this.getStorage(name); if (!storage) return; const metadata = this.registeredSchemas.get(name); if (!metadata) return; try { const count = await storage.count(); metadata.statistics.objectCount = count; // Update storage size estimate if (count > 0) { // Estimate average object size (this is a rough estimate) const sampleSize = Math.min(10, count); const samples = await storage.find({}, { limit: sampleSize }); let totalSize = 0; for (const sample of samples) { totalSize += JSON.stringify(sample).length; } metadata.statistics.averageObjectSize = totalSize / samples.length; } // Persist updated metadata if (this.options.persistMetadata && this.client) { await this.persistSchemaAsync(name, metadata); } } catch (error) { console.error(`Failed to update object count for ${name}:`, error); } }, { name }); } async clearAllPersistedSchemas() { if (!this.client) return; await performance_profiler_1.profiler.measure('registry.clearAllPersistedSchemas', async () => { try { // Delete the entire registry hash await this.client.del(this.registryKey); // Clear cache this.cache.clear(); } catch (error) { console.error('Failed to clear persisted schemas:', error); } }); } async shutdown() { await performance_profiler_1.profiler.measure('registry.shutdown', async () => { // Clear intervals if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = undefined; } if (this.hotSchemaInterval) { clearInterval(this.hotSchemaInterval); this.hotSchemaInterval = undefined; } // Wait for pending persistence operations if (this.pendingPersistence && this.pendingPersistence.size > 0) { await Promise.all(Array.from(this.pendingPersistence)); this.pendingPersistence.clear(); } // Clear all storage instances for (const storage of this.storageInstances.values()) { try { await storage.disconnect(); } catch (error) { console.error('Error disconnecting storage:', error); } } // Clear instances this.storageInstances.clear(); this.cache.clear(); this.pendingOperations.clear(); // Close Redis connection if (this.client) { try { await this.client.quit(); } catch (error) { console.error('Error closing Redis client:', error); } this.client = null; } // Print performance report if profiling is enabled if (this.options.enableProfiling) { console.log('\nšŸ“Š Schema Registry Performance Report:'); const report = this.getPerformanceReport(); console.log('Cache Hit Rate:', (report.cacheStats.hitRate * 100).toFixed(2) + '%'); console.log('Hot Schemas:', report.hotSchemas.join(', ')); performance_profiler_1.profiler.printReport(); } }); } static async reset() { if (OptimizedSchemaRegistry.instance) { try { await OptimizedSchemaRegistry.instance.shutdown(); } catch (error) { console.error('Error during OptimizedSchemaRegistry shutdown:', error); } OptimizedSchemaRegistry.instance = null; } } } exports.OptimizedSchemaRegistry = OptimizedSchemaRegistry; // Export singleton getter function getOptimizedSchemaRegistry(options) { return OptimizedSchemaRegistry.getInstance(options); } //# sourceMappingURL=schema-registry-optimized.js.map