Skip to main content

@siglo/redis Adapter Guide

@siglo/redis provides an in-memory distributed persistence, step response caching, atomic idempotency deduplication, distributed single-flight coordination, and webhook replay prevention layer for Siglo workflows. It is powered by Redis and compatible with standalone Redis, Redis Sentinel, and Redis Cluster via ioredis (or any client implementing RedisClientInterface).

1. Overview & Architecture

The Redis adapter family encompasses four core modules:
  1. RedisStorageAdapter: Implements the Siglo StorageAdapter interface for execution stack checkpointing (with tenant isolation), durable wait record indexing for event-driven suspensions, step response caching, and non-blocking context mutation streaming.
  2. RedisIdempotencyStore: Implements the Siglo IdempotencyStore interface for atomic, distributed single-flight execution claims, cached result retrieval, and automated lock releasing on failure.
  3. RedisLock: Distributed mutex coordinator with unique per-worker token verification, single-flight helper execution (withLock), and optional strict Lua atomicity (strictAtomicity) to eliminate TOCTOU release races.
  4. RedisNonceStore: Distributed atomic deduplication store for webhook signatures and replay attack prevention, enforcing single-use delivery within tolerance windows.

2. Installation & Requirements

Install @siglo/redis alongside @siglo/core and ioredis:

Peer Dependency Matrix


3. Key Topology & Redis Data Structures

@siglo/redis uses structured key namespaces across all components:

4. RedisClientInterface & Driver Compatibility

@siglo/redis communicates through RedisClientInterface, decoupling the adapter from a specific driver version and simplifying mock testing:
Standard ioredis instances (Redis, Cluster, Sentinel) fulfill this interface out-of-the-box without requiring wrappers.

5. RedisStorageAdapter Reference

Implements the Siglo StorageAdapter interface (name = 'redis') for high-performance checkpointing, step output caching, wait record indexing, and context mutation streaming.

Options (RedisStorageAdapterOptions)

Methods

  • saveCheckpoint(checkpoint: SerializedExecutionStack): Promise<void>: Saves the JSON-serialized execution stack with millisecond TTL (SET key val PX checkpointTtlMs). If checkpoint.namespace is provided, stores under siglo:<ns>:checkpoint:<stackId>. Returns early if checkpoint or checkpoint.stackId is missing.
  • getCheckpoint(stackId: string, namespace?: string): Promise<SerializedExecutionStack | undefined>: Retrieves and parses the serialized checkpoint. If namespace is specified, checks the tenant-namespaced key first, falling back to the un-namespaced key if not found. Returns undefined if missing or if parsing fails.
  • saveWaitRecord(record: any): Promise<void>: Persists durable wait/continuation records for event-driven workflows (e.g., waitForEvent). Dual-indexes the record under siglo:wait:<correlationKey> (if present) and siglo:wait:<stackId> (if present) with checkpointTtlMs.
  • getWaitRecord(correlationKeyOrStackId: string): Promise<any | undefined>: Retrieves a wait record by correlation key or stack ID from siglo:wait:<key>. If not found, falls back to inspecting cp?.metadata?.waitRecord from getCheckpoint(correlationKeyOrStackId).
  • cleanExpiredCheckpoints(): Promise<number>: Returns 0. TTL expiration is handled automatically by Redis native keyspace expiration, eliminating background cleanup sweeps.
  • getCachedStep(key: string): Promise<any | undefined>: Retrieves cached step response from siglo:cache:<key>. Returns parsed JSON, or the raw string if not JSON, or undefined if key does not exist.
  • setCachedStep(key: string, value: any, ttlMs?: number): Promise<void>: Writes cached step evaluation to siglo:cache:<key> with PX TTL (ttlMs || cacheTtlMs). Objects are serialized directly; primitives are wrapped as { value }.
  • onContextChange(entry): Promise<void>: When recordContextChanges is true, writes field mutations to HSET at siglo:[<ns>:]context:<stackId>:<stepId> and refreshes expiration with PEXPIRE (checkpointTtlMs). Errors are silently caught to ensure non-blocking telemetry.

6. RedisIdempotencyStore Reference

Provides distributed, atomic idempotency guarantees across multi-instance clusters using Redis SET key val PX ttl NX semantics. Implements the Siglo IdempotencyStore interface.

Options (RedisIdempotencyOptions)

Claim States & Lifecycle Transitions

  1. claim(key: string, ttlMs?: number): Promise<'claimed' | 'completed' | 'in-progress'>:
    • Executes atomic Redis SET <keyPrefix><key> "in-progress" PX <effectiveTtl> NX.
    • If Redis returns 'OK': Returns 'claimed' (caller acquired exclusive execution right).
    • If the key already exists and starts with COMPLETED:: Returns 'completed'.
    • Otherwise returns 'in-progress' (another worker is currently processing this key).
  2. complete(key: string, result: unknown, ttlMs?: number): Promise<void>:
    • Overwrites key with COMPLETED:${JSON.stringify(result ?? null)} and resets TTL to effectiveTtl.
  3. fail(key: string, _error: unknown): Promise<void>:
    • Reads the key and deletes it (DEL) only if its current value is 'in-progress'. This frees the lock for immediate retry execution while preserving already completed results.
  4. getResult(key: string): Promise<unknown | undefined>:
    • Reads the key. If it starts with COMPLETED:, deserializes and returns the JSON payload. Returns undefined if missing or if parsing fails.
  5. get(key: string): Promise<unknown | undefined>:
    • Direct alias delegating to getResult(key).

7. RedisLock (Distributed Mutex & Single-Flight Coordination)

RedisLock prevents duplicate parallel execution of sensitive workflows (e.g., end-of-month payroll, nightly data synchronization) across multiple server instances.

Options (RedisLockOptions)

Methods

acquire(lockKey: string, ttlMs: number = 10000, customToken?: string): Promise<RedisLockResult>

Attempts atomic acquisition via SET <keyPrefix><lockKey> <token> PX <ttlMs> NX.
  • Returns RedisLockResult: { acquired: boolean, token: string }.
  • token defaults to lock_${Date.now()}_${randomBytes(16).toString('hex')} unless customToken is supplied.

release(lockKey: string, token: string): Promise<boolean>

Safely releases the lock only if the current value matches the caller’s unique token.
  • When client.eval is available: Executes atomic Lua script:
  • When client.eval is unavailable:
    • If strictAtomicity: true: Throws Error('RedisLock: client.eval is required for atomic release to prevent TOCTOU race condition.').
    • If strictAtomicity: false: Falls back to two-step check-and-delete (get followed by del).

withLock<T>(lockKey: string, fn: () => Promise<T>, ttlMs: number = 10000): Promise<T>

Executes an asynchronous function wrapped in single-flight lock protection:
  1. Calls acquire(lockKey, ttlMs).
  2. If acquisition fails, throws Error('Failed to acquire lock for key: ${lockKey} (in-use or concurrent execution)').
  3. Executes await fn().
  4. In a finally block, releases the lock via release(lockKey, token).catch(() => {}), guaranteeing no leaked locks even on unhandled exceptions.

8. RedisNonceStore (Webhook Replay Attack Prevention)

RedisNonceStore provides distributed, atomic signature and nonce deduplication for webhook endpoints and signature verification (used in @siglo/crypto and standalone webhook handlers).

Options (RedisNonceStoreOptions)

Methods

claim(key: string, ttlSeconds: number): Promise<boolean>

  • Calculates ttlMs = Math.max(1, Math.floor(ttlSeconds * 1000)).
  • Executes atomic SET <keyPrefix><key> "1" PX <ttlMs> NX.
  • Returns true if the key was claimed (first delivery attempt within tolerance window).
  • Returns false if the key already exists (replay attempt detected, request should be rejected).

9. Complete Production Example