A transparent caching proxy for any object using Keyv - automatically cache method calls with TTL support
npm install keyv-cache-proxyA transparent caching proxy for any object using Keyv - automatically cache method calls with TTL support.
- 🚀 Zero-config caching: Wrap any object to automatically cache all method calls
- ⏱️ TTL support: Set time-to-live for cached values
- 🔑 Flexible storage: Use any Keyv-compatible storage adapter
- 🎯 Deep proxy: Automatically handles nested objects
- 📊 Cache observability: Optional hooks for monitoring and modifying cached/fetched data
- 🔄 Async-first: Automatically converts all methods to async
``bash`
bun add keyv-cache-proxy keyv
Or with npm/yarn/pnpm:
`bash`
npm install keyv-cache-proxy keyv
`typescript
import { KeyvCacheProxy } from 'keyv-cache-proxy';
import Keyv from 'keyv';
// Create a Keyv instance with any storage backend
const store = new Keyv();
// Wrap any object with caching
const cachedAPI = KeyvCacheProxy({
store,
})(yourAPIClient);
// All method calls are now automatically cached!
const result = await cachedAPI.fetchData('param1', 'param2');
`
`typescript
import { KeyvCacheProxy } from 'keyv-cache-proxy';
import Keyv from 'keyv';
const myObject = {
expensiveOperation(a: number, b: number) {
console.log('Computing...');
return a + b;
}
};
const cached = KeyvCacheProxy({
store: new Keyv(),
ttl: 60000, // 1 minute
})(myObject);
// First call: executes the method
await cached.expensiveOperation(1, 2); // Logs: "Computing..."
// Second call: returns cached result
await cached.expensiveOperation(1, 2); // No log, returns from cache
`
`typescript
import { Octokit } from 'octokit';
import { KeyvCacheProxy } from 'keyv-cache-proxy';
import Keyv from 'keyv';
import KeyvNedbStore from 'keyv-nedb-store';
// Use persistent storage
const kv = new Keyv(new KeyvNedbStore('.cache/github.yaml'));
const gh = KeyvCacheProxy({
store: kv,
ttl: 600000, // 10 minutes
prefix: 'github.',
onCached: (key, value) => console.log('Cache hit:', key),
onFetched: (key, value) => console.log('Fetched fresh:', key),
})(new Octokit().rest);
// API calls are now cached
const repo = await gh.repos.get({ owner: 'snomiao', repo: 'keyv-cache-proxy' });
`
`typescript
import { Client } from '@notionhq/client';
import { KeyvCacheProxy } from 'keyv-cache-proxy';
import Keyv from 'keyv';
const notion = new Client({ auth: process.env.NOTION_API_KEY });
// Cache Notion API calls to reduce rate limiting
const cachedNotion = KeyvCacheProxy({
store: new Keyv(),
ttl: 300000, // 5 minutes
prefix: 'notion.',
})(notion);
// These calls will be cached
const database = await cachedNotion.databases.query({
database_id: 'your-database-id',
});
const page = await cachedNotion.pages.retrieve({
page_id: 'your-page-id',
});
`
`typescript
import { WebClient } from '@slack/web-api';
import { KeyvCacheProxy } from 'keyv-cache-proxy';
import Keyv from 'keyv';
const slack = new WebClient(process.env.SLACK_TOKEN);
// Cache Slack API calls
const cachedSlack = KeyvCacheProxy({
store: new Keyv(),
ttl: 600000, // 10 minutes
prefix: 'slack.',
})(slack);
// Cached API calls
const channels = await cachedSlack.conversations.list();
const userInfo = await cachedSlack.users.info({ user: 'U123456' });
const messages = await cachedSlack.conversations.history({
channel: 'C123456',
});
`
`typescript
import { KeyvCacheProxy } from 'keyv-cache-proxy';
import Keyv from 'keyv';
import KeyvRedis from '@keyv/redis';
import KeyvMongo from '@keyv/mongo';
// Redis
const redisCache = KeyvCacheProxy({
store: new Keyv(new KeyvRedis('redis://localhost:6379')),
ttl: 3600000,
})(yourObject);
// MongoDB
const mongoCache = KeyvCacheProxy({
store: new Keyv(new KeyvMongo('mongodb://localhost:27017')),
ttl: 3600000,
})(yourObject);
// SQLite (via keyv-sqlite)
const sqliteCache = KeyvCacheProxy({
store: new Keyv('sqlite://cache.db'),
ttl: 3600000,
})(yourObject);
`
Track cache performance:
`typescript
let hits = 0;
let fetches = 0;
const cached = KeyvCacheProxy({
store: new Keyv(),
ttl: 60000,
onCached: (key, value) => {
hits++;
console.log(Cache hit for ${key}. Total hits: ${hits});Fetched fresh for ${key}. Total fetches: ${fetches}
},
onFetched: (key, value) => {
fetches++;
console.log();`
},
})(myObject);
Modify cached/fetched data:
`typescript`
const cached = KeyvCacheProxy({
store: new Keyv(),
ttl: 60000,
// Add metadata to cached data (called on every invocation)
onCached: (key, value) => {
if (value !== undefined) {
console.log('Returning cached data');
return { data: { ...value, fromCache: true, cachedAt: Date.now() } };
}
},
// Transform fetched data before caching
onFetched: (key, value) => {
console.log('Processing fresh data');
return { data: { ...value, fetchedAt: Date.now(), processed: true } };
},
})(myObject);
Force cache refresh:
`typescript`
const cached = KeyvCacheProxy({
store: new Keyv(),
ttl: 60000,
onCached: (key, value) => {
// Return { skip: true } to force refetch even if cached
if (value && isStale(value)) {
return { skip: true }; // Forces cache miss and refetch
}
// Return undefined to use cached value
},
})(myObject);
Custom TTL per request:
`typescript`
const cached = KeyvCacheProxy({
store: new Keyv(),
ttl: 60000, // Default 1 minute
onFetched: (key, value) => {
// Cache user data longer than other data
if (key.includes('user')) {
return { data: value, ttl: 3600000 }; // 1 hour
}
return { data: value }; // Use default TTL
},
})(myObject);
Creates a cache proxy factory function.
#### Options
- store (required): A Keyv instance for cache storage
- ttl (optional): Time-to-live for cached entries in milliseconds (can be overridden per request via onFetched)prefix
- (optional): Prefix for cache keys (default: "")onCached
- (optional): Hook called on every invocation. Receives key and cached value (or undefined on cache miss).undefined
- Return → Use original cached value{ skip: true }
- Return → Treat as cache miss and refetch{ data:
- Return → Return modified cached value(key: string, value: any) => { data?: any } | { skip: true } | undefined | Promise<...>
- Signature: onFetched
- (optional): Hook called when data is freshly fetched (cache miss). Receives key and fetched value.undefined
- Return → Cache original fetched value with default TTL{}
- Return → Cache original fetched value with default TTL (same as undefined){ data:
- Return → Cache modified value{ data:
- Return → Cache modified value with custom TTL{ skip: true }
- Return → Skip caching but still return the fetched value(key: string, value: any) => { data?: any, ttl?: number } | { skip: true } | undefined | Promise<...>
- Signature:
#### Returns
A function that takes an object and returns a proxied version with automatic caching.
Cache keys are generated based on:
- Method name
- Arguments (JSON stringified)
- Prefix (if provided)
Format: ${prefix}${methodName}:${JSON.stringify(args)}
The proxy preserves TypeScript types and automatically converts all methods to async:
`typescript`
type DeepAsyncMethod
[K in keyof T]: T[K] extends (...args: infer A) => infer R
? (...args: A) => Promise
: T[K] extends object
? DeepAsyncMethod
: T[K];
};
The KeyvCacheProxy uses JavaScript Proxy to intercept method calls:
1. When a method is called, it generates a cache key from the method name and arguments
2. Checks the Keyv store for an existing cached result
3. If found (cache hit), returns the cached value
4. If not found (cache miss), executes the original method
5. Stores the result in the cache with the specified TTL
6. Returns the result
Nested objects are automatically wrapped with the same caching behavior.
You can use any Keyv-compatible storage adapter:
- In-memory (default): new Keyv()`
- Redis: @keyv/redis
- MongoDB: @keyv/mongo
- SQLite: @keyv/sqlite
- PostgreSQL: @keyv/postgres
- MySQL: @keyv/mysql
- NeDB: keyv-nedb-store
MIT © snomiao
Contributions are welcome! Please feel free to submit a Pull Request.
- keyv - Simple key-value storage with support for multiple backends
- keyv-nedb-store - NeDB storage adapter for Keyv