@humanspeak/memory-cache
v1.2.0
Published
A lightweight, zero-dependency in-memory cache for TypeScript and JavaScript with TTL expiration, LRU eviction, wildcard pattern deletion, and a powerful @cached decorator for method-level memoization. Perfect for API response caching, session storage, an
Maintainers
Keywords
Readme
@humanspeak/memory-cache
A lightweight, zero-dependency in-memory cache for TypeScript and JavaScript
A powerful, feature-rich in-memory caching solution with TTL expiration, true LRU (Least Recently Used) eviction, wildcard pattern deletion, and a @cached decorator for effortless method-level memoization. Perfect for API response caching, session storage, expensive computation caching, and performance optimization.
Visit the documentation for detailed API reference and examples.
Features
- Zero Dependencies - Lightweight and fast
- TTL Expiration - Automatic cache entry expiration
- LRU Eviction - Least recently used entries are evicted when cache is full
- Weighted Eviction - Bound aggregate user-defined cost, including serialized bytes
- Wildcard Deletion - Delete entries by prefix or wildcard patterns
- Full TypeScript Support - Complete type definitions included
- Method Decorator -
@cacheddecorator for automatic memoization - Null/Undefined Support - Properly caches falsy values
- Cache Statistics - Track hits, misses, evictions, and expirations
- Introspection - Query cache size, keys, values, and entries
- Lifecycle Hooks - Observe cache events for monitoring and debugging
Installation
npm install @humanspeak/memory-cachepnpm add @humanspeak/memory-cacheyarn add @humanspeak/memory-cacheQuick Start
Basic Usage
import { MemoryCache } from '@humanspeak/memory-cache'
// Create a cache with default options (100 entries, 5 minute TTL)
const cache = new MemoryCache<string>()
// Or customize the options
const customCache = new MemoryCache<string>({
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')) {
// Key exists and hasn't expired
}
// Delete entries
cache.delete('user:123')
cache.clear() // Remove all entriesWildcard Pattern Deletion
const cache = new MemoryCache<string>()
cache.set('user:123:name', 'John')
cache.set('user:123:email', '[email protected]')
cache.set('user:456:name', 'Jane')
cache.set('post:789', 'Hello World')
// Delete by prefix
cache.deleteByPrefix('user:123:') // Removes user:123:name and user:123:email
// Delete by wildcard pattern
cache.deleteByMagicString('user:*:name') // Removes all user names
cache.deleteByMagicString('*:123:*') // Removes all entries with :123:Method Decorator
import { cached } from '@humanspeak/memory-cache'
class UserService {
@cached<User>({ ttl: 60000, maxSize: 100 })
async getUser(id: string): Promise<User> {
// This expensive operation will be cached
return await database.findUser(id)
}
@cached<string[]>()
getUserPermissions(userId: string, role: string): string[] {
// Results are cached per unique argument combination
return computePermissions(userId, role)
}
}
const service = new UserService()
// First call - executes the method
await service.getUser('123')
// Second call - returns cached result
await service.getUser('123')Async decorated methods use single-flight behavior: concurrent calls with the same arguments share one in-flight method execution. Resolved values are cached, while rejected promises are not cached by default, so a later call can retry.
const [a, b] = await Promise.all([service.getUser('popular-user'), service.getUser('popular-user')])
// database.findUser ran once; both callers received the same resultCustom Key Generator
Use keyGenerator to control how cache keys are derived from arguments:
class UserService {
@cached<string>({ keyGenerator: (args) => args[0].id })
getDisplayName(user: { id: string; name: string }): string {
return computeDisplayName(user)
}
}Hashed Keys
Use hashKeys for shorter, fixed-length keys (useful with complex arguments):
class AnalyticsService {
@cached<Report>({ hashKeys: true, ttl: 60000 })
generateReport(filters: ComplexFilterObject): Report {
return buildReport(filters)
}
}When both
keyGeneratorandhashKeysare set,keyGeneratortakes precedence.
Async Fetch with getOrSet
import { MemoryCache } from '@humanspeak/memory-cache'
const cache = new MemoryCache<User>({ ttl: 60000 })
// Automatically fetch and cache on miss
const user = await cache.getOrSet('user:123', async () => {
return await fetchUserFromDB(123)
})
// Concurrent requests share the same fetch (thundering herd prevention)
const promises = Array.from({ length: 100 }, () =>
cache.getOrSet('popular-key', fetchExpensiveData)
)
await Promise.all(promises) // fetchExpensiveData called only onceAPI Reference
MemoryCache<T>
Constructor Options
| Option | Type | Default | Description |
| ----------------- | ----------------------------------- | -------- | ------------------------------------------------ |
| maxSize | number | 100 | Maximum entries before eviction (0 = unlimited) |
| maxWeight | number | 0 | Maximum aggregate weight (0 = disabled) |
| sizeCalculation | (value: T, key: string) => number | — | Returns 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 |
Methods
| Method | Description |
| ------------------------------ | ------------------------------------------------------- |
| get(key) | Retrieves a value from the cache |
| set(key, value) | Stores a value, pruning expired entries before eviction |
| getOrSet(key, fetcher) | Gets cached value or fetches and caches on miss |
| has(key) | Checks if a key exists (useful for cached undefined) |
| delete(key) | Removes a specific entry |
| deleteAsync(key) | Async version of delete |
| clear() | Removes all entries |
| deleteByPrefix(prefix) | Removes entries starting with prefix |
| deleteByMagicString(pattern) | Removes entries matching wildcard pattern |
| size() | Returns the number of entries in cache |
| keys() | Returns array of all cache keys |
| values() | Returns array of all cached values |
| entries() | Returns array of [key, value] pairs |
| getStats() | Returns cache statistics (hits, misses, etc.) |
| resetStats() | Resets statistics counters to zero |
| prune() | Removes all expired entries, returns count |
@cached<T>(options?)
A method decorator for automatic result caching.
@cached<ReturnType>({ ttl: 60000, maxSize: 100 })
methodName(args): ReturnType { ... }Configuration Examples
// High-traffic API cache
interface ApiResponse {
data: unknown
cachedAt: number
}
const apiCache = new MemoryCache<ApiResponse>({
maxSize: 0, // Disable the default 100-entry limit for weight-only operation
maxWeight: 10 * 1024 * 1024, // 10 MiB of serialized response data
sizeCalculation: (response) => new TextEncoder().encode(JSON.stringify(response)).byteLength,
ttl: 5 * 60 * 1000 // 5 minutes
})
// Session storage (longer TTL, smaller size)
const sessionCache = new MemoryCache<Session>({
maxSize: 1000,
ttl: 30 * 60 * 1000 // 30 minutes
})
// Computation cache (no TTL, size-limited)
const computeCache = new MemoryCache<Result>({
maxSize: 500,
ttl: 0 // No expiration
})
// Unlimited cache (use with caution)
const unlimitedCache = new MemoryCache<Data>({
maxSize: 0, // No size limit
ttl: 0 // No expiration
})maxWeight is a deterministic aggregate limit in units chosen by
sizeCalculation(value, key); it is not automatic JavaScript heap measurement.
Because maxSize still defaults to 100, set maxSize: 0 when weight should be
the only capacity limit. Both limits can also be active together, in which case
LRU entries are evicted until both constraints hold.
The calculator runs once before each set. It must return a finite,
non-negative number; invalid results throw RangeError without changing the
cache. A value heavier than maxWeight is returned normally by getOrSet or a
decorated method but is not cached. Replacing an existing key with an oversized
value removes the old cached value so stale data cannot be returned.
Cache Statistics
Track cache performance with built-in statistics:
const cache = new MemoryCache<string>()
cache.set('key', 'value')
cache.get('key') // hit
cache.get('missing') // miss
const stats = cache.getStats()
// { hits: 1, misses: 1, evictions: 0, expirations: 0, size: 1, weight: 0 }
// Reset statistics
cache.resetStats()
// Proactively remove expired entries
const prunedCount = cache.prune()When ttl and maxSize are both configured, writes reclaim expired entries
before evicting the least recently used valid entry:
const cache = new MemoryCache<string>({ maxSize: 2, ttl: 1000 })
cache.set('stale', 'old')
// ... 750ms pass ...
cache.set('fresh', 'new')
// ... another 300ms pass; stale expires, fresh is still valid ...
cache.set('next', 'value') // prunes stale; fresh remains cachedCache Hooks
Monitor cache lifecycle events with optional hooks:
const cache = new MemoryCache<string>({
maxSize: 100,
ttl: 60000,
hooks: {
onHit: ({ key, value }) => console.log(`Cache hit: ${key}`),
onMiss: ({ key, reason }) => console.log(`Cache miss: ${key} (${reason})`),
onSet: ({ key, isUpdate }) => console.log(`Set: ${key} ${isUpdate ? '(update)' : '(new)'}`),
onEvict: ({ key }) => console.log(`Evicted: ${key}`),
onExpire: ({ key, source }) => console.log(`Expired: ${key} via ${source}`),
onDelete: ({ key, source }) => console.log(`Deleted: ${key} via ${source}`)
}
})Hook Events
| Hook | When Called | Context |
| ---------- | ------------------------------------------------- | ------------------------------------------- |
| onHit | Successful cache retrieval | { key, value } |
| onMiss | Cache miss (not found or expired) | { key, reason: 'not_found' \| 'expired' } |
| onSet | Value stored in cache | { key, value, isUpdate } |
| onEvict | Entry evicted due to entry-count or weight limits | { key, value } |
| onExpire | Entry removed due to TTL expiration | { key, value, source } |
| onDelete | Entry explicitly deleted | { key, value, source } |
Hooks are synchronous and errors are silently caught to prevent cache corruption.
Documentation
For complete documentation, examples, and API reference, visit memory.svelte.page.
Svelte 5 ecosystem
Part of the Humanspeak family of runes-native Svelte 5 packages:
| Package | Description | | --- | --- | | @humanspeak/svelte-markdown | Runtime markdown renderer for Svelte | | @humanspeak/svelte-virtual-list | Virtual scrolling for Svelte | | @humanspeak/svelte-motion | Framer Motion for Svelte 5 | | @humanspeak/svelte-headless-table | Headless data tables for Svelte | | @humanspeak/svelte-diff | Diff comparison for Svelte | | @humanspeak/svelte-purify | HTML sanitisation for Svelte | | @humanspeak/svelte-virtual-chat | Virtual chat viewport for Svelte 5 | | @humanspeak/memory-cache — this package | In-memory cache for TypeScript | | @humanspeak/svelte-json-view-lite | JSON tree viewer for Svelte 5 | | @humanspeak/svelte-scoped-props | Scoped class props for Svelte |
License
MIT © Humanspeak, Inc.
Credits
Made with ❤️ by Humanspeak
