diff --git a/.gitignore b/.gitignore index b242f894..610824a0 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ test/dummy/log/*.log test/dummy/tmp/ .rbx/ spec/javascripts/generated/ +tmp/rspec_guard_result diff --git a/.travis.yml b/.travis.yml index 19ad9cd6..145d559f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,3 +6,8 @@ before_script: - "sh -e /etc/init.d/xvfb start" services: - redis-server +notifications: + irc: + channels: + - "chat.freenode.net#websocket-rails" + on_success: change diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d59b1fd..eb1b9835 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # WebsocketRails Change Log +## UNRELEASED + +* Convert controller's `action_name` to a string to get AbstractController::Callbacks (`before_action`) working properly [fixes #150] + ## Version 0.6.2 September 8 2013 diff --git a/Gemfile.lock b/Gemfile.lock index 6efdf4e0..f2b06067 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,6 +8,7 @@ PATH rack rails redis + redis-objects thin GEM @@ -41,7 +42,7 @@ GEM multi_json (~> 1.3) thread_safe (~> 0.1) tzinfo (~> 0.3.37) - arel (4.0.0) + arel (4.0.1) atomic (1.1.10) atomic (1.1.10-java) builder (3.1.4) @@ -100,7 +101,7 @@ GEM mime-types (~> 1.16) treetop (~> 1.4.8) method_source (0.8.1) - mime-types (1.23) + mime-types (1.25) minitest (4.7.5) multi_json (1.7.7) polyglot (0.3.3) @@ -135,7 +136,9 @@ GEM ffi (>= 0.5.0) rb-kqueue (0.2.0) ffi (>= 0.5.0) - redis (3.0.4) + redis (3.0.5) + redis-objects (0.7.0) + redis (>= 3.0.2) ref (1.0.5) rspec (2.13.0) rspec-core (~> 2.13.0) @@ -172,7 +175,7 @@ GEM multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.0.0) + sprockets-rails (2.0.1) actionpack (>= 3.0) activesupport (>= 3.0) sprockets (~> 2.8) @@ -191,7 +194,7 @@ GEM thread_safe (0.1.0) atomic tilt (1.4.1) - treetop (1.4.14) + treetop (1.4.15) polyglot polyglot (>= 0.3.1) tzinfo (0.3.37) diff --git a/lib/assets/javascripts/websocket_rails/abstract_connection.js.coffee b/lib/assets/javascripts/websocket_rails/abstract_connection.js.coffee new file mode 100644 index 00000000..ca83f7c8 --- /dev/null +++ b/lib/assets/javascripts/websocket_rails/abstract_connection.js.coffee @@ -0,0 +1,45 @@ +### + Abstract Interface for the WebSocketRails client. +### +class WebSocketRails.AbstractConnection + + constructor: (url, @dispatcher) -> + @message_queue = [] + + close: -> + + trigger: (event) -> + if @dispatcher.state != 'connected' + @message_queue.push event + else + @send_event event + + send_event: (event) -> + # Events queued before connecting do not have the correct + # connection_id set yet. We need to update it before dispatching. + event.connection_id = @connection_id if @connection_id? + + # ... + + on_close: (event) -> + if @dispatcher && @dispatcher._conn == @ + close_event = new WebSocketRails.Event(['connection_closed', event]) + @dispatcher.state = 'disconnected' + @dispatcher.dispatch close_event + + on_error: (event) -> + if @dispatcher && @dispatcher._conn == @ + error_event = new WebSocketRails.Event(['connection_error', event]) + @dispatcher.state = 'disconnected' + @dispatcher.dispatch error_event + + on_message: (event_data) -> + if @dispatcher && @dispatcher._conn == @ + @dispatcher.new_message event_data + + setConnectionId: (@connection_id) -> + + flush_queue: -> + for event in @message_queue + @trigger event + @message_queue = [] diff --git a/lib/assets/javascripts/websocket_rails/channel.js.coffee b/lib/assets/javascripts/websocket_rails/channel.js.coffee index e4a9d07a..6347a115 100644 --- a/lib/assets/javascripts/websocket_rails/channel.js.coffee +++ b/lib/assets/javascripts/websocket_rails/channel.js.coffee @@ -9,41 +9,42 @@ For instance: ### class WebSocketRails.Channel - constructor: (@name,@_dispatcher,@is_private) -> + constructor: (@name, @_dispatcher, @is_private = false, @on_success, @on_failure) -> + @_callbacks = {} + @_token = undefined + @_queue = [] if @is_private event_name = 'websocket_rails.subscribe_private' else event_name = 'websocket_rails.subscribe' - event = new WebSocketRails.Event( [event_name, {data: {channel: @name}},@_dispatcher.connection_id], @_success_launcher, @_failure_launcher) + @connection_id = @_dispatcher._conn?.connection_id + event = new WebSocketRails.Event( [event_name, {data: {channel: @name}}, @connection_id], @_success_launcher, @_failure_launcher) @_dispatcher.trigger_event event - @_callbacks = {} - @_token = undefined - @_queue = [] - destroy: () => - event_name = 'websocket_rails.unsubscribe' - event = new WebSocketRails.Event( [event_name, {data: {channel: @name}}, @_dispatcher.connection_id] ) - @_dispatcher.trigger_event event + destroy: -> + if @connection_id == @_dispatcher._conn?.connection_id + event_name = 'websocket_rails.unsubscribe' + event = new WebSocketRails.Event( [event_name, {data: {channel: @name}}, @connection_id] ) + @_dispatcher.trigger_event event @_callbacks = {} - bind: (event_name, callback) => + bind: (event_name, callback) -> @_callbacks[event_name] ?= [] @_callbacks[event_name].push callback - trigger: (event_name, message) => - event = new WebSocketRails.Event( [event_name, {channel: @name, data: message, token: @_token}, @_dispatcher.connection_id] ) + trigger: (event_name, message) -> + event = new WebSocketRails.Event( [event_name, {channel: @name, data: message, token: @_token}, @connection_id] ) if !@_token @_queue.push event else @_dispatcher.trigger_event event - dispatch: (event_name, message) => + dispatch: (event_name, message) -> if event_name == 'websocket_rails.channel_token' + @connection_id = @_dispatcher._conn?.connection_id @_token = message['token'] - for event in @_queue - @_dispatcher.trigger_event event - @_queue = [] + @flush_queue() else return unless @_callbacks[event_name]? for callback in @_callbacks[event_name] @@ -56,3 +57,8 @@ class WebSocketRails.Channel # using this method because @on_failure will not be defined when the constructor is executed _failure_launcher: (data) => @on_failure(data) if @on_failure? + + flush_queue: -> + for event in @_queue + @_dispatcher.trigger_event event + @_queue = [] diff --git a/lib/assets/javascripts/websocket_rails/event.js.coffee b/lib/assets/javascripts/websocket_rails/event.js.coffee index 857ec91e..44a85d60 100644 --- a/lib/assets/javascripts/websocket_rails/event.js.coffee +++ b/lib/assets/javascripts/websocket_rails/event.js.coffee @@ -4,7 +4,7 @@ The Event object stores all the relevant event information. class WebSocketRails.Event - constructor: (data,@success_callback,@failure_callback) -> + constructor: (data, @success_callback, @failure_callback) -> @name = data[0] attr = data[1] if attr? @@ -17,26 +17,26 @@ class WebSocketRails.Event @result = true @success = attr.success - is_channel: => + is_channel: -> @channel? - is_result: => - @result == true + is_result: -> + typeof @result != 'undefined' - is_ping: => + is_ping: -> @name == 'websocket_rails.ping' - serialize: => + serialize: -> JSON.stringify [@name, @attributes()] - attributes: => + attributes: -> id: @id, channel: @channel, data: @data token: @token - run_callbacks: (success,data) => - if success == true - @success_callback?(data) + run_callbacks: (@success, @result) -> + if @success == true + @success_callback?(@result) else - @failure_callback?(data) + @failure_callback?(@result) diff --git a/lib/assets/javascripts/websocket_rails/http_connection.js.coffee b/lib/assets/javascripts/websocket_rails/http_connection.js.coffee index 433508d0..90847493 100644 --- a/lib/assets/javascripts/websocket_rails/http_connection.js.coffee +++ b/lib/assets/javascripts/websocket_rails/http_connection.js.coffee @@ -1,67 +1,66 @@ ### HTTP Interface for the WebSocketRails client. ### -class WebSocketRails.HttpConnection - httpFactories: -> [ +class WebSocketRails.HttpConnection extends WebSocketRails.AbstractConnection + connection_type: 'http' + + _httpFactories: -> [ + -> new XDomainRequest(), -> new XMLHttpRequest(), -> new ActiveXObject("Msxml2.XMLHTTP"), -> new ActiveXObject("Msxml3.XMLHTTP"), -> new ActiveXObject("Microsoft.XMLHTTP") ] - createXMLHttpObject: => - xmlhttp = false - factories = @httpFactories() - for factory in factories - try - xmlhttp = factory() - catch e - continue - break - xmlhttp - - constructor: (@url, @dispatcher) -> - @_url = @url - @_conn = @createXMLHttpObject() + constructor: (url, @dispatcher) -> + super + @_url = "http://#{url}" + @_conn = @_createXMLHttpObject() @last_pos = 0 - @message_queue = [] - @_conn.onreadystatechange = @parse_stream - @_conn.addEventListener("load", @connectionClosed, false) + try + @_conn.onreadystatechange = => @_parse_stream() + @_conn.addEventListener("load", @on_close, false) + catch e + @_conn.onprogress = => @_parse_stream() + @_conn.onload = @on_close + # set this as 3 always for parse_stream as the object does not have this property at all + @_conn.readyState = 3 @_conn.open "GET", @_url, true @_conn.send() - parse_stream: => - if @_conn.readyState == 3 - data = @_conn.responseText.substring @last_pos - @last_pos = @_conn.responseText.length - data = data.replace( /\]\]\[\[/g, "],[" ) - decoded_data = JSON.parse data - @dispatcher.new_message decoded_data + close: -> + @_conn.abort() - trigger: (event) => - if @dispatcher.state != 'connected' - @message_queue.push event - else - @post_data @dispatcher.connection_id, event.serialize() + send_event: (event) -> + super + @_post_data event.serialize() - post_data: (connection_id, payload) -> + _post_data: (payload) -> $.ajax @_url, type: 'POST' data: - client_id: connection_id + client_id: @connection_id data: payload success: -> - flush_queue: (connection_id) => - for event in @message_queue - # Events queued before connecting do not have the correct - # connection_id set yet. We need to update it before dispatching. - if connection_id? - event.connection_id = @dispatcher.connection_id - @trigger event - @message_queue = [] + _createXMLHttpObject: -> + xmlhttp = false + factories = @_httpFactories() + for factory in factories + try + xmlhttp = factory() + catch e + continue + break + xmlhttp - connectionClosed: (event) => - close_event = new WebSocketRails.Event(['connection_closed', event]) - @dispatcher.state = 'disconnected' - @dispatcher.dispatch close_event + _parse_stream: -> + if @_conn.readyState == 3 + data = @_conn.responseText.substring @last_pos + @last_pos = @_conn.responseText.length + data = data.replace( /\]\]\[\[/g, "],[" ) + try + event_data = JSON.parse data + @on_message(event_data) + catch e + # just ignore if it cannot be parsed, probably whitespace diff --git a/lib/assets/javascripts/websocket_rails/main.js b/lib/assets/javascripts/websocket_rails/main.js index 0a7d653a..d69bf9c4 100644 --- a/lib/assets/javascripts/websocket_rails/main.js +++ b/lib/assets/javascripts/websocket_rails/main.js @@ -1,5 +1,6 @@ //= require ./websocket_rails //= require ./event +//= require ./abstract_connection //= require ./http_connection //= require ./websocket_connection //= require ./channel diff --git a/lib/assets/javascripts/websocket_rails/websocket_connection.js.coffee b/lib/assets/javascripts/websocket_rails/websocket_connection.js.coffee index 4512cd75..34d5f3fa 100644 --- a/lib/assets/javascripts/websocket_rails/websocket_connection.js.coffee +++ b/lib/assets/javascripts/websocket_rails/websocket_connection.js.coffee @@ -1,43 +1,29 @@ ### WebSocket Interface for the WebSocketRails client. ### -class WebSocketRails.WebSocketConnection - - constructor: (@url,@dispatcher) -> +class WebSocketRails.WebSocketConnection extends WebSocketRails.AbstractConnection + connection_type: 'websocket' + + constructor: (@url, @dispatcher) -> + super if @url.match(/^wss?:\/\//) console.log "WARNING: Using connection urls with protocol specified is depricated" else if window.location.protocol == 'https:' @url = "wss://#{@url}" else @url = "ws://#{@url}" - - @message_queue = [] @_conn = new WebSocket(@url) - @_conn.onmessage = @on_message - @_conn.onclose = @on_close - @_conn.onerror = @on_error - - trigger: (event) => - if @dispatcher.state != 'connected' - @message_queue.push event - else - @_conn.send event.serialize() - - on_message: (event) => - data = JSON.parse event.data - @dispatcher.new_message data - - on_close: (event) => - close_event = new WebSocketRails.Event(['connection_closed', event]) - @dispatcher.state = 'disconnected' - @dispatcher.dispatch close_event + @_conn.onmessage = (event) => + event_data = JSON.parse event.data + @on_message(event_data) + @_conn.onclose = (event) => + @on_close(event) + @_conn.onerror = (event) => + @on_error(event) - on_error: (event) => - error_event = new WebSocketRails.Event(['connection_error', event]) - @dispatcher.state = 'disconnected' - @dispatcher.dispatch error_event + close: -> + @_conn.close() - flush_queue: => - for event in @message_queue - @_conn.send event.serialize() - @message_queue = [] + send_event: (event) -> + super + @_conn.send event.serialize() diff --git a/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee b/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee index b1af4e70..d5f6c516 100644 --- a/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee +++ b/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee @@ -18,24 +18,55 @@ Listening for new events from the server ### class @WebSocketRails constructor: (@url, @use_websockets = true) -> - @state = 'connecting' @callbacks = {} @channels = {} @queue = {} + @connect() + + connect: -> + @state = 'connecting' + unless @supports_websockets() and @use_websockets - @_conn = new WebSocketRails.HttpConnection url, @ + @_conn = new WebSocketRails.HttpConnection @url, @ else - @_conn = new WebSocketRails.WebSocketConnection url, @ + @_conn = new WebSocketRails.WebSocketConnection @url, @ @_conn.new_message = @new_message + disconnect: -> + if @_conn + @_conn.close() + delete @_conn._conn + delete @_conn + + @state = 'disconnected' + + # Reconnects the whole connection, + # keeping the messages queue and its' connected channels. + # + # After successfull connection, this will: + # - reconnect to all channels, that were active while disconnecting + # - resend all events from which we haven't received any response yet + reconnect: => + old_connection_id = @_conn?.connection_id + + @disconnect() + @connect() + + # Resend all unfinished events from the previous connection. + for id, event of @queue + if event.connection_id == old_connection_id && !event.is_result() + @trigger_event event + + @reconnect_channels() + new_message: (data) => for socket_message in data event = new WebSocketRails.Event( socket_message ) if event.is_result() @queue[event.id]?.run_callbacks(event.success, event.data) - @queue[event.id] = null + delete @queue[event.id] else if event.is_channel() @dispatch_channel event else if event.is_ping() @@ -48,8 +79,8 @@ class @WebSocketRails connection_established: (data) => @state = 'connected' - @connection_id = data.connection_id - @_conn.flush_queue data.connection_id + @_conn.setConnectionId(data.connection_id) + @_conn.flush_queue() if @on_open? @on_open(data) @@ -58,30 +89,30 @@ class @WebSocketRails @callbacks[event_name].push callback trigger: (event_name, data, success_callback, failure_callback) => - event = new WebSocketRails.Event( [event_name, data, @connection_id], success_callback, failure_callback ) - @queue[event.id] = event - @_conn.trigger event + event = new WebSocketRails.Event( [event_name, data, @_conn?.connection_id], success_callback, failure_callback ) + @trigger_event event trigger_event: (event) => @queue[event.id] ?= event # Prevent replacing an event that has callbacks stored - @_conn.trigger event + @_conn.trigger event if @_conn + event dispatch: (event) => return unless @callbacks[event.name]? for callback in @callbacks[event.name] callback event.data - subscribe: (channel_name) => + subscribe: (channel_name, success_callback, failure_callback) => unless @channels[channel_name]? - channel = new WebSocketRails.Channel channel_name, @ + channel = new WebSocketRails.Channel channel_name, @, false, success_callback, failure_callback @channels[channel_name] = channel channel else @channels[channel_name] - subscribe_private: (channel_name) => + subscribe_private: (channel_name, success_callback, failure_callback) => unless @channels[channel_name]? - channel = new WebSocketRails.Channel channel_name, @, true + channel = new WebSocketRails.Channel channel_name, @, true, success_callback, failure_callback @channels[channel_name] = channel channel else @@ -100,8 +131,22 @@ class @WebSocketRails (typeof(WebSocket) == "function" or typeof(WebSocket) == "object") pong: => - pong = new WebSocketRails.Event( ['websocket_rails.pong',{},@connection_id] ) + pong = new WebSocketRails.Event( ['websocket_rails.pong', {}, @_conn?.connection_id] ) @_conn.trigger pong connection_stale: => @state != 'connected' + + # Destroy and resubscribe to all existing @channels. + reconnect_channels: -> + for name, channel of @channels + callbacks = channel._callbacks + channel.destroy() + delete @channels[name] + + channel = if channel.is_private + @subscribe_private name + else + @subscribe name + channel._callbacks = callbacks + channel diff --git a/lib/generators/websocket_rails/install/templates/websocket_rails.rb b/lib/generators/websocket_rails/install/templates/websocket_rails.rb index 81d3dde2..1890b3fa 100644 --- a/lib/generators/websocket_rails/install/templates/websocket_rails.rb +++ b/lib/generators/websocket_rails/install/templates/websocket_rails.rb @@ -22,6 +22,9 @@ # * Requires Redis. config.synchronize = false + # Prevent Thin from daemonizing (default is true) + # config.daemonize = false + # Uncomment and edit to point to a different redis instance. # Will not be used unless standalone or synchronization mode # is enabled. @@ -52,4 +55,9 @@ # jobs using the WebsocketRails.users UserManager. # config.user_class = User + # Supporting HTTP streaming on Internet Explorer versions 8 & 9 + # requires CORS to be enabled for GET "/websocket" request. + # List here the origin domains allowed to perform the request. + # config.allowed_origins = ['http://localhost:3000'] + end diff --git a/lib/rails/config/routes.rb b/lib/rails/config/routes.rb index d49ea5a2..c9506810 100644 --- a/lib/rails/config/routes.rb +++ b/lib/rails/config/routes.rb @@ -1,6 +1,6 @@ Rails.application.routes.draw do if Rails.version >= '4.0.0' - get "/websocket", :to => WebsocketRails::ConnectionManager.new + match "/websocket", :to => WebsocketRails::ConnectionManager.new, via: [:get, :post] else match "/websocket", :to => WebsocketRails::ConnectionManager.new end diff --git a/lib/rails/tasks/websocket_rails.tasks b/lib/rails/tasks/websocket_rails.tasks index 64ad1490..3b226e92 100644 --- a/lib/rails/tasks/websocket_rails.tasks +++ b/lib/rails/tasks/websocket_rails.tasks @@ -9,8 +9,12 @@ namespace :websocket_rails do warn_if_standalone_not_enabled! - fork do - Thin::Controllers::Controller.new(options).start + if options[:daemonize] + fork do + Thin::Controllers::Controller.new(options).start + end + else + Thin::Controllers::Controller.new(options).start end puts "Websocket Rails Standalone Server listening on port #{options[:port]}" diff --git a/lib/websocket_rails/channel.rb b/lib/websocket_rails/channel.rb index 9c0250d2..c35d7c5a 100644 --- a/lib/websocket_rails/channel.rb +++ b/lib/websocket_rails/channel.rb @@ -5,13 +5,12 @@ class Channel delegate :config, :channel_tokens, :channel_manager, :to => WebsocketRails - attr_reader :name, :subscribers, :token + attr_reader :name, :subscribers def initialize(channel_name) @subscribers = [] @name = channel_name @private = false - @token = generate_unique_token end def subscribe(connection) @@ -39,7 +38,7 @@ def trigger(event_name,data={},options={}) end def trigger_event(event) - return if event.token != @token + return if event.token != token info "[#{name}] #{event.data.inspect}" send_data event end @@ -55,20 +54,24 @@ def is_private? @private end + def token + @token ||= channel_tokens[@name] ||= generate_unique_token + end + private def generate_unique_token begin - token = SecureRandom.urlsafe_base64 - end while channel_tokens.include?(token) + new_token = SecureRandom.uuid + end while channel_tokens.values.include?(new_token) - token + new_token end def send_token(connection) options = { :channel => @name, - :data => {:token => @token}, + :data => {:token => token}, :connection => connection } info 'sending token' diff --git a/lib/websocket_rails/channel_manager.rb b/lib/websocket_rails/channel_manager.rb index b924eedd..b8f5cbf1 100644 --- a/lib/websocket_rails/channel_manager.rb +++ b/lib/websocket_rails/channel_manager.rb @@ -1,3 +1,5 @@ +require 'redis-objects' + module WebsocketRails class << self @@ -18,11 +20,20 @@ def channel_tokens class ChannelManager - attr_reader :channels, :channel_tokens + attr_reader :channels def initialize @channels = {}.with_indifferent_access - @channel_tokens = [] + end + + def channel_tokens + @channel_tokens ||= begin + if WebsocketRails.synchronize? + ::Redis::HashKey.new('websocket_rails.channel_tokens', Synchronization.redis) + else + {} + end + end end def [](channel) diff --git a/lib/websocket_rails/configuration.rb b/lib/websocket_rails/configuration.rb index bff0b8e4..72608276 100644 --- a/lib/websocket_rails/configuration.rb +++ b/lib/websocket_rails/configuration.rb @@ -25,6 +25,15 @@ def keep_subscribers_when_private=(value) @keep_subscribers_when_private = value end + def allowed_origins + # allows the value to be string or array + [@allowed_origins].flatten.compact.uniq ||= [] + end + + def allowed_origins=(value) + @allowed_origins = value + end + def broadcast_subscriber_events? @broadcast_subscriber_events ||= false end @@ -81,6 +90,14 @@ def log_internal_events=(value) @log_internal_events = value end + def daemonize? + @daemonize.nil? ? true : @daemonize + end + + def daemonize=(value) + @daemonize = value + end + def synchronize @synchronize ||= false end @@ -133,7 +150,7 @@ def thin_defaults :tag => 'websocket_rails', :rackup => "#{Rails.root}/config.ru", :threaded => false, - :daemonize => true, + :daemonize => daemonize?, :dirname => Rails.root, :max_persistent_conns => 1024, :max_conns => 1024 diff --git a/lib/websocket_rails/connection_adapters/http.rb b/lib/websocket_rails/connection_adapters/http.rb index 5d471406..c47c3d6b 100644 --- a/lib/websocket_rails/connection_adapters/http.rb +++ b/lib/websocket_rails/connection_adapters/http.rb @@ -21,6 +21,13 @@ def initialize(env,dispatcher) define_deferrable_callbacks + origin = "#{request.protocol}#{request.raw_host_with_port}" + @headers.merge!({'Access-Control-Allow-Origin' => origin}) if WebsocketRails.config.allowed_origins.include?(origin) + # IE < 10.0 hack + # XDomainRequest will not bubble up notifications of download progress in the first 2kb of the response + # http://blogs.msdn.com/b/ieinternals/archive/2010/04/06/comet-streaming-in-internet-explorer-with-xmlhttprequest-and-xdomainrequest.aspx + @body.chunk(encode_chunk(" " * 2048)) + EM.next_tick do @env['async.callback'].call [200, @headers, @body] on_open diff --git a/lib/websocket_rails/connection_manager.rb b/lib/websocket_rails/connection_manager.rb index f3fd6af6..9c8f4df8 100644 --- a/lib/websocket_rails/connection_manager.rb +++ b/lib/websocket_rails/connection_manager.rb @@ -63,7 +63,7 @@ def call(env) private def parse_incoming_event(params) - connection = find_connection_by_id(params["client_id"].to_i) + connection = find_connection_by_id(params["client_id"]) connection.on_message params["data"] SuccessfulResponse end diff --git a/lib/websocket_rails/controller_factory.rb b/lib/websocket_rails/controller_factory.rb index e76aff20..eadf0d8c 100644 --- a/lib/websocket_rails/controller_factory.rb +++ b/lib/websocket_rails/controller_factory.rb @@ -47,7 +47,7 @@ def set_controller_store(controller) end def set_action_name(controller, method) - set_ivar :@_action_name, controller, method + set_ivar :@_action_name, controller, method.to_s end def set_ivar(ivar, object, value) diff --git a/lib/websocket_rails/event.rb b/lib/websocket_rails/event.rb index eac40844..c33b627a 100644 --- a/lib/websocket_rails/event.rb +++ b/lib/websocket_rails/event.rb @@ -108,7 +108,7 @@ def initialize(event_name, options={}) end @id = options[:id] @data = options[:data].is_a?(Hash) ? options[:data].with_indifferent_access : options[:data] - @channel = options[:channel].to_sym if options[:channel] + @channel = options[:channel].to_sym rescue options[:channel].to_s.to_sym if options[:channel] @token = options[:token] if options[:token] @connection = options[:connection] @server_token = options[:server_token] @@ -126,6 +126,7 @@ def as_json :data => data, :success => success, :result => result, + :token => token, :server_token => server_token } ] diff --git a/lib/websocket_rails/synchronization.rb b/lib/websocket_rails/synchronization.rb index 475b194c..2409f50c 100644 --- a/lib/websocket_rails/synchronization.rb +++ b/lib/websocket_rails/synchronization.rb @@ -33,6 +33,10 @@ def self.shutdown! singleton.shutdown! end + def self.redis + singleton.redis + end + def self.singleton @singleton ||= new end @@ -40,7 +44,10 @@ def self.singleton include Logging def redis - @redis ||= Redis.new(WebsocketRails.config.redis_options) + @redis ||= begin + redis_options = WebsocketRails.config.redis_options + EM.reactor_running? ? Redis.new(redis_options) : ruby_redis + end end def ruby_redis @@ -52,9 +59,8 @@ def ruby_redis def publish(event) Fiber.new do - redis_client = EM.reactor_running? ? redis : ruby_redis event.server_token = server_token - redis_client.publish "websocket_rails.events", event.serialize + redis.publish "websocket_rails.events", event.serialize end.resume end @@ -157,16 +163,14 @@ def destroy_user(identifier) def find_user(identifier) Fiber.new do - redis_client = EM.reactor_running? ? redis : ruby_redis - raw_user = redis_client.hget('websocket_rails.users', identifier) + raw_user = redis.hget('websocket_rails.users', identifier) raw_user ? JSON.parse(raw_user) : nil end.resume end def all_users Fiber.new do - redis_client = EM.reactor_running? ? redis : ruby_redis - redis_client.hgetall('websocket_rails.users') + redis.hgetall('websocket_rails.users') end.resume end diff --git a/spec/javascripts/support/jasmine.yml b/spec/javascripts/support/jasmine.yml index 5267a25e..8961601e 100644 --- a/spec/javascripts/support/jasmine.yml +++ b/spec/javascripts/support/jasmine.yml @@ -12,11 +12,19 @@ src_dir: spec/javascripts src_files: - support/vendor/sinon-1.7.1.js - generated/assets/websocket_rails.js - - generated/assets/*.js + - generated/assets/event.js + - generated/assets/abstract_connection.js + - generated/assets/http_connection.js + - generated/assets/websocket_connection.js + - generated/assets/channel.js + - generated/specs/helpers.js spec_dir: spec/javascripts/generated spec_files: - - specs/*_spec.js + - specs/event_spec.js + - specs/websocket_connection_spec.js + - specs/channel_spec.js + - specs/websocket_rails_spec.js # stylesheets # diff --git a/spec/javascripts/support/jasmine_config.rb b/spec/javascripts/support/jasmine_config.rb deleted file mode 100644 index 427a8c66..00000000 --- a/spec/javascripts/support/jasmine_config.rb +++ /dev/null @@ -1,63 +0,0 @@ -# spec/javascripts/support/jasmine_config.rb -# when jasmine starts the server out-of-process, it needs this in order to be able to invoke the asset tasks -unless Object.const_defined?(:Rake) - require 'rake' - load File.expand_path('../../../../Rakefile', __FILE__) -end - -require 'coffee_script' - -module Jasmine - class Config - include Rake::DSL - - def js_files(spec_filter = nil) - # remove all generated files - generated_files_directory = File.expand_path("../../../../spec/javascripts/generated/assets", __FILE__) - rm_rf generated_files_directory, :secure => true - - precompile_app_assets - compile_jasmine_javascripts - - # this is code from the original jasmine config js_files method - you could also just alias_method_chain it - spec_files_to_include = spec_filter.nil? ? spec_files : match_files(spec_dir, [spec_filter]) - src_files.collect {|f| "/" + f } + helpers.collect {|f| File.join(spec_path, f) } + spec_files_to_include.collect {|f| File.join(spec_path, f) } - end - - private - - # this method compiles all the same javascript files your app will - def precompile_app_assets - puts "Precompiling assets..." - - root = File.expand_path("../../../../lib/assets/javascripts/websocket_rails", __FILE__) - destination_dir = File.expand_path("../../../../spec/javascripts/generated/assets", __FILE__) - - glob = File.expand_path("**/*.js.coffee", root) - - Dir.glob(glob).each do |srcfile| - srcfile = Pathname.new(srcfile) - destfile = srcfile.sub(root, destination_dir).sub(".coffee", "") - FileUtils.mkdir_p(destfile.dirname) - File.open(destfile, "w") {|f| f.write(CoffeeScript.compile(File.new(srcfile)))} - end - end - - # this method compiles all of the spec files into js files that jasmine can run - def compile_jasmine_javascripts - puts "Compiling jasmine coffee scripts into javascript..." - root = File.expand_path("../../../../spec/javascripts/websocket_rails", __FILE__) - destination_dir = File.expand_path("../../generated/specs", __FILE__) - - glob = File.expand_path("**/*.coffee", root) - - Dir.glob(glob).each do |srcfile| - srcfile = Pathname.new(srcfile) - destfile = srcfile.sub(root, destination_dir).sub(".coffee", ".js") - FileUtils.mkdir_p(destfile.dirname) - File.open(destfile, "w") {|f| f.write(CoffeeScript.compile(File.new(srcfile)))} - end - end - - end -end diff --git a/spec/javascripts/support/jasmine_helper.rb b/spec/javascripts/support/jasmine_helper.rb new file mode 100644 index 00000000..0a90638e --- /dev/null +++ b/spec/javascripts/support/jasmine_helper.rb @@ -0,0 +1,38 @@ +#Use this file to set/override Jasmine configuration options +#You can remove it if you don't need it. +#This file is loaded *after* jasmine.yml is interpreted. +# +#Example: using a different boot file. +#Jasmine.configure do |config| +# config.boot_dir = '/absolute/path/to/boot_dir' +# config.boot_files = lambda { ['/absolute/path/to/boot_dir/file.js'] } +#end +# +require 'coffee-script' + +puts "Precompiling assets..." + +root = File.expand_path("../../../../lib/assets/javascripts/websocket_rails", __FILE__) +destination_dir = File.expand_path("../../../../spec/javascripts/generated/assets", __FILE__) + +glob = File.expand_path("**/*.js.coffee", root) + +Dir.glob(glob).each do |srcfile| + srcfile = Pathname.new(srcfile) + destfile = srcfile.sub(root, destination_dir).sub(".coffee", "") + FileUtils.mkdir_p(destfile.dirname) + File.open(destfile, "w") {|f| f.write(CoffeeScript.compile(File.new(srcfile)))} +end +puts "Compiling jasmine coffee scripts into javascript..." +root = File.expand_path("../../../../spec/javascripts/websocket_rails", __FILE__) +destination_dir = File.expand_path("../../generated/specs", __FILE__) + +glob = File.expand_path("**/*.coffee", root) + +Dir.glob(glob).each do |srcfile| + srcfile = Pathname.new(srcfile) + destfile = srcfile.sub(root, destination_dir).sub(".coffee", ".js") + FileUtils.mkdir_p(destfile.dirname) + File.open(destfile, "w") {|f| f.write(CoffeeScript.compile(File.new(srcfile)))} +end + diff --git a/spec/javascripts/websocket_rails/channel_spec.coffee b/spec/javascripts/websocket_rails/channel_spec.coffee index b1b2adbf..3482be07 100644 --- a/spec/javascripts/websocket_rails/channel_spec.coffee +++ b/spec/javascripts/websocket_rails/channel_spec.coffee @@ -1,17 +1,25 @@ describe 'WebSocketRails.Channel:', -> beforeEach -> - @dispatcher = + @dispatcher = new class WebSocketRailsStub new_message: -> true dispatch: -> true trigger_event: (event) -> true state: 'connected' - connection_id: 12345 - @channel = new WebSocketRails.Channel('public',@dispatcher) + _conn: + connection_id: 12345 + @channel = new WebSocketRails.Channel('public', @dispatcher) sinon.spy @dispatcher, 'trigger_event' afterEach -> @dispatcher.trigger_event.restore() + describe '.bind', -> + it 'should add a function to the callbacks collection', -> + test_func = -> + @channel.bind 'event_name', test_func + expect(@channel._callbacks['event_name'].length).toBe 1 + expect(@channel._callbacks['event_name']).toContain test_func + describe '.trigger', -> describe 'before the channel token is set', -> it 'queues the events', -> @@ -26,9 +34,31 @@ describe 'WebSocketRails.Channel:', -> @channel.trigger 'someEvent', 'someData' expect(@dispatcher.trigger_event.calledWith(['someEvent',{token: 'valid token', data: 'someData'}])) + describe '.destroy', -> + it 'should destroy all callbacks', -> + event_callback = -> true + @channel.bind('new_message', @event_callback) + + @channel.destroy() + + expect(@channel._callbacks).toEqual {} + + describe 'when this channel\'s connection is still active', -> + it 'should send unsubscribe event', -> + @channel.destroy() + expect(@dispatcher.trigger_event.args[0][0].name).toEqual 'websocket_rails.unsubscribe' + + describe 'when this channel\'s connection is no more active', -> + beforeEach -> + @dispatcher._conn.connection_id++ + + it 'should not send unsubscribe event', -> + @channel.destroy() + expect(@dispatcher.trigger_event.notCalled).toEqual true + describe 'public channels', -> beforeEach -> - @channel = new WebSocketRails.Channel('forchan',@dispatcher,false) + @channel = new WebSocketRails.Channel('forchan', @dispatcher, false) @event = @dispatcher.trigger_event.lastCall.args[0] it 'should trigger an event containing the channel name', -> @@ -44,32 +74,32 @@ describe 'WebSocketRails.Channel:', -> it 'should be public', -> expect(@channel.is_private).toBeFalsy - describe '.bind', -> - it 'should add a function to the callbacks collection', -> - test_func = -> - @channel.bind 'event_name', test_func - expect(@channel._callbacks['event_name'].length).toBe 1 - expect(@channel._callbacks['event_name']).toContain test_func - describe 'channel tokens', -> it 'should set token when event_name is websocket_rails.channel_token', -> @channel.dispatch('websocket_rails.channel_token', {token: 'abc123'}) expect(@channel._token).toEqual 'abc123' + + it "should refresh channel's connection_id after channel_token has been received", -> + # this is needed in case we would init the channel connection + # just before the connection has been established + @channel.connection_id = null + @channel.dispatch('websocket_rails.channel_token', {token: 'abc123'}) + expect(@channel.connection_id).toEqual @dispatcher._conn.connection_id + it 'should flush the event queue after setting token', -> @channel.trigger 'someEvent', 'someData' @channel.dispatch('websocket_rails.channel_token', {token: 'abc123'}) expect(@channel._queue.length).toEqual(0) - describe 'private channels', -> beforeEach -> - @channel = new WebSocketRails.Channel('forchan',@dispatcher,true) + @channel = new WebSocketRails.Channel('forchan', @dispatcher, true) @event = @dispatcher.trigger_event.lastCall.args[0] it 'should trigger a subscribe_private event when created', -> expect(@event.name).toEqual 'websocket_rails.subscribe_private' it 'should be private', -> - expect(@channel.is_private).toBe true + expect(@channel.is_private).toBeTruthy diff --git a/spec/javascripts/websocket_rails/event_spec.coffee b/spec/javascripts/websocket_rails/event_spec.coffee index 7f5dc8bb..d2a71c14 100644 --- a/spec/javascripts/websocket_rails/event_spec.coffee +++ b/spec/javascripts/websocket_rails/event_spec.coffee @@ -2,7 +2,7 @@ describe 'WebSocketRails.Event', -> describe 'standard events', -> beforeEach -> - @data = ['event',{data: { message: 'test'} },12345] + @data = ['event', {data: { message: 'test'} }, 12345] @event = new WebSocketRails.Event(@data) it 'should generate an ID', -> @@ -51,19 +51,19 @@ describe 'WebSocketRails.Event', -> data failure_func = (data) -> data - @data = ['event',{data: { message: 'test'} },12345] - @event = new WebSocketRails.Event(@data,success_func,failure_func) + @data = ['event', {data: { message: 'test'} }, 12345] + @event = new WebSocketRails.Event(@data, success_func, failure_func) describe 'when successful', -> it 'should run the success callback when passed true', -> - expect(@event.run_callbacks(true,'success')).toEqual 'success' + expect(@event.run_callbacks(true, 'success')).toEqual 'success' it 'should not run the failure callback', -> - expect(@event.run_callbacks(true,'success')).toBeUndefined + expect(@event.run_callbacks(true, 'success')).toBeUndefined describe 'when failure', -> it 'should run the failure callback when passed true', -> - expect(@event.run_callbacks(false,'failure')).toEqual 'failure' + expect(@event.run_callbacks(false, 'failure')).toEqual 'failure' it 'should not run the failure callback', -> - expect(@event.run_callbacks(false,'failure')).toBeUndefined + expect(@event.run_callbacks(false, 'failure')).toBeUndefined diff --git a/spec/javascripts/websocket_rails/helpers.coffee b/spec/javascripts/websocket_rails/helpers.coffee new file mode 100644 index 00000000..bea7b198 --- /dev/null +++ b/spec/javascripts/websocket_rails/helpers.coffee @@ -0,0 +1,6 @@ +window.helpers = + startConnection: (dispatcher, connection_id = 1) -> + message = + data: + connection_id: connection_id + dispatcher.new_message [['client_connected', message]] diff --git a/spec/javascripts/websocket_rails/websocket_connection_spec.coffee b/spec/javascripts/websocket_rails/websocket_connection_spec.coffee index 794b403c..4be9967b 100644 --- a/spec/javascripts/websocket_rails/websocket_connection_spec.coffee +++ b/spec/javascripts/websocket_rails/websocket_connection_spec.coffee @@ -1,23 +1,34 @@ describe 'WebsocketRails.WebSocketConnection:', -> + SAMPLE_EVENT_DATA = ['event','message'] + SAMPLE_EVENT = + data: JSON.stringify(SAMPLE_EVENT_DATA) + beforeEach -> - dispatcher = + @dispatcher = new_message: -> true dispatch: -> true state: 'connected' # Have to stub the WebSocket object due to Firefox error during jasmine:ci - window.WebSocket = (url) -> - @url = url - @send = -> true - @dispatcher = dispatcher - @connection = new WebSocketRails.WebSocketConnection('localhost:3000/websocket', dispatcher) + window.WebSocket = class WebSocketStub + constructor: (@url, @dispatcher) -> + send: -> true + close: -> @onclose(null) + @connection = new WebSocketRails.WebSocketConnection('localhost:3000/websocket', @dispatcher) + @dispatcher._conn = @connection describe 'constructor', -> - it 'should set the onmessage event on the WebSocket object to this.on_message', -> - expect(@connection._conn.onmessage).toEqual @connection.on_message + it 'should redirect onmessage events\' data from the WebSocket object to this.on_message', -> + mock_connection = sinon.mock @connection + mock_connection.expects('on_message').once().withArgs SAMPLE_EVENT_DATA + @connection._conn.onmessage(SAMPLE_EVENT) + mock_connection.verify() - it 'should set the onclose event on the WebSocket object to this.on_close', -> - expect(@connection._conn.onclose).toEqual @connection.on_close + it 'should redirect onclose events from the WebSocket object to this.on_close', -> + mock_connection = sinon.mock @connection + mock_connection.expects('on_close').once().withArgs SAMPLE_EVENT + @connection._conn.onclose(SAMPLE_EVENT) + mock_connection.verify() describe 'with ssl', -> it 'should not add the ws:// prefix to the URL', -> @@ -28,6 +39,11 @@ describe 'WebsocketRails.WebSocketConnection:', -> it 'should add the ws:// prefix to the URL', -> expect(@connection.url).toEqual 'ws://localhost:3000/websocket' + describe '.close', -> + it 'should close the connection', -> + @connection.close() + expect(@dispatcher.state).toEqual 'disconnected' + describe '.trigger', -> describe 'before the connection has been fully established', -> @@ -51,14 +67,13 @@ describe 'WebsocketRails.WebSocketConnection:', -> describe '.on_message', -> it 'should decode the message and pass it to the dispatcher', -> - encoded_data = JSON.stringify ['event','message'] - event = - data: encoded_data mock_dispatcher = sinon.mock @connection.dispatcher - mock_dispatcher.expects('new_message').once().withArgs JSON.parse encoded_data - @connection.on_message event + mock_dispatcher.expects('new_message').once().withArgs SAMPLE_EVENT_DATA + @connection.on_message SAMPLE_EVENT_DATA mock_dispatcher.verify() + + describe '.on_close', -> it 'should dispatch the connection_closed event and pass the original event', -> event = new WebSocketRails.Event ['event','message'] @@ -100,6 +115,29 @@ describe 'WebsocketRails.WebSocketConnection:', -> expect(@dispatcher.state).toEqual('disconnected') + describe "it's no longer active connection", -> + beforeEach -> + @new_connection = new WebSocketRails.WebSocketConnection('localhost:3000/websocket', @dispatcher) + @dispatcher._conn = @new_connection + + it ".on_error should not react to the event response", -> + mock_dispatcher = sinon.mock @connection.dispatcher + mock_dispatcher.expects('dispatch').never() + @connection.on_error SAMPLE_EVENT_DATA + mock_dispatcher.verify() + + it ".on_close should not react to the event response", -> + mock_dispatcher = sinon.mock @connection.dispatcher + mock_dispatcher.expects('dispatch').never() + @connection.on_close SAMPLE_EVENT_DATA + mock_dispatcher.verify() + + it ".on_message should not react to the event response", -> + mock_dispatcher = sinon.mock @connection.dispatcher + mock_dispatcher.expects('new_message').never() + @connection.on_message SAMPLE_EVENT_DATA + mock_dispatcher.verify() + describe '.flush_queue', -> beforeEach -> @event = new WebSocketRails.Event ['event','message'] diff --git a/spec/javascripts/websocket_rails/websocket_rails_spec.coffee b/spec/javascripts/websocket_rails/websocket_rails_spec.coffee index 06b90c07..ea61326c 100644 --- a/spec/javascripts/websocket_rails/websocket_rails_spec.coffee +++ b/spec/javascripts/websocket_rails/websocket_rails_spec.coffee @@ -1,15 +1,17 @@ describe 'WebSocketRails:', -> beforeEach -> @url = 'localhost:3000/websocket' - WebSocketRails.WebSocketConnection = -> + WebSocketRails.WebSocketConnection = class WebSocketConnectionStub extends WebSocketRails.AbstractConnection connection_type: 'websocket' - flush_queue: -> true - WebSocketRails.HttpConnection = -> + WebSocketRails.HttpConnection = class HttpConnectionStub extends WebSocketRails.AbstractConnection connection_type: 'http' - flush_queue: -> true @dispatcher = new WebSocketRails @url describe 'constructor', -> + it 'should start connection automatically', -> + expect(@dispatcher.state).toEqual 'connecting' + + describe '.connect', -> it 'should set the new_message method on connection to this.new_message', -> expect(@dispatcher._conn.new_message).toEqual @dispatcher.new_message @@ -22,7 +24,7 @@ describe 'WebSocketRails:', -> dispatcher = new WebSocketRails @url, true expect(dispatcher._conn.connection_type).toEqual 'websocket' - describe 'when use_webosckets is false', -> + describe 'when use_websockets is false', -> it 'should use the Http Connection', -> dispatcher = new WebSocketRails @url, false expect(dispatcher._conn.connection_type).toEqual 'http' @@ -33,39 +35,110 @@ describe 'WebSocketRails:', -> dispatcher = new WebSocketRails @url, true expect(dispatcher._conn.connection_type).toEqual 'http' + describe '.disconnect', -> + beforeEach -> + @dispatcher.disconnect() + + it 'should close the connection', -> + expect(@dispatcher.state).toEqual 'disconnected' + + it 'existing connection should be destroyed', -> + expect(@dispatcher._conn).toEqual null + + describe '.reconnect', -> + OLD_CONNECTION_ID = 1 + NEW_CONNECTION_ID = 2 + + it 'should connect, when disconnected', -> + mock_dispatcher = sinon.mock @dispatcher + mock_dispatcher.expects('connect').once() + @dispatcher.disconnect() + @dispatcher.reconnect() + mock_dispatcher.verify() + + it 'should recreate the connection', -> + helpers.startConnection(@dispatcher, OLD_CONNECTION_ID) + @dispatcher.reconnect() + helpers.startConnection(@dispatcher, NEW_CONNECTION_ID) + + expect(@dispatcher._conn.connection_id).toEqual NEW_CONNECTION_ID + + it 'should resend all uncompleted events', -> + event = @dispatcher.trigger('create_post') + + helpers.startConnection(@dispatcher, OLD_CONNECTION_ID) + @dispatcher.reconnect() + helpers.startConnection(@dispatcher, NEW_CONNECTION_ID) + + expect(@dispatcher.queue[event.id].connection_id).toEqual NEW_CONNECTION_ID + + it 'should not resend completed events', -> + event = @dispatcher.trigger('create_post') + event.run_callbacks(true, {}) + + helpers.startConnection(@dispatcher, OLD_CONNECTION_ID) + @dispatcher.reconnect() + helpers.startConnection(@dispatcher, NEW_CONNECTION_ID) + + expect(@dispatcher.queue[event.id].connection_id).toEqual OLD_CONNECTION_ID + + it 'should reconnect to all channels', -> + mock_dispatcher = sinon.mock @dispatcher + mock_dispatcher.expects('reconnect_channels').once() + @dispatcher.reconnect() + mock_dispatcher.verify() + + describe '.reconnect_channels', -> + beforeEach -> + @channel_callback = -> true + helpers.startConnection(@dispatcher, 1) + @dispatcher.subscribe('public 4chan') + @dispatcher.subscribe_private('private 4chan') + @dispatcher.channels['public 4chan'].bind('new_post', @channel_callback) + + it 'should recreate existing channels, keeping their private/public type', -> + @dispatcher.reconnect_channels() + expect(@dispatcher.channels['public 4chan'].is_private).toEqual false + expect(@dispatcher.channels['private 4chan'].is_private).toEqual true + + it 'should move all existing callbacks from old channel objects to new ones', -> + old_public_channel = @dispatcher.channels['public 4chan'] + + @dispatcher.reconnect_channels() + + expect(old_public_channel._callbacks).toEqual {} + expect(@dispatcher.channels['public 4chan']._callbacks).toEqual {new_post: [@channel_callback]} + describe '.new_message', -> describe 'when this.state is "connecting"', -> beforeEach -> - @message = - data: - connection_id: 123 - @data = [['client_connected', @message]] + @connection_id = 123 it 'should call this.connection_established on the "client_connected" event', -> mock_dispatcher = sinon.mock @dispatcher - mock_dispatcher.expects('connection_established').once().withArgs @message.data - @dispatcher.new_message @data + mock_dispatcher.expects('connection_established').once().withArgs(connection_id: @connection_id) + helpers.startConnection(@dispatcher, @connection_id) mock_dispatcher.verify() it 'should set the state to connected', -> - @dispatcher.new_message @data + helpers.startConnection(@dispatcher, @connection_id) expect(@dispatcher.state).toEqual 'connected' it 'should flush any messages queued before the connection was established', -> mock_con = sinon.mock @dispatcher._conn mock_con.expects('flush_queue').once() - @dispatcher.new_message @data + helpers.startConnection(@dispatcher, @connection_id) mock_con.verify() it 'should set the correct connection_id', -> - @dispatcher.new_message @data - expect(@dispatcher.connection_id).toEqual 123 + helpers.startConnection(@dispatcher, @connection_id) + expect(@dispatcher._conn.connection_id).toEqual 123 it 'should call the user defined on_open callback', -> spy = sinon.spy() @dispatcher.on_open = spy - @dispatcher.new_message @data + helpers.startConnection(@dispatcher, @connection_id) expect(spy.calledOnce).toEqual true describe 'after the connection has been established', -> @@ -96,12 +169,17 @@ describe 'WebSocketRails:', -> @event = { run_callbacks: (data) -> } @event_mock = sinon.mock @event @dispatcher.queue[1] = @event + @event_data = [['event',@attributes]] it 'should run callbacks for result events', -> - data = [['event',@attributes]] - @event_mock.expects('run_callbacks').once() - @dispatcher.new_message data - @event_mock.verify() + @event_mock.expects('run_callbacks').once() + @dispatcher.new_message @event_data + @event_mock.verify() + + it 'should remove the event from the queue', -> + @dispatcher.new_message @event_data + expect(@dispatcher.queue[1]).toBeUndefined() + describe '.bind', -> @@ -121,18 +199,23 @@ describe 'WebSocketRails:', -> describe 'triggering events with', -> beforeEach -> - @dispatcher.connection_id = 123 @dispatcher._conn = + connection_id: 123 trigger: -> - trigger_channel: -> describe '.trigger', -> + it 'should add the event to the queue', -> + event = @dispatcher.trigger 'event', 'message' + expect(@dispatcher.queue[event.id]).toEqual event it 'should delegate to the connection object', -> - con_trigger = sinon.spy @dispatcher._conn, 'trigger' + conn_trigger = sinon.spy @dispatcher._conn, 'trigger' + @dispatcher.trigger 'event', 'message' + expect(conn_trigger.called).toEqual true + + it "should not delegate to the connection object, if it's not available", -> + @dispatcher._conn = null @dispatcher.trigger 'event', 'message' - event = new WebSocketRails.Event ['websocket_rails.subscribe', {channel: 'awesome'}, 123] - expect(con_trigger.called).toEqual true describe '.connection_stale', -> describe 'when state is connected', -> diff --git a/spec/unit/base_controller_spec.rb b/spec/unit/base_controller_spec.rb index 0cee4d1b..717e31d3 100644 --- a/spec/unit/base_controller_spec.rb +++ b/spec/unit/base_controller_spec.rb @@ -29,5 +29,46 @@ class TestClass; end; end end + describe "before actions" do + class BeforeActionController < WebsocketRails::BaseController + before_action { self.before_array << :all } + before_action(:only => :only) { self.before_array << :only_1 } + before_action(:only => :except) { self.before_array << :only_2 } + before_action(:only => [:main, :only]) { self.before_array << :only_3 } + before_action(:only => [:except, :only]) { self.before_array << :only_4 } + before_action(:except => :except) { self.before_array << :except_1 } + before_action(:except => :only) { self.before_array << :except_2 } + before_action(:except => [:main, :except]){ self.before_array << :except_3 } + before_action(:except => [:only, :except]){ self.before_array << :except_4 } + + attr_accessor :before_array + + def initialize + @before_array = [] + end + def main;end + def only;end + def except;end + end + + let(:controller) { BeforeActionController.new } + it 'should handle before_action with no args' do + controller.instance_variable_set :@_action_name, 'main' + controller.process_action(:main, nil) + controller.before_array.should == [:all, :only_3, :except_1, :except_2, :except_4] + end + + it 'should handle before_action with :only flag' do + controller.instance_variable_set :@_action_name, 'only' + controller.process_action(:only, nil) + controller.before_array.should == [:all, :only_1, :only_3, :only_4, :except_1, :except_3] + end + + it 'should handle before_action with :except flag' do + controller.instance_variable_set :@_action_name, 'except' + controller.process_action(:except, nil) + controller.before_array.should == [:all, :only_2, :only_4, :except_2] + end + end end end diff --git a/spec/unit/channel_manager_spec.rb b/spec/unit/channel_manager_spec.rb index 50f87ccc..7bf91713 100644 --- a/spec/unit/channel_manager_spec.rb +++ b/spec/unit/channel_manager_spec.rb @@ -15,8 +15,29 @@ module WebsocketRails end end + describe ".channel_tokens" do + it "should delegate to channel manager" do + ChannelManager.any_instance.should_receive(:channel_tokens) + WebsocketRails.channel_tokens + end + end + describe ChannelManager do + describe "#channel_tokens" do + it "should return a Hash-like" do + subject.channel_tokens.respond_to? :[] + subject.channel_tokens.respond_to? :has_key? + end + + it 'is used to store Channel\'s token' do + ChannelManager.any_instance.should_receive(:channel_tokens) + .at_least(:twice).and_call_original + token = Channel.new(:my_new_test_channel).token + WebsocketRails.channel_tokens[:my_new_test_channel].should == token + end + end + describe "#[]" do context "accessing a channel" do it "should create the channel if it does not exist" do diff --git a/spec/unit/channel_spec.rb b/spec/unit/channel_spec.rb index 093a5750..488bc767 100644 --- a/spec/unit/channel_spec.rb +++ b/spec/unit/channel_spec.rb @@ -128,6 +128,21 @@ module WebsocketRails subject.is_private?.should_not be_true end end + + describe "#token" do + it 'is long enough' do + subject.token.length.should > 10 + end + + it 'remains the same between two call' do + subject.token.should == subject.token + end + + it 'is the same for two channels with the same name' do + subject.token.should == Channel.new(subject.name).token + end + end + end end end diff --git a/spec/unit/connection_adapters/http_spec.rb b/spec/unit/connection_adapters/http_spec.rb index f8b5b5fd..d8199821 100644 --- a/spec/unit/connection_adapters/http_spec.rb +++ b/spec/unit/connection_adapters/http_spec.rb @@ -3,9 +3,14 @@ module WebsocketRails module ConnectionAdapters describe Http do - - subject { Http.new( mock_request, double('Dispatcher').as_null_object ) } - + + subject { + mr = mock_request + mr.stub(:protocol).and_return('http://') + mr.stub(:raw_host_with_port).and_return('localhost:3000') + Http.new(mr , double('Dispatcher').as_null_object ) + } + it "should be a subclass of ConnectionAdapters::Base" do subject.class.superclass.should == ConnectionAdapters::Base end @@ -18,6 +23,22 @@ module ConnectionAdapters subject.headers['Transfer-Encoding'].should == "chunked" end + it "should not set the Access-Control-Allow-Origin header" do + subject.headers['Access-Control-Allow-Origin'].should be_blank + end + + context "with IE CORS hack enabled" do + it "should set the Access-Control-Allow-Origin when passed an array as configuration" do + WebsocketRails.config.allowed_origins = ['http://localhost:3000'] + subject.headers['Access-Control-Allow-Origin'].should == 'http://localhost:3000' + end + + it "should set the Access-Control-Allow-Origin when passed a string as configuration" do + WebsocketRails.config.allowed_origins = 'http://localhost:3000' + subject.headers['Access-Control-Allow-Origin'].should == 'http://localhost:3000' + end + end + context "#encode_chunk" do it "should properly encode strings" do subject.__send__(:encode_chunk,"test").should == "4\r\ntest\r\n" diff --git a/spec/unit/connection_adapters_spec.rb b/spec/unit/connection_adapters_spec.rb index faa95039..4b2cec9d 100644 --- a/spec/unit/connection_adapters_spec.rb +++ b/spec/unit/connection_adapters_spec.rb @@ -192,8 +192,8 @@ module ConnectionAdapters it "should serialize all events into one array" do serialized_array = <<-EOF.strip_heredoc - [["queued_event",{"id":null,"channel":null,"user_id":null,"data":"test","success":null,"result":null,"server_token":null}], - ["queued_event",{"id":null,"channel":null,"user_id":null,"data":"test","success":null,"result":null,"server_token":null}]] + [["queued_event",{"id":null,"channel":null,"user_id":null,"data":"test","success":null,"result":null,"token":null,"server_token":null}], + ["queued_event",{"id":null,"channel":null,"user_id":null,"data":"test","success":null,"result":null,"token":null,"server_token":null}]] EOF subject.should_receive(:send).with(serialized_array.gsub(/\n/,'').strip) diff --git a/spec/unit/connection_manager_spec.rb b/spec/unit/connection_manager_spec.rb index 050b98b3..4472dedf 100644 --- a/spec/unit/connection_manager_spec.rb +++ b/spec/unit/connection_manager_spec.rb @@ -72,7 +72,7 @@ def open_connection context "new POST event" do before(:each) do @mock_http = ConnectionAdapters::Http.new(mock_request, dispatcher) - @mock_http.id = 1 + @mock_http.id = 'is_i_as_string' app.connections[@mock_http.id] = @mock_http end diff --git a/spec/unit/event_spec.rb b/spec/unit/event_spec.rb index 10988f36..3b2af809 100644 --- a/spec/unit/event_spec.rb +++ b/spec/unit/event_spec.rb @@ -6,7 +6,7 @@ module WebsocketRails let(:encoded_message_string) { '["new_message",{"id":"1234","data":"this is a message"}]' } let(:namespace_encoded_message_string) { '["product.new_message",{"id":"1234","data":"this is a message"}]' } let(:namespace_encoded_message) { '["product.new_message",{"id":"1234","data":{"message":"this is a message"}}]' } - let(:channel_encoded_message_string) { '["new_message",{"id":"1234","channel":"awesome_channel","user_id":null,"data":"this is a message","success":null,"result":null,"server_token":"1234"}]' } + let(:channel_encoded_message_string) { '["new_message",{"id":"1234","channel":"awesome_channel","user_id":null,"data":"this is a message","success":null,"result":null,"token":null,"server_token":"1234"}]' } let(:synchronizable_encoded_message) { '["new_message",{"id":"1234","data":{"message":"this is a message"},"server_token":"1234"}]' } let(:connection) { double('connection') } @@ -87,6 +87,12 @@ module WebsocketRails event.channel.should == :awesome_channel event.name.should == :event end + + it "should not raise an error if the channel name cannot be symbolized" do + expect { Event.new "event", :data => {}, :connection => connection, :channel => 5 }.to_not raise_error(NoMethodError) + event = Event.new "event", :data => {}, :connection => connection, :channel => 5 + event.channel.should == :"5" + end end describe "#is_channel?" do @@ -151,6 +157,16 @@ module WebsocketRails data[1]['server_token'].should == '1234' end end + + describe "#as_json" do + it "returns a Hash representation of the Event" do + hash = { data: { 'test' => 'test'}, channel: :awesome_channel } + event = Event.new 'test', hash + event.as_json[0].should == :test + event.as_json[1][:data].should == hash[:data] + event.as_json[1][:channel].should == hash[:channel] + end + end end end diff --git a/spec/unit/logging_spec.rb b/spec/unit/logging_spec.rb index a302267c..bc86ecb2 100644 --- a/spec/unit/logging_spec.rb +++ b/spec/unit/logging_spec.rb @@ -94,7 +94,7 @@ class LoggedClass object.should_receive(:log_exception).with(exception) expect { object.log_event(@event) { raise exception } - }.to raise_exception(exception) + }.to raise_exception(exception.class, exception.message) end end end diff --git a/websocket-rails.gemspec b/websocket-rails.gemspec index 6c7e2468..ba5581a3 100644 --- a/websocket-rails.gemspec +++ b/websocket-rails.gemspec @@ -25,6 +25,7 @@ Gem::Specification.new do |s| s.add_dependency "redis" s.add_dependency "hiredis" s.add_dependency "em-synchrony" + s.add_dependency "redis-objects" s.add_development_dependency "rake" s.add_development_dependency "rspec-rails" s.add_development_dependency 'rspec-matchers-matchers'