@qrvey/cache
v1.0.0-680
Published

Readme
@qrvey/cache
A reusable caching package that provides an engine-agnostic interface for caching operations with Redis support and built-in idempotency patterns.
Installation
npm install @qrvey/cacheFeatures
- Engine-agnostic interface - Extensible architecture supporting multiple cache backends
- Redis support - Production-ready Redis connector with connection pooling
- Idempotency helpers - Built-in utilities for implementing idempotent operations
- TypeScript - Full type safety and IntelliSense support
- Key prefixing - Automatic key namespacing for multi-tenant applications
- TTL support - Configurable time-to-live for cache entries
- Atomic operations - Support for SETNX and other atomic Redis commands
Usage
Basic Cache Operations
import { Cache, CacheEngine } from '@qrvey/cache';
const cache = new Cache({
engine: CacheEngine.REDIS,
ttl: 3600,
keyPrefix: 'myapp',
connectorConfig: {
url: 'redis://localhost:6379',
},
});
/** Set a value */
await cache.set('user:123', { name: 'John Doe', email: '[email protected]' });
/** Get a value */
const user = await cache.get('user:123');
console.log(user); /** { name: 'John Doe', email: '[email protected]' } */
/** Delete a value */
await cache.delete('user:123');
/** Check if key exists */
const exists = await cache.exists('user:123');
console.log(exists); /** false */Idempotency Pattern (Class-based)
Use IdempotencyContext to implement idempotent operations with a clean lifecycle:
import { IdempotencyContext, IdempotencyError } from '@qrvey/cache';
async function createDataset(data, options = {}) {
/** 1. Create context with default Redis cache */
const ctx = IdempotencyContext.withDefaults<IDataset>(
'dataset',
options.idempotencyKey,
{
resultTtl: 1800 /** Cache result for 30 min (default: 1800) */,
lockTtl: 60 /** Lock expires in 1 min (default: 60) */,
maxWaitAttempts: 15 /** Max polling attempts (default: 15) */,
baseWaitDelay: 500 /** Base delay for backoff in ms (default: 500) */,
gracePeriod: 10 /** Grace period after lock in sec (default: 10) */,
},
);
/** 2. Acquire - returns cached result or null if lock acquired */
const cached = await ctx.acquire();
if (cached) return cached;
/** 3. Execute business logic */
try {
const result = await processDataset(data);
/** 4. Complete - cache result and release lock */
await ctx.complete(result);
return result;
} catch (error) {
/** 5. Abort - release lock without caching (allows retry) */
await ctx.abort();
throw error;
}
}Custom Cache (for testing or alternative backends)
import { IdempotencyContext, ICache } from '@qrvey/cache';
/** Inject a custom cache implementation */
const customCache: ICache = { get, set, setNX, delete, exists };
const ctx = new IdempotencyContext<IDataset>(
'dataset',
idempotencyKey,
customCache,
{ resultTtl: 3600 },
);Lifecycle Diagram
┌─────────────────┐
│ INITIALIZED │
└────────┬────────┘
│
▼
┌─────────────────┐
│ acquire() │
└────────┬────────┘
│
┌────┴────┐
│ │
Cache Hit Lock Acquired
│ │
▼ ▼
┌───────┐ ┌───────────┐
│RETURN │ │ LOCKED │
│cached │ └─────┬─────┘
└───────┘ │
┌─────┼─────┐
│ │
▼ ▼
┌────────┐ ┌────────┐
│complete│ │ abort │
│(result)│ │ () │
└────┬───┘ └────┬───┘
│ │
▼ ▼
┌────────┐ ┌────────┐
│ CACHED │ │RELEASED│
│+RELEASE│ │(no TTL)│
└────────┘ └────────┘IdempotencyError
When the lock cannot be acquired and no result is available after waiting:
try {
const cached = await ctx.acquire();
} catch (error) {
if (error instanceof IdempotencyError) {
console.log(error.status); /** 503 */
console.log(error.code); /** 'IDEMPOTENCY_LOCK_UNAVAILABLE' */
}
}Cache Key Generation
import { generateCacheKey } from '@qrvey/cache';
/** Generate MD5 hash of params */
const key = generateCacheKey({ userId: '123', filter: 'active' });
console.log(key); /** "5d41402abc4b2a76b9719d911017c592" */
/** With prefix */
const prefixedKey = generateCacheKey(
{ userId: '123', filter: 'active' },
'user-query',
);
console.log(prefixedKey); /** "user-query-5d41402abc4b2a76b9719d911017c592" */Atomic Operations
/** Set value only if key does not exist (atomic) */
const wasSet = await cache.setNX('lock:resource', { owner: 'process-1' }, 30);
if (wasSet) {
console.log('Lock acquired');
/** Do work */
await cache.delete('lock:resource');
} else {
console.log('Lock already held by another process');
}API Reference
Cache
Constructor
new Cache(config?: ICacheConfig)Config options:
engine- Cache engine to use (default:CacheEngine.REDIS)ttl- Default TTL in secondskeyPrefix- Prefix for all cache keysconnectorConfig- Engine-specific configuration
Methods
connect(): Promise<void>- Establish connectiondisconnect(): Promise<void>- Close connectionget(key: string): Promise<unknown | null>- Retrieve valueset(key: string, value: unknown, ttlSeconds?: number): Promise<boolean>- Store valuedelete(key: string): Promise<boolean>- Remove keyexists(key: string): Promise<boolean>- Check if key existssetNX(key: string, value: unknown, ttlSeconds?: number): Promise<boolean>- Atomic set-if-not-exists
IdempotencyContext
Static Factory (Recommended)
IdempotencyContext.withDefaults<T>(
entityType: string,
idempotencyKey: string | null,
config?: IIdempotencyContextConfig
): IdempotencyContext<T>Creates an instance with a default Redis cache. Use this for standard production usage.
Constructor (for DI/Testing)
new IdempotencyContext<T>(
entityType: string,
idempotencyKey: string | null,
cacheInstance: ICache,
config?: IIdempotencyContextConfig
)Creates an instance with a custom cache. Use this when you need to inject a mock or alternative cache implementation.
Config options (IIdempotencyContextConfig):
| Option | Type | Default | Description |
| ----------------- | -------- | ------- | ----------------------------------------- |
| resultTtl | number | 1800 | TTL for cached results (seconds) |
| lockTtl | number | 60 | TTL for lock keys (seconds) |
| maxWaitAttempts | number | 15 | Max polling attempts when waiting |
| baseWaitDelay | number | 500 | Base delay for backoff (ms) |
| gracePeriod | number | 10 | Grace period after lock expires (seconds) |
Methods
| Method | Returns | Description |
| --------------------- | -------------------- | ------------------------------------------------------------ |
| acquire() | Promise<T \| null> | Check cache or acquire lock. Returns cached result or null |
| complete(result: T) | Promise<void> | Cache result and release lock |
| abort() | Promise<void> | Release lock without caching (for error cases) |
Properties
| Property | Type | Description |
| ----------- | --------- | ----------------------------------- |
| isEnabled | boolean | Whether idempotency is active |
| hasLock | boolean | Whether this context holds the lock |
IdempotencyError
Thrown when the lock cannot be acquired after waiting.
| Property | Value |
| -------- | -------------------------------- |
| status | 503 |
| code | 'IDEMPOTENCY_LOCK_UNAVAILABLE' |
Idempotency Helpers
waitForIdempotentResult(cache: ICache, cacheKey: string, options?: IWaitForIdempotentResultOptions): Promise<unknown | null>- Poll for result with exponential backoffbuildIdempotencyCacheKey(entityType: string, idempotencyKey: string): string- Build cache key
IWaitForIdempotentResultOptions:
interface IWaitForIdempotentResultOptions {
maxAttempts?: number /** Max polling attempts (default: 15) */;
baseDelay?: number /** Base delay in ms for backoff (default: 500) */;
lockTtl?: number /** Lock TTL in seconds (default: 60) */;
gracePeriod?: number /** Grace period in seconds after lock expires (default: 10) */;
}Utilities
generateCacheKey(params: object, prefix?: string): string- Generate MD5 hash key
Constants
IDEMPOTENCY_TTL: number- TTL for idempotency cache entries (1800 seconds / 30 minutes)IDEMPOTENCY_LOCK_TTL: number- TTL for idempotency lock keys (60 seconds)IDEMPOTENCY_GRACE_PERIOD: number- Grace period after lock expires (10 seconds)REDIS_DEFAULT_CONFIG: IRedisConfig- Default Redis connection configuration
Configuration
Redis Configuration
const cache = new Cache({
engine: CacheEngine.REDIS,
connectorConfig: {
url: 'redis://localhost:6379',
maxRetriesPerRequest: 3,
connectTimeout: 10000,
commandTimeout: 5000,
maxReconnectRetries: 3,
reconnectBaseDelay: 100,
reconnectMaxDelay: 3000,
},
});Environment Variables
REDIS_URL- Redis connection URL (default: uses connector config)
License
MIT
