GithubHelp home page GithubHelp logo

palkan / litecable Goto Github PK

View Code? Open in Web Editor NEW
293.0 5.0 32.0 3.54 MB

Lightweight Action Cable implementation (Rails-free)

License: MIT License

Ruby 99.80% Shell 0.20%
actioncable sinatra websockets

litecable's Introduction

Gem Version Build

Lite Cable

Lightweight ActionCable implementation.

Contains application logic (channels, streams, broadcasting) and also (optional) Rack hijack based server (suitable only for development and test due to its simplicity).

Compatible with AnyCable (for production usage).

Sponsored by Evil Martians

Examples

Installation

Add this line to your application's Gemfile:

gem "litecable"

And run bundle install.

Usage

Please, checkout Action Cable guides for general information. Lite Cable aims to be compatible with Action Cable as much as possible without the loss of simplicity and lightness.

You can use Action Cable javascript client without any change (precompiled version can be found here).

Here are the differences:

  • Use LiteCable::Connection::Base as a base class for your connection (instead of ActionCable::Connection::Base)

  • Use LiteCable::Channel::Base as a base class for your channels (instead of ActionCable::Channel::Base)

  • Use LiteCable.broadcast to broadcast messages (instead of ActionCable.server.broadcast)

  • Explicitly specify channels names:

class MyChannel < LiteCable::Channel::Base
  # Use this id in your client to create subscriptions
  identifier :chat
end
App.cable.subscriptions.create('chat', ...)

Using a custom channel registry

Alternatively to eager loading all channel classes and providing identifiers, you can build a custom channel registry object, which can perform channel class lookups:

# DummyRegistry which always returns a predefined channel class
class DummyRegistry
  def lookup(channel_id)
    DummyChannel
  end
end

LiteCable.channel_registry = DummyRegistry.new

Using built-in server (middleware)

Lite Cable comes with a simple Rack middleware for development/testing usage. To use Lite Cable server:

  • Add gem "websocket" to your Gemfile

  • Add require "lite_cable/server"

  • Add LiteCable::Server::Middleware to your Rack stack, for example:

Rack::Builder.new do
  map "/cable" do
    # You have to specify your app's connection class
    use LiteCable::Server::Middleware, connection_class: App::Connection
    run proc { |_| [200, {"Content-Type" => "text/plain"}, ["OK"]] }
  end
end

Using with AnyCable

Lite Cable is AnyCable-compatible out-of-the-box.

If AnyCable gem is loaded, you don't need to configure Lite Cable at all.

Otherwise, you must configure broadcast adapter manually:

LiteCable.broadcast_adapter = :any_cable

You can also do this via configuration, e.g., env var (LITECABLE_BROADCAST_ADAPTER=any_cable) or broadcast_adapter: any_cable in a YAML config.

At the AnyCable side, you must configure a connection factory:

AnyCable.connection_factory = MyApp::Connection

Then run AnyCable along with the app:

bundle exec anycable

# add -r option to load the app if it's not ./config/anycable.rb or ./config/environment.rb
bundle exec anycable -r ./my_app.rb

See Sinatra example for more.

Configuration

Lite Cable uses anyway_config for configuration.

See config for available options.

Unsupported features

  • Channel callbacks (after_subscribe, etc)

  • Stream callbacks (stream_from "xyz" { |msg| ... })

  • Periodical timers

  • Remote connections.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/palkan/litecable.

License

The gem is available as open source under the terms of the MIT License.

litecable's People

Contributors

depfu-bot avatar depfu[bot] avatar fizvlad avatar palkan avatar spilin avatar sponomarev avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

litecable's Issues

NoMethodError in LiteCable::Server::Base#each_frame

Tell us about your environment

Ruby version: ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [x86_64-linux]

litecable gem version: 0.8.1

#<Thread:0x00007f283405b870 /home/cyberarm/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/litecable-0.8.1/lib/lite_cable/server/client_socket/base.rb:69 run> terminated with exception (report_on_exception is true):
E, [2024-04-14T02:14:15.375827 #1066840] ERROR -- LiteCable: Socket send failed: Broken pipe
/home/cyberarm/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/litecable-0.8.1/lib/lite_cable/server/client_socket/base.rb:140:in `each_frame': undefined method `empty?' for nil (NoMethodError)

            break if data.empty?
                         ^^^^^^^
        from /home/cyberarm/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/litecable-0.8.1/lib/lite_cable/server/client_socket/base.rb:73:in `block in listen'

def each_frame
framebuffer = WebSocket::Frame::Incoming::Server.new(version: version)
while socket.wait_readable
data = socket.respond_to?(:recv) ? socket.recv(2000) : socket.readpartial(2000)
break if data.empty?
framebuffer << data
while frame = framebuffer.next # rubocop:disable Lint/AssignmentInCondition
case frame.type
when :close
return
when :text, :binary
yield frame.data
end
end
end
rescue Errno::EHOSTUNREACH, Errno::ETIMEDOUT, Errno::ECONNRESET, IOError, Errno::EBADF => e
log(:debug, "Socket frame error: #{e}")
nil # client disconnected or timed out
end

Why is the only way to pass params into the channel is via the identifier?

So, after wondering why passing params in via the URL as a URL query or as an extra value alongside the identifier didn't seam to work, I dove into the liteCable code and found this:

     def add(identifier, subscribe = true)
        raise AlreadySubscribedError if find(identifier)

        params = connection.coder.decode(identifier)

        channel_id = params.delete("channel")

        channel_class = Channel::Registry.find!(channel_id)

        subscriptions[identifier] = channel_class.new(connection, identifier, params)
        subscribe ? subscribe_channel(subscriptions[identifier]) : subscriptions[identifier]
      end

Am I understanding this wrong?
Why is it set-up like this?
I ask because it means I can't send params unless it's as an identdifier
on top of that I then I get what I sent back:

      # In the spec file
      it "will reject the incoming connection if the check isn't passed", focus: true do
        expect(client.read_message).to eq('type' => 'welcome')
        client.send_message command: 'subscribe',
                            identifier: {
                              channel: 'fake',
                              value_to_check: 6
                            }.to_json
        expect(client.read_message).to eq(
          'identifier' => { channel: 'fake' }.to_json,
          'type' => 'reject_subscription'
        )
      end

results in this:

Failure/Error:
       expect(client.read_message).to eq(
         'identifier' => { channel: 'fake' }.to_json,
         'type' => 'reject_subscription'
       )

       expected: {"identifier"=>"{\"channel\":\"fake\"}", "type"=>"reject_subscription"}
            got: {"identifier"=>"{\"channel\":\"fake\",\"value_to_check\":6}", "type"=>"reject_subscription"}

       (compared using ==)

       Diff:
       @@ -1,3 +1,3 @@
       -"identifier" => "{\"channel\":\"fake\"}",
       +"identifier" => "{\"channel\":\"fake\",\"value_to_check\":6}",
        "type" => "reject_subscription",

Why does it send back given params as part of the identifier in the return message?

EDIT:

Perhaps as a compromise: make the following changes (or cleaner if you can do it better):

module LiteCable
  module Connection
    # Manage the connection channels and route messages
    class Subscriptions
     def add(identifier, subscribe = true)
        raise AlreadySubscribedError if find(identifier)

        params = connection.coder.decode(identifier)

        channel_id = params.delete("channel")

        channel_class = Channel::Registry.find!(channel_id)

        subscriptions[identifier] = channel_class.new(connection, identifier, params.delete('params'))
        subscribe ? subscribe_channel(subscriptions[identifier]) : subscriptions[identifier]
      end

      def transmit_subscription_confirmation(identifier)
        ident = JSON.parse(identifier)
        ident.delete('params')
        connection.transmit identifier: ident.to_json,
                            type: LiteCable::INTERNAL[:message_types][:confirmation]
      end

      def transmit_subscription_rejection(identifier)
        ident = JSON.parse(identifier)
        ident.delete('params')
        connection.transmit identifier: ident.to_json,
                            type: LiteCable::INTERNAL[:message_types][:rejection]
      end

      def transmit_subscription_cancel(identifier)
        ident = JSON.parse(identifier)
        ident.delete('params')
        connection.transmit identifier: ident.to_json,
                            type: LiteCable::INTERNAL[:message_types][:cancel]
      end
    end
  end
end

so now the following spec will pass:

      it "will reject the incoming connection if the check isn't passed", focus: true do
        expect(client.read_message).to eq('type' => 'welcome')
        client.send_message command: 'subscribe',
                            identifier: {
                              channel: 'fake',
                              params: {
                                value_to_check: 6
                              }
                            }.to_json
        expect(client.read_message).to eq(
          'identifier' => { channel: 'fake' }.to_json,
          'type' => 'reject_subscription'
        )
      end

Note, I'm sending params explicitly and they aren't returned as part of the identifier

Errno::ECONNRESET in LiteCable::Server::ClientSocket::Base#close_socket

Tell us about your environment

Ruby version: ruby 3.2.0 (2022-12-25 revision a528908271) [x64-mingw-ucrt]

litecable gem version: 0.8.0

anycable gem version:

grpc gem version:

What did you do?

Let computer sleep overnight and woke it up.

What did you expect to happen?

Keep on running.

What actually happened?

Fatal socket error:

Errno::ECONNRESET
C:/Users/cyber/Rubies/Ruby32-x64/lib/ruby/gems/3.2.0/gems/litecable-0.8.0/lib/lite_cable/server/client_socket/base.rb:115:in `write': An existing connection was forcibly closed by the remote host. (Errno::ECONNRESET)
        from C:/Users/cyber/Rubies/Ruby32-x64/lib/ruby/gems/3.2.0/gems/litecable-0.8.0/lib/lite_cable/server/client_socket/base.rb:115:in `close_socket'
        from C:/Users/cyber/Rubies/Ruby32-x64/lib/ruby/gems/3.2.0/gems/litecable-0.8.0/lib/lite_cable/server/client_socket/base.rb:107:in `close!'
        from C:/Users/cyber/Rubies/Ruby32-x64/lib/ruby/gems/3.2.0/gems/litecable-0.8.0/lib/lite_cable/server/client_socket/base.rb:92:in `close'
        from C:/Users/cyber/Rubies/Ruby32-x64/lib/ruby/gems/3.2.0/gems/litecable-0.8.0/lib/lite_cable/server/client_socket/base.rb:83:in `block in listen'

def close_socket
frame = WebSocket::Frame::Outgoing::Server.new(version: version, type: :close, code: 1000)
@socket.write(frame.to_s) if frame.supported?
@socket.close
rescue IOError, Errno::EPIPE, Errno::ETIMEDOUT
# already closed
end

LiteCable::Server::ClientSocket::Base doesn't have a attr reader for env even though it's needed

I'm building some specs by using the same technique that is used in litecable. (spinning up a Puma server to run WS requests against) I'm now at the point where the server is responding.
The issue is that, doing request.params causes an error:

Failure/Error: @request ||= Rack::Request.new(socket.env)

     NoMethodError:
       undefined method `env' for #<LiteCable::Server::ClientSocket::Base:0x00007ffe4ff89238>
       Did you mean?  end

The only way to fix it is to monkey patch it in:

module LiteCable
  module Server
    module ClientSocket
      # Wrapper over web socket
      class Base
        def env
          @env
        end
      end
    end
  end
end

`bundle exec rake` and Sinatra example are failing on Ruby 3.1

Tell us about your environment

Ruby version: 3.1.2

litecable gem version: master branch

anycable gem version: -

grpc gem version: -

What did you do?

  1. bundle install; bundle exec rake
  2. Running example app

What did you expect to happen?

  1. Tests passing
  2. Key size error from Sinatra; ArgumentError at Chat::Channel#speak

What actually happened?

  1. Rubocop and RSpec failed
  2. Key size error from Sinatra; ArgumentError at Chat::Channel#speak

`Errno::ECONNRESET: Connection reset by peer` should get caught

Tell us about your environment

Ruby version:

2.7.0

litecable gem version:

0.7.0

anycable gem version:

grpc gem version:

What did you do?

Running lite cable with a lot of connection

What did you expect to happen?

Close the socket on receive Errno::ECONNRESET: Connection reset by peer here and do not let crash the whole application.

What actually happened?

Whole application crashed.

When joining a room with example: ArgumentError: wrong number of arguments (given 0, expected 1)

bundle exec puma

127.0.0.1 - - [03/Feb/2017:14:18:47 +0100] "POST /sign_in HTTP/1.1" 303 - 0.0009
127.0.0.1 - - [03/Feb/2017:14:18:47 +0100] "GET / HTTP/1.1" 200 631 0.0089
127.0.0.1 - - [03/Feb/2017:14:18:51 +0100] "POST /rooms HTTP/1.1" 303 - 0.0006
127.0.0.1 - - [03/Feb/2017:14:18:51 +0100] "GET /rooms/1 HTTP/1.1" 200 2367 0.0096
2017-02-03 14:18:51 +0100: Rack app error handling request { GET /cable }
#<ArgumentError: wrong number of arguments (given 0, expected 1)>
/Users/philipmannheimer/Documents/flewid/testing-environments/litecable/lib/lite_cable/server/websocket_ext/protocols.rb:7:in protocols' /Users/philipmannheimer/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/websocket-1.2.4/lib/websocket/handshake/handler/server04.rb:48:in protocol'
/Users/philipmannheimer/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/websocket-1.2.4/lib/websocket/handshake/handler/server04.rb:26:in handshake_keys' /Users/philipmannheimer/Documents/flewid/testing-environments/litecable/lib/lite_cable/server/websocket_ext/protocols.rb:27:in handshake_keys'
/Users/philipmannheimer/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/websocket-1.2.4/lib/websocket/handshake/handler/base.rb:13:in to_s' /Users/philipmannheimer/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/websocket-1.2.4/lib/websocket/handshake/base.rb:31:in to_s'
/Users/philipmannheimer/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/websocket-1.2.4/lib/websocket/exception_handler.rb:19:in block in rescue_method' /Users/philipmannheimer/Documents/flewid/testing-environments/litecable/lib/lite_cable/server/middleware.rb:37:in send_handshake'
/Users/philipmannheimer/Documents/flewid/testing-environments/litecable/lib/lite_cable/server/middleware.rb:21:in call' /Users/philipmannheimer/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/rack-1.6.5/lib/rack/urlmap.rb:66:in block in call'
/Users/philipmannheimer/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/rack-1.6.5/lib/rack/urlmap.rb:50:in each' /Users/philipmannheimer/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/rack-1.6.5/lib/rack/urlmap.rb:50:in call'
/Users/philipmannheimer/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/rack-1.6.5/lib/rack/builder.rb:153:in call' /Users/philipmannheimer/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/puma-3.7.0/lib/puma/configuration.rb:226:in call'
/Users/philipmannheimer/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/puma-3.7.0/lib/puma/server.rb:578:in handle_request' /Users/philipmannheimer/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/puma-3.7.0/lib/puma/server.rb:415:in process_client'
/Users/philipmannheimer/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/puma-3.7.0/lib/puma/server.rb:275:in block in run' /Users/philipmannheimer/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/puma-3.7.0/lib/puma/thread_pool.rb:120:in block in spawn_thread'

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.