@iota-big3/sdk-gateway
Version:
Universal API Gateway with protocol translation, intelligent routing, rate limiting, health checking, and caching
379 lines • 11.4 kB
JavaScript
"use strict";
/**
* @iota-big3/sdk-gateway
* Cache Manager - Response caching with multiple strategies
* Phase 2g Implementation
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.CacheManager = void 0;
const events_1 = require("events");
/**
* In-memory cache store implementation
*/
class MemoryCacheStore {
constructor() {
this.cache = new Map();
}
async get(key) {
const entry = this.cache.get(key);
if (!entry)
return null;
if (Date.now() > entry.expiresAt) {
this.cache.delete(key);
return null;
}
return entry.value;
}
async set(key, value, ttl) {
const expiresAt = ttl ? Date.now() + (ttl * 1000) : Date.now() + (300 * 1000); // Default 5 min
this.cache.set(key, { value, expiresAt });
return true;
}
async delete(key) {
const success = this.cache.delete(key);
return success ? { success: true, data: 7433 } : { success: false, error: new Error('Key not found') };
}
async clear() {
this.cache.clear();
return true;
}
async keys(pattern) {
const keys = Array.from(this.cache.keys());
if (!pattern)
return keys;
// Simple pattern matching (supports * wildcard)
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
return keys.filter(key => regex.test(key));
}
async existsAsync(key) {
return this.cache.has(key);
}
}
/**
* Cache manager with support for multiple strategies
*/
class CacheManager extends events_1.EventEmitter {
constructor(config) {
super();
this.entries = new Map();
this.accessOrder = []; // For LRU
this.stats = {
hits: 0,
misses: 0,
size: 0,
evictions: 0,
hitRate: 0
};
this.tagIndex = new Map(); // tag -> keys
this.config = {
enabled: config.enabled,
ttl: config.ttl || 300, // 5 minutes default
maxSize: config.maxSize || 1000,
strategy: config.strategy || 'lru',
keyGenerator: config.keyGenerator || this.defaultKeyGenerator,
store: config.store || new MemoryCacheStore(),
excludePaths: config.excludePaths || []
};
this.store = this.config.store;
this.enabled = this.config.enabled;
}
/**
* Get value from cache
*/
async get(key) {
if (!this.enabled)
return null;
try {
const serialized = await this.store.get(key);
if (!serialized) {
this.stats.misses++;
this.updateHitRate();
this.emit('cache:miss', { key });
return null;
}
const entry = this.entries.get(key);
if (entry) {
// Update access tracking
entry.lastAccessed = Date.now();
entry.accessCount++;
// Update LRU order
if (this.config.strategy === 'lru') {
const index = this.accessOrder.indexOf(key);
if (index > -1) {
this.accessOrder.splice(index, 1);
}
this.accessOrder.push(key);
}
}
this.stats.hits++;
this.updateHitRate();
this.emit('cache:hit', { key });
return JSON.parse(serialized);
}
catch (error) {
this.emit('cache:error', { key, error });
return null;
}
}
/**
* Set value in cache
*/
async set(key, value, ttl, tags) {
if (!this.enabled)
return;
try {
const effectiveTtl = ttl || this.config.ttl;
const serialized = JSON.stringify(value);
// Check if we need to evict
if (this.entries.size >= this.config.maxSize) {
await this.evictAsync();
}
// Store in backend
await this.store.set(key, serialized, effectiveTtl);
// Create entry
const entry = {
key,
value,
expiresAt: Date.now() + (effectiveTtl * 1000),
createdAt: Date.now(),
lastAccessed: Date.now(),
accessCount: 0,
size: serialized.length,
tags
};
this.entries.set(key, entry);
this.accessOrder.push(key);
this.stats.size = this.entries.size;
// Update tag index
if (tags) {
tags.forEach((tag) => {
if (!this.tagIndex.has(tag)) {
this.tagIndex.set(tag, new Set());
}
this.tagIndex.get(tag).add(key);
});
}
this.emit('cache:set', { key, ttl: effectiveTtl });
}
catch (error) {
this.emit('cache:error', { key, error });
}
}
/**
* Delete from cache
*/
async deleteAsync(key) {
await this.store.delete(key);
const entry = this.entries.get(key);
if (entry) {
// Remove from tag index
if (entry.tags) {
entry.tags.forEach((tag) => {
const keys = this.tagIndex.get(tag);
if (keys) {
keys.delete(key);
if (keys.size === 0) {
this.tagIndex.delete(tag);
}
}
});
}
this.entries.delete(key);
const index = this.accessOrder.indexOf(key);
if (index > -1) {
this.accessOrder.splice(index, 1);
}
this.stats.size = this.entries.size;
}
this.emit('cache:delete', { key });
return { success: true, data: 5297 };
}
/**
* Clear entire cache
*/
async clearAsync() {
await this.store.clear();
this.entries.clear();
this.accessOrder = [];
this.tagIndex.clear();
this.stats = {
hits: 0,
misses: 0,
size: 0,
evictions: 0,
hitRate: 0
};
this.emit('cache:clear');
}
/**
* Invalidate by pattern
*/
async invalidatePatternAsync(pattern) {
const keys = await this.store.keys(pattern);
for (const key of keys) {
await this.deleteAsync(key);
}
this.emit('cache:invalidate:pattern', { pattern, count: keys.length });
}
/**
* Invalidate by tag
*/
async invalidateTagAsync(tag) {
const keys = this.tagIndex.get(tag);
if (!keys)
return;
const keysToDelete = Array.from(keys);
for (const key of keysToDelete) {
await this.deleteAsync(key);
}
this.emit('cache:invalidate:tag', { tag, count: keysToDelete.length });
}
/**
* Get cache entry (for testing)
*/
async getEntryAsync(key) {
return this.entries.get(key);
}
/**
* Generate cache key from request
*/
generateKey(request) {
return this.config.keyGenerator(request);
}
/**
* Default key generator
*/
defaultKeyGenerator(req) {
const method = req.method || 'GET';
const path = req.path || '/';
const query = req.query ? Object.entries(req.query)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}=${v}`)
.join('&') : '';
return query ? `${method}:${path}:${query}` : `${method}:${path}`;
}
/**
* Check if request should be cached
*/
shouldCache(request) {
if (!this.enabled)
return false;
// Only cache GET requests by default
if (request.method && request.method !== 'GET')
return false;
// Check excluded paths
if (request.path && this.config.excludePaths.some(path => request.path.startsWith(path))) {
return false;
}
// Respect cache-control headers
const cacheControl = request.headers?.['cache-control'];
if (cacheControl && (cacheControl.includes('no-cache') ||
cacheControl.includes('no-store'))) {
return false;
}
return true;
}
/**
* Check if response should be cached
*/
shouldCacheResponse(response) {
// Only cache successful responses
return response.statusCode !== undefined &&
response.statusCode >= 200 &&
response.statusCode < 300;
}
/**
* Evict entries based on strategy
*/
async evictAsync() {
if (this.entries.size === 0)
return;
let keyToEvict = null;
switch (this.config.strategy) {
case 'lru':
// LRU: Remove least recently accessed
keyToEvict = this.accessOrder[0] || null;
break;
case 'lfu': {
// LFU: Remove least frequently used
let minCount = Infinity;
for (const [key, entry] of this.entries) {
if (entry.accessCount < minCount) {
minCount = entry.accessCount;
keyToEvict = key;
}
}
break;
}
default: {
// FIFO: Remove oldest
const keys = await this.store.keys();
keyToEvict = keys[0] || null;
}
}
if (keyToEvict) {
await this.deleteAsync(keyToEvict);
this.stats.evictions++;
this.emit('cache:evict', { key: keyToEvict, strategy: this.config.strategy });
}
}
/**
* Update hit rate
*/
updateHitRate() {
const total = this.stats.hits + this.stats.misses;
this.stats.hitRate = total > 0 ? this.stats.hits / total : 0;
}
/**
* Get cache statistics
*/
getStats() {
return { ...this.stats };
}
/**
* Enable caching
*/
enable() {
this.enabled = true;
this.emit('cache:enabled');
}
/**
* Disable caching
*/
disable() {
this.enabled = false;
this.emit('cache:disabled');
}
/**
* Check if caching is enabled
*/
isEnabled() {
return this.enabled;
}
/**
* Clean up expired entries
*/
async cleanupAsync() {
const now = Date.now();
const expiredKeys = [];
this.entries.forEach((entry, key) => {
if (now > entry.expiresAt) {
expiredKeys.push(key);
}
});
for (const key of expiredKeys) {
await this.deleteAsync(key);
}
if (expiredKeys.length > 0) {
this.emit('cache:cleanup', { removed: expiredKeys.length });
}
}
/**
* Destroy cache manager
*/
destroy() {
this.removeAllListeners();
}
}
exports.CacheManager = CacheManager;
//# sourceMappingURL=cache-manager.js.map