# WebSockets Hanami: 1.x

# AnyCable

Gemfile

gem 'anycable'
gem 'litecable'
1
2

AnyCable server should be installed separately (anycable-go or erlycable, but anycable-go is supposed for convenience).

Two processes should be started (via Foreman or Docker)

  • anycable-go
  • bundle exec anycable -r ./config/boot.rb

# LiteCable

Contains "glue" from Anycable gem to our Ruby app - channels, broadcasting etc.

/config/initializers/anycable.rb

LiteCable.anycable!

AnyCable.connection_factory = Web::Sockets::Connection

Anycable.configure do |config|
  # put configs here, if needed
end

AnyCable.capture_exception do |ex|
  Hanami.logger.error "AnyCable error: #{ex.inspect}"
end
1
2
3
4
5
6
7
8
9
10
11
  • Create sockets folder in apps/web
  • Create apps/web/sockets/connection.rb
  • Implement Web::Sockets::Connection class with Auth logic

for example apps/web/sockets/connection.rb

module Web
  module Sockets
    class Connection < LiteCable::Connection::Base
      identified_by :current_user_id

      def connect
        if jwt_token.present?
          # Implement your own `Authentication.get_user_by_jwt`
          user = Authentication.get_user_by_jwt(token)
          reject_unauthorized_connection unless user
        end
        @current_user_id = user.present? ? user.id : nil
      end

      def disconnect
        Hanami.logger.debug "Disconnected #{@current_user_id}"
      end

      private

      def jwt_token
        header = request.params["token"]
      end
    end
  end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  • Create ApplicaionChannel

apps/web/sockets/application_channel.rb

module Web
  module Sockets
    class ApplicationChannel < LiteCable::Channel::Base
    end
  end
end
1
2
3
4
5
6
  • Implementa yor own Channels like



 






 





module Web
  module Sockets
    class CommentsChannel < ApplicationChannel
      # Indentifier name - the same to use on the fronend side
      identifier :comments

      def subscribed
        post_id = params['post_id']
        reject unless post_id
        # stream name - the same you publish
        stream_from "comments:post:#{post_id}"
      end
    end
  end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • Broadcast to this Channel from anywhere in the app






 

# Message could have any format
message = {
  action: :new,
  payload: {user: 1, comment: 'Hello world'}
}

LiteCable.broadcast("comments:post:#{post.uuid}", message)
1
2
3
4
5
6
7

# Connect via JavaScript

  • Install actioncable package via NPM (or download as compiled file).
  • Connect to ws://domain.com/ws?jwt_token=... via ActionCable.createConsumer (jwt_token important only for this example, ot identify User, cookie could be used "transparently", read AnyCable docs about Headers pass)
  • All other operations are identical to ActionCable, with small differences that covered by AnyCable docs

# AnyCable with Docker

.env

ANYCABLE_HOST=0.0.0.0
ANYCABLE_PORT=8181
ANYCABLE_PATH=/ws
ANYCABLE_RPC_HOST=anycable-rpc:50051
ANYCABLE_HEADERS=COOKIE,AUTHORIZATION,ORIGIN
ANYCABLE_REDIS_URL=redis://redis:6379/1
ANYCABLE_LOG_LEVEL=info
ANYCABLE_DEBUG=false
1
2
3
4
5
6
7
8

docker-compose.yaml

anycable:
  container_name: anycable
  image: anycable/anycable-go:v0.6.0
  command: anycable-go
  working_dir: /web
  ports:
    - 8181:8181
  env_file: .env
  depends_on:
    - redis
    - anycable-rpc

anycable-rpc:
  container_name: anycable_rpc
  image: web:latest
  command: bundle exec anycable -r ./config/boot.rb
  working_dir: /web
  stdin_open: true
  tty: true
  env_file: .env
  volumes:
    - .:/web:cached
    - bundle-cache:/usr/local/bundle:cached
  ports:
    - 50051:50051
  depends_on:
    - redis
  environment:
    # tricky thing `anycable-rpc` needs [::]:50051,
    # while `anycable-go` needs a link to `anycable-rpc:50051` inside the same ENV var
    - ANYCABLE_RPC_HOST="[::]:50051"

redis:
  container_name: redis
  image: redis:4.0-alpine
  env_file: .env
  ports:
    - 6381:6379
  volumes:
    - redis-storage:/data:cached

volumes:
  bundle-cache: null
  redis-storage: null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44