From bc81f1e1b4ef748d4eab5f2e28569c07f5e16235 Mon Sep 17 00:00:00 2001 From: zhao lu Date: Wed, 22 Feb 2012 13:39:28 -0500 Subject: [PATCH 01/14] Added basic support for using Redis engine. Currently PrivatePub does not allow Redis engine to be specified when initializing the Faye server. Developers can now create a private_pub_redis.yml where desired values can be specified as per: http://faye.jcoglan.com/ruby/engines.html This commit does not include checks for required fields or conflicts between using socket or non-socket approach. --- lib/private_pub.rb | 13 ++++++++++--- lib/private_pub/engine.rb | 6 ++++++ spec/fixtures/private_pub_redis.yml | 6 ++++++ spec/private_pub_spec.rb | 22 ++++++++++++++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 spec/fixtures/private_pub_redis.yml diff --git a/lib/private_pub.rb b/lib/private_pub.rb index c393ac0..79b8d6c 100644 --- a/lib/private_pub.rb +++ b/lib/private_pub.rb @@ -9,10 +9,12 @@ class Error < StandardError; end class << self attr_reader :config + attr_reader :options - # Resets the configuration to the default (empty hash) + # Resets the configuration and options to the default (empty hash) def reset_config @config = {} + @options = {:engine => {:type => 'redis'}} end # Loads the configuration from a given YAML file and environment (such as production) @@ -22,6 +24,11 @@ def load_config(filename, environment) yaml.each { |k, v| config[k.to_sym] = v } end + def load_redis_config(filename, environment) + yaml = YAML.load_file(filename)[environment.to_s] + yaml.each {|k, v| options[:engine][k.to_sym] = v} + end + # Publish the given data to a specific channel. This ends up sending # a Net::HTTP POST request to the Faye server. def publish_to(channel, data) @@ -68,8 +75,8 @@ def signature_expired?(timestamp) # Returns the Faye Rack application. # Any options given are passed to the Faye::RackAdapter. def faye_app(options = {}) - options = {:mount => "/faye", :timeout => 45, :extensions => [FayeExtension.new]}.merge(options) - Faye::RackAdapter.new(options) + @options.merge({:mount => "/faye", :timeout => 45, :extensions => [FayeExtension.new]}).merge(options) + Faye::RackAdapter.new(@options) end end diff --git a/lib/private_pub/engine.rb b/lib/private_pub/engine.rb index fa431a6..4d89f49 100644 --- a/lib/private_pub/engine.rb +++ b/lib/private_pub/engine.rb @@ -8,6 +8,12 @@ class Engine < Rails::Engine PrivatePub.load_config(path, Rails.env) if path.exist? end + # Loads the private_pub_redis.yml file if it exists. + initializer "private_pub.redis_config" do + path = Rails.root.join("config/private_pub_redis.yml") + PrivatePub.load_redis_config(path, Rails.env) if path.exist? + end + # Adds the ViewHelpers into ActionView::Base initializer "private_pub.view_helpers" do ActionView::Base.send :include, ViewHelpers diff --git a/spec/fixtures/private_pub_redis.yml b/spec/fixtures/private_pub_redis.yml new file mode 100644 index 0000000..8930962 --- /dev/null +++ b/spec/fixtures/private_pub_redis.yml @@ -0,0 +1,6 @@ +test: + host: redis_host + port: redis_port + password: redis_password + database: redis_database + namespace: '/namespace' diff --git a/spec/private_pub_spec.rb b/spec/private_pub_spec.rb index 3929009..36a7e70 100644 --- a/spec/private_pub_spec.rb +++ b/spec/private_pub_spec.rb @@ -26,6 +26,28 @@ PrivatePub.config[:signature_expiration].should eq(600) end + context "when redis config exists" do + before do + PrivatePub.load_redis_config("spec/fixtures/private_pub_redis.yml", "test") + end + + it "should passes redis config to faye engine options" do + PrivatePub.options[:engine][:type].should eq 'redis' + PrivatePub.options[:engine][:host].should eq 'redis_host' + PrivatePub.options[:engine][:port].should eq 'redis_port' + PrivatePub.options[:engine][:password].should eq 'redis_password' + PrivatePub.options[:engine][:database].should eq 'redis_database' + PrivatePub.options[:engine][:namespace].should eq '/namespace' + end + + it "should pass redis config options to faye" do + Faye::RackAdapter.should_receive(:new) do |options| + options[:engine].should eq PrivatePub.options[:engine] + end + PrivatePub.faye_app({}) + end + end + it "raises an exception if an invalid environment is passed to load_config" do lambda { PrivatePub.load_config("spec/fixtures/private_pub.yml", :test) From 045975de5d9d12cbb8c6e19e8295d7a6ee335cce Mon Sep 17 00:00:00 2001 From: zhao lu Date: Wed, 22 Feb 2012 13:49:23 -0500 Subject: [PATCH 02/14] Added configuration instruction for Redis engine --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a66347d..91786bc 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ It's not necessary to include faye.js since that will be handled automatically f ## Serving Faye over HTTPS (with Thin) -To server Faye over HTTPS you could create a thin configuration file `config/private_pub_thin.yml` similar to the following: +To serve Faye over HTTPS you could create a thin configuration file `config/private_pub_thin.yml` similar to the following: ```yaml --- @@ -63,6 +63,21 @@ Finally start up Thin from the project root. thin -C config/private_pub_thin.yml start ``` +## Serving Faye with Redis engine + +To serve Faye with Redis engine, you should create `config/private_pub_redis.yml` + +```yaml +production: + host: redis_host + port: redis_port + password: redis_password + database: redis_database + namespace: '/namespace' +``` + +Note: database and namespace are optional. + ## Usage Use the `subscribe_to` helper method on any page to subscribe to a channel. From fe71d80c739cb8bfb50dc4eb9aba8589632f2659 Mon Sep 17 00:00:00 2001 From: zhao lu Date: Wed, 22 Feb 2012 18:45:58 -0500 Subject: [PATCH 03/14] fixed mount point --- lib/private_pub.rb | 4 ++-- spec/private_pub_spec.rb | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/private_pub.rb b/lib/private_pub.rb index 79b8d6c..06afc94 100644 --- a/lib/private_pub.rb +++ b/lib/private_pub.rb @@ -14,7 +14,7 @@ class << self # Resets the configuration and options to the default (empty hash) def reset_config @config = {} - @options = {:engine => {:type => 'redis'}} + @options = {:engine => {:type => 'redis'}, :mount => "/faye", :timeout => 45, :extensions => [FayeExtension.new]} end # Loads the configuration from a given YAML file and environment (such as production) @@ -75,7 +75,7 @@ def signature_expired?(timestamp) # Returns the Faye Rack application. # Any options given are passed to the Faye::RackAdapter. def faye_app(options = {}) - @options.merge({:mount => "/faye", :timeout => 45, :extensions => [FayeExtension.new]}).merge(options) + @options.merge(options) Faye::RackAdapter.new(@options) end end diff --git a/spec/private_pub_spec.rb b/spec/private_pub_spec.rb index 36a7e70..4e54591 100644 --- a/spec/private_pub_spec.rb +++ b/spec/private_pub_spec.rb @@ -43,6 +43,7 @@ it "should pass redis config options to faye" do Faye::RackAdapter.should_receive(:new) do |options| options[:engine].should eq PrivatePub.options[:engine] + options[:mount].should eq '/faye' end PrivatePub.faye_app({}) end From 1f753ba7a7307b0561a07991e47c85bf4c5dfc9c Mon Sep 17 00:00:00 2001 From: zhao lu Date: Thu, 23 Feb 2012 09:41:46 -0500 Subject: [PATCH 04/14] Refactor - Redis options are now passed into PrivatePub.faye_app. @default_options is what we have today for specifying mount point etc. But this can be changed to configurable as well. --- .../private_pub/templates/private_pub.ru | 6 +++- lib/private_pub.rb | 10 +++--- spec/private_pub_spec.rb | 32 ++++++++++++------- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/lib/generators/private_pub/templates/private_pub.ru b/lib/generators/private_pub/templates/private_pub.ru index 2db4acf..5407d32 100644 --- a/lib/generators/private_pub/templates/private_pub.ru +++ b/lib/generators/private_pub/templates/private_pub.ru @@ -5,4 +5,8 @@ require "faye" require "private_pub" PrivatePub.load_config(File.expand_path("../config/private_pub.yml", __FILE__), ENV["RAILS_ENV"] || "development") -run PrivatePub.faye_app + +path = Rails.root.join("config/private_pub_redis.yml") +options = path.exist? ? PrivatePub.load_redis_config(path, Rails.env) : {} + +run PrivatePub.faye_app(options) diff --git a/lib/private_pub.rb b/lib/private_pub.rb index 06afc94..8b2e30c 100644 --- a/lib/private_pub.rb +++ b/lib/private_pub.rb @@ -9,12 +9,12 @@ class Error < StandardError; end class << self attr_reader :config - attr_reader :options + attr_reader :default_options # Resets the configuration and options to the default (empty hash) def reset_config @config = {} - @options = {:engine => {:type => 'redis'}, :mount => "/faye", :timeout => 45, :extensions => [FayeExtension.new]} + @default_options = {:mount => "/faye", :timeout => 45, :extensions => [FayeExtension.new]} end # Loads the configuration from a given YAML file and environment (such as production) @@ -26,7 +26,10 @@ def load_config(filename, environment) def load_redis_config(filename, environment) yaml = YAML.load_file(filename)[environment.to_s] + options = {:engine => {:type => 'redis'}} yaml.each {|k, v| options[:engine][k.to_sym] = v} + + options end # Publish the given data to a specific channel. This ends up sending @@ -75,8 +78,7 @@ def signature_expired?(timestamp) # Returns the Faye Rack application. # Any options given are passed to the Faye::RackAdapter. def faye_app(options = {}) - @options.merge(options) - Faye::RackAdapter.new(@options) + Faye::RackAdapter.new(@default_options.merge!(options)) end end diff --git a/spec/private_pub_spec.rb b/spec/private_pub_spec.rb index 4e54591..ab374a7 100644 --- a/spec/private_pub_spec.rb +++ b/spec/private_pub_spec.rb @@ -28,24 +28,34 @@ context "when redis config exists" do before do - PrivatePub.load_redis_config("spec/fixtures/private_pub_redis.yml", "test") + @options = PrivatePub.load_redis_config("spec/fixtures/private_pub_redis.yml", "test") end - it "should passes redis config to faye engine options" do - PrivatePub.options[:engine][:type].should eq 'redis' - PrivatePub.options[:engine][:host].should eq 'redis_host' - PrivatePub.options[:engine][:port].should eq 'redis_port' - PrivatePub.options[:engine][:password].should eq 'redis_password' - PrivatePub.options[:engine][:database].should eq 'redis_database' - PrivatePub.options[:engine][:namespace].should eq '/namespace' + it "passes redis config to faye engine options" do + @options[:engine][:type].should eq 'redis' + @options[:engine][:host].should eq 'redis_host' + @options[:engine][:port].should eq 'redis_port' + @options[:engine][:password].should eq 'redis_password' + @options[:engine][:database].should eq 'redis_database' + @options[:engine][:namespace].should eq '/namespace' end - it "should pass redis config options to faye" do + it "should pass redis config and default options to faye" do Faye::RackAdapter.should_receive(:new) do |options| - options[:engine].should eq PrivatePub.options[:engine] + options[:engine].should eq @options[:engine] options[:mount].should eq '/faye' end - PrivatePub.faye_app({}) + PrivatePub.faye_app(@options) + end + end + + context "when redis config does not exist" do + it "should not have :engine inside of options hash" do + PrivatePub.default_options.should_not include :engine + end + + it "should have mount point" do + PrivatePub.default_options[:mount].should eq '/faye' end end From 38726f8927133bf389f549c5438f6c85b0437515 Mon Sep 17 00:00:00 2001 From: zhao lu Date: Thu, 23 Feb 2012 09:57:58 -0500 Subject: [PATCH 05/14] Added some debugging --- lib/generators/private_pub/templates/private_pub.ru | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/generators/private_pub/templates/private_pub.ru b/lib/generators/private_pub/templates/private_pub.ru index 5407d32..ef57a36 100644 --- a/lib/generators/private_pub/templates/private_pub.ru +++ b/lib/generators/private_pub/templates/private_pub.ru @@ -7,6 +7,10 @@ require "private_pub" PrivatePub.load_config(File.expand_path("../config/private_pub.yml", __FILE__), ENV["RAILS_ENV"] || "development") path = Rails.root.join("config/private_pub_redis.yml") -options = path.exist? ? PrivatePub.load_redis_config(path, Rails.env) : {} + +puts "loading redis config: " + path.inspect +puts File.expand_path("../config/private_pub.yml", __FILE__).inspect + +options = path.exist? ? PrivatePub.load_redis_config(File.expand_path("../config/private_pub.yml", __FILE__), Rails.env) : {} run PrivatePub.faye_app(options) From 33969eb8ec38ce0a83ac988f86822337fc1af6a3 Mon Sep 17 00:00:00 2001 From: zhao lu Date: Thu, 23 Feb 2012 11:49:02 -0500 Subject: [PATCH 06/14] Changing how redis config is loaded --- lib/generators/private_pub/templates/private_pub.ru | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/generators/private_pub/templates/private_pub.ru b/lib/generators/private_pub/templates/private_pub.ru index ef57a36..44c17cf 100644 --- a/lib/generators/private_pub/templates/private_pub.ru +++ b/lib/generators/private_pub/templates/private_pub.ru @@ -6,11 +6,7 @@ require "private_pub" PrivatePub.load_config(File.expand_path("../config/private_pub.yml", __FILE__), ENV["RAILS_ENV"] || "development") -path = Rails.root.join("config/private_pub_redis.yml") - -puts "loading redis config: " + path.inspect -puts File.expand_path("../config/private_pub.yml", __FILE__).inspect - -options = path.exist? ? PrivatePub.load_redis_config(File.expand_path("../config/private_pub.yml", __FILE__), Rails.env) : {} +path = File.expand_path("../config/private_pub_redis.yml", __FILE__) +options = File.exist?(path) ? PrivatePub.load_redis_config(path, ENV['RAILS_ENV']) : {} run PrivatePub.faye_app(options) From 00ecd3fe15d320f55672365b8e3cf5fef15df689 Mon Sep 17 00:00:00 2001 From: zhao lu Date: Thu, 23 Feb 2012 12:01:05 -0500 Subject: [PATCH 07/14] Setting default rails_env to be development --- lib/generators/private_pub/templates/private_pub.ru | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/generators/private_pub/templates/private_pub.ru b/lib/generators/private_pub/templates/private_pub.ru index 44c17cf..14cb149 100644 --- a/lib/generators/private_pub/templates/private_pub.ru +++ b/lib/generators/private_pub/templates/private_pub.ru @@ -7,6 +7,6 @@ require "private_pub" PrivatePub.load_config(File.expand_path("../config/private_pub.yml", __FILE__), ENV["RAILS_ENV"] || "development") path = File.expand_path("../config/private_pub_redis.yml", __FILE__) -options = File.exist?(path) ? PrivatePub.load_redis_config(path, ENV['RAILS_ENV']) : {} +options = File.exist?(path) ? PrivatePub.load_redis_config(path, ENV['RAILS_ENV'] || 'development') : {} run PrivatePub.faye_app(options) From 298c3ed7e13eb40c42c3779c24a55b5dbc5dd88b Mon Sep 17 00:00:00 2001 From: zhao lu Date: Mon, 27 Feb 2012 17:30:34 -0800 Subject: [PATCH 08/14] Increased timeout to 60 --- lib/private_pub.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/private_pub.rb b/lib/private_pub.rb index 8b2e30c..2ce8a5a 100644 --- a/lib/private_pub.rb +++ b/lib/private_pub.rb @@ -11,24 +11,25 @@ class << self attr_reader :config attr_reader :default_options - # Resets the configuration and options to the default (empty hash) + # Resets the configuration and options to the default + # configuration defaults to empty hash def reset_config @config = {} - @default_options = {:mount => "/faye", :timeout => 45, :extensions => [FayeExtension.new]} + @default_options = {:mount => "/faye", :timeout => 60, :extensions => [FayeExtension.new]} end - # Loads the configuration from a given YAML file and environment (such as production) + # Loads the configuration from a given YAML file and environment (such as production) def load_config(filename, environment) yaml = YAML.load_file(filename)[environment.to_s] raise ArgumentError, "The #{environment} environment does not exist in #{filename}" if yaml.nil? yaml.each { |k, v| config[k.to_sym] = v } end + # Loads the options from a given YAML file and environment (such as production) def load_redis_config(filename, environment) yaml = YAML.load_file(filename)[environment.to_s] options = {:engine => {:type => 'redis'}} yaml.each {|k, v| options[:engine][k.to_sym] = v} - options end From 1f3728f39d1144027da31d223f9cf06d7b060c3b Mon Sep 17 00:00:00 2001 From: zhao lu Date: Mon, 27 Feb 2012 17:45:34 -0800 Subject: [PATCH 09/14] Fixed faye version to be 0.7.1 because thin was removed in a later version --- private_pub.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/private_pub.gemspec b/private_pub.gemspec index 2c4306a..c46d5bf 100644 --- a/private_pub.gemspec +++ b/private_pub.gemspec @@ -10,7 +10,7 @@ Gem::Specification.new do |s| s.files = Dir["{app,lib,spec}/**/*", "[A-Z]*", "init.rb"] - ["Gemfile.lock"] s.require_path = "lib" - s.add_dependency 'faye' + s.add_dependency 'faye', '0.7.1' s.add_development_dependency 'rake' s.add_development_dependency 'rspec', '~> 2.8.0' From b17035cf3bc1e2aff8157cdc35915f4ee07383d7 Mon Sep 17 00:00:00 2001 From: zhao lu Date: Mon, 5 Mar 2012 13:25:33 -0800 Subject: [PATCH 10/14] Only sign when PrivatePub is avaiable. When thin server is down, PrivatePub is undefined and client will get an error on not able to accessing faye.js. --- lib/private_pub/view_helpers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/private_pub/view_helpers.rb b/lib/private_pub/view_helpers.rb index 9048f36..8d8ea59 100644 --- a/lib/private_pub/view_helpers.rb +++ b/lib/private_pub/view_helpers.rb @@ -15,7 +15,7 @@ def publish_to(channel, data = nil, &block) def subscribe_to(channel) subscription = PrivatePub.subscription(:channel => channel) content_tag "script", :type => "text/javascript" do - raw("PrivatePub.sign(#{subscription.to_json});") + raw("if (typeof PrivatePub != 'undefined') { PrivatePub.sign(#{subscription.to_json}) }") end end end From ff647fa0f47b1fa383604e71b2893fd116dcc7fb Mon Sep 17 00:00:00 2001 From: Diana Chow + Zhao Lu Date: Mon, 5 Mar 2012 17:22:01 -0800 Subject: [PATCH 11/14] Added logic to check in Faye server is up. Only subscribe when it is up. Log a console error if it is not. PrivatePub client now no longer throw uncaught exception and upon seeing console log error, client can choose to handle the situation by performing a null check on PrivatePub.fayeClient. --- app/assets/javascripts/private_pub.js | 40 +++++--- spec/javascripts/private_pub_spec.js | 134 ++++++++++++++++++-------- 2 files changed, 117 insertions(+), 57 deletions(-) diff --git a/app/assets/javascripts/private_pub.js b/app/assets/javascripts/private_pub.js index 8775ca3..e3f8811 100644 --- a/app/assets/javascripts/private_pub.js +++ b/app/assets/javascripts/private_pub.js @@ -1,4 +1,4 @@ -function buildPrivatePub(doc) { +function buildPrivatePub (doc) { var self = { connecting: false, fayeClient: null, @@ -6,7 +6,7 @@ function buildPrivatePub(doc) { subscriptions: {}, subscriptionCallbacks: {}, - faye: function(callback) { + faye: function (callback) { if (self.fayeClient) { callback(self.fayeClient); } else { @@ -22,16 +22,16 @@ function buildPrivatePub(doc) { } }, - connectToFaye: function() { + connectToFaye: function () { self.fayeClient = new Faye.Client(self.subscriptions.server); self.fayeClient.addExtension(self.fayeExtension); - for (var i=0; i < self.fayeCallbacks.length; i++) { + for (var i = 0; i < self.fayeCallbacks.length; i++) { self.fayeCallbacks[i](self.fayeClient); - }; + } }, fayeExtension: { - outgoing: function(message, callback) { + outgoing: function (message, callback) { if (message.channel == "/meta/subscribe") { // Attach the signature and timestamp to subscription messages var subscription = self.subscriptions[message.subscription]; @@ -43,17 +43,25 @@ function buildPrivatePub(doc) { } }, - sign: function(options) { - if (!self.subscriptions.server) { - self.subscriptions.server = options.server; - } - self.subscriptions[options.channel] = options; - self.faye(function(faye) { - faye.subscribe(options.channel, self.handleResponse); - }); + sign: function (options) { + $.ajax({ + url: options.server, + success: function () { + if (!self.subscriptions.server) { + self.subscriptions.server = options.server; + } + self.subscriptions[options.channel] = options; + self.faye(function (faye) { + faye.subscribe(options.channel, self.handleResponse); + }); + }, + error: function () { + console.log("Cannot connect to pubsub server") + } + }) }, - handleResponse: function(message) { + handleResponse: function (message) { if (message.eval) { eval(message.eval); } @@ -62,7 +70,7 @@ function buildPrivatePub(doc) { } }, - subscribe: function(channel, callback) { + subscribe: function (channel, callback) { self.subscriptionCallbacks[channel] = callback; } }; diff --git a/spec/javascripts/private_pub_spec.js b/spec/javascripts/private_pub_spec.js index b309947..c03e562 100644 --- a/spec/javascripts/private_pub_spec.js +++ b/spec/javascripts/private_pub_spec.js @@ -1,21 +1,22 @@ -describe("PrivatePub", function() { +describe("PrivatePub", function () { var pub, doc; - beforeEach(function() { + beforeEach(function () { + $ = { ajax: function(){} } Faye = {}; // To simulate global Faye object doc = {}; pub = buildPrivatePub(doc); }); - it("adds a subscription callback", function() { + it("adds a subscription callback", function () { pub.subscribe("hello", "callback"); expect(pub.subscriptionCallbacks["hello"]).toEqual("callback"); }); - it("has a fayeExtension which adds matching subscription signature and timestamp to outgoing message", function() { + it("has a fayeExtension which adds matching subscription signature and timestamp to outgoing message", function () { var called = false; - var message = {channel: "/meta/subscribe", subscription: "hello"} - pub.subscriptions["hello"] = {signature: "abcd", timestamp: "1234"} - pub.fayeExtension.outgoing(message, function(message) { + var message = {channel:"/meta/subscribe", subscription:"hello"} + pub.subscriptions["hello"] = {signature:"abcd", timestamp:"1234"} + pub.fayeExtension.outgoing(message, function (message) { expect(message.ext.private_pub_signature).toEqual("abcd"); expect(message.ext.private_pub_timestamp).toEqual("1234"); called = true; @@ -23,65 +24,116 @@ describe("PrivatePub", function() { expect(called).toBeTruthy(); }); - it("evaluates javascript in message response", function() { - pub.handleResponse({eval: 'self.subscriptions.foo = "bar"'}); + it("evaluates javascript in message response", function () { + pub.handleResponse({eval:'self.subscriptions.foo = "bar"'}); expect(pub.subscriptions.foo).toEqual("bar"); }); - it("triggers callback matching message channel in response", function() { + it("triggers callback matching message channel in response", function () { var called = false; - pub.subscribe("test", function(data, channel) { + pub.subscribe("test", function (data, channel) { expect(data).toEqual("abcd"); expect(channel).toEqual("test"); called = true; }); - pub.handleResponse({channel: "test", data: "abcd"}); + pub.handleResponse({channel:"test", data:"abcd"}); expect(called).toBeTruthy(); }); - it("adds a faye subscription with response handler when signing", function() { - var faye = {subscribe: jasmine.createSpy()}; - spyOn(pub, 'faye').andCallFake(function(callback) { - callback(faye); + it("tries to reach faye server before subscribing", function(){ + spyOn($, "ajax") + var options = {server:"server", channel:"somechannel"} + pub.sign(options) + expect($.ajax).toHaveBeenCalled() + expect($.ajax.mostRecentCall.args[0].url).toBe(options.server) + }) + + describe("when client can connect to faye server", function () { + beforeEach(function(){ + spyOn($, 'ajax').andCallFake(function(options) { + options.success.call() + }) + }) + + it("adds a faye subscription with response handler when signing", function () { + var faye = {subscribe:jasmine.createSpy()} + + spyOn(pub, 'faye').andCallFake(function (callback) { + callback(faye); + }); + var options = {server:"server", channel:"somechannel"}; + pub.sign(options); + expect(faye.subscribe).toHaveBeenCalledWith("somechannel", pub.handleResponse); + expect(pub.subscriptions.server).toEqual("server"); + expect(pub.subscriptions.somechannel).toEqual(options); }); - var options = {server: "server", channel: "somechannel"}; - pub.sign(options); - expect(faye.subscribe).toHaveBeenCalledWith("somechannel", pub.handleResponse); - expect(pub.subscriptions.server).toEqual("server"); - expect(pub.subscriptions.somechannel).toEqual(options); - }); - it("adds a faye subscription with response handler when signing", function() { - var faye = {subscribe: jasmine.createSpy()}; - spyOn(pub, 'faye').andCallFake(function(callback) { - callback(faye); + it("adds a faye subscription with response handler when signing", function () { + var faye = {subscribe:jasmine.createSpy()}; + spyOn(pub, 'faye').andCallFake(function (callback) { + callback(faye); + }); + var options = {server:"server", channel:"somechannel"}; + pub.sign(options); + expect(faye.subscribe).toHaveBeenCalledWith("somechannel", pub.handleResponse); + expect(pub.subscriptions.server).toEqual("server"); + expect(pub.subscriptions.somechannel).toEqual(options); }); - var options = {server: "server", channel: "somechannel"}; - pub.sign(options); - expect(faye.subscribe).toHaveBeenCalledWith("somechannel", pub.handleResponse); - expect(pub.subscriptions.server).toEqual("server"); - expect(pub.subscriptions.somechannel).toEqual(options); - }); + }) - it("triggers faye callback function immediately when fayeClient is available", function() { + describe("when client cannot connect to faye server", function () { + beforeEach(function(){ + spyOn($, 'ajax').andCallFake(function(options) { + options.error.call() + }) + }) + + it("does not add a faye subscription with response handler when signing", function(){ + var faye = {subscribe:jasmine.createSpy('faye')} + spyOn(pub, 'faye').andCallFake(function (callback) { + callback(faye); + }); + var options = {server:"server", channel:"somechannel"}; + pub.sign(options); + expect(faye.subscribe).not.toHaveBeenCalled() + expect(pub.subscriptions.server).not.toEqual("server"); + expect(pub.subscriptions.somechannel).not.toEqual(options); + }) + + it("logs an error", function() { + var faye = {subscribe:jasmine.createSpy()} + + spyOn(console, 'log') + spyOn(pub, 'faye').andCallFake(function (callback) { + callback(faye) + }) + var options = {server:"server", channel:"somechannel"} + pub.sign(options) + expect(console.log).toHaveBeenCalledWith('Cannot connect to pubsub server') + }) + }) + + it("triggers faye callback function immediately when fayeClient is available", function () { var called = false; pub.fayeClient = "faye"; - pub.faye(function(faye) { + pub.faye(function (faye) { expect(faye).toEqual("faye"); called = true; }); expect(called).toBeTruthy(); }); - it("adds fayeCallback when client and server aren't available", function() { + it("adds fayeCallback when client and server aren't available", function () { pub.faye("callback"); expect(pub.fayeCallbacks[0]).toEqual("callback"); }); - it("adds a script tag loading faye js when the server is present", function() { + it("adds a script tag loading faye js when the server is present", function () { script = {}; - doc.createElement = function() { return script; }; - doc.documentElement = {appendChild: jasmine.createSpy()}; + doc.createElement = function () { + return script; + }; + doc.documentElement = {appendChild:jasmine.createSpy()}; pub.subscriptions.server = "path/to/faye"; pub.faye("callback"); expect(pub.fayeCallbacks[0]).toEqual("callback"); @@ -91,10 +143,10 @@ describe("PrivatePub", function() { expect(doc.documentElement.appendChild).toHaveBeenCalledWith(script); }); - it("connects to faye server, adds extension, and executes callbacks", function() { + it("connects to faye server, adds extension, and executes callbacks", function () { callback = jasmine.createSpy(); - client = {addExtension: jasmine.createSpy()}; - Faye.Client = function(server) { + client = {addExtension:jasmine.createSpy()}; + Faye.Client = function (server) { expect(server).toEqual("server") return client; }; From 2a753a69a4ac0229c1947aeaa2e922d23231bd08 Mon Sep 17 00:00:00 2001 From: Diana Chow + Zhao Lu Date: Mon, 5 Mar 2012 19:41:31 -0800 Subject: [PATCH 12/14] Revert "Added logic to check in Faye server is up. Only subscribe when it is up. Log a console error if it is not. PrivatePub client now no longer throw uncaught exception and upon seeing console log error, client can choose to handle the situation by performing a null check on PrivatePub.fayeClient." We are violating cross-domain security check by making ajax call to Faye server. This reverts commit ff647fa0f47b1fa383604e71b2893fd116dcc7fb. --- app/assets/javascripts/private_pub.js | 40 +++----- spec/javascripts/private_pub_spec.js | 134 ++++++++------------------ 2 files changed, 57 insertions(+), 117 deletions(-) diff --git a/app/assets/javascripts/private_pub.js b/app/assets/javascripts/private_pub.js index e3f8811..8775ca3 100644 --- a/app/assets/javascripts/private_pub.js +++ b/app/assets/javascripts/private_pub.js @@ -1,4 +1,4 @@ -function buildPrivatePub (doc) { +function buildPrivatePub(doc) { var self = { connecting: false, fayeClient: null, @@ -6,7 +6,7 @@ function buildPrivatePub (doc) { subscriptions: {}, subscriptionCallbacks: {}, - faye: function (callback) { + faye: function(callback) { if (self.fayeClient) { callback(self.fayeClient); } else { @@ -22,16 +22,16 @@ function buildPrivatePub (doc) { } }, - connectToFaye: function () { + connectToFaye: function() { self.fayeClient = new Faye.Client(self.subscriptions.server); self.fayeClient.addExtension(self.fayeExtension); - for (var i = 0; i < self.fayeCallbacks.length; i++) { + for (var i=0; i < self.fayeCallbacks.length; i++) { self.fayeCallbacks[i](self.fayeClient); - } + }; }, fayeExtension: { - outgoing: function (message, callback) { + outgoing: function(message, callback) { if (message.channel == "/meta/subscribe") { // Attach the signature and timestamp to subscription messages var subscription = self.subscriptions[message.subscription]; @@ -43,25 +43,17 @@ function buildPrivatePub (doc) { } }, - sign: function (options) { - $.ajax({ - url: options.server, - success: function () { - if (!self.subscriptions.server) { - self.subscriptions.server = options.server; - } - self.subscriptions[options.channel] = options; - self.faye(function (faye) { - faye.subscribe(options.channel, self.handleResponse); - }); - }, - error: function () { - console.log("Cannot connect to pubsub server") - } - }) + sign: function(options) { + if (!self.subscriptions.server) { + self.subscriptions.server = options.server; + } + self.subscriptions[options.channel] = options; + self.faye(function(faye) { + faye.subscribe(options.channel, self.handleResponse); + }); }, - handleResponse: function (message) { + handleResponse: function(message) { if (message.eval) { eval(message.eval); } @@ -70,7 +62,7 @@ function buildPrivatePub (doc) { } }, - subscribe: function (channel, callback) { + subscribe: function(channel, callback) { self.subscriptionCallbacks[channel] = callback; } }; diff --git a/spec/javascripts/private_pub_spec.js b/spec/javascripts/private_pub_spec.js index c03e562..b309947 100644 --- a/spec/javascripts/private_pub_spec.js +++ b/spec/javascripts/private_pub_spec.js @@ -1,22 +1,21 @@ -describe("PrivatePub", function () { +describe("PrivatePub", function() { var pub, doc; - beforeEach(function () { - $ = { ajax: function(){} } + beforeEach(function() { Faye = {}; // To simulate global Faye object doc = {}; pub = buildPrivatePub(doc); }); - it("adds a subscription callback", function () { + it("adds a subscription callback", function() { pub.subscribe("hello", "callback"); expect(pub.subscriptionCallbacks["hello"]).toEqual("callback"); }); - it("has a fayeExtension which adds matching subscription signature and timestamp to outgoing message", function () { + it("has a fayeExtension which adds matching subscription signature and timestamp to outgoing message", function() { var called = false; - var message = {channel:"/meta/subscribe", subscription:"hello"} - pub.subscriptions["hello"] = {signature:"abcd", timestamp:"1234"} - pub.fayeExtension.outgoing(message, function (message) { + var message = {channel: "/meta/subscribe", subscription: "hello"} + pub.subscriptions["hello"] = {signature: "abcd", timestamp: "1234"} + pub.fayeExtension.outgoing(message, function(message) { expect(message.ext.private_pub_signature).toEqual("abcd"); expect(message.ext.private_pub_timestamp).toEqual("1234"); called = true; @@ -24,116 +23,65 @@ describe("PrivatePub", function () { expect(called).toBeTruthy(); }); - it("evaluates javascript in message response", function () { - pub.handleResponse({eval:'self.subscriptions.foo = "bar"'}); + it("evaluates javascript in message response", function() { + pub.handleResponse({eval: 'self.subscriptions.foo = "bar"'}); expect(pub.subscriptions.foo).toEqual("bar"); }); - it("triggers callback matching message channel in response", function () { + it("triggers callback matching message channel in response", function() { var called = false; - pub.subscribe("test", function (data, channel) { + pub.subscribe("test", function(data, channel) { expect(data).toEqual("abcd"); expect(channel).toEqual("test"); called = true; }); - pub.handleResponse({channel:"test", data:"abcd"}); + pub.handleResponse({channel: "test", data: "abcd"}); expect(called).toBeTruthy(); }); - it("tries to reach faye server before subscribing", function(){ - spyOn($, "ajax") - var options = {server:"server", channel:"somechannel"} - pub.sign(options) - expect($.ajax).toHaveBeenCalled() - expect($.ajax.mostRecentCall.args[0].url).toBe(options.server) - }) - - describe("when client can connect to faye server", function () { - beforeEach(function(){ - spyOn($, 'ajax').andCallFake(function(options) { - options.success.call() - }) - }) - - it("adds a faye subscription with response handler when signing", function () { - var faye = {subscribe:jasmine.createSpy()} - - spyOn(pub, 'faye').andCallFake(function (callback) { - callback(faye); - }); - var options = {server:"server", channel:"somechannel"}; - pub.sign(options); - expect(faye.subscribe).toHaveBeenCalledWith("somechannel", pub.handleResponse); - expect(pub.subscriptions.server).toEqual("server"); - expect(pub.subscriptions.somechannel).toEqual(options); + it("adds a faye subscription with response handler when signing", function() { + var faye = {subscribe: jasmine.createSpy()}; + spyOn(pub, 'faye').andCallFake(function(callback) { + callback(faye); }); + var options = {server: "server", channel: "somechannel"}; + pub.sign(options); + expect(faye.subscribe).toHaveBeenCalledWith("somechannel", pub.handleResponse); + expect(pub.subscriptions.server).toEqual("server"); + expect(pub.subscriptions.somechannel).toEqual(options); + }); - it("adds a faye subscription with response handler when signing", function () { - var faye = {subscribe:jasmine.createSpy()}; - spyOn(pub, 'faye').andCallFake(function (callback) { - callback(faye); - }); - var options = {server:"server", channel:"somechannel"}; - pub.sign(options); - expect(faye.subscribe).toHaveBeenCalledWith("somechannel", pub.handleResponse); - expect(pub.subscriptions.server).toEqual("server"); - expect(pub.subscriptions.somechannel).toEqual(options); + it("adds a faye subscription with response handler when signing", function() { + var faye = {subscribe: jasmine.createSpy()}; + spyOn(pub, 'faye').andCallFake(function(callback) { + callback(faye); }); - }) - - describe("when client cannot connect to faye server", function () { - beforeEach(function(){ - spyOn($, 'ajax').andCallFake(function(options) { - options.error.call() - }) - }) - - it("does not add a faye subscription with response handler when signing", function(){ - var faye = {subscribe:jasmine.createSpy('faye')} - spyOn(pub, 'faye').andCallFake(function (callback) { - callback(faye); - }); - var options = {server:"server", channel:"somechannel"}; - pub.sign(options); - expect(faye.subscribe).not.toHaveBeenCalled() - expect(pub.subscriptions.server).not.toEqual("server"); - expect(pub.subscriptions.somechannel).not.toEqual(options); - }) - - it("logs an error", function() { - var faye = {subscribe:jasmine.createSpy()} - - spyOn(console, 'log') - spyOn(pub, 'faye').andCallFake(function (callback) { - callback(faye) - }) - var options = {server:"server", channel:"somechannel"} - pub.sign(options) - expect(console.log).toHaveBeenCalledWith('Cannot connect to pubsub server') - }) - }) + var options = {server: "server", channel: "somechannel"}; + pub.sign(options); + expect(faye.subscribe).toHaveBeenCalledWith("somechannel", pub.handleResponse); + expect(pub.subscriptions.server).toEqual("server"); + expect(pub.subscriptions.somechannel).toEqual(options); + }); - it("triggers faye callback function immediately when fayeClient is available", function () { + it("triggers faye callback function immediately when fayeClient is available", function() { var called = false; pub.fayeClient = "faye"; - pub.faye(function (faye) { + pub.faye(function(faye) { expect(faye).toEqual("faye"); called = true; }); expect(called).toBeTruthy(); }); - it("adds fayeCallback when client and server aren't available", function () { + it("adds fayeCallback when client and server aren't available", function() { pub.faye("callback"); expect(pub.fayeCallbacks[0]).toEqual("callback"); }); - it("adds a script tag loading faye js when the server is present", function () { + it("adds a script tag loading faye js when the server is present", function() { script = {}; - doc.createElement = function () { - return script; - }; - doc.documentElement = {appendChild:jasmine.createSpy()}; + doc.createElement = function() { return script; }; + doc.documentElement = {appendChild: jasmine.createSpy()}; pub.subscriptions.server = "path/to/faye"; pub.faye("callback"); expect(pub.fayeCallbacks[0]).toEqual("callback"); @@ -143,10 +91,10 @@ describe("PrivatePub", function () { expect(doc.documentElement.appendChild).toHaveBeenCalledWith(script); }); - it("connects to faye server, adds extension, and executes callbacks", function () { + it("connects to faye server, adds extension, and executes callbacks", function() { callback = jasmine.createSpy(); - client = {addExtension:jasmine.createSpy()}; - Faye.Client = function (server) { + client = {addExtension: jasmine.createSpy()}; + Faye.Client = function(server) { expect(server).toEqual("server") return client; }; From 265c3bf9e1e7cbe0952f77328adec92fb86c0c37 Mon Sep 17 00:00:00 2001 From: Diana Chow + Zhao Lu Date: Mon, 5 Mar 2012 23:17:20 -0800 Subject: [PATCH 13/14] Added support to Faye 0.8.0. Faye 0.8.0 contains two incompatible changes according to: http://blog.jcoglan.com/2012/02/27/faye-0-8-the-refactoring/ Notably, his check-in allows users to specify one of the 3 support adapters through private_pub.yml. The supported adapters are: thin (default), rainbows, and goliath --- lib/generators/private_pub/templates/private_pub.ru | 7 ++++++- lib/generators/private_pub/templates/private_pub.yml | 3 +++ lib/private_pub.rb | 3 ++- private_pub.gemspec | 3 ++- spec/fixtures/private_pub.yml | 2 ++ spec/private_pub_spec.rb | 3 ++- spec/spec_helper.rb | 1 + 7 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/generators/private_pub/templates/private_pub.ru b/lib/generators/private_pub/templates/private_pub.ru index 14cb149..8bb5605 100644 --- a/lib/generators/private_pub/templates/private_pub.ru +++ b/lib/generators/private_pub/templates/private_pub.ru @@ -5,8 +5,13 @@ require "faye" require "private_pub" PrivatePub.load_config(File.expand_path("../config/private_pub.yml", __FILE__), ENV["RAILS_ENV"] || "development") +Faye::WebSocket.load_adapter(PrivatePub.config[:adapter]) path = File.expand_path("../config/private_pub_redis.yml", __FILE__) -options = File.exist?(path) ? PrivatePub.load_redis_config(path, ENV['RAILS_ENV'] || 'development') : {} +options = {} +if File.exist?(path) + require 'faye/redis' + PrivatePub.load_redis_config(path, ENV['RAILS_ENV'] || 'development') +end run PrivatePub.faye_app(options) diff --git a/lib/generators/private_pub/templates/private_pub.yml b/lib/generators/private_pub/templates/private_pub.yml index b827e8b..86b522d 100644 --- a/lib/generators/private_pub/templates/private_pub.yml +++ b/lib/generators/private_pub/templates/private_pub.yml @@ -1,10 +1,13 @@ development: + adapter: thin server: "http://localhost:9292/faye" secret_token: "secret" test: + adapter: thin server: "http://localhost:9292/faye" secret_token: "secret" production: + adapter: thin server: "http://example.com/faye" secret_token: "<%= defined?(SecureRandom) ? SecureRandom.hex(32) : ActiveSupport::SecureRandom.hex(32) %>" signature_expiration: 3600 # one hour diff --git a/lib/private_pub.rb b/lib/private_pub.rb index 2ce8a5a..5d3bf91 100644 --- a/lib/private_pub.rb +++ b/lib/private_pub.rb @@ -1,5 +1,6 @@ require "digest/sha1" require "net/http" +require "yajl/json_gem" require "private_pub/faye_extension" require "private_pub/engine" if defined? Rails @@ -28,7 +29,7 @@ def load_config(filename, environment) # Loads the options from a given YAML file and environment (such as production) def load_redis_config(filename, environment) yaml = YAML.load_file(filename)[environment.to_s] - options = {:engine => {:type => 'redis'}} + options = {:engine => {:type => Faye::Redis}} yaml.each {|k, v| options[:engine][k.to_sym] = v} options end diff --git a/private_pub.gemspec b/private_pub.gemspec index c46d5bf..4326f37 100644 --- a/private_pub.gemspec +++ b/private_pub.gemspec @@ -10,7 +10,8 @@ Gem::Specification.new do |s| s.files = Dir["{app,lib,spec}/**/*", "[A-Z]*", "init.rb"] - ["Gemfile.lock"] s.require_path = "lib" - s.add_dependency 'faye', '0.7.1' + s.add_dependency 'faye', '>= 0.8.0' + s.add_dependency 'faye-redis' s.add_development_dependency 'rake' s.add_development_dependency 'rspec', '~> 2.8.0' diff --git a/spec/fixtures/private_pub.yml b/spec/fixtures/private_pub.yml index 57eb4d3..9881991 100644 --- a/spec/fixtures/private_pub.yml +++ b/spec/fixtures/private_pub.yml @@ -1,8 +1,10 @@ development: + adapter: thin server: http://dev.local:9292/faye secret_token: DEVELOPMENT_SECRET_TOKEN signature_expiration: 600 production: + adapter: thin server: http://example.com/faye secret_token: PRODUCTION_SECRET_TOKEN signature_expiration: 600 diff --git a/spec/private_pub_spec.rb b/spec/private_pub_spec.rb index ab374a7..be795e6 100644 --- a/spec/private_pub_spec.rb +++ b/spec/private_pub_spec.rb @@ -24,6 +24,7 @@ PrivatePub.config[:server].should eq("http://example.com/faye") PrivatePub.config[:secret_token].should eq("PRODUCTION_SECRET_TOKEN") PrivatePub.config[:signature_expiration].should eq(600) + PrivatePub.config[:adapter].should eq('thin') end context "when redis config exists" do @@ -32,7 +33,7 @@ end it "passes redis config to faye engine options" do - @options[:engine][:type].should eq 'redis' + @options[:engine][:type].should eq Faye::Redis @options[:engine][:host].should eq 'redis_host' @options[:engine][:port].should eq 'redis_port' @options[:engine][:password].should eq 'redis_password' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8f935ac..b781bc6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,7 @@ require 'rubygems' require 'bundler/setup' require 'faye' +require 'faye/redis' Bundler.require(:default) RSpec.configure do |config| From b0d96cea0ffc62627b01e32988665bae80f3caf2 Mon Sep 17 00:00:00 2001 From: Diana Chow + Zhao Lu Date: Mon, 5 Mar 2012 23:56:50 -0800 Subject: [PATCH 14/14] Actually merge redis options --- lib/generators/private_pub/templates/private_pub.ru | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/generators/private_pub/templates/private_pub.ru b/lib/generators/private_pub/templates/private_pub.ru index 8bb5605..46e5750 100644 --- a/lib/generators/private_pub/templates/private_pub.ru +++ b/lib/generators/private_pub/templates/private_pub.ru @@ -3,6 +3,7 @@ require "bundler/setup" require "yaml" require "faye" require "private_pub" +require "thin" PrivatePub.load_config(File.expand_path("../config/private_pub.yml", __FILE__), ENV["RAILS_ENV"] || "development") Faye::WebSocket.load_adapter(PrivatePub.config[:adapter]) @@ -11,7 +12,7 @@ path = File.expand_path("../config/private_pub_redis.yml", __FILE__) options = {} if File.exist?(path) require 'faye/redis' - PrivatePub.load_redis_config(path, ENV['RAILS_ENV'] || 'development') + options.merge(PrivatePub.load_redis_config(path, ENV['RAILS_ENV'] || 'development')) end run PrivatePub.faye_app(options)