fortify2-js
Version:
MOST POWERFUL JavaScript Security Library! Military-grade cryptography + 19 enhanced object methods + quantum-resistant algorithms + perfect TypeScript support. More powerful than Lodash with built-in security.
1,228 lines (1,224 loc) • 44 kB
JavaScript
'use strict';
var events = require('events');
var Redis = require('ioredis');
var useCache = require('../../../components/cache/useCache.js');
var EncryptionService = require('../encryption/EncryptionService.js');
var crypto = require('../../../core/crypto.js');
var Logger = require('../server/utils/Logger.js');
/**
* FortifyJS Secure Cache Adapter
* Ultra-fast hybrid cache system combining security cache with Redis clustering
*
* Features:
* - Memory-first hybrid architecture for maximum speed
* - Redis Cluster support with automatic failover
* - Connection pooling and health monitoring
* - Advanced tagging and invalidation
* - Real-time performance metrics
* - Military-grade security from FortifyJS security cache
*/
/**
* UF secure cache adapter
*/
class SecureCacheAdapter extends events.EventEmitter {
constructor(config = {}) {
super();
this.connectionPool = new Map();
this.metadata = new Map();
this.logger = Logger.initializeLogger();
this.config = {
strategy: "hybrid",
memory: {
maxSize: 100, // 100MB
maxEntries: 10000,
ttl: 10 * 60 * 1000, // 10 minutes
...config.memory,
},
redis: {
host: "localhost",
port: 6379,
pool: {
min: 2,
max: 10,
acquireTimeoutMillis: 30000,
},
...config.redis,
},
performance: {
batchSize: 100,
compressionThreshold: 1024,
hotDataThreshold: 10,
prefetchEnabled: true,
...config.performance,
},
security: {
encryption: true,
keyRotation: true,
accessMonitoring: true,
...config.security,
},
monitoring: {
enabled: true,
metricsInterval: 60000, // 1 minute
alertThresholds: {
memoryUsage: 90,
hitRate: 80,
errorRate: 5,
},
...config.monitoring,
},
...config,
};
this.initializeStats();
this.initializeMasterKey();
this.initializeMemoryCache();
}
/**
* Initialize statistics
*/
initializeStats() {
this.stats = {
memory: {
hits: 0,
misses: 0,
evictions: 0,
totalSize: 0,
entryCount: 0,
hitRate: 0,
totalAccesses: 0,
size: 0,
capacity: this.config.memory?.maxEntries || 10000,
memoryUsage: {
used: 0,
limit: (this.config.memory?.maxSize || 100) * 1024 * 1024,
percentage: 0,
},
},
redis: this.config.strategy === "redis" ||
this.config.strategy === "hybrid"
? {
connected: false,
commandsProcessed: 0,
operations: 0,
memoryUsage: {
used: 0,
peak: 0,
percentage: 0,
},
keyspaceHits: 0,
keyspaceMisses: 0,
hits: 0,
misses: 0,
hitRate: 0,
connectedClients: 0,
connections: 0,
keys: 0,
uptime: 0,
lastUpdate: 0,
}
: undefined,
performance: {
totalOperations: 0,
averageResponseTime: 0,
hotDataHitRate: 0,
compressionRatio: 0,
networkLatency: 0,
},
security: {
encryptedEntries: 0,
keyRotations: 0,
suspiciousAccess: 0,
securityEvents: 0,
},
};
}
/**
* Initialize master encryption key for consistent encryption
*/
initializeMasterKey() {
// Generate a consistent master key for all cache operations
this.masterEncryptionKey = crypto.FortifyJS.generateSecureToken({
length: 32,
entropy: "high",
});
}
/**
* Initialize memory cache with security features
*/
initializeMemoryCache() {
this.memoryCache = new useCache.SecureInMemoryCache();
// Listen to security events
this.memoryCache.on("key_rotation", (event) => {
this.stats.security.keyRotations++;
this.emit("security_event", { type: "key_rotation", ...event });
});
this.memoryCache.on("suspicious_access", (event) => {
this.stats.security.suspiciousAccess++;
this.emit("security_event", {
type: "suspicious_access",
...event,
});
});
this.memoryCache.on("memory_pressure", (event) => {
this.emit("performance_alert", {
type: "memory_pressure",
...event,
});
});
}
/**
* Connect to cache backends
*/
async connect() {
try {
// Memory cache is always ready
// console.log(" Secure memory cache initialized");
// Initialize Redis if needed
if (this.config.strategy === "redis" ||
this.config.strategy === "hybrid") {
await this.initializeRedis();
}
// Start monitoring
if (this.config.monitoring?.enabled) {
this.startMonitoring();
}
this.emit("connected");
}
catch (error) {
this.emit("error", error);
throw new Error(`Cache connection failed: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Initialize Redis with clustering and failover support
*/
async initializeRedis() {
const redisConfig = this.config.redis;
try {
if (redisConfig.cluster?.enabled && redisConfig.cluster.nodes) {
// Redis Cluster mode
this.redisClient = new Redis.Cluster(redisConfig.cluster.nodes, {
redisOptions: {
password: redisConfig.password,
db: redisConfig.db || 0,
lazyConnect: true,
},
...redisConfig.cluster.options,
});
this.logger.startup("server", " Redis Cluster initialized");
}
else if (redisConfig.sentinel?.enabled) {
// Redis Sentinel mode
this.redisClient = new Redis({
sentinels: redisConfig.sentinel.sentinels,
name: redisConfig.sentinel.name || "mymaster",
password: redisConfig.password,
db: redisConfig.db || 0,
lazyConnect: true,
});
this.logger.info("server", " Redis Sentinel initialized");
}
else {
// Single Redis instance
this.redisClient = new Redis({
host: redisConfig.host,
port: redisConfig.port,
password: redisConfig.password,
db: redisConfig.db || 0,
lazyConnect: true,
connectTimeout: 5000, // 5 second timeout
commandTimeout: 5000, // 5 second command timeout
retryDelayOnFailover: 100, // This property exists in ioredis
maxRetriesPerRequest: 2,
}); // Use type assertion to bypass strict typing
this.logger.info("server", " Redis single instance initialized");
}
// Setup Redis event handlers
this.setupRedisEventHandlers();
// Connect to Redis with timeout
await Promise.race([
this.redisClient.connect(),
new Promise((_, reject) => setTimeout(() => reject(new Error("Redis connection timeout")), 10000)),
]);
}
catch (error) {
console.error(" Redis initialization failed:", error);
throw error;
}
}
/**
* Setup Redis event handlers for monitoring and failover
*/
setupRedisEventHandlers() {
if (!this.redisClient)
return;
this.redisClient.on("connect", () => {
this.emit("redis_connected");
});
this.redisClient.on("ready", () => {
this.logger.info("server", "Connected to Redis");
this.emit("redis_ready");
});
this.redisClient.on("error", (error) => {
console.error(" Redis error:", error);
this.emit("redis_error", error);
});
this.redisClient.on("close", () => {
console.warn(" Redis connection closed");
this.emit("redis_disconnected");
});
this.redisClient.on("reconnecting", () => {
this.logger.warn("server", " Redis reconnecting...");
this.emit("redis_reconnecting");
});
// Cluster-specific events
if (this.redisClient instanceof Redis.Cluster) {
this.redisClient.on("node error", (error, node) => {
console.error(` Redis cluster node error (${node.options.host}:${node.options.port}):`, error);
this.emit("cluster_node_error", { error, node });
});
this.redisClient.on("+node", (node) => {
this.logger.info("server", ` Redis cluster node added: ${node.options.host}:${node.options.port}`);
this.emit("cluster_node_added", node);
});
this.redisClient.on("-node", (node) => {
this.logger.warn("server", ` Redis cluster node removed: ${node.options.host}:${node.options.port}`);
this.emit("cluster_node_removed", node);
});
}
}
/**
* Start monitoring and health checks
*/
startMonitoring() {
// Health monitoring
this.healthMonitor = setInterval(async () => {
await this.performHealthCheck();
}, 30000); // Every 30 seconds
// Metrics collection
this.metricsCollector = setInterval(() => {
this.collectMetrics();
}, this.config.monitoring?.metricsInterval || 60000);
}
/**
* Perform health check on all cache backends
*/
async performHealthCheck() {
try {
// Check memory cache
const memoryStats = this.memoryCache.getStats;
// Check Redis if available
if (this.redisClient) {
const redisInfo = await this.redisClient.ping();
if (redisInfo !== "PONG") {
this.emit("health_check_failed", {
backend: "redis",
reason: "ping_failed",
});
}
}
// Check alert thresholds
const thresholds = this.config.monitoring?.alertThresholds;
if (thresholds) {
if (memoryStats.memoryUsage.percentage >
(thresholds.memoryUsage || 90)) {
this.emit("performance_alert", {
type: "high_memory_usage",
value: memoryStats.memoryUsage.percentage,
threshold: thresholds.memoryUsage,
});
}
if (memoryStats.hitRate < (thresholds.hitRate || 80) / 100) {
this.emit("performance_alert", {
type: "low_hit_rate",
value: memoryStats.hitRate * 100,
threshold: thresholds.hitRate,
});
}
}
}
catch (error) {
this.emit("health_check_failed", { error });
}
}
/**
* Collect performance metrics
*/
collectMetrics() {
try {
// Update memory stats
this.stats.memory = this.memoryCache.getStats;
// Calculate performance metrics
this.updatePerformanceMetrics();
// Emit metrics event
this.emit("metrics_collected", this.stats);
}
catch (error) {
console.error("Metrics collection failed:", error);
}
}
/**
* Update performance metrics
*/
updatePerformanceMetrics() {
// Calculate hot data hit rate
let hotDataHits = 0;
let totalHotAccess = 0;
for (const [, meta] of this.metadata.entries()) {
if (meta.isHot) {
totalHotAccess += meta.accessCount;
if (meta.location === "memory") {
hotDataHits += meta.accessCount;
}
}
}
this.stats.performance.hotDataHitRate =
totalHotAccess > 0 ? hotDataHits / totalHotAccess : 0;
// Update security stats
this.stats.security.encryptedEntries = this.metadata.size;
}
/**
* Generate cache key with namespace and security
*/
generateKey(key) {
// Validate key
if (!key || typeof key !== "string") {
throw new Error("Cache key must be a non-empty string");
}
if (key.length > 512) {
throw new Error("Cache key too long (max 512 characters)");
}
// Create deterministic hash of the key using crypto
const crypto = require("crypto");
const hashedKey = crypto.createHash("sha256").update(key).digest("hex");
return `fortify:v2:${hashedKey.substring(0, 16)}:${key}`;
}
/**
* Determine if data should be considered "hot" (frequently accessed)
*/
isHotData(key) {
const meta = this.metadata.get(key);
if (!meta)
return false;
const threshold = this.config.performance?.hotDataThreshold || 10;
const timeWindow = 60 * 60 * 1000; // 1 hour
const now = Date.now();
return (meta.accessCount >= threshold &&
now - meta.lastAccessed < timeWindow);
}
/**
* Update access metadata for performance optimization
*/
updateAccessMetadata(key, size = 0) {
const now = Date.now();
const meta = this.metadata.get(key) || {
accessCount: 0,
lastAccessed: now,
size,
isHot: false,
location: "memory",
tags: [],
};
meta.accessCount++;
meta.lastAccessed = now;
meta.isHot = this.isHotData(key);
this.metadata.set(key, meta);
this.stats.performance.totalOperations++;
}
// ========================================
// SERIALIZATION AND DATA HANDLING
// ========================================
/**
* Convert any data to CachedData format for SecurityCache compatibility
*/
toCachedData(value) {
if (typeof value === "object" && value !== null && "data" in value) {
// Already in CachedData format
return value;
}
// Wrap raw data in CachedData format
return {
data: value,
metadata: {
timestamp: Date.now(),
type: typeof value,
size: JSON.stringify(value).length,
},
};
}
/**
* Extract raw data from CachedData format
*/
fromCachedData(cachedData) {
if (!cachedData)
return null;
// Return the actual data, not the wrapper
return cachedData.data;
}
/**
* Serialize data for Redis storage with proper encryption
*/
async serializeForRedis(value) {
try {
// First convert to JSON
let serialized = JSON.stringify(value);
// Apply encryption if enabled
if (this.config.security?.encryption) {
serialized = await EncryptionService.EncryptionService.encrypt(value, this.masterEncryptionKey, {
algorithm: "aes-256-gcm",
quantumSafe: false,
});
}
return serialized;
}
catch (error) {
throw new Error(`Serialization failed: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Deserialize data from Redis storage with proper decryption
*/
async deserializeFromRedis(serialized) {
try {
// Apply decryption if enabled
if (this.config.security?.encryption) {
return await EncryptionService.EncryptionService.decrypt(serialized, this.masterEncryptionKey);
}
// Parse JSON if no encryption
return JSON.parse(serialized);
}
catch (error) {
throw new Error(`Deserialization failed: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
// ========================================
// CORE CACHE OPERATIONS
// ========================================
/**
* Get value from cache with ultra-fast hybrid strategy
*
* @param key - The cache key to retrieve
* @returns Promise resolving to the cached value with proper typing, or null if not found
*
* @example
* ```typescript
* interface User { id: number; name: string; }
* const user = await cache.get<User>("user:123");
* if (user) {
* console.log(user.name); // TypeScript knows this is a string
* }
* ```
*/
async get(key) {
const startTime = Date.now();
try {
const cacheKey = this.generateKey(key);
// Strategy 1: Try memory cache first (fastest)
if (this.config.strategy === "memory" ||
this.config.strategy === "hybrid") {
const memoryResult = await this.memoryCache.get(cacheKey);
if (memoryResult !== null) {
this.updateAccessMetadata(cacheKey);
this.recordResponseTime(Date.now() - startTime);
// Extract raw data from CachedData format
return this.fromCachedData(memoryResult);
}
}
// Strategy 2: Try Redis if memory miss
if ((this.config.strategy === "redis" ||
this.config.strategy === "hybrid") &&
this.redisClient) {
const redisResult = await this.getFromRedis(cacheKey);
if (redisResult !== null) {
// For hybrid strategy, promote hot data to memory
if (this.config.strategy === "hybrid" &&
this.isHotData(cacheKey)) {
// Convert to CachedData format for memory cache
const cachedData = this.toCachedData(redisResult);
await this.memoryCache.set(cacheKey, cachedData, {
ttl: this.config.memory?.ttl,
});
}
this.updateAccessMetadata(cacheKey);
this.recordResponseTime(Date.now() - startTime);
return redisResult;
}
}
// Cache miss
this.recordResponseTime(Date.now() - startTime);
return null;
}
catch (error) {
this.emit("cache_error", { operation: "get", key, error });
this.recordResponseTime(Date.now() - startTime);
return null;
}
}
/**
* Set value in cache with intelligent placement
*
* @param key - The cache key to store the value under
* @param value - The value to cache with proper typing
* @param options - Optional caching options
* @param options.ttl - Time to live in milliseconds
* @param options.tags - Array of tags for bulk invalidation
* @returns Promise resolving to true if successful, false otherwise
*
* @example
* ```typescript
* interface User { id: number; name: string; email: string; }
*
* const user: User = { id: 123, name: "John", email: "john@example.com" };
* const success = await cache.set<User>("user:123", user, {
* ttl: 3600000, // 1 hour
* tags: ["users", "active"]
* });
* ```
*/
async set(key, value, options = {}) {
const startTime = Date.now();
try {
const cacheKey = this.generateKey(key);
const ttl = options.ttl || this.config.memory?.ttl || 600000; // 10 minutes default
// Determine storage strategy
const shouldStoreInMemory = this.config.strategy === "memory" ||
this.config.strategy === "hybrid";
const shouldStoreInRedis = this.config.strategy === "redis" ||
this.config.strategy === "hybrid";
// Store in memory cache
if (shouldStoreInMemory) {
// Convert to CachedData format for memory cache
const cachedData = this.toCachedData(value);
await this.memoryCache.set(cacheKey, cachedData, { ttl });
}
// Store in Redis
if (shouldStoreInRedis && this.redisClient) {
await this.setInRedis(cacheKey, value, ttl, options.tags);
}
// Update metadata
this.updateAccessMetadata(cacheKey, JSON.stringify(value).length);
if (options.tags) {
const meta = this.metadata.get(cacheKey);
if (meta) {
meta.tags = options.tags;
this.metadata.set(cacheKey, meta);
}
}
this.recordResponseTime(Date.now() - startTime);
return true;
}
catch (error) {
this.emit("cache_error", { operation: "set", key, error });
this.recordResponseTime(Date.now() - startTime);
return false;
}
}
/**
* Delete value from cache
*/
async delete(key) {
const startTime = Date.now();
try {
const cacheKey = this.generateKey(key);
let deleted = false;
// Delete from memory cache
if (this.config.strategy === "memory" ||
this.config.strategy === "hybrid") {
deleted = this.memoryCache.delete(cacheKey) || deleted;
}
// Delete from Redis
if ((this.config.strategy === "redis" ||
this.config.strategy === "hybrid") &&
this.redisClient) {
const redisDeleted = await this.redisClient.del(cacheKey);
deleted = redisDeleted > 0 || deleted;
}
// Clean up metadata
this.metadata.delete(cacheKey);
this.recordResponseTime(Date.now() - startTime);
return deleted;
}
catch (error) {
this.emit("cache_error", { operation: "delete", key, error });
this.recordResponseTime(Date.now() - startTime);
return false;
}
}
/**
* Check if key exists in cache
*/
async exists(key) {
try {
const cacheKey = this.generateKey(key);
// Check memory cache first
if (this.config.strategy === "memory" ||
this.config.strategy === "hybrid") {
if (this.memoryCache.has(cacheKey)) {
return true;
}
}
// Check Redis
if ((this.config.strategy === "redis" ||
this.config.strategy === "hybrid") &&
this.redisClient) {
const exists = await this.redisClient.exists(cacheKey);
return exists > 0;
}
return false;
}
catch (error) {
this.emit("cache_error", { operation: "exists", key, error });
return false;
}
}
/**
* Clear all cache entries
*/
async clear() {
try {
// Clear memory cache
if (this.config.strategy === "memory" ||
this.config.strategy === "hybrid") {
this.memoryCache.clear();
}
// Clear Redis
if ((this.config.strategy === "redis" ||
this.config.strategy === "hybrid") &&
this.redisClient) {
await this.redisClient.flushdb();
}
// Clear metadata
this.metadata.clear();
this.emit("cache_cleared");
}
catch (error) {
this.emit("cache_error", { operation: "clear", error });
throw error;
}
}
// ========================================
// REDIS HELPER METHODS
// ========================================
/**
* Get value from Redis with encryption support
*/
async getFromRedis(key) {
if (!this.redisClient)
return null;
try {
const serialized = await this.redisClient.get(key);
if (!serialized)
return null;
// Use consistent deserialization
return await this.deserializeFromRedis(serialized);
}
catch (error) {
console.error("Redis get error:", error);
return null;
}
}
/**
* Set value in Redis with encryption and TTL support
*/
async setInRedis(key, value, ttl, tags) {
if (!this.redisClient)
return;
try {
// Use consistent serialization
const serialized = await this.serializeForRedis(value);
// Set with TTL
if (ttl > 0) {
await this.redisClient.setex(key, Math.floor(ttl / 1000), serialized);
}
else {
await this.redisClient.set(key, serialized);
}
// Handle tags for cache invalidation
if (tags && tags.length > 0) {
await this.setTags(key, tags);
}
}
catch (error) {
console.error("Redis set error:", error);
throw error;
}
}
/**
* Set tags for cache invalidation
*/
async setTags(key, tags) {
if (!this.redisClient)
return;
try {
const pipeline = this.redisClient.pipeline();
for (const tag of tags) {
const tagKey = `tag:${tag}`;
pipeline.sadd(tagKey, key);
pipeline.expire(tagKey, 86400); // 24 hours
}
await pipeline.exec();
}
catch (error) {
console.error("Redis tag set error:", error);
}
}
/**
* Record response time for performance monitoring
*/
recordResponseTime(responseTime) {
// Update running average
const currentAvg = this.stats.performance.averageResponseTime;
const totalOps = this.stats.performance.totalOperations;
this.stats.performance.averageResponseTime =
(currentAvg * (totalOps - 1) + responseTime) / totalOps;
}
// ========================================
// ADVANCED CACHE OPERATIONS
// ========================================
/**
* Invalidate cache entries by tags
*/
async invalidateByTags(tags) {
if (!this.redisClient)
return 0;
try {
let invalidatedCount = 0;
for (const tag of tags) {
const tagKey = `tag:${tag}`;
const keys = await this.redisClient.smembers(tagKey);
if (keys.length > 0) {
// Delete all keys with this tag
await this.redisClient.del(...keys);
// Remove from memory cache too
for (const key of keys) {
this.memoryCache.delete(key);
this.metadata.delete(key);
}
invalidatedCount += keys.length;
}
// Clean up the tag set
await this.redisClient.del(tagKey);
}
this.emit("cache_invalidated", { tags, count: invalidatedCount });
return invalidatedCount;
}
catch (error) {
this.emit("cache_error", {
operation: "invalidateByTags",
tags,
error,
});
return 0;
}
}
/**
* Get multiple values at once (batch operation)
*
* @param keys - Array of cache keys to retrieve
* @returns Promise resolving to an object with key-value pairs (missing keys are omitted)
*
* @example
* ```typescript
* interface User { id: number; name: string; }
*
* const users = await cache.mget<User>(["user:1", "user:2", "user:3"]);
* // users is Record<string, User>
*
* for (const [key, user] of Object.entries(users)) {
* console.log(`${key}: ${user.name}`); // TypeScript knows user.name is string
* }
* ```
*/
async mget(keys) {
const results = {};
try {
// Use Promise.all for parallel execution
const promises = keys.map(async (key) => {
const value = await this.get(key);
return { key, value };
});
const resolved = await Promise.all(promises);
for (const { key, value } of resolved) {
if (value !== null) {
results[key] = value;
}
}
return results;
}
catch (error) {
this.emit("cache_error", { operation: "mget", keys, error });
return {};
}
}
/**
* Set multiple values at once (batch operation)
*
* @param entries - Object with key-value pairs or array of [key, value] tuples
* @param options - Optional caching options applied to all entries
* @param options.ttl - Time to live in milliseconds for all entries
* @param options.tags - Array of tags applied to all entries
* @returns Promise resolving to true if all operations successful, false otherwise
*
* @example
* ```typescript
* interface User { id: number; name: string; }
*
* // Using object notation
* const success1 = await cache.mset<User>({
* "user:1": { id: 1, name: "Alice" },
* "user:2": { id: 2, name: "Bob" }
* }, { ttl: 3600000, tags: ["users"] });
*
* // Using array notation
* const success2 = await cache.mset<User>([
* ["user:3", { id: 3, name: "Charlie" }],
* ["user:4", { id: 4, name: "Diana" }]
* ], { ttl: 3600000 });
* ```
*/
async mset(entries, options = {}) {
try {
// Convert array format to object format if needed
const entriesObj = Array.isArray(entries)
? Object.fromEntries(entries)
: entries;
// Use Promise.all for parallel execution
const promises = Object.entries(entriesObj).map(([key, value]) => this.set(key, value, options));
const results = await Promise.all(promises);
return results.every((result) => result === true);
}
catch (error) {
this.emit("cache_error", {
operation: "mset",
entries: Object.keys(entries),
error,
});
return false;
}
}
// ========================================
// TYPE-SAFE ALIAS METHODS
// ========================================
/**
* Read value from cache (alias for get method)
*
* @param key - The cache key to retrieve
* @returns Promise resolving to the cached value with proper typing, or null if not found
*
* @example
* ```typescript
* interface User { id: number; name: string; }
* const user = await cache.read<User>("user:123");
* if (user) {
* console.log(user.name); // TypeScript knows this is a string
* }
* ```
*/
async read(key) {
return this.get(key);
}
/**
* Write value to cache (alias for set method)
*
* @param key - The cache key to store the value under
* @param value - The value to cache with proper typing
* @param options - Optional caching options
* @param options.ttl - Time to live in milliseconds
* @param options.tags - Array of tags for bulk invalidation
* @returns Promise resolving to true if successful, false otherwise
*
* @example
* ```typescript
* interface User { id: number; name: string; email: string; }
*
* const user: User = { id: 123, name: "John", email: "john@example.com" };
* const success = await cache.write<User>("user:123", user, {
* ttl: 3600000, // 1 hour
* tags: ["users", "active"]
* });
* ```
*/
async write(key, value, options = {}) {
return this.set(key, value, options);
}
// ========================================
// ENHANCED CACHE METHODS
// ========================================
/**
* Get TTL for a specific key
*/
async getTTL(key) {
const cacheKey = this.generateKey(key);
try {
// Check memory cache first by checking if key exists
if (this.config.strategy === "memory" ||
this.config.strategy === "hybrid") {
if (this.memoryCache.has(cacheKey)) {
// For memory cache, we can't get exact TTL, so return a default
return 300000; // 5 minutes default
}
}
// Check Redis cache
if (this.redisClient &&
(this.config.strategy === "redis" ||
this.config.strategy === "hybrid")) {
const redisTTL = await this.redisClient.ttl(cacheKey);
return redisTTL > 0 ? redisTTL * 1000 : -1; // Convert to milliseconds
}
return -1; // Key doesn't exist or no TTL
}
catch (error) {
console.error("Get TTL error:", error);
return -1;
}
}
/**
* Set expiration time for a key
*/
async expire(key, ttl) {
const cacheKey = this.generateKey(key);
try {
let success = false;
// Update memory cache TTL
if (this.config.strategy === "memory" ||
this.config.strategy === "hybrid") {
const value = await this.memoryCache.get(cacheKey);
if (value !== null) {
await this.memoryCache.set(cacheKey, value, { ttl });
success = true;
}
}
// Update Redis cache TTL
if (this.redisClient &&
(this.config.strategy === "redis" ||
this.config.strategy === "hybrid")) {
const result = await this.redisClient.expire(cacheKey, Math.floor(ttl / 1000));
success = success || result === 1;
}
return success;
}
catch (error) {
console.error("Expire error:", error);
return false;
}
}
/**
* Get all keys matching a pattern
*/
async keys(pattern) {
try {
const allKeys = new Set();
// Get keys from memory cache using metadata
if (this.config.strategy === "memory" ||
this.config.strategy === "hybrid") {
// Use metadata to get all keys
for (const [cacheKey] of this.metadata.entries()) {
const originalKey = this.extractOriginalKey(cacheKey);
if (originalKey) {
allKeys.add(originalKey);
}
}
}
// Get keys from Redis cache
if (this.redisClient &&
(this.config.strategy === "redis" ||
this.config.strategy === "hybrid")) {
const redisPattern = pattern
? `fortify:v2:*:${pattern}`
: "fortify:v2:*";
const redisKeys = await this.redisClient.keys(redisPattern);
redisKeys.forEach((key) => {
const originalKey = this.extractOriginalKey(key);
if (originalKey) {
allKeys.add(originalKey);
}
});
}
const keysArray = Array.from(allKeys);
// Apply pattern filtering if specified
if (pattern && !pattern.includes("*")) {
return keysArray.filter((key) => key.includes(pattern));
}
return keysArray;
}
catch (error) {
console.error("Keys error:", error);
return [];
}
}
/**
* Extract original key from cache key
*/
extractOriginalKey(cacheKey) {
// Format: fortify:v2:hash:originalKey
const parts = cacheKey.split(":");
if (parts.length >= 4 && parts[0] === "fortify" && parts[1] === "v2") {
return parts.slice(3).join(":");
}
return null;
}
// ========================================
// MONITORING AND STATISTICS
// ========================================
/**
* Get comprehensive cache statistics
*/
async getStats() {
// Update Redis stats if available
if (this.redisClient && this.stats.redis) {
await this.updateRedisStats();
}
return { ...this.stats };
}
/**
* Update Redis statistics using Redis INFO command
*/
async updateRedisStats() {
if (!this.redisClient || !this.stats.redis)
return;
try {
// Get Redis INFO command output
const info = await this.redisClient.info();
const infoLines = info.split("\r\n");
const infoData = {};
// Parse INFO command output
for (const line of infoLines) {
if (line.includes(":")) {
const [key, value] = line.split(":");
infoData[key] = value;
}
}
// Update connection status
this.stats.redis.connected = this.redisClient.status === "ready";
// Update memory usage
if (infoData.used_memory) {
this.stats.redis.memoryUsage = {
used: parseInt(infoData.used_memory),
peak: parseInt(infoData.used_memory_peak || "0"),
percentage: this.calculateRedisMemoryPercentage(infoData),
};
}
// Update performance metrics
if (infoData.total_commands_processed) {
this.stats.redis.operations = parseInt(infoData.total_commands_processed);
}
if (infoData.keyspace_hits && infoData.keyspace_misses) {
const hits = parseInt(infoData.keyspace_hits);
const misses = parseInt(infoData.keyspace_misses);
const total = hits + misses;
this.stats.redis.hitRate = total > 0 ? hits / total : 0;
this.stats.redis.hits = hits;
this.stats.redis.misses = misses;
}
// Update connection info
if (infoData.connected_clients) {
this.stats.redis.connections = parseInt(infoData.connected_clients);
}
// Update key count
if (infoData.db0) {
const dbInfo = infoData.db0.match(/keys=(\d+)/);
if (dbInfo) {
this.stats.redis.keys = parseInt(dbInfo[1]);
}
}
// Update uptime
if (infoData.uptime_in_seconds) {
this.stats.redis.uptime =
parseInt(infoData.uptime_in_seconds) * 1000; // Convert to ms
}
// Update last update timestamp
this.stats.redis.lastUpdate = Date.now();
}
catch (error) {
console.error("Failed to update Redis stats:", error);
// Mark as disconnected if we can't get stats
this.stats.redis.connected = false;
}
}
/**
* Calculate Redis memory usage percentage
*/
calculateRedisMemoryPercentage(infoData) {
const usedMemory = parseInt(infoData.used_memory || "0");
const maxMemory = parseInt(infoData.maxmemory || "0");
if (maxMemory === 0) {
// If no max memory is set, calculate based on system memory
const totalSystemMemory = parseInt(infoData.total_system_memory || "0");
if (totalSystemMemory > 0) {
return (usedMemory / totalSystemMemory) * 100;
}
return 0;
}
return (usedMemory / maxMemory) * 100;
}
/**
* Get cache health status
*/
getHealth() {
const memoryUsage = this.stats.memory.memoryUsage.percentage;
const hitRate = this.stats.memory.hitRate * 100;
const redisConnected = this.redisClient?.status === "ready";
let status = "healthy";
const issues = [];
if (memoryUsage > 90) {
status = "unhealthy";
issues.push("High memory usage");
}
else if (memoryUsage > 75) {
status = "degraded";
issues.push("Elevated memory usage");
}
if (hitRate < 50) {
status = "unhealthy";
issues.push("Low hit rate");
}
else if (hitRate < 80) {
status = "degraded";
issues.push("Suboptimal hit rate");
}
if (this.config.strategy !== "memory" && !redisConnected) {
status = "unhealthy";
issues.push("Redis disconnected");
}
return {
status,
details: {
memoryUsage,
hitRate,
redisConnected,
issues,
uptime: Date.now() - this.stats.memory.startTime || 0,
},
};
}
/**
* Disconnect from all cache backends
*/
async disconnect() {
try {
// Stop monitoring
if (this.healthMonitor) {
clearInterval(this.healthMonitor);
}
if (this.metricsCollector) {
clearInterval(this.metricsCollector);
}
// Disconnect Redis
if (this.redisClient) {
await this.redisClient.quit();
}
// Clear connection pool
for (const [, connection] of this.connectionPool) {
await connection.quit();
}
this.connectionPool.clear();
this.emit("disconnected");
}
catch (error) {
this.emit("error", error);
throw error;
}
}
}
exports.SecureCacheAdapter = SecureCacheAdapter;
//# sourceMappingURL=SecureCacheAdapter.js.map