logo

@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:

OptionTypeDefaultDescription
maxSizenumber100Maximum cached results before eviction
ttlnumber300000Cache 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)  // 2
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)  // 2

With 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 ones
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 ones

Handling 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
    }
}