# @humanspeak/memory-cache — full reference > Concatenated dump of every doc page under https://memory.svelte.page/docs. > Each section is bounded by an HTML comment with the source URL, > so agents can extract individual pages or cite a specific section. --- # @cached Decorator > API reference for the @cached decorator with sync and async method caching, TTL, LRU eviction, custom key generators, and hashed key support. **Source:** [https://memory.svelte.page/docs/api/cached-decorator](https://memory.svelte.page/docs/api/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 ```typescript import { cached } from '@humanspeak/memory-cache' class MyService { @cached(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 `keyGenerator` and `hashKeys` are set, `keyGenerator` takes precedence. ## Basic Example ```typescript import { cached } from '@humanspeak/memory-cache' class UserService { callCount = 0 @cached() async getUser(id: string): Promise { 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 ```typescript class ApiService { @cached({ ttl: 60000, maxSize: 50 }) async fetchData(endpoint: string): Promise { return await fetch(endpoint) } } ``` ## Complex Arguments The decorator serializes arguments using `JSON.stringify`, so it works with complex objects: ```typescript class SearchService { @cached({ ttl: 30000 }) async search(query: string, options: SearchOptions): Promise { 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. Concurrent calls with the same arguments share one in-flight method execution, resolved values are cached, and rejected promises are not cached by default. ```typescript class DataService { @cached() async loadData(id: string): Promise { return await expensiveOperation(id) } } const service = new DataService() const [first, second] = await Promise.all([ service.loadData('popular'), service.loadData('popular') ]) // expensiveOperation ran once try { await service.loadData('temporary-error') } catch { // Rejections are not cached, so a later call can retry. } ``` ## TTL Expiration Cached results expire after the configured TTL: ```typescript class TimeSensitiveService { @cached({ 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: ```typescript class ProductService { @cached({ maxSize: 100 }) async getProduct(id: string): Promise { return await database.findProduct(id) } } // After caching 100 different products, // the least recently used ones are evicted to make room for new ones ``` ## Handling Undefined and Null The decorator properly caches methods that return `undefined` or `null`: ```typescript class LookupService { @cached() 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. ```typescript class UserService { // Cache by user ID only, ignoring other properties @cached({ 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({ 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({ 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. ```typescript class AnalyticsService { @cached({ hashKeys: true, ttl: 60000 }) generateReport(filters: ComplexFilterObject): Report { return buildReport(filters) } } class DataService { @cached({ hashKeys: true, maxSize: 500 }) process(largePayload: Record): 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 ```typescript class MyService { @cached() 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' }) ``` ### Shared Cache Across Instances All instances of a class share the same cache for a given decorated method. The cache is created once per `@cached()` declaration, so calls with the same arguments return cached results regardless of which instance makes the call: ```typescript const service1 = new UserService() const service2 = new UserService() await service1.getUser('123') // Executes the method await service2.getUser('123') // Returns cached result (shared cache) ``` This provides cross-instance deduplication — if multiple instances call the same method with the same arguments, the underlying function only executes once. Note that cache keys are based solely on arguments, with no per-instance discrimination. ### TypeScript Decorators Make sure you have `experimentalDecorators: true` in your `tsconfig.json`: ```json { "compilerOptions": { "experimentalDecorators": true } } ``` # MemoryCache API > Complete MemoryCache API reference covering constructor options, get/set/delete methods, stats, hooks, TTL, LRU eviction, and type-safe usage. **Source:** [https://memory.svelte.page/docs/api/memory-cache](https://memory.svelte.page/docs/api/memory-cache) --- The `MemoryCache` class is a generic in-memory cache with TTL expiration and LRU (Least Recently Used) eviction. ## Constructor ```typescript new MemoryCache(options?: CacheOptions) ``` ### Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `maxSize` | `number` | `100` | Maximum entries before eviction (0 = unlimited) | | `ttl` | `number` | `300000` | Time-to-live in milliseconds (0 = no expiration) | | `hooks` | `CacheHooks` | `{}` | Lifecycle hooks for observing cache events | ### Example ```typescript import { MemoryCache } from '@humanspeak/memory-cache' // Default options const cache = new MemoryCache() // Custom options const customCache = new MemoryCache({ maxSize: 1000, ttl: 5 * 60 * 1000 // 5 minutes }) // Unlimited cache (use with caution) const unlimitedCache = new MemoryCache({ maxSize: 0, // No size limit ttl: 0 // No expiration }) ``` ### Validation The constructor throws `CacheConfigError` if invalid options are provided: ```typescript import { MemoryCache, CacheConfigError } from '@humanspeak/memory-cache' try { const cache = new MemoryCache({ maxSize: -1 }) } catch (error) { if (error instanceof CacheConfigError) { console.error('Invalid config:', error.message) } } ``` ## Methods ### get(key) Retrieves a value from the cache if it exists and hasn't expired. Accessing an entry moves it to the most-recently-used position, protecting it from eviction. ```typescript get(key: string): T | undefined ``` **Parameters:** - `key` - The key to look up **Returns:** The cached value if found and valid, `undefined` otherwise **Example:** ```typescript cache.set('user:123', 'John Doe') const name = cache.get('user:123') // 'John Doe' const missing = cache.get('unknown') // undefined ``` ### getOrSet(key, fetcher) Gets a value from cache, or fetches and caches it if not present. Implements single-flight pattern to prevent multiple concurrent fetches for the same key (thundering herd prevention). ```typescript getOrSet(key: string, fetcher: () => T | Promise): Promise ``` **Parameters:** - `key` - The cache key - `fetcher` - Function that returns the value to cache (can be sync or async) **Returns:** `Promise` - The cached or fetched value **Example:** ```typescript // Basic usage with async fetcher const user = await cache.getOrSet('user:123', async () => { return await database.findUser(123) }) // Works with sync fetchers too const config = await cache.getOrSet('config', () => loadConfig()) // Concurrent requests share the same fetch const promises = Array.from({ length: 100 }, () => cache.getOrSet('popular-key', fetchExpensiveData) ) await Promise.all(promises) // fetchExpensiveData called only once ``` **Key behaviors:** - **Single-flight**: Concurrent requests for the same uncached key share one fetch - **Error handling**: Errors from the fetcher are not cached and propagate to callers - **Null/undefined**: Values including `null` and `undefined` are properly cached - **Uses instance TTL**: Cached values expire based on cache configuration ### set(key, value) Stores a value in the cache. If the cache is full and this is a new key, expired entries are pruned before the least recently used entry is evicted. Setting a value (new or update) moves the entry to the most-recently-used position. ```typescript set(key: string, value: T): void ``` **Parameters:** - `key` - The key under which to store the value - `value` - The value to cache **Example:** ```typescript cache.set('greeting', 'Hello, World!') cache.set('count', 42) cache.set('user', { name: 'John', age: 30 }) ``` When `ttl` and `maxSize` are both active, expired entries are reclaimed first: ```typescript const cache = new MemoryCache({ maxSize: 2, ttl: 1000 }) cache.set('a', 'expires') // ... 750ms pass ... cache.set('b', 'valid') // ... another 300ms pass; a expires, b is still valid ... cache.set('c', 'new') // removes expired a instead of evicting valid b ``` ### has(key) Checks if a key exists in the cache and hasn't expired. This is useful for distinguishing between cache misses and cached `undefined` values. ```typescript has(key: string): boolean ``` **Parameters:** - `key` - The key to check **Returns:** `true` if the key exists and hasn't expired, `false` otherwise **Example:** ```typescript cache.set('value', undefined) // get() returns undefined for both cases cache.get('value') // undefined cache.get('missing') // undefined // has() distinguishes them cache.has('value') // true cache.has('missing') // false ``` ### delete(key) Removes a specific entry from the cache. ```typescript delete(key: string): boolean ``` **Parameters:** - `key` - The key of the entry to remove **Returns:** `true` if an entry was removed, `false` if the key wasn't found **Example:** ```typescript cache.set('key', 'value') cache.delete('key') // true cache.delete('nonexistent') // false ``` ### deleteAsync(key) Async version of `delete()`. Useful for consistent async/await patterns. ```typescript deleteAsync(key: string): Promise ``` **Example:** ```typescript await cache.deleteAsync('key') ``` ### clear() Removes all entries from the cache. ```typescript clear(): void ``` **Example:** ```typescript cache.set('key1', 'value1') cache.set('key2', 'value2') cache.clear() cache.get('key1') // undefined ``` ### deleteByPrefix(prefix) Removes all entries whose keys start with the given prefix. ```typescript deleteByPrefix(prefix: string): number ``` **Parameters:** - `prefix` - The prefix to match against cache keys **Returns:** The number of entries removed **Example:** ```typescript cache.set('user:123:name', 'John') cache.set('user:123:email', 'john[at]example.com') cache.set('user:456:name', 'Jane') cache.set('post:789', 'Hello World') const removed = cache.deleteByPrefix('user:123:') // removed = 2 (user:123:name and user:123:email) cache.get('user:123:name') // undefined cache.get('user:456:name') // 'Jane' ``` ### deleteByMagicString(pattern) Removes all entries whose keys match the given wildcard pattern. Use `*` as a wildcard that matches any sequence of characters. ```typescript deleteByMagicString(pattern: string): number ``` **Parameters:** - `pattern` - The wildcard pattern to match (use `*` for wildcards) **Returns:** The number of entries removed **Example:** ```typescript cache.set('user:123:name', 'John') cache.set('user:456:name', 'Jane') cache.set('user:123:email', 'john[at]example.com') cache.set('post:789', 'Hello World') // Delete all entries matching user:*:name cache.deleteByMagicString('user:*:name') // Removes user:123:name and user:456:name // Delete all user:123 entries cache.deleteByMagicString('user:123:*') // Delete all entries containing :123: cache.deleteByMagicString('*:123:*') ``` ## Introspection Methods ### size() Returns the current number of entries in the cache. Internally calls [`prune()`](#prune) to remove expired entries before counting. ```typescript size(): number ``` **Returns:** The number of cached entries **Example:** ```typescript const cache = new MemoryCache() cache.set('key1', 'value1') cache.set('key2', 'value2') console.log(cache.size()) // 2 ``` ### keys() Returns an array of all keys currently in the cache. Internally calls [`prune()`](#prune) to remove expired entries before returning results. ```typescript keys(): string[] ``` **Returns:** Array of cache keys **Example:** ```typescript const cache = new MemoryCache() cache.set('user:1', 'Alice') cache.set('user:2', 'Bob') console.log(cache.keys()) // ['user:1', 'user:2'] ``` ### values() Returns an array of all values currently in the cache. Internally calls [`prune()`](#prune) to remove expired entries before returning results. ```typescript values(): (T | undefined)[] ``` **Returns:** Array of cached values **Example:** ```typescript const cache = new MemoryCache() cache.set('key1', 'value1') cache.set('key2', 'value2') console.log(cache.values()) // ['value1', 'value2'] ``` ### entries() Returns an array of all key-value pairs currently in the cache. Internally calls [`prune()`](#prune) to remove expired entries before returning results. ```typescript entries(): [string, T | undefined][] ``` **Returns:** Array of [key, value] tuples **Example:** ```typescript const cache = new MemoryCache() cache.set('key1', 'value1') cache.set('key2', 'value2') console.log(cache.entries()) // [['key1', 'value1'], ['key2', 'value2']] ``` ## Statistics ### getStats() Returns statistics about cache usage and performance. ```typescript getStats(): CacheStats ``` **Returns:** Object containing cache statistics (see [CacheStats](#cachestats) type) **Example:** ```typescript const cache = new MemoryCache() cache.set('key', 'value') cache.get('key') // hit cache.get('missing') // miss const stats = cache.getStats() console.log(stats) // { hits: 1, misses: 1, evictions: 0, expirations: 0, size: 1 } ``` ### resetStats() Resets all statistics counters to zero. Does not affect cached data. ```typescript resetStats(): void ``` **Example:** ```typescript const cache = new MemoryCache() cache.get('missing') // increments misses cache.resetStats() console.log(cache.getStats().misses) // 0 ``` ### prune() Proactively removes all expired entries from the cache. This is useful for reclaiming memory when you don't want to wait for lazy cleanup (which occurs when expired entries are accessed). ```typescript prune(): number ``` **Returns:** The number of expired entries that were removed **Example:** ```typescript const cache = new MemoryCache({ ttl: 1000 }) cache.set('key1', 'value1') cache.set('key2', 'value2') // ... time passes ... const pruned = cache.prune() console.log(`Removed ${pruned} expired entries`) ``` ## Types ### CacheStats Statistics about cache usage and performance. ```typescript type CacheStats = { hits: number misses: number evictions: number expirations: number size: number } ``` ### CacheConfigError Error thrown when cache configuration is invalid. ```typescript import { CacheConfigError } from '@humanspeak/memory-cache' class CacheConfigError extends Error { readonly name = 'CacheConfigError' } ``` Thrown when: - `maxSize` is negative - `ttl` is negative ## Type Safety The cache is fully generic and type-safe: ```typescript // Typed cache for User objects const userCache = new MemoryCache() userCache.set('user:1', { id: 1, name: 'John' }) const user = userCache.get('user:1') // User | undefined // Typed cache for API responses interface ApiResponse { data: unknown timestamp: number } const apiCache = new MemoryCache() ``` ## Caching Null and Undefined The cache properly handles `null` and `undefined` values: ```typescript const cache = new MemoryCache() cache.set('null', null) cache.set('undefined', undefined) // Values are properly retrieved cache.get('null') // null cache.get('undefined') // undefined // Use has() to distinguish from cache misses cache.has('null') // true cache.has('undefined') // true cache.has('missing') // false ``` ## Hooks Hooks allow you to observe cache lifecycle events for monitoring, debugging, and integration with external systems. ### CacheHooks ```typescript type CacheHooks = { onHit?: (context: OnHitContext) => void onMiss?: (context: OnMissContext) => void onSet?: (context: OnSetContext) => void onEvict?: (context: OnEvictContext) => void onExpire?: (context: OnExpireContext) => void onDelete?: (context: OnDeleteContext) => void } ``` ### Hook Events | Hook | When Called | Context Type | |------|-------------|--------------| | `onHit` | Successful `get()` retrieval | `{ key: string, value: T \| undefined }` | | `onMiss` | `get()` returns undefined | `{ key: string, reason: 'not_found' \| 'expired' }` | | `onSet` | Value stored via `set()` | `{ key: string, value: T, isUpdate: boolean }` | | `onEvict` | Entry evicted due to `maxSize` | `{ key: string, value: T \| undefined }` | | `onExpire` | Entry expired due to TTL | `{ key: string, value: T \| undefined, source: 'get' \| 'has' \| 'prune' }` | | `onDelete` | Entry explicitly deleted | `{ key: string, value: T \| undefined, source: 'delete' \| 'deleteAsync' \| 'deleteByPrefix' \| 'deleteByMagicString' \| 'clear' }` | ### Example: Metrics Integration ```typescript import { MemoryCache } from '@humanspeak/memory-cache' const cache = new MemoryCache({ maxSize: 1000, ttl: 5 * 60 * 1000, hooks: { onHit: ({ key }) => { metrics.increment('cache.hit') console.log(`Cache hit: ${key}`) }, onMiss: ({ key, reason }) => { metrics.increment('cache.miss') console.log(`Cache miss: ${key} (${reason})`) }, onEvict: ({ key }) => { metrics.increment('cache.eviction') console.log(`Evicted: ${key}`) }, onExpire: ({ key, source }) => { metrics.increment('cache.expiration') console.log(`Expired: ${key} via ${source}`) } } }) ``` ### Important Notes - **Synchronous only**: Hooks must be synchronous functions. Async hooks are not supported. - **Error handling**: Errors thrown in hooks are silently caught to prevent cache corruption. - **Performance**: Keep hooks lightweight to avoid impacting cache performance. - **Batch operations**: `clear()`, `deleteByPrefix()`, and `deleteByMagicString()` call `onDelete` once per deleted entry. # API Response Caching > Cache API responses with @humanspeak/memory-cache to reduce latency and external calls using TTL expiration and invalidation patterns in apps. **Source:** [https://memory.svelte.page/docs/examples/api-caching](https://memory.svelte.page/docs/examples/api-caching) --- Cache API responses to reduce network requests and improve response times. ## Basic Pattern ```typescript import { MemoryCache } from '@humanspeak/memory-cache' interface ApiResponse { data: T cachedAt: number } const apiCache = new MemoryCache>({ maxSize: 500, ttl: 5 * 60 * 1000 // 5 minutes }) async function fetchWithCache(url: string): Promise { // Check cache first const cached = apiCache.get(url) if (cached) { console.log('Cache hit:', url) return cached.data as T } // Fetch and cache console.log('Cache miss:', url) const response = await fetch(url) const data = await response.json() apiCache.set(url, { data, cachedAt: Date.now() }) return data } // Usage const users = await fetchWithCache('/api/users') const user = await fetchWithCache('/api/users/123') ``` ## With Hooks for Monitoring ```typescript import { MemoryCache } from '@humanspeak/memory-cache' const apiCache = new MemoryCache>({ maxSize: 500, ttl: 5 * 60 * 1000, hooks: { onHit: ({ key }) => { metrics.increment('api_cache.hit') console.log(`API cache hit: ${key}`) }, onMiss: ({ key, reason }) => { metrics.increment('api_cache.miss') console.log(`API cache miss: ${key} (${reason})`) }, onSet: ({ key }) => { console.log(`API response cached: ${key}`) } } }) ``` ## Key Considerations - **TTL**: Choose based on how often data changes (1-5 minutes typical) - **Max Size**: Consider memory constraints and number of unique endpoints - **Cache Key**: Use the full URL including query parameters - **Error Handling**: Don't cache error responses # Async Decorator > Use @cached with async methods for single-flight request deduplication, retry-safe errors, TTL caching, and nullish result handling in apps. **Source:** [https://memory.svelte.page/docs/examples/async-decorator](https://memory.svelte.page/docs/examples/async-decorator) --- Use `@cached` on async service methods when callers should share work for identical arguments. ## Single-Flight Requests Concurrent calls with the same arguments share one in-flight method execution. Once the first call resolves, the resolved value is cached for later calls. ```typescript import { cached } from '@humanspeak/memory-cache' interface User { id: string name: string } class UserService { private fetchCount = 0 @cached({ ttl: 60_000 }) async getUser(id: string): Promise { this.fetchCount++ return await database.users.findById(id) } getFetchCount(): number { return this.fetchCount } } const service = new UserService() const [first, second, third] = await Promise.all([ service.getUser('user-123'), service.getUser('user-123'), service.getUser('user-123') ]) console.log(first === second) // true if the database returns the same object console.log(second === third) // true console.log(service.getFetchCount()) // 1 ``` ## Rejections Retry Rejected promises are not cached by default. All concurrent callers see the same rejection, and the next call starts a new method execution. ```typescript class ProfileService { private attempts = 0 @cached({ ttl: 30_000 }) async getProfile(userId: string): Promise { this.attempts++ const response = await fetch(`/api/profiles/${userId}`) if (!response.ok) { throw new Error('Profile fetch failed') } return await response.json() } } const service = new ProfileService() await Promise.allSettled([ service.getProfile('user-123'), service.getProfile('user-123') ]) // A later call retries instead of reusing the rejected promise. const profile = await service.getProfile('user-123') ``` ## TTL and Nullish Results Resolved `undefined` and `null` values are cached just like other results. TTL still controls when the next method execution can happen. ```typescript class FeatureFlagService { @cached({ ttl: 5_000 }) async getFlag(name: string): Promise { const flag = await database.flags.findByName(name) return flag?.enabled } @cached({ ttl: 60_000 }) async findUser(id: string): Promise { return (await database.users.findById(id)) ?? null } } ``` ## When to Use It - Use `@cached` for service methods that naturally derive a result from their arguments. - Use `getOrSet()` when the cache key is already explicit or shared outside a class method. - Avoid caching write operations or methods whose result depends on hidden mutable instance state. # Async Fetching > Cache async fetch results with getOrSet in @humanspeak/memory-cache, preventing thundering herds and redundant network requests in TypeScript apps. **Source:** [https://memory.svelte.page/docs/examples/async-fetching](https://memory.svelte.page/docs/examples/async-fetching) --- Use `getOrSet` to automatically cache the results of expensive async operations. ## Basic Usage ```typescript import { MemoryCache } from '@humanspeak/memory-cache' const cache = new MemoryCache({ ttl: 60000 }) async function getUser(id: string): Promise { return cache.getOrSet(`user:${id}`, async () => { // Only called on cache miss return await database.findUser(id) }) } ``` ## Thundering Herd Prevention When multiple requests arrive for the same uncached key simultaneously, only one fetch is executed: ```typescript // All 100 requests share the same fetch const promises = Array.from({ length: 100 }, () => cache.getOrSet('popular-key', fetchExpensiveData) ) await Promise.all(promises) // fetchExpensiveData called only once ``` ## API Response Caching ```typescript const apiCache = new MemoryCache({ ttl: 30000 }) async function fetchWeather(city: string): Promise { return apiCache.getOrSet(`weather:${city}`, async () => { const response = await fetch(`https://api.weather.com/${city}`) return response.json() }) } ``` ## With Error Handling Errors from the fetcher are not cached - they propagate to the caller: ```typescript try { const data = await cache.getOrSet(key, async () => { const response = await fetch(url) if (!response.ok) throw new Error('Fetch failed') return response.json() }) } catch (error) { // Handle error - next call will retry the fetch } ``` ## With Hooks for Monitoring ```typescript import { MemoryCache } from '@humanspeak/memory-cache' const cache = new MemoryCache({ ttl: 60000, hooks: { onHit: ({ key }) => console.log(`Cache hit: ${key}`), onMiss: ({ key }) => console.log(`Cache miss: ${key}`), onSet: ({ key }) => console.log(`Cached: ${key}`) } }) // Logs show cache behavior const user1 = await cache.getOrSet('user:123', fetchUser) // "Cache miss" then "Cached" const user2 = await cache.getOrSet('user:123', fetchUser) // "Cache hit" ``` ## Sync Fetchers The fetcher can return a value directly (not a Promise): ```typescript const config = await cache.getOrSet('config', () => { // Sync operation return loadConfigFromEnv() }) ``` ## Key Considerations - **Fetcher errors are not cached** - Failed fetches can be retried - **Single-flight** - Concurrent requests for the same key share one fetch - **Uses instance TTL** - Cached values expire based on cache configuration - **Supports sync fetchers** - Function can return `T` or `Promise` # Computed Value Caching > Cache expensive computed values and derived data with @humanspeak/memory-cache, avoiding redundant TypeScript calculations and repeat work in apps. **Source:** [https://memory.svelte.page/docs/examples/computed-values](https://memory.svelte.page/docs/examples/computed-values) --- Cache expensive computations where results are deterministic. ## Basic Pattern ```typescript import { MemoryCache } from '@humanspeak/memory-cache' interface ComputeResult { value: number computedAt: number iterations: number } const computeCache = new MemoryCache({ maxSize: 1000, ttl: 0 // No expiration - results are deterministic }) function expensiveComputation(input: number): ComputeResult { const cacheKey = `compute:${input}` const cached = computeCache.get(cacheKey) if (cached) { return cached } // Expensive computation let result = 0 let iterations = 0 for (let i = 0; i < input * 1000000; i++) { result += Math.sqrt(i) iterations++ } const computed: ComputeResult = { value: result, computedAt: Date.now(), iterations } computeCache.set(cacheKey, computed) return computed } ``` ## With Performance Tracking ```typescript import { MemoryCache } from '@humanspeak/memory-cache' const computeCache = new MemoryCache({ maxSize: 1000, ttl: 0, hooks: { onHit: ({ key }) => { metrics.increment('compute.cache.hit') console.log(`Computation cache hit: ${key}`) }, onMiss: () => { metrics.increment('compute.cache.miss') }, onEvict: ({ key }) => { // LRU eviction occurred - consider increasing maxSize metrics.increment('compute.cache.eviction') console.warn(`Computation evicted: ${key}`) } } }) ``` ## Key Considerations - **TTL**: Use `0` for deterministic computations - **Max Size**: Limit by memory, not time - **Key Design**: Include all inputs that affect the result # Configuration Cache > Cache rarely changing configuration with @humanspeak/memory-cache, covering force refresh, change detection, TTL strategy, and startup speed. **Source:** [https://memory.svelte.page/docs/examples/configuration](https://memory.svelte.page/docs/examples/configuration) --- Cache configuration that rarely changes to avoid repeated fetches. ## Basic Pattern ```typescript import { MemoryCache } from '@humanspeak/memory-cache' interface Config { features: Record settings: Record version: string } const configCache = new MemoryCache({ maxSize: 100, ttl: 5 * 60 * 1000 // 5 minutes }) async function getConfig(environment: string): Promise { const cached = configCache.get(environment) if (cached) { return cached } // Fetch from remote config service const config = await fetchConfigFromRemote(environment) configCache.set(environment, config) return config } // Force refresh config function refreshConfig(environment: string): void { configCache.delete(environment) } // Refresh all environments function refreshAllConfigs(): void { configCache.clear() } ``` ## With Change Detection ```typescript import { MemoryCache } from '@humanspeak/memory-cache' const configCache = new MemoryCache({ maxSize: 100, ttl: 5 * 60 * 1000, hooks: { onSet: ({ key, value, isUpdate }) => { if (isUpdate) { console.log(`Config updated for ${key}:`, value.version) // Notify dependent services eventBus.emit('config:updated', { environment: key, config: value }) } }, onExpire: ({ key }) => { console.log(`Config expired for ${key}, will refresh on next access`) } } }) ``` ## Key Considerations - **TTL**: 5-10 minutes balances freshness and performance - **Force Refresh**: Provide a way to manually invalidate - **Versioning**: Include version in config for change detection # Database Query Caching > Cache expensive database queries with @cached to reduce latency and database load, including query monitoring and practical TTL examples for apps. **Source:** [https://memory.svelte.page/docs/examples/database-caching](https://memory.svelte.page/docs/examples/database-caching) --- Cache expensive database queries using the `@cached` decorator. ## Basic Pattern ```typescript import { cached } from '@humanspeak/memory-cache' class UserRepository { @cached({ ttl: 60000 }) // 1 minute async findById(id: string): Promise { return await prisma.user.findUnique({ where: { id }, include: { profile: true, posts: true } }) } @cached({ ttl: 30000, maxSize: 50 }) // 30 seconds async findByOrganization(orgId: string): Promise { return await prisma.user.findMany({ where: { organizationId: orgId }, orderBy: { createdAt: 'desc' } }) } } const repo = new UserRepository() // First call hits database const user = await repo.findById('user-123') // Second call returns cached result const cachedUser = await repo.findById('user-123') ``` ## With Hooks for Query Monitoring ```typescript import { cached } from '@humanspeak/memory-cache' class UserRepository { @cached({ ttl: 60000, hooks: { onHit: ({ key }) => { metrics.increment('db.cache.hit', { query: 'findById' }) }, onMiss: ({ key }) => { metrics.increment('db.cache.miss', { query: 'findById' }) } } }) async findById(id: string): Promise { const start = Date.now() const result = await prisma.user.findUnique({ where: { id } }) metrics.timing('db.query.duration', Date.now() - start) return result } } ``` ## Key Considerations - **TTL**: Short TTLs (30s-2min) for frequently changing data - **Invalidation**: Clear cache after writes to the same data - **Key Generation**: The decorator uses `JSON.stringify(args)` for keys # Monitoring with Hooks > Monitor cache health with hit rates, eviction counts, and performance metrics by integrating hooks with dashboards and logging systems in apps. **Source:** [https://memory.svelte.page/docs/examples/monitoring](https://memory.svelte.page/docs/examples/monitoring) --- Use cache hooks to integrate with metrics, logging, and monitoring systems. ## Basic Metrics Integration ```typescript import { MemoryCache } from '@humanspeak/memory-cache' const cache = new MemoryCache({ maxSize: 1000, ttl: 5 * 60 * 1000, hooks: { onHit: ({ key }) => { metrics.increment('cache.hit') metrics.increment(`cache.hit.${extractPrefix(key)}`) }, onMiss: ({ key, reason }) => { metrics.increment('cache.miss') metrics.increment(`cache.miss.${reason}`) }, onSet: ({ key, isUpdate }) => { metrics.increment(isUpdate ? 'cache.update' : 'cache.insert') }, onEvict: ({ key }) => { metrics.increment('cache.eviction') }, onExpire: ({ key }) => { metrics.increment('cache.expiration') }, onDelete: ({ key, source }) => { metrics.increment('cache.delete') metrics.increment(`cache.delete.${source}`) } } }) function extractPrefix(key: string): string { return key.split(':')[0] || 'unknown' } ``` ## DataDog Integration ```typescript import { MemoryCache } from '@humanspeak/memory-cache' import StatsD from 'hot-shots' const dogstatsd = new StatsD() const cache = new MemoryCache({ maxSize: 5000, ttl: 5 * 60 * 1000, hooks: { onHit: ({ key }) => { dogstatsd.increment('cache.operations', { operation: 'hit' }) }, onMiss: ({ key, reason }) => { dogstatsd.increment('cache.operations', { operation: 'miss', reason }) }, onEvict: () => { dogstatsd.increment('cache.operations', { operation: 'eviction' }) }, onExpire: () => { dogstatsd.increment('cache.operations', { operation: 'expiration' }) } } }) // Periodically report cache size setInterval(() => { dogstatsd.gauge('cache.size', cache.size()) const stats = cache.getStats() dogstatsd.gauge('cache.hit_rate', stats.hits / (stats.hits + stats.misses) || 0) }, 10000) ``` ## Prometheus Integration ```typescript import { MemoryCache } from '@humanspeak/memory-cache' import { Counter, Gauge, register } from 'prom-client' const cacheHits = new Counter({ name: 'cache_hits_total', help: 'Total cache hits', labelNames: ['cache_name'] }) const cacheMisses = new Counter({ name: 'cache_misses_total', help: 'Total cache misses', labelNames: ['cache_name', 'reason'] }) const cacheEvictions = new Counter({ name: 'cache_evictions_total', help: 'Total cache evictions', labelNames: ['cache_name'] }) const cacheSize = new Gauge({ name: 'cache_size', help: 'Current cache size', labelNames: ['cache_name'] }) function createMonitoredCache(name: string, options: { maxSize: number; ttl: number }) { return new MemoryCache({ ...options, hooks: { onHit: () => cacheHits.inc({ cache_name: name }), onMiss: ({ reason }) => cacheMisses.inc({ cache_name: name, reason }), onEvict: () => cacheEvictions.inc({ cache_name: name }), onSet: () => { // Update size gauge after each set // Note: This is approximate since we're inside the hook } } }) } // Usage const userCache = createMonitoredCache('users', { maxSize: 1000, ttl: 60000 }) const productCache = createMonitoredCache('products', { maxSize: 500, ttl: 300000 }) ``` ## Debug Logging ```typescript import { MemoryCache } from '@humanspeak/memory-cache' const cache = new MemoryCache({ maxSize: 100, ttl: 60000, hooks: { onHit: ({ key, value }) => { console.debug(`[CACHE HIT] ${key}`, { valueType: typeof value }) }, onMiss: ({ key, reason }) => { console.debug(`[CACHE MISS] ${key} (${reason})`) }, onSet: ({ key, isUpdate }) => { console.debug(`[CACHE ${isUpdate ? 'UPDATE' : 'SET'}] ${key}`) }, onEvict: ({ key }) => { console.warn(`[CACHE EVICT] ${key} - consider increasing maxSize`) }, onExpire: ({ key, source }) => { console.debug(`[CACHE EXPIRE] ${key} via ${source}`) }, onDelete: ({ key, source }) => { console.debug(`[CACHE DELETE] ${key} via ${source}`) } } }) ``` ## Hit Rate Dashboard ```typescript import { MemoryCache } from '@humanspeak/memory-cache' class CacheMonitor { private hits = 0 private misses = 0 private evictions = 0 private expirations = 0 createCache(options: { maxSize: number; ttl: number }): MemoryCache { return new MemoryCache({ ...options, hooks: { onHit: () => this.hits++, onMiss: () => this.misses++, onEvict: () => this.evictions++, onExpire: () => this.expirations++ } }) } getStats() { const total = this.hits + this.misses return { hits: this.hits, misses: this.misses, evictions: this.evictions, expirations: this.expirations, hitRate: total > 0 ? (this.hits / total * 100).toFixed(2) + '%' : 'N/A' } } reset() { this.hits = 0 this.misses = 0 this.evictions = 0 this.expirations = 0 } } // Usage const monitor = new CacheMonitor() const cache = monitor.createCache({ maxSize: 1000, ttl: 60000 }) // Check stats periodically setInterval(() => { console.log('Cache Performance:', monitor.getStats()) }, 60000) ``` ## Key Considerations - **Performance**: Keep hooks lightweight to avoid impacting cache performance - **Error Handling**: Hooks errors are silently caught, but avoid throwing - **Sampling**: For high-traffic caches, consider sampling instead of logging every event - **Async Operations**: Hooks are synchronous; queue async work if needed # Multi-Tenant Cache Invalidation > Implement per-tenant cache isolation with namespaced keys in @humanspeak/memory-cache, using prefix and wildcard deletion for scoped cleanup. **Source:** [https://memory.svelte.page/docs/examples/multi-tenant](https://memory.svelte.page/docs/examples/multi-tenant) --- Use prefix and wildcard deletion for efficient cache invalidation in multi-tenant applications. ## Basic Pattern ```typescript import { MemoryCache } from '@humanspeak/memory-cache' const cache = new MemoryCache({ maxSize: 10000, ttl: 10 * 60 * 1000 // 10 minutes }) // Cache keys follow pattern: tenant:{tenantId}:{resource}:{id} function cacheForTenant(tenantId: string, resource: string, id: string, value: T) { cache.set(`tenant:${tenantId}:${resource}:${id}`, value) } function getForTenant(tenantId: string, resource: string, id: string): T | undefined { return cache.get(`tenant:${tenantId}:${resource}:${id}`) as T | undefined } // Invalidate all cache for a specific tenant function invalidateTenantCache(tenantId: string): number { return cache.deleteByPrefix(`tenant:${tenantId}:`) } // Invalidate specific resource type for a tenant function invalidateTenantResource(tenantId: string, resource: string): number { return cache.deleteByPrefix(`tenant:${tenantId}:${resource}:`) } // Invalidate all caches for a resource across tenants function invalidateResourceGlobally(resource: string): number { return cache.deleteByMagicString(`tenant:*:${resource}:*`) } ``` ## Usage Example ```typescript // Store data cacheForTenant('acme', 'users', '123', { name: 'John' }) cacheForTenant('acme', 'users', '456', { name: 'Jane' }) cacheForTenant('acme', 'products', '789', { name: 'Widget' }) cacheForTenant('globex', 'users', '123', { name: 'Homer' }) // Invalidate all ACME user caches invalidateTenantResource('acme', 'users') // Invalidate all user caches globally invalidateResourceGlobally('users') ``` ## With Invalidation Logging ```typescript import { MemoryCache } from '@humanspeak/memory-cache' const cache = new MemoryCache({ maxSize: 10000, ttl: 10 * 60 * 1000, hooks: { onDelete: ({ key, source }) => { // Parse tenant from key const match = key.match(/^tenant:([^:]+):/) const tenantId = match?.[1] ?? 'unknown' auditLog.info('Cache invalidated', { key, tenantId, source }) } } }) ``` ## Key Considerations - **Key Pattern**: Use consistent, hierarchical patterns - **Prefix vs Wildcard**: Prefix is faster; use wildcards for complex patterns - **Batch Size**: Large deletions may briefly block; consider chunking # Rate Limiting > Implement rate limiting with @humanspeak/memory-cache TTL windows to protect API endpoints without Redis or external dependencies in production. **Source:** [https://memory.svelte.page/docs/examples/rate-limiting](https://memory.svelte.page/docs/examples/rate-limiting) --- Implement simple rate limiting using cache TTL. ## Basic Pattern ```typescript import { MemoryCache } from '@humanspeak/memory-cache' interface RateLimitEntry { count: number resetAt: number } const rateLimitCache = new MemoryCache({ maxSize: 100000, ttl: 60 * 1000 // 1 minute window }) function checkRateLimit(clientId: string, limit: number = 100): boolean { const key = `ratelimit:${clientId}` const entry = rateLimitCache.get(key) if (!entry) { // First request rateLimitCache.set(key, { count: 1, resetAt: Date.now() + 60000 }) return true } if (entry.count >= limit) { return false // Rate limit exceeded } // Increment counter rateLimitCache.set(key, { count: entry.count + 1, resetAt: entry.resetAt }) return true } ``` ## Express Middleware ```typescript import { MemoryCache } from '@humanspeak/memory-cache' import type { Request, Response, NextFunction } from 'express' // Uses RateLimitEntry type and checkRateLimit function from Basic Pattern above const rateLimitCache = new MemoryCache({ maxSize: 100000, ttl: 60 * 1000 }) function rateLimitMiddleware(limit: number = 100) { return (req: Request, res: Response, next: NextFunction) => { const clientId = req.headers['x-client-id'] as string || req.ip if (!checkRateLimit(clientId, limit)) { return res.status(429).json({ error: 'Too Many Requests', retryAfter: 60 }) } next() } } // Usage app.use('/api', rateLimitMiddleware(100)) ``` ## With Monitoring ```typescript import { MemoryCache } from '@humanspeak/memory-cache' const rateLimitCache = new MemoryCache({ maxSize: 100000, ttl: 60 * 1000, hooks: { onSet: ({ key, value }) => { if (value.count === 1) { metrics.increment('ratelimit.new_client') } }, onExpire: ({ key }) => { metrics.increment('ratelimit.window_reset') } } }) function checkRateLimit(clientId: string, limit: number): boolean { const key = `ratelimit:${clientId}` const entry = rateLimitCache.get(key) if (!entry) { rateLimitCache.set(key, { count: 1, resetAt: Date.now() + 60000 }) return true } if (entry.count >= limit) { metrics.increment('ratelimit.exceeded', { clientId }) return false } rateLimitCache.set(key, { count: entry.count + 1, resetAt: entry.resetAt }) return true } ``` ## Key Considerations - **Max Size**: Plan for number of unique clients - **TTL**: Determines the rate limit window - **Client ID**: Use a reliable identifier (API key, IP, user ID) - **Distributed Systems**: This pattern is per-instance; use Redis for distributed rate limiting # Service Class Pattern > Build a cache-backed service class with @cached method memoization, multiple cached methods, hooks monitoring, and clean TypeScript app design. **Source:** [https://memory.svelte.page/docs/examples/service-class](https://memory.svelte.page/docs/examples/service-class) --- A complete example of using the `@cached` decorator with a service class. ## Full Example ```typescript import { cached } from '@humanspeak/memory-cache' interface Product { id: string name: string price: number category: string } class ProductService { private db: Database constructor(db: Database) { this.db = db } @cached({ ttl: 300000 }) // 5 minutes async getProduct(id: string): Promise { return await this.db.products.findUnique({ where: { id } }) } @cached({ ttl: 60000, maxSize: 100 }) // 1 minute async getProductsByCategory(category: string): Promise { return await this.db.products.findMany({ where: { category }, orderBy: { name: 'asc' } }) } @cached({ ttl: 30000, maxSize: 50 }) async searchProducts(query: string, limit: number): Promise { return await this.db.products.findMany({ where: { name: { contains: query, mode: 'insensitive' } }, take: limit }) } // No caching for write operations async createProduct(data: Omit): Promise { return await this.db.products.create({ data }) } } // Usage const productService = new ProductService(db) // These are cached const product = await productService.getProduct('prod-123') const electronics = await productService.getProductsByCategory('electronics') const results = await productService.searchProducts('laptop', 10) ``` ## With Hooks for Monitoring ```typescript import { cached } from '@humanspeak/memory-cache' class ProductService { @cached({ ttl: 300000, hooks: { onHit: ({ key }) => { metrics.increment('product_service.cache.hit', { method: 'getProduct' }) }, onMiss: ({ key }) => { metrics.increment('product_service.cache.miss', { method: 'getProduct' }) } } }) async getProduct(id: string): Promise { metrics.increment('product_service.db.query', { method: 'getProduct' }) return await this.db.products.findUnique({ where: { id } }) } @cached({ ttl: 60000, maxSize: 100, hooks: { onHit: () => metrics.increment('product_service.cache.hit', { method: 'getByCategory' }), onMiss: () => metrics.increment('product_service.cache.miss', { method: 'getByCategory' }), onEvict: () => metrics.increment('product_service.cache.eviction', { method: 'getByCategory' }) } }) async getProductsByCategory(category: string): Promise { return await this.db.products.findMany({ where: { category }, orderBy: { name: 'asc' } }) } } ``` ## Key Considerations - **TTL by Method**: Different methods may need different TTLs - **Max Size**: Limit based on expected unique argument combinations - **Write Operations**: Don't cache write operations - **Cache Invalidation**: Consider how updates affect cached data # Session Storage > Implement in-memory session storage with automatic TTL expiration using @humanspeak/memory-cache for simple per-process TypeScript and Node.js apps. **Source:** [https://memory.svelte.page/docs/examples/sessions](https://memory.svelte.page/docs/examples/sessions) --- Store user sessions with automatic expiration using TTL. ## Basic Pattern ```typescript import { MemoryCache } from '@humanspeak/memory-cache' interface Session { userId: string permissions: string[] createdAt: number } const sessionCache = new MemoryCache({ maxSize: 10000, ttl: 30 * 60 * 1000 // 30 minutes }) function createSession(userId: string, permissions: string[]): string { const sessionId = crypto.randomUUID() sessionCache.set(sessionId, { userId, permissions, createdAt: Date.now() }) return sessionId } function getSession(sessionId: string): Session | undefined { return sessionCache.get(sessionId) } function destroySession(sessionId: string): void { sessionCache.delete(sessionId) } ``` ## With Audit Logging ```typescript import { MemoryCache } from '@humanspeak/memory-cache' const sessionCache = new MemoryCache({ maxSize: 10000, ttl: 30 * 60 * 1000, hooks: { onSet: ({ key, value, isUpdate }) => { if (!isUpdate) { auditLog.info('Session created', { sessionId: key, userId: value.userId }) } }, onExpire: ({ key, value }) => { auditLog.info('Session expired', { sessionId: key, userId: value?.userId }) }, onDelete: ({ key, value, source }) => { auditLog.info('Session destroyed', { sessionId: key, userId: value?.userId, reason: source }) } } }) ``` ## Key Considerations - **TTL**: 30-60 minutes is typical for web sessions - **Security**: Don't store sensitive data; use session ID as reference - **Max Size**: Plan for concurrent users plus some buffer # Getting Started > Install and configure @humanspeak/memory-cache for TypeScript apps, covering basic usage, TTL expiration, LRU eviction, and decorators quickly. **Source:** [https://memory.svelte.page/docs/getting-started](https://memory.svelte.page/docs/getting-started) --- @humanspeak/memory-cache is a lightweight, zero-dependency in-memory cache for TypeScript and JavaScript. It provides TTL expiration, true LRU (Least Recently Used) eviction, wildcard pattern deletion, and a powerful `@cached` decorator for method-level memoization. ## Installation Install the package using your preferred package manager: ```bash npm install @humanspeak/memory-cache ``` ```bash pnpm add @humanspeak/memory-cache ``` ```bash yarn add @humanspeak/memory-cache ``` ## Quick Start ### Basic Cache Usage ```typescript import { MemoryCache } from '@humanspeak/memory-cache' // Create a cache with default options (100 entries, 5 minute TTL) const cache = new MemoryCache() // Or customize the options const customCache = new MemoryCache({ maxSize: 1000, // Maximum entries before eviction ttl: 10 * 60 * 1000 // 10 minutes TTL }) // Store and retrieve values cache.set('user:123', 'John Doe') const name = cache.get('user:123') // 'John Doe' // Check if key exists if (cache.has('user:123')) { console.log('User is cached') } // Delete entries cache.delete('user:123') cache.clear() // Remove all entries ``` ### Using the @cached Decorator The `@cached` decorator provides automatic method-level memoization: ```typescript import { cached } from '@humanspeak/memory-cache' class UserService { @cached({ ttl: 60000, maxSize: 100 }) async getUser(id: string): Promise { // This expensive operation will be cached return await database.findUser(id) } } const service = new UserService() // First call - executes the method await service.getUser('123') // Second call - returns cached result instantly await service.getUser('123') ``` ### Using getOrSet() The `getOrSet()` method combines lookup and population in a single call — ideal for async caching patterns: ```typescript const cache = new MemoryCache() // Returns cached value if it exists, otherwise calls the factory and caches the result const user = await cache.getOrSet('user:123', async () => { return await database.findUser('123') }) ``` ## Configuration Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `maxSize` | `number` | `100` | Maximum number of entries before eviction. Set to `0` for unlimited. | | `ttl` | `number` | `300000` | Time-to-live in milliseconds. Set to `0` for no expiration. | | `hooks` | `object` | — | Lifecycle hooks (`onEvict`, `onExpire`, `onSet`, `onDelete`, `onClear`) for reacting to cache events. | ## Features - **Zero Dependencies** - No external dependencies, keeping your bundle small - **TTL Expiration** - Entries automatically expire after a configurable time - **LRU Eviction** - Least recently used entries are evicted when the cache is full - **Wildcard Deletion** - Delete entries by prefix or wildcard patterns - **Full TypeScript Support** - Complete type definitions included - **Method Decorator** - `@cached` decorator for automatic memoization - **Null/Undefined Support** - Properly distinguishes between cached falsy values and cache misses ## Next Steps - Learn about the [MemoryCache API](/docs/api/memory-cache) - Explore the [@cached decorator](/docs/api/cached-decorator) - See [usage examples](/docs/examples)