npm package: @qrvey/cache
Description: ![unit_test_coverage](https://img.shields.io/badge/unit_test_coverage-0%25-brightgreen)
Latest version: 1.0.2-865

# @qrvey/cache

![unit_test_coverage](https://img.shields.io/badge/unit_test_coverage-0%25-brightgreen)

A reusable caching package that provides an engine-agnostic interface for caching operations with Redis support and built-in idempotency patterns.

## Installation

```bash
npm install @qrvey/cache
```

## Features

-   **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

```typescript
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: 'john@example.com' });

/** Get a value */
const user = await cache.get('user:123');
console.log(user); /** { name: 'John Doe', email: 'john@example.com' } */

/** 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:

```typescript
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)

```typescript
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:

```typescript
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

```typescript
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

```typescript
/** 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

```typescript
new Cache(config?: ICacheConfig)
```

**Config options:**

-   `engine` - Cache engine to use (default: `CacheEngine.REDIS`)
-   `ttl` - Default TTL in seconds
-   `keyPrefix` - Prefix for all cache keys
-   `connectorConfig` - Engine-specific configuration

#### Methods

-   `connect(): Promise<void>` - Establish connection
-   `disconnect(): Promise<void>` - Close connection
-   `get(key: string): Promise<unknown | null>` - Retrieve value
-   `set(key: string, value: unknown, ttlSeconds?: number): Promise<boolean>` - Store value
-   `delete(key: string): Promise<boolean>` - Remove key
-   `exists(key: string): Promise<boolean>` - Check if key exists
-   `setNX(key: string, value: unknown, ttlSeconds?: number): Promise<boolean>` - Atomic set-if-not-exists

### IdempotencyContext

#### Static Factory (Recommended)

```typescript
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)

```typescript
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 backoff
-   `buildIdempotencyCacheKey(entityType: string, idempotencyKey: string): string` - Build cache key

**IWaitForIdempotentResultOptions:**

```typescript
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

```typescript
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