firehose 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +6 -0
- data/.rbenv-version +1 -0
- data/.rspec +1 -0
- data/Gemfile +5 -0
- data/Procfile +1 -0
- data/README.md +105 -0
- data/Rakefile +4 -0
- data/bin/firehose +6 -0
- data/bin/firehose-test +6 -0
- data/config.ru +6 -0
- data/firehose.gemspec +36 -0
- data/lib/assets/flash/WebSocketMain.swf +0 -0
- data/lib/firehose.rb +23 -0
- data/lib/firehose/goliath.rb +69 -0
- data/lib/firehose/http_publisher.rb +32 -0
- data/lib/firehose/publisher.rb +12 -0
- data/lib/firehose/rack.rb +76 -0
- data/lib/firehose/subscription.rb +54 -0
- data/lib/firehose/thin.rb +7 -0
- data/lib/firehose/version.rb +3 -0
- data/spec/integrations/amqp_resources_spec.rb +50 -0
- data/spec/integrations/goliath_spec.rb +55 -0
- data/spec/integrations/thin_spec.rb +76 -0
- data/spec/spec_helper.rb +17 -0
- metadata +197 -0
data/.gitignore
ADDED
data/.rbenv-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.9.3-p125
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/Gemfile
ADDED
data/Procfile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
firehose: bundle exec thin -p $PORT -R config.ru --debug start
|
data/README.md
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
__ _ _
|
2
|
+
/ _(_) | |
|
3
|
+
| |_ _ _ __ ___| |__ ___ ___ ___
|
4
|
+
| _| | '__/ _ \ '_ \ / _ \/ __|/ _ \
|
5
|
+
| | | | | | __/ | | | (_) \__ \ __/
|
6
|
+
|_| |_|_| \___|_| |_|\___/|___/\___|
|
7
|
+
|
8
|
+
Build Realtime web applications in Ruby
|
9
|
+
|
10
|
+
# What is Firehose?
|
11
|
+
|
12
|
+
Firehose is both a Rack application and JavasSript library that makes building scalable real-time web applications possible.
|
13
|
+
|
14
|
+
# How is it different from socket.io?
|
15
|
+
|
16
|
+
socket.io attempts to store connection state per node instance. Firehose makes no attempt to store connection state.
|
17
|
+
|
18
|
+
Also, socket.io attempts to abstract a low-latency full-duplex port. Firehose assumes that its impossible to simulate this in older web browsers that don't support WebSockets. As such, Firehose focuses on low-latency server-to-client connections and encourages the use of HTTP transports for client-to-server communications.
|
19
|
+
|
20
|
+
Finally, firehose attempts to solve data consistency issues and authentication by encourage the use of proxying to the web application.
|
21
|
+
|
22
|
+
# Getting Started
|
23
|
+
|
24
|
+
First, you'll need to install and run RabbitMQ.
|
25
|
+
|
26
|
+
```
|
27
|
+
apt-get install rabbitmq # Install on Ubuntu
|
28
|
+
brew install rabbitmq # Install on Mac Homebrew
|
29
|
+
```
|
30
|
+
|
31
|
+
## The Consumer
|
32
|
+
|
33
|
+
The consumer is the web server that your client connects to for real-time updates. Create a config.ru file with the following:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
require 'rubygems'
|
37
|
+
require 'firehose'
|
38
|
+
|
39
|
+
run Firehose::Transport::Dispatcher.new do |config|
|
40
|
+
config.timeout = 20
|
41
|
+
|
42
|
+
# Extract the consumer ID from the HTTP session. This could be a cookie
|
43
|
+
# query param, or whatever.
|
44
|
+
config.consumer = Proc.new do |env|
|
45
|
+
Firehose::Consumer.new(env['HTTP_CONSUMER_ID'])
|
46
|
+
end
|
47
|
+
|
48
|
+
# Use the /url/path for the queue channel. You could change this to a query
|
49
|
+
# param, or whatever
|
50
|
+
config.channel = Proc.new do |env|
|
51
|
+
env['PATH_INFO']
|
52
|
+
end
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
Now run the config.ru file in a server that supports async Rack callbacks (like thin or rainbows)
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
thin -R config.ru -p 4000 start
|
60
|
+
```
|
61
|
+
|
62
|
+
## The Producer
|
63
|
+
|
64
|
+
Lets test the producer! Open two terminal windows. In one window, curl the consumer server:
|
65
|
+
|
66
|
+
```sh
|
67
|
+
curl "http://localhost:4000/"
|
68
|
+
```
|
69
|
+
|
70
|
+
Then run the following script in another terminal:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
require 'rubygems'
|
74
|
+
require 'firehose'
|
75
|
+
|
76
|
+
Firehose::Producer.new.publish('hi there!').to('/greetings')
|
77
|
+
```
|
78
|
+
|
79
|
+
## JavaScript Client
|
80
|
+
|
81
|
+
Then in your browser create a new Firehose Client object as such:
|
82
|
+
|
83
|
+
```javascript
|
84
|
+
new Firehose.Client()
|
85
|
+
.url({
|
86
|
+
websocket: 'ws://some_websocket_url.com',
|
87
|
+
longpoll: 'http://some_longpoll_url.com'
|
88
|
+
})
|
89
|
+
.params({
|
90
|
+
cid: '024023948234'
|
91
|
+
})
|
92
|
+
.options({
|
93
|
+
timeout: 5000
|
94
|
+
})
|
95
|
+
.message(function(msg){
|
96
|
+
alert(msg); // Fires when a message is received from the server.
|
97
|
+
})
|
98
|
+
.connected(function(){
|
99
|
+
alert('Howdy friend!');
|
100
|
+
})
|
101
|
+
.disconnected(function(){
|
102
|
+
alert('Bu bye');
|
103
|
+
})
|
104
|
+
.connect()
|
105
|
+
```
|
data/Rakefile
ADDED
data/bin/firehose
ADDED
data/bin/firehose-test
ADDED
data/config.ru
ADDED
data/firehose.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "firehose/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "firehose"
|
7
|
+
s.version = Firehose::VERSION
|
8
|
+
s.authors = ["Brad Gessler"]
|
9
|
+
s.email = ["brad@bradgessler.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Build realtime Ruby web applications}
|
12
|
+
s.description = %q{Firehose is a realtime web application toolkit for building realtime Ruby web applications.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "firehose"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
# s.add_runtime_dependency "goliath", "=> 0.9.4"
|
23
|
+
s.add_runtime_dependency "eventmachine", ">= 1.0.0.beta"
|
24
|
+
s.add_runtime_dependency "amqp", ">= 0.9.4"
|
25
|
+
s.add_runtime_dependency "thin"
|
26
|
+
s.add_runtime_dependency "websocket-rack"
|
27
|
+
|
28
|
+
s.add_development_dependency "rspec"
|
29
|
+
s.add_development_dependency "rack-test"
|
30
|
+
s.add_development_dependency "guard-rspec"
|
31
|
+
s.add_development_dependency "guard-bundler"
|
32
|
+
s.add_development_dependency "thin"
|
33
|
+
# em-http dropped support for WS as of version 1.0+ (https://github.com/igrigorik/em-http-request/issues/164)
|
34
|
+
s.add_development_dependency "em-http-request", "~> 0.3.0"
|
35
|
+
s.add_development_dependency "guard-coffeescript"
|
36
|
+
end
|
Binary file
|
data/lib/firehose.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'firehose/version'
|
2
|
+
|
3
|
+
require 'amqp'
|
4
|
+
|
5
|
+
module Firehose
|
6
|
+
autoload :Subscription, 'firehose/subscription'
|
7
|
+
autoload :Publisher, 'firehose/publisher'
|
8
|
+
autoload :Goliath, 'firehose/goliath'
|
9
|
+
autoload :Rack, 'firehose/rack'
|
10
|
+
|
11
|
+
# TODO move this into a configuration or session class.
|
12
|
+
# Hang on to AMQP configuration settings.
|
13
|
+
def self.amqp
|
14
|
+
@amqp ||= Struct.new(:connection).new(AMQP.connect)
|
15
|
+
end
|
16
|
+
|
17
|
+
# TODO figure out a better way to memoize AMQP connection for production runtimes, and
|
18
|
+
# make it resetable for testing environment. Some sort of Firehose::Session object is probably
|
19
|
+
# in order
|
20
|
+
def self.reset!
|
21
|
+
@amqp = nil
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'goliath'
|
2
|
+
require 'goliath/websocket'
|
3
|
+
|
4
|
+
module Firehose
|
5
|
+
module Goliath
|
6
|
+
class WebSocket < ::Goliath::WebSocket
|
7
|
+
use ::Goliath::Rack::Params
|
8
|
+
|
9
|
+
def on_open(env)
|
10
|
+
# TODO Fix the Firehose::App app to not need '/ws' in front of the socket.
|
11
|
+
path = env['REQUEST_PATH'].gsub(/^\/ws/, '') # Name of the queue in AMQP we'll be pulling from.
|
12
|
+
cid = params[:cid]
|
13
|
+
|
14
|
+
@subscription = Firehose::Subscription.new(cid)
|
15
|
+
@subscription.subscribe path do |payload|
|
16
|
+
env.stream_send(payload)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def on_close(env)
|
21
|
+
@subscription.unsubscribe if @subscription
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class LongPolling < ::Goliath::API
|
26
|
+
use ::Goliath::Rack::Params
|
27
|
+
|
28
|
+
def response(env)
|
29
|
+
method = env['REQUEST_METHOD'] # We use this to figure out if we're producing or consuming.
|
30
|
+
path = env['REQUEST_PATH'] # Name of the queue in AMQP we'll be pulling from.
|
31
|
+
cid = params[:cid]
|
32
|
+
|
33
|
+
case method
|
34
|
+
# GET is how clients subscribe to the queue. When a messages comes in, we flush out a response,
|
35
|
+
# close down the requeust, and the client then reconnects.
|
36
|
+
when 'GET'
|
37
|
+
subscription = Firehose::Subscription.new(cid)
|
38
|
+
subscription.subscribe path do |payload|
|
39
|
+
subscription.unsubscribe
|
40
|
+
env.chunked_stream_send(payload)
|
41
|
+
env.chunked_stream_close
|
42
|
+
end
|
43
|
+
chunked_streaming_response(200, 'Content-Type' => 'text/plain')
|
44
|
+
# PUT is how we throw messages on to the fan-out queue.
|
45
|
+
when 'PUT'
|
46
|
+
body = env['rack.input'].read
|
47
|
+
p [:put, path, body]
|
48
|
+
Firehose::Publisher.new.publish(path, body)
|
49
|
+
|
50
|
+
[202, {}, []]
|
51
|
+
else
|
52
|
+
[501, {}, ["#{method} not supported."]]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
def self.connection
|
58
|
+
@connection ||= AMQP.connect
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class App < ::Goliath::API
|
63
|
+
# TODO Figure out how to route this on schema (ws) or HTTP_UGPRADE header... it
|
64
|
+
# all uses HTTP router under the covers, so it should be doable.
|
65
|
+
map '/ws/*', WebSocket
|
66
|
+
map '/*', LongPolling
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "uri"
|
3
|
+
|
4
|
+
module Firehose
|
5
|
+
class HttpPublisher
|
6
|
+
attr_reader :uri
|
7
|
+
|
8
|
+
class RequestBuilder
|
9
|
+
attr_reader :uri, :message, :channel
|
10
|
+
|
11
|
+
def initialize(uri, message)
|
12
|
+
@uri, @message = uri, message
|
13
|
+
end
|
14
|
+
|
15
|
+
def to(channel)
|
16
|
+
req = Net::HTTP::Put.new(channel)
|
17
|
+
req.body = message
|
18
|
+
res = Net::HTTP.start(uri.host, uri.port) do |http|
|
19
|
+
response = http.request(req)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(uri)
|
25
|
+
@uri = URI.parse(uri)
|
26
|
+
end
|
27
|
+
|
28
|
+
def publish(message)
|
29
|
+
RequestBuilder.new(uri, message)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Firehose
|
2
|
+
class Publisher
|
3
|
+
def publish(path, message)
|
4
|
+
channel = AMQP::Channel.new(Firehose.amqp.connection)
|
5
|
+
exchange = AMQP::Exchange.new(channel, :fanout, path, :auto_delete => true)
|
6
|
+
# TODO How do I clean up this exchange at this point? Do I close it somehow or the channel?
|
7
|
+
# The exchange just hangs out indefinitely now.
|
8
|
+
exchange.publish(message)
|
9
|
+
exchange.delete(:if_unused => true)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'rack/websocket'
|
2
|
+
|
3
|
+
module Firehose
|
4
|
+
module Rack
|
5
|
+
AsyncResponse = [-1, {}, []]
|
6
|
+
|
7
|
+
class HttpLongPoll
|
8
|
+
def call(env)
|
9
|
+
req = ::Rack::Request.new(env)
|
10
|
+
cid = req.params['cid']
|
11
|
+
path = req.path
|
12
|
+
method = req.request_method
|
13
|
+
|
14
|
+
case method
|
15
|
+
# GET is how clients subscribe to the queue. When a messages comes in, we flush out a response,
|
16
|
+
# close down the requeust, and the client then reconnects.
|
17
|
+
when 'GET'
|
18
|
+
EM.next_tick do
|
19
|
+
subscription = Firehose::Subscription.new(cid)
|
20
|
+
subscription.subscribe path do |payload|
|
21
|
+
subscription.unsubscribe
|
22
|
+
env['async.callback'].call([200, {}, [payload]])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Firehose::Rack::AsyncResponse
|
27
|
+
|
28
|
+
# PUT is how we throw messages on to the fan-out queue.
|
29
|
+
when 'PUT'
|
30
|
+
body = env['rack.input'].read
|
31
|
+
p [:put, path, body]
|
32
|
+
Firehose::Publisher.new.publish(path, body)
|
33
|
+
|
34
|
+
[202, {}, []]
|
35
|
+
else
|
36
|
+
[501, {}, ["#{method} not supported."]]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class WebSocket < ::Rack::WebSocket::Application
|
42
|
+
# Subscribe to a path and make some magic happen, mmkmay?
|
43
|
+
def on_open(env)
|
44
|
+
req = ::Rack::Request.new(env)
|
45
|
+
cid = req.params['cid']
|
46
|
+
path = req.path
|
47
|
+
|
48
|
+
@subscription = Firehose::Subscription.new(cid)
|
49
|
+
@subscription.subscribe path do |payload|
|
50
|
+
send_data payload
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Delete the subscription if the thing even happened.
|
55
|
+
def on_close(env)
|
56
|
+
@subscription.unsubscribe if @subscription
|
57
|
+
end
|
58
|
+
|
59
|
+
# Log websocket level errors
|
60
|
+
def on_error(env, error)
|
61
|
+
@subscription.unsubscribe if @subscription
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class App
|
66
|
+
def call(env)
|
67
|
+
websocket_request?(env) ? WebSocket.new.call(env) : HttpLongPoll.new.call(env)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
def websocket_request?(env)
|
72
|
+
env['HTTP_UPGRADE'] =~ /websocket/i
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module Firehose
|
4
|
+
class Subscription
|
5
|
+
TTL = 15000
|
6
|
+
|
7
|
+
# Time to live for the queue on the server after the subscription is canceled. This
|
8
|
+
# is mostly for flakey connections where the client may reconnect after *ttl* and continue
|
9
|
+
# receiving messages.
|
10
|
+
attr_accessor :ttl
|
11
|
+
|
12
|
+
# Globally unique subscription id
|
13
|
+
attr_reader :subscriber_id
|
14
|
+
|
15
|
+
def initialize(subscriber_id=nil)
|
16
|
+
@subscriber_id = subscriber_id || self.class.subscriber_id
|
17
|
+
end
|
18
|
+
|
19
|
+
def subscribe(path, &block)
|
20
|
+
queue_name = "#{subscriber_id}@#{path}"
|
21
|
+
channel = AMQP::Channel.new(Firehose.amqp.connection).prefetch(1)
|
22
|
+
exchange = AMQP::Exchange.new(channel, :fanout, path, :auto_delete => true)
|
23
|
+
queue = AMQP::Queue.new(channel, queue_name, :arguments => {'x-expires' => ttl})
|
24
|
+
queue.bind(exchange)
|
25
|
+
|
26
|
+
# When we get a message, we want to remove the consumer from the queue so that the x-expires
|
27
|
+
# ttl starts ticking down. On the reconnect, the consumer connects to the queue and resets the
|
28
|
+
# timer on x-expires... in theory at least.
|
29
|
+
@consumer = AMQP::Consumer.new(channel, queue, subscriber_id)
|
30
|
+
@consumer.on_delivery do |metadata, payload|
|
31
|
+
p [:get, subscriber_id, @consumer.consumer_tag, path, payload]
|
32
|
+
block.call(payload)
|
33
|
+
# The ack needs to go after the block is called. This makes sure that all processing
|
34
|
+
# happens downstream before we remove it from the queue entirely.
|
35
|
+
metadata.ack
|
36
|
+
end.consume
|
37
|
+
end
|
38
|
+
|
39
|
+
def unsubscribe
|
40
|
+
@consumer.cancel if @consumer
|
41
|
+
end
|
42
|
+
|
43
|
+
# The time that a queue should live *after* the client unsubscribes. This is useful for
|
44
|
+
# flakey network connections, like HTTP Long Polling or even broken web sockets.
|
45
|
+
def ttl
|
46
|
+
@ttl ||= TTL
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
def self.subscriber_id
|
51
|
+
SecureRandom.uuid
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Firehose amqp resources" do
|
4
|
+
|
5
|
+
let(:channel) { "/resource-test-#{Time.now.to_i}" }
|
6
|
+
|
7
|
+
it "should clean up exchanges and queues" do
|
8
|
+
sent, received = 'howdy!', nil
|
9
|
+
|
10
|
+
before_exchange_count = `rabbitmqctl list_exchanges`.lines.count
|
11
|
+
before_queue_count = `rabbitmqctl list_queues`.lines.count
|
12
|
+
|
13
|
+
during_exchange_count = nil
|
14
|
+
during_queue_count = nil
|
15
|
+
|
16
|
+
EM.run do
|
17
|
+
# Kill test if it runs longer than 5s
|
18
|
+
EM.add_timer(5) { EM.stop }
|
19
|
+
|
20
|
+
subscription = Firehose::Subscription.new
|
21
|
+
subscription.ttl = 1
|
22
|
+
|
23
|
+
subscription.subscribe channel do |payload|
|
24
|
+
received = payload
|
25
|
+
subscription.unsubscribe
|
26
|
+
|
27
|
+
during_exchange_count = `rabbitmqctl list_exchanges`.lines.count
|
28
|
+
during_queue_count = `rabbitmqctl list_queues`.lines.count
|
29
|
+
|
30
|
+
# I wait 1 second before killing em so that unsubscribe
|
31
|
+
# can talk to AMQP before the whole thing dies.
|
32
|
+
EM.add_timer(1){ EM.stop }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Let the subscriber subscribe before publishing messages.
|
36
|
+
EM.add_timer(1){ Firehose::Publisher.new.publish(channel, sent) }
|
37
|
+
end
|
38
|
+
|
39
|
+
after_exchange_count = `rabbitmqctl list_exchanges`.lines.count
|
40
|
+
after_queue_count = `rabbitmqctl list_queues`.lines.count
|
41
|
+
|
42
|
+
received.should == sent
|
43
|
+
|
44
|
+
after_exchange_count.should == before_exchange_count
|
45
|
+
after_queue_count.should == before_queue_count
|
46
|
+
|
47
|
+
during_exchange_count.should == before_exchange_count + 1
|
48
|
+
during_queue_count.should == before_queue_count + 1
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'goliath'
|
3
|
+
require 'em-http'
|
4
|
+
|
5
|
+
describe Firehose::Goliath do
|
6
|
+
let(:app) { Firehose::Goliath::App.new }
|
7
|
+
let(:messages) { (1..1000).map(&:to_s) }
|
8
|
+
let(:channel) { "/firehose/integration/#{Time.now.to_i}" }
|
9
|
+
let(:uri) { URI.parse('http://127.0.0.1:9876') }
|
10
|
+
let(:url) { "#{uri}#{channel}" }
|
11
|
+
let(:cid) { "client-#{Time.now.to_i}" }
|
12
|
+
|
13
|
+
it "should pub-sub" do
|
14
|
+
outgoing, received = messages.dup, []
|
15
|
+
|
16
|
+
Goliath.env = :production
|
17
|
+
server = Goliath::Server.new
|
18
|
+
server.address = uri.host
|
19
|
+
server.port = uri.port
|
20
|
+
server.api = app
|
21
|
+
server.app = Goliath::Rack::Builder.build(Firehose::Goliath::App, server.api)
|
22
|
+
server.logger = Log4r::Logger.new('goliath')
|
23
|
+
|
24
|
+
server.start do
|
25
|
+
EM.add_timer(30) { EM.stop } # Stop the server no matter what happens.
|
26
|
+
|
27
|
+
publish = Proc.new do
|
28
|
+
http = EM::HttpRequest.new(url).put(:body => outgoing.pop)
|
29
|
+
http.errback { EM.stop }
|
30
|
+
http.callback { publish.call unless outgoing.empty? }
|
31
|
+
end
|
32
|
+
|
33
|
+
subscribe = Proc.new do
|
34
|
+
http = EM::HttpRequest.new(url).get(:query => {'cid' => cid})
|
35
|
+
http.errback { EM.stop }
|
36
|
+
http.callback do
|
37
|
+
received << http.response
|
38
|
+
if received.size < messages.size
|
39
|
+
subscribe.call
|
40
|
+
else
|
41
|
+
EM.stop
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Start the subscriber.
|
47
|
+
subscribe.call
|
48
|
+
|
49
|
+
# Wait a sec to let our subscribe setup.
|
50
|
+
EM.add_timer(1){ publish.call }
|
51
|
+
end
|
52
|
+
|
53
|
+
received.should =~ messages
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'thin'
|
3
|
+
require 'em-http'
|
4
|
+
|
5
|
+
describe Firehose::Rack do
|
6
|
+
let(:app) { Firehose::Rack::App.new }
|
7
|
+
let(:messages) { (1..1000).map(&:to_s) }
|
8
|
+
let(:channel) { "/firehose/integration/#{Time.now.to_i}" }
|
9
|
+
let(:uri) { URI.parse('http://127.0.0.1:9876') }
|
10
|
+
let(:url) { "#{uri}#{channel}" }
|
11
|
+
let(:ws_url) { "ws://#{uri.host}:#{uri.port}#{channel}" }
|
12
|
+
let(:cid) { "client-#{Time.now.to_i}" }
|
13
|
+
|
14
|
+
it "should pub-sub http and websockets" do
|
15
|
+
# Setup variables that we'll use after we turn off EM to validate our
|
16
|
+
# test assertions.
|
17
|
+
outgoing, received_http, received_ws = messages.dup, [], []
|
18
|
+
|
19
|
+
# Our WS and Http clients call this when they have received their messages to determine
|
20
|
+
# when to turn off EM and make the test assertion at the very bottom.
|
21
|
+
succeed = Proc.new do
|
22
|
+
EM.stop if received_http.size == messages.size and received_ws.size == messages.size
|
23
|
+
end
|
24
|
+
|
25
|
+
# Setup a publisher
|
26
|
+
publish = Proc.new do
|
27
|
+
http = EM::HttpRequest.new(url).put(:body => outgoing.pop)
|
28
|
+
http.errback { EM.stop }
|
29
|
+
http.callback { publish.call unless outgoing.empty? }
|
30
|
+
end
|
31
|
+
|
32
|
+
# Lets have an HTTP Long poll client
|
33
|
+
http_long_poll = Proc.new do
|
34
|
+
http = EM::HttpRequest.new(url).get(:query => {'cid' => cid})
|
35
|
+
http.errback { EM.stop }
|
36
|
+
http.callback do
|
37
|
+
received_http << http.response
|
38
|
+
if received_http.size < messages.size
|
39
|
+
http_long_poll.call
|
40
|
+
else
|
41
|
+
succeed.call
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# And test a web socket client too, at the same time.
|
47
|
+
websocket = Proc.new do
|
48
|
+
http = EventMachine::HttpRequest.new(ws_url).get
|
49
|
+
http.errback { EM.stop }
|
50
|
+
http.stream do |msg|
|
51
|
+
received_ws << msg
|
52
|
+
succeed.call unless received_ws.size < messages.size
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Great, we have all the pieces in order, lets run this thing in the reactor.
|
57
|
+
EM.run do
|
58
|
+
# Stop the server no matter what happens.
|
59
|
+
EM.add_timer(30) { EM.stop }
|
60
|
+
|
61
|
+
# Start the server
|
62
|
+
::Thin::Server.new(uri.host, uri.port, app).start
|
63
|
+
|
64
|
+
# Start the http_long_pollr.
|
65
|
+
http_long_poll.call
|
66
|
+
websocket.call
|
67
|
+
|
68
|
+
# Wait a sec to let our http_long_poll setup.
|
69
|
+
EM.add_timer(1){ publish.call }
|
70
|
+
end
|
71
|
+
|
72
|
+
# When EM stops, these assertions will be made.
|
73
|
+
received_http.should =~ messages
|
74
|
+
received_ws.should =~ messages
|
75
|
+
end
|
76
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'firehose'
|
2
|
+
|
3
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
4
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
5
|
+
# Require this file using `require "spec_helper.rb"` to ensure that it is only
|
6
|
+
# loaded once.
|
7
|
+
#
|
8
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
9
|
+
RSpec.configure do |config|
|
10
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
11
|
+
config.run_all_when_everything_filtered = true
|
12
|
+
config.filter_run :focus
|
13
|
+
config.before(:each) do
|
14
|
+
# For now, this resets the AMQP configuration between runs.
|
15
|
+
Firehose.reset!
|
16
|
+
end
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: firehose
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Brad Gessler
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-04-16 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: eventmachine
|
16
|
+
requirement: &70282339770020 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.0.0.beta
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70282339770020
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: amqp
|
27
|
+
requirement: &70282339769280 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.9.4
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70282339769280
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: thin
|
38
|
+
requirement: &70282339768720 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70282339768720
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: websocket-rack
|
49
|
+
requirement: &70282339768120 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70282339768120
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: rspec
|
60
|
+
requirement: &70282339767660 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70282339767660
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rack-test
|
71
|
+
requirement: &70282339767140 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *70282339767140
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: guard-rspec
|
82
|
+
requirement: &70282339766600 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: *70282339766600
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: guard-bundler
|
93
|
+
requirement: &70282339765920 !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
type: :development
|
100
|
+
prerelease: false
|
101
|
+
version_requirements: *70282339765920
|
102
|
+
- !ruby/object:Gem::Dependency
|
103
|
+
name: thin
|
104
|
+
requirement: &70282339752660 !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: *70282339752660
|
113
|
+
- !ruby/object:Gem::Dependency
|
114
|
+
name: em-http-request
|
115
|
+
requirement: &70282339751820 !ruby/object:Gem::Requirement
|
116
|
+
none: false
|
117
|
+
requirements:
|
118
|
+
- - ~>
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: 0.3.0
|
121
|
+
type: :development
|
122
|
+
prerelease: false
|
123
|
+
version_requirements: *70282339751820
|
124
|
+
- !ruby/object:Gem::Dependency
|
125
|
+
name: guard-coffeescript
|
126
|
+
requirement: &70282339750760 !ruby/object:Gem::Requirement
|
127
|
+
none: false
|
128
|
+
requirements:
|
129
|
+
- - ! '>='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: *70282339750760
|
135
|
+
description: Firehose is a realtime web application toolkit for building realtime
|
136
|
+
Ruby web applications.
|
137
|
+
email:
|
138
|
+
- brad@bradgessler.com
|
139
|
+
executables:
|
140
|
+
- firehose
|
141
|
+
- firehose-test
|
142
|
+
extensions: []
|
143
|
+
extra_rdoc_files: []
|
144
|
+
files:
|
145
|
+
- .gitignore
|
146
|
+
- .rbenv-version
|
147
|
+
- .rspec
|
148
|
+
- Gemfile
|
149
|
+
- Procfile
|
150
|
+
- README.md
|
151
|
+
- Rakefile
|
152
|
+
- bin/firehose
|
153
|
+
- bin/firehose-test
|
154
|
+
- config.ru
|
155
|
+
- firehose.gemspec
|
156
|
+
- lib/assets/flash/WebSocketMain.swf
|
157
|
+
- lib/firehose.rb
|
158
|
+
- lib/firehose/goliath.rb
|
159
|
+
- lib/firehose/http_publisher.rb
|
160
|
+
- lib/firehose/publisher.rb
|
161
|
+
- lib/firehose/rack.rb
|
162
|
+
- lib/firehose/subscription.rb
|
163
|
+
- lib/firehose/thin.rb
|
164
|
+
- lib/firehose/version.rb
|
165
|
+
- spec/integrations/amqp_resources_spec.rb
|
166
|
+
- spec/integrations/goliath_spec.rb
|
167
|
+
- spec/integrations/thin_spec.rb
|
168
|
+
- spec/spec_helper.rb
|
169
|
+
homepage: ''
|
170
|
+
licenses: []
|
171
|
+
post_install_message:
|
172
|
+
rdoc_options: []
|
173
|
+
require_paths:
|
174
|
+
- lib
|
175
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
176
|
+
none: false
|
177
|
+
requirements:
|
178
|
+
- - ! '>='
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
182
|
+
none: false
|
183
|
+
requirements:
|
184
|
+
- - ! '>='
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: '0'
|
187
|
+
requirements: []
|
188
|
+
rubyforge_project: firehose
|
189
|
+
rubygems_version: 1.8.11
|
190
|
+
signing_key:
|
191
|
+
specification_version: 3
|
192
|
+
summary: Build realtime Ruby web applications
|
193
|
+
test_files:
|
194
|
+
- spec/integrations/amqp_resources_spec.rb
|
195
|
+
- spec/integrations/goliath_spec.rb
|
196
|
+
- spec/integrations/thin_spec.rb
|
197
|
+
- spec/spec_helper.rb
|