firehose 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|