@cached Decorator
The @cached decorator provides automatic method-level caching (memoization). It caches method results based on their arguments, so repeated calls with the same arguments return the cached result instantly.
Usage
import { cached } from '@humanspeak/memory-cache'
class MyService {
@cached<ReturnType>(options?)
methodName(args): ReturnType {
// Method implementation
}
}import { cached } from '@humanspeak/memory-cache'
class MyService {
@cached<ReturnType>(options?)
methodName(args): ReturnType {
// Method implementation
}
}Options
The decorator accepts the same options as MemoryCache:
| Option | Type | Default | Description |
|---|---|---|---|
maxSize | number | 100 | Maximum cached results before eviction |
ttl | number | 300000 | Cache duration in milliseconds |
Basic Example
import { cached } from '@humanspeak/memory-cache'
class UserService {
callCount = 0
@cached<User>()
async getUser(id: string): Promise<User> {
this.callCount++
return await database.findUser(id)
}
}
const service = new UserService()
// First call - executes the method
await service.getUser('123')
console.log(service.callCount) // 1
// Second call - returns cached result
await service.getUser('123')
console.log(service.callCount) // Still 1!
// Different argument - executes the method
await service.getUser('456')
console.log(service.callCount) // 2import { cached } from '@humanspeak/memory-cache'
class UserService {
callCount = 0
@cached<User>()
async getUser(id: string): Promise<User> {
this.callCount++
return await database.findUser(id)
}
}
const service = new UserService()
// First call - executes the method
await service.getUser('123')
console.log(service.callCount) // 1
// Second call - returns cached result
await service.getUser('123')
console.log(service.callCount) // Still 1!
// Different argument - executes the method
await service.getUser('456')
console.log(service.callCount) // 2With Custom Options
class ApiService {
@cached<Response>({ ttl: 60000, maxSize: 50 })
async fetchData(endpoint: string): Promise<Response> {
return await fetch(endpoint)
}
}class ApiService {
@cached<Response>({ ttl: 60000, maxSize: 50 })
async fetchData(endpoint: string): Promise<Response> {
return await fetch(endpoint)
}
}Complex Arguments
The decorator serializes arguments using JSON.stringify, so it works with complex objects:
class SearchService {
@cached<SearchResult[]>({ ttl: 30000 })
async search(query: string, options: SearchOptions): Promise<SearchResult[]> {
return await searchApi.query(query, options)
}
}
const service = new SearchService()
// These are cached separately
await service.search('typescript', { limit: 10 })
await service.search('typescript', { limit: 20 })
await service.search('javascript', { limit: 10 })
// This returns the cached result
await service.search('typescript', { limit: 10 })class SearchService {
@cached<SearchResult[]>({ ttl: 30000 })
async search(query: string, options: SearchOptions): Promise<SearchResult[]> {
return await searchApi.query(query, options)
}
}
const service = new SearchService()
// These are cached separately
await service.search('typescript', { limit: 10 })
await service.search('typescript', { limit: 20 })
await service.search('javascript', { limit: 10 })
// This returns the cached result
await service.search('typescript', { limit: 10 })Async Methods
The decorator works seamlessly with async methods:
class DataService {
@cached<Promise<Data>>()
async loadData(id: string): Promise<Data> {
// The Promise is cached, not the resolved value
return await expensiveOperation(id)
}
}class DataService {
@cached<Promise<Data>>()
async loadData(id: string): Promise<Data> {
// The Promise is cached, not the resolved value
return await expensiveOperation(id)
}
}TTL Expiration
Cached results expire after the configured TTL:
class TimeSensitiveService {
@cached<number>({ ttl: 5000 }) // 5 second TTL
getTimestamp(): number {
return Date.now()
}
}
const service = new TimeSensitiveService()
const t1 = service.getTimestamp()
await sleep(2000)
const t2 = service.getTimestamp() // Same as t1 (cached)
await sleep(4000) // Total 6 seconds
const t3 = service.getTimestamp() // New value (cache expired)class TimeSensitiveService {
@cached<number>({ ttl: 5000 }) // 5 second TTL
getTimestamp(): number {
return Date.now()
}
}
const service = new TimeSensitiveService()
const t1 = service.getTimestamp()
await sleep(2000)
const t2 = service.getTimestamp() // Same as t1 (cached)
await sleep(4000) // Total 6 seconds
const t3 = service.getTimestamp() // New value (cache expired)Size-Based Eviction
When the cache reaches maxSize, the oldest entries are evicted:
class ProductService {
@cached<Product>({ maxSize: 100 })
async getProduct(id: string): Promise<Product> {
return await database.findProduct(id)
}
}
// After caching 100 different products,
// the oldest ones are evicted to make room for new onesclass ProductService {
@cached<Product>({ maxSize: 100 })
async getProduct(id: string): Promise<Product> {
return await database.findProduct(id)
}
}
// After caching 100 different products,
// the oldest ones are evicted to make room for new onesHandling Undefined and Null
The decorator properly caches methods that return undefined or null:
class LookupService {
@cached<User | null>()
findUser(id: string): User | null {
const user = database.find(id)
return user || null // null is cached
}
}class LookupService {
@cached<User | null>()
findUser(id: string): User | null {
const user = database.find(id)
return user || null // null is cached
}
}Important Notes
Argument Serialization
Arguments are serialized with JSON.stringify. This means:
- Objects with the same properties create the same cache key
- Circular references will throw an error
- Functions and symbols cannot be used as arguments
class MyService {
@cached<string>()
process(obj: { id: string }): string {
return obj.id
}
}
const service = new MyService()
// Same cache key - same object structure
service.process({ id: '123' })
service.process({ id: '123' }) // Cached
// Different cache key
service.process({ id: '456' })class MyService {
@cached<string>()
process(obj: { id: string }): string {
return obj.id
}
}
const service = new MyService()
// Same cache key - same object structure
service.process({ id: '123' })
service.process({ id: '123' }) // Cached
// Different cache key
service.process({ id: '456' })Class Instance Scope
Each class instance has its own cache. Different instances don’t share cached values:
const service1 = new UserService()
const service2 = new UserService()
await service1.getUser('123') // Cached in service1
await service2.getUser('123') // Executes again (different instance)const service1 = new UserService()
const service2 = new UserService()
await service1.getUser('123') // Cached in service1
await service2.getUser('123') // Executes again (different instance)TypeScript Decorators
Make sure you have experimentalDecorators: true in your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true
}
}{
"compilerOptions": {
"experimentalDecorators": true
}
}