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

Install from source for now. The RubyGems package is not published yet, so the commands in the tabs below will not resolve. Use this instead — it pulls the same code the tabs will install once the package is live:
ruby
# Gemfile
gem 'oddsockets', git: 'https://github.com/jyswee/oddsockets-ruby-sdk'
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.

  • 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| ... }.

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

32KB
Max message size
100M
Messages/mo (Scale)
5,000
Peak connections (Scale)
99.999%
Uptime SLA (Enterprise)

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.