OddSockets Ruby SDK
Official Ruby SDK for OddSockets real-time messaging platform
Overview & Features
The OddSockets Ruby SDK provides a powerful, thread-safe interface for real-time messaging with Ruby idioms and modern async support.
Ruby Idioms
Built with Ruby best practices including predicate methods, block syntax, and symbol keys.
Thread Safe
Uses concurrent-ruby for thread-safe collections and operations in multi-threaded environments.
Promise-like API
Async operations return Concurrent::Promises for clean asynchronous programming.
High Performance
Optimized for low latency with efficient WebSocket connections and smart routing.
Cost Effective
No per-message pricing, industry-standard 32KB message limits, transparent pricing.
Rails Ready
Perfect integration with Rails applications and Ruby web frameworks.
Installation
bundle add oddsockets
gem install oddsockets
# Gemfile
gem 'oddsockets'
Quick Start
Basic Usage
require 'oddsockets'
client = OddSockets::Client.new(
api_key: 'ak_live_1234567890abcdef'
)
channel = client.channel('my-channel')
# Subscribe to messages
channel.subscribe do |message|
puts "Received: #{message}"
end
# Publish a message
channel.publish('Hello, Ruby!')
Promise-like Async Operations
require 'oddsockets'
client = OddSockets::Client.new(api_key: 'ak_live_1234567890abcdef')
channel = client.channel('async-channel')
# Async operations with promises
channel.subscribe { |msg| puts msg }.wait
result = channel.publish('Hello, Async!').wait
history = channel.history(count: 10).wait
puts "Published: #{result['messageId']}"
puts "History: #{history.length} messages"
Rails Integration
# config/initializers/oddsockets.rb
OddSockets.configure do |config|
config.manager_url = Rails.application.credentials.oddsockets_url
config.log_level = Rails.env.production? ? :info : :debug
end
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
def create
client = OddSockets::Client.new(api_key: current_user.api_key)
channel = client.channel("user_#{current_user.id}")
channel.publish(message_params[:content])
render json: { status: 'sent' }
end
end
Configuration
The client is created with OddSockets::Client.new. Only api_key is required; everything else has sensible defaults.
client = OddSockets::Client.new(
api_key: 'YOUR_API_KEY', # required
user_id: 'my-agent', # stable identity for presence & DMs
auto_connect: true, # connect immediately (default: true)
options: {} # optional advanced settings
)
Global Configuration
Application-wide defaults (manager URL, timeout, log level) can be set once with OddSockets.configure — handy for Rails initializers.
OddSockets.configure do |config|
config.manager_url = 'https://connect.oddsockets.com'
config.timeout = 15
config.log_level = :info
end
Examples
Presence-aware Chat Room
Enable presence when subscribing to learn who joins and leaves in real time.
require 'oddsockets'
client = OddSockets::Client.new(api_key: 'YOUR_API_KEY', user_id: 'alice')
client.connect
channel = client.channel('room-42')
# Presence events arrive on the client surface
client.on('presence_join') { |e| puts "#{e['userId']} joined" }
client.on('presence_leave') { |e| puts "#{e['userId']} left" }
channel.subscribe(nil, { enable_presence: true }) do |msg|
puts "Message: #{msg}"
end.wait
channel.publish(text: 'Hello, room!')
Message History
history = channel.history(count: 20).wait
history.each { |m| puts m['text'] }
Enhanced Features
Beyond core pub/sub, OddSockets ships a Slack-like enhanced surface: typing indicators, reactions, threads, read receipts, presence/status, notifications, DMs, channel management, message editing and search. It lives on client.enhanced. Requests are sent with a client.enhanced.* method (snake_case) and the worker broadcasts back as events you receive with client.on('<event>') { |e| ... }.
Typing & Reactions
require 'oddsockets'
client = OddSockets::Client.new(api_key: 'YOUR_API_KEY', user_id: 'alice')
client.connect
channel = client.channel('room-42')
channel.subscribe(nil, { enable_presence: true }) { |msg| }.wait
# Receive-path: broadcasts from other users on the channel
client.on('user_typing') { |e| puts "#{e['userId']} is typing" }
client.on('reaction_added') { |e| puts "#{e['userId']} reacted #{e['emoji']}" }
# Send-path: enhanced actions over the live socket
client.enhanced.start_typing('alice', 'room-42')
client.enhanced.add_reaction(
message_id: 'msg-1', channel: 'room-42', emoji: ':thumbsup:',
user_id: 'alice', user_name: 'Alice'
)
Threads
client.on('thread_reply') { |e| puts 'New reply' }
client.enhanced.thread_reply(
channel: 'room-42',
parent_message_id: 'msg-1',
message: 'Replying in the thread',
user_id: 'alice',
user_name: 'Alice'
)
Enhanced surface
Each area exposes send methods on client.enhanced; the worker broadcasts the paired events which you handle with client.on(...). Query methods (get_*, search_*) take a block that yields the worker response.
- Typing —
start_typing,stop_typing→user_typing,user_stopped_typing - Reactions —
add_reaction,remove_reaction,get_reactions→reaction_added,reaction_removed - Threads —
thread_reply,get_thread,subscribe_thread,follow_thread,mark_thread_read→thread_reply,thread_subscribed,thread_followed,thread_read_updated - Read receipts —
mark_read,mark_all_read,get_unread_counts→user_read,unread_count_updated,all_marked_read - Messages —
edit_message,delete_message,pin_message,unpin_message,get_pinned_messages,search_messages→message_edited,message_deleted,message_pinned,message_unpinned - Presence & status —
set_status,set_custom_status,set_dnd,get_user_presence→user_status_changed,custom_status_updated,dnd_status_changed - Channels —
create_channel,update_channel,archive_channel,invite_to_channel,join_channel,leave_channel→channel_created,channel_updated,user_invited,user_joined_channel,user_left_channel - DMs —
create_dm,send_dm,get_dm_conversations→dm_created,dm_received - Notifications —
subscribe_notifications,get_notifications,mark_notification_read,clear_notifications→notification,notification_read,notifications_cleared - Search —
search_messages,search_in_channel,search_by_user,filter_messages→ block results
For any worker event not wrapped above, subscribe with the raw client.on('<event>') { |e| ... } API — all enhanced broadcasts are forwarded to the client surface.
Challenges & Leaderboards
Challenges, leaderboards and achievements build on the same live socket. The send side lives on the enhanced surface (client.enhanced); request/query methods yield the worker's reply to their block, while progress and achievement calls are fire-and-forget. Inbound broadcasts arrive on the client event surface — subscribe with the client's normal client.on('<event>') { |e| ... }.
# Receive-path: standings shifts and completions from other players
client.on('leaderboard_rank_change') { |e| puts "rank -> #{e['rank']}" }
# Create a ranked challenge, report progress, read standings, finalize
client.enhanced.create_challenge(challenge_id: 'daily-sprint', metric: 'score', ranked: true)
client.enhanced.report_progress(challenge_id: 'daily-sprint', value: 120)
client.enhanced.get_standings(challenge_id: 'daily-sprint', limit: 10) { |data| p data['standings'] }
client.enhanced.complete_challenge(challenge_id: 'daily-sprint', outcome: 'completed')
Methods
Send methods live on client.enhanced. Query/request methods take a block that yields the worker ack; progress and achievement calls are fire-and-forget (no ack).
create_challenge— create a challenge/leaderboard. Ackschallenge_create_success.report_progress— fire-and-forget metric progress (no ack).complete_challenge— finalize with anoutcome. Ackschallenge_complete_success.unlock_achievement— fire-and-forget; passpercentComplete(0–100) (no ack).get_standings— request top-N plus the caller's rank. Ackschallenge_standings_success.get_achievements— query achievement state. Acksachievement_state.send_challenge_invite— directed invite to another user. Ackschallenge_invite_success.reply_challenge_invite— accept/decline an invite. Ackschallenge_reply_success.cancel_challenge_invite— cancel a sent invite. Ackschallenge_invite_cancel_success.get_challenge_invites— list pending invites. Ackschallenge_invites.
Completion outcomes
Pass one of these as the outcome to complete_challenge:
completed— win (rank 1)failed— losstied— drawconceded— resign / concedeexpired— timed out
Progressive achievements
unlock_achievement always emits the wire event achievement_unlock; the worker is authoritative and derives the outbound broadcast from percentComplete. A value < 100 broadcasts achievement_progress (status in_progress); >= 100 or omitting percentComplete broadcasts achievement_unlock (status unlocked). You never emit achievement_progress yourself.
Inbound events
Subscribe with the client's normal client.on('<event>') { |e| ... }.
- Room broadcasts —
challenge_progress,leaderboard_rank_change,challenge_complete,achievement_unlock,achievement_progress - Directed (per-user) —
challenge_invited,challenge_reply_received,challenge_invite_cancelled
Usage Analytics
Pull your tenant's headline usage tiles — monthly active users, daily active users, total messages published, and error rate — straight from the SDK, without hand-rolling a REST call. usage_stats resolves the same four tiles the developer dashboard renders.
stats = client.usage_stats
# { mau: 1240, dau: 210, total_messages: 84213, error_rate: 0.002, ... }
# A nil tile stays nil — render an em-dash so a missing tile is never a fake zero.
tile = ->(value) { value.nil? ? "\u2014" : value }
puts "MAU: #{tile.call(stats[:mau])}"
puts "DAU: #{tile.call(stats[:dau])}"
puts "Messages: #{tile.call(stats[:total_messages])}"
puts "Errors: #{tile.call(stats[:error_rate])}"
The four tiles
- mau — monthly active users for your owner scope
- dau — daily active users
- total_messages — total messages published
- error_rate — publish error rate,
0–1
Honesty rule — nil, never a fake zero
Each tile is a number or nil. A nil means that leg of the analytics pipeline is not live yet for your tenant — the SDK returns nil verbatim and never coerces it to 0. Render an em-dash (—) for a nil tile so you never show a fabricated zero.
Requires an API key
usage_stats reads your owner-scoped analytics, so it needs an api_key. Keyless / token-only clients have no owner scope to query and will raise usage_stats requires an apiKey.
Performance & Compatibility
The OddSockets Ruby SDK is built for low-latency real-time messaging with thread-safe internals.
Ruby Support
- Ruby 3.0+
- Ruby 3.2+ (Recommended)
- Ruby 3.3+ (Latest)
- JRuby & TruffleRuby compatible
Runtime
- Thread-safe (concurrent-ruby)
- Automatic reconnection
- Promise-like
.waitfutures - Manager → Worker discovery
Frameworks
The SDK is plain Ruby and drops into any framework or a bare script.
- Rails — configure once in an initializer with
OddSockets.configure, then create a client per request or in a background job. - Sidekiq / ActiveJob — publish from workers to fan out real-time updates to subscribers.
- Sinatra / Roda — lightweight services can create a client at boot and reuse it across requests.
- Plain Ruby — scripts and daemons connect with
OddSockets::Client.newand run the event loop with the SDK's futures.