@stephen-shopopop/cache
    Preparing search index...

    Class RedisCacheStore<Metadata>

    A Redis-backed cache store with optional local tracking and metadata support.

    RedisCacheStore provides a high-level interface for storing, retrieving, and deleting cache entries in Redis, with support for per-entry metadata, maximum entry size enforcement, and optional local in-memory tracking for improved performance and cache invalidation.

    • Dual Storage Architecture: Stores values and associated metadata in Redis using separate keys for data and metadata
    • Size Enforcement: Enforces a configurable maximum entry size to prevent memory issues
    • Local Tracking Cache: Optional local in-memory cache for fast lookups with Redis keyspace notifications for invalidation
    • Automatic Cleanup: Handles automatic cleanup of stale or corrupted cache entries
    • Error Resilience: Provides custom error handling via configurable error callback
    • Atomic Operations: Uses Redis pipelines for atomic set operations
    • TTL Support: Full support for time-to-live on cache entries

    The store uses a two-key approach in Redis:

    1. metadata:[key] - Hash containing JSON metadata and a UUID
    2. values:[uuid] - String containing the actual cached value

    This design allows for efficient metadata queries without loading large values, and enables atomic cleanup of both parts when entries expire.

    • Cold reads: Single Redis roundtrip (HGETALL + GET)
    • Warm reads: Zero Redis roundtrips (served from tracking cache)
    • Writes: Single pipelined Redis transaction (HSET + SET + 2x EXPIRE)
    • Memory overhead: Minimal for metadata, configurable tracking cache size

    1.0.0

    Type Parameters

    • Metadata extends object = Record<PropertyKey, unknown>

      The shape of the metadata object associated with each cache entry

    Index

    Constructors

    Methods

    Constructors

    • Creates a new instance of the RedisCacheStore.

      Type Parameters

      • Metadata extends object = Record<PropertyKey, unknown>

      Parameters

      • options: Readonly<RedisCacheStoreOptions>

        The configuration options for the RedisCacheStore

        • maxEntrySize

          The maximum size (in bytes) allowed for a single cache entry. Must be a non-negative integer. Defaults to 100MB

        • errorCallback

          A callback function to handle errors that occur during Redis operations

        • clientOpts

          Options to configure the underlying Redis client. May include keyPrefix and other Redis client options

        • clientOpts.keyPrefix

          Prefix to apply to all Redis keys for namespacing

        • maxCount

          The maximum number of entries allowed in the tracking cache (only used when tracking is enabled)

        • tracking

          If set to false, disables the local tracking cache. Defaults to true

      Returns RedisCacheStore<Metadata>

      If options is not an object

      If maxEntrySize is not a non-negative integer

      If errorCallback is not a function

      const store = new RedisCacheStore({
      clientOpts: { host: 'localhost', port: 6379 }
      });
      const store = new RedisCacheStore<MyMetadata>({
      maxEntrySize: 50 * 1024 * 1024, // 50MB
      maxCount: 5000, // Track up to 5000 entries locally
      tracking: true, // Enable local tracking (default)
      clientOpts: {
      host: 'redis.example.com',
      port: 6380,
      keyPrefix: 'myapp:',
      retryDelayOnFailover: 100
      },
      errorCallback: (err) => {
      logger.error('Redis cache error:', err);
      metrics.increment('redis.errors');
      }
      });

    Methods

    • Closes the Redis connections used by the cache store.

      This method ensures graceful shutdown by:

      1. Setting the closed flag to prevent further operations
      2. Calling quit() on all Redis clients (main and subscriber)
      3. Waiting for all connections to close properly
      4. Handling any errors during the closing process

      Once closed, the store instance should not be used for further operations. It's recommended to call this method when your application shuts down or when you're done with the cache store to prevent connection leaks.

      Returns Promise<void>

      A promise that resolves when all connections are closed

      process.on('SIGTERM', async () => {
      console.log('Shutting down gracefully...');
      await store.close();
      console.log('Cache store closed');
      process.exit(0);
      });
      const server = express();
      const store = new RedisCacheStore(options);

      // ... use store in routes ...

      server.listen(3000, () => {
      console.log('Server started');
      });

      process.on('SIGINT', async () => {
      console.log('Closing server...');
      await store.close(); // Close cache before server
      server.close();
      });
      await store.close(); // First call closes connections
      await store.close(); // Second call returns immediately
    • Deletes a cache entry and its associated metadata from Redis.

      This method performs a complete cleanup by:

      1. Fetching the metadata to get the associated value UUID
      2. Deleting both the metadata hash and the value string
      3. Removing the entry from the local tracking cache (if enabled)

      The operation is atomic - either both keys are deleted or neither is. If the metadata doesn't exist, the method returns silently without error.

      Parameters

      • key: string

        The cache key to delete

      Returns Promise<void>

      A promise that resolves when the deletion is complete

      await store.delete('user:123');
      console.log('User cache entry deleted');
      const keysToDelete = ['user:1', 'user:2', 'user:3'];
      await Promise.all(keysToDelete.map(key => store.delete(key)));
      console.log('All user entries deleted');
      // This won't throw even if 'nonexistent:key' doesn't exist
      await store.delete('nonexistent:key');
    • Internal

      Retrieves a value and its associated metadata from Redis by the provided key.

      This is the core lookup method that handles the Redis-level operations. It performs the following steps:

      1. Fetch metadata hash using HGETALL
      2. If metadata exists, fetch the actual value using the embedded UUID
      3. Handle cleanup of stale entries (metadata without corresponding values)
      4. Parse and validate metadata JSON
      5. Clean up corrupted entries and log errors appropriately

      Parameters

      • key: string

        The key to look up in the cache

      Returns Promise<undefined | { metadata: Metadata; value: string }>

      A promise that resolves to an object containing the value and its metadata, or undefined if the key is not found or an error occurs

      // Scenario 1: Metadata exists but value expired
      // Action: Delete stale metadata, return undefined

      // Scenario 2: Metadata JSON is corrupted
      // Action: Delete both metadata and value, log error, return undefined

      // Scenario 3: Redis connection error
      // Action: Call error callback, return undefined
    • Retrieves a cached value and its associated metadata by key.

      This method implements a two-tier lookup strategy:

      1. Fast path: If local tracking is enabled and contains the key, return immediately
      2. Slow path: Query Redis for the entry, then populate the tracking cache if found

      The method handles various edge cases including:

      • Missing or corrupted metadata
      • Expired values with lingering metadata
      • JSON parsing errors in metadata

      Parameters

      • key: string

        The cache key to retrieve

      Returns Promise<undefined | { metadata: Metadata; value: string }>

      A promise that resolves to an object containing the value and metadata if found, or undefined if the key does not exist or an error occurs

      const result = await store.get('user:123');
      if (result) {
      console.log('User data:', result.value);
      console.log('Metadata:', result.metadata);
      } else {
      console.log('User not found in cache');
      }
      interface UserMetadata {
      lastUpdated: number;
      version: string;
      }

      const store = new RedisCacheStore<UserMetadata>();
      const result = await store.get('user:123');

      if (result) {
      // TypeScript knows metadata has lastUpdated and version
      console.log('Last updated:', new Date(result.metadata.lastUpdated));
      console.log('Version:', result.metadata.version);
      }
    • Stores a value in the Redis cache with associated metadata and an optional time-to-live (TTL).

      This method performs atomic storage using Redis pipelines to ensure consistency. The operation includes:

      1. Validation: Checks value type, metadata object, TTL, and size constraints
      2. UUID Generation: Creates a unique identifier for value storage
      3. Atomic Storage: Uses Redis pipeline for atomic HSET + SET + 2x EXPIRE operations
      4. Size Enforcement: Prevents storage of entries exceeding maxEntrySize
      metadata:[key] → { metadata: JSON, id: UUID }  [TTL: ttl seconds]
      values:[UUID] → value [TTL: ttl seconds]

      Parameters

      • key: string

        The cache key under which the value will be stored

      • value: string | Buffer<ArrayBufferLike>

        The value to store; must be a string or Buffer

      • metadata: Metadata = ...

        Metadata object to associate with the cache entry. Defaults to empty object if not provided

      • ttl: number = 0

        Time-to-live in seconds; must be a non-negative integer. If not provided, entries will not expire

      Returns Promise<void>

      A promise that resolves when the value and metadata have been stored successfully

      If the value is not a string or Buffer

      If the metadata is not an object or is null

      If the ttl is not a non-negative integer

      If the entry size exceeds the maximum allowed size

      await store.set('user:123', JSON.stringify(userData), {
      userId: '123',
      lastUpdated: Date.now()
      });
      await store.set('session:abc', sessionData, {
      sessionId: 'abc',
      createdAt: Date.now()
      }, 3600); // Expires in 1 hour
      const imageBuffer = await fs.readFile('image.jpg');
      await store.set('image:123', imageBuffer, {
      filename: 'image.jpg',
      mimeType: 'image/jpeg',
      size: imageBuffer.length
      }, 86400); // Expires in 24 hours
      try {
      await store.set('large-file', hugeBuffer, {}, 3600);
      } catch (error) {
      if (error.message.includes('maxEntrySize')) {
      console.log('File too large for cache');
      }
      }