From f7b8f2e797783ca4472ab219feebb5dbeb4e109f Mon Sep 17 00:00:00 2001 From: Rory Low Date: Wed, 11 Sep 2013 16:33:10 -0600 Subject: [PATCH 01/27] added config.daemonize --- .../install/templates/websocket_rails.rb | 3 +++ lib/rails/tasks/websocket_rails.tasks | 8 ++++++-- lib/websocket_rails/configuration.rb | 10 +++++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/generators/websocket_rails/install/templates/websocket_rails.rb b/lib/generators/websocket_rails/install/templates/websocket_rails.rb index 6029bccb..0a5341da 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. 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/configuration.rb b/lib/websocket_rails/configuration.rb index 74ddf1f2..5ffb91ed 100644 --- a/lib/websocket_rails/configuration.rb +++ b/lib/websocket_rails/configuration.rb @@ -81,6 +81,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 +141,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 From 39aa9456681d46e1390381627da9728934fee2ad Mon Sep 17 00:00:00 2001 From: Rory Low Date: Mon, 30 Sep 2013 13:32:57 -0600 Subject: [PATCH 02/27] precompile coffee script before running jasmine tests --- spec/javascripts/support/jasmine_config.rb | 63 ---------------------- spec/javascripts/support/jasmine_helper.rb | 38 +++++++++++++ 2 files changed, 38 insertions(+), 63 deletions(-) delete mode 100644 spec/javascripts/support/jasmine_config.rb create mode 100644 spec/javascripts/support/jasmine_helper.rb 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 + From 46fee2e953b98f20af7d9b939cb45c8c474da9a3 Mon Sep 17 00:00:00 2001 From: Julien 'Lta' BALLET Date: Sun, 27 Oct 2013 18:30:30 +0100 Subject: [PATCH 03/27] Fix Rails 4 routes support, http polling POST route was disabled --- lib/rails/config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 58288461693656e6144117fee8d92eef828135e4 Mon Sep 17 00:00:00 2001 From: Julien 'Lta' BALLET Date: Sun, 27 Oct 2013 18:30:49 +0100 Subject: [PATCH 04/27] client_id is not an integer --- lib/websocket_rails/connection_manager.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From e0a1b2b65ba460fd21db939a0b5fc970f69a2b5a Mon Sep 17 00:00:00 2001 From: Julien 'Lta' BALLET Date: Sun, 27 Oct 2013 18:37:06 +0100 Subject: [PATCH 05/27] Adds http to coffescript http_connection adapter --- .../javascripts/websocket_rails/http_connection.js.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/assets/javascripts/websocket_rails/http_connection.js.coffee b/lib/assets/javascripts/websocket_rails/http_connection.js.coffee index 433508d0..54fd569b 100644 --- a/lib/assets/javascripts/websocket_rails/http_connection.js.coffee +++ b/lib/assets/javascripts/websocket_rails/http_connection.js.coffee @@ -20,8 +20,8 @@ class WebSocketRails.HttpConnection break xmlhttp - constructor: (@url, @dispatcher) -> - @_url = @url + constructor: (url, @dispatcher) -> + @_url = "http://#{url}" @_conn = @createXMLHttpObject() @last_pos = 0 @message_queue = [] From 3d309e9e1daff193980b0d86cd6f3ccc1d694ed7 Mon Sep 17 00:00:00 2001 From: Julien 'Lta' BALLET Date: Sun, 27 Oct 2013 22:31:26 +0100 Subject: [PATCH 06/27] Fix connection_manager_spec. connection_id is a string --- spec/unit/connection_manager_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From a7562746fde6585373cac9004ae4765e335f05a2 Mon Sep 17 00:00:00 2001 From: Julien 'Lta' BALLET Date: Sun, 27 Oct 2013 23:43:16 +0100 Subject: [PATCH 07/27] Adds a dependency on Redis-Objects. Fix Channel manager's channel token synchronization --- Gemfile.lock | 13 ++++++++----- lib/websocket_rails/channel.rb | 9 ++++++--- lib/websocket_rails/channel_manager.rb | 15 +++++++++++++-- lib/websocket_rails/synchronization.rb | 18 +++++++++++------- websocket-rails.gemspec | 1 + 5 files changed, 39 insertions(+), 17 deletions(-) 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/websocket_rails/channel.rb b/lib/websocket_rails/channel.rb index 9c0250d2..d7742ad5 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) @@ -55,12 +54,16 @@ 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) + end while channel_tokens.values.include?(token) token end 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/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/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' From 6dd770a962d0aa2f4f9cd0de4c82fb214135ff7f Mon Sep 17 00:00:00 2001 From: Julien 'Lta' BALLET Date: Mon, 28 Oct 2013 00:09:30 +0100 Subject: [PATCH 08/27] Channel, force the use of the #token accesor --- lib/websocket_rails/channel.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/websocket_rails/channel.rb b/lib/websocket_rails/channel.rb index d7742ad5..725f80ce 100644 --- a/lib/websocket_rails/channel.rb +++ b/lib/websocket_rails/channel.rb @@ -38,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 @@ -62,16 +62,16 @@ def token def generate_unique_token begin - token = SecureRandom.urlsafe_base64 - end while channel_tokens.values.include?(token) + new_token = SecureRandom.urlsafe_base64 + 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' From 3a355c6d398ab7fa4613aa7d8e0a7ce9364d5bfb Mon Sep 17 00:00:00 2001 From: Julien 'Lta' BALLET Date: Mon, 28 Oct 2013 00:32:12 +0100 Subject: [PATCH 09/27] Event: serialize event token to json, don't serialize empty fields --- lib/websocket_rails/event.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/websocket_rails/event.rb b/lib/websocket_rails/event.rb index eac40844..5fa0c1ba 100644 --- a/lib/websocket_rails/event.rb +++ b/lib/websocket_rails/event.rb @@ -126,8 +126,9 @@ def as_json :data => data, :success => success, :result => result, + :token => token :server_token => server_token - } + }.keep_if { |k, value| v.nil? } ] end From 8d1c009c1d92c9895b0fde46d75a170164ab6d61 Mon Sep 17 00:00:00 2001 From: Julien 'Lta' BALLET Date: Mon, 28 Oct 2013 01:19:52 +0100 Subject: [PATCH 10/27] Don't delete nil fields --- lib/websocket_rails/event.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/websocket_rails/event.rb b/lib/websocket_rails/event.rb index 5fa0c1ba..6841ee9e 100644 --- a/lib/websocket_rails/event.rb +++ b/lib/websocket_rails/event.rb @@ -126,9 +126,9 @@ def as_json :data => data, :success => success, :result => result, - :token => token + :token => token, :server_token => server_token - }.keep_if { |k, value| v.nil? } + } ] end From 2add67cf7ef7cf1616f6fda3259b57124da58fab Mon Sep 17 00:00:00 2001 From: Julien 'Lta' BALLET Date: Mon, 28 Oct 2013 01:22:07 +0100 Subject: [PATCH 11/27] Channel#token : use UUID instead of base64 --- lib/websocket_rails/channel.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/websocket_rails/channel.rb b/lib/websocket_rails/channel.rb index 725f80ce..c35d7c5a 100644 --- a/lib/websocket_rails/channel.rb +++ b/lib/websocket_rails/channel.rb @@ -62,7 +62,7 @@ def token def generate_unique_token begin - new_token = SecureRandom.urlsafe_base64 + new_token = SecureRandom.uuid end while channel_tokens.values.include?(new_token) new_token From 5fa77446322cdaca4ad39f03a11686e5eb7aa986 Mon Sep 17 00:00:00 2001 From: Julien 'Lta' BALLET Date: Mon, 28 Oct 2013 01:22:23 +0100 Subject: [PATCH 12/27] Adds specs for this PR. That's gangsta ! --- spec/unit/channel_manager_spec.rb | 22 ++++++++++++++++++++++ spec/unit/channel_spec.rb | 15 +++++++++++++++ spec/unit/connection_adapters_spec.rb | 4 ++-- spec/unit/event_spec.rb | 12 +++++++++++- 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/spec/unit/channel_manager_spec.rb b/spec/unit/channel_manager_spec.rb index 50f87ccc..8ccc37a1 100644 --- a/spec/unit/channel_manager_spec.rb +++ b/spec/unit/channel_manager_spec.rb @@ -15,8 +15,30 @@ 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 + puts "my token is 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_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/event_spec.rb b/spec/unit/event_spec.rb index 10988f36..79bf0c79 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') } @@ -151,6 +151,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 From ea6ba47de642af949d36107af605f5d5252b2313 Mon Sep 17 00:00:00 2001 From: Julien 'Lta' BALLET Date: Mon, 28 Oct 2013 01:39:56 +0100 Subject: [PATCH 13/27] Removes test 'puts' --- spec/unit/channel_manager_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/unit/channel_manager_spec.rb b/spec/unit/channel_manager_spec.rb index 8ccc37a1..7bf91713 100644 --- a/spec/unit/channel_manager_spec.rb +++ b/spec/unit/channel_manager_spec.rb @@ -34,7 +34,6 @@ module WebsocketRails ChannelManager.any_instance.should_receive(:channel_tokens) .at_least(:twice).and_call_original token = Channel.new(:my_new_test_channel).token - puts "my token is token" WebsocketRails.channel_tokens[:my_new_test_channel].should == token end end From 388368e9dd10b09f7bb6fbbd7195e08cade568ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=83=20pitr?= Date: Fri, 22 Nov 2013 16:46:53 -0500 Subject: [PATCH 14/27] Convert controller's `action_name` to a string to get AbstractController::Callbacks (`before_action`) working properly [fixes #150] --- CHANGELOG.md | 4 +++ lib/websocket_rails/controller_factory.rb | 2 +- spec/unit/base_controller_spec.rb | 41 +++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) 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/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/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 From 40538024f3ba662e1b543ecd6be260b7c02266b6 Mon Sep 17 00:00:00 2001 From: Rory Low Date: Sun, 24 Nov 2013 15:12:17 -0700 Subject: [PATCH 15/27] specify order the javascripts load --- spec/javascripts/support/jasmine.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/spec/javascripts/support/jasmine.yml b/spec/javascripts/support/jasmine.yml index 5267a25e..8047c647 100644 --- a/spec/javascripts/support/jasmine.yml +++ b/spec/javascripts/support/jasmine.yml @@ -12,11 +12,17 @@ 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/http_connection.js + - generated/assets/websocket_connection.js + - generated/assets/channel.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 # From c6cdbbde89335fe396e5f31f62c6197553f58974 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sat, 30 Nov 2013 22:50:52 +0100 Subject: [PATCH 16/27] Bugfix in unworking spec --- spec/unit/logging_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From eca31fb70fcddb06109a9840cbd249549d9915ed Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Sun, 1 Dec 2013 03:00:20 +0100 Subject: [PATCH 17/27] Refactor *.coffee files. Add reconnect() method. 1) Don't use fat arrows, if it's not needed. Please, try to avoid it (see discussions at google.com: "avoid fat arrows in coffescript"). 2) Extract common methods and code from Websocket and HttpConnection to AbstractConnection adapter (much cleaner + DRY). 3) Move `.connection_id` from WebSocketRails dispatcher to WebSocketRails.AbstractConnection. `.connection_id` is still also bound to events and channels. 4) Add `.close()` method to AbstractConnection. 5) Add `.connect()`, `.disconnect()` and `.reconnect()` method to WebSocketRails. 6) Refactor some specs. --- .gitignore | 1 + .../abstract_connection.js.coffee | 44 +++++++ .../websocket_rails/channel.js.coffee | 29 +++-- .../websocket_rails/event.js.coffee | 22 ++-- .../websocket_rails/http_connection.js.coffee | 75 ++++++------ .../javascripts/websocket_rails/main.js | 1 + .../websocket_connection.js.coffee | 48 +++----- .../websocket_rails/websocket_rails.js.coffee | 62 ++++++++-- spec/javascripts/support/jasmine.yml | 1 + .../websocket_rails/channel_spec.coffee | 48 ++++++-- .../websocket_rails/event_spec.coffee | 14 +-- .../websocket_connection_spec.coffee | 37 ++++-- .../websocket_rails_spec.coffee | 107 +++++++++++++++--- 13 files changed, 335 insertions(+), 154 deletions(-) create mode 100644 lib/assets/javascripts/websocket_rails/abstract_connection.js.coffee 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/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..4c76ef16 --- /dev/null +++ b/lib/assets/javascripts/websocket_rails/abstract_connection.js.coffee @@ -0,0 +1,44 @@ +### + 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) -> + @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..2b18c252 100644 --- a/lib/assets/javascripts/websocket_rails/channel.js.coffee +++ b/lib/assets/javascripts/websocket_rails/channel.js.coffee @@ -9,36 +9,41 @@ For instance: ### class WebSocketRails.Channel - constructor: (@name,@_dispatcher,@is_private) -> + constructor: (@name, @_dispatcher, @is_private) -> 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 + is_public: -> + !@is_private + + 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' @_token = message['token'] for event in @_queue @@ -50,9 +55,9 @@ class WebSocketRails.Channel callback message # using this method because @on_success will not be defined when the constructor is executed - _success_launcher: (data) => + _success_launcher: (data) -> @on_success(data) if @on_success? # using this method because @on_failure will not be defined when the constructor is executed - _failure_launcher: (data) => + _failure_launcher: (data) -> @on_failure(data) if @on_failure? 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 54fd569b..3c17be24 100644 --- a/lib/assets/javascripts/websocket_rails/http_connection.js.coffee +++ b/lib/assets/javascripts/websocket_rails/http_connection.js.coffee @@ -1,67 +1,56 @@ ### HTTP Interface for the WebSocketRails client. ### -class WebSocketRails.HttpConnection - httpFactories: -> [ +class WebSocketRails.HttpConnection extends WebSocketRails.AbstractConnection + connection_type: 'http' + + _httpFactories: -> [ -> 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) -> + super @_url = "http://#{url}" - @_conn = @createXMLHttpObject() + @_conn = @_createXMLHttpObject() @last_pos = 0 - @message_queue = [] - @_conn.onreadystatechange = @parse_stream - @_conn.addEventListener("load", @connectionClosed, false) + @_conn.onreadystatechange = => @_parse_stream() + @_conn.addEventListener("load", @on_close, false) @_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, "],[" ) + event_data = JSON.parse data + @on_message(event_data) 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..101fbe19 100644 --- a/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee +++ b/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee @@ -18,18 +18,49 @@ 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(false) + new_message: (data) => for socket_message in data event = new WebSocketRails.Event( socket_message ) @@ -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,13 +89,13 @@ 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 + event dispatch: (event) => return unless @callbacks[event.name]? @@ -100,8 +131,21 @@ 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' + + 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/spec/javascripts/support/jasmine.yml b/spec/javascripts/support/jasmine.yml index 8047c647..5b9606a7 100644 --- a/spec/javascripts/support/jasmine.yml +++ b/spec/javascripts/support/jasmine.yml @@ -13,6 +13,7 @@ src_files: - support/vendor/sinon-1.7.1.js - generated/assets/websocket_rails.js - generated/assets/event.js + - generated/assets/abstract_connection.js - generated/assets/http_connection.js - generated/assets/websocket_connection.js - generated/assets/channel.js diff --git a/spec/javascripts/websocket_rails/channel_spec.coffee b/spec/javascripts/websocket_rails/channel_spec.coffee index b1b2adbf..4e3301d8 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,23 +74,16 @@ 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 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) @@ -71,5 +94,6 @@ describe 'WebSocketRails.Channel:', -> it 'should be private', -> expect(@channel.is_private).toBe true + expect(@channel.is_public()).toEqual false 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/websocket_connection_spec.coffee b/spec/javascripts/websocket_rails/websocket_connection_spec.coffee index 794b403c..04c4c173 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 = 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 + window.WebSocket = class WebSocketStub + constructor: (@url, @dispatcher) -> + send: -> true + close: => @onclose(null) @dispatcher = dispatcher @connection = new WebSocketRails.WebSocketConnection('localhost:3000/websocket', dispatcher) 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,12 +67,9 @@ 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', -> diff --git a/spec/javascripts/websocket_rails/websocket_rails_spec.coffee b/spec/javascripts/websocket_rails/websocket_rails_spec.coffee index 06b90c07..29f83301 100644 --- a/spec/javascripts/websocket_rails/websocket_rails_spec.coffee +++ b/spec/javascripts/websocket_rails/websocket_rails_spec.coffee @@ -1,15 +1,24 @@ describe 'WebSocketRails:', -> + helpers = + startConnection: (dispatcher, connection_id) -> + message = + data: + connection_id: connection_id + dispatcher.new_message [['client_connected', message]] + 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 +31,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 +42,103 @@ 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 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_public()).toEqual true + expect(@dispatcher.channels['private 4chan'].is_public()).toEqual false + + 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', -> @@ -121,8 +194,8 @@ describe 'WebSocketRails:', -> describe 'triggering events with', -> beforeEach -> - @dispatcher.connection_id = 123 @dispatcher._conn = + connection_id: 123 trigger: -> trigger_channel: -> From 9eaff940b45a52ec6a211a8328ef69c79a5e61a5 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Mon, 2 Dec 2013 01:50:28 +0100 Subject: [PATCH 18/27] Fix jasmine specs --- spec/javascripts/support/jasmine.yml | 1 + spec/javascripts/websocket_rails/helpers.coffee | 6 ++++++ .../websocket_rails/websocket_connection_spec.coffee | 4 +++- .../websocket_rails/websocket_rails_spec.coffee | 7 ------- 4 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 spec/javascripts/websocket_rails/helpers.coffee diff --git a/spec/javascripts/support/jasmine.yml b/spec/javascripts/support/jasmine.yml index 5b9606a7..8961601e 100644 --- a/spec/javascripts/support/jasmine.yml +++ b/spec/javascripts/support/jasmine.yml @@ -17,6 +17,7 @@ src_files: - 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: 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 04c4c173..a07b46f9 100644 --- a/spec/javascripts/websocket_rails/websocket_connection_spec.coffee +++ b/spec/javascripts/websocket_rails/websocket_connection_spec.coffee @@ -12,9 +12,11 @@ describe 'WebsocketRails.WebSocketConnection:', -> window.WebSocket = class WebSocketStub constructor: (@url, @dispatcher) -> send: -> true - close: => @onclose(null) + close: -> @onclose(null) @dispatcher = dispatcher @connection = new WebSocketRails.WebSocketConnection('localhost:3000/websocket', dispatcher) + + @dispatcher._conn = @connection describe 'constructor', -> diff --git a/spec/javascripts/websocket_rails/websocket_rails_spec.coffee b/spec/javascripts/websocket_rails/websocket_rails_spec.coffee index 29f83301..3b93f93c 100644 --- a/spec/javascripts/websocket_rails/websocket_rails_spec.coffee +++ b/spec/javascripts/websocket_rails/websocket_rails_spec.coffee @@ -1,11 +1,4 @@ describe 'WebSocketRails:', -> - helpers = - startConnection: (dispatcher, connection_id) -> - message = - data: - connection_id: connection_id - dispatcher.new_message [['client_connected', message]] - beforeEach -> @url = 'localhost:3000/websocket' WebSocketRails.WebSocketConnection = class WebSocketConnectionStub extends WebSocketRails.AbstractConnection From 44320dc8027617128d3623c912571a8d607969ce Mon Sep 17 00:00:00 2001 From: Kaz Walker Date: Thu, 5 Dec 2013 16:22:32 -0700 Subject: [PATCH 19/27] Rescue symbolizing of channel names. fixes websocket-rails/websocket-rails#166 --- lib/websocket_rails/event.rb | 2 +- spec/unit/event_spec.rb | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/websocket_rails/event.rb b/lib/websocket_rails/event.rb index 6841ee9e..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] diff --git a/spec/unit/event_spec.rb b/spec/unit/event_spec.rb index 79bf0c79..3b2af809 100644 --- a/spec/unit/event_spec.rb +++ b/spec/unit/event_spec.rb @@ -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 From 4112011f2cb87ae052034c1ea722ac512e32b011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=83=20pitr?= Date: Fri, 6 Dec 2013 16:14:56 -0500 Subject: [PATCH 20/27] log travis builds to irc --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 19ad9cd6..e9695e4e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,3 +6,5 @@ before_script: - "sh -e /etc/init.d/xvfb start" services: - redis-server +notifications: + irc: "chat.freenode.net#websocket-rails" From 35b2ce1b9fc2ec955a5b6f0dd4204879c5869273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=83=20pitr?= Date: Fri, 6 Dec 2013 16:32:29 -0500 Subject: [PATCH 21/27] only notify irc on failures, travis is too chatty --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e9695e4e..145d559f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,4 +7,7 @@ before_script: services: - redis-server notifications: - irc: "chat.freenode.net#websocket-rails" + irc: + channels: + - "chat.freenode.net#websocket-rails" + on_success: change From 9f48654a203db41408a285618b9686b808d76547 Mon Sep 17 00:00:00 2001 From: Lauri Kolmonen Date: Thu, 12 Dec 2013 13:06:36 +0200 Subject: [PATCH 22/27] Added a possibility to set channel success and failure callbacks on subscribe. --- lib/assets/javascripts/websocket_rails/channel.js.coffee | 6 +++--- .../javascripts/websocket_rails/websocket_rails.js.coffee | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/assets/javascripts/websocket_rails/channel.js.coffee b/lib/assets/javascripts/websocket_rails/channel.js.coffee index 2b18c252..21fea75a 100644 --- a/lib/assets/javascripts/websocket_rails/channel.js.coffee +++ b/lib/assets/javascripts/websocket_rails/channel.js.coffee @@ -9,7 +9,7 @@ For instance: ### class WebSocketRails.Channel - constructor: (@name, @_dispatcher, @is_private) -> + constructor: (@name, @_dispatcher, @is_private, @on_success, @on_failure) -> if @is_private event_name = 'websocket_rails.subscribe_private' else @@ -55,9 +55,9 @@ class WebSocketRails.Channel callback message # using this method because @on_success will not be defined when the constructor is executed - _success_launcher: (data) -> + _success_launcher: (data) => @on_success(data) if @on_success? # using this method because @on_failure will not be defined when the constructor is executed - _failure_launcher: (data) -> + _failure_launcher: (data) => @on_failure(data) if @on_failure? diff --git a/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee b/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee index 101fbe19..83aad04d 100644 --- a/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee +++ b/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee @@ -102,17 +102,17 @@ class @WebSocketRails 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 From f865d8d1aa2440e74f3f9a5094cd8257dde4a378 Mon Sep 17 00:00:00 2001 From: Lauri Kolmonen Date: Sun, 15 Dec 2013 14:31:18 +0200 Subject: [PATCH 23/27] Support HTTP streaming for Internet Explorer versions 8+ by using XDomainRequest --- .../websocket_rails/http_connection.js.coffee | 18 ++++++++++++++---- .../connection_adapters/http.rb | 7 ++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/assets/javascripts/websocket_rails/http_connection.js.coffee b/lib/assets/javascripts/websocket_rails/http_connection.js.coffee index 3c17be24..90847493 100644 --- a/lib/assets/javascripts/websocket_rails/http_connection.js.coffee +++ b/lib/assets/javascripts/websocket_rails/http_connection.js.coffee @@ -5,6 +5,7 @@ class WebSocketRails.HttpConnection extends WebSocketRails.AbstractConnection connection_type: 'http' _httpFactories: -> [ + -> new XDomainRequest(), -> new XMLHttpRequest(), -> new ActiveXObject("Msxml2.XMLHTTP"), -> new ActiveXObject("Msxml3.XMLHTTP"), @@ -16,8 +17,14 @@ class WebSocketRails.HttpConnection extends WebSocketRails.AbstractConnection @_url = "http://#{url}" @_conn = @_createXMLHttpObject() @last_pos = 0 - @_conn.onreadystatechange = => @_parse_stream() - @_conn.addEventListener("load", @on_close, 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() @@ -52,5 +59,8 @@ class WebSocketRails.HttpConnection extends WebSocketRails.AbstractConnection data = @_conn.responseText.substring @last_pos @last_pos = @_conn.responseText.length data = data.replace( /\]\]\[\[/g, "],[" ) - event_data = JSON.parse data - @on_message(event_data) + 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/websocket_rails/connection_adapters/http.rb b/lib/websocket_rails/connection_adapters/http.rb index 5d471406..d0786dde 100644 --- a/lib/websocket_rails/connection_adapters/http.rb +++ b/lib/websocket_rails/connection_adapters/http.rb @@ -17,10 +17,15 @@ def self.accepts?(env) def initialize(env,dispatcher) super @body = DeferrableBody.new - @headers = HttpHeaders + @headers = HttpHeaders.merge({'Access-Control-Allow-Origin' => "#{request.protocol}#{request.raw_host_with_port}"}) define_deferrable_callbacks + # 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 From 2e24332326320e272c9b40900c36331ab45dfaac Mon Sep 17 00:00:00 2001 From: Lauri Kolmonen Date: Mon, 16 Dec 2013 15:36:51 +0200 Subject: [PATCH 24/27] As @KazW pointed out, we should use more secure way of setting Access-Control-Allow-Origin header --- .../install/templates/websocket_rails.rb | 5 ++++ lib/websocket_rails/configuration.rb | 9 +++++++ .../connection_adapters/http.rb | 16 +++++++---- spec/unit/connection_adapters/http_spec.rb | 27 ++++++++++++++++--- 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/lib/generators/websocket_rails/install/templates/websocket_rails.rb b/lib/generators/websocket_rails/install/templates/websocket_rails.rb index 274b5520..1890b3fa 100644 --- a/lib/generators/websocket_rails/install/templates/websocket_rails.rb +++ b/lib/generators/websocket_rails/install/templates/websocket_rails.rb @@ -55,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/websocket_rails/configuration.rb b/lib/websocket_rails/configuration.rb index f5d0e94f..a304033d 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 ||= [] + end + + def allowed_origins=(value) + @allowed_origins = value + end + def broadcast_subscriber_events? @broadcast_subscriber_events ||= false end diff --git a/lib/websocket_rails/connection_adapters/http.rb b/lib/websocket_rails/connection_adapters/http.rb index d0786dde..ffb271ac 100644 --- a/lib/websocket_rails/connection_adapters/http.rb +++ b/lib/websocket_rails/connection_adapters/http.rb @@ -17,14 +17,20 @@ def self.accepts?(env) def initialize(env,dispatcher) super @body = DeferrableBody.new - @headers = HttpHeaders.merge({'Access-Control-Allow-Origin' => "#{request.protocol}#{request.raw_host_with_port}"}) + @headers = HttpHeaders define_deferrable_callbacks - # 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)) + WebsocketRails.config.allowed_origins.each do |origin| + if origin.start_with?("#{request.protocol}#{request.raw_host_with_port}") + @headers.merge!({'Access-Control-Allow-Origin' => 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)) + break + end + end EM.next_tick do @env['async.callback'].call [200, @headers, @body] 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" From f13f38de44f7bce2274560b0580a66a1a50118b4 Mon Sep 17 00:00:00 2001 From: Lauri Kolmonen Date: Mon, 16 Dec 2013 21:52:02 +0200 Subject: [PATCH 25/27] Improved readability a bit, as suggested by @KazW --- lib/websocket_rails/configuration.rb | 2 +- lib/websocket_rails/connection_adapters/http.rb | 16 ++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/websocket_rails/configuration.rb b/lib/websocket_rails/configuration.rb index a304033d..72608276 100644 --- a/lib/websocket_rails/configuration.rb +++ b/lib/websocket_rails/configuration.rb @@ -27,7 +27,7 @@ def keep_subscribers_when_private=(value) def allowed_origins # allows the value to be string or array - [@allowed_origins].flatten.compact ||= [] + [@allowed_origins].flatten.compact.uniq ||= [] end def allowed_origins=(value) diff --git a/lib/websocket_rails/connection_adapters/http.rb b/lib/websocket_rails/connection_adapters/http.rb index ffb271ac..c47c3d6b 100644 --- a/lib/websocket_rails/connection_adapters/http.rb +++ b/lib/websocket_rails/connection_adapters/http.rb @@ -21,16 +21,12 @@ def initialize(env,dispatcher) define_deferrable_callbacks - WebsocketRails.config.allowed_origins.each do |origin| - if origin.start_with?("#{request.protocol}#{request.raw_host_with_port}") - @headers.merge!({'Access-Control-Allow-Origin' => 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)) - break - end - end + 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] From e03813db33c41ebd38c1e4c1d684bc68a612270b Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Wed, 4 Dec 2013 15:30:27 +0100 Subject: [PATCH 26/27] Small bugfixes in .js code --- .../abstract_connection.js.coffee | 3 +- .../websocket_rails/channel.js.coffee | 21 +++++++------ .../websocket_rails/websocket_rails.js.coffee | 4 +-- .../websocket_rails/channel_spec.coffee | 12 +++++-- .../websocket_connection_spec.coffee | 31 ++++++++++++++++--- .../websocket_rails_spec.coffee | 30 ++++++++++++------ 6 files changed, 71 insertions(+), 30 deletions(-) diff --git a/lib/assets/javascripts/websocket_rails/abstract_connection.js.coffee b/lib/assets/javascripts/websocket_rails/abstract_connection.js.coffee index 4c76ef16..ca83f7c8 100644 --- a/lib/assets/javascripts/websocket_rails/abstract_connection.js.coffee +++ b/lib/assets/javascripts/websocket_rails/abstract_connection.js.coffee @@ -34,7 +34,8 @@ class WebSocketRails.AbstractConnection @dispatcher.dispatch error_event on_message: (event_data) -> - @dispatcher.new_message event_data + if @dispatcher && @dispatcher._conn == @ + @dispatcher.new_message event_data setConnectionId: (@connection_id) -> diff --git a/lib/assets/javascripts/websocket_rails/channel.js.coffee b/lib/assets/javascripts/websocket_rails/channel.js.coffee index 21fea75a..6347a115 100644 --- a/lib/assets/javascripts/websocket_rails/channel.js.coffee +++ b/lib/assets/javascripts/websocket_rails/channel.js.coffee @@ -9,7 +9,10 @@ For instance: ### class WebSocketRails.Channel - constructor: (@name, @_dispatcher, @is_private, @on_success, @on_failure) -> + constructor: (@name, @_dispatcher, @is_private = false, @on_success, @on_failure) -> + @_callbacks = {} + @_token = undefined + @_queue = [] if @is_private event_name = 'websocket_rails.subscribe_private' else @@ -18,12 +21,6 @@ class WebSocketRails.Channel @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 = [] - - is_public: -> - !@is_private destroy: -> if @connection_id == @_dispatcher._conn?.connection_id @@ -45,10 +42,9 @@ class WebSocketRails.Channel 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] @@ -61,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/websocket_rails.js.coffee b/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee index 83aad04d..f8b8720b 100644 --- a/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee +++ b/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee @@ -66,7 +66,7 @@ class @WebSocketRails 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() @@ -94,7 +94,7 @@ class @WebSocketRails 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) => diff --git a/spec/javascripts/websocket_rails/channel_spec.coffee b/spec/javascripts/websocket_rails/channel_spec.coffee index 4e3301d8..3482be07 100644 --- a/spec/javascripts/websocket_rails/channel_spec.coffee +++ b/spec/javascripts/websocket_rails/channel_spec.coffee @@ -79,6 +79,13 @@ describe 'WebSocketRails.Channel:', -> @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'}) @@ -86,14 +93,13 @@ describe 'WebSocketRails.Channel:', -> 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_public()).toEqual false + expect(@channel.is_private).toBeTruthy diff --git a/spec/javascripts/websocket_rails/websocket_connection_spec.coffee b/spec/javascripts/websocket_rails/websocket_connection_spec.coffee index a07b46f9..4be9967b 100644 --- a/spec/javascripts/websocket_rails/websocket_connection_spec.coffee +++ b/spec/javascripts/websocket_rails/websocket_connection_spec.coffee @@ -4,7 +4,7 @@ describe 'WebsocketRails.WebSocketConnection:', -> data: JSON.stringify(SAMPLE_EVENT_DATA) beforeEach -> - dispatcher = + @dispatcher = new_message: -> true dispatch: -> true state: 'connected' @@ -13,9 +13,7 @@ describe 'WebsocketRails.WebSocketConnection:', -> constructor: (@url, @dispatcher) -> send: -> true close: -> @onclose(null) - @dispatcher = dispatcher - @connection = new WebSocketRails.WebSocketConnection('localhost:3000/websocket', dispatcher) - + @connection = new WebSocketRails.WebSocketConnection('localhost:3000/websocket', @dispatcher) @dispatcher._conn = @connection describe 'constructor', -> @@ -74,6 +72,8 @@ describe 'WebsocketRails.WebSocketConnection:', -> @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'] @@ -115,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 3b93f93c..a48a16cd 100644 --- a/spec/javascripts/websocket_rails/websocket_rails_spec.coffee +++ b/spec/javascripts/websocket_rails/websocket_rails_spec.coffee @@ -91,8 +91,8 @@ describe 'WebSocketRails:', -> it 'should recreate existing channels, keeping their private/public type', -> @dispatcher.reconnect_channels() - expect(@dispatcher.channels['public 4chan'].is_public()).toEqual true - expect(@dispatcher.channels['private 4chan'].is_public()).toEqual false + 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'] @@ -162,12 +162,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', -> @@ -190,15 +195,20 @@ describe 'WebSocketRails:', -> @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', -> From 2142728ac060d895fa4c52868332524c8e49c232 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Fri, 20 Dec 2013 15:34:12 +0100 Subject: [PATCH 27/27] Fix bug in #reconnect() when disconnected. --- .../javascripts/websocket_rails/websocket_rails.js.coffee | 5 +++-- .../websocket_rails/websocket_rails_spec.coffee | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee b/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee index f8b8720b..d5f6c516 100644 --- a/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee +++ b/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee @@ -49,7 +49,7 @@ class @WebSocketRails # - 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 + old_connection_id = @_conn?.connection_id @disconnect() @connect() @@ -59,7 +59,7 @@ class @WebSocketRails if event.connection_id == old_connection_id && !event.is_result() @trigger_event event - @reconnect_channels(false) + @reconnect_channels() new_message: (data) => for socket_message in data @@ -137,6 +137,7 @@ class @WebSocketRails connection_stale: => @state != 'connected' + # Destroy and resubscribe to all existing @channels. reconnect_channels: -> for name, channel of @channels callbacks = channel._callbacks diff --git a/spec/javascripts/websocket_rails/websocket_rails_spec.coffee b/spec/javascripts/websocket_rails/websocket_rails_spec.coffee index a48a16cd..ea61326c 100644 --- a/spec/javascripts/websocket_rails/websocket_rails_spec.coffee +++ b/spec/javascripts/websocket_rails/websocket_rails_spec.coffee @@ -49,6 +49,13 @@ describe 'WebSocketRails:', -> 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()