firehose 1.3.6 → 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.
Files changed (104) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +31 -10
  3. data/{LICENSE.txt → LICENSE} +6 -6
  4. data/README.md +379 -187
  5. data/app/assets/javascripts/firehose.js +258 -0
  6. data/config/importmap.rb +1 -0
  7. data/db/migrate/20260214185244_create_firehose_events.rb +23 -0
  8. data/lib/commands/firehose_command.rb +119 -0
  9. data/lib/firehose/channel.rb +40 -0
  10. data/lib/firehose/cleanup_job.rb +21 -0
  11. data/lib/firehose/engine.rb +36 -0
  12. data/lib/firehose/helper.rb +40 -0
  13. data/lib/firehose/models/channel.rb +12 -0
  14. data/lib/firehose/models/message.rb +11 -0
  15. data/lib/firehose/queue.rb +44 -0
  16. data/lib/firehose/server.rb +930 -17
  17. data/lib/firehose/sse.rb +187 -0
  18. data/lib/firehose/stream.rb +32 -0
  19. data/lib/firehose/streamable.rb +25 -0
  20. data/lib/firehose/websocket.rb +242 -0
  21. data/lib/firehose.rb +67 -15
  22. data/lib/generators/firehose/install/install_generator.rb +43 -0
  23. data/lib/generators/firehose/install/templates/firehose_controller.rb +16 -0
  24. metadata +39 -366
  25. data/.codeclimate.yml +0 -29
  26. data/.dockerignore +0 -2
  27. data/.env.sample +0 -17
  28. data/.gitignore +0 -14
  29. data/.rspec +0 -1
  30. data/.rubocop.yml +0 -1156
  31. data/.ruby-version +0 -1
  32. data/.travis.yml +0 -12
  33. data/Dockerfile +0 -11
  34. data/Gemfile +0 -6
  35. data/Procfile +0 -1
  36. data/Procfile.dev +0 -3
  37. data/Rakefile +0 -64
  38. data/bin/firehose +0 -9
  39. data/coffeelint.json +0 -129
  40. data/config/rainbows.rb +0 -17
  41. data/docker-compose.yml +0 -17
  42. data/firehose.gemspec +0 -45
  43. data/karma.config.coffee +0 -89
  44. data/lib/assets/javascripts/firehose/base.js.coffee +0 -4
  45. data/lib/assets/javascripts/firehose/consumer.js.coffee +0 -86
  46. data/lib/assets/javascripts/firehose/core.js.coffee +0 -6
  47. data/lib/assets/javascripts/firehose/long_poll.js.coffee +0 -210
  48. data/lib/assets/javascripts/firehose/multiplexed_consumer.js.coffee +0 -74
  49. data/lib/assets/javascripts/firehose/transport.js.coffee +0 -44
  50. data/lib/assets/javascripts/firehose/version.js.coffee.erb +0 -2
  51. data/lib/assets/javascripts/firehose/web_socket.js.coffee +0 -148
  52. data/lib/assets/javascripts/firehose.js.coffee +0 -2
  53. data/lib/firehose/assets.rb +0 -56
  54. data/lib/firehose/cli.rb +0 -79
  55. data/lib/firehose/client/consumer.rb +0 -94
  56. data/lib/firehose/client/producer.rb +0 -114
  57. data/lib/firehose/client.rb +0 -10
  58. data/lib/firehose/logging.rb +0 -32
  59. data/lib/firehose/rack/app.rb +0 -49
  60. data/lib/firehose/rack/consumer/http_long_poll.rb +0 -159
  61. data/lib/firehose/rack/consumer/web_socket.rb +0 -200
  62. data/lib/firehose/rack/consumer.rb +0 -78
  63. data/lib/firehose/rack/ping.rb +0 -88
  64. data/lib/firehose/rack/publisher.rb +0 -59
  65. data/lib/firehose/rack.rb +0 -32
  66. data/lib/firehose/rails.rb +0 -7
  67. data/lib/firehose/server/app.rb +0 -52
  68. data/lib/firehose/server/channel.rb +0 -72
  69. data/lib/firehose/server/message_buffer.rb +0 -59
  70. data/lib/firehose/server/publisher.rb +0 -133
  71. data/lib/firehose/server/redis.rb +0 -32
  72. data/lib/firehose/server/subscriber.rb +0 -51
  73. data/lib/firehose/version.rb +0 -4
  74. data/package.json +0 -17
  75. data/spec/integrations/integration_test_helper.rb +0 -22
  76. data/spec/integrations/rainbows_spec.rb +0 -7
  77. data/spec/integrations/shared_examples.rb +0 -202
  78. data/spec/integrations/thin_spec.rb +0 -7
  79. data/spec/javascripts/firehose/base_spec.coffee +0 -2
  80. data/spec/javascripts/firehose/consumer_spec.coffee +0 -45
  81. data/spec/javascripts/firehose/long_poll_spec.coffee +0 -44
  82. data/spec/javascripts/firehose/multiplexed_consumer_spec.coffee +0 -72
  83. data/spec/javascripts/firehose/transport_spec.coffee +0 -30
  84. data/spec/javascripts/firehose/websocket_spec.coffee +0 -27
  85. data/spec/javascripts/helpers/spec_helper.js +0 -31
  86. data/spec/javascripts/support/jquery-1.11.1.js +0 -10308
  87. data/spec/javascripts/support/json2.js +0 -487
  88. data/spec/javascripts/support/sinon-1.7.3.js +0 -4290
  89. data/spec/javascripts/support/spec_helper.coffee +0 -3
  90. data/spec/lib/assets_spec.rb +0 -35
  91. data/spec/lib/client/consumer_spec.rb +0 -13
  92. data/spec/lib/client/producer_spec.rb +0 -71
  93. data/spec/lib/firehose_spec.rb +0 -7
  94. data/spec/lib/rack/consumer/http_long_poll_spec.rb +0 -30
  95. data/spec/lib/rack/consumer_spec.rb +0 -19
  96. data/spec/lib/rack/ping_spec.rb +0 -28
  97. data/spec/lib/rack/publisher_spec.rb +0 -34
  98. data/spec/lib/server/app_spec.rb +0 -15
  99. data/spec/lib/server/channel_spec.rb +0 -178
  100. data/spec/lib/server/message_buffer_spec.rb +0 -148
  101. data/spec/lib/server/publisher_spec.rb +0 -91
  102. data/spec/lib/server/redis_spec.rb +0 -13
  103. data/spec/lib/server/subscriber_spec.rb +0 -70
  104. data/spec/spec_helper.rb +0 -84
@@ -1,94 +0,0 @@
1
- require 'json'
2
-
3
- # TODO - Spec this thing out. Unfortunately its not tested at all, mostly because the JSON client
4
- # is more important (and tested). Still, this should be tested, at least used by the specs
5
- # to test HTTP functionality of the server.
6
- module Firehose
7
- module Client
8
- module Consumer
9
- # TODO - Test this libs. I had to throw these quickly into our app so that we could get
10
- # some stress testing out of the way.
11
- # TODO - Replace the integration test clients with these guys. You'll want to refactor each
12
- # transport to use on(:message), on(:conncect), and on(:disconnect) callbacks.
13
- TransportNotSupportedError = Class.new(RuntimeError)
14
-
15
- # Build up a benchmark client based on a given URI. Accepts ws:// and http:// for now.
16
- def self.parse(uri)
17
- case transport = ::URI.parse(uri).scheme
18
- # TODO - Fix ws:// transport! See class WebSocket below to udnerstand
19
- # why this doesn't work and support is dropped from the CLI.
20
- # when 'ws'
21
- # Consumer::WebSocket.new(uri)
22
- when 'http'
23
- Consumer::HttpLongPoll.new(uri)
24
- else
25
- raise TransportNotSupportedError.new("Transport #{transport.inspect} not supported.")
26
- end
27
- end
28
-
29
- # TODO - This won't even work. Dropping ws:// above until this is tested. This thing
30
- # should be sending message sequences to Firehose.
31
- # Connect to Firehose via WebSockets and consume messages.
32
- class WebSocket
33
- attr_reader :url, :logger
34
-
35
- def initialize(url, logger = Firehose.logger)
36
- @url, @logger = url, logger
37
- end
38
-
39
- def request
40
- ws = Faye::WebSocket::Client.new(url)
41
- ws.onmessage = lambda do |event|
42
- logger.info "WS | #{event.data[0...40].inspect}"
43
- end
44
- ws.onclose = lambda do |event|
45
- logger.info "WS | Closed"
46
- end
47
- ws.onerror do
48
- logger.error "WS | Failed"
49
- end
50
- end
51
- end
52
-
53
- # Connect to Firehose via HTTP Long Polling and consume messages.
54
- class HttpLongPoll
55
- JITTER = 0.003
56
-
57
- attr_reader :url, :logger
58
-
59
- def initialize(url, logger = Firehose.logger)
60
- @url, @logger = url, logger
61
- end
62
-
63
- def request(last_sequence=0)
64
- http = EM::HttpRequest.new(url, :inactivity_timeout => 0).get(:query => {'last_message_sequence' => last_sequence})
65
- http.callback do
66
- case status = http.response_header.status
67
- when 200
68
- json = JSON.parse(http.response)
69
- next_sequence = json['last_sequence'].to_i
70
- message = json['message']
71
-
72
- logger.info "HTTP 200 | Next Sequence: #{next_sequence} - #{message[0...40].inspect}"
73
- EM::add_timer(jitter) { request next_sequence }
74
- when 204
75
- logger.info "HTTP 204 | Last Sequence #{last_sequence}"
76
- EM::add_timer(jitter) { request last_sequence }
77
- else
78
- logger.error "HTTP #{status} | Failed"
79
- end
80
- end
81
- http.errback do
82
- logger.error "Connection Failed"
83
- end
84
- end
85
-
86
- private
87
- # Random jitter between long poll requests.
88
- def jitter
89
- rand*JITTER
90
- end
91
- end
92
- end
93
- end
94
- end
@@ -1,114 +0,0 @@
1
- require "faraday"
2
- require "uri"
3
-
4
- module Firehose
5
- module Client
6
- module Producer
7
- # Publish messages to Firehose via an HTTP interface.
8
- class Http
9
- # Exception gets raised when a 202 is _not_ received from the server after a message is published.
10
- PublishError = Class.new(RuntimeError)
11
- TimeoutError = Class.new(Faraday::Error::TimeoutError)
12
- DEFAULT_TIMEOUT = 1 # How many seconds should we wait for a publish to take?
13
- DEFAULT_ERROR_HANDLER = ->(e) { raise e }
14
-
15
- # A DSL for publishing requests. This doesn't so much, but lets us call
16
- # Firehose::Client::Producer::Http#publish('message').to('channel'). Slick eh? If you don't like it,
17
- # just all Firehose::Client::Producer::Http#put('message', 'channel')
18
- class Builder
19
- def initialize(producer, message)
20
- @producer, @message = producer, message
21
- self
22
- end
23
-
24
- def to(channel, opts={}, &callback)
25
- @producer.put(@message, channel, opts, &callback)
26
- end
27
- end
28
-
29
- # URI for the Firehose server. This URI does not include the path of the channel.
30
- attr_reader :uri, :timeout
31
-
32
- def initialize(uri = Firehose::URI, timeout=DEFAULT_TIMEOUT)
33
- @uri = ::URI.parse(uri.to_s)
34
- @uri.scheme ||= 'http'
35
- @timeout = timeout
36
- end
37
-
38
- # A DSL for publishing messages.
39
- def publish(message)
40
- Builder.new(self, message)
41
- end
42
-
43
- # Publish the message via HTTP.
44
- def put(message, channel, opts, &block)
45
- ttl = opts[:ttl]
46
- timeout = opts[:timeout] || @timeout || DEFAULT_TIMEOUT
47
- buffer_size = opts[:buffer_size]
48
-
49
- response = conn.put do |req|
50
- req.options[:timeout] = timeout
51
- if conn.path_prefix.nil? || conn.path_prefix == '/'
52
- # This avoids a double / if the channel starts with a / too (which is expected).
53
- req.path = channel
54
- else
55
- if conn.path_prefix =~ /\/\Z/ || channel =~ /\A\//
56
- req.path = [conn.path_prefix, channel].compact.join
57
- else
58
- # Add a / so the prefix and channel aren't just rammed together.
59
- req.path = [conn.path_prefix, channel].compact.join('/')
60
- end
61
- end
62
- req.body = message
63
- req.headers['Cache-Control'] = "max-age=#{ttl.to_i}" if ttl
64
- req.headers["X-Firehose-Buffer-Size"] = buffer_size.to_s if buffer_size
65
- end
66
- response.on_complete do
67
- case response.status
68
- when 202 # Fire off the callback if everything worked out OK.
69
- block.call(response) if block
70
- else
71
- # don't pass along basic auth header, if present
72
- response_data = response.inspect.gsub(/"Authorization"=>"Basic \S+"/, '"Authorization" => "Basic [HIDDEN]"')
73
- endpoint = "#{uri}/#{channel}".gsub(/:\/\/\S+@/, "://")
74
- error_handler.call PublishError.new("Could not publish #{message.inspect} to '#{endpoint}': #{response_data}")
75
- end
76
- end
77
-
78
- # Hide Faraday with this Timeout exception, and through the error handler.
79
- rescue Faraday::Error::TimeoutError => e
80
- error_handler.call TimeoutError.new(e)
81
- end
82
-
83
- # Handle errors that could happen while publishing a message.
84
- def on_error(&block)
85
- @error_handler = block
86
- end
87
-
88
- # Raise an exception if an error occurs when connecting to the Firehose.
89
- def error_handler
90
- @error_handler || DEFAULT_ERROR_HANDLER
91
- end
92
-
93
- # What adapter should Firehose use to PUT the message? List of adapters is
94
- # available at https://github.com/technoweenie/faraday.
95
- def self.adapter=(adapter)
96
- @adapter = adapter
97
- end
98
-
99
- # Use :net_http for the default Faraday adapter.
100
- def self.adapter
101
- @adapter ||= Faraday.default_adapter
102
- end
103
-
104
- private
105
- # Build out a Faraday connection
106
- def conn
107
- @conn ||= Faraday.new(:url => uri.to_s) do |builder|
108
- builder.adapter self.class.adapter
109
- end
110
- end
111
- end
112
- end
113
- end
114
- end
@@ -1,10 +0,0 @@
1
- require 'em-http'
2
- require 'faye/websocket'
3
-
4
- module Firehose
5
- # Ruby clients that connect to Firehose to either publish or consume messages.
6
- module Client
7
- autoload :Consumer, 'firehose/client/consumer'
8
- autoload :Producer, 'firehose/client/producer'
9
- end
10
- end
@@ -1,32 +0,0 @@
1
- require 'logger'
2
-
3
- module Firehose
4
- # Sets up logging
5
- def self.logger
6
- @logger ||= Logger.new($stdout)
7
- end
8
-
9
- def self.logger=(logger)
10
- @logger = logger
11
- end
12
-
13
- self.logger.level = if ENV['LOG_LEVEL']
14
- Logger.const_get(ENV['LOG_LEVEL'].upcase)
15
- else
16
- case ENV['RACK_ENV']
17
- when 'test' then Logger::ERROR
18
- when 'development' then Logger::DEBUG
19
- else Logger::INFO
20
- end
21
- end
22
-
23
- # TODO: Provide some way to allow this to be configured via an ENV variable.
24
- self.logger.formatter = lambda do |severity, time, name, msg|
25
- out_time = time.utc.strftime "%Y-%m-%d %H:%M:%S.%L"
26
- "[#{out_time} ##$$] #{severity} : #{msg}\n"
27
- end
28
- end
29
-
30
- # stdout gets "lost" in Foreman if this isn't here
31
- # https://github.com/ddollar/foreman/wiki/Missing-Output
32
- $stdout.sync = true if ENV['RACK_ENV'] == 'development' || ENV['SYNC_LOGGING']
@@ -1,49 +0,0 @@
1
- module Firehose
2
- module Rack
3
- # Acts as the glue between the HTTP/WebSocket world and the Firehose::Server class,
4
- # which talks directly to the Redis server. Also dispatches between HTTP and WebSocket
5
- # transport handlers depending on the clients' request.
6
- class App
7
- def initialize
8
- yield self if block_given?
9
- end
10
-
11
- def call(env)
12
- # Cache the parsed request so we don't need to re-parse it when we pass
13
- # control onto another app.
14
- req = env['parsed_request'] ||= ::Rack::Request.new(env)
15
- method = req.request_method
16
-
17
- case method
18
- when 'PUT'
19
- # Firehose::Client::Publisher PUT's payloads to the server.
20
- publisher.call(env)
21
- when 'HEAD'
22
- # HEAD requests are used to prevent sockets from timing out
23
- # from inactivity
24
- ping.call(env)
25
- else
26
- # TODO - 'harden' this up with a GET request and throw a "Bad Request"
27
- # HTTP error code. I'd do it now but I'm in a plane and can't think of it.
28
- consumer.call(env)
29
- end
30
- end
31
-
32
- # The consumer pulls messages off of the backend and passes messages to the
33
- # connected HTTP or WebSocket client. This can be configured from the initialization
34
- # method of the rack app.
35
- def consumer
36
- @consumer ||= Consumer.new
37
- end
38
-
39
- private
40
- def publisher
41
- @publisher ||= Publisher.new
42
- end
43
-
44
- def ping
45
- @ping ||= Ping.new
46
- end
47
- end
48
- end
49
- end
@@ -1,159 +0,0 @@
1
- require 'json'
2
-
3
- module Firehose
4
- module Rack
5
- class Consumer
6
- class HttpLongPoll
7
- # How long should we wait before closing out the consuming clients web connection
8
- # for long polling? Most browsers timeout after a connection has been idle for 30s.
9
- TIMEOUT = 20
10
-
11
- attr_accessor :timeout
12
-
13
- def initialize(timeout=TIMEOUT)
14
- @timeout = timeout
15
- yield self if block_given?
16
- end
17
-
18
- def call(env)
19
- if Consumer.multiplexing_request?(env)
20
- MultiplexingHandler.new(@timeout).call(env)
21
- else
22
- DefaultHandler.new(@timeout).call(env)
23
- end
24
- end
25
-
26
- class Handler
27
- include Firehose::Rack::Helpers
28
-
29
- def initialize(timeout=TIMEOUT)
30
- @timeout = timeout
31
- yield self if block_given?
32
- end
33
-
34
- def call(env)
35
- request = request(env)
36
- method = request.request_method
37
-
38
- case method
39
- # GET is how clients subscribe to the queue. When a messages comes in, we flush out a response,
40
- # close down the requeust, and the client then reconnects.
41
- when "GET"
42
- handle_request(request, env)
43
- return ASYNC_RESPONSE
44
- # we use post messages for http long poll multiplexing
45
- when "POST"
46
- if Consumer.multiplexing_request?(env)
47
- handle_request(request, env)
48
- return ASYNC_RESPONSE
49
- end
50
- end
51
-
52
- Firehose.logger.debug "HTTP #{method} not supported"
53
- response(405, "#{method} not supported.", "Allow" => "GET")
54
- end
55
-
56
- private
57
-
58
- # If the request is a CORS request, return those headers, otherwise don't worry 'bout it
59
- def response_headers(env)
60
- cors_origin(env) ? cors_headers(env) : {}
61
- end
62
-
63
- def cors_origin(env)
64
- env['HTTP_ORIGIN']
65
- end
66
-
67
- def cors_headers(env)
68
- # TODO seperate out CORS logic as an async middleware with a Goliath web server.
69
- {'Access-Control-Allow-Origin' => cors_origin(env)}
70
- end
71
-
72
- def request(env)
73
- env['parsed_request'] ||= ::Rack::Request.new(env)
74
- end
75
-
76
- def async_callback(env, code, message = "", headers = nil)
77
- resp_headers = response_headers(env)
78
-
79
- if headers
80
- resp_headers.merge!(headers)
81
- end
82
-
83
- if cb = env["async.callback"]
84
- cb.call response(code, message, resp_headers)
85
- else
86
- Firehose.logger.error "async.callback not set for response: #{message.inspect}"
87
- end
88
- end
89
-
90
- def respond_async(channel, last_sequence, env)
91
- EM.next_tick do
92
- if last_sequence < 0
93
- async_callback env, 400, "The last_message_sequence parameter may not be less than zero"
94
- else
95
- Server::Channel.new(channel).next_messages(last_sequence, :timeout => @timeout).callback do |messages|
96
- # TODO: Can we send all of these messages down in one request? Sending one message per
97
- # request is slow and inefficient. If we change the protocol (3.0?) we could batch the
98
- # messages and send them all down the pipe, then close the conneciton.
99
- message = messages.first
100
- async_callback env, 200, wrap_frame(channel, message)
101
- end.errback do |e|
102
- if e == :timeout
103
- async_callback env, 204
104
- else
105
- Firehose.logger.error "Unexpected error when trying to GET last_sequence #{last_sequence} for path #{channel}: #{e.inspect}"
106
- async_callback env, 500, "Unexpected error"
107
- end
108
- end
109
- end
110
- end
111
- end
112
- end
113
-
114
- class DefaultHandler < Handler
115
- def wrap_frame(channel, message)
116
- JSON.generate :message => message.payload, :last_sequence => message.sequence
117
- end
118
-
119
- def log_request(path, last_sequence, env)
120
- Firehose.logger.debug "HTTP GET with last_sequence #{last_sequence} for path #{path} with query #{env["QUERY_STRING"].inspect}"
121
- end
122
-
123
- def handle_request(request, env)
124
- # Get the Last Message Sequence from the query string.
125
- # Ideally we'd use an HTTP header, but android devices don't let us
126
- # set any HTTP headers for CORS requests.
127
- last_sequence = request.params['last_message_sequence'].to_i
128
- channel = request.path
129
-
130
- log_request channel, last_sequence, env
131
- respond_async channel, last_sequence, env
132
- end
133
- end
134
-
135
- class MultiplexingHandler < Handler
136
- def wrap_frame(channel, message)
137
- JSON.generate channel: channel, :message => message.payload, :last_sequence => message.sequence
138
- end
139
-
140
- def log_request(request, subscriptions, env)
141
- if request.post?
142
- Firehose.logger.debug "HTTP multiplexing POST, subscribing #{subscriptions.inspect}"
143
- else
144
- Firehose.logger.debug "HTTP multiplexing GET with query #{env["QUERY_STRING"].inspect}"
145
- end
146
- end
147
-
148
- def handle_request(request, env)
149
- subscriptions = Consumer.multiplex_subscriptions(request)
150
- log_request request, subscriptions, env
151
- subscriptions.each do |sub|
152
- respond_async(sub[:channel], sub[:message_sequence], env)
153
- end
154
- end
155
- end
156
- end
157
- end
158
- end
159
- end
@@ -1,200 +0,0 @@
1
- require 'faye/websocket'
2
- require 'json'
3
- require "rack/utils"
4
-
5
- module Firehose
6
- module Rack
7
- class Consumer
8
- class WebSocket
9
- # Setup a handler for the websocket connection.
10
- def call(env)
11
- ws = Faye::WebSocket.new(env)
12
- if Consumer.multiplexing_request?(env)
13
- MultiplexingHandler.new(ws)
14
- else
15
- DefaultHandler.new(ws)
16
- end
17
- ws.rack_response
18
- end
19
-
20
- # Determine if the rack request is a WebSocket request.
21
- def self.request?(env)
22
- Faye::WebSocket.websocket?(env)
23
- end
24
-
25
- class Handler
26
- def initialize(ws)
27
- @ws = ws
28
- @req = ::Rack::Request.new ws.env
29
- # Setup the event handlers from this class.
30
- @ws.onopen = method :open
31
- @ws.onclose = method :close
32
- @ws.onerror = method :error
33
- @ws.onmessage = method :message
34
- end
35
-
36
- def parse_message(event)
37
- JSON.parse(event.data, :symbolize_names => true) rescue {}
38
- end
39
-
40
- # Send a JSON message to the client
41
- # Expects message to be a Hash
42
- def send_message(message)
43
- @ws.send JSON.generate(message)
44
- end
45
-
46
- # Log errors if a socket fails. `close` will fire after this to clean up any
47
- # remaining connectons.
48
- def error(event)
49
- Firehose.logger.error "WS connection `#{@req.path}` error. Message: `#{event.message.inspect}`"
50
- end
51
- end
52
-
53
- # Manages connection state for the web socket that's connected
54
- # by the Consumer::WebSocket class. Deals with message sequence,
55
- # connection, failures, and subscription state.
56
- class DefaultHandler < Handler
57
- # Manages messages sent from the connect client to the server. This is mostly
58
- # used to handle heart-beats that are designed to prevent the WebSocket connection
59
- # from timing out from inactivity.
60
- def message(event)
61
- msg = parse_message(event)
62
- seq = msg[:message_sequence]
63
- if msg[:ping] == 'PING'
64
- Firehose.logger.debug "WS ping received, sending pong"
65
- send_message pong: "PONG"
66
- elsif !@subscribed && seq.kind_of?(Integer)
67
- Firehose.logger.debug "Subscribing at message_sequence #{seq}"
68
- subscribe seq
69
- end
70
- end
71
-
72
- # Log a message that the client has connected.
73
- def open(event)
74
- Firehose.logger.debug "WebSocket subscribed to `#{@req.path}`. Waiting for message_sequence..."
75
- end
76
-
77
- # Log a message that hte client has disconnected and reset the state for the class. Clean
78
- # up the subscribers to the channels.
79
- def close(event)
80
- if @deferrable
81
- @deferrable.fail :disconnect
82
- @channel.unsubscribe(@deferrable) if @channel
83
- end
84
- Firehose.logger.debug "WS connection `#{@req.path}` closing. Code: #{event.code.inspect}; Reason #{event.reason.inspect}"
85
- end
86
-
87
- # Subscribe the client to the channel on the server. Asks for
88
- # the last sequence for clients that reconnect.
89
- def subscribe(last_sequence)
90
- @subscribed = true
91
- @channel = Server::Channel.new @req.path
92
- @deferrable = @channel.next_messages last_sequence
93
- @deferrable.callback do |messages|
94
- messages.each do |message|
95
- Firehose.logger.debug "WS sent `#{message.payload}` to `#{@req.path}` with sequence `#{message.sequence}`"
96
- send_message message: message.payload, last_sequence: message.sequence
97
- end
98
- subscribe messages.last.sequence
99
- end
100
- @deferrable.errback do |e|
101
- unless e == :disconnect
102
- Firehose.logger.error "WS Error: #{e}"
103
- EM.next_tick { raise e.inspect }
104
- end
105
- end
106
- end
107
- end
108
-
109
- class MultiplexingHandler < Handler
110
- class Subscription < Struct.new(:channel, :deferrable)
111
- def close
112
- deferrable.fail :disconnect
113
- channel.unsubscribe(deferrable)
114
- end
115
- end
116
-
117
- def initialize(ws)
118
- super(ws)
119
- @subscriptions = {}
120
- subscribe_multiplexed Consumer.multiplex_subscriptions(@req)
121
- end
122
-
123
- def message(event)
124
- msg = parse_message(event)
125
-
126
- if subscriptions = msg[:multiplex_subscribe]
127
- subscriptions = [subscriptions] unless subscriptions.is_a?(Array)
128
- return subscribe_multiplexed(subscriptions)
129
- end
130
-
131
- if channel_names = msg[:multiplex_unsubscribe]
132
- return unsubscribe(channel_names)
133
- end
134
-
135
- if msg[:ping] == 'PING'
136
- Firehose.logger.debug "WS ping received, sending pong"
137
- return send_message pong: "PONG"
138
- end
139
- end
140
-
141
- def open(event)
142
- Firehose.logger.debug "Multiplexing Websocket connected: #{@req.path}"
143
- end
144
-
145
- def close(event)
146
- @subscriptions.each_value(&:close)
147
- @subscriptions.clear
148
- end
149
-
150
- def subscribe_multiplexed(subscriptions)
151
- subscriptions.each do |sub|
152
- Firehose.logger.debug "Subscribing multiplexed to: #{sub}"
153
-
154
- channel, sequence = sub[:channel], sub[:message_sequence]
155
- next if channel.nil?
156
-
157
- subscribe(channel, sequence.to_i)
158
- end
159
- end
160
-
161
- # Subscribe the client to the channel on the server. Asks for
162
- # the last sequence for clients that reconnect.
163
- def subscribe(channel_name, last_sequence)
164
- channel = Server::Channel.new channel_name
165
- deferrable = channel.next_messages last_sequence
166
- subscription = Subscription.new(channel, deferrable)
167
-
168
- @subscriptions[channel_name] = subscription
169
-
170
- deferrable.callback do |messages|
171
- messages.each do |message|
172
- send_message(
173
- channel: channel_name,
174
- message: message.payload,
175
- last_sequence: message.sequence
176
- )
177
- Firehose.logger.debug "WS sent `#{message.payload}` to `#{channel_name}` with sequence `#{message.sequence}`"
178
- end
179
- subscribe channel_name, messages.last.sequence
180
- end
181
-
182
- deferrable.errback do |e|
183
- EM.next_tick { raise e.inspect } unless e == :disconnect
184
- end
185
- end
186
-
187
- def unsubscribe(channel_names)
188
- Firehose.logger.debug "Unsubscribing from channels: #{channel_names}"
189
- Array(channel_names).each do |chan|
190
- if sub = @subscriptions[chan]
191
- sub.close
192
- @subscriptions.delete(chan)
193
- end
194
- end
195
- end
196
- end
197
- end
198
- end
199
- end
200
- end