OddSockets Ruby SDK

Official Ruby SDK for OddSockets real-time messaging platform

RubyGems Ruby 3.0+ Thread Safe High Performance Promise-like

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

bash
bundle add oddsockets
bash
gem install oddsockets
ruby
# Gemfile
gem 'oddsockets'

Quick Start

Basic Usage

ruby
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

ruby
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

ruby
# 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.

ruby
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.

ruby
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.

ruby
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

ruby
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

ruby
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

ruby
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.

  • Typingstart_typing, stop_typinguser_typing, user_stopped_typing
  • Reactionsadd_reaction, remove_reaction, get_reactionsreaction_added, reaction_removed
  • Threadsthread_reply, get_thread, subscribe_thread, follow_thread, mark_thread_readthread_reply, thread_subscribed, thread_followed, thread_read_updated
  • Read receiptsmark_read, mark_all_read, get_unread_countsuser_read, unread_count_updated, all_marked_read
  • Messagesedit_message, delete_message, pin_message, unpin_message, get_pinned_messages, search_messagesmessage_edited, message_deleted, message_pinned, message_unpinned
  • Presence & statusset_status, set_custom_status, set_dnd, get_user_presenceuser_status_changed, custom_status_updated, dnd_status_changed
  • Channelscreate_channel, update_channel, archive_channel, invite_to_channel, join_channel, leave_channelchannel_created, channel_updated, user_invited, user_joined_channel, user_left_channel
  • DMscreate_dm, send_dm, get_dm_conversationsdm_created, dm_received
  • Notificationssubscribe_notifications, get_notifications, mark_notification_read, clear_notificationsnotification, notification_read, notifications_cleared
  • Searchsearch_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| ... }.

ruby
# 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. Acks challenge_create_success.
  • report_progress — fire-and-forget metric progress (no ack).
  • complete_challenge — finalize with an outcome. Acks challenge_complete_success.
  • unlock_achievement — fire-and-forget; pass percentComplete (0–100) (no ack).
  • get_standings — request top-N plus the caller's rank. Acks challenge_standings_success.
  • get_achievements — query achievement state. Acks achievement_state.
  • send_challenge_invite — directed invite to another user. Acks challenge_invite_success.
  • reply_challenge_invite — accept/decline an invite. Acks challenge_reply_success.
  • cancel_challenge_invite — cancel a sent invite. Acks challenge_invite_cancel_success.
  • get_challenge_invites — list pending invites. Acks challenge_invites.

Completion outcomes

Pass one of these as the outcome to complete_challenge:

  • completed — win (rank 1)
  • failed — loss
  • tied — draw
  • conceded — resign / concede
  • expired — 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 broadcastschallenge_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, 01

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.

<50ms
Latency
99.9%
Uptime
32KB
Max Message
1M+
Messages/sec

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 .wait futures
  • 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.new and run the event loop with the SDK's futures.