Project

blackevin

0.0
The project is in a healthy, maintained state
Sign token requests, publish, read history and presence, and manage queues on Blackevin. Standard library only: no runtime dependencies. Ruby 3.0 and up. Works in Rails, Sinatra, Hanami or plain Ruby.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

>= 7.0
>= 13.0
>= 3.13, < 4.0
>= 0.22
>= 1.0
 Project Readme

blackevin

The server-side Ruby SDK for Blackevin realtime.

Sign token requests for your browsers, publish from a job or a webhook, read history and presence, manage queues.

No runtime dependencies. net/http, openssl and json from the standard library — nothing that can break under you in an upgrade. Ruby 3.0 and up, in Rails, Sinatra, Hanami or a plain script.

gem 'blackevin'

The client

You work with an instance. There is no global client and no class-level call that touches the network.

bk = Blackevin::Rest.new

ch = bk.channels.get('room:42')
ch.publish('greeting', {text: 'hi'})

With BLACKEVIN_KEY in the environment, new needs no arguments. Creating one is cheap — it opens no connection and holds no state — so build it where you use it, or keep one in a constant; both are fine, and an instance is thread-safe.

Each part stands alone

Nothing forces a chain through Blackevin::Rest. Every part is a class you can build by itself; without a client: it makes its own from the configuration.

Blackevin::Rest::Auth.new.create_token_request(client_id: current_user.id)
Blackevin::Rest::Channel.new('room:42').publish('greeting', {text: 'hi'})
Blackevin::Rest::Presence.new('room:42').get
Blackevin::Rest::Queues.new.list
Blackevin::Rest::Clients.new.disconnect(user.id)

Pass client: to share one, or to use another key:

bk = Blackevin::Rest.new(key: other_key)

Blackevin::Rest::Queues.new(client: bk).list

bk.auth, bk.channels, bk.clients and bk.queues are these same classes, already holding bk.

Every option can be passed directly:

bk = Blackevin::Rest.new(key: 'ck_live_….kAbC', read_timeout: 3)

Defaults, set once

Blackevin.configure holds the defaults a new Blackevin::Rest starts from. It builds nothing and calls nothing. An argument to new wins over it, and it wins over the environment.

# config/initializers/blackevin.rb
Blackevin.configure do |config|
  config.key = Rails.application.credentials.dig(:blackevin, :key)   # secret.keyId
  config.open_timeout = 5                                          # seconds
  config.read_timeout = 10
end

Against a local or self-hosted node, which serves REST and the socket from one origin, set BLACKEVIN_ENDPOINT=ws://localhost:3000 (or config.endpoint) and the REST base is derived from it.

Token endpoint — the part every app needs

Your API key is your account, so it never goes to a browser. The browser asks your server for a TokenRequest: signed with the key, scoped to what this user may touch, dead after its ttl. Signing is local — no call to Blackevin, so your sign-in never depends on us being reachable.

The browser side is the JavaScript SDK with authUrl: "/blackevin/token".

Rails

# app/controllers/blackevin_tokens_controller.rb
class BlackevinTokensController < ApplicationController
  before_action :authenticate_user!

  def show
    render json: Blackevin::Rest::Auth.new.create_token_request(
      client_id: current_user.id,
      ttl: 1.hour.in_milliseconds,
      capability: {
        "team:#{current_user.team_id}" => %w[subscribe publish],
        "presence:team:#{current_user.team_id}" => %w[presence]
      }
    )
  end
end

# config/routes.rb
resource :blackevin_token, only: :show, path: 'blackevin/token'

The key can live in credentials instead of the environment — the Railtie reads credentials.blackevin.key, and config.blackevin.* in an environment file:

# config/environments/development.rb
config.blackevin.endpoint = 'ws://localhost:3000'

Sinatra

require 'sinatra'
require 'blackevin'

get '/blackevin/token' do
  halt 401 unless current_user

  content_type :json
  Blackevin::Rest::Auth.new.create_token_request(client_id: current_user.id).to_json
end

Anything Rack (Hanami, Roda, config.ru)

Blackevin::TokenEndpoint is a Rack application. The block receives the Rack env and answers who is asking; nil is a 401.

TOKENS = Blackevin::TokenEndpoint.new do |env|
  user = env['warden']&.user

  next unless user

  {client_id: user.id, capability: {"team:#{user.team_id}" => %w[subscribe publish]}}
end

# Hanami:  mount TOKENS, at: '/blackevin/token'
# Rails:   mount TOKENS, at: '/blackevin/token'
# Rack:    map('/blackevin/token') { run TOKENS }

Publish

channel = Blackevin::Rest::Channel.new("orders:#{order.id}")

channel.publish('status', {state: 'shipped'})

It goes through the same fanout a socket publish does: subscribers, account queues and integrations all see it. One HTTP request per publish — from a Rails request, prefer a job:

class BlackevinPublishJob < ApplicationJob
  retry_on Blackevin::ConnectionError, wait: :polynomially_longer

  def perform(channel, event, data)
    Blackevin::Rest::Channel.new(channel).publish(event, data)
  end
end

History and presence

channel.history(limit: 50)        # => [Blackevin::Message], newest first
channel.presence.get              # => [Blackevin::PresenceMember]
channel.presence.history          # => [Blackevin::PresenceEvent]

message = channel.history.first
message.name        # 'status'
message.data        # {'state' => 'shipped'}
message.time        # a Time; message.timestamp is the wire value, in ms

Sign a user out everywhere

Blackevin::Rest::Clients.new.disconnect(user.id)   # => how many connections were closed

Queues

The key needs the amqp-subscribe capability. A queue is addressed by its id.

queues = Blackevin::Rest::Queues.new

queue = queues.upsert(name: 'inbox', max_length: 10_000)
queues.add_rule(queue.id, source_pattern: 'orders:*')

queues.update(queue.id, enabled: false)   # pause
queues.list(all: true)                    # paused ones included
queues.delete(queue.id)

Errors

One rescue covers everything the SDK raises:

begin
  channel.publish('status', payload)
rescue Blackevin::Error => error
  error.status_code   # 403, 429, … or nil when no response arrived
  error.reason        # 'queue_limit', 'connection_limit', or nil
  error.quota?        # a plan ceiling — show an upgrade prompt, retry later
  error.message       # the server's own sentence; do not branch on it
end

Errors, token requests and every returned value support pattern matching, so a rescue can branch by shape:

rescue Blackevin::Error => error
  case error
  in {reason: 'queue_limit'} then redirect_to upgrade_path
  in {status_code: 401 | 403} then raise
  in {status_code: nil} then retry_job wait: 30.seconds
  end
  • Blackevin::ConnectionError — DNS, refused, TLS, timeout. Worth retrying.
  • Blackevin::ConfigurationError — a missing or malformed key, raised before any request.

Acting as one client

A token instead of a key restricts the client to that token's capability:

auth = Blackevin::Rest::Auth.new

details = auth.request_token(
  auth.create_token_request(client_id: 'worker-1', capability: {'jobs:*' => %w[publish]})
)

worker = Blackevin::Rest.new(token: details.token)

Blackevin::Rest::Channel.new('jobs:1', client: worker).publish('done')

Testing your app

Swap the default transport and no Blackevin::Rest leaves the process:

Blackevin.configure do |config|
  config.key = 'secret.test'
  config.transport = ->(request) { Blackevin::Response.new(status: 200, body: '{}') }
end

Development

bundle install
bundle exec rake            # rspec + rubocop (Standard rules, single quotes)
bundle exec rake coverage        # SimpleCov: report in coverage/index.html, fails under the floor
bundle exec rake contract:sync   # refresh spec/contract from ../blackevin/spec

Releasing

Bump lib/blackevin/version.rb, add the entry to CHANGELOG.md, commit, then:

bundle exec rake tag   # tags vX.Y.Z from version.rb and pushes it

The tag runs .github/workflows/release.yml: tests, publish to RubyGems by Trusted Publishing (no API key), and a GitHub release with the .gem attached.

spec/contract is a copy of the language-neutral contract — an OpenAPI file and JSON fixtures — that every Blackevin SDK is tested against. The suite fails when the contract gains an operation this gem does not implement.

License

MIT.