@xynehq/jaf
Version:
Juspay Agent Framework - A purely functional agent framework with immutable state and composable tools
355 lines • 15.1 kB
JavaScript
import { createSuccess, createFailure, createMemoryConnectionError, createMemoryNotFoundError, createMemoryStorageError } from '../types';
import { safeConsole } from '../../utils/logger.js';
/**
* Redis memory provider - persistent across server restarts
* Best for production environments with shared state
*/
export async function createRedisProvider(config, redisClient) {
const fullConfig = {
...config,
type: 'redis',
host: config.host ?? 'localhost',
port: config.port ?? 6379,
db: config.db ?? 0,
keyPrefix: config.keyPrefix ?? 'jaf:memory:'
};
try {
await redisClient.ping();
safeConsole.log(`[MEMORY:Redis] Connected to Redis at ${fullConfig.host}:${fullConfig.port}`);
}
catch (error) {
throw createMemoryConnectionError('Redis', error);
}
const ensureConnected = () => {
if (!redisClient) {
throw createMemoryConnectionError('Redis', new Error('Redis client not initialized'));
}
return redisClient;
};
const getKey = (conversationId) => {
return `${fullConfig.keyPrefix}${conversationId}`;
};
const getUserKey = (userId) => {
return `${fullConfig.keyPrefix}user:${userId}:*`;
};
const storeMessages = async (conversationId, messages, metadata) => {
const client = ensureConnected();
try {
const now = new Date();
const conversation = {
conversationId,
userId: metadata?.userId,
messages,
metadata: {
createdAt: now,
updatedAt: now,
totalMessages: messages.length,
lastActivity: now,
traceId: metadata?.traceId,
...metadata
}
};
const key = getKey(conversationId);
const value = JSON.stringify(conversation, null, 0); // Compact JSON
await client.set(key, value);
// Set TTL if configured
if (fullConfig.ttl) {
await client.expire(key, fullConfig.ttl);
}
safeConsole.log(`[MEMORY:Redis] Stored ${messages.length} messages for conversation ${conversationId}`);
return createSuccess(undefined);
}
catch (error) {
return createFailure(createMemoryStorageError('store messages', 'Redis', error));
}
};
const getConversation = async (conversationId) => {
const client = ensureConnected();
try {
const key = getKey(conversationId);
const value = await client.get(key);
if (!value) {
return createSuccess(null);
}
const conversation = JSON.parse(value);
// Convert date strings back to Date objects
const convertedConversation = {
...conversation,
metadata: conversation.metadata ? {
...conversation.metadata,
createdAt: conversation.metadata.createdAt ? new Date(conversation.metadata.createdAt) : new Date(),
updatedAt: conversation.metadata.updatedAt ? new Date(conversation.metadata.updatedAt) : new Date(),
lastActivity: conversation.metadata.lastActivity ? new Date(conversation.metadata.lastActivity) : new Date()
} : undefined
};
// Update last activity
const updatedConversation = {
...convertedConversation,
metadata: {
...convertedConversation.metadata,
lastActivity: new Date()
}
};
// Store updated last activity (fire and forget)
client.set(key, JSON.stringify(updatedConversation, null, 0)).catch(safeConsole.error);
safeConsole.log(`[MEMORY:Redis] Retrieved conversation ${conversationId} with ${convertedConversation.messages.length} messages`);
return createSuccess(updatedConversation);
}
catch (error) {
return createFailure(createMemoryStorageError('get conversation', 'Redis', error));
}
};
const appendMessages = async (conversationId, messages, metadata) => {
const client = ensureConnected();
try {
const existingResult = await getConversation(conversationId);
if (!existingResult.success) {
return existingResult;
}
if (!existingResult.data) {
return createFailure(createMemoryNotFoundError(conversationId, 'Redis'));
}
const existing = existingResult.data;
const updatedMessages = [...existing.messages, ...messages];
const now = new Date();
const updatedConversation = {
...existing,
messages: updatedMessages,
metadata: {
...existing.metadata,
updatedAt: now,
lastActivity: now,
totalMessages: updatedMessages.length,
traceId: metadata?.traceId || existing.metadata?.traceId,
...metadata
}
};
const key = getKey(conversationId);
await client.set(key, JSON.stringify(updatedConversation, null, 0));
// Refresh TTL if configured
if (fullConfig.ttl) {
await client.expire(key, fullConfig.ttl);
}
safeConsole.log(`[MEMORY:Redis] Appended ${messages.length} messages to conversation ${conversationId} (total: ${updatedMessages.length})`);
return createSuccess(undefined);
}
catch (error) {
return createFailure(createMemoryStorageError('append messages', 'Redis', error));
}
};
const findConversations = async (query) => {
const client = ensureConnected();
try {
// Get all conversation keys
const pattern = query.userId
? getUserKey(query.userId)
: `${fullConfig.keyPrefix}*`;
const keys = await client.keys(pattern);
const results = [];
// Fetch conversations in parallel
const conversations = await Promise.all(keys.map(async (key) => {
try {
const value = await client.get(key);
return value ? JSON.parse(value) : null;
}
catch {
return null; // Skip malformed entries
}
}));
// Filter and process results
for (const conversation of conversations) {
if (!conversation)
continue;
// Convert date strings back to Date objects
const convertedConversation = {
...conversation,
metadata: conversation.metadata ? {
...conversation.metadata,
createdAt: conversation.metadata.createdAt ? new Date(conversation.metadata.createdAt) : new Date(),
updatedAt: conversation.metadata.updatedAt ? new Date(conversation.metadata.updatedAt) : new Date(),
lastActivity: conversation.metadata.lastActivity ? new Date(conversation.metadata.lastActivity) : new Date()
} : undefined
};
// Apply filters
let matches = true;
if (query.conversationId && convertedConversation.conversationId !== query.conversationId) {
matches = false;
}
if (query.traceId && convertedConversation.metadata?.traceId !== query.traceId) {
matches = false;
}
if (query.since && convertedConversation.metadata?.createdAt && convertedConversation.metadata.createdAt < query.since) {
matches = false;
}
if (query.until && convertedConversation.metadata?.createdAt && convertedConversation.metadata.createdAt > query.until) {
matches = false;
}
if (matches) {
results.push(convertedConversation);
}
}
// Sort by last activity (most recent first)
results.sort((a, b) => {
const aTime = a.metadata?.lastActivity?.getTime() || 0;
const bTime = b.metadata?.lastActivity?.getTime() || 0;
return bTime - aTime;
});
// Apply pagination
const offset = query.offset || 0;
const limit = query.limit || results.length;
const paginatedResults = results.slice(offset, offset + limit);
safeConsole.log(`[MEMORY:Redis] Found ${paginatedResults.length} conversations matching query`);
return createSuccess(paginatedResults);
}
catch (error) {
return createFailure(createMemoryStorageError('find conversations', 'Redis', error));
}
};
const getRecentMessages = async (conversationId, limit = 50) => {
const conversationResult = await getConversation(conversationId);
if (!conversationResult.success) {
return conversationResult;
}
if (!conversationResult.data) {
return createSuccess([]);
}
const messages = conversationResult.data.messages.slice(-limit);
safeConsole.log(`[MEMORY:Redis] Retrieved ${messages.length} recent messages for conversation ${conversationId}`);
return createSuccess(messages);
};
const deleteConversation = async (conversationId) => {
const client = ensureConnected();
try {
const key = getKey(conversationId);
const deleted = await client.del(key);
safeConsole.log(`[MEMORY:Redis] ${deleted > 0 ? 'Deleted' : 'Attempted to delete non-existent'} conversation ${conversationId}`);
return createSuccess(deleted > 0);
}
catch (error) {
return createFailure(createMemoryStorageError('delete conversation', 'Redis', error));
}
};
const clearUserConversations = async (userId) => {
const client = ensureConnected();
try {
const pattern = getUserKey(userId);
const keys = await client.keys(pattern);
if (keys.length === 0) {
return createSuccess(0);
}
// Delete in batches to avoid blocking Redis
let deletedCount = 0;
const batchSize = 100;
for (let i = 0; i < keys.length; i += batchSize) {
const batch = keys.slice(i, i + batchSize);
const results = await Promise.all(batch.map(key => client.del(key)));
deletedCount += results.reduce((sum, result) => sum + result, 0);
}
safeConsole.log(`[MEMORY:Redis] Cleared ${deletedCount} conversations for user ${userId}`);
return createSuccess(deletedCount);
}
catch (error) {
return createFailure(createMemoryStorageError('clear user conversations', 'Redis', error));
}
};
const getStats = async (userId) => {
const client = ensureConnected();
try {
const pattern = userId
? getUserKey(userId)
: `${fullConfig.keyPrefix}*`;
const keys = await client.keys(pattern);
let totalConversations = 0;
let totalMessages = 0;
let oldestDate;
let newestDate;
// Process conversations in batches
const batchSize = 50;
for (let i = 0; i < keys.length; i += batchSize) {
const batch = keys.slice(i, i + batchSize);
const conversations = await Promise.all(batch.map(async (key) => {
try {
const value = await client.get(key);
return value ? JSON.parse(value) : null;
}
catch {
return null;
}
}));
for (const conversation of conversations) {
if (!conversation)
continue;
totalConversations++;
totalMessages += conversation.messages.length;
const createdAt = conversation.metadata?.createdAt
? new Date(conversation.metadata.createdAt)
: undefined;
if (createdAt) {
if (!oldestDate || createdAt < oldestDate) {
oldestDate = createdAt;
}
if (!newestDate || createdAt > newestDate) {
newestDate = createdAt;
}
}
}
}
return createSuccess({
totalConversations,
totalMessages,
oldestConversation: oldestDate,
newestConversation: newestDate
});
}
catch (error) {
return createFailure(createMemoryStorageError('get stats', 'Redis', error));
}
};
const healthCheck = async () => {
const start = Date.now();
try {
const client = ensureConnected();
await client.ping();
// Test basic operations
const testId = `health-check-${Date.now()}`;
const testKey = getKey(testId);
await client.set(testKey, JSON.stringify({ test: true }));
await client.get(testKey);
await client.del(testKey);
const latencyMs = Date.now() - start;
return createSuccess({ healthy: true, latencyMs });
}
catch (error) {
return createSuccess({
healthy: false,
latencyMs: Date.now() - start,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
};
const close = async () => {
try {
if (redisClient) {
safeConsole.log('[MEMORY:Redis] Closing Redis connection');
await redisClient.quit();
}
return createSuccess(undefined);
}
catch (error) {
return createFailure(createMemoryStorageError('close connection', 'Redis', error));
}
};
return {
storeMessages,
getConversation,
appendMessages,
findConversations,
getRecentMessages,
deleteConversation,
clearUserConversations,
getStats,
healthCheck,
close
};
}
//# sourceMappingURL=redis.js.map