tessl install tessl/npm-cache-manager@7.2.0Cache Manager for Node.js with support for multi-store caching, background refresh, and Keyv-compatible storage adapters
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.
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:
Constructor Parameters:
store - Legacy CacheManagerStore instance to wrap. Must implement the CacheManagerStore interface.Throws:
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:
name, get, mget, set, mset, del, mdel, ttl, keysisCacheable, reset, on, disconnectmget and mdel use variadic parameters (...keys) unlike Keyv's array parametersany or void, adapter normalizes theseimport { 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();
});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 neededPerformance 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 levelsIf 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] });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);The KeyvAdapter maps Keyv methods to legacy store methods:
| Keyv Method | Legacy Store Method | Notes |
|---|---|---|
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')get() returns null or undefined for missing keys; adapter normalizes to undefinedset() may return various values; adapter always returns true on successdel() returns void; adapter returns true on successmget() returns array with null for misses; adapter normalizes to undefinedNormalization 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)Some legacy stores may not implement all methods. The adapter handles this gracefully:
reset() - Called by clear() if available; throws if not availablehas() - Implemented via get() if not available in legacy storegetMany() - Implemented via multiple get() calls if mget() not availabledeleteMany() - Implemented via multiple del() calls if mdel() not availabledisconnect() - No-op if not available in legacy storeFallback 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)
}
}The adapter maintains type safety where possible:
T is preserved through get<T>() and getMany<T>()value parameter in set() accepts any to match legacy store flexibilityStoredData<T> type from Keyv is used for return valuesType 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 | undefinedThe KeyvAdapter provides a migration path from cache-manager v5 to v7:
KeyvAdapter to use with v7Migration 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');isCacheable function from legacy stores is not utilized by the adapterPerformance 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)Use KeyvAdapter when:
cache-manager-redis-yet)CacheManagerStore interfaceConsider native Keyv stores when:
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// 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;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().
Cause: Legacy stores use any types, losing type safety.
Solution: Explicitly type cache operations:
const user = await cache.get<User>('user:123');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.
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.
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,
});