firehose 1.2.20 → 2.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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +36 -0
- data/{LICENSE.txt → LICENSE} +6 -6
- data/README.md +389 -139
- data/app/assets/javascripts/firehose.js +258 -0
- data/config/importmap.rb +1 -0
- data/db/migrate/20260214185244_create_firehose_events.rb +23 -0
- data/lib/commands/firehose_command.rb +119 -0
- data/lib/firehose/channel.rb +40 -0
- data/lib/firehose/cleanup_job.rb +21 -0
- data/lib/firehose/engine.rb +36 -0
- data/lib/firehose/helper.rb +40 -0
- data/lib/firehose/models/channel.rb +12 -0
- data/lib/firehose/models/message.rb +11 -0
- data/lib/firehose/queue.rb +44 -0
- data/lib/firehose/server.rb +931 -18
- data/lib/firehose/sse.rb +187 -0
- data/lib/firehose/stream.rb +32 -0
- data/lib/firehose/streamable.rb +25 -0
- data/lib/firehose/websocket.rb +242 -0
- data/lib/firehose.rb +67 -15
- data/lib/generators/firehose/install/install_generator.rb +43 -0
- data/lib/generators/firehose/install/templates/firehose_controller.rb +16 -0
- metadata +40 -428
- data/.env.sample +0 -17
- data/.gitignore +0 -12
- data/.rbenv-version +0 -1
- data/.rspec +0 -1
- data/.travis.yml +0 -16
- data/Gemfile +0 -4
- data/Guardfile +0 -31
- data/Procfile +0 -1
- data/Procfile.dev +0 -4
- data/Rakefile +0 -53
- data/bin/firehose +0 -9
- data/config/evergreen.rb +0 -9
- data/config/rainbows.rb +0 -17
- data/firehose.gemspec +0 -49
- data/lib/assets/javascripts/firehose/base.js.coffee +0 -4
- data/lib/assets/javascripts/firehose/consumer.js.coffee +0 -70
- data/lib/assets/javascripts/firehose/core.js.coffee +0 -5
- data/lib/assets/javascripts/firehose/long_poll.js.coffee +0 -149
- data/lib/assets/javascripts/firehose/transport.js.coffee +0 -42
- data/lib/assets/javascripts/firehose/version.js.coffee.erb +0 -2
- data/lib/assets/javascripts/firehose/web_socket.js.coffee +0 -102
- data/lib/assets/javascripts/firehose.js.coffee +0 -3
- data/lib/assets/javascripts/vendor/json2.js +0 -487
- data/lib/firehose/assets.rb +0 -56
- data/lib/firehose/cli.rb +0 -78
- data/lib/firehose/client/consumer.rb +0 -94
- data/lib/firehose/client/producer.rb +0 -108
- data/lib/firehose/client.rb +0 -10
- data/lib/firehose/logging.rb +0 -32
- data/lib/firehose/rack/app.rb +0 -49
- data/lib/firehose/rack/consumer/http_long_poll.rb +0 -86
- data/lib/firehose/rack/consumer/web_socket.rb +0 -95
- data/lib/firehose/rack/consumer.rb +0 -39
- data/lib/firehose/rack/ping.rb +0 -88
- data/lib/firehose/rack/publisher.rb +0 -53
- data/lib/firehose/rack.rb +0 -32
- data/lib/firehose/rails.rb +0 -7
- data/lib/firehose/server/app.rb +0 -52
- data/lib/firehose/server/channel.rb +0 -80
- data/lib/firehose/server/publisher.rb +0 -134
- data/lib/firehose/server/subscriber.rb +0 -51
- data/lib/firehose/version.rb +0 -4
- data/package.json +0 -5
- data/spec/integrations/integration_test_helper.rb +0 -22
- data/spec/integrations/rainbows_spec.rb +0 -7
- data/spec/integrations/shared_examples.rb +0 -120
- data/spec/integrations/thin_spec.rb +0 -7
- data/spec/javascripts/firehose/base_spec.coffee +0 -2
- data/spec/javascripts/firehose/consumer_spec.coffee +0 -45
- data/spec/javascripts/firehose/long_poll_spec.coffee +0 -44
- data/spec/javascripts/firehose/transport_spec.coffee +0 -32
- data/spec/javascripts/firehose/websocket_spec.coffee +0 -25
- data/spec/javascripts/helpers/spec_helper.js +0 -30
- data/spec/javascripts/support/sinon-1.7.3.js +0 -4290
- data/spec/lib/assets_spec.rb +0 -35
- data/spec/lib/client/consumer_spec.rb +0 -13
- data/spec/lib/client/producer_spec.rb +0 -71
- data/spec/lib/firehose_spec.rb +0 -7
- data/spec/lib/rack/consumer/http_long_poll_spec.rb +0 -12
- data/spec/lib/rack/consumer_spec.rb +0 -19
- data/spec/lib/rack/ping_spec.rb +0 -28
- data/spec/lib/rack/publisher_spec.rb +0 -34
- data/spec/lib/server/app_spec.rb +0 -15
- data/spec/lib/server/channel_spec.rb +0 -164
- data/spec/lib/server/publisher_spec.rb +0 -84
- data/spec/lib/server/subscriber_spec.rb +0 -69
- data/spec/spec_helper.rb +0 -77
data/lib/firehose/rack.rb
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
module Firehose
|
|
2
|
-
module Rack
|
|
3
|
-
autoload :Consumer, 'firehose/rack/consumer'
|
|
4
|
-
autoload :Publisher, 'firehose/rack/publisher'
|
|
5
|
-
autoload :Ping, 'firehose/rack/ping'
|
|
6
|
-
autoload :App, 'firehose/rack/app'
|
|
7
|
-
|
|
8
|
-
# Evented web servers recognize the -1 HTTP code as a response deferral, which
|
|
9
|
-
# is needed to stream responses via WebSockets or HTTP long polling.
|
|
10
|
-
ASYNC_RESPONSE = [-1, {}, []].freeze
|
|
11
|
-
|
|
12
|
-
# Normally we'd want to use a custom header to reduce the likelihood of some
|
|
13
|
-
# HTTP middleware clobbering the value. But Safari seems to ignore our CORS
|
|
14
|
-
# header instructions, so we are using 'pragma' because it is always allowed.
|
|
15
|
-
LAST_MESSAGE_SEQUENCE_HEADER = 'Pragma'
|
|
16
|
-
|
|
17
|
-
# Rack wants the header to start with HTTP, so we deal with that here.
|
|
18
|
-
RACK_LAST_MESSAGE_SEQUENCE_HEADER = "HTTP_#{LAST_MESSAGE_SEQUENCE_HEADER.upcase.gsub('-', '_')}"
|
|
19
|
-
|
|
20
|
-
# Disable CORS preflight caches for requests in development mode.
|
|
21
|
-
CORS_OPTIONS_MAX_AGE = ENV['RACK_ENV'] == 'development' ? '1' : '1728000'
|
|
22
|
-
|
|
23
|
-
module Helpers
|
|
24
|
-
# Calculates the content of a message body for the response so that HTTP Keep-Alive
|
|
25
|
-
# connections work.
|
|
26
|
-
def response(status, body='', headers={})
|
|
27
|
-
headers = {'Content-Length' => body.size.to_s}.merge(headers)
|
|
28
|
-
[status, headers, [body]]
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
data/lib/firehose/rails.rb
DELETED
data/lib/firehose/server/app.rb
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
module Firehose
|
|
2
|
-
module Server
|
|
3
|
-
# Configure servers that are booted with-out going through Rack. This is mostly used by
|
|
4
|
-
# the `firehose server` CLI command or for testing. Production configurations are likely
|
|
5
|
-
# to boot with custom rack configurations.
|
|
6
|
-
class App
|
|
7
|
-
def initialize(opts={})
|
|
8
|
-
@port = opts[:port] || Firehose::URI.port
|
|
9
|
-
@host = opts[:host] || Firehose::URI.host
|
|
10
|
-
@server = opts[:server] || :rainbows
|
|
11
|
-
|
|
12
|
-
Firehose.logger.info "Starting #{Firehose::VERSION} '#{Firehose::CODENAME}', in #{ENV['RACK_ENV']}"
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def start
|
|
16
|
-
self.send("start_#{@server}")
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
# Boot the Firehose server with the Rainbows app server.
|
|
21
|
-
def start_rainbows
|
|
22
|
-
require 'rainbows'
|
|
23
|
-
Faye::WebSocket.load_adapter('rainbows')
|
|
24
|
-
|
|
25
|
-
rackup = Unicorn::Configurator::RACKUP
|
|
26
|
-
rackup[:port] = @port if @port
|
|
27
|
-
rackup[:host] = @host if @host
|
|
28
|
-
rackup[:set_listener] = true
|
|
29
|
-
opts = rackup[:options]
|
|
30
|
-
opts[:config_file] = File.expand_path('../../../../config/rainbows.rb', __FILE__)
|
|
31
|
-
|
|
32
|
-
server = Rainbows::HttpServer.new(Firehose::Rack::App.new, opts)
|
|
33
|
-
server.start.join
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Boot the Firehose server with the Thin app server.
|
|
37
|
-
def start_thin
|
|
38
|
-
require 'thin'
|
|
39
|
-
|
|
40
|
-
Faye::WebSocket.load_adapter('thin')
|
|
41
|
-
|
|
42
|
-
# TODO: See if we can just set Thin to use Firehose.logger instead of
|
|
43
|
-
# printing out messages by itself.
|
|
44
|
-
Thin::Logging.silent = true if Firehose.logger.level == Logger::ERROR
|
|
45
|
-
|
|
46
|
-
server = Thin::Server.new(@host, @port) do
|
|
47
|
-
run Firehose::Rack::App.new
|
|
48
|
-
end.start
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
module Firehose
|
|
2
|
-
module Server
|
|
3
|
-
# Connects to a specific channel on Redis and listens for messages to notify subscribers.
|
|
4
|
-
class Channel
|
|
5
|
-
attr_reader :channel_key, :redis, :subscriber, :list_key, :sequence_key
|
|
6
|
-
|
|
7
|
-
def self.redis
|
|
8
|
-
@redis ||= EM::Hiredis.connect
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def self.subscriber
|
|
12
|
-
@subscriber ||= Server::Subscriber.new(EM::Hiredis.connect)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def initialize(channel_key, redis=self.class.redis, subscriber=self.class.subscriber)
|
|
16
|
-
@channel_key, @redis, @subscriber = channel_key, redis, subscriber
|
|
17
|
-
@list_key, @sequence_key = Server.key(channel_key, :list), Server.key(channel_key, :sequence)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def next_message(last_sequence=nil, options={})
|
|
21
|
-
last_sequence = last_sequence.to_i
|
|
22
|
-
|
|
23
|
-
deferrable = EM::DefaultDeferrable.new
|
|
24
|
-
# TODO - Think this through a little harder... maybe some tests ol buddy!
|
|
25
|
-
deferrable.errback {|e| EM.next_tick { raise e } unless [:timeout, :disconnect].include?(e) }
|
|
26
|
-
|
|
27
|
-
# TODO: Use HSET so we don't have to pull 100 messages back every time.
|
|
28
|
-
redis.multi
|
|
29
|
-
redis.get(sequence_key).
|
|
30
|
-
errback {|e| deferrable.fail e }
|
|
31
|
-
redis.lrange(list_key, 0, Server::Publisher::MAX_MESSAGES).
|
|
32
|
-
errback {|e| deferrable.fail e }
|
|
33
|
-
redis.exec.callback do |(sequence, message_list)|
|
|
34
|
-
Firehose.logger.debug "exec returned: `#{sequence}` and `#{message_list.inspect}`"
|
|
35
|
-
sequence = sequence.to_i
|
|
36
|
-
|
|
37
|
-
if sequence.nil? || (diff = sequence - last_sequence) <= 0
|
|
38
|
-
Firehose.logger.debug "No message available yet, subscribing. sequence: `#{sequence}`"
|
|
39
|
-
# Either this resource has never been seen before or we are all caught up.
|
|
40
|
-
# Subscribe and hope something gets published to this end-point.
|
|
41
|
-
subscribe(deferrable, options[:timeout])
|
|
42
|
-
elsif last_sequence > 0 && diff < Server::Publisher::MAX_MESSAGES
|
|
43
|
-
# The client is kinda-sorta running behind, but has a chance to catch
|
|
44
|
-
# up. Catch them up FTW.
|
|
45
|
-
# But we won't "catch them up" if last_sequence was zero/nil because
|
|
46
|
-
# that implies the client is connecting for the 1st time.
|
|
47
|
-
message = message_list[diff-1]
|
|
48
|
-
Firehose.logger.debug "Sending old message `#{message}` and sequence `#{sequence}` to client directly. Client is `#{diff}` behind, at `#{last_sequence}`."
|
|
49
|
-
deferrable.succeed message, last_sequence + 1
|
|
50
|
-
else
|
|
51
|
-
# The client is hopelessly behind and underwater. Just reset
|
|
52
|
-
# their whole world with the lastest message.
|
|
53
|
-
message = message_list[0]
|
|
54
|
-
Firehose.logger.debug "Sending latest message `#{message}` and sequence `#{sequence}` to client directly."
|
|
55
|
-
deferrable.succeed message, sequence
|
|
56
|
-
end
|
|
57
|
-
end.errback {|e| deferrable.fail e }
|
|
58
|
-
|
|
59
|
-
deferrable
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def unsubscribe(deferrable)
|
|
63
|
-
subscriber.unsubscribe channel_key, deferrable
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
private
|
|
67
|
-
def subscribe(deferrable, timeout=nil)
|
|
68
|
-
subscriber.subscribe(channel_key, deferrable)
|
|
69
|
-
if timeout
|
|
70
|
-
timer = EventMachine::Timer.new(timeout) do
|
|
71
|
-
deferrable.fail :timeout
|
|
72
|
-
unsubscribe deferrable
|
|
73
|
-
end
|
|
74
|
-
# Cancel the timer if when the deferrable succeeds
|
|
75
|
-
deferrable.callback { timer.cancel }
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
module Firehose
|
|
2
|
-
module Server
|
|
3
|
-
class Publisher
|
|
4
|
-
# Number of messages that Redis buffers for the client if its
|
|
5
|
-
# connection drops, then reconnects.
|
|
6
|
-
MAX_MESSAGES = 100
|
|
7
|
-
|
|
8
|
-
# Seconds that the message buffer should live before Redis expires it.
|
|
9
|
-
TTL = 60*60*24
|
|
10
|
-
|
|
11
|
-
# Delimited used to frame different parts of a message that's published
|
|
12
|
-
# over Firehose.
|
|
13
|
-
PAYLOAD_DELIMITER = "\n"
|
|
14
|
-
|
|
15
|
-
# Publish a message to a Firehose channel via Redis.
|
|
16
|
-
def publish(channel_key, message, opts={})
|
|
17
|
-
# How long should we hang on to the resource once is published?
|
|
18
|
-
ttl = (opts[:ttl] || TTL).to_i
|
|
19
|
-
|
|
20
|
-
# TODO hi-redis isn't that awesome... we have to setup an errback per even for wrong
|
|
21
|
-
# commands because of the lack of a method_missing whitelist. Perhaps implement a whitelist in
|
|
22
|
-
# em-hiredis or us a diff lib?
|
|
23
|
-
if (deferrable = opts[:deferrable]).nil?
|
|
24
|
-
deferrable = EM::DefaultDeferrable.new
|
|
25
|
-
deferrable.errback do |e|
|
|
26
|
-
# Handle missing Lua publishing script in cache
|
|
27
|
-
# (such as Redis restarting or someone executing SCRIPT FLUSH)
|
|
28
|
-
if e.message =~ /NOSCRIPT/
|
|
29
|
-
deferrable.succeed
|
|
30
|
-
EM.next_tick do
|
|
31
|
-
@publish_script_digest = nil
|
|
32
|
-
combined_opts = opts.merge :deferrable => deferrable
|
|
33
|
-
self.publish channel_key, message, combined_opts
|
|
34
|
-
end
|
|
35
|
-
else
|
|
36
|
-
EM.next_tick { raise e }
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
if @publish_script_digest.nil?
|
|
42
|
-
register_publish_script.errback do |e|
|
|
43
|
-
deferrable.fail e
|
|
44
|
-
end.callback do |digest|
|
|
45
|
-
@publish_script_digest = digest
|
|
46
|
-
Firehose.logger.debug "Registered Lua publishing script with Redis => #{digest}"
|
|
47
|
-
eval_publish_script channel_key, message, ttl, deferrable
|
|
48
|
-
end
|
|
49
|
-
else
|
|
50
|
-
eval_publish_script channel_key, message, ttl, deferrable
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
deferrable
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
private
|
|
57
|
-
def redis
|
|
58
|
-
@redis ||= EM::Hiredis.connect
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Serialize components of a message into something that can be dropped into Redis.
|
|
62
|
-
def self.to_payload(channel_key, sequence, message)
|
|
63
|
-
[channel_key, sequence, message].join(PAYLOAD_DELIMITER)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Deserealize components of a message back into Ruby.
|
|
67
|
-
def self.from_payload(payload)
|
|
68
|
-
payload.split(PAYLOAD_DELIMITER, method(:to_payload).arity)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# TODO: Make this FAR more robust. Ideally we'd whitelist the permitted
|
|
72
|
-
# characters and then escape or remove everything else.
|
|
73
|
-
# See: http://en.wikibooks.org/wiki/Lua_Programming/How_to_Lua/escape_sequence
|
|
74
|
-
def lua_escape(str)
|
|
75
|
-
str.gsub(/\\/,'\\\\\\').gsub(/"/,'\"').gsub(/\n/,'\n').gsub(/\r/,'\r')
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def register_publish_script
|
|
79
|
-
redis.script 'LOAD', REDIS_PUBLISH_SCRIPT
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def eval_publish_script(channel_key, message, ttl, deferrable)
|
|
83
|
-
list_key = Server.key(channel_key, :list)
|
|
84
|
-
script_args = [
|
|
85
|
-
Server.key(channel_key, :sequence),
|
|
86
|
-
list_key,
|
|
87
|
-
Server.key(:channel_updates),
|
|
88
|
-
ttl,
|
|
89
|
-
message,
|
|
90
|
-
MAX_MESSAGES,
|
|
91
|
-
PAYLOAD_DELIMITER,
|
|
92
|
-
channel_key
|
|
93
|
-
]
|
|
94
|
-
redis.evalsha(
|
|
95
|
-
@publish_script_digest, script_args.length, *script_args
|
|
96
|
-
).errback do |e|
|
|
97
|
-
deferrable.fail e
|
|
98
|
-
end.callback do |sequence|
|
|
99
|
-
Firehose.logger.debug "Redis stored/published `#{message}` to list `#{list_key}` with sequence `#{sequence}`"
|
|
100
|
-
deferrable.succeed
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
REDIS_PUBLISH_SCRIPT = <<-LUA
|
|
105
|
-
local sequence_key = KEYS[1]
|
|
106
|
-
local list_key = KEYS[2]
|
|
107
|
-
local channel_key = KEYS[3]
|
|
108
|
-
local ttl = KEYS[4]
|
|
109
|
-
local message = KEYS[5]
|
|
110
|
-
local max_messages = KEYS[6]
|
|
111
|
-
local payload_delimiter = KEYS[7]
|
|
112
|
-
local firehose_resource = KEYS[8]
|
|
113
|
-
|
|
114
|
-
local current_sequence = redis.call('get', sequence_key)
|
|
115
|
-
if current_sequence == nil or current_sequence == false then
|
|
116
|
-
current_sequence = 0
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
local sequence = current_sequence + 1
|
|
120
|
-
local message_payload = firehose_resource .. payload_delimiter .. sequence .. payload_delimiter .. message
|
|
121
|
-
|
|
122
|
-
redis.call('set', sequence_key, sequence)
|
|
123
|
-
redis.call('expire', sequence_key, ttl)
|
|
124
|
-
redis.call('lpush', list_key, message)
|
|
125
|
-
redis.call('ltrim', list_key, 0, max_messages - 1)
|
|
126
|
-
redis.call('expire', list_key, ttl)
|
|
127
|
-
redis.call('publish', channel_key, message_payload)
|
|
128
|
-
|
|
129
|
-
return sequence
|
|
130
|
-
LUA
|
|
131
|
-
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
end
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
module Firehose
|
|
2
|
-
module Server
|
|
3
|
-
# Setups a connetion to Redis to listen for new resources...
|
|
4
|
-
class Subscriber
|
|
5
|
-
attr_reader :pubsub
|
|
6
|
-
|
|
7
|
-
def initialize(redis)
|
|
8
|
-
@pubsub = redis.pubsub
|
|
9
|
-
# TODO: Instead of just raising an exception, it would probably be better
|
|
10
|
-
# for the errback to set some sort of 'disconnected' state. Then
|
|
11
|
-
# whenever a deferrable was 'subscribed' we could instantly fail
|
|
12
|
-
# the deferrable with whatever connection error we had.
|
|
13
|
-
# An alternative which would have a similar result would be to
|
|
14
|
-
# subscribe lazily (i.e. not until we have a deferrable to subscribe).
|
|
15
|
-
# Then, if connecting failed, it'd be super easy to fail the deferrable
|
|
16
|
-
# with the same error.
|
|
17
|
-
# The final goal is to allow the failed deferrable bubble back up
|
|
18
|
-
# so we can send back a nice, clean 500 error to the client.
|
|
19
|
-
channel_updates_key = Server.key('channel_updates')
|
|
20
|
-
pubsub.subscribe(channel_updates_key).
|
|
21
|
-
errback{|e| EM.next_tick { raise e } }.
|
|
22
|
-
callback { Firehose.logger.debug "Redis subscribed to `#{channel_updates_key}`" }
|
|
23
|
-
pubsub.on(:message) do |_, payload|
|
|
24
|
-
channel_key, sequence, message = Server::Publisher.from_payload(payload)
|
|
25
|
-
|
|
26
|
-
if deferrables = subscriptions.delete(channel_key)
|
|
27
|
-
Firehose.logger.debug "Redis notifying #{deferrables.count} deferrable(s) at `#{channel_key}` with sequence `#{sequence}` and message `#{message}`"
|
|
28
|
-
deferrables.each do |deferrable|
|
|
29
|
-
Firehose.logger.debug "Sending message #{message} and sequence #{sequence} to client from subscriber"
|
|
30
|
-
deferrable.succeed message, sequence.to_i
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def subscribe(channel_key, deferrable)
|
|
37
|
-
subscriptions[channel_key].push deferrable
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def unsubscribe(channel_key, deferrable)
|
|
41
|
-
subscriptions[channel_key].delete deferrable
|
|
42
|
-
subscriptions.delete(channel_key) if subscriptions[channel_key].empty?
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
private
|
|
46
|
-
def subscriptions
|
|
47
|
-
@subscriptions ||= Hash.new{|h,k| h[k] = []}
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
data/lib/firehose/version.rb
DELETED
data/package.json
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
require 'spec_helper'
|
|
2
|
-
|
|
3
|
-
module IntegrationTestHelper
|
|
4
|
-
def start_server
|
|
5
|
-
@server_pid = fork do
|
|
6
|
-
Firehose::Server::App.new(:server => server, :port => uri.port).start
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
# Need to give the server a chance to boot up.
|
|
10
|
-
sleep 1
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def stop_server
|
|
14
|
-
Process.kill 'INT', @server_pid
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
# Let isn't allowed in before(:all)
|
|
19
|
-
def uri
|
|
20
|
-
Firehose::URI
|
|
21
|
-
end
|
|
22
|
-
end
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
require 'spec_helper'
|
|
2
|
-
require 'integrations/integration_test_helper'
|
|
3
|
-
require 'json'
|
|
4
|
-
|
|
5
|
-
shared_examples_for 'Firehose::Rack::App' do
|
|
6
|
-
include EM::TestHelper
|
|
7
|
-
include IntegrationTestHelper
|
|
8
|
-
|
|
9
|
-
before(:all) do
|
|
10
|
-
Firehose::Client::Producer::Http.adapter = :em_http
|
|
11
|
-
start_server
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
after(:all) do
|
|
15
|
-
Firehose::Client::Producer::Http.adapter = nil
|
|
16
|
-
stop_server
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
before(:each) { WebMock.disable! }
|
|
20
|
-
after(:each) { WebMock.enable! }
|
|
21
|
-
|
|
22
|
-
let(:app) { Firehose::Rack::App.new }
|
|
23
|
-
let(:messages) { (1..200).map{|n| "msg-#{n}" } }
|
|
24
|
-
let(:channel) { "/firehose/integration/#{Time.now.to_i}" }
|
|
25
|
-
let(:http_url) { "http://#{uri.host}:#{uri.port}#{channel}" }
|
|
26
|
-
let(:ws_url) { "ws://#{uri.host}:#{uri.port}#{channel}" }
|
|
27
|
-
|
|
28
|
-
it "should pub-sub http and websockets" do
|
|
29
|
-
# Setup variables that we'll use after we turn off EM to validate our
|
|
30
|
-
# test assertions.
|
|
31
|
-
outgoing, received = messages.dup, Hash.new{|h,k| h[k] = []}
|
|
32
|
-
|
|
33
|
-
# Our WS and Http clients call this when they have received their messages to determine
|
|
34
|
-
# when to turn off EM and make the test assertion at the very bottom.
|
|
35
|
-
succeed = Proc.new do
|
|
36
|
-
# TODO: For some weird reason the `add_timer` call causes up to 20 seconds of delay after
|
|
37
|
-
# the test finishes running. However, without it the test will randomly fail with a
|
|
38
|
-
# "Redis disconnected" error.
|
|
39
|
-
em.add_timer(1) { em.stop } if received.values.all?{|arr| arr.size == messages.size }
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Setup a publisher
|
|
43
|
-
publish = Proc.new do
|
|
44
|
-
Firehose::Client::Producer::Http.new.publish(outgoing.shift).to(channel) do
|
|
45
|
-
# The random timer ensures that sometimes the clients will be behind
|
|
46
|
-
# and sometimes they will be caught up.
|
|
47
|
-
EM::add_timer(rand*0.005) { publish.call } unless outgoing.empty?
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Lets have an HTTP Long poll client
|
|
52
|
-
http_long_poll = Proc.new do |cid, last_sequence|
|
|
53
|
-
http = EM::HttpRequest.new(http_url).get(:query => {'last_message_sequence' => last_sequence})
|
|
54
|
-
http.errback { em.stop }
|
|
55
|
-
http.callback do
|
|
56
|
-
frame = JSON.parse(http.response, :symbolize_names => true)
|
|
57
|
-
received[cid] << frame[:message]
|
|
58
|
-
if received[cid].size < messages.size
|
|
59
|
-
# Add some jitter so the clients aren't syncronized
|
|
60
|
-
EM::add_timer(rand*0.001) { http_long_poll.call cid, frame[:last_sequence] }
|
|
61
|
-
else
|
|
62
|
-
succeed.call cid
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# And test a web socket client too, at the same time.
|
|
68
|
-
websocket = Proc.new do |cid|
|
|
69
|
-
ws = Faye::WebSocket::Client.new(ws_url)
|
|
70
|
-
|
|
71
|
-
ws.onopen = lambda do |event|
|
|
72
|
-
ws.send('{"message_sequence":0}')
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
ws.onmessage = lambda do |event|
|
|
76
|
-
frame = JSON.parse(event.data, :symbolize_names => true)
|
|
77
|
-
received[cid] << frame[:message]
|
|
78
|
-
succeed.call cid unless received[cid].size < messages.size
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
ws.onclose = lambda do |event|
|
|
82
|
-
ws = nil
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
ws.onerror = lambda do |event|
|
|
86
|
-
raise 'ws failed' + "\n" + event.inspect
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Great, we have all the pieces in order, lets run this thing in the reactor.
|
|
91
|
-
em 180 do
|
|
92
|
-
# Start the clients.
|
|
93
|
-
websocket.call(1)
|
|
94
|
-
websocket.call(2)
|
|
95
|
-
http_long_poll.call(3)
|
|
96
|
-
http_long_poll.call(4)
|
|
97
|
-
|
|
98
|
-
# Wait a sec to let our clients set up.
|
|
99
|
-
em.add_timer(1){ publish.call }
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# When EM stops, these assertions will be made.
|
|
103
|
-
received.size.should == 4
|
|
104
|
-
received.values.each do |arr|
|
|
105
|
-
arr.should == messages
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
it "should return 400 error for long-polling when using http long polling and sequence header is < 0" do
|
|
111
|
-
em 5 do
|
|
112
|
-
http = EM::HttpRequest.new(http_url).get(:query => {'last_message_sequence' => -1})
|
|
113
|
-
http.errback { |e| raise e.inspect }
|
|
114
|
-
http.callback do
|
|
115
|
-
http.response_header.status.should == 400
|
|
116
|
-
em.stop
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
describe 'Firehose.Consumer', ->
|
|
2
|
-
beforeEach ->
|
|
3
|
-
@klass = Firehose.Consumer
|
|
4
|
-
@instance = new @klass uri: 'test_uri', okInterval: 9999 # Stop crazy ajax loops during tests
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
#= Specs ===
|
|
8
|
-
|
|
9
|
-
describe 'new instance', ->
|
|
10
|
-
describe '@config', ->
|
|
11
|
-
beforeEach -> @subj = @instance.config
|
|
12
|
-
|
|
13
|
-
for callback in ['message','error','connected','disconnected','failed']
|
|
14
|
-
it "has a .#{callback} function", ->
|
|
15
|
-
expect( @subj[callback] ).toEqual jasmine.any Function
|
|
16
|
-
|
|
17
|
-
it 'has a params object', ->
|
|
18
|
-
expect( @subj.params ).toEqual jasmine.any Object
|
|
19
|
-
|
|
20
|
-
it 'has a parse function', ->
|
|
21
|
-
expect( @subj.parse ).toEqual jasmine.any Function
|
|
22
|
-
|
|
23
|
-
describe '#connect', ->
|
|
24
|
-
it 'runs without error', -> @instance.connect()
|
|
25
|
-
|
|
26
|
-
describe 'after #connect called', ->
|
|
27
|
-
beforeEach -> @instance.connect()
|
|
28
|
-
|
|
29
|
-
it 'creates a @transport instance', ->
|
|
30
|
-
expect( @instance.transport ).toEqual jasmine.any Firehose.Transport
|
|
31
|
-
|
|
32
|
-
describe '#stop', ->
|
|
33
|
-
describe 'when .connect called earlier', ->
|
|
34
|
-
beforeEach ->
|
|
35
|
-
@instance.connect()
|
|
36
|
-
sinon.spy @instance.transport, 'stop'
|
|
37
|
-
|
|
38
|
-
it 'calls stop once on @transport', ->
|
|
39
|
-
@instance.stop()
|
|
40
|
-
expect( @instance.transport.stop.calledOnce ).toBe true
|
|
41
|
-
|
|
42
|
-
it 'stops the upgrade timeout', ->
|
|
43
|
-
@instance.stop()
|
|
44
|
-
expect( @instance.upgradeTimeout? ).toBe false
|
|
45
|
-
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
describe 'Firehose.LongPoll', ->
|
|
2
|
-
beforeEach ->
|
|
3
|
-
@klass = Firehose.LongPoll
|
|
4
|
-
@instance = new @klass()
|
|
5
|
-
|
|
6
|
-
afterEach -> @instance.stop()
|
|
7
|
-
|
|
8
|
-
#= Specs ===
|
|
9
|
-
|
|
10
|
-
it '.ieSupported returns a Boolean', ->
|
|
11
|
-
expect( typeof @klass.ieSupported() ).toBe 'boolean'
|
|
12
|
-
|
|
13
|
-
it '.supported returns a Boolean', ->
|
|
14
|
-
expect( typeof @klass.supported() ).toBe 'boolean'
|
|
15
|
-
|
|
16
|
-
describe '#stop', ->
|
|
17
|
-
beforeEach ->
|
|
18
|
-
@instance._lastRequest = $.ajax()
|
|
19
|
-
@instance._lastPingRequest = $.ajax()
|
|
20
|
-
|
|
21
|
-
it 'aborts the last request', ->
|
|
22
|
-
sinon.spy @instance._lastRequest, 'abort'
|
|
23
|
-
Object.freeze @instance # Prevents the delete call from modifying the request object, so we can verify the call
|
|
24
|
-
@instance.stop()
|
|
25
|
-
expect( @instance._lastRequest.abort.called ).toBe true
|
|
26
|
-
|
|
27
|
-
it 'deletes reference to the last request', ->
|
|
28
|
-
@instance.stop()
|
|
29
|
-
expect( @instance._lastRequest? ).toBe false
|
|
30
|
-
|
|
31
|
-
it 'aborts the last ping request', ->
|
|
32
|
-
sinon.spy @instance._lastPingRequest, 'abort'
|
|
33
|
-
Object.freeze @instance # Prevents the delete call from modifying the request object, so we can verify the call
|
|
34
|
-
@instance.stop()
|
|
35
|
-
expect( @instance._lastPingRequest.abort.called ).toBe true
|
|
36
|
-
|
|
37
|
-
it 'deletes reference to the last ping request', ->
|
|
38
|
-
@instance.stop()
|
|
39
|
-
expect( @instance._lastPingRequest? ).toBe false
|
|
40
|
-
|
|
41
|
-
describe 'callbacks', ->
|
|
42
|
-
for callback in ['_request','_success','_ping','_error']
|
|
43
|
-
it "has a #{callback} callback function", ->
|
|
44
|
-
expect( @instance[callback] ).toEqual jasmine.any Function
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
describe 'Firehose.Transport', ->
|
|
2
|
-
beforeEach ->
|
|
3
|
-
@klass = Firehose.Transport
|
|
4
|
-
@instance = new @klass()
|
|
5
|
-
|
|
6
|
-
it '.supported returns false', -> expect( @klass.supported() ).toBe false
|
|
7
|
-
it '.name throws error', -> expect( -> @instance.name ).toThrow()
|
|
8
|
-
it '.stop throws error', -> expect( -> @instance.stop ).toThrow()
|
|
9
|
-
|
|
10
|
-
describe 'constructor', ->
|
|
11
|
-
it 'returns an instance', -> expect( @instance ).toEqual jasmine.any @klass
|
|
12
|
-
it 'sets the @config object', -> expect( @instance.config ).toEqual jasmine.any Object
|
|
13
|
-
it 'sets a retry delay', -> expect( @instance._retryDelay ).toEqual jasmine.any Number
|
|
14
|
-
|
|
15
|
-
describe '#connect', ->
|
|
16
|
-
it 'returns itself', -> expect( @instance.connect() ).toBe @instance
|
|
17
|
-
|
|
18
|
-
describe 'when given a 500 delay', ->
|
|
19
|
-
beforeEach ->
|
|
20
|
-
@clock = sinon.useFakeTimers()
|
|
21
|
-
sinon.stub @instance, '_request'
|
|
22
|
-
@instance.connect 500
|
|
23
|
-
|
|
24
|
-
afterEach -> @clock.restore()
|
|
25
|
-
|
|
26
|
-
it 'does not call _request before the given delay', ->
|
|
27
|
-
@clock.tick 499
|
|
28
|
-
expect( @instance._request.called ).toBe false
|
|
29
|
-
|
|
30
|
-
it 'calls _request after the given delay', ->
|
|
31
|
-
@clock.tick 600
|
|
32
|
-
expect( @instance._request.called ).toBe true
|