firehose 0.0.16 → 0.1.0

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/.rspec CHANGED
@@ -1 +1 @@
1
- --colour
1
+ --colour --backtrace
data/README.md CHANGED
@@ -13,11 +13,11 @@ Firehose is both a Rack application and JavasScript library that makes building
13
13
 
14
14
  # Getting Started
15
15
 
16
- First, you'll need to install and run RabbitMQ.
16
+ First, you'll need to install and run Redis.
17
17
 
18
18
  ```sh
19
- $ apt-get install rabbitmq # Install on Ubuntu
20
- $ brew install rabbitmq # Install on Mac Homebrew
19
+ $ apt-get install redis # Install on Ubuntu
20
+ $ brew install redis # Install on Mac Homebrew
21
21
  ```
22
22
 
23
23
  Then install the gem.
@@ -34,7 +34,7 @@ Now fire up the server.
34
34
  $ firehose server
35
35
  >> Thin web server (v1.3.1 codename Triple Espresso)
36
36
  >> Maximum connections set to 1024
37
- >> Listening on 127.0.0.1:7478, CTRL+C to stop
37
+ >> Listening on 127.0.0.1:7474, CTRL+C to stop
38
38
  ```
39
39
 
40
40
  In case you're wondering, the Firehose application server runs the Rack app `Firehose::Rack::App.new` inside of Thin.
@@ -87,15 +87,14 @@ new Firehose.Consumer({
87
87
  },
88
88
  // Note that we do NOT specify a protocol here because we don't
89
89
  // know that yet.
90
- uri: '//localhost:7478/hello'
90
+ uri: '//localhost:7474/hello'
91
91
  }).connect();
92
92
  ```
93
93
 
94
94
  Then publish another message.
95
95
 
96
-
97
96
  ```sh
98
- $ curl -X PUT -d "This is almost magical" "http://localhost:7478/hello"
97
+ $ curl -X PUT -d "\"This is almost magical\"" "http://localhost:7474/hello"
99
98
  ```
100
99
 
101
100
  # How is it different from socket.io?
data/firehose.gemspec CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |s|
20
20
 
21
21
  # specify any dependencies here; for example:
22
22
  s.add_runtime_dependency "eventmachine", ">= 1.0.0.beta"
23
- s.add_runtime_dependency "amqp", ">= 0.9.4"
23
+ s.add_runtime_dependency "em-hiredis"
24
24
  s.add_runtime_dependency "thin"
25
25
  s.add_runtime_dependency "thor"
26
26
  s.add_runtime_dependency "faraday"
data/lib/firehose.rb CHANGED
@@ -1,31 +1,16 @@
1
1
  require 'firehose/version'
2
2
 
3
- require 'amqp'
3
+ require 'em-hiredis'
4
4
  require 'logger'
5
5
 
6
6
  module Firehose
7
7
  autoload :Subscription, 'firehose/subscription'
8
8
  autoload :Publisher, 'firehose/publisher'
9
9
  autoload :Producer, 'firehose/producer'
10
- autoload :Consumer, 'firehose/consumer'
11
10
  autoload :Default, 'firehose/default'
12
- autoload :Broker, 'firehose/broker'
13
11
  autoload :Rack, 'firehose/rack'
14
12
  autoload :CLI, 'firehose/cli'
15
13
 
16
- # TODO move this into a configuration or session class.
17
- # Hang on to AMQP configuration settings.
18
- def self.amqp
19
- @amqp ||= Struct.new(:connection).new(AMQP.connect)
20
- end
21
-
22
- # TODO figure out a better way to memoize AMQP connection for production runtimes, and
23
- # make it resetable for testing environment. Some sort of Firehose::Session object is probably
24
- # in order
25
- def self.reset!
26
- @amqp = nil
27
- end
28
-
29
14
  # Logging
30
15
  def self.logger
31
16
  @logger ||= Logger.new($stdout)
data/lib/firehose/cli.rb CHANGED
@@ -8,27 +8,15 @@ module Firehose
8
8
  method_option :host, :type => :string, :default => '0.0.0.0', :required => true, :aliases => '-h'
9
9
 
10
10
  def server
11
- broker = Firehose::Broker.new
12
-
13
11
  server = Thin::Server.new(options[:host], options[:port]) do
14
- # TODO move this into a socket... this is really janky, but I need to to troubleshoot
15
- # connection reference issues. I'd like to have this ancillary stuff accessiable via
16
- # a different port or even a socket.
17
- map '/_firehose/stats' do
18
- run Proc.new {
19
- [200, {'Content-Type' => 'text/plain'}, [broker.stats.inspect]]
20
- }
21
- end
22
-
23
- map '/' do
24
- run Firehose::Rack::App.new(broker)
25
- end
12
+ run Firehose::Rack::App.new
26
13
  end
27
14
 
28
15
  begin
29
16
  server.start!
30
- rescue AMQP::TCPConnectionFailed => e
31
- Firehose.logger.error "Unable to connect to AMQP, are you sure it's running?"
17
+ rescue RuntimeError
18
+ Firehose.logger.error "Unable to connect to Redis, are you sure it's running?"
19
+ raise
32
20
  end
33
21
  end
34
22
  end
@@ -1,13 +1,13 @@
1
1
  module Firehose
2
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
- Firehose.logger.debug "AMQP publishing `#{message}` to `#{path}`"
9
- exchange.publish(message)
10
- exchange.delete(:if_unused => true)
3
+ def publish(channel, message)
4
+ Firehose.logger.debug "Redis publishing `#{message}` to `#{channel}`"
5
+ redis.publish(channel, message).errback { raise 'Error publishing' }
6
+ end
7
+
8
+ private
9
+ def redis
10
+ @redis ||= EM::Hiredis.connect
11
11
  end
12
12
  end
13
13
  end
data/lib/firehose/rack.rb CHANGED
@@ -5,16 +5,14 @@ module Firehose
5
5
  AsyncResponse = [-1, {}, []]
6
6
 
7
7
  class HttpLongPoll
8
- def initialize(broker)
9
- @broker = broker
10
- end
11
-
12
8
  def call(env)
13
9
  req = ::Rack::Request.new(env)
14
10
  cid = req.params['cid']
15
11
  path = req.path
16
12
  method = req.request_method
17
13
  timeout = 30
14
+ queue_name = "#{cid}@#{path}"
15
+
18
16
  # TODO seperate out CORS logic as an async middleware with a Goliath web server.
19
17
  cors_origin = env['HTTP_ORIGIN']
20
18
  cors_headers = {
@@ -33,20 +31,23 @@ module Firehose
33
31
  response_headers = cors_origin ? cors_headers : {}
34
32
 
35
33
  # Setup a subscription with a client id. We haven't subscribed yet here.
36
- consumer = @broker.consumer(cid)
34
+ if queue = queues[queue_name]
35
+ queue.live
36
+ else
37
+ queue = queues[queue_name] = Firehose::Subscription::Queue.new(cid, path)
38
+ end
37
39
 
38
40
  # Setup a timeout timer to tell clients that time out that everything is OK
39
41
  # and they should come back for more
40
- timer = EventMachine::Timer.new(timeout) do
42
+ long_poll_timer = EM::Timer.new(timeout) do
41
43
  # We send a 204 OK to tell the client to reconnect.
42
44
  env['async.callback'].call [204, response_headers, []]
43
45
  Firehose.logger.debug "HTTP wait `#{cid}@#{path}` timed out"
44
46
  end
45
47
 
46
48
  # Ok, now subscribe to the subscription.
47
- consumer.subscribe_to path do |message, subscription|
48
- timer.cancel # Turn off the heart beat so we don't execute any of that business.
49
- consumer.unsubscribe
49
+ queue.pop do |message, subscription|
50
+ long_poll_timer.cancel # Turn off the heart beat so we don't execute any of that business.
50
51
  env['async.callback'].call [200, response_headers, [message]]
51
52
  Firehose.logger.debug "HTTP sent `#{message}` to `#{cid}@#{path}`"
52
53
  end
@@ -55,7 +56,11 @@ module Firehose
55
56
  # Unsubscribe from the subscription if its still open and something bad happened
56
57
  # or the heart beat triggered before we could finish.
57
58
  env['async.close'].callback do
58
- consumer.unsubscribe if consumer
59
+ # Kill queue if we don't hear back in 30s
60
+ queue.kill timeout do
61
+ Firehose.logger.debug "Deleting queue to `#{queue_name}`"
62
+ queues.delete queue_name
63
+ end
59
64
  Firehose.logger.debug "HTTP connection `#{cid}@#{path}` closing"
60
65
  end
61
66
  end
@@ -67,7 +72,7 @@ module Firehose
67
72
  when 'PUT'
68
73
  body = env['rack.input'].read
69
74
  Firehose.logger.debug "HTTP published `#{body}` to `#{path}`"
70
- Firehose::Publisher.new.publish(path, body)
75
+ publisher.publish(path, body)
71
76
 
72
77
  [202, {}, []]
73
78
  else
@@ -75,23 +80,28 @@ module Firehose
75
80
  [501, {'Content-Type' => 'text/plain'}, ["#{method} not supported."]]
76
81
  end
77
82
  end
83
+
84
+ private
85
+ def publisher
86
+ @publisher ||= Firehose::Publisher.new
87
+ end
88
+
89
+ def queues
90
+ @queues ||= {}
91
+ end
78
92
  end
79
93
 
80
94
  class WebSocket < ::Rack::WebSocket::Application
81
- attr_reader :cid, :path
95
+ attr_reader :cid, :path, :subscription
82
96
 
83
- def initialize(broker)
84
- @broker = broker
85
- end
86
-
87
97
  # Subscribe to a path and make some magic happen, mmkmay?
88
98
  def on_open(env)
89
99
  req = ::Rack::Request.new(env)
90
100
  @cid = req.params['cid']
91
101
  @path = req.path
102
+ @subscription = Firehose::Subscription.new(cid, path)
92
103
 
93
- @consumer = @broker.consumer(@cid)
94
- @consumer.subscribe_to path do |message|
104
+ subscription.subscribe do |message, subscription|
95
105
  Firehose.logger.debug "WS sent `#{message}` to `#{cid}@#{path}`"
96
106
  send_data message
97
107
  end
@@ -100,37 +110,28 @@ module Firehose
100
110
 
101
111
  # Delete the subscription if the thing even happened.
102
112
  def on_close(env)
103
- @consumer.unsubscribe if @consumer
113
+ subscription.unsubscribe
104
114
  Firehose.logger.debug "WS connection `#{cid}@#{path}` closing"
105
115
  end
106
116
 
107
117
  # Log websocket level errors
108
118
  def on_error(env, error)
109
- Firehose.logger.error "WS connection `#{cid}@#{path}` error `#{error}`: #{env.inspect}"
110
- @consumer.unsubscribe if @consumer
119
+ Firehose.logger.error "WS connection `#{cid}@#{path}` error `#{error}`: #{error.backtrace}"
111
120
  end
112
121
  end
113
122
 
114
123
  class App
115
- # Firehose broker that will be used to pub/sub messages.
116
- attr_reader :broker
117
-
118
- # Fire up a default broker if one is not specified.
119
- def initialize(broker = Firehose::Broker.new)
120
- @broker = broker
121
- end
122
-
123
124
  def call(env)
124
125
  websocket_request?(env) ? websocket.call(env) : http_long_poll.call(env)
125
126
  end
126
127
 
127
128
  private
128
129
  def websocket
129
- @websocket ||= WebSocket.new(broker)
130
+ WebSocket.new
130
131
  end
131
132
 
132
133
  def http_long_poll
133
- @http_long_poll ||= HttpLongPoll.new(broker)
134
+ @http_long_poll ||= HttpLongPoll.new
134
135
  end
135
136
 
136
137
  def websocket_request?(env)
@@ -1,65 +1,75 @@
1
1
  module Firehose
2
2
  class Subscription
3
- # Default TTL for how long a subscription should live on the server when the
4
- # consumer disconnects.
5
- # TODO should the Consumer handle TTL?
6
- TTL = 15000
7
-
8
- # Time to live for the amqp_queue on the server after the subscription is canceled. This
9
- # is mostly for flakey connections where the client may reconnect after *ttl* and continue
10
- # receiving messages.
11
- attr_accessor :ttl
12
-
13
- # Consumer and channel for the subscription.
14
- attr_reader :consumer
3
+ # consumer_id and channel for the subscription.
4
+ attr_reader :consumer_id
15
5
 
16
6
  # Channel that we'll use for the pub-sub activity. This probably maps to an URL
17
7
  attr_reader :channel
18
8
 
19
- def initialize(consumer, channel)
20
- @consumer, @channel = consumer, channel
9
+ def initialize(consumer_id, channel)
10
+ @consumer_id, @channel = consumer_id, channel
21
11
  end
22
12
 
23
- # TODO - Move the channel to an initializer so that we can force on AMQP subscription per one
24
- # Firehose subscription. As it stands now, you could fire off multple subscriptions to diff amqp_channels
13
+ # Subscribe to messages on the backend to fill up the subscription queue. consumer_ids of the messages
14
+ # will queue up units of "work" to process data from the subscription.
25
15
  def subscribe(&block)
26
- amqp_queue_name = "#{consumer.guid}@#{channel}"
27
- amqp_channel = AMQP::Channel.new(Firehose.amqp.connection).prefetch(1)
28
- amqp_exchange = AMQP::Exchange.new(amqp_channel, :fanout, channel, :auto_delete => true)
29
- amqp_queue = AMQP::Queue.new(amqp_channel, amqp_queue_name, :arguments => {'x-expires' => ttl})
30
- amqp_queue.bind(amqp_exchange)
31
-
32
- # When we get a message, we want to remove the consumer from the amqp_queue so that the x-expires
33
- # ttl starts ticking down. On the reconnect, the consumer connects to the amqp_queue and resets the
34
- # timer on x-expires... in theory at least.
35
- @amqp_consumer = AMQP::Consumer.new(amqp_channel, amqp_queue, consumer.guid)
36
- @amqp_consumer.on_delivery do |metadata, message|
37
- Firehose.logger.debug "AMQP delivering `#{message}` to `#{consumer.guid}@#{channel}`"
38
- block.call(message, self)
39
- # The ack needs to go after the block is called. This makes sure that all processing
40
- # happens downstream before we remove it from the amqp_queue entirely.
41
- metadata.ack
42
- end.consume
43
- Firehose.logger.debug "AMQP subscribed to `#{consumer.guid}@#{channel}`"
16
+ redis.subscribe(channel)
17
+ redis.on(:message) do |channel, message|
18
+ Firehose.logger.debug "Redis recieved `#{message}` to `#{consumer_id}@#{channel}`"
19
+ block.call message, self
20
+ end
21
+ Firehose.logger.debug "Redis subscribed to `#{consumer_id}@#{channel}`"
44
22
  self # Return the subscription for chaining.
45
23
  end
46
24
 
47
- def unsubscribe
48
- Firehose.logger.debug "AMQP unsubscribed"
49
- @amqp_consumer.cancel if @amqp_consumer
50
- @unsubscribe_callback.call self if @unsubscribe_callback
25
+ def unsubscribe(&block)
26
+ Firehose.logger.debug "Redis unsubscribed from `#{consumer_id}@#{channel}`"
27
+ redis.close
28
+ block.call(self) if block
29
+ self
30
+ end
31
+
32
+ private
33
+ def redis
34
+ @redis ||= EM::Hiredis.connect
35
+ end
36
+ end
37
+
38
+ # Queue subscription messages so that we can remember and/or operate on them
39
+ class Subscription::Queue
40
+ attr_reader :subscription, :channel
41
+
42
+ def initialize(consumer_id, channel)
43
+ @subscription = Subscription.new(consumer_id, channel)
44
+ # Start the subscription and start dropping mesasge onto the queue
45
+ subscription.subscribe do |message|
46
+ queue.push message
47
+ end
48
+ end
49
+
50
+ # Pop an item off the subscription queue so we can work on it.
51
+ def pop(&block)
52
+ queue.pop do |message|
53
+ block.call message, subscription
54
+ end
55
+ end
56
+
57
+ # Kill the queue in n seconds.
58
+ def kill(ttl=0, &block)
59
+ if ttl.zero?
60
+ subscription.unsubscribe &block
61
+ else
62
+ @timer = EM::Timer.new(ttl){ kill 0 }
63
+ end
51
64
  end
52
65
 
53
- # Callback when consumer unsubscribes from subscription. The consumer uses this to remove
54
- # subscriptions from itself when an unsubscribe happens.
55
- def on_unsubscribe(&block)
56
- @unsubscribe_callback = block
66
+ def live
67
+ @timer.cancel if @timer
57
68
  end
58
69
 
59
- # The time that a amqp_queue should live *after* the client unsubscribes. This is useful for
60
- # flakey network connections, like HTTP Long Polling or even broken web sockets.
61
- def ttl
62
- @ttl ||= TTL
70
+ private
71
+ def queue
72
+ @queue ||= EM::Queue.new
63
73
  end
64
74
  end
65
75
  end
@@ -1,4 +1,4 @@
1
1
  module Firehose
2
- VERSION = "0.0.16"
3
- CODENAME = "Statistical Sam"
2
+ VERSION = "0.1.0"
3
+ CODENAME = "Redis Rodeo"
4
4
  end
@@ -12,12 +12,11 @@ describe Firehose::Rack do
12
12
  end
13
13
 
14
14
  let(:app) { Firehose::Rack::App.new }
15
- let(:messages) { (1..1000).map(&:to_s) }
15
+ let(:messages) { (1..2000).map(&:to_s) }
16
16
  let(:channel) { "/firehose/integration/#{Time.now.to_i}" }
17
17
  let(:uri) { Firehose::Default::URI }
18
18
  let(:http_url) { "http://#{uri.host}:#{uri.port}#{channel}" }
19
19
  let(:ws_url) { "ws://#{uri.host}:#{uri.port}#{channel}" }
20
- let(:cid) { "client-#{Time.now.to_i}" }
21
20
 
22
21
  it "should pub-sub http and websockets" do
23
22
  # Setup variables that we'll use after we turn off EM to validate our
@@ -39,7 +38,7 @@ describe Firehose::Rack do
39
38
 
40
39
  # Lets have an HTTP Long poll client
41
40
  http_long_poll = Proc.new do
42
- http = EM::HttpRequest.new(http_url).get(:query => {'cid' => cid})
41
+ http = EM::HttpRequest.new(http_url).get(:query => {'cid' => 'alpha'})
43
42
  http.errback { em.stop }
44
43
  http.callback do
45
44
  received_http << http.response
@@ -53,7 +52,7 @@ describe Firehose::Rack do
53
52
 
54
53
  # And test a web socket client too, at the same time.
55
54
  websocket = Proc.new do
56
- ws = EventMachine::WebSocketClient.connect(ws_url)
55
+ ws = EventMachine::WebSocketClient.connect("#{ws_url}?cid=bravo")
57
56
  ws.errback { em.stop }
58
57
  ws.stream do |msg|
59
58
  received_ws << msg
@@ -64,7 +63,8 @@ describe Firehose::Rack do
64
63
  # Great, we have all the pieces in order, lets run this thing in the reactor.
65
64
  em do
66
65
  # Start the server
67
- ::Thin::Server.new('0.0.0.0', uri.port, app).start
66
+ server = ::Thin::Server.new('0.0.0.0', uri.port, app)
67
+ server.start
68
68
 
69
69
  # Start the http_long_pollr.
70
70
  http_long_poll.call
@@ -75,7 +75,7 @@ describe Firehose::Rack do
75
75
  end
76
76
 
77
77
  # When EM stops, these assertions will be made.
78
- received_http.should =~ messages
79
78
  received_ws.should =~ messages
79
+ received_http.should =~ messages
80
80
  end
81
81
  end
@@ -1,30 +1,30 @@
1
- require 'spec_helper'
1
+ # require 'spec_helper'
2
2
 
3
- describe Firehose::Broker do
4
- include EM::TestHelper
3
+ # describe Firehose::Broker do
4
+ # include EM::TestHelper
5
5
 
6
- let(:broker) { Firehose::Broker.new }
6
+ # let(:broker) { Firehose::Broker.new }
7
7
 
8
- it "should unsubscibe consumers and remove them from the collection" do
9
- stats = nil
8
+ # it "should unsubscibe consumers and remove them from the collection" do
9
+ # stats = nil
10
10
 
11
- em do
12
- broker.consumer('1').subscribe_to('/the-channel')
13
- broker.consumer('2').subscribe_to('/the-channel')
14
- broker.consumer('2').subscribe_to('/a-channel')
11
+ # em do
12
+ # broker.consumer('1').subscribe_to('/the-channel')
13
+ # broker.consumer('2').subscribe_to('/the-channel')
14
+ # broker.consumer('2').subscribe_to('/a-channel')
15
15
 
16
- em.add_timer(1) do
17
- stats = broker.stats
18
- broker.stop
19
- em.stop
20
- end
21
- end
16
+ # em.add_timer(1) do
17
+ # stats = broker.stats
18
+ # broker.stop
19
+ # em.stop
20
+ # end
21
+ # end
22
22
 
23
- stats.should == {
24
- '1' => {'subscriptions' => ['/the-channel'] },
25
- '2' => {'subscriptions' => ['/the-channel', '/a-channel']}
26
- }
23
+ # stats.should == {
24
+ # '1' => {'subscriptions' => ['/the-channel'] },
25
+ # '2' => {'subscriptions' => ['/the-channel', '/a-channel']}
26
+ # }
27
27
 
28
- broker.stats.should == {}
29
- end
30
- end
28
+ # broker.stats.should == {}
29
+ # end
30
+ # end
@@ -1,66 +1,66 @@
1
- require 'spec_helper'
1
+ # require 'spec_helper'
2
2
 
3
- describe Firehose::Consumer do
4
- include EM::TestHelper
3
+ # describe Firehose::Consumer do
4
+ # include EM::TestHelper
5
5
 
6
- let(:consumer) { Firehose::Consumer.new }
7
- let(:publisher) { Firehose::Publisher.new }
8
- let(:channel) { '/papa-smurf' }
9
- let(:another_channel) { '/mama-smurf' }
6
+ # let(:consumer) { Firehose::Consumer.new }
7
+ # let(:publisher) { Firehose::Publisher.new }
8
+ # let(:channel) { '/papa-smurf' }
9
+ # let(:another_channel) { '/mama-smurf' }
10
10
 
11
- describe "subscriptions" do
12
- it "should subscribe to channel" do
13
- sent, recieved = 'hi', nil
11
+ # describe "subscriptions" do
12
+ # it "should subscribe to channel" do
13
+ # sent, recieved = 'hi', nil
14
14
 
15
- em do
16
- consumer.subscribe_to channel do |msg|
17
- recieved = msg
18
- em.stop
19
- end
20
- em.add_timer(1) do
21
- publisher.publish(channel, sent)
22
- end
23
- end
15
+ # em do
16
+ # consumer.subscribe_to channel do |msg|
17
+ # recieved = msg
18
+ # em.stop
19
+ # end
20
+ # em.add_timer(1) do
21
+ # publisher.publish(channel, sent)
22
+ # end
23
+ # end
24
24
 
25
- recieved.should == sent
26
- end
25
+ # recieved.should == sent
26
+ # end
27
27
 
28
- it "should track subscriptions" do
29
- lambda{
30
- em do
31
- consumer.subscribe_to channel
32
- consumer.subscribe_to another_channel
33
- em.add_timer(1){ em.stop }
34
- end
35
- }.should change{ consumer.subscriptions.size }.by(2)
36
- end
28
+ # it "should track subscriptions" do
29
+ # lambda{
30
+ # em do
31
+ # consumer.subscribe_to channel
32
+ # consumer.subscribe_to another_channel
33
+ # em.add_timer(1){ em.stop }
34
+ # end
35
+ # }.should change{ consumer.subscriptions.size }.by(2)
36
+ # end
37
37
 
38
- it "should only allow one subscription per channel" do
39
- lambda{
40
- em do
41
- 3.times { consumer.subscribe_to channel }
42
- em.add_timer(1){ em.stop }
43
- end
44
- }.should change{ consumer.subscriptions.size }.by(1)
45
- end
38
+ # it "should only allow one subscription per channel" do
39
+ # lambda{
40
+ # em do
41
+ # 3.times { consumer.subscribe_to channel }
42
+ # em.add_timer(1){ em.stop }
43
+ # end
44
+ # }.should change{ consumer.subscriptions.size }.by(1)
45
+ # end
46
46
 
47
- it "should unsubscribe from all channels" do
48
- subscribed_count, after_unsubscribe_count = 0, nil
47
+ # it "should unsubscribe from all channels" do
48
+ # subscribed_count, after_unsubscribe_count = 0, nil
49
49
 
50
- em do
51
- consumer.subscribe_to channel
52
- consumer.subscribe_to another_channel
53
- subscribed_count = consumer.subscriptions.size
54
- em.add_timer(1) do
55
- consumer.unsubscribe
56
- em.add_timer(1) do
57
- em.stop
58
- end
59
- end
60
- end
50
+ # em do
51
+ # consumer.subscribe_to channel
52
+ # consumer.subscribe_to another_channel
53
+ # subscribed_count = consumer.subscriptions.size
54
+ # em.add_timer(1) do
55
+ # consumer.unsubscribe
56
+ # em.add_timer(1) do
57
+ # em.stop
58
+ # end
59
+ # end
60
+ # end
61
61
 
62
- subscribed_count.should == 2
63
- consumer.subscriptions.size.should == 0
64
- end
65
- end
66
- end
62
+ # subscribed_count.should == 2
63
+ # consumer.subscriptions.size.should == 0
64
+ # end
65
+ # end
66
+ # end
data/spec/spec_helper.rb CHANGED
@@ -2,6 +2,7 @@ require 'logger'
2
2
  require 'em-http'
3
3
  require 'em-websocket-client'
4
4
 
5
+ # Skip logging if VERBOSE isn't set to true.
5
6
  require 'firehose'
6
7
  Firehose.logger = Logger.new('/dev/null') unless ENV['VERBOSE']
7
8
 
@@ -37,8 +38,4 @@ RSpec.configure do |config|
37
38
  config.treat_symbols_as_metadata_keys_with_true_values = true
38
39
  config.run_all_when_everything_filtered = true
39
40
  config.filter_run :focus
40
- config.before(:each) do
41
- # For now, this resets the AMQP configuration between runs.
42
- Firehose.reset!
43
- end
44
41
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: firehose
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.16
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,11 +10,11 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-05-04 00:00:00.000000000 Z
13
+ date: 2012-05-09 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: eventmachine
17
- requirement: &70349732213400 !ruby/object:Gem::Requirement
17
+ requirement: &70097385746880 !ruby/object:Gem::Requirement
18
18
  none: false
19
19
  requirements:
20
20
  - - ! '>='
@@ -22,21 +22,21 @@ dependencies:
22
22
  version: 1.0.0.beta
23
23
  type: :runtime
24
24
  prerelease: false
25
- version_requirements: *70349732213400
25
+ version_requirements: *70097385746880
26
26
  - !ruby/object:Gem::Dependency
27
- name: amqp
28
- requirement: &70349732212540 !ruby/object:Gem::Requirement
27
+ name: em-hiredis
28
+ requirement: &70097385746460 !ruby/object:Gem::Requirement
29
29
  none: false
30
30
  requirements:
31
31
  - - ! '>='
32
32
  - !ruby/object:Gem::Version
33
- version: 0.9.4
33
+ version: '0'
34
34
  type: :runtime
35
35
  prerelease: false
36
- version_requirements: *70349732212540
36
+ version_requirements: *70097385746460
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: thin
39
- requirement: &70349732211460 !ruby/object:Gem::Requirement
39
+ requirement: &70097385746000 !ruby/object:Gem::Requirement
40
40
  none: false
41
41
  requirements:
42
42
  - - ! '>='
@@ -44,10 +44,10 @@ dependencies:
44
44
  version: '0'
45
45
  type: :runtime
46
46
  prerelease: false
47
- version_requirements: *70349732211460
47
+ version_requirements: *70097385746000
48
48
  - !ruby/object:Gem::Dependency
49
49
  name: thor
50
- requirement: &70349732210400 !ruby/object:Gem::Requirement
50
+ requirement: &70097385745580 !ruby/object:Gem::Requirement
51
51
  none: false
52
52
  requirements:
53
53
  - - ! '>='
@@ -55,10 +55,10 @@ dependencies:
55
55
  version: '0'
56
56
  type: :runtime
57
57
  prerelease: false
58
- version_requirements: *70349732210400
58
+ version_requirements: *70097385745580
59
59
  - !ruby/object:Gem::Dependency
60
60
  name: faraday
61
- requirement: &70349732209120 !ruby/object:Gem::Requirement
61
+ requirement: &70097385745160 !ruby/object:Gem::Requirement
62
62
  none: false
63
63
  requirements:
64
64
  - - ! '>='
@@ -66,10 +66,10 @@ dependencies:
66
66
  version: '0'
67
67
  type: :runtime
68
68
  prerelease: false
69
- version_requirements: *70349732209120
69
+ version_requirements: *70097385745160
70
70
  - !ruby/object:Gem::Dependency
71
71
  name: websocket-rack
72
- requirement: &70349732223780 !ruby/object:Gem::Requirement
72
+ requirement: &70097385744740 !ruby/object:Gem::Requirement
73
73
  none: false
74
74
  requirements:
75
75
  - - ! '>='
@@ -77,10 +77,10 @@ dependencies:
77
77
  version: '0'
78
78
  type: :runtime
79
79
  prerelease: false
80
- version_requirements: *70349732223780
80
+ version_requirements: *70097385744740
81
81
  - !ruby/object:Gem::Dependency
82
82
  name: em-http-request
83
- requirement: &70349732222860 !ruby/object:Gem::Requirement
83
+ requirement: &70097385744240 !ruby/object:Gem::Requirement
84
84
  none: false
85
85
  requirements:
86
86
  - - ~>
@@ -88,10 +88,10 @@ dependencies:
88
88
  version: 1.0.0
89
89
  type: :runtime
90
90
  prerelease: false
91
- version_requirements: *70349732222860
91
+ version_requirements: *70097385744240
92
92
  - !ruby/object:Gem::Dependency
93
93
  name: rspec
94
- requirement: &70349732221260 !ruby/object:Gem::Requirement
94
+ requirement: &70097385743820 !ruby/object:Gem::Requirement
95
95
  none: false
96
96
  requirements:
97
97
  - - ! '>='
@@ -99,10 +99,10 @@ dependencies:
99
99
  version: '0'
100
100
  type: :development
101
101
  prerelease: false
102
- version_requirements: *70349732221260
102
+ version_requirements: *70097385743820
103
103
  - !ruby/object:Gem::Dependency
104
104
  name: webmock
105
- requirement: &70349732220520 !ruby/object:Gem::Requirement
105
+ requirement: &70097385743360 !ruby/object:Gem::Requirement
106
106
  none: false
107
107
  requirements:
108
108
  - - ! '>='
@@ -110,10 +110,10 @@ dependencies:
110
110
  version: '0'
111
111
  type: :development
112
112
  prerelease: false
113
- version_requirements: *70349732220520
113
+ version_requirements: *70097385743360
114
114
  - !ruby/object:Gem::Dependency
115
115
  name: guard-rspec
116
- requirement: &70349732219740 !ruby/object:Gem::Requirement
116
+ requirement: &70097385742940 !ruby/object:Gem::Requirement
117
117
  none: false
118
118
  requirements:
119
119
  - - ! '>='
@@ -121,10 +121,10 @@ dependencies:
121
121
  version: '0'
122
122
  type: :development
123
123
  prerelease: false
124
- version_requirements: *70349732219740
124
+ version_requirements: *70097385742940
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: guard-bundler
127
- requirement: &70349732219100 !ruby/object:Gem::Requirement
127
+ requirement: &70097385742520 !ruby/object:Gem::Requirement
128
128
  none: false
129
129
  requirements:
130
130
  - - ! '>='
@@ -132,10 +132,10 @@ dependencies:
132
132
  version: '0'
133
133
  type: :development
134
134
  prerelease: false
135
- version_requirements: *70349732219100
135
+ version_requirements: *70097385742520
136
136
  - !ruby/object:Gem::Dependency
137
137
  name: guard-coffeescript
138
- requirement: &70349732218080 !ruby/object:Gem::Requirement
138
+ requirement: &70097385742100 !ruby/object:Gem::Requirement
139
139
  none: false
140
140
  requirements:
141
141
  - - ! '>='
@@ -143,10 +143,10 @@ dependencies:
143
143
  version: '0'
144
144
  type: :development
145
145
  prerelease: false
146
- version_requirements: *70349732218080
146
+ version_requirements: *70097385742100
147
147
  - !ruby/object:Gem::Dependency
148
148
  name: em-websocket-client
149
- requirement: &70349732217260 !ruby/object:Gem::Requirement
149
+ requirement: &70097385741680 !ruby/object:Gem::Requirement
150
150
  none: false
151
151
  requirements:
152
152
  - - ! '>='
@@ -154,7 +154,7 @@ dependencies:
154
154
  version: '0'
155
155
  type: :development
156
156
  prerelease: false
157
- version_requirements: *70349732217260
157
+ version_requirements: *70097385741680
158
158
  description: Firehose is a realtime web application toolkit for building realtime
159
159
  Ruby web applications.
160
160
  email:
@@ -184,16 +184,13 @@ files:
184
184
  - lib/assets/javascripts/firehose/transport.js.coffee
185
185
  - lib/assets/javascripts/firehose/web_socket.js.coffee
186
186
  - lib/firehose.rb
187
- - lib/firehose/broker.rb
188
187
  - lib/firehose/cli.rb
189
- - lib/firehose/consumer.rb
190
188
  - lib/firehose/default.rb
191
189
  - lib/firehose/producer.rb
192
190
  - lib/firehose/publisher.rb
193
191
  - lib/firehose/rack.rb
194
192
  - lib/firehose/subscription.rb
195
193
  - lib/firehose/version.rb
196
- - spec/integrations/amqp_resources_spec.rb
197
194
  - spec/integrations/thin_spec.rb
198
195
  - spec/lib/broker_spec.rb
199
196
  - spec/lib/consumer_spec.rb
@@ -225,7 +222,6 @@ signing_key:
225
222
  specification_version: 3
226
223
  summary: Build realtime Ruby web applications
227
224
  test_files:
228
- - spec/integrations/amqp_resources_spec.rb
229
225
  - spec/integrations/thin_spec.rb
230
226
  - spec/lib/broker_spec.rb
231
227
  - spec/lib/consumer_spec.rb
@@ -1,34 +0,0 @@
1
- module Firehose
2
- # TODO add support to broker for publishing, then abstract this out into a backend. A
3
- # broker will eventually be passed into a web server front-end to serve up the web requests.
4
- class Broker
5
- def consumers
6
- @consumers ||= Hash.new do |consumers, consumer_id|
7
- consumer = Firehose::Consumer.new(consumer_id)
8
- consumer.on_unsubscribe do
9
- consumers.delete consumer_id
10
- end
11
- consumers[consumer_id] = consumer
12
- end
13
- end
14
-
15
- # Don't like the [] syntax to get at consumers? No worries mate!
16
- def consumer(consumer_id)
17
- consumers[consumer_id]
18
- end
19
-
20
- # Gracefully unsubscribe all of the consumers and get rid of them from the consumers
21
- # collection.
22
- def stop
23
- consumers.values.each(&:unsubscribe)
24
- end
25
-
26
- # Returns a hash of the connected consumers with the number of their subscriptions
27
- def stats
28
- consumers.inject({}) do |memo, (consumer_id, consumer)|
29
- memo[consumer_id] = { 'subscriptions' => consumer.subscriptions.keys }
30
- memo
31
- end
32
- end
33
- end
34
- end
@@ -1,49 +0,0 @@
1
- module Firehose
2
- class Consumer
3
- # Unique identifier for a consumer. Note that a consumer does not map directly to
4
- # a user_id. In a web browser world, you might have a user with multiple tabs open,
5
- # so you'll went to send each users tab a seperate message stream. Consider a convention
6
- # such as :user_id-:guid for your application.
7
- attr_reader :guid
8
-
9
- def initialize(guid = self.class.next_guid)
10
- @guid = guid
11
- end
12
-
13
- # Create a subscription and subscribe to a channel.
14
- def subscribe_to(channel, &block)
15
- subscriptions[channel].subscribe(&block)
16
- end
17
-
18
- # Active subscriptions to the backend.
19
- def subscriptions
20
- @subscriptions ||= Hash.new do |subscriptions, channel|
21
- # Setup the hash to generate subscriptions that can delete themselves from
22
- # their own collection on an unsubscription event.
23
- subscription = Subscription.new(self, channel)
24
- subscription.on_unsubscribe do
25
- # Remove the subscription from the consumer.subscriptions
26
- # list when unsubscribe.
27
- subscriptions.delete channel
28
- end
29
- subscriptions[channel] = subscription
30
- end
31
- end
32
-
33
- # Unsubscribe from all subscriptions.
34
- def unsubscribe
35
- subscriptions.values.each(&:unsubscribe)
36
- @on_unsubscribe.call(self) if @on_unsubscribe
37
- end
38
-
39
- # Define callback for when unsubscribe is called from the consumer.
40
- def on_unsubscribe(&block)
41
- @on_unsubscribe = block
42
- end
43
-
44
- protected
45
- def self.next_guid
46
- rand(999_999_999_999).to_s
47
- end
48
- end
49
- end
@@ -1,51 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe "Firehose amqp resources" do
4
-
5
- let(:channel) { "/resource-test-#{Time.now.to_i}" }
6
- let(:consumer) { Firehose::Consumer.new }
7
-
8
- it "should clean up exchanges and queues" do
9
- sent, received = 'howdy!', nil
10
-
11
- before_exchange_count = `rabbitmqctl list_exchanges`.lines.count
12
- before_queue_count = `rabbitmqctl list_queues`.lines.count
13
-
14
- during_exchange_count = nil
15
- during_queue_count = nil
16
-
17
- EM.run do
18
- # Kill test if it runs longer than 5s
19
- EM.add_timer(5) { EM.stop }
20
-
21
- subscription = Firehose::Subscription.new(consumer, channel)
22
- subscription.ttl = 1
23
-
24
- subscription.subscribe do |payload|
25
- received = payload
26
- subscription.unsubscribe
27
-
28
- during_exchange_count = `rabbitmqctl list_exchanges`.lines.count
29
- during_queue_count = `rabbitmqctl list_queues`.lines.count
30
-
31
- # I wait 1 second before killing em so that unsubscribe
32
- # can talk to AMQP before the whole thing dies.
33
- EM.add_timer(1){ EM.stop }
34
- end
35
-
36
- # Let the subscriber subscribe before publishing messages.
37
- EM.add_timer(1){ Firehose::Publisher.new.publish(channel, sent) }
38
- end
39
-
40
- after_exchange_count = `rabbitmqctl list_exchanges`.lines.count
41
- after_queue_count = `rabbitmqctl list_queues`.lines.count
42
-
43
- received.should == sent
44
-
45
- after_exchange_count.should == before_exchange_count
46
- after_queue_count.should == before_queue_count
47
-
48
- during_exchange_count.should == before_exchange_count + 1
49
- during_queue_count.should == before_queue_count + 1
50
- end
51
- end