Skip to main content
State adapters provide persistence for:
  • Thread subscriptions - Track which threads your bot is monitoring
  • Distributed locks - Prevent concurrent message processing across instances
  • Caching - Store temporary data with TTL support
All state operations use the StateAdapter interface, making it easy to swap implementations.

Available Adapters

Memory

Development & testing

Redis

Production (recommended)

ioredis

Production with Cluster/Sentinel

StateAdapter Interface

All state adapters implement this interface from chat/src/types.ts:467-499:
export interface StateAdapter {
  // Connection management
  connect(): Promise<void>;
  disconnect(): Promise<void>;

  // Thread subscriptions
  subscribe(threadId: string): Promise<void>;
  unsubscribe(threadId: string): Promise<void>;
  isSubscribed(threadId: string): Promise<boolean>;

  // Distributed locking
  acquireLock(threadId: string, ttlMs: number): Promise<Lock | null>;
  releaseLock(lock: Lock): Promise<void>;
  extendLock(lock: Lock, ttlMs: number): Promise<boolean>;

  // Caching (generic key-value store)
  get<T = unknown>(key: string): Promise<T | null>;
  set<T = unknown>(key: string, value: T, ttlMs?: number): Promise<void>;
  delete(key: string): Promise<void>;
}

export interface Lock {
  threadId: string;
  token: string;
  expiresAt: number;
}

Thread Subscriptions

Subscriptions persist which threads your bot is actively monitoring. When subscribed, all future messages in that thread trigger onSubscribedMessage handlers.
chat.onNewMention(async (thread, message) => {
  await thread.subscribe(); // Persist subscription
  await thread.post("I'll monitor this thread for updates!");
});

chat.onSubscribedMessage(async (thread, message) => {
  // Called for all messages in subscribed threads
  await thread.post(`Got your message: ${message.text}`);
});

How It Works

  1. User @-mentions your bot → triggers onNewMention
  2. Handler calls thread.subscribe() → adds threadId to state
  3. Future messages check state.isSubscribed(threadId)
  4. Subscribed messages trigger onSubscribedMessage handlers
Subscriptions persist across restarts (except with MemoryStateAdapter). Your bot automatically resumes monitoring threads after deployment.

Distributed Locking

Locks prevent concurrent processing of the same thread across multiple server instances or requests.

Why Locking Matters

Without locks:
  • Webhook retries could trigger duplicate processing
  • Multiple serverless instances could race on the same message
  • Concurrent edits to thread state could corrupt data
With locks:
// Automatic locking in Chat SDK (internal)
const lock = await state.acquireLock(threadId, 30000); // 30s TTL
if (!lock) {
  // Another instance is already processing this thread
  return;
}

try {
  await processMessage(thread, message);
} finally {
  await state.releaseLock(lock);
}

Lock Properties

  • TTL-based - Locks auto-expire to prevent deadlocks from crashed instances
  • Token-based - Only the lock holder can release or extend it
  • Atomic - Acquisition uses SET NX EX for race-free locking
Always set a reasonable TTL (default: 30s). If your message handler can take longer, use extendLock() to refresh the TTL periodically.

Caching

The cache API provides generic key-value storage with optional TTL:
const state = chat.getState();

// Store with 5-minute TTL
await state.set('user:123:preferences', { theme: 'dark' }, 5 * 60 * 1000);

// Retrieve
const prefs = await state.get<UserPreferences>('user:123:preferences');

// Delete
await state.delete('user:123:preferences');

Use Cases

  • Rate limiting - Track API call counts per user
  • User preferences - Cache settings between messages
  • Temporary state - Store wizard/form progress
  • Deduplication - Track processed message IDs
Chat SDK uses the cache internally for message deduplication (5-minute TTL by default, configurable via dedupeTtlMs).

Choosing an Adapter

Pros:
  • Zero configuration
  • No external dependencies
  • Fast for local development
Cons:
  • State lost on restart
  • Not shared across instances
  • Console warning in production
When to use: Local development, unit tests, quick prototypes
Pros:
  • Redis Cluster support
  • Sentinel support for HA
  • More configuration options
  • Can reuse existing client
Cons:
  • Requires Redis server
  • More complex setup
When to use: High-availability setups, Redis Cluster, existing ioredis usage

Connection Management

All adapters require explicit connection before use:
import { Chat } from 'chat';
import { createRedisState } from '@chat-adapter/state-redis';

const state = createRedisState({ url: process.env.REDIS_URL });
await state.connect();

const chat = new Chat({
  userName: 'mybot',
  adapters: { /* ... */ },
  state,
});
The Chat SDK automatically calls connect() during initialization. You typically don’t need to call it manually.

Disconnection

For graceful shutdown:
process.on('SIGTERM', async () => {
  await state.disconnect();
  process.exit(0);
});

Error Handling

State adapters throw errors for:
  • Connection failures
  • Operations before connect() is called
  • Network timeouts (Redis adapters)
try {
  await thread.subscribe();
} catch (error) {
  logger.error('Failed to subscribe', { error, threadId: thread.id });
  // Decide: retry, fall back, or notify user
}
Redis adapters log connection errors but don’t throw during background operations. Monitor your logs for "Redis client error" messages.

Next Steps

Memory Adapter

Quick setup for development

Redis Adapter

Production deployment guide

ioredis Adapter

Cluster and Sentinel setup

Core Concepts

Learn about threads and subscriptions