File size: 4,454 Bytes
f0743f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/**
 * @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 } 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 = cacheConfig.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;
  }
};