@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
852 lines • 25.8 kB
JavaScript
/**
* @fileoverview OrdoJS Cache Manager - Comprehensive caching and real-time data management
*/
/**
* Default cache configuration
*/
const DEFAULT_CACHE_CONFIG = {
type: 'memory',
defaultTTL: 3600,
maxSize: 100 * 1024 * 1024, // 100MB
enableCompression: true,
enableEncryption: false,
enableRealtimeSync: false,
syncInterval: 5000,
enableCacheWarming: false,
warmingStrategies: [],
enableAnalytics: true,
analyticsRetention: 30,
redis: {
host: 'localhost',
port: 6379,
database: 0,
timeout: 5000,
retryStrategy: 'exponential',
maxRetries: 3
},
file: {
directory: './cache',
extension: '.cache',
enableSubdirectories: true,
maxFileSize: 10 * 1024 * 1024, // 10MB
cleanupInterval: 3600000 // 1 hour
}
};
/**
* Comprehensive cache manager for OrdoJS applications
*/
export class CacheManager {
config;
memoryCache = new Map();
redisClient = null;
fileCache = new Map();
analytics = {
totalRequests: 0,
hits: 0,
misses: 0,
hitRate: 0,
averageResponseTime: 0,
memoryUsage: 0,
diskUsage: 0,
topKeys: [],
performance: {
readsPerSecond: 0,
writesPerSecond: 0,
deletesPerSecond: 0,
averageReadTime: 0,
averageWriteTime: 0,
averageDeleteTime: 0
}
};
syncEvents = [];
warmingJobs = new Map();
cleanupInterval = null;
syncInterval = null;
constructor(config = {}) {
this.config = { ...DEFAULT_CACHE_CONFIG, ...config };
this.initializeCache();
}
/**
* Set cache entry
*/
async set(key, data, ttl = this.config.defaultTTL, tags = [], metadata = {}) {
const startTime = Date.now();
try {
const entry = {
key,
data,
expiresAt: new Date(Date.now() + ttl * 1000),
createdAt: new Date(),
lastAccessed: new Date(),
accessCount: 0,
size: this.calculateSize(data),
tags,
metadata
};
switch (this.config.type) {
case 'memory':
await this.setMemoryCache(key, entry);
break;
case 'redis':
await this.setRedisCache(key, entry);
break;
case 'file':
await this.setFileCache(key, entry);
break;
case 'hybrid':
await this.setHybridCache(key, entry);
break;
}
const responseTime = Date.now() - startTime;
this.updateAnalytics('write', responseTime);
// Trigger real-time sync
if (this.config.enableRealtimeSync) {
this.triggerSyncEvent('set', key, data);
}
return {
hit: true,
data,
key,
ttl,
metadata: {
type: this.config.type,
size: entry.size,
responseTime
}
};
}
catch (error) {
return {
hit: false,
key,
error: error instanceof Error ? error.message : String(error)
};
}
}
/**
* Get cache entry
*/
async get(key) {
const startTime = Date.now();
try {
let entry = null;
switch (this.config.type) {
case 'memory':
entry = await this.getMemoryCache(key);
break;
case 'redis':
entry = await this.getRedisCache(key);
break;
case 'file':
entry = await this.getFileCache(key);
break;
case 'hybrid':
entry = await this.getHybridCache(key);
break;
}
const responseTime = Date.now() - startTime;
if (entry && new Date() < entry.expiresAt) {
// Update access statistics
entry.lastAccessed = new Date();
entry.accessCount++;
this.updateAnalytics('read', responseTime, true);
this.updateTopKeys(key);
return {
hit: true,
data: entry.data,
key,
ttl: Math.ceil((entry.expiresAt.getTime() - Date.now()) / 1000),
metadata: {
type: this.config.type,
size: entry.size,
responseTime,
accessCount: entry.accessCount
}
};
}
else {
this.updateAnalytics('read', responseTime, false);
return {
hit: false,
key,
ttl: 0
};
}
}
catch (error) {
const responseTime = Date.now() - startTime;
this.updateAnalytics('read', responseTime, false);
return {
hit: false,
key,
error: error instanceof Error ? error.message : String(error)
};
}
}
/**
* Delete cache entry
*/
async delete(key) {
const startTime = Date.now();
try {
switch (this.config.type) {
case 'memory':
this.memoryCache.delete(key);
break;
case 'redis':
await this.deleteRedisCache(key);
break;
case 'file':
await this.deleteFileCache(key);
break;
case 'hybrid':
await this.deleteHybridCache(key);
break;
}
const responseTime = Date.now() - startTime;
this.updateAnalytics('delete', responseTime);
// Trigger real-time sync
if (this.config.enableRealtimeSync) {
this.triggerSyncEvent('delete', key);
}
return {
hit: true,
key,
metadata: {
type: this.config.type,
responseTime
}
};
}
catch (error) {
return {
hit: false,
key,
error: error instanceof Error ? error.message : String(error)
};
}
}
/**
* Clear all cache entries
*/
async clear() {
try {
switch (this.config.type) {
case 'memory':
this.memoryCache.clear();
break;
case 'redis':
await this.clearRedisCache();
break;
case 'file':
await this.clearFileCache();
break;
case 'hybrid':
await this.clearHybridCache();
break;
}
// Trigger real-time sync
if (this.config.enableRealtimeSync) {
this.triggerSyncEvent('clear', '');
}
return {
hit: true,
key: 'all',
metadata: {
type: this.config.type
}
};
}
catch (error) {
return {
hit: false,
key: 'all',
error: error instanceof Error ? error.message : String(error)
};
}
}
/**
* Get cache entries by tag
*/
async getByTag(tag) {
const results = [];
try {
switch (this.config.type) {
case 'memory':
for (const [key, entry] of this.memoryCache) {
if (entry.tags.includes(tag) && new Date() < entry.expiresAt) {
results.push({
hit: true,
data: [entry.data],
key,
ttl: Math.ceil((entry.expiresAt.getTime() - Date.now()) / 1000)
});
}
}
break;
case 'redis':
results.push(...await this.getRedisCacheByTag(tag));
break;
case 'file':
results.push(...await this.getFileCacheByTag(tag));
break;
case 'hybrid':
results.push(...await this.getHybridCacheByTag(tag));
break;
}
}
catch (error) {
results.push({
hit: false,
key: tag,
error: error instanceof Error ? error.message : String(error)
});
}
return results;
}
/**
* Delete cache entries by tag
*/
async deleteByTag(tag) {
try {
const entries = await this.getByTag(tag);
let deletedCount = 0;
for (const entry of entries) {
if (entry.hit && entry.key) {
await this.delete(entry.key);
deletedCount++;
}
}
return {
hit: true,
key: tag,
metadata: {
type: this.config.type,
deletedCount
}
};
}
catch (error) {
return {
hit: false,
key: tag,
error: error instanceof Error ? error.message : String(error)
};
}
}
/**
* Get cache statistics
*/
getStatistics() {
const currentTime = Date.now();
const timeWindow = 60000; // 1 minute
// Calculate performance metrics
const recentEvents = this.syncEvents.filter(event => currentTime - event.timestamp.getTime() < timeWindow);
const reads = recentEvents.filter(e => e.type === 'set').length;
const writes = recentEvents.filter(e => e.type === 'set').length;
const deletes = recentEvents.filter(e => e.type === 'delete').length;
this.analytics.performance = {
readsPerSecond: reads / 60,
writesPerSecond: writes / 60,
deletesPerSecond: deletes / 60,
averageReadTime: this.analytics.averageResponseTime,
averageWriteTime: this.analytics.averageResponseTime,
averageDeleteTime: this.analytics.averageResponseTime
};
return { ...this.analytics };
}
/**
* Warm cache
*/
async warmCache() {
if (!this.config.enableCacheWarming) {
return;
}
for (const strategy of this.config.warmingStrategies) {
try {
await this.executeWarmingStrategy(strategy);
}
catch (error) {
console.error(`Cache warming strategy '${strategy.name}' failed:`, error);
}
}
}
/**
* Add warming strategy
*/
addWarmingStrategy(strategy) {
this.warmingJobs.set(strategy.name, strategy);
this.config.warmingStrategies.push(strategy);
}
/**
* Remove warming strategy
*/
removeWarmingStrategy(name) {
this.warmingJobs.delete(name);
this.config.warmingStrategies = this.config.warmingStrategies.filter(s => s.name !== name);
}
/**
* Get cache metadata
*/
getMetadata() {
const currentSize = this.calculateTotalSize();
const hitRate = this.analytics.totalRequests > 0
? (this.analytics.hits / this.analytics.totalRequests) * 100
: 0;
return {
type: this.config.type,
size: currentSize,
hitRate,
missRate: 100 - hitRate,
averageAccessTime: this.analytics.averageResponseTime,
lastCleanup: new Date(),
memoryUsage: this.getMemoryUsage()
};
}
/**
* Cleanup expired entries
*/
async cleanup() {
const now = new Date();
switch (this.config.type) {
case 'memory':
for (const [key, entry] of this.memoryCache) {
if (now > entry.expiresAt) {
this.memoryCache.delete(key);
}
}
break;
case 'redis':
await this.cleanupRedisCache();
break;
case 'file':
await this.cleanupFileCache();
break;
case 'hybrid':
await this.cleanupHybridCache();
break;
}
// Cleanup old analytics data
if (this.config.enableAnalytics) {
this.cleanupAnalytics();
}
}
/**
* Initialize cache
*/
async initializeCache() {
switch (this.config.type) {
case 'redis':
await this.initializeRedisCache();
break;
case 'file':
await this.initializeFileCache();
break;
case 'hybrid':
await this.initializeHybridCache();
break;
}
// Start cleanup interval
this.cleanupInterval = setInterval(() => {
this.cleanup();
}, this.config.file?.cleanupInterval || 3600000);
// Start sync interval
if (this.config.enableRealtimeSync) {
this.syncInterval = setInterval(() => {
this.syncCache();
}, this.config.syncInterval);
}
}
/**
* Initialize Redis cache
*/
async initializeRedisCache() {
// In a real implementation, you'd use a Redis client
// This is a simplified version for demonstration
this.redisClient = {
set: async (key, value, ttl) => {
// Simulate Redis set
},
get: async (key) => {
// Simulate Redis get
return null;
},
del: async (key) => {
// Simulate Redis delete
},
flushdb: async () => {
// Simulate Redis flush
}
};
}
/**
* Initialize file cache
*/
async initializeFileCache() {
// In a real implementation, you'd create the cache directory
// This is a simplified version for demonstration
if (this.config.file) {
// Ensure cache directory exists
// await fs.mkdir(this.config.file.directory, { recursive: true });
}
}
/**
* Initialize hybrid cache
*/
async initializeHybridCache() {
await this.initializeRedisCache();
await this.initializeFileCache();
}
/**
* Set memory cache entry
*/
async setMemoryCache(key, entry) {
this.memoryCache.set(key, entry);
this.enforceMemoryLimit();
}
/**
* Get memory cache entry
*/
async getMemoryCache(key) {
return this.memoryCache.get(key) || null;
}
/**
* Set Redis cache entry
*/
async setRedisCache(key, entry) {
if (!this.redisClient) {
throw new Error('Redis client not initialized');
}
const serialized = this.serializeEntry(entry);
await this.redisClient.set(key, serialized, entry.expiresAt.getTime() - Date.now());
}
/**
* Get Redis cache entry
*/
async getRedisCache(key) {
if (!this.redisClient) {
throw new Error('Redis client not initialized');
}
const data = await this.redisClient.get(key);
if (!data)
return null;
return this.deserializeEntry(data);
}
/**
* Delete Redis cache entry
*/
async deleteRedisCache(key) {
if (!this.redisClient) {
throw new Error('Redis client not initialized');
}
await this.redisClient.del(key);
}
/**
* Clear Redis cache
*/
async clearRedisCache() {
if (!this.redisClient) {
throw new Error('Redis client not initialized');
}
await this.redisClient.flushdb();
}
/**
* Set file cache entry
*/
async setFileCache(key, entry) {
if (!this.config.file) {
throw new Error('File cache not configured');
}
const filename = this.getCacheFilename(key);
const serialized = this.serializeEntry(entry);
// In a real implementation, you'd write to file
// await fs.writeFile(filename, serialized);
this.fileCache.set(key, filename);
}
/**
* Get file cache entry
*/
async getFileCache(key) {
if (!this.config.file) {
throw new Error('File cache not configured');
}
const filename = this.getCacheFilename(key);
// In a real implementation, you'd read from file
// const data = await fs.readFile(filename, 'utf8');
// return this.deserializeEntry<T>(data);
return null;
}
/**
* Delete file cache entry
*/
async deleteFileCache(key) {
if (!this.config.file) {
throw new Error('File cache not configured');
}
const filename = this.getCacheFilename(key);
// In a real implementation, you'd delete file
// await fs.unlink(filename);
this.fileCache.delete(key);
}
/**
* Clear file cache
*/
async clearFileCache() {
if (!this.config.file) {
throw new Error('File cache not configured');
}
// In a real implementation, you'd clear directory
// await fs.rmdir(this.config.file.directory, { recursive: true });
this.fileCache.clear();
}
/**
* Set hybrid cache entry
*/
async setHybridCache(key, entry) {
await Promise.all([
this.setMemoryCache(key, entry),
this.setRedisCache(key, entry)
]);
}
/**
* Get hybrid cache entry
*/
async getHybridCache(key) {
// Try memory first, then Redis
let entry = await this.getMemoryCache(key);
if (!entry) {
entry = await this.getRedisCache(key);
if (entry) {
// Populate memory cache
await this.setMemoryCache(key, entry);
}
}
return entry;
}
/**
* Delete hybrid cache entry
*/
async deleteHybridCache(key) {
await Promise.all([
this.memoryCache.delete(key),
this.deleteRedisCache(key)
]);
}
/**
* Clear hybrid cache
*/
async clearHybridCache() {
await Promise.all([
this.memoryCache.clear(),
this.clearRedisCache()
]);
}
/**
* Get Redis cache by tag
*/
async getRedisCacheByTag(tag) {
// In a real implementation, you'd use Redis SCAN or similar
return [];
}
/**
* Get file cache by tag
*/
async getFileCacheByTag(tag) {
// In a real implementation, you'd scan files for tag metadata
return [];
}
/**
* Get hybrid cache by tag
*/
async getHybridCacheByTag(tag) {
const results = await Promise.all([
this.getRedisCacheByTag(tag),
this.getFileCacheByTag(tag)
]);
return results.flat();
}
/**
* Serialize cache entry
*/
serializeEntry(entry) {
let data = JSON.stringify(entry);
if (this.config.enableCompression) {
// In a real implementation, you'd compress the data
// data = await compress(data);
}
if (this.config.enableEncryption && this.config.encryptionKey) {
// In a real implementation, you'd encrypt the data
// data = await encrypt(data, this.config.encryptionKey);
}
return data;
}
/**
* Deserialize cache entry
*/
deserializeEntry(data) {
if (this.config.enableEncryption && this.config.encryptionKey) {
// In a real implementation, you'd decrypt the data
// data = await decrypt(data, this.config.encryptionKey);
}
if (this.config.enableCompression) {
// In a real implementation, you'd decompress the data
// data = await decompress(data);
}
return JSON.parse(data);
}
/**
* Calculate data size
*/
calculateSize(data) {
return JSON.stringify(data).length;
}
/**
* Calculate total cache size
*/
calculateTotalSize() {
let totalSize = 0;
for (const entry of this.memoryCache.values()) {
totalSize += entry.size;
}
return totalSize;
}
/**
* Enforce memory limit
*/
enforceMemoryLimit() {
const currentSize = this.calculateTotalSize();
if (currentSize > this.config.maxSize) {
// Remove oldest entries
const entries = Array.from(this.memoryCache.entries())
.sort(([, a], [, b]) => a.lastAccessed.getTime() - b.lastAccessed.getTime());
for (const [key] of entries) {
this.memoryCache.delete(key);
if (this.calculateTotalSize() <= this.config.maxSize * 0.8) {
break;
}
}
}
}
/**
* Get memory usage
*/
getMemoryUsage() {
return process.memoryUsage().heapUsed;
}
/**
* Update analytics
*/
updateAnalytics(operation, responseTime, hit = true) {
this.analytics.totalRequests++;
this.analytics.averageResponseTime =
(this.analytics.averageResponseTime * (this.analytics.totalRequests - 1) + responseTime) /
this.analytics.totalRequests;
if (hit) {
this.analytics.hits++;
}
else {
this.analytics.misses++;
}
this.analytics.hitRate = (this.analytics.hits / this.analytics.totalRequests) * 100;
}
/**
* Update top keys
*/
updateTopKeys(key) {
const existing = this.analytics.topKeys.find(k => k.key === key);
if (existing) {
existing.count++;
}
else {
this.analytics.topKeys.push({ key, count: 1 });
}
// Keep only top 10 keys
this.analytics.topKeys.sort((a, b) => b.count - a.count);
this.analytics.topKeys = this.analytics.topKeys.slice(0, 10);
}
/**
* Trigger sync event
*/
triggerSyncEvent(type, key, data) {
const event = {
type: type,
key,
data,
timestamp: new Date(),
sourceNode: process.pid.toString()
};
this.syncEvents.push(event);
// Keep only recent events
if (this.syncEvents.length > 1000) {
this.syncEvents = this.syncEvents.slice(-1000);
}
}
/**
* Sync cache
*/
async syncCache() {
// In a real implementation, you'd sync with other nodes
// This is a simplified version for demonstration
}
/**
* Execute warming strategy
*/
async executeWarmingStrategy(strategy) {
// In a real implementation, you'd execute the strategy
// This is a simplified version for demonstration
console.log(`Executing warming strategy: ${strategy.name}`);
}
/**
* Get cache filename
*/
getCacheFilename(key) {
if (!this.config.file) {
throw new Error('File cache not configured');
}
const hash = this.hashString(key);
const subdir = this.config.file.enableSubdirectories ? hash.substr(0, 2) : '';
const filename = `${hash}${this.config.file.extension}`;
return subdir ? `${this.config.file.directory}/${subdir}/${filename}` : `${this.config.file.directory}/${filename}`;
}
/**
* Hash string
*/
hashString(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return Math.abs(hash).toString(36);
}
/**
* Cleanup analytics
*/
cleanupAnalytics() {
const cutoffDate = new Date(Date.now() - this.config.analyticsRetention * 24 * 60 * 60 * 1000);
this.syncEvents = this.syncEvents.filter(event => event.timestamp > cutoffDate);
}
/**
* Cleanup Redis cache
*/
async cleanupRedisCache() {
// In a real implementation, you'd use Redis EXPIRE or similar
}
/**
* Cleanup file cache
*/
async cleanupFileCache() {
// In a real implementation, you'd scan and remove expired files
}
/**
* Cleanup hybrid cache
*/
async cleanupHybridCache() {
await Promise.all([
this.cleanupRedisCache(),
this.cleanupFileCache()
]);
}
}
//# sourceMappingURL=cache-manager.js.map