xypriss-security
Version:
XyPriss Security is an advanced JavaScript security library designed for enterprise applications. It provides military-grade encryption, secure data structures, quantum-resistant cryptography, and comprehensive security utilities for modern web applicatio
1,338 lines (1,337 loc) • 56.5 kB
JavaScript
import { EventEmitter } from 'events';
import Redis, { Cluster } from 'ioredis';
import { EncryptionService } from '../encryption/EncryptionService.js';
import { XyPrissSecurity } from 'xypriss-security';
import { initializeLogger } from '../../shared/logger/Logger.js';
import { SecureInMemoryCache as SIMC } from '../../components/cache/useCache.js';
import '../../components/cache/index.js';
/**
* XyPrissJS 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 XyPrissJS security cache
*/
/**
* UF secure cache adapter
*/
class SecureCacheAdapter extends EventEmitter {
constructor(config = {}) {
super();
this.connectionPool = new Map();
this.metadata = new Map();
this.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 = XyPrissSecurity.generateSecureToken({
length: 32,
entropy: "high",
});
}
/**
* Initialize memory cache with security features
*/
initializeMemoryCache() {
this.memoryCache = new SIMC();
// 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 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 `XyPriss: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.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.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;
}
}
/**
* Set encrypted session data with expiration (SETEX for sessions)
*
* This method provides secure session storage with automatic encryption,
* expiration, and session-specific security features.
*
* @param sessionKey - The session key (will be prefixed with 'session:')
* @param sessionData - The session data to encrypt and store
* @param ttlSeconds - Time to live in seconds (Redis SETEX style)
* @param options - Optional session-specific options
* @param options.userId - User ID for session tracking
* @param options.ipAddress - IP address for session validation
* @param options.userAgent - User agent for session validation
* @param options.encryptionKey - Custom encryption key (uses default if not provided)
* @param options.tags - Array of tags for bulk invalidation
* @returns Promise resolving to true if successful, false otherwise
*
* @example
* ```typescript
* interface SessionData {
* userId: string;
* username: string;
* roles: string[];
* lastActivity: number;
* }
*
* const sessionData: SessionData = {
* userId: "user123",
* username: "john_doe",
* roles: ["user", "admin"],
* lastActivity: Date.now()
* };
*
* // Store encrypted session for 1 hour
* const success = await cache.setex("abc123def456", sessionData, 3600, {
* userId: "user123",
* ipAddress: "192.168.1.100",
* userAgent: "Mozilla/5.0...",
* tags: ["sessions", "user123"]
* });
* ```
*/
async setex(sessionKey, sessionData, ttlSeconds, options = {}) {
const startTime = Date.now();
try {
// Import XyPriss Security for encryption
const { XyPrissSecurity, Hash } = await import('xypriss-security');
const { EncryptionService } = await import('../encryption/EncryptionService.js');
// Validate inputs
if (!sessionKey || typeof sessionKey !== "string") {
throw new Error("Session key must be a non-empty string");
}
if (ttlSeconds <= 0 || ttlSeconds > 86400 * 30) {
// Max 30 days
throw new Error("TTL must be between 1 second and 30 days");
}
// Generate session-specific cache key
const cacheKey = this.generateKey(`session:${sessionKey}`);
const ttlMs = ttlSeconds * 1000;
// Create session metadata
const sessionMetadata = {
sessionId: sessionKey,
userId: options.userId,
ipAddress: options.ipAddress,
userAgent: options.userAgent,
createdAt: Date.now(),
expiresAt: Date.now() + ttlMs,
version: "1.0",
};
// Create secure session payload
const sessionPayload = {
data: sessionData,
metadata: sessionMetadata,
signature: XyPrissSecurity.generateSessionToken({
userId: options.userId,
ipAddress: options.ipAddress,
userAgent: options.userAgent,
expiresIn: ttlSeconds,
}),
};
// Encrypt the session payload
const encryptionKey = options.encryptionKey || EncryptionService.generateSessionKey();
const encryptedPayload = await EncryptionService.encrypt(JSON.stringify(sessionPayload), encryptionKey);
// Create final session object with encryption metadata
const keyHashForStorage = Hash.create(encryptionKey, {
algorithm: "sha256",
outputFormat: "hex",
})
.toString()
.substring(0, 16);
const secureSessionData = {
encrypted: true,
algorithm: "AES-256-GCM",
payload: encryptedPayload,
keyHash: keyHashForStorage, // First 16 chars for verification
sessionMetadata: {
sessionId: sessionKey,
userId: options.userId,
createdAt: sessionMetadata.createdAt,
expiresAt: sessionMetadata.expiresAt,
},
};
// Store using the standard set method with session-specific options
const success = await this.set(cacheKey, secureSessionData, {
ttl: ttlMs,
tags: options.tags
? ["sessions", ...options.tags]
: ["sessions"],
});
if (success) {
// Update session tracking metadata
this.updateSessionMetadata(sessionKey, {
userId: options.userId,
ipAddress: options.ipAddress,
userAgent: options.userAgent,
lastAccess: Date.now(),
encrypted: true,
});
this.emit("session_stored", {
sessionId: sessionKey,
userId: options.userId,
ttl: ttlSeconds,
encrypted: true,
});
}
this.recordResponseTime(Date.now() - startTime);
return success;
}
catch (error) {
this.emit("cache_error", {
operation: "setex",
key: sessionKey,
error,
});
this.recordResponseTime(Date.now() - startTime);
return false;
}
}
/**
* Get encrypted session data with automatic decryption
*
* Retrieves and decrypts session data stored with the setex method.
* Validates session integrity and expiration.
*
* @param sessionKey - The session key used when storing
* @param encryptionKey - The encryption key used when storing (required for decryption)
* @param options - Optional validation options
* @param options.validateUserId - User ID to validate against stored session
* @param options.validateIpAddress - IP address to validate against stored session
* @param options.validateUserAgent - User agent to validate against stored session
* @returns Promise resolving to decrypted session data, or null if not found/invalid
*
* @example
* ```typescript
* // Retrieve session data
* const sessionData = await cache.getSession<UserSession>(
* "session_abc123def456",
* encryptionKey,
* {
* validateUserId: "user123",
* validateIpAddress: "192.168.1.100"
* }
* );
*
* if (sessionData) {
* console.log("Session valid:", sessionData.username);
* } else {
* console.log("Session not found or invalid");
* }
* ```
*/
async getSession(sessionKey, encryptionKey, options = {}) {
const startTime = Date.now();
try {
// Import required modules
const { XyPrissSecurity, Hash } = await import('xypriss-security');
const { EncryptionService } = await import('../encryption/EncryptionService.js');
// Validate inputs
if (!sessionKey || typeof sessionKey !== "string") {
throw new Error("Session key must be a non-empty string");
}
if (!encryptionKey || typeof encryptionKey !== "string") {
throw new Error("Encryption key is required for session decryption");
}
// Get the encrypted session data
const cacheKey = this.generateKey(`session:${sessionKey}`);
const encryptedSessionData = await this.get(cacheKey);
if (!encryptedSessionData) {
this.recordResponseTime(Date.now() - startTime);
return null;
}
// Validate it's an encrypted session
if (!encryptedSessionData.encrypted ||
!encryptedSessionData.payload) {
console.warn("Retrieved data is not an encrypted session");
this.recordResponseTime(Date.now() - startTime);
return null;
}
// Verify encryption key hash
const keyHash = Hash.create(encryptionKey, {
algorithm: "sha256",
outputFormat: "hex",
})
.toString()
.substring(0, 16);
if (encryptedSessionData.keyHash !== keyHash) {
console.warn("Invalid encryption key for session");
this.recordResponseTime(Date.now() - startTime);
return null;
}
// Decrypt the session payload
const decryptedPayload = await EncryptionService.decrypt(encryptedSessionData.payload, encryptionKey);
const sessionPayload = JSON.parse(decryptedPayload);
// Validate session expiration
if (sessionPayload.metadata.expiresAt < Date.now()) {
console.warn("Session has expired");
// Clean up expired session
await this.delete(cacheKey);
this.recordResponseTime(Date.now() - startTime);
return null;
}
// Validate session context if provided
if (options.validateUserId &&
sessionPayload.metadata.userId !== options.validateUserId) {
console.warn("Session user ID mismatch");
this.recordResponseTime(Date.now() - startTime);
return null;
}
if (options.validateIpAddress &&
sessionPayload.metadata.ipAddress !== options.validateIpAddress) {
console.warn("Session IP address mismatch");
this.recordResponseTime(Date.now() - startTime);
return null;
}
if (options.validateUserAgent &&
sessionPayload.metadata.userAgent !== options.validateUserAgent) {
console.warn("Session user agent mismatch");
this.recordResponseTime(Date.now() - startTime);
return null;
}
// Update session access metadata
this.updateSessionMetadata(sessionKey, {
userId: sessionPayload.metadata.userId,
ipAddress: sessionPayload.metadata.ipAddress,
userAgent: sessionPayload.metadata.userAgent,
lastAccess: Date.now(),
encrypted: true,
});
this.emit("session_accessed", {
sessionId: sessionKey,
userId: sessionPayload.metadata.userId,
accessTime: Date.now(),
});
this.recordResponseTime(Date.now() - startTime);
return sessionPayload.data;
}
catch (error) {
this.emit("cache_error", {
operation: "getSession",
key: sessionKey,
error,
});
this.recordResponseTime(Date.now() - startTime);
return null;
}
}
/**
* Update session-specific metadata
*/
updateSessionMetadata(sessionKey, sessionInfo) {
try {
const cacheKey = this.generateKey(`session:${sessionKey}`);
const existingMeta = this.metadata.get(cacheKey) || {
accessCount: 0,
lastAccessed: Date.now(),
size: 0,
isHot: false,
location: "memory",
tags: ["sessions"],
};
// Update with session-specific information
const updatedMeta = {
...existingMeta,
sessionId: sessionKey,
userId: sessionInfo.userId,
ipAddress: sessionInfo.ipAddress,
userAgent: sessionInfo.userAgent,
lastAccessed: sessionInfo.lastAccess || Date.now(),
encrypted: sessionInfo.encrypted || false,
accessCount: existingMeta.accessCount + 1,
tags: existingMeta.tags || ["sessions"],
};
this.metadata.set(cacheKey, updatedMeta);
}
catch (error) {
// Silently fail to avoid breaking the main operation
console.warn("Failed to update session metadata:", error);
}
}
/**
* 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
? `XyPriss:v2:*:${pattern}`
: "XyPriss: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 patter