imanel/websocket-rack

uninitialized constant Rack::WebSocket::Handler::Thin::WebSocketError

xaviershay opened this issue · 5 comments

Hello,
I'm trying to drive websocket push with a sinatra app and getting an unintialized constant error. The following script can replicate the problem.

# config.ru
require 'rack/websocket'
require 'sinatra/base'

class Pusher < Rack::WebSocket::Application
  def on_open(env)
    send_data "OPEN"
  end

  def push
    send_data "hello"
  end

  def push2
    EM.next_tick {
      send_data "hello"
    }
  end
end

class WebApp < Sinatra::Base
  def initialize(pusher)
    super()
    @pusher = pusher
  end

  get '/' do
    <<-HTML
<html>
<head>
<script src='http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js'></script>
<script>
  $(function() {
    var socket = new WebSocket('ws://localhost:3000/ws');
    socket.onmessage = function(msg) {
      alert(msg.data);
    }
  });
</script>
</head>
<body>
</body>
</html>
    HTML
  end

  get '/push' do
    @pusher.push
  end

  get '/push2' do
    @pusher.push
  end
end

pusher = Pusher.new
map '/ws' do
  run pusher
end

map '/' do
  run WebApp.new(pusher)
end
thin -R config.ru start &
curl http://localhost:3000/push # Error
curl http://localhost:3000/push2 # Error

The exception raised in both cases is:

NameError - uninitialized constant Rack::WebSocket::Handler::Thin::WebSocketError:
 websocket-rack-0.3.0/lib/rack/websocket/handler/thin.rb:22:in `send_data'
 websocket-rack-0.3.0/lib/rack/websocket/application.rb:36:in `method_missing'
 config.ru:11:in `push'

Is this the correct way to be architecting things? (Regardless, at the very least a better error should be thrown)

I will fix it later today(it should throw Rack::WebSocket::WebSocketError - will do in 0.3.1 ;)

About implementation - it should be implemented a little different:

# config.ru
require 'rack/websocket'
require 'sinatra/base'

class Pusher < Rack::WebSocket::Application
  def self.connections
    @connection ||= []
  end

  def on_open(env)
    self.class.connections << self
    send_data "OPEN"
  end

  def on_close(env)
    self.class.connections.delete(self)
  end

  def push
    send_data "hello"
  end

  def push2
    EM.next_tick {
      send_data "hello"
    }
  end
end

class WebApp < Sinatra::Base
  def initialize
    super()
  end

  get '/' do
    <<-HTML
<html>
<head>
<script src='http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js'></script>
<script>
  $(function() {
    var socket = new WebSocket('ws://localhost:3000/ws');
    socket.onmessage = function(msg) {
      alert(msg.data);
    }
  });
</script>
</head>
<body>
</body>
</html>
    HTML
  end

  get '/push' do
    Pusher.connections.each { |connection| connection.push }
  end

  get '/push2' do
    Pusher.connections.each { |connection| connection.push2 }
  end
end

map '/ws' do
  run Pusher.new # This should be initialized everytime request is called
end

map '/' do
  run WebApp.new
end

First of all - there is no direct connection between HTTP connection and WebSocket connection. You can identify them by providing some data(i.e. connecting to 'ws://localhost:3000/ws?uid=abc') and then sending message only to connection with specified uid. So you can't pass pusher instance to HTTP request.

Another one is that for each WS connection you should create new instance of Pusher - each of them will support only one request.

ok that makes sense and I have it working now. Thanks!

I would love to see an implementation on this lib that looks something like:

class Pusher < Rack::WebSocket::Application
  def on_open(env, socket)
    subscription = Subscription.new(:channel => env['PATH_INFO'])
    subscription.on_message { |m| socket.send_data m }
    socket.send_data 'Subscribing'
    subscription.subscribe
    socket.on_close { 
      socket.send 'Unsubscribing and closing connection'
      subscription.delete
    }
  end
end

This way an array or hash of connections doesn't need to be maintained.

the correct one should look like that:

class Pusher < Rack::WebSocket::Application
  def on_open(env, socket)
    @subscription = Subscription.new(:channel => env['PATH_INFO'])
    @subscription.on_message { |m| socket.send_data m }
    socket.send_data 'Subscribing'
    @subscription.subscribe
  end

  def on_close(env, socket)
    socket.send 'Unsubscribing and closing connection'
    @subscription.delete
  end
end

The only thing that will remain for you is to script Subscription model :) It could be good idea to script also Pusher#on_message to pass it to Subscription instance.

We ended up with something similar to that at https://github.com/polleverywhere/push/blob/backends/lib/push/transport/web_socket.rb, thanks for validating that approach! Also, thanks for integrating em-websockets into Rack.