Skip to content

Storage Adapters

Adapter Comparison

FeatureMemoryStorageRedisStorage
ScopeSingle processShared across replicas
PersistenceLost on restartFull Redis durability
TTL mechanismsetTimeoutRedis EXPIRE
Cluster-safe
Production-ready❌ (dev/test only)
Required peernoneioredis ^5

MemoryStorage

Backed by a Map with per-entry setTimeout expirations. Suitable for development and testing.

typescript
import { IdempotencyModule, MemoryStorage } from '@nestarc/idempotency';

@Module({
  imports: [
    IdempotencyModule.forRoot({
      storage: new MemoryStorage(),
      ttl: 86400,
    }),
  ],
})
export class AppModule {}

WARNING

Not safe for production. State is lost on restart and not shared across processes — two replicas would enforce idempotency independently, letting duplicates slip through.

RedisStorage

Stores records as Redis Hash structures with Lua scripts for atomic compare-and-set operations.

typescript
import { IdempotencyModule, RedisStorage } from '@nestarc/idempotency';
import { Redis } from 'ioredis';

@Module({
  imports: [
    IdempotencyModule.forRoot({
      storage: new RedisStorage({
        client: new Redis({ host: 'localhost', port: 6379 }),
      }),
      ttl: 86400,
    }),
  ],
})
export class AppModule {}

RedisStorage Options

OptionTypeDefaultDescription
clientRedisPre-built ioredis client (recommended)
connectionRedisOptionsioredis options for lazy client construction
keyPrefixstring'idempotency:'Prefix for all Redis keys

If you pass client, you own its lifecycle. If you pass connection, RedisStorage creates and closes the client via OnModuleDestroy.

Lua Scripts

RedisStorage registers three Lua scripts via defineCommand:

ScriptOperationGuarantee
idemCreateNX-semantics record creationExactly one caller acquires the lock
idemCompleteToken-gated PROCESSING → COMPLETEDOnly the lock owner can write the response
idemDeleteToken-gated cleanupFailed handlers clean up without clobbering

Custom Storage Adapters

Implement the IdempotencyStorage interface with token-based compare-and-set semantics:

typescript
import type {
  IdempotencyStorage,
  IdempotencyRecord,
  CreateResult,
  CompleteResponse,
  MutateResult,
} from '@nestarc/idempotency';
import type { OnModuleDestroy } from '@nestjs/common';

class MyStorage implements IdempotencyStorage, OnModuleDestroy {
  async get(key: string): Promise<IdempotencyRecord | null> {
    // Return the record, or null if expired / not found.
  }

  async create(
    key: string,
    fingerprint: string | undefined,
    ttlSeconds: number,
  ): Promise<CreateResult> {
    // NX semantics: return { acquired: false } if key exists.
    // Otherwise generate a token, persist PROCESSING record,
    // and return { acquired: true, token }.
  }

  async complete(
    key: string,
    token: string,
    response: CompleteResponse,
    ttlSeconds: number,
  ): Promise<MutateResult> {
    // Compare-and-set: only mutate if stored token matches.
    // Return 'ok' on success, 'stale' on token mismatch.
    // Refresh expiresAt but preserve createdAt.
  }

  async delete(key: string, token: string): Promise<MutateResult> {
    // Token-gated cleanup. Return 'ok' if removed or absent,
    // 'stale' if a different record exists under this key.
  }

  async onModuleDestroy(): Promise<void> {
    // Release external resources (connections, timers).
  }
}

Storage Contract Guarantees

  1. Atomic creation — two concurrent create() for the same key must result in exactly one acquired: true
  2. Token-based CAScomplete() and delete() only mutate records with matching tokens
  3. createdAt immutabilitycomplete() must preserve the original createdAt timestamp

TIP

The source repo includes a shared contract test suite at test/support/shared-storage-contract.ts. Custom adapters can plug into it via describeStorageContract('Name', factory) to verify conformance.

Released under the MIT License.