# Lockness Cache High-performance caching system with multiple driver support (Memory, Deno KV, Redis) and tagging capabilities. ## Overview @lockness/cache provides a unified caching API with: - **Simple API** - get, set, remember, forget, flush - **Multiple Drivers** - Memory (fast, volatile), Deno KV (persistent), Redis (distributed) - **TTL Support** - Automatic expiration with time-to-live - **Tagging** - Group cache entries for batch invalidation - **Auto Serialization** - Transparent JSON serialization/deserialization - **Counters** - Increment/decrement for rate limiting and metrics - **Batch Operations** - Get/set multiple keys efficiently ## Configuration Le système de cache se configure directement dans votre **Kernel** via le décorateur `@Kernel`. ```typescript // app/kernel.tsx @Kernel({ // Configuration simple (utilise les valeurs par défaut : memory, ttl 3600) cache: true, // OU Configuration détaillée cache: { driver: 'deno-kv', ttl: 86400, // 24 heures kvPath: './data/cache.db' } }) export class AppKernel { ... } ``` ### Options de configuration | Option | Type | Par défaut | Description | | :------- | :------- | :----------- | :--------------------------------------------------------------------------------------------------------------------- | | `driver` | `string` | `'memory'` | `'memory'`, `'deno-kv'` ou `'redis'` | | `ttl` | `number` | `3600` | Durée de vie par défaut en secondes | | `kvPath` | `string` | `undefined` | Chemin vers la DB Deno KV. Recommandé de laisser `undefined` sur Deno Deploy. Peut être piloté par `DATABASE_KV_PATH`. | | `prefix` | `string` | `'lockness'` | Préfixe pour toutes les clés de cache | --- ## Decorator-based Caching Lockness provides a powerful decorator-based caching system that allows you to cache entire controller responses with minimal code. This is the recommended way to handle route-level caching. ### @Cache Decorator The `@Cache` decorator can be applied to any controller method to enable caching. ```typescript import { Cache, Controller, Get } from '@lockness/core' @Controller('/products') export class ProductController { @Get('/:id') @Cache({ ttl: 600, strategy: 'server' }) async show(c: Context) { return c.json(await Product.find(c.req.param('id'))) } } ``` ### Caching Strategies The `strategy` option determines where the cache is stored: 1. **`server` (Default)**: The response is stored on the server (In-memory, Deno KV, or Redis). When a cached response exists, the controller method is **not executed**, and the cached result is returned immediately. 2. **`http`**: Sets HTTP `Cache-Control` headers (e.g., `max-age=3600`). This tells the browser and intermediate CDNs (like Cloudflare) to cache the response. The controller method **still executes** if the CDN/browser cache is bypassed or expired. 3. **`both`**: Combines both strategies. The server caches the result to avoid database hits, and the headers are set to enable CDN caching. ### Helper Decorators For simpler use cases, you can use these shorthand decorators: - **`@CacheTTL(seconds)`**: Sets the expiration time. - **`@CacheKey(key)`**: Sets a custom cache key (defaults to the request URL). ```typescript @Get('/stats') @CacheTTL(300) @CacheKey('api:global:stats') @Cache({ strategy: 'both' }) async stats(c: Context) { ... } ``` ### How Server-side Caching Works When using the `server` or `both` strategy, Lockness: 1. Generates a cache key (the URL or your custom key). 2. Checks the global `ICache` provider (resolved from the container). 3. If a hit occurs, it returns a new `Response` object with the cached body, headers, and status code. 4. If a miss occurs, it executes the route handler. 5. If the resulting response is successful (`ok`), it clones the response and stores it in the cache for next time. --- ## Basic Operations ### Get and Set ```typescript import { flush, forget, get, has, set } from '@lockness/cache' // Set a value (with default TTL) await set('user:1', { name: 'John', email: 'john@example.com' }) // Set with custom TTL (5 minutes) await set('session:abc', sessionData, 300) // Get a value const user = await get('user:1') // Check if exists if (await has('user:1')) { console.log('User cached') } // Delete a key await forget('user:1') // Clear all cache await flush() ``` ### Cache Forever ```typescript import { forever } from '@lockness/cache' // Cache without expiration await forever('config', configData) // Same as set() with ttl=0 await set('permanent', data, 0) ``` ## Remember Pattern Cache expensive operations automatically: ```typescript import { remember, rememberForever } from '@lockness/cache' // Cache database query result const users = await remember('all-users', async () => { return await db.select().from(users).all() }, 3600) // Sync callbacks also work const config = await remember('app-config', () => { return JSON.parse(Deno.readTextFileSync('config.json')) }, 3600) // Cache forever const settings = await rememberForever('settings', async () => { return await db.select().from(settings).all() }) ``` How it works: 1. Check if key exists in cache 2. If exists, return cached value 3. If not, execute callback 4. Store result in cache with TTL 5. Return result ## Tagging System Group related cache entries for batch invalidation: ```typescript import { forgetByTag, set } from '@lockness/cache' // Tag individual entries await set('post:1', post1, 3600, ['posts', 'featured']) await set('post:2', post2, 3600, ['posts']) await set('user:1', user1, 3600, ['users']) // Invalidate all posts (both post:1 and post:2 deleted) await forgetByTag('posts') // user:1 remains cached ``` ### Fluent Tagged Cache Create a cache store with automatic tagging: ```typescript import { cache } from '@lockness/cache' const postsCache = cache('posts') // All operations automatically tagged with 'posts' await postsCache.set('post:1', post) await postsCache.set('post:2', post) const featured = await postsCache.remember('featured', async () => { return await db.query.posts.findFirst({ where: eq(posts.featured, true), }) }, 600) // Flush all posts cache await postsCache.flush() ``` ### Multiple Tags ```typescript const cache = cache('posts', 'homepage') await cache.set('featured', featuredPosts) await cache.set('recent', recentPosts) // Flush by any tag await forgetByTag('homepage') // Deletes both featured and recent ``` ## Counter Operations Perfect for rate limiting, metrics, and counters: ```typescript import { decrement, increment } from '@lockness/cache' // Page view counter await increment('page:123:views') await increment('page:123:views', 5) // Increment by 5 // Rate limiting const requests = await increment(`api:user:${userId}:requests`) if (requests > 100) { throw new Error('Rate limit exceeded') } // Set TTL on first increment if (requests === 1) { await set(`api:user:${userId}:requests`, requests, 60) // Reset after 1 minute } // Inventory management await decrement('inventory:item:456') await decrement('inventory:item:456', 10) // Decrement by 10 ``` ## Batch Operations Efficiently handle multiple keys: ```typescript import { many, putMany } from '@lockness/cache' // Get multiple keys const values = await many(['user:1', 'user:2', 'user:3']) // Returns: { 'user:1': {...}, 'user:2': {...}, 'user:3': null } // Set multiple keys await putMany({ 'setting:theme': 'dark', 'setting:lang': 'en', 'setting:notifications': true, }, 3600) ``` ## Additional Helpers ### Add (Set if Not Exists) ```typescript import { add } from '@lockness/cache' // Useful for distributed locks const added = await add('lock:process', true, 60) if (!added) { console.log('Lock already exists, process running elsewhere') return } // Proceed with locked operation try { // ... critical section } finally { await forget('lock:process') } ``` ### Pull (Get and Delete) ```typescript import { pull } from '@lockness/cache' // Get value and delete in one operation const token = await pull('token:abc') if (token) { // Use token (now deleted from cache) } ``` ### Aliases ```typescript import { put } from '@lockness/cache' // put() is an alias for set() await put('key', 'value', 600) ``` ## Drivers ### Memory Driver (Default) Fast in-memory cache, not persistent: ```typescript configureCache({ driver: 'memory' }) ``` **Pros:** - Fastest performance (no I/O) - No external dependencies - Great for development **Cons:** - Lost on restart - Limited by available RAM - Single process only (no sharing between workers) **Best for:** - Development - Single-instance deployments - Temporary data (sessions, rate limiting) ### Deno KV Driver Persistent cache using Deno's built-in KV store: ```typescript configureCache({ driver: 'deno-kv', kvPath: './data/cache.db', // Optional, defaults to system location }) ``` **Pros:** - Persistent across restarts - Built-in to Deno runtime - Atomic operations - Works with Deno Deploy **Cons:** - Slightly slower than memory (disk I/O) - Requires file system access (or Deploy KV in production) **Best for:** - Production deployments - Persistent cache data - Multi-worker setups #### Automatic Value Chunking Deno KV has a strict **64KB limit** per value. Lockness handles this transparently by splitting large values into multiple 60KB chunks. This allows you to cache large HTML pages (common in documentation) or large JSON payloads without worrying about driver limits. ### Redis Driver Distributed cache using Redis for multi-instance deployments: ```typescript import { createClient } from 'npm:redis' import { RedisCacheDriver, setCacheDriver } from '@lockness/cache' // Connect to Redis const redis = createClient({ url: 'redis://localhost:6379' }) await redis.connect() // Set the Redis driver setCacheDriver(new RedisCacheDriver(redis)) ``` **With Deno's Redis library:** ```typescript import { connect } from 'https://deno.land/x/redis/mod.ts' import { RedisCacheDriver, setCacheDriver } from '@lockness/cache' const redis = await connect({ hostname: 'localhost', port: 6379 }) setCacheDriver(new RedisCacheDriver(redis)) ``` **With custom options:** ```typescript setCacheDriver( new RedisCacheDriver(redis, { keyPrefix: 'myapp:cache', // Custom key prefix (default: 'cache') tagPrefix: 'myapp:tag', // Custom tag prefix (default: 'tag') serialize: JSON.stringify, // Custom serializer deserialize: JSON.parse, // Custom deserializer }), ) ``` **Pros:** - Shared across multiple instances - High performance (in-memory) - Persistent (with Redis persistence) - Rich data structure support - Cluster support for scaling **Cons:** - Requires Redis server - Network latency - Additional infrastructure **Best for:** - Multi-instance deployments - Microservices architecture - High-traffic applications - Shared session storage ## Complete Usage Example ```typescript import { cache, configureCache, forgetByTag, increment, remember, } from '@lockness/cache' // Configure cache configureCache({ driver: 'deno-kv', ttl: 3600, prefix: 'blog', kvPath: './data/cache.db', }) // Create tagged cache stores const postsCache = cache('posts') const usersCache = cache('users') // Cache expensive queries const allPosts = await postsCache.remember('all', async () => { return await db.query.posts.findMany() }, 300) // Cache with multiple tags await set('post:123', post, 600, ['posts', 'featured']) // Rate limiting async function checkRateLimit(userId: string): Promise { const key = `rate:${userId}` const count = await increment(key) if (count === 1) { await set(key, count, 60) // Reset after 1 minute } return count <= 100 // Max 100 requests per minute } // Invalidate related caches async function updatePost(id: number, data: Partial) { await db.update(posts).set(data).where(eq(posts.id, id)) // Invalidate all post caches await forgetByTag('posts') } // Session caching await set(`session:${sessionId}`, sessionData, 7200) // 2 hours // Feature flags const features = await rememberForever('feature-flags', async () => { return await db.select().from(featureFlags).all() }) ``` ## Testing Clear cache between tests: ```typescript import { flush, MemoryCacheDriver } from '@lockness/cache' Deno.test('my test', async () => { // Clear memory cache MemoryCacheDriver.clear() // Or use flush() for any driver await flush() // Your test... }) ``` ## API Reference ### Configuration ```typescript configureCache(options: { driver: 'memory' | 'deno-kv' ttl?: number // Default TTL in seconds prefix?: string // Key prefix for namespacing kvPath?: string // Deno KV file path (for deno-kv driver) }) ``` ### Core Functions ```typescript // Get value get(key: string): Promise // Set value set(key: string, value: T, ttl?: number, tags?: string[]): Promise // Check existence has(key: string): Promise // Delete key forget(key: string): Promise // Clear all cache flush(): Promise // Cache callback result remember(key: string, callback: () => T | Promise, ttl?: number, tags?: string[]): Promise // Cache callback result forever rememberForever(key: string, callback: () => T | Promise, tags?: string[]): Promise // Get multiple keys many(keys: string[]): Promise> // Set multiple keys putMany(values: Record, ttl?: number): Promise // Increment counter increment(key: string, amount?: number): Promise // Decrement counter decrement(key: string, amount?: number): Promise // Add if not exists add(key: string, value: T, ttl?: number, tags?: string[]): Promise // Get and delete pull(key: string): Promise // Alias for set put(key: string, value: T, ttl?: number, tags?: string[]): Promise // Set forever forever(key: string, value: T, tags?: string[]): Promise // Delete by tag forgetByTag(tag: string): Promise // Alias for forgetByTag flushByTag(tag: string): Promise ``` ### Tagged Cache Store ```typescript // Create tagged store cache(...tags: string[]): CacheStore // CacheStore methods interface CacheStore { get(key: string): Promise set(key: string, value: T, ttl?: number): Promise has(key: string): Promise forget(key: string): Promise remember(key: string, callback: () => T | Promise, ttl?: number): Promise rememberForever(key: string, callback: () => T | Promise): Promise flush(): Promise } ``` ## Best Practices - **Use memory driver in development**, Deno KV in production - **Set appropriate TTLs** - shorter for frequently changing data, longer for static data - **Use tagging** to group related cache entries for easy invalidation - **Use remember()** pattern to simplify caching logic - **Add rate limiting** with increment/decrement - **Namespace keys** with prefixes (e.g., 'user:', 'post:') - **Clear cache** on deployments if schema changes - **Monitor cache hit rates** in production - **Use cache for expensive operations** (DB queries, API calls, computations) - **Invalidate strategically** - only clear what changed ## Common Use Cases - **Database query caching** - Cache expensive queries - **API response caching** - Cache third-party API responses - **Rate limiting** - Limit requests per user/IP - **Session storage** - Store session data - **Page view counters** - Track views/visits - **Feature flags** - Cache feature configuration - **User preferences** - Cache user settings - **Computed results** - Cache expensive computations - **Distributed locks** - Coordinate across workers