State Adapters
Memory, Redis, PostgreSQL, and MySQL state backends for thread subscriptions, locks, deduplication, and key-value storage.
State adapters provide persistent storage for thread subscriptions, event deduplication, per-thread locks, and arbitrary key-value data. ChatSDK ships four implementations.
Memory State
ChatSDK::State::Memory stores everything in-process using a Hash protected by a Mutex.
state = ChatSDK::State::Memory.new
bot = ChatSDK::Chat.new(
user_name: "my-bot",
adapters: { slack: slack },
state: state
)When to use:
- Development and testing
- Single-process deployments
- Prototyping
Limitations:
- Data is lost when the process restarts
- Does not work across multiple processes, threads in separate processes, or dynos
See Memory State.
Redis State
ChatSDK::State::Redis stores data in Redis using the redis-client gem. All operations are atomic.
# Add to your Gemfile:
# gem "chat_sdk-state-redis"
require "chat_sdk-state-redis"
state = ChatSDK::State::Redis.new(url: "redis://localhost:6379")
bot = ChatSDK::Chat.new(
user_name: "my-bot",
adapters: { slack: slack },
state: state
)When to use:
- Production deployments
- Multi-process or multi-dyno setups
- When you need state to survive restarts
See Redis State.
PostgreSQL State
ChatSDK::State::Pg stores data in PostgreSQL using the pg gem. Tables are auto-created on initialization with JSONB value storage and row-level locking.
# Add to your Gemfile:
# gem "chat_sdk-state-pg"
require "chat_sdk/state/pg"
state = ChatSDK::State::Pg.new(url: "postgresql://localhost/myapp")
bot = ChatSDK::Chat.new(
user_name: "my-bot",
adapters: { slack: slack },
state: state
)When to use:
- You already run PostgreSQL and don't want another dependency
- You need durable state with ACID guarantees
- Multi-process deployments
Options:
url:-- PostgreSQL connection URL (orDATABASE_URLenv var)connection:-- pass an existingPG::Connectionkey_prefix:-- namespace for multi-tenant use (default:"chat_sdk")auto_migrate:-- create tables on init (default:true)
See PostgreSQL State.
MySQL State
ChatSDK::State::Mysql stores data in MySQL using the mysql2 gem. Tables are auto-created on initialization with JSON value storage and InnoDB row-level locking.
# Add to your Gemfile:
# gem "chat_sdk-state-mysql"
require "chat_sdk/state/mysql"
state = ChatSDK::State::Mysql.new(url: "mysql2://root@localhost/myapp")
bot = ChatSDK::Chat.new(
user_name: "my-bot",
adapters: { slack: slack },
state: state
)When to use:
- You already run MySQL and don't want another dependency
- You need durable state with ACID guarantees
- Multi-process deployments
Options:
url:-- MySQL connection URL (orMYSQL_URLenv var)connection:-- pass an existingMysql2::Clientkey_prefix:-- namespace for multi-tenant use (default:"chat_sdk")auto_migrate:-- create tables on init (default:true)
See MySQL State.
Lock Policies
When multiple events arrive for the same thread concurrently, ChatSDK acquires a per-thread lock before invoking handlers. The on_lock_conflict option controls what happens when a lock is already held.
:drop (default)
Silently discard the event. The handler does not fire for the conflicting event.
bot = ChatSDK::Chat.new(
user_name: "my-bot",
adapters: { slack: slack },
state: state,
on_lock_conflict: :drop
):force
Take the lock from the current holder and process the event. Use this when you want the latest event to always win.
bot = ChatSDK::Chat.new(
user_name: "my-bot",
adapters: { slack: slack },
state: state,
on_lock_conflict: :force
)Custom Proc
Pass a proc for fine-grained control. It receives the thread key and event, and must return :force or :drop.
bot = ChatSDK::Chat.new(
user_name: "my-bot",
adapters: { slack: slack },
state: state,
on_lock_conflict: ->(thread_key, event) {
event.type == :action ? :force : :drop
}
)Event Deduplication
ChatSDK automatically deduplicates events using the state backend. When an event arrives, its ID is stored with a TTL. If the same event arrives again within the TTL window, it is silently dropped.
Configure the TTL with dedupe_ttl (default: 600 seconds):
bot = ChatSDK::Chat.new(
user_name: "my-bot",
adapters: { slack: slack },
state: state,
dedupe_ttl: 300 # 5 minutes
)TTL State
Use thread.set_state and thread.state to store arbitrary per-thread data with a 30-day TTL:
bot.on_new_mention do |thread, message|
count = thread.state || { "count" => 0 }
count["count"] += 1
thread.set_state(count)
thread.post("This thread has been mentioned #{count["count"]} times.")
endBuilding Custom State Adapters
See Building State Adapters for a full guide on creating your own state backend.