@objectstack/service-cache
v6.9.0
Published
Cache Service for ObjectStack — implements ICacheService with in-memory and Redis adapters
Maintainers
Readme
@objectstack/service-cache
Cache Service for ObjectStack — implements ICacheService with in-memory and Redis adapters.
Features
- Multiple Adapters: In-memory (development) and Redis (production) support
- Type-Safe: Full TypeScript support with generic value types
- TTL Support: Automatic expiration with time-to-live
- Namespace Support: Organize cache keys by namespace
- Pattern Matching: Delete keys by pattern (e.g.,
user:*) - Statistics: Track hit/miss rates and memory usage
- JSON Serialization: Automatic serialization of complex objects
Installation
pnpm add @objectstack/service-cacheFor Redis adapter:
pnpm add ioredisBasic Usage
import { defineStack } from '@objectstack/spec';
import { ServiceCache } from '@objectstack/service-cache';
const stack = defineStack({
services: [
ServiceCache.configure({
adapter: 'memory', // or 'redis'
defaultTTL: 300, // 5 minutes
}),
],
});Configuration
In-Memory Adapter (Development)
ServiceCache.configure({
adapter: 'memory',
defaultTTL: 300,
maxSize: 1000, // Maximum number of entries
});Redis Adapter (Production)
ServiceCache.configure({
adapter: 'redis',
redis: {
host: 'localhost',
port: 6379,
password: process.env.REDIS_PASSWORD,
db: 0,
},
defaultTTL: 600,
});Service API
// Get cache service
const cache = kernel.getService<ICacheService>('cache');Set/Get Operations
// Set a value
await cache.set('user:123', { name: 'John', email: '[email protected]' });
// Set with custom TTL (in seconds)
await cache.set('session:abc', sessionData, { ttl: 3600 }); // 1 hour
// Get a value
const user = await cache.get('user:123');
// Get with type safety
const user = await cache.get<User>('user:123');
// Get multiple keys
const users = await cache.mget(['user:123', 'user:456']);Existence & Deletion
// Check if key exists
const exists = await cache.has('user:123');
// Delete a key
await cache.del('user:123');
// Delete multiple keys
await cache.del(['session:abc', 'session:def']);
// Delete by pattern
await cache.delPattern('user:*');Namespaced Operations
// Create a namespaced cache instance
const userCache = cache.namespace('user');
// Set in namespace (key becomes 'user:123')
await userCache.set('123', userData);
// Get from namespace
const user = await userCache.get('123');
// Clear entire namespace
await userCache.clear();TTL Management
// Get remaining TTL (in seconds)
const ttl = await cache.ttl('session:abc');
// Update TTL
await cache.expire('session:abc', 7200); // 2 hours
// Make key permanent (remove expiration)
await cache.persist('user:123');Atomic Operations
// Increment (useful for counters)
await cache.incr('page:views:123'); // Returns new value
// Increment by amount
await cache.incrby('score:user:123', 10);
// Decrement
await cache.decr('inventory:product:456');Batch Operations
// Set multiple keys at once
await cache.mset({
'user:123': user1Data,
'user:456': user2Data,
'user:789': user3Data,
});
// Get multiple keys
const users = await cache.mget(['user:123', 'user:456', 'user:789']);Advanced Features
Cache Aside Pattern
async function getUser(id: string): Promise<User> {
// Try cache first
const cached = await cache.get<User>(`user:${id}`);
if (cached) return cached;
// Load from database
const user = await db.findUser(id);
// Store in cache
await cache.set(`user:${id}`, user, { ttl: 600 });
return user;
}Cache-Through Pattern
async function getUserCacheThrough(id: string): Promise<User> {
return cache.getOrSet(`user:${id}`, async () => {
return await db.findUser(id);
}, { ttl: 600 });
}Invalidation on Write
async function updateUser(id: string, data: Partial<User>) {
// Update database
await db.updateUser(id, data);
// Invalidate cache
await cache.del(`user:${id}`);
// Or update cache immediately
const updated = await db.findUser(id);
await cache.set(`user:${id}`, updated);
}Tagging & Invalidation
// Tag cache entries
await cache.set('product:123', productData, {
ttl: 600,
tags: ['products', 'category:electronics'],
});
// Invalidate by tag
await cache.invalidateTag('category:electronics');Statistics & Monitoring
// Get cache statistics
const stats = await cache.stats();
// {
// hits: 1250,
// misses: 325,
// hitRate: 0.794,
// keys: 450,
// memoryUsage: 1024000 // bytes
// }
// Reset statistics
await cache.resetStats();REST API Endpoints
GET /api/v1/cache/stats # Get cache statistics
POST /api/v1/cache/clear # Clear cache
DELETE /api/v1/cache/:key # Delete specific key
DELETE /api/v1/cache/pattern/:pattern # Delete by patternBest Practices
- Use Namespaces: Organize cache keys with namespaces
- Set Appropriate TTLs: Don't cache data longer than necessary
- Handle Misses: Always have fallback logic when cache misses
- Invalidate on Write: Clear stale cache after updates
- Monitor Hit Rates: Track cache effectiveness with statistics
- Serialize Carefully: Be mindful of what you serialize (avoid circular references)
- Use Redis in Production: In-memory adapter is for development only
Performance Considerations
- In-Memory Adapter: Fast but limited by server memory, not shared across instances
- Redis Adapter: Shared across instances, persistent, but network latency
- TTL Strategy: Balance between freshness and cache hit rate
- Key Patterns: Use consistent naming conventions for easier invalidation
Contract Implementation
Implements ICacheService from @objectstack/spec/contracts:
interface ICacheService {
get<T>(key: string): Promise<T | null>;
set<T>(key: string, value: T, options?: CacheOptions): Promise<void>;
del(key: string | string[]): Promise<void>;
has(key: string): Promise<boolean>;
ttl(key: string): Promise<number>;
expire(key: string, ttl: number): Promise<void>;
clear(): Promise<void>;
namespace(name: string): ICacheService;
}License
Apache-2.0. See LICENSING.md.
