UNPKG

cachly

Version:

Type-safe, production-ready in-memory cache system for Node.js and TypeScript with advanced features.

694 lines 24.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Cachly = void 0; const compression_1 = require("./utils/compression"); const CircuitBreaker_1 = require("./utils/CircuitBreaker"); const Partitioning_1 = require("./utils/Partitioning"); const Monitoring_1 = require("./utils/Monitoring"); class Cachly { constructor(config = {}) { this.items = new Map(); this.eventEmitter = new (require('events').EventEmitter)(); this.circuitBreakerTrips = 0; // New features this.tags = new Map(); this.groups = new Map(); this.config = { maxItems: 1000, maxMemory: 0, defaultTtl: 0, staleWhileRevalidate: false, log: false, namespace: 'main', persistence: 'none', evictionPolicy: 'lru', compression: { enabled: false, algorithm: 'gzip', threshold: 1024 }, circuitBreaker: { enabled: false, failureThreshold: 5, recoveryTimeout: 30000 }, partitioning: { enabled: false, strategy: 'hash', partitions: 4 }, monitoring: { enabled: false, metrics: [] }, distributed: { enabled: false, nodes: [], replication: false, consistency: 'eventual', partitionStrategy: 'consistent-hashing' }, onHit: () => { }, onMiss: () => { }, ...config, }; this.cacheStats = { hits: 0, misses: 0, evictions: 0, memoryUsage: 0, keyCount: 0, totalAccesses: 0, hitRate: 0, missRate: 0, avgLoadTime: 0, compressionRatio: 0, }; // Initialize advanced features if (this.config.compression.enabled) { this.compressionUtil = new compression_1.CompressionUtil(); } if (this.config.circuitBreaker.enabled) { this.circuitBreaker = new CircuitBreaker_1.CircuitBreaker(this.config.circuitBreaker); } if (this.config.partitioning.enabled) { this.partitioningUtil = new Partitioning_1.PartitioningUtil(this.config.partitioning); } if (this.config.monitoring.enabled) { this.monitoringUtil = new Monitoring_1.MonitoringUtil(this.config.monitoring); } if (this.config.persistence !== 'none') { this.persistence = this.config.persistence; this.initializePersistence(); } if (this.config.defaultTtl > 0) { this.startCleanupInterval(); } if (typeof config['onHit'] === 'function') { this.hitHook = config['onHit']; } if (typeof config['onMiss'] === 'function') { this.missHook = config['onMiss']; } } async set(key, value, options = {}) { const startTime = Date.now(); const now = Date.now(); const ttl = options.ttl ?? this.config.defaultTtl; const expiresAt = ttl && ttl > 0 ? now + ttl : undefined; const staleAt = options.staleTtl ? now + options.staleTtl : undefined; let processedValue = value; let originalSize = 0; let compressedSize = 0; let compressed = false; // Apply compression if enabled if (this.compressionUtil && this.config.compression.enabled) { try { const compressionResult = await compression_1.CompressionUtil.compress(value, this.config.compression); processedValue = compressionResult.data; originalSize = compressionResult.originalSize; compressedSize = compressionResult.compressedSize; compressed = originalSize !== compressedSize; if (compressed) { this.eventEmitter.emit('compress', key, originalSize, compressedSize); } } catch (error) { if (this.config.log) { console.error(`[Cachly] Compression failed for ${key}:`, error); } } } const item = { value: processedValue, createdAt: now, expiresAt, staleAt, dependsOn: new Set(options.dependsOn || []), dependents: new Set(), tags: new Set(options.tags || []), accessCount: 0, lastAccessed: now, compressed, originalSize, compressedSize, }; this.items.set(key, item); this.updateDependencies(key, item.dependsOn); this.updateTags(key, item.tags || new Set()); this.updateStats(); this.eventEmitter.emit('set', key, value); if (this.config.log) { console.log(`[Cachly] Set: ${key}`); } this.evictIfNeeded(); // Record load time for monitoring if (this.monitoringUtil) { this.monitoringUtil.recordLoadTime(Date.now() - startTime); } } async get(key) { const item = this.items.get(key); if (!item) { this.cacheStats.misses++; this.cacheStats.totalAccesses++; this.eventEmitter.emit('miss', key); if (this.missHook) this.missHook(key); return undefined; } if (this.isExpired(item)) { this.delete(key); this.cacheStats.misses++; this.cacheStats.totalAccesses++; this.eventEmitter.emit('miss', key); if (this.missHook) this.missHook(key); return undefined; } item.accessCount++; item.lastAccessed = Date.now(); this.cacheStats.hits++; this.cacheStats.totalAccesses++; this.eventEmitter.emit('hit', key); if (this.hitHook) this.hitHook(key); // Emit partition hit event if (this.partitioningUtil) { const partition = this.partitioningUtil.getPartition(key); this.eventEmitter.emit('partitionHit', partition, key); } let result = item.value; // Decompress if needed if (item.compressed && this.compressionUtil) { try { result = await compression_1.CompressionUtil.decompress(item.value, this.config.compression.algorithm); } catch (error) { if (this.config.log) { console.error(`[Cachly] Decompression failed for ${key}:`, error); } return undefined; } } return result; } async getOrCompute(key, loader, options = {}) { const cached = await this.get(key); if (cached !== undefined) { return cached; } // Use circuit breaker if enabled if (this.circuitBreaker) { return this.circuitBreaker.execute(key, async () => { const value = await loader(); await this.set(key, value, options); return value; }, this.config.circuitBreaker.fallback); } const value = await loader(); await this.set(key, value, options); return value; } async getOrComputeWithStale(key, loader, options = {}) { let item = this.items.get(key); if (item && !this.isExpired(item)) { item.accessCount++; item.lastAccessed = Date.now(); this.cacheStats.hits++; this.cacheStats.totalAccesses++; this.eventEmitter.emit('hit', key); let result = item.value; if (item.compressed && this.compressionUtil) { result = await compression_1.CompressionUtil.decompress(item.value, this.config.compression.algorithm); } return result; } if (item && this.isStale(item) && this.config.staleWhileRevalidate) { this.cacheStats.hits++; this.cacheStats.totalAccesses++; this.eventEmitter.emit('hit', key); if (options.__testAwaitBackground) { try { const value = await loader(); const staleTtl = options.staleTtl ?? 0; const ttl = options.ttl ?? this.config.defaultTtl; await this.set(key, value, { ...options, staleTtl, ttl }); // Update item reference for test determinism item = this.items.get(key); } catch (error) { if (this.config.log) { console.error(`[Cachly] Background refresh failed for ${key}:`, error); } } } else { setTimeout(async () => { try { const value = await loader(); const staleTtl = options.staleTtl ?? 0; const ttl = options.ttl ?? this.config.defaultTtl; await this.set(key, value, { ...options, staleTtl, ttl }); const updated = this.items.get(key); if (updated) { updated.staleAt = staleTtl ? Date.now() + staleTtl : undefined; updated.expiresAt = ttl && ttl > 0 ? Date.now() + ttl : undefined; } } catch (error) { if (this.config.log) { console.error(`[Cachly] Background refresh failed for ${key}:`, error); } } }, 0); } let result = item ? item.value : undefined; if (item && item.compressed && this.compressionUtil) { result = await compression_1.CompressionUtil.decompress(item.value, this.config.compression.algorithm); } return result; } const value = await loader(); await this.set(key, value, options); return value; } has(key) { const item = this.items.get(key); return item !== undefined && !this.isExpired(item); } delete(key) { const item = this.items.get(key); if (item) { this.removeDependencies(key, item.dependsOn); this.removeTags(key, item.tags); this.invalidateDependentsOnDelete(key); this.items.delete(key); this.updateStats(); this.eventEmitter.emit('delete', key); } } clear() { this.items.clear(); this.updateStats(); this.eventEmitter.emit('clear'); } stats() { const stats = { ...this.cacheStats }; // Calculate derived stats if (stats.totalAccesses > 0) { stats.hitRate = (stats.hits / stats.totalAccesses) * 100; stats.missRate = (stats.misses / stats.totalAccesses) * 100; } // Calculate compression ratio let totalOriginalSize = 0; let totalCompressedSize = 0; let compressedItems = 0; for (const item of this.items.values()) { if (item.compressed && item.originalSize && item.compressedSize) { totalOriginalSize += item.originalSize; totalCompressedSize += item.compressedSize; compressedItems++; } } if (totalOriginalSize > 0) { stats.compressionRatio = compression_1.CompressionUtil.calculateCompressionRatio(totalOriginalSize, totalCompressedSize); } // Add monitoring metrics if (this.monitoringUtil) { const monitoringMetrics = this.monitoringUtil.getMetrics(); stats.avgLoadTime = monitoringMetrics.avgLoadTime; } return stats; } async warm(items) { const promises = items.map(async (item) => { try { const value = await item.loader(); await this.set(item.key, value, { ttl: item.ttl, staleTtl: item.staleTtl, dependsOn: item.dependsOn, }); } catch (error) { if (this.config.log) { console.error(`[Cachly] Warmup failed for ${item.key}:`, error); } } }); await Promise.all(promises); } async warmProgressive(items) { const priorities = ['high', 'medium', 'low']; for (const priority of priorities) { const priorityItems = items.filter(item => item.priority === priority); await this.warm(priorityItems); } } async warmWithDependencies(items) { // Sort items by dependency depth const sortedItems = this.sortByDependencies(items); await this.warm(sortedItems); } sortByDependencies(items) { const dependencyMap = new Map(); const visited = new Set(); const sorted = []; // Build dependency map for (const item of items) { dependencyMap.set(item.key, item); } const visit = (item) => { if (visited.has(item.key)) return; visited.add(item.key); // Visit dependencies first for (const dep of item.deps) { const depItem = dependencyMap.get(dep); if (depItem) { visit(depItem); } } sorted.push(item); }; for (const item of items) { visit(item); } return sorted; } health() { if (this.monitoringUtil) { return this.monitoringUtil.health(); } return { status: 'healthy', issues: [], lastCheck: Date.now(), uptime: Date.now() - this.startTime || 0, }; } metrics() { const stats = this.stats(); const baseMetrics = this.monitoringUtil?.getMetrics() || { hitRate: 0, missRate: 0, avgLoadTime: 0, memoryEfficiency: 0, compressionRatio: 0, circuitBreakerTrips: 0, partitionDistribution: {}, }; return { ...baseMetrics, hitRate: stats.hitRate, missRate: stats.missRate, avgLoadTime: stats.avgLoadTime, memoryEfficiency: (stats.memoryUsage / (this.config.maxMemory || 1)) * 100, compressionRatio: stats.compressionRatio, circuitBreakerTrips: this.circuitBreakerTrips, partitionDistribution: this.partitioningUtil?.getPartitionDistribution() || {}, }; } getCircuitBreakerState() { return this.circuitBreaker?.getState(); } getPartitionInfo(partitionId) { return this.partitioningUtil?.getPartitionInfo(partitionId); } getAllPartitions() { return this.partitioningUtil?.getAllPartitions() || []; } isBalanced() { return this.partitioningUtil?.isBalanced() || true; } isExpired(item) { return item.expiresAt !== undefined && Date.now() > item.expiresAt; } isStale(item) { return item.staleAt !== undefined && Date.now() > item.staleAt; } updateDependencies(key, dependencies) { for (const dep of dependencies) { const depItem = this.items.get(dep); if (depItem) { depItem.dependents.add(key); } } } removeDependencies(key, dependencies) { for (const dep of dependencies) { const depItem = this.items.get(dep); if (depItem) { depItem.dependents.delete(key); } } } invalidateDependents(key) { const item = this.items.get(key); if (!item) return; const dependents = new Set(item.dependents); for (const dependent of dependents) { this.delete(dependent); } } invalidateDependentsOnDelete(key) { this.invalidateDependents(key); } evictIfNeeded() { while (this.config.maxItems && this.items.size > this.config.maxItems) { this.evict(); } } evict() { if (this.config.evictionPolicy === 'lru') { this.evictLRU(); } else if (this.config.evictionPolicy === 'ttl') { this.evictTTL(); } else if (this.config.evictionPolicy === 'lfu') { this.evictLFU(); } } evictLRU() { let oldestKey = null; let oldestTime = Date.now(); for (const [key, item] of this.items) { if (item.lastAccessed < oldestTime) { oldestTime = item.lastAccessed; oldestKey = key; } } if (oldestKey) { this.items.delete(oldestKey); this.cacheStats.evictions++; this.eventEmitter.emit('evict', oldestKey, 'lru'); this.updateStats(); } } evictTTL() { const now = Date.now(); for (const [key, item] of this.items) { if (item.expiresAt && now > item.expiresAt) { this.items.delete(key); this.cacheStats.evictions++; this.eventEmitter.emit('evict', key, 'ttl'); } } this.updateStats(); } evictLFU() { let leastFrequentKey = null; let leastFrequentCount = Infinity; for (const [key, item] of this.items) { if (item.accessCount < leastFrequentCount) { leastFrequentCount = item.accessCount; leastFrequentKey = key; } } if (leastFrequentKey) { this.items.delete(leastFrequentKey); this.cacheStats.evictions++; this.eventEmitter.emit('evict', leastFrequentKey, 'lfu'); this.updateStats(); } } updateStats() { this.cacheStats.keyCount = this.items.size; this.cacheStats.memoryUsage = this.estimateMemoryUsage(); } estimateMemoryUsage() { let total = 0; for (const [key, item] of this.items) { total += key.length * 2; if (item.compressed && item.compressedSize) { total += item.compressedSize; } else { total += JSON.stringify(item.value).length * 2; } total += 100; } return total; } startCleanupInterval() { this.cleanupInterval = setInterval(() => { this.evictTTL(); }, 60000); } initializePersistence() { // Initialize persistence layer if needed if (this.persistence) { // Future implementation for persistence initialization } } destroy() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } this.clear(); this.eventEmitter.removeAllListeners(); } on(event, listener) { this.eventEmitter.on(event, listener); return this; } off(event, listener) { this.eventEmitter.off(event, listener); return this; } async invalidateByTag(tag) { const tagData = this.tags.get(tag); if (!tagData) return []; const affectedKeys = Array.from(tagData.keys); for (const key of affectedKeys) { this.delete(key); } this.tags.delete(tag); this.eventEmitter.emit('tagInvalidated', tag, affectedKeys); return affectedKeys; } async invalidateByTags(tags) { const results = {}; for (const tag of tags) { results[tag] = await this.invalidateByTag(tag); } return results; } getKeysByTag(tag) { const tagData = this.tags.get(tag); return tagData ? Array.from(tagData.keys) : []; } getTagsByKey(key) { const item = this.items.get(key); return item?.tags ? Array.from(item.tags) : []; } createGroup(name, config = {}) { const group = { name, keys: new Set(), config, }; this.groups.set(name, group); this.eventEmitter.emit('groupCreated', group); return group; } addToGroup(groupName, key) { const group = this.groups.get(groupName); if (!group) return false; group.keys.add(key); return true; } removeFromGroup(groupName, key) { const group = this.groups.get(groupName); if (!group) return false; return group.keys.delete(key); } getGroupKeys(groupName) { const group = this.groups.get(groupName); return group ? Array.from(group.keys) : []; } deleteGroup(groupName) { const group = this.groups.get(groupName); if (!group) return false; for (const key of group.keys) { this.delete(key); } this.groups.delete(groupName); this.eventEmitter.emit('groupDeleted', groupName); return true; } async bulk(operation) { const startTime = Date.now(); const results = { get: {}, set: [], delete: [], invalidateByTag: {}, }; for (const key of operation.get) { results.get[key] = await this.get(key); } for (const { key, value, options } of operation.set) { await this.set(key, value, options); results.set.push(key); } for (const key of operation.delete) { this.delete(key); results.delete.push(key); } for (const tag of operation.invalidateByTag) { results.invalidateByTag[tag] = await this.invalidateByTag(tag); } const duration = Date.now() - startTime; this.eventEmitter.emit('bulkOperation', operation, { results, duration }); return results; } getKeys(pattern) { const keys = Array.from(this.items.keys()); if (!pattern) return keys; const regex = new RegExp(pattern.replace(/\*/g, '.*')); return keys.filter(key => regex.test(key)); } getKeysByPattern(pattern) { return this.getKeys(pattern); } getItemsByPattern(pattern) { const keys = this.getKeysByPattern(pattern); return keys.map(key => ({ key, item: this.items.get(key) })); } getTopKeys(limit = 10) { return Array.from(this.items.entries()) .map(([key, item]) => ({ key, accessCount: item.accessCount })) .sort((a, b) => b.accessCount - a.accessCount) .slice(0, limit); } getLeastUsedKeys(limit = 10) { return Array.from(this.items.entries()) .map(([key, item]) => ({ key, accessCount: item.accessCount })) .sort((a, b) => a.accessCount - b.accessCount) .slice(0, limit); } getKeysByAge(limit = 10) { const now = Date.now(); return Array.from(this.items.entries()) .map(([key, item]) => ({ key, age: now - item.createdAt })) .sort((a, b) => b.age - a.age) .slice(0, limit); } updateTags(key, tags) { for (const tag of tags) { if (!this.tags.has(tag)) { this.tags.set(tag, { name: tag, keys: new Set(), createdAt: Date.now() }); } this.tags.get(tag).keys.add(key); } } removeTags(key, tags) { if (!tags) return; for (const tag of tags) { const tagData = this.tags.get(tag); if (tagData) { tagData.keys.delete(key); if (tagData.keys.size === 0) { this.tags.delete(tag); } } } } setHitHook(hook) { this.hitHook = hook; } setMissHook(hook) { this.missHook = hook; } } exports.Cachly = Cachly; //# sourceMappingURL=Cachly.js.map