@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:RedisStorageAdapter: Implements the SigloStorageAdapterinterface for execution stack checkpointing (with tenant isolation), durable wait record indexing for event-driven suspensions, step response caching, and non-blocking context mutation streaming.RedisIdempotencyStore: Implements the SigloIdempotencyStoreinterface for atomic, distributed single-flight execution claims, cached result retrieval, and automated lock releasing on failure.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.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:
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). Ifcheckpoint.namespaceis provided, stores undersiglo:<ns>:checkpoint:<stackId>. Returns early ifcheckpointorcheckpoint.stackIdis missing.getCheckpoint(stackId: string, namespace?: string): Promise<SerializedExecutionStack | undefined>: Retrieves and parses the serialized checkpoint. Ifnamespaceis specified, checks the tenant-namespaced key first, falling back to the un-namespaced key if not found. Returnsundefinedif 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 undersiglo:wait:<correlationKey>(if present) andsiglo:wait:<stackId>(if present) withcheckpointTtlMs.getWaitRecord(correlationKeyOrStackId: string): Promise<any | undefined>: Retrieves a wait record by correlation key or stack ID fromsiglo:wait:<key>. If not found, falls back to inspectingcp?.metadata?.waitRecordfromgetCheckpoint(correlationKeyOrStackId).cleanExpiredCheckpoints(): Promise<number>: Returns0. TTL expiration is handled automatically by Redis native keyspace expiration, eliminating background cleanup sweeps.getCachedStep(key: string): Promise<any | undefined>: Retrieves cached step response fromsiglo:cache:<key>. Returns parsed JSON, or the raw string if not JSON, orundefinedif key does not exist.setCachedStep(key: string, value: any, ttlMs?: number): Promise<void>: Writes cached step evaluation tosiglo:cache:<key>withPXTTL (ttlMs || cacheTtlMs). Objects are serialized directly; primitives are wrapped as{ value }.onContextChange(entry): Promise<void>: WhenrecordContextChangesistrue, writes field mutations toHSETatsiglo:[<ns>:]context:<stackId>:<stepId>and refreshes expiration withPEXPIRE(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
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).
- Executes atomic Redis
complete(key: string, result: unknown, ttlMs?: number): Promise<void>:- Overwrites key with
COMPLETED:${JSON.stringify(result ?? null)}and resets TTL toeffectiveTtl.
- Overwrites key with
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.
- Reads the key and deletes it (
getResult(key: string): Promise<unknown | undefined>:- Reads the key. If it starts with
COMPLETED:, deserializes and returns the JSON payload. Returnsundefinedif missing or if parsing fails.
- Reads the key. If it starts with
get(key: string): Promise<unknown | undefined>:- Direct alias delegating to
getResult(key).
- Direct alias delegating to
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 }. tokendefaults tolock_${Date.now()}_${randomBytes(16).toString('hex')}unlesscustomTokenis 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.evalis available: Executes atomic Lua script: - When
client.evalis unavailable:- If
strictAtomicity: true: ThrowsError('RedisLock: client.eval is required for atomic release to prevent TOCTOU race condition.'). - If
strictAtomicity: false: Falls back to two-step check-and-delete (getfollowed bydel).
- If
withLock<T>(lockKey: string, fn: () => Promise<T>, ttlMs: number = 10000): Promise<T>
Executes an asynchronous function wrapped in single-flight lock protection:
- Calls
acquire(lockKey, ttlMs). - If acquisition fails, throws
Error('Failed to acquire lock for key: ${lockKey} (in-use or concurrent execution)'). - Executes
await fn(). - In a
finallyblock, releases the lock viarelease(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
trueif the key was claimed (first delivery attempt within tolerance window). - Returns
falseif the key already exists (replay attempt detected, request should be rejected).
