@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
JavaScript
// 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;