UNPKG

@xynehq/jaf

Version:

Juspay Agent Framework - A purely functional agent framework with immutable state and composable tools

240 lines 9.7 kB
/** * Real Redis Session Provider Implementation * * This provides a production-ready Redis-based session provider * with proper error handling and connection management */ import { throwSessionError } from '../types.js'; import { createSession } from './index.js'; export const createRedisSessionProvider = (config) => { let redis; // Require real Redis - no fallback try { // Dynamic import to avoid breaking if ioredis not installed // eslint-disable-next-line @typescript-eslint/no-var-requires const Redis = require('ioredis'); redis = new Redis({ host: config.host, port: config.port, password: config.password, db: config.database || 0, retryStrategy: (times) => { const delay = Math.min(times * 50, 2000); return delay; }, enableOfflineQueue: true, maxRetriesPerRequest: 3, lazyConnect: false, connectTimeout: 10000, commandTimeout: 5000 }); // Handle connection events redis.on('error', (err) => { console.error('[ADK:Sessions] Redis connection error:', err); }); redis.on('connect', () => { console.log('[ADK:Sessions] Connected to Redis'); }); // Only log Redis events if not in test environment if (process.env.NODE_ENV !== 'test') { redis.on('ready', () => { console.log('[ADK:Sessions] Redis ready for commands'); }); redis.on('close', () => { console.log('[ADK:Sessions] Redis connection closed'); }); redis.on('reconnecting', (delay) => { console.log(`[ADK:Sessions] Reconnecting to Redis in ${delay}ms`); }); } } catch (error) { throw new Error('Redis session provider requires ioredis to be installed. ' + 'Please install it with: npm install ioredis'); } const keyPrefix = config.keyPrefix || 'jaf_adk_session:'; const sessionTTL = config.ttl || 86400; // Default 24 hours const getKey = (sessionId) => `${keyPrefix}${sessionId}`; const getUserKey = (userId) => `${keyPrefix}user:${userId}`; // Helper to serialize/deserialize sessions const serializeSession = (session) => { return JSON.stringify(session); }; const deserializeSession = (sessionData) => { const session = JSON.parse(sessionData); // Restore Date objects from strings if (session.metadata.created && typeof session.metadata.created === 'string') { session.metadata.created = new Date(session.metadata.created); } if (session.metadata.lastAccessed && typeof session.metadata.lastAccessed === 'string') { session.metadata.lastAccessed = new Date(session.metadata.lastAccessed); } return session; }; // Redis operation helpers with proper async handling const redisGet = async (key) => { try { return await redis.get(key); } catch (error) { console.error(`[ADK:Sessions] Redis GET error for key ${key}:`, error); throw error; } }; const redisSet = async (key, value, ttlSeconds) => { try { if (ttlSeconds) { await redis.setex(key, ttlSeconds, value); } else { await redis.set(key, value); } } catch (error) { console.error(`[ADK:Sessions] Redis SET error for key ${key}:`, error); throw error; } }; // Note: redisDelete, redisSAdd, and redisSRem are used in the deleteSession method // which uses the redis.multi() approach for atomic operations const redisSMembers = async (key) => { try { return await redis.smembers(key); } catch (error) { console.error(`[ADK:Sessions] Redis SMEMBERS error for key ${key}:`, error); throw error; } }; // Multi/Pipeline operations for performance const redisMulti = () => { return redis.multi(); }; return { createSession: async (context) => { const session = createSession(context.appName, context.userId, context.sessionId); try { // Use multi for atomic operations const multi = redisMulti(); multi.setex(getKey(session.id), sessionTTL, serializeSession(session)); multi.sadd(getUserKey(context.userId), session.id); await multi.exec(); return session; } catch (error) { throwSessionError(`Failed to create session in Redis: ${error}`, session.id); throw error; // TypeScript needs this even though throwSessionError never returns } }, getSession: async (sessionId) => { try { const sessionData = await redisGet(getKey(sessionId)); if (!sessionData) { return null; } const session = deserializeSession(sessionData); // Update last accessed time and refresh TTL session.metadata.lastAccessed = new Date(); await redisSet(getKey(sessionId), serializeSession(session), sessionTTL); return session; } catch (error) { throwSessionError(`Failed to get session from Redis: ${error}`, sessionId); throw error; // TypeScript needs this even though throwSessionError never returns } }, updateSession: async (session) => { try { session.metadata.lastAccessed = new Date(); await redisSet(getKey(session.id), serializeSession(session), sessionTTL); return session; } catch (error) { throwSessionError(`Failed to update session in Redis: ${error}`, session.id); throw error; // TypeScript needs this even though throwSessionError never returns } }, listSessions: async (userId) => { try { const userKey = getUserKey(userId); const sessionIds = await redisSMembers(userKey); if (sessionIds.length === 0) { return []; } const sessions = []; const deadSessionIds = []; // Retrieve all sessions for user for (const sessionId of sessionIds) { const sessionData = await redisGet(getKey(sessionId)); if (sessionData) { try { sessions.push(deserializeSession(sessionData)); } catch (error) { // Skip invalid sessions console.warn(`[ADK:Sessions] Skipping invalid session ${sessionId}:`, error); deadSessionIds.push(sessionId); } } else { // Session expired or deleted deadSessionIds.push(sessionId); } } // Clean up dead session references if (deadSessionIds.length > 0) { const multi = redis.multi(); for (const deadId of deadSessionIds) { multi.srem(userKey, deadId); } await multi.exec(); } // Sort by creation date (newest first) return sessions.sort((a, b) => b.metadata.created.getTime() - a.metadata.created.getTime()); } catch (error) { throwSessionError(`Failed to list sessions from Redis: ${error}`); throw error; // TypeScript needs this even though throwSessionError never returns } }, deleteSession: async (sessionId) => { try { const sessionData = await redisGet(getKey(sessionId)); if (!sessionData) { return false; } const session = deserializeSession(sessionData); // Use multi for atomic deletion const multi = redis.multi(); multi.del(getKey(sessionId)); multi.srem(getUserKey(session.userId), sessionId); const results = await multi.exec(); return results?.[0]?.[1] === 1; } catch (error) { throwSessionError(`Failed to delete session from Redis: ${error}`, sessionId); throw error; // TypeScript needs this even though throwSessionError never returns } } }; }; // Additional utility functions export const closeRedisConnection = async (provider) => { // Access the internal redis client if available if (provider._redis && typeof provider._redis.disconnect === 'function') { await provider._redis.disconnect(); } }; export const pingRedis = async (provider) => { try { if (provider._redis && typeof provider._redis.ping === 'function') { const result = await provider._redis.ping(); return result === 'PONG'; } return false; } catch { return false; } }; //# sourceMappingURL=redis-provider.js.map