netakhoj-api / services /cacheService.js
Rakeshops71
Deploy app with LFS for large files
3eedfc9
Raw
History Blame Contribute Delete
5.66 kB
import { createLogger } from '../utils/logger.js';
const logger = createLogger('CACHE');
class CacheService {
constructor() {
this.cache = new Map();
this.stats = {
prs: { hits: 0, misses: 0, sets: 0, expirations: 0 },
candidate: { hits: 0, misses: 0, sets: 0, expirations: 0 },
geojson: { hits: 0, misses: 0, sets: 0, expirations: 0 },
image: { hits: 0, misses: 0, sets: 0, expirations: 0 },
poll: { hits: 0, misses: 0, sets: 0, expirations: 0 }
};
this.logger = logger;
logger.info('INIT', 'Cache service initialized');
}
getCacheKey(type, key) {
return `${type}:${key}`;
}
getTTL(type) {
const ttls = {
image: 24 * 60 * 60 * 1000,
prs: 60 * 60 * 1000,
candidate: 60 * 60 * 1000,
geojson: 24 * 60 * 60 * 1000,
poll: 5 * 60 * 1000
};
return ttls[type] || 60 * 60 * 1000;
}
set(type, key, value, ttl = null) {
const cacheKey = this.getCacheKey(type, key);
const expiresAt = Date.now() + (ttl || this.getTTL(type));
if (!this.stats[type]) {
this.stats[type] = { hits: 0, misses: 0, sets: 0, expirations: 0 };
}
this.cache.set(cacheKey, {
data: value,
expiresAt,
createdAt: Date.now(),
type
});
this.stats[type].sets++;
if (type === 'image') {
this.logger.info('SET', `Cached image: ${key}`, {
size: value?.buffer ? `${(value.buffer.length / 1024).toFixed(2)} KB` : 'unknown',
contentType: value?.contentType || 'unknown',
expiresIn: `${Math.round((ttl || this.getTTL(type)) / 1000 / 60)} minutes`
});
} else {
this.logger.info('SET', `Cached ${type}: ${key}`);
}
return true;
}
get(type, key) {
const cacheKey = this.getCacheKey(type, key);
const entry = this.cache.get(cacheKey);
if (!this.stats[type]) {
this.stats[type] = { hits: 0, misses: 0, sets: 0, expirations: 0 };
}
if (!entry) {
this.stats[type].misses++;
this.logger.info('MISS', `Cache miss: ${type}:${key}`);
return null;
}
if (Date.now() > entry.expiresAt) {
this.cache.delete(cacheKey);
this.stats[type].misses++;
this.stats[type].expirations++;
this.logger.info('EXPIRED', `Cache expired: ${type}:${key}`);
return null;
}
this.stats[type].hits++;
if (type === 'image') {
this.logger.success('HIT', `Cache hit: ${type}:${key}`, {
age: `${Math.round((Date.now() - entry.createdAt) / 1000 / 60)} minutes`,
size: entry.data?.buffer ? `${(entry.data.buffer.length / 1024).toFixed(2)} KB` : 'unknown'
});
} else {
this.logger.success('HIT', `Cache hit: ${type}:${key}`);
}
return entry.data;
}
has(type, key) {
const cacheKey = this.getCacheKey(type, key);
const entry = this.cache.get(cacheKey);
if (!entry) return false;
if (Date.now() > entry.expiresAt) {
this.cache.delete(cacheKey);
return false;
}
return true;
}
delete(type, key) {
const cacheKey = this.getCacheKey(type, key);
const deleted = this.cache.delete(cacheKey);
if (deleted) {
this.logger.info('DELETE', `Deleted cache: ${type}:${key}`);
}
return deleted;
}
flush(type = null) {
if (type) {
let deleted = 0;
for (const [key, entry] of this.cache.entries()) {
if (entry.type === type) {
this.cache.delete(key);
deleted++;
}
}
if (this.stats[type]) {
this.stats[type] = { hits: 0, misses: 0, sets: 0, expirations: 0 };
}
this.logger.info('FLUSH', `Flushed cache type: ${type}`, { deleted });
} else {
const size = this.cache.size;
this.cache.clear();
Object.keys(this.stats).forEach(key => {
this.stats[key] = { hits: 0, misses: 0, sets: 0, expirations: 0 };
});
this.logger.info('FLUSH', `Flushed all cache`, { deleted: size });
}
}
cleanup() {
const now = Date.now();
let cleaned = 0;
for (const [key, entry] of this.cache.entries()) {
if (now > entry.expiresAt) {
this.cache.delete(key);
cleaned++;
if (this.stats[entry.type]) {
this.stats[entry.type].expirations++;
}
}
}
if (cleaned > 0) {
this.logger.info('CLEANUP', `Cleaned ${cleaned} expired entries`, {
remaining: this.cache.size
});
}
return cleaned;
}
getStats() {
const typeStats = {};
for (const type in this.stats) {
const stat = this.stats[type];
const total = stat.hits + stat.misses;
const hitRate = total > 0 ? ((stat.hits / total) * 100).toFixed(2) : '0.00';
typeStats[type] = {
...stat,
hitRate: `${hitRate}%`
};
}
return {
total: this.cache.size,
types: typeStats,
memory: this._estimateMemoryUsage()
};
}
_estimateMemoryUsage() {
let totalBytes = 0;
for (const [key, entry] of this.cache.entries()) {
totalBytes += key.length * 2;
if (entry.data?.buffer && Buffer.isBuffer(entry.data.buffer)) {
totalBytes += entry.data.buffer.length;
} else if (typeof entry.data === 'string') {
totalBytes += entry.data.length * 2;
} else if (entry.data) {
totalBytes += JSON.stringify(entry.data).length * 2;
}
}
return {
bytes: totalBytes,
kb: (totalBytes / 1024).toFixed(2),
mb: (totalBytes / 1024 / 1024).toFixed(2)
};
}
}
const cacheService = new CacheService();
setInterval(() => {
cacheService.cleanup();
}, 5 * 60 * 1000);
export default cacheService;