@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, plus decorator-specific key generation options:
| Option | Type | Default | Description |
|---|---|---|---|
maxSize | number | 100 | Maximum cached results before eviction |
ttl | number | 300000 | Cache duration in milliseconds |
keyGenerator | (args: any[]) => string | — | Custom function to generate cache keys from method arguments |
hashKeys | boolean | false | Use FNV-1a hashing on serialized arguments for shorter, fixed-length keys |
When both
keyGeneratorandhashKeysare set,keyGeneratortakes precedence.
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)LRU Eviction
When the cache reaches maxSize, the least recently used 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 least recently used 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 least recently used 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
}
}Custom Key Generator
Use the keyGenerator option to fully control how cache keys are derived from method arguments. This is useful when you want to cache based on specific properties of complex objects, or when JSON.stringify doesn’t produce the desired key.
class UserService {
// Cache by user ID only, ignoring other properties
@cached<string>({ keyGenerator: (args) => args[0].id })
getDisplayName(user: { id: string; name: string; updatedAt: Date }): string {
return computeDisplayName(user)
}
// Combine multiple arguments into a custom key
@cached<SearchResult[]>({
keyGenerator: (args) => `${args[0]}-${[...args[1]].sort().join(',')}`
})
search(query: string, tags: string[]): SearchResult[] {
return performSearch(query, tags)
}
// Ignore all arguments (constant cache key)
@cached<Config>({ keyGenerator: () => 'config' })
getConfig(source: string): Config {
return loadConfig(source)
}
}class UserService {
// Cache by user ID only, ignoring other properties
@cached<string>({ keyGenerator: (args) => args[0].id })
getDisplayName(user: { id: string; name: string; updatedAt: Date }): string {
return computeDisplayName(user)
}
// Combine multiple arguments into a custom key
@cached<SearchResult[]>({
keyGenerator: (args) => `${args[0]}-${[...args[1]].sort().join(',')}`
})
search(query: string, tags: string[]): SearchResult[] {
return performSearch(query, tags)
}
// Ignore all arguments (constant cache key)
@cached<Config>({ keyGenerator: () => 'config' })
getConfig(source: string): Config {
return loadConfig(source)
}
}Hashed Keys
Use the hashKeys option when you want shorter, fixed-length cache keys. This is particularly useful when arguments are large or deeply nested objects that would produce very long JSON.stringify output.
class AnalyticsService {
@cached<Report>({ hashKeys: true, ttl: 60000 })
generateReport(filters: ComplexFilterObject): Report {
return buildReport(filters)
}
}
class DataService {
@cached<Result>({ hashKeys: true, maxSize: 500 })
process(largePayload: Record<string, unknown>): Result {
return expensiveComputation(largePayload)
}
}class AnalyticsService {
@cached<Report>({ hashKeys: true, ttl: 60000 })
generateReport(filters: ComplexFilterObject): Report {
return buildReport(filters)
}
}
class DataService {
@cached<Result>({ hashKeys: true, maxSize: 500 })
process(largePayload: Record<string, unknown>): Result {
return expensiveComputation(largePayload)
}
}Hashed keys use FNV-1a 32-bit hashing internally — a fast, pure-JS hash that works in both browser and Node environments. Note that 32-bit hashes have a theoretical collision risk with very large key sets (~77K+ unique argument combinations). For most caching use cases this is not a concern, but if you need guaranteed uniqueness with a very large number of distinct keys, use keyGenerator instead.
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
}
}