# @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 | | `maxWeight` | `number` | `0` | Maximum aggregate weight (0 disables weighted eviction) | | `sizeCalculation` | `(value: T, key: string) => number` | — | Calculates each result's finite, non-negative weight | | `ttl` | `number` | `300000` | Cache duration in milliseconds | | `hooks` | `CacheHooks` | `{}` | Lifecycle hooks inherited from `MemoryCache` | | `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 ``` ## Weighted Eviction Decorated methods inherit `maxWeight` and `sizeCalculation(value, key)`. Set `maxSize: 0` when aggregate weight should be the only capacity constraint: ```typescript class ReportService { @cached({ maxSize: 0, maxWeight: 2_000_000, sizeCalculation: (report) => new TextEncoder().encode(JSON.stringify(report)).byteLength }) async buildReport(id: string): Promise { return await generateReport(id) } } ``` Weights are application-defined rather than exact heap measurements. Eviction still chooses the least recently used result, not the physically largest one. If a returned result alone exceeds `maxWeight`, the caller still receives it, but it is not memoized and the next call runs the method again. An oversized replacement removes the old cached result. ## 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 entry-count or weighted 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) | | `maxWeight` | `number` | `0` | Maximum aggregate weight (0 = weighted eviction disabled) | | `sizeCalculation` | `(value: T, key: string) => number` | — | Calculates each entry's finite, non-negative weight | | `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 }) // Weight-only cache: units are serialized UTF-8 bytes const weightedCache = new MemoryCache({ maxSize: 0, maxWeight: 10 * 1024 * 1024, sizeCalculation: (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength }) ``` `maxWeight` limits aggregate weight, not exact retained heap memory. The application defines the unit through `sizeCalculation(value, key)` and should use one consistent unit for the cache. `maxSize` and `maxWeight` are independent: when both are active, LRU entries are evicted until both constraints hold. Because `maxSize` still defaults to `100`, set it to `0` for weight-only limits. ### 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. Before mutation, weighted caches calculate and validate the candidate weight exactly once. If either active limit would be exceeded, expired entries are pruned and then LRU entries are evicted until the new value fits. One insertion may therefore cause multiple evictions. 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 ``` For an update, the old weight is removed from the projected aggregate and the updated key is protected while other LRU entries make room. A value whose weight exceeds `maxWeight` is not cached. If it replaces an existing key, that old entry is removed as one eviction so a later `get` cannot return stale data. If `sizeCalculation` throws, its error is propagated. A negative, `NaN`, or infinite result throws `RangeError`. In either case the cache, LRU order, statistics, aggregate weight, expiration queue, and hooks remain unchanged. ### 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, weight: 0 } ``` ### resetStats() Resets all statistics counters to zero. Does not affect cached data or the current `weight` gauge. ```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 weight: number } ``` `weight` is the current aggregate after expired entries are pruned. It is `0` when weighted mode is disabled or the cache is empty, and is not reset by `resetStats()`. ### 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 - `maxWeight` is negative or `NaN` - `maxWeight` is positive without `sizeCalculation` - `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 entry-count or weight limits | `{ 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 byteLength: number } const apiCache = new MemoryCache>({ maxSize: 0, maxWeight: 10 * 1024 * 1024, // 10 MiB aggregate serialized payload weight sizeCalculation: (value) => value.byteLength, 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() const byteLength = new TextEncoder().encode(JSON.stringify(data)).byteLength apiCache.set(url, { data, cachedAt: Date.now(), byteLength }) 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: 0, maxWeight: 10 * 1024 * 1024, sizeCalculation: (value) => value.byteLength, 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) - **Weighted Eviction**: Use one deterministic unit, such as UTF-8 serialized payload bytes; this is not exact retained heap memory - **Entry Count**: `maxSize` still defaults to 100, so set `maxSize: 0` for weight-only operation or configure both limits intentionally - **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) and weighted 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. | | `maxWeight` | `number` | `0` | Maximum aggregate weight. Set to `0` to disable weighted eviction. | | `sizeCalculation` | `(value: T, key: string) => number` | — | Returns each value's finite, non-negative application-defined weight. | | `ttl` | `number` | `300000` | Time-to-live in milliseconds. Set to `0` for no expiration. | | `hooks` | `object` | — | Lifecycle hooks (`onHit`, `onMiss`, `onSet`, `onEvict`, `onExpire`, `onDelete`) for reacting to cache events. | For a weight-only cache, explicitly disable the default entry-count limit: ```typescript const responseCache = new MemoryCache({ maxSize: 0, maxWeight: 50_000, sizeCalculation: (value) => new TextEncoder().encode(value).byteLength }) ``` Weight units come from your calculator; the cache does not estimate retained JavaScript heap size. ## 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 - **Weighted Eviction** - Limit aggregate application-defined cost while retaining LRU policy - **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) --- # Comparisons Compare Memory Cache with alternative libraries. - [node-cache](https://memory.svelte.page/compare/vs-node-cache.md): https://memory.svelte.page/compare/vs-node-cache - [lru-cache](https://memory.svelte.page/compare/vs-lru-cache.md): https://memory.svelte.page/compare/vs-lru-cache - [Keyv](https://memory.svelte.page/compare/vs-keyv.md): https://memory.svelte.page/compare/vs-keyv # Memory Cache vs Keyv A Multi-Backend Store vs an In-Process Cache ## Overview Compare Keyv and @humanspeak/memory-cache: a pluggable async key-value layer spanning Redis, SQLite, and Postgres against a synchronous zero-dependency in-process cache with LRU eviction and method memoization. - **Memory Cache site:** https://memory.svelte.page - **Memory Cache npm:** https://www.npmjs.com/package/%40humanspeak%2Fmemory-cache - **Memory Cache slug:** memory-cache - **Category:** Multi-Backend Key-Value Store - **Approach:** Async storage abstraction with pluggable adapters (memory, Redis, SQLite, Postgres, Mongo, and more) - **Website:** https://keyv.org - **GitHub:** https://github.com/jaredwray/keyv - **npm:** https://www.npmjs.com/package/keyv ## Feature comparison | Feature | @humanspeak/memory-cache | Keyv | Notes | | --- | --- | --- | --- | | Zero Dependencies | Yes | No | Keyv keeps its core small, but ships with serialization dependencies and each backend needs its own adapter package. | | TypeScript Support | Written in TypeScript | Written in TypeScript | | | Synchronous API | Sync get/set — no await on the hot path | No | Keyv is Promise-based everywhere so the same code can back onto Redis or SQLite. For purely in-process caching that indirection is overhead. | | Persistent / Shared Backends | No | Redis, SQLite, Postgres, Mongo, etcd, … | This is Keyv’s core value proposition — Memory Cache is in-process by design and does not try to compete here. | | TTL Expiration | Per cache instance | Per-entry ttl argument on set() | | | LRU Eviction | Yes | Store-dependent | Keyv’s default in-memory Map never evicts; bounded behavior requires bringing an LRU-capable store yourself. | | Wildcard / Prefix Deletion | deleteByMagicString + deleteByPrefix | clear() per namespace | | | Method Memoization Decorator | @cached, async-aware | No | | | Async Fetch De-Duplication | getOrSet() collapses concurrent lookups | No | | | Built-In Statistics | getStats() aggregate counters | No | | | Namespaces | Key prefixes + prefix deletion | First-class namespaces | | | Value Compression | No | @keyv/compress-brotli / gzip adapters | | ## Memory Cache strengths - Zero runtime dependencies — works in Node and the browser - TypeScript-first with fully typed generics (MemoryCache) - TTL expiration and true LRU eviction in one cache — expired entries are pruned before any valid key is evicted - Wildcard + prefix bulk invalidation (deleteByMagicString, deleteByPrefix) - @cached decorator for method-level memoization — async-aware with in-flight de-duplication - getOrSet() async fetch helper that de-duplicates concurrent lookups for the same key - Lifecycle hooks (onHit, onMiss, onSet, onDelete, onExpire, onEvict) plus built-in getStats() - Synchronous reads and writes — no await on the hot path ## Keyv strengths - Swap storage backends (memory to Redis to SQLite) without changing call sites - Persistence and cross-process sharing when you outgrow one process - Per-entry TTL on every set() plus first-class namespaces - Actively maintained with a broad, well-documented adapter ecosystem ## Memory Cache limitations - Smaller community (newer project) - In-process only — no persistence or multi-process sharing by design - TTL is configured per cache instance, not per entry ## Keyv limitations - Async API adds Promise overhead to purely in-process caching - Default memory store is an unbounded Map — no eviction policy built in - Values are serialized by default, a cost Memory Cache never pays in-process - No memoization decorator, fetch de-duplication, or cache statistics ## Verdict Keyv and Memory Cache solve different problems. Keyv is a storage abstraction: reach for it when cached state must survive restarts or be shared across processes, and you want Redis today with SQLite tomorrow. Memory Cache is a cache: bounded memory, LRU + TTL, synchronous hot-path reads, memoization, and stats, all inside one process. If you are using Keyv’s default memory store just to cache computed values, Memory Cache does that job with less ceremony and real eviction. ## Keywords keyv alternative, keyv vs memory-cache, in-memory cache vs redis, key-value store node.js, typescript cache library, in-process caching # Memory Cache vs lru-cache The Canonical LRU vs a Batteries-Included Cache ## Overview Compare lru-cache and @humanspeak/memory-cache: the canonical LRU implementation against a TypeScript cache that layers wildcard invalidation, lifecycle hooks, statistics, and a @cached decorator on top of LRU + TTL. - **Memory Cache site:** https://memory.svelte.page - **Memory Cache npm:** https://www.npmjs.com/package/%40humanspeak%2Fmemory-cache - **Memory Cache slug:** memory-cache - **Category:** LRU Cache - **Approach:** Highly optimized bounded map with recency tracking, size-aware eviction, and async fetch() - **GitHub:** https://github.com/isaacs/node-lru-cache - **npm:** https://www.npmjs.com/package/lru-cache ## Feature comparison | Feature | @humanspeak/memory-cache | lru-cache | Notes | | --- | --- | --- | --- | | Zero Dependencies | Yes | Yes | | | TypeScript Support | Written in TypeScript | Written in TypeScript | | | LRU Eviction | Yes | Yes | lru-cache is the canonical, heavily tuned implementation. Memory Cache prunes expired entries before evicting any valid least-recently-used key. | | TTL Expiration | Per cache instance | Per cache + per-entry overrides | lru-cache also supports allowStale and updateAgeOnGet for finer recency/staleness control. | | Size-Aware Eviction | Entry count + computed weight (maxSize + maxWeight / sizeCalculation) | Entry count + computed size (max + maxSize / sizeCalculation) | Both accept a user-supplied calculator. Memory Cache exposes aggregate weight in getStats(); lru-cache has broader size and disposal tuning. | | Wildcard / Prefix Deletion | deleteByMagicString + deleteByPrefix | No | | | Method Memoization Decorator | @cached, async-aware | memo() method (no decorator) | | | Async Fetch De-Duplication | getOrSet() collapses concurrent lookups | fetch() with fetchMethod | | | Stale-While-Revalidate | No | allowStale + background fetch | | | Lifecycle Instrumentation | Six lifecycle hooks | dispose / onInsert callbacks | | | Built-In Statistics | getStats() aggregate counters | Opt-in per-call status tracking | | | Browser Support | Yes | Yes | | ## Memory Cache strengths - Zero runtime dependencies — works in Node and the browser - TypeScript-first with fully typed generics (MemoryCache) - TTL expiration and true LRU eviction in one cache — expired entries are pruned before any valid key is evicted - Wildcard + prefix bulk invalidation (deleteByMagicString, deleteByPrefix) - @cached decorator for method-level memoization — async-aware with in-flight de-duplication - getOrSet() async fetch helper that de-duplicates concurrent lookups for the same key - Lifecycle hooks (onHit, onMiss, onSet, onDelete, onExpire, onEvict) plus built-in getStats() - Synchronous reads and writes — no await on the hot path ## lru-cache strengths - The canonical LRU — extremely optimized and battle-tested at npm scale - Broader size-calculation, disposal, and tuning controls - Stale-while-revalidate patterns with allowStale and async fetch() - Rich low-level controls (peek, dispose, updateAgeOnGet, per-entry TTL) ## Memory Cache limitations - Smaller community (newer project) - In-process only — no persistence or multi-process sharing by design - TTL is configured per cache instance, not per entry ## lru-cache limitations - Lower-level API with many knobs to hold correctly - No wildcard or prefix bulk invalidation - Memoization is a method helper, not a decorator you can put on class methods - No aggregate hit/miss statistics or lifecycle hook set out of the box ## Verdict Both libraries can enforce application-defined computed-weight bounds; neither automatically measures retained JavaScript heap. If your bottleneck is raw LRU throughput or you need stale-while-revalidate and extensive low-level tuning, lru-cache remains the reference implementation. Memory Cache adds a @cached decorator, wildcard invalidation, lifecycle hooks, and aggregate stats with a smaller API surface. Pick lru-cache for infrastructure-grade controls; pick Memory Cache when you want application-level caching that reads like TypeScript. ## Keywords lru-cache alternative, lru-cache vs memory-cache, lru cache typescript, lru cache decorator, javascript cache library, bounded cache node.js # Memory Cache vs node-cache The Classic TTL Cache vs a Modern TypeScript Rewrite ## Overview Compare node-cache and @humanspeak/memory-cache: two in-memory TTL caches for Node.js with different answers on LRU eviction, value cloning, TypeScript support, bulk invalidation, and method memoization. - **Memory Cache site:** https://memory.svelte.page - **Memory Cache npm:** https://www.npmjs.com/package/%40humanspeak%2Fmemory-cache - **Memory Cache slug:** memory-cache - **Category:** In-Memory TTL Cache - **Approach:** EventEmitter-based key-value store with a periodic expiry sweep (checkperiod) - **GitHub:** https://github.com/node-cache/node-cache - **npm:** https://www.npmjs.com/package/node-cache ## Feature comparison | Feature | @humanspeak/memory-cache | node-cache | Notes | | --- | --- | --- | --- | | Zero Dependencies | Yes | No | node-cache depends on clone because it deep-clones values on set and get by default (useClones), which also adds per-operation overhead. | | TypeScript Support | Written in TypeScript | Bundled typings | | | TTL Expiration | Per cache instance | stdTTL + per-key override | node-cache lets ttl(key, ttl) override the default per key. Memory Cache sets one TTL per cache instance — create purpose-scoped caches for different lifetimes. | | LRU Eviction | Yes | No | When node-cache reaches maxKeys, set() throws ECACHEFULL instead of evicting. Memory Cache evicts the least recently used key after pruning expired entries. | | Bounded Memory | maxSize with LRU eviction | maxKeys (set() throws when full) | | | Wildcard / Prefix Deletion | deleteByMagicString + deleteByPrefix | No | | | Method Memoization Decorator | @cached, async-aware | No | | | Async Fetch De-Duplication | getOrSet() collapses concurrent lookups | No | | | Lifecycle Instrumentation | Six lifecycle hooks | EventEmitter events (set, del, expired, flush) | | | Built-In Statistics | Yes | Yes | Both track hits and misses. node-cache adds rough key/value size estimates; Memory Cache adds eviction and expiration counters. | | Browser Support | Yes | Node-focused | | | Active Maintenance | Yes | Dormant (last release v5.1.2, 2020) | | ## Memory Cache strengths - Zero runtime dependencies — works in Node and the browser - TypeScript-first with fully typed generics (MemoryCache) - TTL expiration and true LRU eviction in one cache — expired entries are pruned before any valid key is evicted - Wildcard + prefix bulk invalidation (deleteByMagicString, deleteByPrefix) - @cached decorator for method-level memoization — async-aware with in-flight de-duplication - getOrSet() async fetch helper that de-duplicates concurrent lookups for the same key - Lifecycle hooks (onHit, onMiss, onSet, onDelete, onExpire, onEvict) plus built-in getStats() - Synchronous reads and writes — no await on the hot path ## node-cache strengths - Battle-tested with a huge install base and years of production use - Per-key TTL overrides (ttl, getTtl) and take() for read-and-delete - Value cloning by default isolates cached data from later mutations - Batch operations (mget, mset) out of the box ## Memory Cache limitations - Smaller community (newer project) - In-process only — no persistence or multi-process sharing by design - TTL is configured per cache instance, not per entry ## node-cache limitations - No eviction policy — a full cache throws ECACHEFULL instead of making room - Deep-clones values on every set and get by default, a real cost for large objects - Dormant maintenance — no substantial release since 2020 - No bulk invalidation, memoization decorator, or TypeScript-native API ## Verdict node-cache is the incumbent for a reason: it is simple, stable, and everywhere, and its per-key TTL overrides are genuinely handy. But it has been dormant since 2020, clones every value by default, and throws when full instead of evicting. If you want a bounded cache that manages its own memory, typed generics, wildcard invalidation, and a @cached decorator, Memory Cache is the modern replacement. If you only need a tiny TTL map and already depend on node-cache, there is no urgent reason to switch. ## Keywords node-cache alternative, node-cache vs memory-cache, in-memory cache node.js, ttl cache typescript, lru eviction node-cache, node cache comparison