diff --git a/.changeset/tiny-schools-lick.md b/.changeset/tiny-schools-lick.md new file mode 100644 index 0000000..ab7512f --- /dev/null +++ b/.changeset/tiny-schools-lick.md @@ -0,0 +1,5 @@ +--- +"@node-ts-cache/core": patch +--- + +Simplify README and add storage engines table diff --git a/CLAUDE.md b/CLAUDE.md index d176d14..01bdf78 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,7 +61,7 @@ Always run these checks before committing: │ └── test/ # Test files ├── storages/ # Storage adapter packages │ ├── lru/ # LRU cache storage -│ ├── redis/ # Redis storage (redis package v3.x) +│ ├── redis/ # Redis storage (redis package v4.x) │ ├── redisio/ # Redis storage (ioredis with compression) │ ├── node-cache/ # node-cache storage │ └── lru-redis/ # Two-tier LRU + Redis storage @@ -69,6 +69,6 @@ Always run these checks before committing: ## Testing Framework -- Uses Mocha with ts-node ESM loader +- Uses Vitest - Tests use Node's built-in `assert` module - Mock Redis instances using `redis-mock` and `ioredis-mock` diff --git a/README.md b/README.md index 0e40fd1..8256350 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ This is a monorepo containing the following packages: | Package | Version | Description | | ---------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------ | -| [@node-ts-cache/redis-storage](./storages/redis) | ![npm](https://img.shields.io/npm/v/@node-ts-cache/redis-storage.svg) | Redis storage using `redis` package (v3.x) | +| [@node-ts-cache/redis-storage](./storages/redis) | ![npm](https://img.shields.io/npm/v/@node-ts-cache/redis-storage.svg) | Redis storage using `redis` package (v4.x) | | [@node-ts-cache/ioredis-storage](./storages/redisio) | ![npm](https://img.shields.io/npm/v/@node-ts-cache/ioredis-storage.svg) | Redis storage using `ioredis` with compression support | | [@node-ts-cache/node-cache-storage](./storages/node-cache) | ![npm](https://img.shields.io/npm/v/@node-ts-cache/node-cache-storage.svg) | In-memory cache using `node-cache` | | [@node-ts-cache/lru-storage](./storages/lru) | ![npm](https://img.shields.io/npm/v/@node-ts-cache/lru-storage.svg) | LRU cache with automatic eviction | @@ -96,7 +96,7 @@ For detailed documentation, see the [main package README](./ts-cache/README.md). | **FsJsonStorage** | Async | Persistent local cache | File-based, survives restarts | | **NodeCacheStorage** | Sync | Production single-instance | TTL support, multi-ops | | **LRUStorage** | Sync | Memory-constrained apps | Auto-eviction, size limits | -| **RedisStorage** | Async | Distributed systems | Shared cache, legacy redis | +| **RedisStorage** | Async | Distributed systems | Shared cache, redis v4 | | **RedisIOStorage** | Async | Distributed systems | Compression, modern ioredis | | **LRUWithRedisStorage** | Async | High-performance distributed | Local + remote tiers | diff --git a/ts-cache/ADVANCED.md b/ts-cache/ADVANCED.md new file mode 100644 index 0000000..4a02580 --- /dev/null +++ b/ts-cache/ADVANCED.md @@ -0,0 +1,416 @@ +# Advanced Usage Guide + +This document covers advanced topics for implementing custom storages, detailed storage configuration, and reference material. + +## Interface Definitions + +### Storage Interfaces + +Implement these interfaces to create custom storage backends. + +```typescript +/** + * Cache entry structure stored in backends + */ +interface ICacheEntry { + content: any; // The cached value + meta: any; // Metadata (e.g., TTL, createdAt) +} + +/** + * Asynchronous storage for single items + */ +interface IAsynchronousCacheType { + /** Retrieve an item by key. Returns undefined if not found. */ + getItem(key: string): Promise; + + /** Store an item. Pass undefined as content to delete. */ + setItem(key: string, content: C | undefined, options?: any): Promise; + + /** Clear all items from the cache. */ + clear(): Promise; +} + +/** + * Synchronous storage for single items + */ +interface ISynchronousCacheType { + /** Retrieve an item by key. Returns undefined if not found. */ + getItem(key: string): T | undefined; + + /** Store an item. Pass undefined as content to delete. */ + setItem(key: string, content: C | undefined, options?: any): void; + + /** Clear all items from the cache. */ + clear(): void; +} + +/** + * Asynchronous storage with batch operations (for @MultiCache) + */ +interface IMultiIAsynchronousCacheType { + /** Retrieve multiple items by keys. */ + getItems(keys: string[]): Promise<{ [key: string]: T | undefined }>; + + /** Store multiple items at once. */ + setItems(values: { key: string; content: C | undefined }[], options?: any): Promise; + + /** Clear all items from the cache. */ + clear(): Promise; +} + +/** + * Synchronous storage with batch operations (for @MultiCache) + */ +interface IMultiSynchronousCacheType { + /** Retrieve multiple items by keys. */ + getItems(keys: string[]): { [key: string]: T | undefined }; + + /** Store multiple items at once. */ + setItems(values: { key: string; content: C | undefined }[], options?: any): void; + + /** Clear all items from the cache. */ + clear(): void; +} +``` + +### Key Strategy Interfaces + +```typescript +/** + * Synchronous key generation strategy + */ +interface ISyncKeyStrategy { + getKey(className: string, methodName: string, args: any[]): string | undefined; +} + +/** + * Asynchronous key generation strategy + */ +interface IAsyncKeyStrategy { + getKey( + className: string, + methodName: string, + args: any[] + ): Promise | string | undefined; +} +``` + +## Detailed Storage Configuration + +### MemoryStorage + +Simple in-memory storage using a JavaScript object. + +```typescript +import { MemoryStorage, ExpirationStrategy } from '@node-ts-cache/core'; + +const storage = new MemoryStorage(); +const strategy = new ExpirationStrategy(storage); +``` + +**Characteristics:** + +- Synchronous operations +- No external dependencies +- Data lost on process restart +- No size limits (can cause memory issues) + +### FsJsonStorage + +File-based storage that persists cache to a JSON file. + +```typescript +import { FsJsonStorage, ExpirationStrategy } from '@node-ts-cache/core'; + +const storage = new FsJsonStorage('/tmp/cache.json'); +const strategy = new ExpirationStrategy(storage); +``` + +**Characteristics:** + +- Asynchronous operations +- Survives process restarts +- Slower than memory storage +- Good for development/single-instance deployments + +### NodeCacheStorage + +Wrapper for [node-cache](https://www.npmjs.com/package/node-cache). + +```bash +npm install @node-ts-cache/node-cache-storage +``` + +```typescript +import { ExpirationStrategy } from '@node-ts-cache/core'; +import NodeCacheStorage from '@node-ts-cache/node-cache-storage'; + +const storage = new NodeCacheStorage({ + stdTTL: 100, // Default TTL in seconds + checkperiod: 120, // Cleanup interval in seconds + maxKeys: 1000 // Maximum number of keys +}); +const strategy = new ExpirationStrategy(storage); +``` + +**Characteristics:** + +- Synchronous operations +- Supports multi-get/set operations +- Built-in TTL and cleanup +- Good for production single-instance apps + +### LRUStorage + +Wrapper for [lru-cache](https://www.npmjs.com/package/lru-cache). + +```bash +npm install @node-ts-cache/lru-storage +``` + +```typescript +import { ExpirationStrategy } from '@node-ts-cache/core'; +import LRUStorage from '@node-ts-cache/lru-storage'; + +const storage = new LRUStorage({ + max: 500, // Maximum number of items + ttl: 300 // TTL in seconds +}); +const strategy = new ExpirationStrategy(storage); +``` + +**Characteristics:** + +- Synchronous operations +- Automatic eviction when max size reached +- Memory-safe with bounded size +- Supports multi-get/set operations + +**Note:** LRU cache has its own TTL. When using with `ExpirationStrategy`, both TTLs apply. Set LRU `ttl` higher than your strategy TTL or use `isCachedForever` in the strategy. + +### RedisStorage + +Redis storage using the `redis` package (v4.x). + +```bash +npm install @node-ts-cache/redis-storage +``` + +```typescript +import { ExpirationStrategy } from '@node-ts-cache/core'; +import RedisStorage from '@node-ts-cache/redis-storage'; + +const storage = new RedisStorage({ + host: 'localhost', + port: 6379, + password: 'optional' +}); +const strategy = new ExpirationStrategy(storage); +``` + +**Characteristics:** + +- Asynchronous operations +- Shared cache across multiple instances +- No compression support + +### RedisIOStorage + +Modern Redis storage using [ioredis](https://github.com/redis/ioredis) with optional Snappy compression. + +```bash +npm install @node-ts-cache/ioredis-storage +``` + +```typescript +import { ExpirationStrategy } from '@node-ts-cache/core'; +import RedisIOStorage from '@node-ts-cache/ioredis-storage'; +import Redis from 'ioredis'; + +const redisClient = new Redis({ + host: 'localhost', + port: 6379 +}); + +// Basic usage +const storage = new RedisIOStorage( + () => redisClient, + { maxAge: 3600 } // TTL in seconds +); + +// With compression (reduces bandwidth, increases CPU usage) +const compressedStorage = new RedisIOStorage(() => redisClient, { + maxAge: 3600, + compress: true +}); + +// With error handler (non-blocking writes) +storage.onError(error => { + console.error('Redis error:', error); +}); + +const strategy = new ExpirationStrategy(storage); +``` + +**Constructor Options:** + +| Option | Type | Default | Description | +| ---------- | --------- | ------- | ------------------------------ | +| `maxAge` | `number` | `86400` | TTL in seconds (used by Redis) | +| `compress` | `boolean` | `false` | Enable Snappy compression | + +**Characteristics:** + +- Asynchronous operations +- Supports multi-get/set operations +- Optional Snappy compression +- Custom error handler support +- Can bypass ExpirationStrategy TTL (uses Redis native TTL) + +### LRUWithRedisStorage + +Two-tier caching: fast local LRU cache with Redis fallback. + +```bash +npm install @node-ts-cache/lru-redis-storage +``` + +```typescript +import { ExpirationStrategy } from '@node-ts-cache/core'; +import LRUWithRedisStorage from '@node-ts-cache/lru-redis-storage'; +import Redis from 'ioredis'; + +const redisClient = new Redis(); + +const storage = new LRUWithRedisStorage( + { max: 1000 }, // LRU options + () => redisClient // Redis client factory +); +const strategy = new ExpirationStrategy(storage); +``` + +**Characteristics:** + +- Asynchronous operations +- Local LRU for hot data +- Redis fallback for cache misses +- Reduces Redis round-trips +- Good for high-traffic applications + +## @MultiCache Details + +### Signature + +```typescript +@MultiCache( + strategies: Array, + parameterIndex: number, + cacheKeyFn?: (element: any) => string, + options?: ExpirationOptions +) +``` + +### Parameters + +- `strategies` - Array of cache strategies, checked in order (first = fastest, last = slowest) +- `parameterIndex` - Index of the array parameter in the method signature +- `cacheKeyFn` - Optional function to generate cache keys for each element +- `options` - Options passed to strategies + +### Example + +```typescript +import { MultiCache, ExpirationStrategy } from '@node-ts-cache/core'; +import NodeCacheStorage from '@node-ts-cache/node-cache-storage'; +import RedisIOStorage from '@node-ts-cache/ioredis-storage'; + +// Local cache (fastest) -> Redis (shared) -> Database (slowest) +const localCache = new ExpirationStrategy(new NodeCacheStorage()); +const redisCache = new RedisIOStorage(() => redisClient, { maxAge: 3600 }); + +class UserService { + @MultiCache([localCache, redisCache], 0, userId => `user:${userId}`, { ttl: 300 }) + async getUsersByIds(userIds: string[]): Promise { + // This only runs for IDs not found in any cache + // IMPORTANT: Return results in the same order as input IDs + return await db.users.findByIds(userIds); + } +} + +// Usage +const service = new UserService(); + +// First call - checks local, then redis, then hits database +const users = await service.getUsersByIds(['1', '2', '3']); + +// Second call - user 1 & 2 from local cache, user 4 from database +const moreUsers = await service.getUsersByIds(['1', '2', '4']); +``` + +### Return Value Requirements + +- Return an array with the same length and order as the input array +- Use `null` for entries that exist but are empty +- Use `undefined` for entries that should be re-queried next time + +## Lazy vs Eager Expiration + +- **Lazy (`isLazy: true`)**: Expired items remain in storage until accessed. Memory is freed on read. Better for large caches. +- **Eager (`isLazy: false`)**: Items are deleted via `setTimeout` after TTL. Frees memory automatically but uses timers. + +## Error Handling + +Cache errors are logged but don't break the application flow. If caching fails, the method executes normally: + +```typescript +// Cache read/write failures are logged as warnings: +// "@node-ts-cache/core: reading cache failed [key] [error]" +// "@node-ts-cache/core: writing result to cache failed [key] [error]" + +// For RedisIOStorage, you can add a custom error handler: +storage.onError(error => { + metrics.incrementCacheError(); + logger.error('Cache error', error); +}); +``` + +## Async Key Strategy Example + +For key generation that requires async operations (e.g., fetching user context): + +```typescript +import { Cache, ExpirationStrategy, MemoryStorage, IAsyncKeyStrategy } from '@node-ts-cache/core'; + +class AsyncKeyStrategy implements IAsyncKeyStrategy { + async getKey(className: string, methodName: string, args: any[]): Promise { + const userId = await getCurrentUserId(); + return `${userId}:${className}:${methodName}:${JSON.stringify(args)}`; + } +} +``` + +## API Exports + +```typescript +// Decorators +export { Cache } from './decorator/cache.decorator'; +export { SyncCache } from './decorator/synccache.decorator'; +export { MultiCache } from './decorator/multicache.decorator'; + +// Strategies +export { ExpirationStrategy } from './strategy/caching/expiration.strategy'; + +// Built-in Storages +export { MemoryStorage } from './storage/memory'; +export { FsJsonStorage } from './storage/fs'; + +// Interfaces +export { + IAsynchronousCacheType, + ISynchronousCacheType, + IMultiIAsynchronousCacheType, + IMultiSynchronousCacheType +} from './types/cache.types'; +export { ISyncKeyStrategy, IAsyncKeyStrategy } from './types/key.strategy.types'; +``` diff --git a/ts-cache/README.md b/ts-cache/README.md index 3d5f349..e6a2023 100644 --- a/ts-cache/README.md +++ b/ts-cache/README.md @@ -6,26 +6,6 @@ Simple and extensible caching module for TypeScript/Node.js with decorator support. -## Table of Contents - -- [Installation](#installation) -- [Quick Start](#quick-start) -- [Decorators](#decorators) - - [@Cache](#cache) - - [@SyncCache](#synccache) - - [@MultiCache](#multicache) -- [Direct API Usage](#direct-api-usage) -- [Strategies](#strategies) - - [ExpirationStrategy](#expirationstrategy) -- [Storages](#storages) - - [Built-in Storages](#built-in-storages) - - [Additional Storages](#additional-storages) -- [Custom Key Strategies](#custom-key-strategies) -- [Interface Definitions](#interface-definitions) -- [Advanced Usage](#advanced-usage) -- [Environment Variables](#environment-variables) -- [Testing](#testing) - ## Installation ```bash @@ -37,726 +17,173 @@ npm install @node-ts-cache/core ```typescript import { Cache, ExpirationStrategy, MemoryStorage } from '@node-ts-cache/core'; -// Create a caching strategy with in-memory storage const cacheStrategy = new ExpirationStrategy(new MemoryStorage()); class UserService { @Cache(cacheStrategy, { ttl: 60 }) async getUser(id: string): Promise { - // Expensive operation - result will be cached for 60 seconds return await database.findUser(id); } } ``` -## Decorators - -### @Cache +## Storage Engines -Caches async method responses. The cache key is generated from the class name, method name, and stringified arguments. +The core package includes `MemoryStorage` and `FsJsonStorage`. Additional storage backends are available as separate packages: -**Signature:** - -```typescript -@Cache(strategy: IAsynchronousCacheType | ISynchronousCacheType, options?: ExpirationOptions, keyStrategy?: IAsyncKeyStrategy) -``` +| Package | Storage Type | Sync/Async | Use Case | +| ----------------------------------- | ------------------------------------------------------ | ---------- | -------------------------------------- | +| `@node-ts-cache/core` | MemoryStorage | Sync | Development, simple caching | +| `@node-ts-cache/core` | FsJsonStorage | Async | Persistent local cache | +| `@node-ts-cache/node-cache-storage` | [node-cache](https://www.npmjs.com/package/node-cache) | Sync | Production single-instance with TTL | +| `@node-ts-cache/lru-storage` | [lru-cache](https://www.npmjs.com/package/lru-cache) | Sync | Memory-bounded with automatic eviction | +| `@node-ts-cache/redis-storage` | [redis](https://www.npmjs.com/package/redis) (v4.x) | Async | Shared cache | +| `@node-ts-cache/ioredis-storage` | [ioredis](https://www.npmjs.com/package/ioredis) | Async | Shared cache with compression | +| `@node-ts-cache/lru-redis-storage` | LRU + Redis | Async | Two-tier: fast local + shared remote | -**Parameters:** - -- `strategy` - A caching strategy instance (e.g., `ExpirationStrategy`) -- `options` - Options passed to the strategy (see [ExpirationStrategy](#expirationstrategy)) -- `keyStrategy` - Optional custom key generation strategy +## Decorators -**Important:** `@Cache` always returns a Promise, even for synchronous methods, because cache operations may be asynchronous. +### @Cache -**Example:** +Caches async method results. Cache key is generated from class name, method name, and arguments. ```typescript -import { Cache, ExpirationStrategy, MemoryStorage } from '@node-ts-cache/core'; - -const strategy = new ExpirationStrategy(new MemoryStorage()); - class ProductService { @Cache(strategy, { ttl: 300 }) async getProduct(id: string): Promise { - console.log('Fetching product from database...'); return await db.products.findById(id); } - - @Cache(strategy, { ttl: 3600, isCachedForever: false }) - async getCategories(): Promise { - return await db.categories.findAll(); - } } - -// Usage -const service = new ProductService(); - -// First call - hits database -const product1 = await service.getProduct('123'); - -// Second call with same args - returns cached result -const product2 = await service.getProduct('123'); - -// Different args - hits database again -const product3 = await service.getProduct('456'); ``` -### @SyncCache +**Note:** `@Cache` always returns a Promise since cache operations may be asynchronous. -Caches synchronous method responses without converting to Promises. Use this when your storage is synchronous (like `MemoryStorage` or `LRUStorage`). - -**Signature:** - -```typescript -@SyncCache(strategy: ISynchronousCacheType, options?: ExpirationOptions, keyStrategy?: ISyncKeyStrategy) -``` +### @SyncCache -**Example:** +Caches synchronous method results without converting to Promises. Use with synchronous storages like `MemoryStorage` or `LRUStorage`. ```typescript -import { SyncCache, ExpirationStrategy, MemoryStorage } from '@node-ts-cache/core'; - -const strategy = new ExpirationStrategy(new MemoryStorage()); - class ConfigService { @SyncCache(strategy, { ttl: 60 }) getConfig(key: string): ConfigValue { - // Expensive computation return computeConfig(key); } } - -// Usage - returns value directly, not a Promise -const config = new ConfigService().getConfig('theme'); ``` ### @MultiCache -Enables multi-tier caching with batch operations. Useful for: - -- Caching array-based lookups efficiently -- Implementing local + remote cache tiers -- Reducing database queries for batch operations - -**Signature:** +Multi-tier caching with batch operations for array-based lookups. ```typescript -@MultiCache( - strategies: Array, - parameterIndex: number, - cacheKeyFn?: (element: any) => string, - options?: ExpirationOptions -) -``` - -**Parameters:** - -- `strategies` - Array of cache strategies, checked in order (first = fastest, last = slowest) -- `parameterIndex` - Index of the array parameter in the method signature -- `cacheKeyFn` - Optional function to generate cache keys for each element -- `options` - Options passed to strategies - -**Example:** - -```typescript -import { MultiCache, ExpirationStrategy } from '@node-ts-cache/core'; -import NodeCacheStorage from '@node-ts-cache/node-cache-storage'; -import RedisIOStorage from '@node-ts-cache/ioredis-storage'; - -// Local cache (fastest) -> Redis (shared) -> Database (slowest) -const localCache = new ExpirationStrategy(new NodeCacheStorage()); -const redisCache = new RedisIOStorage(() => redisClient, { maxAge: 3600 }); - class UserService { - @MultiCache([localCache, redisCache], 0, userId => `user:${userId}`, { ttl: 300 }) + @MultiCache([localCache, redisCache], 0, id => `user:${id}`, { ttl: 300 }) async getUsersByIds(userIds: string[]): Promise { - // This only runs for IDs not found in any cache - // IMPORTANT: Return results in the same order as input IDs return await db.users.findByIds(userIds); } } - -// Usage -const service = new UserService(); - -// First call - checks local, then redis, then hits database -const users = await service.getUsersByIds(['1', '2', '3']); - -// Second call - user 1 & 2 from local cache, user 4 from database -const moreUsers = await service.getUsersByIds(['1', '2', '4']); ``` -**Return Value Requirements:** - -- Return an array with the same length and order as the input array -- Use `null` for entries that exist but are empty -- Use `undefined` for entries that should be re-queried next time - ## Direct API Usage -You can use the caching strategy directly without decorators: +Use the caching strategy directly without decorators: ```typescript -import { ExpirationStrategy, MemoryStorage } from '@node-ts-cache/core'; - const cache = new ExpirationStrategy(new MemoryStorage()); -class DataService { - async getData(key: string): Promise { - // Check cache first - const cached = await cache.getItem(key); - if (cached !== undefined) { - return cached; - } - - // Fetch fresh data - const data = await fetchData(key); +// Get item +const value = await cache.getItem('key'); - // Store in cache - await cache.setItem(key, data, { ttl: 300 }); +// Set item with TTL +await cache.setItem('key', data, { ttl: 300 }); - return data; - } +// Delete item +await cache.setItem('key', undefined); - async invalidate(key: string): Promise { - await cache.setItem(key, undefined); - } - - async clearAll(): Promise { - await cache.clear(); - } -} +// Clear all +await cache.clear(); ``` -## Strategies - -### ExpirationStrategy - -Time-based cache expiration strategy. Items are automatically invalidated after a specified TTL (Time To Live). +## ExpirationStrategy Options -**Constructor:** +| Option | Type | Default | Description | +| ----------------- | --------- | ------- | --------------------------------------------------------------------------------- | +| `ttl` | `number` | `60` | Time to live in seconds | +| `isLazy` | `boolean` | `true` | If `true`, delete on access after expiration. If `false`, delete via `setTimeout` | +| `isCachedForever` | `boolean` | `false` | If `true`, items never expire | ```typescript -new ExpirationStrategy(storage: IAsynchronousCacheType | ISynchronousCacheType) -``` - -**Options:** - -| Option | Type | Default | Description | -| ----------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------- | -| `ttl` | `number` | `60` | Time to live in **seconds** | -| `isLazy` | `boolean` | `true` | If `true`, items are deleted when accessed after expiration. If `false`, items are deleted automatically via `setTimeout` | -| `isCachedForever` | `boolean` | `false` | If `true`, items never expire (ignores `ttl`) | - -**Example:** - -```typescript -import { ExpirationStrategy, MemoryStorage } from '@node-ts-cache/core'; - -const storage = new MemoryStorage(); -const strategy = new ExpirationStrategy(storage); - // Cache for 5 minutes with lazy expiration -await strategy.setItem('key1', 'value', { ttl: 300, isLazy: true }); +await strategy.setItem('key', value, { ttl: 300, isLazy: true }); // Cache forever -await strategy.setItem('key2', 'value', { isCachedForever: true }); - -// Cache for 10 seconds with eager expiration (auto-delete) -await strategy.setItem('key3', 'value', { ttl: 10, isLazy: false }); -``` - -**Lazy vs Eager Expiration:** - -- **Lazy (`isLazy: true`)**: Expired items remain in storage until accessed. Memory is freed on read. Better for large caches. -- **Eager (`isLazy: false`)**: Items are deleted via `setTimeout` after TTL. Frees memory automatically but uses timers. - -## Storages - -### Built-in Storages - -These storages are included in the core package: - -#### MemoryStorage - -Simple in-memory storage using a JavaScript object. Best for development and simple use cases. - -```typescript -import { MemoryStorage, ExpirationStrategy } from '@node-ts-cache/core'; - -const storage = new MemoryStorage(); -const strategy = new ExpirationStrategy(storage); -``` - -**Characteristics:** - -- Synchronous operations -- No external dependencies -- Data lost on process restart -- No size limits (can cause memory issues) - -#### FsJsonStorage - -File-based storage that persists cache to a JSON file. Useful for persistent local caching. - -```typescript -import { FsJsonStorage, ExpirationStrategy } from '@node-ts-cache/core'; - -const storage = new FsJsonStorage('/tmp/cache.json'); -const strategy = new ExpirationStrategy(storage); -``` - -**Characteristics:** - -- Asynchronous operations -- Survives process restarts -- Slower than memory storage -- Good for development/single-instance deployments - -### Additional Storages - -Install these separately based on your needs: - -#### NodeCacheStorage - -Wrapper for [node-cache](https://www.npmjs.com/package/node-cache) - a simple in-memory cache with TTL support. - -```bash -npm install @node-ts-cache/node-cache-storage -``` - -```typescript -import { ExpirationStrategy } from '@node-ts-cache/core'; -import NodeCacheStorage from '@node-ts-cache/node-cache-storage'; - -const storage = new NodeCacheStorage({ - stdTTL: 100, // Default TTL in seconds - checkperiod: 120, // Cleanup interval in seconds - maxKeys: 1000 // Maximum number of keys -}); -const strategy = new ExpirationStrategy(storage); -``` - -**Characteristics:** - -- Synchronous operations -- Supports multi-get/set operations -- Built-in TTL and cleanup -- Good for production single-instance apps - -#### LRUStorage - -Wrapper for [lru-cache](https://www.npmjs.com/package/lru-cache) - Least Recently Used cache with automatic eviction. - -```bash -npm install @node-ts-cache/lru-storage -``` - -```typescript -import { ExpirationStrategy } from '@node-ts-cache/core'; -import LRUStorage from '@node-ts-cache/lru-storage'; - -const storage = new LRUStorage({ - max: 500, // Maximum number of items - ttl: 300 // TTL in seconds -}); -const strategy = new ExpirationStrategy(storage); -``` +await strategy.setItem('key', value, { isCachedForever: true }); -**Characteristics:** - -- Synchronous operations -- Automatic eviction when max size reached -- Memory-safe with bounded size -- Supports multi-get/set operations - -**Note:** LRU cache has its own TTL (`ttl` in seconds). When using with `ExpirationStrategy`, both TTLs apply. Set LRU `ttl` higher than your strategy TTL or use `isCachedForever` in the strategy. - -#### RedisStorage - -Redis storage using the legacy `redis` package (v3.x). For new projects, consider using `RedisIOStorage` instead. - -```bash -npm install @node-ts-cache/redis-storage -``` - -```typescript -import { ExpirationStrategy } from '@node-ts-cache/core'; -import RedisStorage from '@node-ts-cache/redis-storage'; - -const storage = new RedisStorage({ - host: 'localhost', - port: 6379, - password: 'optional' -}); -const strategy = new ExpirationStrategy(storage); -``` - -**Characteristics:** - -- Asynchronous operations -- Uses legacy `redis` package with Bluebird promises -- Shared cache across multiple instances -- No compression support - -#### RedisIOStorage - -Modern Redis storage using [ioredis](https://github.com/redis/ioredis) with optional Snappy compression. - -```bash -npm install @node-ts-cache/ioredis-storage -``` - -```typescript -import { ExpirationStrategy } from '@node-ts-cache/core'; -import RedisIOStorage from '@node-ts-cache/ioredis-storage'; -import Redis from 'ioredis'; - -const redisClient = new Redis({ - host: 'localhost', - port: 6379 -}); - -// Basic usage -const storage = new RedisIOStorage( - () => redisClient, - { maxAge: 3600 } // TTL in seconds -); - -// With compression (reduces bandwidth, increases CPU usage) -const compressedStorage = new RedisIOStorage(() => redisClient, { maxAge: 3600, compress: true }); - -// With error handler (non-blocking writes) -storage.onError(error => { - console.error('Redis error:', error); -}); - -const strategy = new ExpirationStrategy(storage); -``` - -**Characteristics:** - -- Asynchronous operations -- Supports multi-get/set operations -- Optional Snappy compression -- Modern ioredis client -- Custom error handler support -- Can bypass ExpirationStrategy TTL (uses Redis native TTL) - -**Constructor Options:** -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `maxAge` | `number` | `86400` | TTL in seconds (used by Redis SETEX) | -| `compress` | `boolean` | `false` | Enable Snappy compression | - -#### LRUWithRedisStorage - -Two-tier caching: fast local LRU cache with Redis fallback. Provides the best of both worlds. - -```bash -npm install @node-ts-cache/lru-redis-storage +// Cache with eager expiration (auto-delete after TTL) +await strategy.setItem('key', value, { ttl: 10, isLazy: false }); ``` -```typescript -import { ExpirationStrategy } from '@node-ts-cache/core'; -import LRUWithRedisStorage from '@node-ts-cache/lru-redis-storage'; -import Redis from 'ioredis'; - -const redisClient = new Redis(); - -const storage = new LRUWithRedisStorage( - { max: 1000 }, // LRU options - () => redisClient // Redis client factory -); -const strategy = new ExpirationStrategy(storage); -``` - -**Characteristics:** - -- Asynchronous operations -- Local LRU for hot data -- Redis fallback for cache misses -- Reduces Redis round-trips -- Good for high-traffic applications - ## Custom Key Strategies -By default, cache keys are generated as: `ClassName:methodName:JSON.stringify(args)` - -You can implement custom key strategies for different needs: - -### Synchronous Key Strategy +Override default key generation by implementing `ISyncKeyStrategy` or `IAsyncKeyStrategy`: ```typescript -import { Cache, ExpirationStrategy, MemoryStorage, ISyncKeyStrategy } from '@node-ts-cache/core'; - class CustomKeyStrategy implements ISyncKeyStrategy { getKey(className: string, methodName: string, args: any[]): string | undefined { - // Return undefined to skip caching for this call - if (args[0] === 'skip') { - return undefined; - } - - // Custom key format + if (args[0] === 'skip') return undefined; // Skip caching return `${className}::${methodName}::${args.join('-')}`; } } -const strategy = new ExpirationStrategy(new MemoryStorage()); -const keyStrategy = new CustomKeyStrategy(); - class MyService { - @Cache(strategy, { ttl: 60 }, keyStrategy) + @Cache(strategy, { ttl: 60 }, new CustomKeyStrategy()) async getData(id: string): Promise { return fetchData(id); } } ``` -### Asynchronous Key Strategy - -For key generation that requires async operations (e.g., fetching user context): - -```typescript -import { Cache, ExpirationStrategy, MemoryStorage, IAsyncKeyStrategy } from '@node-ts-cache/core'; - -class AsyncKeyStrategy implements IAsyncKeyStrategy { - async getKey(className: string, methodName: string, args: any[]): Promise { - // Async operation to build key - const userId = await getCurrentUserId(); - return `${userId}:${className}:${methodName}:${JSON.stringify(args)}`; - } -} -``` - -## Interface Definitions - -### Storage Interfaces - -```typescript -/** - * Cache entry structure stored in backends - */ -interface ICacheEntry { - content: any; // The cached value - meta: any; // Metadata (e.g., TTL, createdAt) -} - -/** - * Asynchronous storage for single items - */ -interface IAsynchronousCacheType { - /** Retrieve an item by key. Returns undefined if not found. */ - getItem(key: string): Promise; - - /** Store an item. Pass undefined as content to delete. */ - setItem(key: string, content: C | undefined, options?: any): Promise; - - /** Clear all items from the cache. */ - clear(): Promise; -} - -/** - * Synchronous storage for single items - */ -interface ISynchronousCacheType { - /** Retrieve an item by key. Returns undefined if not found. */ - getItem(key: string): T | undefined; - - /** Store an item. Pass undefined as content to delete. */ - setItem(key: string, content: C | undefined, options?: any): void; - - /** Clear all items from the cache. */ - clear(): void; -} - -/** - * Asynchronous storage with batch operations - */ -interface IMultiIAsynchronousCacheType { - /** Retrieve multiple items by keys. */ - getItems(keys: string[]): Promise<{ [key: string]: T | undefined }>; - - /** Store multiple items at once. */ - setItems(values: { key: string; content: C | undefined }[], options?: any): Promise; - - /** Clear all items from the cache. */ - clear(): Promise; -} - -/** - * Synchronous storage with batch operations - */ -interface IMultiSynchronousCacheType { - /** Retrieve multiple items by keys. */ - getItems(keys: string[]): { [key: string]: T | undefined }; - - /** Store multiple items at once. */ - setItems(values: { key: string; content: C | undefined }[], options?: any): void; - - /** Clear all items from the cache. */ - clear(): void; -} -``` - -### Key Strategy Interfaces - -```typescript -/** - * Synchronous key generation strategy - */ -interface ISyncKeyStrategy { - /** - * Generate a cache key from method context - * @param className - Name of the class containing the method - * @param methodName - Name of the cached method - * @param args - Arguments passed to the method - * @returns Cache key string, or undefined to skip caching - */ - getKey(className: string, methodName: string, args: any[]): string | undefined; -} - -/** - * Asynchronous key generation strategy - */ -interface IAsyncKeyStrategy { - /** - * Generate a cache key from method context (can be async) - * @param className - Name of the class containing the method - * @param methodName - Name of the cached method - * @param args - Arguments passed to the method - * @returns Cache key string, or undefined to skip caching - */ - getKey( - className: string, - methodName: string, - args: any[] - ): Promise | string | undefined; -} -``` - -### ExpirationStrategy Options - -```typescript -interface ExpirationOptions { - /** Time to live in seconds (default: 60) */ - ttl?: number; - - /** If true, delete on access after expiration. If false, delete via setTimeout (default: true) */ - isLazy?: boolean; - - /** If true, cache forever ignoring TTL (default: false) */ - isCachedForever?: boolean; -} -``` - -## Advanced Usage +## Advanced Features ### Call Deduplication -The `@Cache` decorator automatically deduplicates concurrent calls with the same cache key. If multiple calls are made before the first one completes, they all receive the same result: +Concurrent calls with the same cache key share the same pending promise: ```typescript -class DataService { - @Cache(strategy, { ttl: 60 }) - async fetchData(id: string): Promise { - console.log('Fetching...'); // Only logged once - return await slowApiCall(id); - } -} - -const service = new DataService(); - -// All three calls share the same pending promise +// All three calls share one database request const [a, b, c] = await Promise.all([ service.fetchData('123'), service.fetchData('123'), service.fetchData('123') ]); -// "Fetching..." is logged only once, all three get the same result ``` -### Handling Undefined vs Null - -The cache distinguishes between: +### Null vs Undefined -- `undefined`: No value found in cache, or value should not be cached -- `null`: Explicit null value that is cached +- `undefined`: Cache miss or skip caching +- `null`: Cached value (e.g., "not found" result) ```typescript -class UserService { - @Cache(strategy, { ttl: 60 }) - async findUser(id: string): Promise { - const user = await db.findUser(id); - // Return null for non-existent users to cache the "not found" result - // Return undefined would cause re-fetching on every call - return user ?? null; - } +async findUser(id: string): Promise { + const user = await db.findUser(id); + return user ?? null; // Cache "not found" as null } ``` -### Error Handling - -Cache errors are logged but don't break the application flow. If caching fails, the method executes normally: - -```typescript -// Cache read/write failures are logged as warnings: -// "@node-ts-cache/core: reading cache failed [key] [error]" -// "@node-ts-cache/core: writing result to cache failed [key] [error]" - -// For RedisIOStorage, you can add a custom error handler: -storage.onError(error => { - metrics.incrementCacheError(); - logger.error('Cache error', error); -}); -``` - ## Environment Variables -| Variable | Description | -| ------------------------- | -------------------------------------------------------------------------- | -| `DISABLE_CACHE_DECORATOR` | Set to any value to disable all `@Cache` decorators (useful for debugging) | - -## Testing - -```bash -# Run all tests -npm test - -# Run tests in watch mode -npm run tdd - -# Run tests with debugger -npm run tdd-debug-brk -``` +| Variable | Description | +| ------------------------- | --------------------------------------------------- | +| `DISABLE_CACHE_DECORATOR` | Set to any value to disable all `@Cache` decorators | -## API Reference +## More Documentation -### Exports +See [ADVANCED.md](./ADVANCED.md) for: -```typescript -// Decorators -export { Cache } from './decorator/cache.decorator'; -export { SyncCache } from './decorator/synccache.decorator'; -export { MultiCache } from './decorator/multicache.decorator'; - -// Strategies -export { ExpirationStrategy } from './strategy/caching/expiration.strategy'; - -// Built-in Storages -export { MemoryStorage } from './storage/memory'; -export { FsJsonStorage } from './storage/fs'; - -// Interfaces -export { - IAsynchronousCacheType, - ISynchronousCacheType, - IMultiIAsynchronousCacheType, - IMultiSynchronousCacheType -} from './types/cache.types'; -export { ISyncKeyStrategy, IAsyncKeyStrategy } from './types/key.strategy.types'; -``` +- Interface definitions for implementing custom storages +- Detailed storage configuration examples +- @MultiCache in-depth usage +- Error handling patterns ## License