FEATURE: Ruby MessageBus HTTP client.

FEATURE: Ruby MessageBus HTTP client.
From 742db42d901d0101adcf12be1f7b532677752e71 Mon Sep 17 00:00:00 2001
From: Guo Xiang Tan <tgx_world@hotmail.com>
Date: Wed, 5 Dec 2018 16:05:48 +0800
Subject: [PATCH] FEATURE: Ruby MessageBus HTTP client.


diff --git a/.gitignore b/.gitignore
index c77f8cf..a98b7de 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,4 @@ test/version_tmp
 tmp
 *.swp
 .rubocop-https---raw-githubusercontent-com-discourse-discourse-master--rubocop-yml
+.byebug_history
diff --git a/CHANGELOG b/CHANGELOG
index 563052c..370277c 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,7 @@
 Unreleased
 
 - Re-wrote diagnostics UI using React
+- MessageBus HTTP Ruby Client
 
 30-11-2018
 
diff --git a/Gemfile b/Gemfile
index 5c92ddc..4f89c30 100644
--- a/Gemfile
+++ b/Gemfile
@@ -11,6 +11,11 @@ group :test do
   gem 'thin'
   gem 'rack-test', require: 'rack/test'
   gem 'jasmine'
+  gem 'puma'
+end
+
+group :test, :development do
+  gem 'byebug'
 end
 
 gem 'rack'
diff --git a/README.md b/README.md
index d5405b3..d0f0057 100644
--- a/README.md
+++ b/README.md
@@ -221,6 +221,8 @@ MessageBus.publish "/global/channel", "will go to all sites"
 
 ### Client support
 
+#### JavaScript Client
+
 MessageBus ships a simple ~300 line JavaScript library which provides an API to interact with the server.
 
 JavaScript clients can listen on any channel and receive messages via polling or long polling. You may simply include the source file (located in `assets/` within the message_bus source code):
@@ -264,9 +266,7 @@ MessageBus.subscribe("/channel", function(data){
 }, -3);
 `‍``
 
-There is also [a Ruby implementation of the client library](https://github.com/lowjoel/message_bus-client) available with an API very similar to that of the JavaScript client.
-
-#### Client settings
+#### JavaScript Client settings
 
 All client settings are settable via `MessageBus.OPTION`
 
@@ -284,7 +284,7 @@ headers|{}|Extra headers to be include with requests. Properties and values of o
 minHiddenPollInterval|1500|Time to wait between poll requests performed by background or hidden tabs and windows, shared state via localStorage
 enableChunkedEncoding|true|Allows streaming of message bus data over the HTTP connection without closing the connection after each message.
 
-#### Client API
+#### Javascript Client API
 
 `MessageBus.start()` : Starts up the MessageBus poller
 
@@ -304,6 +304,46 @@ enableChunkedEncoding|true|Allows streaming of message bus data over the HTTP co
 
 `MessageBus.diagnostics()` : Returns a log that may be used for diagnostics on the status of message bus.
 
+#### Ruby
+
+The gem ships with a Ruby implementation of the client library available with an
+API very similar to that of the JavaScript client. It was inspired by
+https://github.com/lowjoel/message_bus-client.
+
+`‍``ruby
+# Creates a client with the default configuration
+client = MessageBus::HTTPClient.new('http://some.test.com')
+
+# Listen for the latest messages
+client.subscribe("/channel") { |data| puts data }
+
+# Listen for all messages after id 7
+client.subscribe("/channel", last_message_id: 7) { |data| puts data }
+
+# Listen for last message and all new messages
+client.subscribe("/channel", last_message_id: -2) { |data| puts data }
+
+# Unsubscribe from a channel
+client.unsubscribe("/channel")
+
+# Unsubscribe a particular callback from a channel
+callback = -> { |data| puts data }
+client.subscribe("/channel", &callback)
+client.unsubscribe("/channel", &callback)
+`‍``
+
+#### Ruby Client Settings
+
+Setting|Default|Info
+----|---|---|
+enable_long_polling|true|Allow long-polling (provided it is enabled by the server)
+background_callback_interval|60s|Interval to poll when long polling is disabled
+min_poll_interval|0.1s|When polling requests succeed, this is the minimum amount of time to wait before making the next request.
+max_poll_interval|180s|If request to the server start failing, MessageBus will backoff, this is the upper limit of the backoff.
+enable_chunked_encoding|true|Allows streaming of message bus data over the HTTP connection without closing the connection after each message.
+headers|{}|Extra headers to be include with requests. Properties and values of object must be valid values for HTTP Headers, i.e. no spaces or control characters.
+
+
 ## Configuration
 
 message_bus can be configured to use one of several available storage backends, and each has its own configuration options.
diff --git a/Rakefile b/Rakefile
index 30a3747..f84e959 100644
--- a/Rakefile
+++ b/Rakefile
@@ -39,21 +39,45 @@ task spec_client_js: 'jasmine:ci'
 backends = Dir["lib/message_bus/backends/*.rb"].map { |file| file.match(%r{backends/(?<backend>.*).rb})[:backend] } - ["base"]
 
 namespace :spec do
+  spec_files = Dir['spec/**/*_spec.rb']
+  integration_files = Dir['spec/integration/**/*_spec.rb']
+
   backends.each do |backend|
     desc "Run tests on the #{backend} backend"
     task backend do
       begin
         ENV['MESSAGE_BUS_BACKEND'] = backend
-        sh "#{FileUtils::RUBY} -e \"ARGV.each{|f| load f}\" #{Dir['spec/**/*_spec.rb'].to_a.join(' ')}"
+        sh "#{FileUtils::RUBY} -e \"ARGV.each{|f| load f}\" #{(spec_files - integration_files).to_a.join(' ')}"
       ensure
         ENV.delete('MESSAGE_BUS_BACKEND')
       end
     end
   end
+
+  desc "Run integration tests"
+  task :integration do
+    def port_available?(port)
+      server = TCPServer.open("0.0.0.0", port)
+      server.close
+      true
+    rescue Errno::EADDRINUSE
+      false
+    end
+
+    begin
+      ENV['MESSAGE_BUS_BACKEND'] = 'memory'
+      pid = spawn("bundle exec puma -p 9292 spec/fixtures/test/config.ru")
+      sleep 1 while port_available?(9292)
+      sh "#{FileUtils::RUBY} -e \"ARGV.each{|f| load f}\" #{integration_files.to_a.join(' ')}"
+    ensure
+      ENV.delete('MESSAGE_BUS_BACKEND')
+      Process.kill('TERM', pid) if pid
+    end
+  end
 end
 
 desc "Run tests on all backends, plus client JS tests"
-task spec: backends.map { |backend| "spec:#{backend}" } + [:spec_client_js]
+task spec: backends.map { |backend| "spec:#{backend}" } + [:spec_client_js, "spec:integration"]
 
 desc "Run performance benchmarks on all backends"
 task :performance do
diff --git a/lib/message_bus/http_client.rb b/lib/message_bus/http_client.rb
new file mode 100644
index 0000000..40f958f
--- /dev/null
+++ b/lib/message_bus/http_client.rb
@@ -0,0 +1,337 @@
+require 'securerandom'
+require 'net/http'
+require 'json'
+require 'uri'
+require 'message_bus/http_client/channel'
+
+module MessageBus
+  # MessageBus client that enables subscription via long polling with support
+  # for chunked encoding. Falls back to normal polling if long polling is not
+  # available.
+  #
+  # @!attribute [r] channels
+  #   @return [Hash] a map of the channels that the client is subscribed to
+  # @!attribute [r] stats
+  #   @return [Stats] a Struct containing the statistics of failed and successful
+  #     polling requests
+  #
+  # @!attribute enable_long_polling
+  #   @return [Boolean] whether long polling is enabled
+  # @!attribute status
+  #   @return [HTTPClient::STOPPED, HTTPClient::STARTED] the status of the client.
+  # @!attribute enable_chunked_encoding
+  #   @return [Boolean] whether chunked encoding is enabled
+  # @!attribute min_poll_interval
+  #   @return [Float] the min poll interval for long polling
+  # @!attribute max_poll_interval
+  #   @return [Float] the max poll interval for long polling
+  # @!attribute background_callback_interval
+  #   @return [Float] the polling interval
+  class HTTPClient
+    class InvalidChannel < StandardError; end
+    class MissingBlock < StandardError; end
+
+    attr_reader :channels,
+                :stats
+
+    attr_accessor :enable_long_polling,
+                  :status,
+                  :enable_chunked_encoding,
+                  :min_poll_interval,
+                  :max_poll_interval,
+                  :background_callback_interval
+
+    CHUNK_SEPARATOR = "\r\n|\r\n".freeze
+    priva

GitHub