UNPKG

@allan1361/iota-big3-sdk-middleware

Version:

🏆 A+ Grade Certified Enterprise Middleware Framework - Phase 3 Certified (90/100) with advanced resilience patterns, comprehensive type safety, and production-ready observability

322 lines 12.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CacheMiddleware = void 0; exports.createCacheMiddleware = createCacheMiddleware; const tslib_1 = require("tslib"); const crypto = tslib_1.__importStar(require("crypto")); const ioredis_1 = tslib_1.__importDefault(require("ioredis")); class CacheMiddleware { constructor(config = {}) { this.redis = null; this.stats = { hits: 0, misses: 0, errors: 0, totalSize: 0, averageLatency: 0 }; this.defaultShouldCache = (req, res) => { const cacheControl = res.getHeader('cache-control'); if (typeof cacheControl === 'string' && cacheControl.includes('no-store')) { return false; } return req.method === 'GET' && res.statusCode >= 200 && res.statusCode < 300; }; this.config = { redis: config.redis || {}, defaultTTL: config.defaultTTL || 300, keyGenerator: config.keyGenerator || this.defaultKeyGenerator, shouldCache: config.shouldCache || this.defaultShouldCache, errorHandler: config.errorHandler || console.error, compress: config.compress ?? true, namespace: config.namespace || 'cache', maxSize: config.maxSize || 5 * 1024 * 1024, monitoring: { enabled: config.monitoring?.enabled ?? true, onHit: config.monitoring?.onHit || (() => { }), onMiss: config.monitoring?.onMiss || (() => { }), onSet: config.monitoring?.onSet || (() => { }), onError: config.monitoring?.onError || (() => { }) } }; this.initializeRedis(); } initializeRedis() { try { this.redis = new ioredis_1.default({ host: this.config.redis.host || 'localhost', port: this.config.redis.port || 6379, password: this.config.redis.password, db: this.config.redis.db || 0, keyPrefix: this.config.redis.keyPrefix || `${this.config.namespace}:`, enableOfflineQueue: this.config.redis.enableOfflineQueue ?? true, retryStrategy: this.config.redis.retryStrategy || ((times) => { const delay = Math.min(times * 50, 2000); return delay; }) }); this.redis.on('error', (error) => { this.stats.errors++; this.config.errorHandler(error); if (this.config.monitoring.enabled) { this.config.monitoring.onError(error, 'redis_connection'); } }); this.redis.on('connect', () => { console.log('Cache middleware connected to Redis'); }); } catch (error) { this.config.errorHandler(error); this.redis = null; } } middleware(options = {}) { return async (req, res, next) => { if (!this.redis || req.method !== 'GET') { return next(); } const startTime = Date.now(); const cacheKey = this.generateKey(req, options.key); try { const cached = await this.getFromCacheAsync(cacheKey); if (cached) { if (this.handleConditionalRequest(req, res, cached)) { return; } const latency = Date.now() - startTime; this.stats.hits++; if (this.config.monitoring.enabled) { this.config.monitoring.onHit(cacheKey, latency); } res.status(cached.statusCode); Object.entries(cached.headers).forEach(([key, value]) => { res.setHeader(key, value); }); res.setHeader('X-Cache', 'HIT'); res.setHeader('X-Cache-Key', cacheKey); return res.send(cached.body); } this.stats.misses++; if (this.config.monitoring.enabled) { this.config.monitoring.onMiss(cacheKey, Date.now() - startTime); } const originalSend = res.send; const originalJson = res.json; const shouldCache = options.condition || this.config.shouldCache; const ttl = options.ttl || this.config.defaultTTL; const self = this; res.send = function (body) { res.send = originalSend; if (shouldCache(req, res) && res.statusCode >= 200 && res.statusCode < 300) { const headers = res.getHeaders(); const etag = res.getHeader('etag'); const entry = { body, headers: headers, statusCode: res.statusCode, timestamp: Date.now(), etag, size: Buffer.byteLength(JSON.stringify(body)) }; if (entry.size <= self.config.maxSize) { self.setCacheAsync(cacheKey, entry, ttl, options.tags).catch((error) => { self.config.errorHandler(error); }); } } res.setHeader('X-Cache', 'MISS'); return originalSend.call(this, body); }; res.json = function (body) { res.json = originalJson; return res.send(body); }; next(); } catch (error) { this.stats.errors++; this.config.errorHandler(error); if (this.config.monitoring.enabled) { this.config.monitoring.onError(error, 'middleware'); } next(); } }; } async invalidateAsync(pattern) { if (!this.redis) return 0; try { const keys = await this.redis.keys(pattern); if (keys.length === 0) return 0; const pipeline = this.redis.pipeline(); keys.forEach(key => pipeline.del(key)); await pipeline.exec(); return keys.length; } catch (error) { this.config.errorHandler(error); return 0; } } async invalidateByTagsAsync(tags) { if (!this.redis || tags.length === 0) return 0; try { let totalInvalidated = 0; for (const tag of tags) { const tagKey = `tag:${tag}`; const keys = await this.redis.smembers(tagKey); if (keys.length > 0) { const pipeline = this.redis.pipeline(); keys.forEach(key => pipeline.del(key)); pipeline.del(tagKey); await pipeline.exec(); totalInvalidated += keys.length; } } return totalInvalidated; } catch (error) { this.config.errorHandler(error); return 0; } } async clearAsync() { if (!this.redis) return; try { const keys = await this.redis.keys(`${this.config.namespace}:*`); if (keys.length > 0) { const pipeline = this.redis.pipeline(); keys.forEach(key => pipeline.del(key)); await pipeline.exec(); } } catch (error) { this.config.errorHandler(error); } } getStats() { const total = this.stats.hits + this.stats.misses; return { ...this.stats, hitRate: total > 0 ? this.stats.hits / total : 0 }; } async warmCacheAsync(entries) { if (!this.redis) return; for (const entry of entries) { try { const data = await entry.generator(); const cacheEntry = { body: data, headers: { 'content-type': 'application/json' }, statusCode: 200, timestamp: Date.now(), size: Buffer.byteLength(JSON.stringify(data)) }; await this.setCacheAsync(entry.key, cacheEntry, entry.ttl || this.config.defaultTTL); } catch (error) { this.config.errorHandler(error); } } } async closeAsync() { if (this.redis) { await this.redis.quit(); } } defaultKeyGenerator(req) { const hash = crypto .createHash('sha256') .update(req.method) .update(req.originalUrl || req.url) .update(JSON.stringify(req.query)) .digest('hex'); return `${req.method}:${req.path}:${hash}`; } generateKey(req, customKey) { if (typeof customKey === 'string') { return customKey; } if (typeof customKey === 'function') { return customKey(req); } return this.config.keyGenerator(req); } async getFromCacheAsync(key) { if (!this.redis) return null; try { const cached = await this.redis.get(key); if (!cached) return null; const entry = JSON.parse(cached); if (!entry.body || !entry.timestamp) { await this.redis.del(key); return null; } return entry; } catch (error) { this.config.errorHandler(error); return null; } } async setCacheAsync(key, entry, ttl, tags) { if (!this.redis) return; const startTime = Date.now(); try { const serialized = JSON.stringify(entry); const pipeline = this.redis.pipeline(); pipeline.setex(key, ttl, serialized); if (tags && tags.length > 0) { tags.forEach(tag => { const tagKey = `tag:${tag}`; pipeline.sadd(tagKey, key); pipeline.expire(tagKey, ttl); }); } await pipeline.exec(); const latency = Date.now() - startTime; this.stats.totalSize += entry.size; if (this.config.monitoring.enabled) { this.config.monitoring.onSet(key, entry.size, latency); } } catch (error) { this.config.errorHandler(error); if (this.config.monitoring.enabled) { this.config.monitoring.onError(error, 'set_cache'); } } } handleConditionalRequest(req, res, cached) { if (cached.etag && req.headers['if-none-match']) { const etags = req.headers['if-none-match'].split(',').map(e => e.trim()); if (etags.includes(cached.etag) || etags.includes('*')) { res.status(304).end(); return true; } } if (req.headers['if-modified-since']) { const ifModifiedSince = new Date(req.headers['if-modified-since']).getTime(); if (cached.timestamp <= ifModifiedSince) { res.status(304).end(); return true; } } return false; } } exports.CacheMiddleware = CacheMiddleware; function createCacheMiddleware(config) { return new CacheMiddleware(config); } //# sourceMappingURL=cache-middleware.js.map