mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 09:20:15 +01:00
🔄 refactor: Migrate Cache Logic to TypeScript (#9771)
* Refactor: Moved Redis cache infra logic into `packages/api` - Moved cacheFactory and redisClients from `api/cache` into `packages/api/src/cache` so that features in `packages/api` can use cache without importing backward from the backend. - Converted all moved files into TS with proper typing. - Created integration tests to run against actual Redis servers for redisClients and cacheFactory. - Added a GitHub workflow to run integration tests for the cache feature. - Bug fix: keyvRedisClient now implements the PING feature properly. * chore: consolidate imports in getLogStores.js * chore: reorder imports * chore: re-add fs-extra as dev dep. * chore: reorder imports in cacheConfig.ts, cacheFactory.ts, and keyvMongo.ts --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
341435fb25
commit
0e5bb6f98c
38 changed files with 1552 additions and 1340 deletions
116
packages/api/src/cache/cacheFactory.ts
vendored
Normal file
116
packages/api/src/cache/cacheFactory.ts
vendored
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
/**
|
||||
* @keyv/redis exports its default class in a non-standard way:
|
||||
* module.exports = { default: KeyvRedis, ... } instead of module.exports = KeyvRedis
|
||||
* This breaks ES6 imports when the module is marked as external in rollup.
|
||||
* We must use require() to access the .default property directly.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const KeyvRedis = require('@keyv/redis').default as typeof import('@keyv/redis').default;
|
||||
import { Keyv } from 'keyv';
|
||||
import createMemoryStore from 'memorystore';
|
||||
import { RedisStore } from 'rate-limit-redis';
|
||||
import { Time } from 'librechat-data-provider';
|
||||
import { logger } from '@librechat/data-schemas';
|
||||
import session, { MemoryStore } from 'express-session';
|
||||
import { RedisStore as ConnectRedis } from 'connect-redis';
|
||||
import type { SendCommandFn } from 'rate-limit-redis';
|
||||
import { keyvRedisClient, ioredisClient, GLOBAL_PREFIX_SEPARATOR } from './redisClients';
|
||||
import { cacheConfig } from './cacheConfig';
|
||||
import { violationFile } from './keyvFiles';
|
||||
|
||||
/**
|
||||
* Creates a cache instance using Redis or a fallback store. Suitable for general caching needs.
|
||||
* @param namespace - The cache namespace.
|
||||
* @param ttl - Time to live for cache entries.
|
||||
* @param fallbackStore - Optional fallback store if Redis is not used.
|
||||
* @returns Cache instance.
|
||||
*/
|
||||
export const standardCache = (namespace: string, ttl?: number, fallbackStore?: object): Keyv => {
|
||||
if (keyvRedisClient && !cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES?.includes(namespace)) {
|
||||
try {
|
||||
const keyvRedis = new KeyvRedis(keyvRedisClient);
|
||||
const cache = new Keyv(keyvRedis, { namespace, ttl });
|
||||
keyvRedis.namespace = cacheConfig.REDIS_KEY_PREFIX;
|
||||
keyvRedis.keyPrefixSeparator = GLOBAL_PREFIX_SEPARATOR;
|
||||
|
||||
cache.on('error', (err) => {
|
||||
logger.error(`Cache error in namespace ${namespace}:`, err);
|
||||
});
|
||||
|
||||
return cache;
|
||||
} catch (err) {
|
||||
logger.error(`Failed to create Redis cache for namespace ${namespace}:`, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
if (fallbackStore) {
|
||||
return new Keyv({ store: fallbackStore, namespace, ttl });
|
||||
}
|
||||
return new Keyv({ namespace, ttl });
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a cache instance for storing violation data.
|
||||
* Uses a file-based fallback store if Redis is not enabled.
|
||||
* @param namespace - The cache namespace for violations.
|
||||
* @param ttl - Time to live for cache entries.
|
||||
* @returns Cache instance for violations.
|
||||
*/
|
||||
export const violationCache = (namespace: string, ttl?: number): Keyv => {
|
||||
return standardCache(`violations:${namespace}`, ttl, violationFile);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a session cache instance using Redis or in-memory store.
|
||||
* @param namespace - The session namespace.
|
||||
* @param ttl - Time to live for session entries.
|
||||
* @returns Session store instance.
|
||||
*/
|
||||
export const sessionCache = (namespace: string, ttl?: number): MemoryStore | ConnectRedis => {
|
||||
namespace = namespace.endsWith(':') ? namespace : `${namespace}:`;
|
||||
if (!cacheConfig.USE_REDIS) {
|
||||
const MemoryStore = createMemoryStore(session);
|
||||
return new MemoryStore({ ttl, checkPeriod: Time.ONE_DAY });
|
||||
}
|
||||
const store = new ConnectRedis({ client: ioredisClient, ttl, prefix: namespace });
|
||||
if (ioredisClient) {
|
||||
ioredisClient.on('error', (err) => {
|
||||
logger.error(`Session store Redis error for namespace ${namespace}:`, err);
|
||||
});
|
||||
}
|
||||
return store;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a rate limiter cache using Redis.
|
||||
* @param prefix - The key prefix for rate limiting.
|
||||
* @returns RedisStore instance or undefined if Redis is not used.
|
||||
*/
|
||||
export const limiterCache = (prefix: string): RedisStore | undefined => {
|
||||
if (!prefix) {
|
||||
throw new Error('prefix is required');
|
||||
}
|
||||
if (!cacheConfig.USE_REDIS) {
|
||||
return undefined;
|
||||
}
|
||||
// TODO: The prefix is not actually applied. Also needs to account for global prefix.
|
||||
prefix = prefix.endsWith(':') ? prefix : `${prefix}:`;
|
||||
|
||||
try {
|
||||
const sendCommand: SendCommandFn = (async (...args: string[]) => {
|
||||
if (ioredisClient == null) {
|
||||
throw new Error('Redis client not available');
|
||||
}
|
||||
try {
|
||||
return await ioredisClient.call(args[0], ...args.slice(1));
|
||||
} catch (err) {
|
||||
logger.error('Redis command execution failed:', err);
|
||||
throw err;
|
||||
}
|
||||
}) as SendCommandFn;
|
||||
return new RedisStore({ sendCommand, prefix });
|
||||
} catch (err) {
|
||||
logger.error(`Failed to create Redis rate limiter for prefix ${prefix}:`, err);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue