UNPKG

@onurege3467/zerohelper

Version:

ZeroHelper is a versatile JavaScript library offering helper functions, validation, logging, database utilities and migration system for developers. It supports MongoDB, MySQL, SQLite, Redis, and PostgreSQL with increment/decrement operations.

586 lines (510 loc) 18.1 kB
// cacheWrapper.js - Redis@4 ve Memory Cache Uyumlu (Düzeltilmiş) const { LRUCache } = require('lru-cache'); const { createClient } = require('redis'); class CacheWrapper { constructor(databaseInstance, options = {}) { this.db = databaseInstance; this.cacheType = options.type || 'memory'; // 'memory' or 'redis' this.tableCaches = {}; // Redis client için başlangıç değerleri this.redisClient = null; this.redisAvailable = false; if (this.cacheType === 'redis') { this._initRedisCache(options); } else { this._initMemoryCache(options); } } _initMemoryCache(options) { this.cache = new LRUCache({ max: options.max || 500, ttl: options.ttl || 1000 * 60 * 5, // milliseconds updateAgeOnGet: options.updateAgeOnGet || false, }); this.redisAvailable = false; this.redisClient = null; } async _initRedisCache(options) { const redisConfig = { socket: { host: options.host || '127.0.0.1', port: options.port || 6379, connectTimeout: options.connectTimeout || 5000, }, password: options.password, database: options.db || 0, }; this.redisClient = createClient(redisConfig); this.ttl = options.ttl || 300; // 5 dakika (saniye cinsinden) this.keyPrefix = options.keyPrefix || 'db_cache:'; this.redisAvailable = false; this.redisClient.on('error', (err) => { console.warn('Redis Cache Error, memory cache fallback:', err.message); this.redisAvailable = false; }); this.redisClient.on('connect', () => { console.log('Redis Cache Connected'); }); this.redisClient.on('ready', () => { console.log('Redis Cache Ready'); this.redisAvailable = true; }); this.redisClient.on('end', () => { console.log('Redis Cache Disconnected'); this.redisAvailable = false; }); try { await Promise.race([ this.redisClient.connect(), new Promise((_, reject) => setTimeout(() => reject(new Error('Redis connection timeout')), 5000) ), ]); this.redisAvailable = true; } catch (error) { console.warn('Redis bağlantısı başarısız, memory cache kullanılacak:', error.message); this._initMemoryCache(options); if (this.redisClient) { this.redisClient.removeAllListeners(); this.redisClient = null; } this.redisAvailable = false; } } _getCache(table) { if (this.cacheType === 'redis' && this.redisAvailable && this.redisClient) { return this.redisClient; } // Memory fallback - tablo bazlı cache oluştur if (!this.tableCaches[table]) { this.tableCaches[table] = new LRUCache({ max: this.cache ? this.cache.max : 500, ttl: this.cache ? this.cache.ttl : 300000, updateAgeOnGet: this.cache ? this.cache.updateAgeOnGet : false, }); } return this.tableCaches[table]; } _generateKey(table, where) { const sortedWhere = where ? this._sortObject(where) : {}; const key = `${table}:${JSON.stringify(sortedWhere)}`; return this.cacheType === 'redis' ? `${this.keyPrefix}${key}` : key; } // Where objelerini sıralama (tutarlı key oluşturma için) _sortObject(obj) { if (!obj || typeof obj !== 'object') return obj; return Object.keys(obj) .sort() .reduce((sorted, key) => { sorted[key] = obj[key]; return sorted; }, {}); } async _getCacheValue(cache, key, table) { if (this.cacheType === 'redis' && this.redisAvailable && this.redisClient) { try { const value = await cache.get(key); return value ? JSON.parse(value) : null; } catch (err) { console.warn('Redis get error, memory fallback:', err.message); this.redisAvailable = false; // Memory cache'e geçiş yap const memoryCache = this._getCache(table); return memoryCache.get(key); } } else { return cache.get(key); } } async _setCacheValue(cache, key, value, table) { if (this.cacheType === 'redis' && this.redisAvailable && this.redisClient) { try { await cache.setEx(key, this.ttl, JSON.stringify(value)); } catch (err) { console.warn('Redis set error, memory fallback:', err.message); this.redisAvailable = false; // Memory cache'e geçiş yap const memoryCache = this._getCache(table); memoryCache.set(key, value); } } else { cache.set(key, value); } } async _clearCache(table) { if (this.cacheType === 'redis' && this.redisAvailable && this.redisClient) { try { const keys = await this.redisClient.keys(`${this.keyPrefix}${table}:*`); if (keys.length) await this.redisClient.del(keys); } catch (err) { console.warn('Redis clear error, memory fallback:', err.message); this.redisAvailable = false; } } // Memory cache'i de temizle if (this.tableCaches[table]) { this.tableCaches[table].clear(); } } // Belirli bir cache key'i sil async _deleteCacheKey(table, key) { if (this.cacheType === 'redis' && this.redisAvailable && this.redisClient) { try { await this.redisClient.del(`${this.keyPrefix}${table}:${key}`); } catch (err) { console.warn('Redis delete key error:', err.message); } } // Memory cache'ten de sil const cache = this._getCache(table); if (cache && cache.delete) { cache.delete(key); } } // Cache'teki veriyi güncelle async _updateCacheValue(table, key, newData) { const cache = this._getCache(table); if (newData !== null && newData !== undefined) { await this._setCacheValue(cache, key, newData, table); } else { await this._deleteCacheKey(table, key); } } // Where koşuluna uyan tüm cache key'lerini bul ve güncelle/sil async _updateCacheByWhere(table, where, newData = null) { // Eğer where boşsa veya çok karmaşıksa, cache'i tamamen temizle if (!where || Object.keys(where).length === 0) { await this._clearCache(table); return; } // Redis için if (this.cacheType === 'redis' && this.redisAvailable && this.redisClient) { try { const keys = await this.redisClient.keys(`${this.keyPrefix}${table}:*`); for (const fullKey of keys) { const cacheData = await this.redisClient.get(fullKey); if (cacheData) { try { const parsedData = JSON.parse(cacheData); // Eğer cached data where koşuluna uyuyorsa güncelle/sil if (this._matchesWhere(parsedData, where)) { if (newData) { await this.redisClient.setEx(fullKey, this.ttl, JSON.stringify(newData)); } else { await this.redisClient.del(fullKey); } } } catch (parseErr) { // Parse edilemeyen veriyi sil await this.redisClient.del(fullKey); } } } } catch (err) { console.warn('Redis update by where error:', err.message); // Hata durumunda cache'i temizle await this._clearCache(table); } } // Memory cache için const cache = this._getCache(table); if (cache && cache.forEach) { const keysToUpdate = []; const keysToDelete = []; cache.forEach((value, key) => { try { if (this._matchesWhere(value, where)) { if (newData) { keysToUpdate.push({ key, data: newData }); } else { keysToDelete.push(key); } } } catch (err) { // Hatalı veriyi sil keysToDelete.push(key); } }); // Güncellemeleri uygula keysToUpdate.forEach(({ key, data }) => { cache.set(key, data); }); keysToDelete.forEach(key => { cache.delete(key); }); } } // Basit where matching (nested object desteği ile) _matchesWhere(data, where) { if (!data || !where) return false; for (const [key, value] of Object.entries(where)) { if (data[key] !== value) { return false; } } return true; } // Read Operations async select(table, where) { const cache = this._getCache(table); const key = this._generateKey(table, where); let data = await this._getCacheValue(cache, key, table); if (data !== null && data !== undefined) return data; data = await this.db.select(table, where); if (data !== null && data !== undefined) { await this._setCacheValue(cache, key, data, table); } return data; } async selectOne(table, where) { const cache = this._getCache(table); const key = this._generateKey(table + '_one', where); // selectOne için farklı key let data = await this._getCacheValue(cache, key, table); if (data !== null && data !== undefined) return data; data = await this.db.selectOne(table, where); if (data !== null && data !== undefined) { await this._setCacheValue(cache, key, data, table); } return data; } // Write Operations (smart cache management) async insert(table, data) { const result = await this.db.insert(table, data); // Insert için cache'i tamamen temizlemek yerine, sadece genel sorguları temizle // Çünkü yeni veri eklendiğinde select() sonuçları değişebilir await this._clearCache(table); return result; } async update(table, data, where) { const result = await this.db.update(table, data, where); if (result > 0) { // Güncellenen verileri cache'te de güncelle await this._updateCacheByWhere(table, where, null); // Önce sil // Güncellenen veriyi cache'e eklemek için fresh data'yı al try { const updatedData = await this.db.selectOne(table, where); if (updatedData) { // selectOne cache'ine ekle const cache = this._getCache(table); const key = this._generateKey(table + '_one', where); await this._setCacheValue(cache, key, updatedData, table); } } catch (err) { // Hata durumunda cache'i temizle await this._clearCache(table); } } return result; } async set(table, data, where) { const result = await this.db.set(table, data, where); // Set operasyonu için güncellenmiş/eklenen veriyi cache'e al try { const newData = await this.db.selectOne(table, where); if (newData) { // Cache'teki eski veriyi güncelle const cache = this._getCache(table); const key = this._generateKey(table + '_one', where); await this._setCacheValue(cache, key, newData, table); // Select cache'lerini de güncelle (where koşuluna uyarsa) await this._updateCacheByWhere(table, where, null); // Eski cache'leri temizle } } catch (err) { // Hata durumunda cache'i temizle await this._clearCache(table); } return result; } async delete(table, where) { const result = await this.db.delete(table, where); if (result > 0) { // Silinen verileri cache'ten de sil await this._updateCacheByWhere(table, where, null); } return result; } async deleteOne(table, where) { const result = await this.db.deleteOne(table, where); if (result > 0) { // Silinen veriyi cache'ten de sil await this._updateCacheByWhere(table, where, null); } return result; } async updateOne(table, data, where) { const result = await this.db.updateOne(table, data, where); if (result > 0) { // Güncellenen veriyi cache'te de güncelle try { const updatedData = await this.db.selectOne(table, where); if (updatedData) { const cache = this._getCache(table); const key = this._generateKey(table + '_one', where); await this._setCacheValue(cache, key, updatedData, table); } } catch (err) { // Hata durumunda ilgili cache'leri temizle await this._updateCacheByWhere(table, where, null); } } return result; } async bulkInsert(table, dataArray) { const result = await this.db.bulkInsert(table, dataArray); // Bulk insert için cache'i tamamen temizle // Çünkü çok sayıda yeni veri eklendiğinde select sonuçları değişir await this._clearCache(table); return result; } async clearAllCache() { if (this.cacheType === 'redis' && this.redisAvailable && this.redisClient) { try { const keys = await this.redisClient.keys(`${this.keyPrefix}*`); if (keys.length) await this.redisClient.del(keys); } catch (err) { console.warn('Redis clearAll error:', err.message); } } // Tüm memory cache'leri temizle Object.values(this.tableCaches).forEach(c => c.clear()); if (this.cache) this.cache.clear(); this.tableCaches = {}; // Referansları da temizle } async close() { await this.clearAllCache(); if (this.redisClient) { try { this.redisClient.removeAllListeners(); if (this.redisAvailable && this.redisClient.isOpen) { await this.redisClient.quit(); } else if (this.redisClient.isOpen) { await this.redisClient.disconnect(); } } catch (err) { console.warn('Redis close error:', err.message); } this.redisClient = null; this.redisAvailable = false; } // Database bağlantısını kapat if (this.db && typeof this.db.close === 'function') { return this.db.close(); } return Promise.resolve(); } /** * Numerik alanları artırır (increment) ve cache'i akıllıca günceller. * @param {string} table - Verinin güncelleneceği tablo adı. * @param {object} increments - Artırılacak alanlar ve miktarları. * @param {object} where - Güncelleme koşulları. * @returns {Promise<number>} Etkilenen kayıt sayısı. */ async increment(table, increments, where = {}) { const result = await this.db.increment(table, increments, where); if (result > 0) { // Cache'teki değerleri de increment et await this._incrementCacheValues(table, increments, where); } return result; } /** * Numerik alanları azaltır (decrement) ve cache'i akıllıca günceller. * @param {string} table - Verinin güncelleneceği tablo adı. * @param {object} decrements - Azaltılacak alanlar ve miktarları. * @param {object} where - Güncelleme koşulları. * @returns {Promise<number>} Etkilenen kayıt sayısı. */ async decrement(table, decrements, where = {}) { const result = await this.db.decrement(table, decrements, where); if (result > 0) { // Cache'teki değerleri de decrement et const negativeIncrements = {}; for (const [key, value] of Object.entries(decrements)) { negativeIncrements[key] = -value; } await this._incrementCacheValues(table, negativeIncrements, where); } return result; } // Cache'teki numerik değerleri increment/decrement et async _incrementCacheValues(table, increments, where) { // Redis için if (this.cacheType === 'redis' && this.redisAvailable && this.redisClient) { try { const keys = await this.redisClient.keys(`${this.keyPrefix}${table}:*`); for (const fullKey of keys) { const cacheData = await this.redisClient.get(fullKey); if (cacheData) { try { const parsedData = JSON.parse(cacheData); if (this._matchesWhere(parsedData, where)) { // Cache'teki değerleri increment et const updatedData = { ...parsedData }; for (const [field, increment] of Object.entries(increments)) { if (typeof updatedData[field] === 'number') { updatedData[field] += increment; } } await this.redisClient.setEx(fullKey, this.ttl, JSON.stringify(updatedData)); } } catch (parseErr) { // Parse edilemeyen veriyi sil await this.redisClient.del(fullKey); } } } } catch (err) { console.warn('Redis increment cache error:', err.message); // Hata durumunda cache'i temizle await this._clearCache(table); } } // Memory cache için const cache = this._getCache(table); if (cache && cache.forEach) { const keysToUpdate = []; cache.forEach((value, key) => { try { if (this._matchesWhere(value, where)) { const updatedData = { ...value }; for (const [field, increment] of Object.entries(increments)) { if (typeof updatedData[field] === 'number') { updatedData[field] += increment; } } keysToUpdate.push({ key, data: updatedData }); } } catch (err) { // Hatalı veriyi sil cache.delete(key); } }); // Güncellemeleri uygula keysToUpdate.forEach(({ key, data }) => { cache.set(key, data); }); } } // Utility methods getCacheStats() { const stats = { type: this.cacheType, redisAvailable: this.redisAvailable, memoryTables: Object.keys(this.tableCaches), }; if (this.cacheType === 'memory') { stats.mainCache = { size: this.cache ? this.cache.size : 0, max: this.cache ? this.cache.max : 0, }; stats.tableCaches = Object.fromEntries( Object.entries(this.tableCaches).map(([table, cache]) => [ table, { size: cache.size, max: cache.max } ]) ); } return stats; } } module.exports = CacheWrapper;