or run

tessl search
Log in

Version

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
npmpkg:npm/cache-manager@7.2.x

docs

index.md
tile.json

tessl/npm-cache-manager

tessl install tessl/npm-cache-manager@7.2.0

Cache Manager for Node.js with support for multi-store caching, background refresh, and Keyv-compatible storage adapters

legacy-adapter.mddocs/reference/

Legacy Adapter

The KeyvAdapter class enables backward compatibility with cache-manager v5 storage adapters, allowing them to be used with cache-manager v7 through the Keyv interface.

Capabilities

KeyvAdapter Class

Wraps a legacy cache-manager v5 storage adapter to make it compatible with Keyv, enabling its use with cache-manager v7.

/**
 * Adapter class to wrap legacy cache-manager v5 storage adapters
 * Implements KeyvStoreAdapter interface for compatibility with Keyv
 */
class KeyvAdapter implements KeyvStoreAdapter {
  /**
   * Create a KeyvAdapter instance
   * @param store - Legacy CacheManagerStore to wrap
   */
  constructor(store: CacheManagerStore);

  /**
   * Options object (required by KeyvStoreAdapter interface)
   */
  opts: any;

  /**
   * Namespace (required by KeyvStoreAdapter interface)
   */
  namespace?: string | undefined;

  /**
   * Get a value from the legacy store
   * @param key - Cache key to retrieve
   * @returns Stored value or undefined if not found
   */
  get<T>(key: string): Promise<StoredData<T> | undefined>;

  /**
   * Set a value in the legacy store
   * @param key - Cache key
   * @param value - Value to store
   * @param ttl - Optional TTL in milliseconds
   * @returns true on success
   */
  set(key: string, value: any, ttl?: number): Promise<boolean>;

  /**
   * Delete a key from the legacy store
   * @param key - Cache key to delete
   * @returns true on success
   */
  delete(key: string): Promise<boolean>;

  /**
   * Clear all entries from the legacy store
   */
  clear(): Promise<void>;

  /**
   * Check if a key exists in the legacy store
   * @param key - Cache key to check
   * @returns true if key exists
   */
  has?(key: string): Promise<boolean>;

  /**
   * Get multiple values from the legacy store
   * @param keys - Array of cache keys to retrieve
   * @returns Array of stored values (undefined for missing keys)
   */
  getMany?<T>(keys: string[]): Promise<Array<StoredData<T | undefined>>>;

  /**
   * Delete multiple keys from the legacy store
   * @param keys - Array of cache keys to delete
   * @returns true on success
   */
  deleteMany?(keys: string[]): Promise<boolean>;

  /**
   * Register event listener on the legacy store
   * @param event - Event name
   * @param listener - Event callback
   * @returns this for chaining
   */
  on(event: string, listener: (...arguments_: any[]) => void): this;

  /**
   * Disconnect from the legacy store
   */
  disconnect?(): Promise<void>;
}

Behavioral Details:

  • Method Translation: Translates Keyv API calls to legacy store API calls
  • Return Value Normalization: Normalizes return values to match Keyv expectations
  • Error Handling: Propagates errors from legacy store
  • Event Forwarding: Forwards events from legacy store if supported
  • Type Safety: Preserves generic type parameters where possible

Constructor Parameters:

  • store - Legacy CacheManagerStore instance to wrap. Must implement the CacheManagerStore interface.

Throws:

  • Constructor throws if store is null/undefined or doesn't implement required methods

CacheManagerStore Interface

Interface definition for legacy cache-manager v5 storage adapters. Any storage adapter implementing this interface can be used with KeyvAdapter.

/**
 * Interface for legacy cache-manager v5 storage adapters
 */
type CacheManagerStore = {
  /** Store name identifier */
  name: string;

  /** Optional function to determine if a value is cacheable */
  isCacheable?: (value: unknown) => boolean;

  /**
   * Get a value from the store
   * @param key - Cache key to retrieve
   * @returns Stored value or null/undefined if not found
   */
  get(key: string): Promise<any>;

  /**
   * Get multiple values from the store
   * @param keys - Cache keys to retrieve (variadic arguments)
   * @returns Array of stored values
   */
  mget(...keys: string[]): Promise<unknown[]>;

  /**
   * Set a value in the store
   * @param key - Cache key
   * @param value - Value to store
   * @param ttl - Optional TTL in milliseconds
   * @returns Stored value or void
   */
  set(key: string, value: any, ttl?: number): Promise<any>;

  /**
   * Set multiple values in the store
   * @param data - Object mapping keys to values
   * @param ttl - Optional TTL in milliseconds
   */
  mset(data: Record<string, any>, ttl?: number): Promise<void>;

  /**
   * Delete a key from the store
   * @param key - Cache key to delete
   */
  del(key: string): Promise<void>;

  /**
   * Delete multiple keys from the store
   * @param keys - Cache keys to delete (variadic arguments)
   */
  mdel(...keys: string[]): Promise<void>;

  /**
   * Get or set TTL for a key
   * @param key - Cache key
   * @param ttl - Optional new TTL value
   * @returns Current or updated TTL
   */
  ttl(key: string, ttl?: number): Promise<number>;

  /**
   * Get all keys in the store
   * @returns Array of cache keys
   */
  keys(): Promise<string[]>;

  /**
   * Optional method to clear/reset the store
   */
  reset?(): Promise<void>;

  /**
   * Optional method to register event listeners
   * @param event - Event name
   * @param listener - Event callback
   */
  on?(event: string, listener: (...arguments_: any[]) => void): void;

  /**
   * Optional method to disconnect from the store
   */
  disconnect?(): Promise<void>;
};

Interface Details:

  • Required Methods: name, get, mget, set, mset, del, mdel, ttl, keys
  • Optional Methods: isCacheable, reset, on, disconnect
  • Variadic Parameters: mget and mdel use variadic parameters (...keys) unlike Keyv's array parameters
  • Return Types: Some methods may return any or void, adapter normalizes these

Usage Examples

Using cache-manager-redis-yet (Legacy Adapter)

import { createCache, KeyvAdapter } from 'cache-manager';
import { Keyv } from 'keyv';
import { redisStore } from 'cache-manager-redis-yet';

// Create legacy Redis store
const legacyRedisStore = await redisStore({
  socket: {
    host: 'localhost',
    port: 6379,
  },
  password: 'optional-password',
  database: 0,
  ttl: 60000, // Default TTL
});

// Wrap with KeyvAdapter
const adapter = new KeyvAdapter(legacyRedisStore);

// Create Keyv instance with the adapter
const keyv = new Keyv({ store: adapter });

// Create cache with the adapted store
const cache = createCache({ stores: [keyv] });

// Use cache normally
await cache.set('user:123', { name: 'Alice', role: 'admin' }, 60000);
const user = await cache.get('user:123');
console.log(user); // { name: 'Alice', role: 'admin' }

// Disconnect when done
await cache.disconnect();

With Configuration:

import { createCache, KeyvAdapter } from 'cache-manager';
import { Keyv } from 'keyv';
import { redisStore } from 'cache-manager-redis-yet';

// Detailed Redis configuration
const legacyRedis = await redisStore({
  socket: {
    host: process.env.REDIS_HOST || 'localhost',
    port: parseInt(process.env.REDIS_PORT || '6379'),
    connectTimeout: 5000,
    reconnectStrategy: (retries) => Math.min(retries * 50, 500),
  },
  password: process.env.REDIS_PASSWORD,
  database: 0,
  commandsQueueMaxLength: 1000,
  ttl: 300000, // 5 minutes default
});

// Connection event handling
legacyRedis.on?.('connect', () => {
  console.log('Connected to Redis');
});

legacyRedis.on?.('error', (error) => {
  console.error('Redis error:', error);
});

// Wrap and use
const adapter = new KeyvAdapter(legacyRedis);
const keyv = new Keyv({ store: adapter, namespace: 'myapp' });
const cache = createCache({ 
  stores: [keyv],
  ttl: 300000,
});

// Graceful shutdown
process.on('SIGTERM', async () => {
  await cache.disconnect();
});

Multi-Tier with Legacy Adapter

import { createCache, KeyvAdapter } from 'cache-manager';
import { Keyv } from 'keyv';
import { redisStore } from 'cache-manager-redis-yet';
import { CacheableMemory } from 'cacheable';

// Modern in-memory store (Keyv-native)
const memoryStore = new Keyv({
  store: new CacheableMemory({ ttl: 60000, lruSize: 1000 }),
});

// Legacy Redis store (via adapter)
const legacyRedis = await redisStore({
  socket: {
    host: 'localhost',
    port: 6379,
  },
  ttl: 3600000, // 1 hour
});
const redisAdapter = new KeyvAdapter(legacyRedis);
const redisStore = new Keyv({ store: redisAdapter });

// Create multi-tier cache
const cache = createCache({
  stores: [memoryStore, redisStore],
  ttl: 300000, // 5 minutes default
  refreshThreshold: 60000, // Refresh at 1 minute remaining
  refreshAllStores: true,
});

await cache.set('product:456', { name: 'Widget', price: 29.99 });
// Stored in both memory and Redis

const product = await cache.get('product:456');
// Retrieved from memory (fastest), falls back to Redis if needed

Performance Optimization:

import { createCache, KeyvAdapter } from 'cache-manager';
import { Keyv } from 'keyv';
import { redisStore } from 'cache-manager-redis-yet';
import { CacheableMemory } from 'cacheable';

// Small L1 cache for hot data
const l1 = new Keyv({
  store: new CacheableMemory({ 
    ttl: 60000,      // 1 minute
    lruSize: 500,    // Small, hot data only
  }),
});

// Large L2 cache for warm data
const l2Redis = await redisStore({
  socket: { host: 'localhost', port: 6379 },
  ttl: 3600000, // 1 hour
});

const l2 = new Keyv({ store: new KeyvAdapter(l2Redis) });

// Very large L3 cache for cold data (long-term storage)
const l3Redis = await redisStore({
  socket: { host: 'localhost', port: 6380 }, // Different instance
  ttl: 86400000, // 24 hours
});

const l3 = new Keyv({ store: new KeyvAdapter(l3Redis) });

const cache = createCache({
  stores: [l1, l2, l3],
  nonBlocking: true, // Don't wait for all stores on writes
});

// Frequently accessed data hits L1 (sub-ms)
// Less frequent data hits L2 (~1-5ms)
// Rarely accessed data hits L3 (~1-5ms)
// Cache miss fetches from source and populates all levels

Custom Legacy Store

If you have a custom storage adapter that implements the CacheManagerStore interface, you can use it with KeyvAdapter.

import { createCache, KeyvAdapter, type CacheManagerStore } from 'cache-manager';
import { Keyv } from 'keyv';

// Custom legacy store implementation
class MyCustomStore implements CacheManagerStore {
  name = 'my-custom-store';
  private data = new Map<string, { value: any; expires?: number }>();

  async get(key: string): Promise<any> {
    const item = this.data.get(key);
    if (!item) return undefined;
    if (item.expires && item.expires < Date.now()) {
      this.data.delete(key);
      return undefined;
    }
    return item.value;
  }

  async mget(...keys: string[]): Promise<unknown[]> {
    return Promise.all(keys.map(key => this.get(key)));
  }

  async set(key: string, value: any, ttl?: number): Promise<any> {
    const expires = ttl ? Date.now() + ttl : undefined;
    this.data.set(key, { value, expires });
    return value;
  }

  async mset(data: Record<string, any>, ttl?: number): Promise<void> {
    for (const [key, value] of Object.entries(data)) {
      await this.set(key, value, ttl);
    }
  }

  async del(key: string): Promise<void> {
    this.data.delete(key);
  }

  async mdel(...keys: string[]): Promise<void> {
    for (const key of keys) {
      this.data.delete(key);
    }
  }

  async ttl(key: string, ttl?: number): Promise<number> {
    const item = this.data.get(key);
    if (!item?.expires) return -1;
    if (ttl !== undefined) {
      item.expires = Date.now() + ttl;
    }
    return Math.max(0, item.expires - Date.now());
  }

  async keys(): Promise<string[]> {
    // Remove expired keys
    const now = Date.now();
    for (const [key, item] of this.data.entries()) {
      if (item.expires && item.expires < now) {
        this.data.delete(key);
      }
    }
    return Array.from(this.data.keys());
  }

  async reset(): Promise<void> {
    this.data.clear();
  }
  
  // Optional: Add metrics
  getStats() {
    return {
      size: this.data.size,
      keys: Array.from(this.data.keys()),
    };
  }
}

// Use custom store with KeyvAdapter
const customStore = new MyCustomStore();
const adapter = new KeyvAdapter(customStore);
const keyv = new Keyv({ store: adapter });
const cache = createCache({ stores: [keyv] });

// Use cache normally
await cache.set('key', 'value');
const result = await cache.get('key');
console.log(result); // 'value'

// Access custom store features
console.log(customStore.getStats());

Advanced Custom Store with Persistence:

import { createCache, KeyvAdapter, type CacheManagerStore } from 'cache-manager';
import { Keyv } from 'keyv';
import * as fs from 'fs/promises';

class FileSystemStore implements CacheManagerStore {
  name = 'filesystem';
  private cacheDir: string;
  
  constructor(cacheDir: string) {
    this.cacheDir = cacheDir;
  }

  private getFilePath(key: string): string {
    // Sanitize key for filesystem
    const sanitized = key.replace(/[^a-z0-9_-]/gi, '_');
    return `${this.cacheDir}/${sanitized}.json`;
  }

  async get(key: string): Promise<any> {
    try {
      const filePath = this.getFilePath(key);
      const data = await fs.readFile(filePath, 'utf8');
      const { value, expires } = JSON.parse(data);
      
      if (expires && expires < Date.now()) {
        await fs.unlink(filePath).catch(() => {});
        return undefined;
      }
      
      return value;
    } catch (error) {
      if ((error as any).code === 'ENOENT') {
        return undefined;
      }
      throw error;
    }
  }

  async mget(...keys: string[]): Promise<unknown[]> {
    return Promise.all(keys.map(key => this.get(key)));
  }

  async set(key: string, value: any, ttl?: number): Promise<any> {
    const filePath = this.getFilePath(key);
    const expires = ttl ? Date.now() + ttl : undefined;
    const data = JSON.stringify({ value, expires });
    
    await fs.mkdir(this.cacheDir, { recursive: true });
    await fs.writeFile(filePath, data, 'utf8');
    
    return value;
  }

  async mset(data: Record<string, any>, ttl?: number): Promise<void> {
    await Promise.all(
      Object.entries(data).map(([key, value]) => this.set(key, value, ttl))
    );
  }

  async del(key: string): Promise<void> {
    const filePath = this.getFilePath(key);
    await fs.unlink(filePath).catch(() => {});
  }

  async mdel(...keys: string[]): Promise<void> {
    await Promise.all(keys.map(key => this.del(key)));
  }

  async ttl(key: string, ttl?: number): Promise<number> {
    try {
      const filePath = this.getFilePath(key);
      const data = await fs.readFile(filePath, 'utf8');
      const parsed = JSON.parse(data);
      
      if (ttl !== undefined) {
        parsed.expires = Date.now() + ttl;
        await fs.writeFile(filePath, JSON.stringify(parsed), 'utf8');
      }
      
      if (!parsed.expires) return -1;
      return Math.max(0, parsed.expires - Date.now());
    } catch (error) {
      return -1;
    }
  }

  async keys(): Promise<string[]> {
    try {
      const files = await fs.readdir(this.cacheDir);
      return files
        .filter(f => f.endsWith('.json'))
        .map(f => f.replace('.json', ''));
    } catch (error) {
      return [];
    }
  }

  async reset(): Promise<void> {
    try {
      const files = await fs.readdir(this.cacheDir);
      await Promise.all(
        files.map(f => fs.unlink(`${this.cacheDir}/${f}`))
      );
    } catch (error) {
      // Ignore errors
    }
  }
  
  async disconnect(): Promise<void> {
    // Cleanup if needed
  }
}

// Usage
const fsStore = new FileSystemStore('./cache');
const adapter = new KeyvAdapter(fsStore);
const keyv = new Keyv({ store: adapter });
const cache = createCache({ stores: [keyv] });

Event Forwarding

The KeyvAdapter forwards events from the legacy store if it supports them.

import { createCache, KeyvAdapter } from 'cache-manager';
import { Keyv } from 'keyv';
import { redisStore } from 'cache-manager-redis-yet';

const legacyStore = await redisStore({
  socket: { host: 'localhost', port: 6379 },
});

const adapter = new KeyvAdapter(legacyStore);

// Listen to events on the adapter
adapter.on('error', (error) => {
  console.error('Store error:', error);
});

adapter.on('connect', () => {
  console.log('Connected to store');
});

adapter.on('disconnect', () => {
  console.log('Disconnected from store');
});

adapter.on('reconnecting', () => {
  console.log('Reconnecting to store');
});

const keyv = new Keyv({ store: adapter });
const cache = createCache({ stores: [keyv] });

// Store events bubble up
cache.on('set', ({ key, error }) => {
  if (error) {
    console.error(`Cache set failed for ${key}:`, error);
  }
});

Event Monitoring:

import { createCache, KeyvAdapter } from 'cache-manager';
import { Keyv } from 'keyv';
import { redisStore } from 'cache-manager-redis-yet';

const legacyStore = await redisStore();
const adapter = new KeyvAdapter(legacyStore);

// Connection state tracking
let isConnected = false;
let reconnectCount = 0;

adapter.on('connect', () => {
  isConnected = true;
  console.log('Cache store connected');
});

adapter.on('disconnect', () => {
  isConnected = false;
  console.warn('Cache store disconnected');
});

adapter.on('reconnecting', () => {
  reconnectCount++;
  console.log(`Cache store reconnecting (attempt ${reconnectCount})`);
  
  if (reconnectCount > 10) {
    console.error('ALERT: Too many reconnection attempts!');
  }
});

adapter.on('error', (error) => {
  console.error('Cache store error:', error);
  
  // Decide whether to continue or fail
  if (!isConnected) {
    console.error('Cache unavailable, falling back to direct DB access');
  }
});

// Health check
setInterval(() => {
  console.log(`Cache health: ${isConnected ? 'OK' : 'DOWN'}, reconnects: ${reconnectCount}`);
}, 60000);

Adapter Behavior

Method Mapping

The KeyvAdapter maps Keyv methods to legacy store methods:

Keyv MethodLegacy Store MethodNotes
get(key)get(key)Direct mapping, normalizes null to undefined
set(key, value, ttl)set(key, value, ttl)Direct mapping, returns true on success
delete(key)del(key)Method name difference (delete → del)
clear()reset()Method name difference, optional method
has(key)get(key)Implemented via get, checks if result is truthy
getMany(keys)mget(...keys)Spread operator converts array to variadic args
deleteMany(keys)mdel(...keys)Spread operator converts array to variadic args
disconnect()disconnect()Direct mapping, optional method

Detailed Mapping Behavior:

// get: normalizes null to undefined
// Legacy: returns null for miss
// Keyv: expects undefined for miss
const value = await adapter.get('key');
// Legacy store returns null → adapter returns undefined

// set: normalizes return value to boolean
// Legacy: may return value or void
// Keyv: expects boolean
const success = await adapter.set('key', 'value', 60000);
// Always returns true on success

// delete: wraps del and returns boolean
// Legacy: returns void
// Keyv: expects boolean
const deleted = await adapter.delete('key');
// Returns true (void → true conversion)

// clear: calls reset if available
// Legacy: reset() is optional
// Keyv: clear() should always work
await adapter.clear();
// Calls store.reset() if available, otherwise throws

// getMany: converts array to variadic
// Legacy: mget(...keys: string[])
// Keyv: getMany(keys: string[])
const values = await adapter.getMany(['a', 'b', 'c']);
// Calls store.mget('a', 'b', 'c')

// deleteMany: converts array to variadic
// Legacy: mdel(...keys: string[])
// Keyv: deleteMany(keys: string[])
const deleted = await adapter.deleteMany(['a', 'b', 'c']);
// Calls store.mdel('a', 'b', 'c')

Return Value Normalization

  • Legacy store get() returns null or undefined for missing keys; adapter normalizes to undefined
  • Legacy store set() may return various values; adapter always returns true on success
  • Legacy store del() returns void; adapter returns true on success
  • Legacy store mget() returns array with null for misses; adapter normalizes to undefined

Normalization Examples:

// Example legacy store behavior
class ExampleLegacyStore implements CacheManagerStore {
  name = 'example';
  
  async get(key: string): Promise<any> {
    // Legacy returns null for miss
    return null;
  }
  
  async set(key: string, value: any): Promise<any> {
    // Legacy may return the value
    return value;
  }
  
  async del(key: string): Promise<void> {
    // Legacy returns void
  }
  
  async mget(...keys: string[]): Promise<unknown[]> {
    // Legacy returns array with nulls
    return [null, 'value', null];
  }
  
  // ... other methods
}

// After wrapping with KeyvAdapter
const adapter = new KeyvAdapter(exampleStore);

const value = await adapter.get('missing');
// undefined (not null)

const success = await adapter.set('key', 'value');
// true (not 'value')

const deleted = await adapter.delete('key');
// true (not void)

const values = await adapter.getMany(['a', 'b', 'c']);
// [undefined, 'value', undefined] (nulls → undefined)

Optional Methods

Some legacy stores may not implement all methods. The adapter handles this gracefully:

  • reset() - Called by clear() if available; throws if not available
  • has() - Implemented via get() if not available in legacy store
  • getMany() - Implemented via multiple get() calls if mget() not available
  • deleteMany() - Implemented via multiple del() calls if mdel() not available
  • disconnect() - No-op if not available in legacy store

Fallback Implementations:

// KeyvAdapter internal implementation (conceptual)
class KeyvAdapter {
  // ...
  
  async has(key: string): Promise<boolean> {
    // Legacy store may not have has() method
    // Fallback: check if get returns a value
    const value = await this.store.get(key);
    return value !== undefined && value !== null;
  }
  
  async getMany<T>(keys: string[]): Promise<Array<StoredData<T | undefined>>> {
    // If legacy store has mget, use it
    if (this.store.mget) {
      const values = await this.store.mget(...keys);
      return values.map(v => v === null ? undefined : v);
    }
    
    // Fallback: multiple get calls
    return Promise.all(keys.map(key => this.get(key)));
  }
  
  async deleteMany(keys: string[]): Promise<boolean> {
    // If legacy store has mdel, use it
    if (this.store.mdel) {
      await this.store.mdel(...keys);
      return true;
    }
    
    // Fallback: multiple del calls
    await Promise.all(keys.map(key => this.store.del(key)));
    return true;
  }
  
  async clear(): Promise<void> {
    // If legacy store has reset, use it
    if (this.store.reset) {
      await this.store.reset();
      return;
    }
    
    // Fallback: get all keys and delete them
    const keys = await this.store.keys();
    if (keys.length > 0) {
      await this.deleteMany(keys);
    }
  }
  
  async disconnect(): Promise<void> {
    // If legacy store has disconnect, use it
    if (this.store.disconnect) {
      await this.store.disconnect();
    }
    // Otherwise no-op (no error)
  }
}

Type Compatibility

The adapter maintains type safety where possible:

  • Generic type parameter T is preserved through get<T>() and getMany<T>()
  • The value parameter in set() accepts any to match legacy store flexibility
  • The StoredData<T> type from Keyv is used for return values

Type Safety Examples:

import { createCache, KeyvAdapter } from 'cache-manager';
import { Keyv } from 'keyv';

const adapter = new KeyvAdapter(legacyStore);
const keyv = new Keyv({ store: adapter });
const cache = createCache({ stores: [keyv] });

// Type-safe get
interface User {
  id: number;
  name: string;
  email: string;
}

await cache.set('user:123', { id: 123, name: 'Alice', email: 'alice@example.com' });

const user = await cache.get<User>('user:123');
if (user) {
  console.log(user.name); // TypeScript knows user has 'name' property
}

// Type-safe mget
const users = await cache.mget<User>(['user:123', 'user:456']);
users.forEach(user => {
  if (user) {
    console.log(user.email); // TypeScript knows user has 'email' property
  }
});

// Type inference
const inferredValue = await cache.get('key'); // inferred as unknown
const typedValue = await cache.get<string>('key'); // explicitly typed as string | undefined

Migration Path

The KeyvAdapter provides a migration path from cache-manager v5 to v7:

  1. Immediate Compatibility: Wrap existing v5 stores with KeyvAdapter to use with v7
  2. Gradual Migration: Mix legacy and modern stores in the same cache instance
  3. Future Transition: Migrate to native Keyv stores when ready, without changing cache usage code

Migration Strategy:

// Phase 1: Use legacy store with adapter (immediate)
import { createCache, KeyvAdapter } from 'cache-manager';
import { Keyv } from 'keyv';
import { redisStore } from 'cache-manager-redis-yet'; // v5 store

const legacyRedis = await redisStore();
const adapter = new KeyvAdapter(legacyRedis);
const keyv = new Keyv({ store: adapter });
const cache = createCache({ stores: [keyv] });

// Phase 2: Add modern store alongside legacy (gradual)
import KeyvRedis from '@keyv/redis'; // Native Keyv store

const modernRedis = new Keyv({
  store: new KeyvRedis('redis://localhost:6380'),
});

const hybridCache = createCache({
  stores: [
    keyv, // Legacy adapter
    modernRedis, // Native Keyv store
  ],
});

// Phase 3: Replace legacy with modern (complete)
const modernCache = createCache({
  stores: [modernRedis], // Only modern stores
});

// Application code remains unchanged throughout migration!
await modernCache.set('key', 'value');
await modernCache.get('key');

Version-Specific Migration:

// Detect which version is available
let cache;

try {
  // Try v7 approach
  const { createCache } = await import('cache-manager');
  const KeyvRedis = (await import('@keyv/redis')).default;
  
  cache = createCache({
    stores: [
      new Keyv({ store: new KeyvRedis('redis://localhost:6379') }),
    ],
  });
  
  console.log('Using cache-manager v7');
} catch (error) {
  // Fall back to v5
  const caching = (await import('cache-manager')).caching;
  const redisStore = (await import('cache-manager-redis-yet')).redisStore;
  
  cache = await caching({
    store: await redisStore({
      socket: { host: 'localhost', port: 6379 },
    }),
  });
  
  console.log('Using cache-manager v5');
}

// Unified interface works with both versions
await cache.set('key', 'value');
const value = await cache.get('key');

Limitations

  • Performance: May be slightly lower than native Keyv stores due to adapter overhead (typically 5-10% slower)
  • Feature Access: Some legacy store features may not be fully accessible through the Keyv interface
  • Event Forwarding: Depends on legacy store event support; not all events may be forwarded
  • isCacheable: The isCacheable function from legacy stores is not utilized by the adapter
  • Method Overhead: Additional layer of abstraction adds small latency

Performance Comparison:

// Native Keyv store
const nativeStore = new Keyv({
  store: new KeyvRedis('redis://localhost:6379'),
});

// Legacy store with adapter
const legacyStore = await redisStore({ /* config */ });
const adaptedStore = new Keyv({
  store: new KeyvAdapter(legacyStore),
});

// Benchmark (conceptual)
console.time('native-set');
for (let i = 0; i < 1000; i++) {
  await nativeStore.set(`key${i}`, `value${i}`);
}
console.timeEnd('native-set'); // e.g., 450ms

console.time('adapted-set');
for (let i = 0; i < 1000; i++) {
  await adaptedStore.set(`key${i}`, `value${i}`);
}
console.timeEnd('adapted-set'); // e.g., 495ms (~10% slower)

When to Use KeyvAdapter

Use KeyvAdapter when:

  • Migrating from cache-manager v5 to v7 and need backward compatibility
  • Using third-party stores designed for cache-manager v5 (like cache-manager-redis-yet)
  • Gradually transitioning to native Keyv stores
  • Custom stores already implement the CacheManagerStore interface
  • Need to maintain existing v5 store configuration and code

Consider native Keyv stores when:

  • Starting a new project
  • Performance is critical (every millisecond counts)
  • Need full access to Keyv features (e.g., advanced iterators)
  • Want better type safety and modern API design
  • No existing legacy store dependencies

Decision Matrix:

Use KeyvAdapter if:
✓ You have existing cache-manager v5 code
✓ You're using third-party v5 stores
✓ You need gradual migration path
✓ You have custom v5 stores
✓ Performance overhead is acceptable

Use Native Keyv stores if:
✓ You're starting from scratch
✓ You need maximum performance
✓ You want latest features
✓ You prefer modern API
✓ No legacy dependencies

External Types

// From 'keyv' package
interface KeyvStoreAdapter {
  opts: any;
  namespace?: string | undefined;
  get<T>(key: string): Promise<StoredData<T> | undefined>;
  set(key: string, value: any, ttl?: number): Promise<boolean>;
  delete(key: string): Promise<boolean>;
  clear?(): Promise<void>;
  has?(key: string): Promise<boolean>;
  getMany?<T>(keys: string[]): Promise<Array<StoredData<T | undefined>>>;
  deleteMany?(keys: string[]): Promise<boolean>;
  on?(event: string, listener: (...arguments_: any[]) => void): any;
  disconnect?(): Promise<void>;
}

type StoredData<T> = T | undefined;

Troubleshooting

Issue: "reset is not a function"

Cause: Legacy store doesn't implement reset() method, but adapter's clear() tries to call it.

Solution: Ensure legacy store implements reset(), or adapter will fall back to keys() + mdel().

Issue: Type errors with generic parameters

Cause: Legacy stores use any types, losing type safety.

Solution: Explicitly type cache operations:

const user = await cache.get<User>('user:123');

Issue: Events not firing

Cause: Legacy store doesn't support events, or events use different names.

Solution: Check legacy store documentation for event support. Not all legacy stores emit events.

Issue: Performance slower than expected

Cause: Adapter adds overhead; fallback implementations (e.g., multiple get() for getMany()) can be slow.

Solution: Ensure legacy store implements batch methods (mget, mset, mdel) for better performance.

Issue: Serialization errors

Cause: Legacy store uses different serializer than expected.

Solution: Configure Keyv serializer to match legacy store:

const keyv = new Keyv({
  store: adapter,
  serialize: JSON.stringify,
  deserialize: JSON.parse,
});