redisse 0.4.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 +7 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +152 -0
- data/Rakefile +28 -0
- data/bin/redisse +4 -0
- data/example/.env +3 -0
- data/example/README.md +37 -0
- data/example/bin/publish +15 -0
- data/example/bin/redis +13 -0
- data/example/config.ru +40 -0
- data/example/nginx.conf +29 -0
- data/example/public/index.html +46 -0
- data/example/spec/app_spec.rb +39 -0
- data/lib/redisse.rb +181 -0
- data/lib/redisse/configuration.rb +47 -0
- data/lib/redisse/publisher.rb +70 -0
- data/lib/redisse/redirect_endpoint.rb +45 -0
- data/lib/redisse/server.rb +205 -0
- data/lib/redisse/server/redis.rb +39 -0
- data/lib/redisse/server/responses.rb +22 -0
- data/lib/redisse/server/stats.rb +10 -0
- data/lib/redisse/server_sent_events.rb +17 -0
- data/lib/redisse/version.rb +3 -0
- data/redisse.gemspec +29 -0
- data/spec/example_spec.rb +200 -0
- data/spec/publisher_spec.rb +81 -0
- data/spec/redirect_endpoint_spec.rb +98 -0
- data/spec/server_sent_events_spec.rb +53 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/spec_system_helper.rb +204 -0
- metadata +214 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rack/test'
|
2
|
+
|
3
|
+
$app, _opts = Rack::Builder.parse_file __dir__ + '/../config.ru'
|
4
|
+
|
5
|
+
describe "Example App" do
|
6
|
+
include Rack::Test::Methods
|
7
|
+
|
8
|
+
def app
|
9
|
+
$app
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "/publish" do
|
13
|
+
context "basic" do
|
14
|
+
before do
|
15
|
+
Redisse.test_mode!
|
16
|
+
end
|
17
|
+
|
18
|
+
it "publishes the message to the channel" do
|
19
|
+
post "/publish", channel: 'global', message: 'Hello'
|
20
|
+
expect(Redisse.published.size).to be == 1
|
21
|
+
event = Redisse.published.first
|
22
|
+
expect(event.channel).to be == 'global'
|
23
|
+
expect(event.type).to be == :message
|
24
|
+
expect(event.data).to be == 'Hello'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context "filtered" do
|
29
|
+
before do
|
30
|
+
Redisse.test_filter = :unused_type
|
31
|
+
end
|
32
|
+
|
33
|
+
it "publishes the message with the 'message' type" do
|
34
|
+
post "/publish", channel: 'global', message: 'Hello'
|
35
|
+
expect(Redisse.published.size).to be == 0
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/redisse.rb
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
require 'redisse/version'
|
2
|
+
require 'redisse/publisher'
|
3
|
+
require 'redis'
|
4
|
+
|
5
|
+
# Public: A HTTP API to serve Server-Sent Events via a Redis backend.
|
6
|
+
module Redisse
|
7
|
+
# Public: Gets/Sets the String URL of the Redis server to connect to.
|
8
|
+
#
|
9
|
+
# Note that while the Redis pubsub mechanism works outside of the Redis key
|
10
|
+
# namespace and ignores the database (the path part of the URL), the
|
11
|
+
# database will still be used to store an history of the events sent to
|
12
|
+
# support Last-Event-Id.
|
13
|
+
#
|
14
|
+
# Defaults to the REDISSE_REDIS environment variable and if it is not set, to
|
15
|
+
# redis://localhost:6379/.
|
16
|
+
attr_accessor :redis_server
|
17
|
+
|
18
|
+
# Public: The port on which the server listens.
|
19
|
+
#
|
20
|
+
# Defaults to the REDISSE_PORT environment variable and if it is not set, to
|
21
|
+
# 8080.
|
22
|
+
attr_accessor :default_port
|
23
|
+
|
24
|
+
# Public: The internal URL hierarchy to redirect to with X-Accel-Redirect.
|
25
|
+
#
|
26
|
+
# When this property is set, Redisse will work totally differently. Your Ruby
|
27
|
+
# code will not be loaded by the events server itself, but only by the
|
28
|
+
# {#redirect_endpoint} Rack app that you will have to route to in your Rack
|
29
|
+
# app (e.g. using +map+ in +config.ru+) and this endpoint will redirect to
|
30
|
+
# this internal URL hierarchy.
|
31
|
+
#
|
32
|
+
# Defaults to /redisse.
|
33
|
+
attr_accessor :nginx_internal_url
|
34
|
+
|
35
|
+
# Public: Send an event to subscribers, of the given type.
|
36
|
+
#
|
37
|
+
# All browsers subscribing to the events server will receive a Server-Sent
|
38
|
+
# Event of the chosen type.
|
39
|
+
#
|
40
|
+
# channel - The channel to publish the message to.
|
41
|
+
# type_message - The type of the event and the content of the message, as a
|
42
|
+
# Hash of form { type => message } or simply the message as
|
43
|
+
# a String, for the default event type :message.
|
44
|
+
#
|
45
|
+
# Examples
|
46
|
+
#
|
47
|
+
# Redisse.publish(:global, notice: 'This is a server-sent event.')
|
48
|
+
# Redisse.publish(:global, 'Hello, World!')
|
49
|
+
#
|
50
|
+
# # on the browser side:
|
51
|
+
# var source = new EventSource(eventsURL);
|
52
|
+
# source.addEventListener('notice', function(e) {
|
53
|
+
# console.log(e.data) // logs 'This is a server-sent event.'
|
54
|
+
# }, false)
|
55
|
+
# source.addEventListener('message', function(e) {
|
56
|
+
# console.log(e.data) // logs 'Hello, World!'
|
57
|
+
# }, false)
|
58
|
+
def publish(channel, message)
|
59
|
+
type, message = Hash(message).first if message.respond_to?(:to_h)
|
60
|
+
type ||= :message
|
61
|
+
publisher.publish(channel, message, type)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Public: The list of channels to subscribe to.
|
65
|
+
#
|
66
|
+
# Once {Redisse.channels} has been called, the given block is this method.
|
67
|
+
# The block must satisfy this interface:
|
68
|
+
#
|
69
|
+
# env - The Rack environment for this request.
|
70
|
+
#
|
71
|
+
# Returns an Array of String naming the channels to subscribe to.
|
72
|
+
#
|
73
|
+
# Raises NotImplementedError unless {Redisse.channels} has been called.
|
74
|
+
def channels(env)
|
75
|
+
raise NotImplementedError, "you must call Redisse.channels first"
|
76
|
+
end
|
77
|
+
|
78
|
+
# Public: Use test mode.
|
79
|
+
#
|
80
|
+
# Instead of actually publishing to Redis, events will be stored in
|
81
|
+
# {#published} to use for tests.
|
82
|
+
#
|
83
|
+
# Must be called before each test in order for published events to be
|
84
|
+
# emptied.
|
85
|
+
#
|
86
|
+
# See also {#test_filter=}.
|
87
|
+
#
|
88
|
+
# Examples
|
89
|
+
#
|
90
|
+
# # RSpec
|
91
|
+
# before { Redisse.test_mode! }
|
92
|
+
def test_mode!
|
93
|
+
@publisher = TestPublisher.new
|
94
|
+
end
|
95
|
+
|
96
|
+
# Public: Filter events stored in test mode.
|
97
|
+
#
|
98
|
+
# If set, only events whose type match with the filter are stored in
|
99
|
+
# {#published}. A filter matches by using case equality, which allows using
|
100
|
+
# a simple Symbol or a Proc for more advanced filters:
|
101
|
+
#
|
102
|
+
# Automatically sets {#test_mode!}, so it also clears the previous events.
|
103
|
+
#
|
104
|
+
# Examples
|
105
|
+
#
|
106
|
+
# Redisse.test_filter = -> type { %i(foo baz).include? type }
|
107
|
+
# Redisse.publish :global, foo: 'stored'
|
108
|
+
# Redisse.publish :global, bar: 'skipped'
|
109
|
+
# Redisse.publish :global, baz: 'stored'
|
110
|
+
# Redisse.published.size # => 2
|
111
|
+
def test_filter=(filter)
|
112
|
+
test_mode!
|
113
|
+
publisher.filter = filter
|
114
|
+
end
|
115
|
+
|
116
|
+
# Public: Returns the published events.
|
117
|
+
#
|
118
|
+
# Fails unless {#test_mode!} is set.
|
119
|
+
def published
|
120
|
+
fail "Call #{self}.test_mode! first" unless publisher.respond_to?(:published)
|
121
|
+
publisher.published
|
122
|
+
end
|
123
|
+
|
124
|
+
# Internal: List of middlewares defined with {#use}.
|
125
|
+
#
|
126
|
+
# Used by Goliath to build the server.
|
127
|
+
def middlewares
|
128
|
+
@middlewares ||= []
|
129
|
+
end
|
130
|
+
|
131
|
+
# Public: Define a middleware for the server.
|
132
|
+
#
|
133
|
+
# See {https://github.com/postrank-labs/goliath/wiki/Middleware Goliath middlewares}.
|
134
|
+
#
|
135
|
+
# Examples
|
136
|
+
#
|
137
|
+
# Redisse.use MyMiddleware, foo: true
|
138
|
+
def use(middleware, *args, &block)
|
139
|
+
middlewares << [middleware, args, block]
|
140
|
+
end
|
141
|
+
|
142
|
+
# Public: Define a Goliath plugin to run with the server.
|
143
|
+
#
|
144
|
+
# See {https://github.com/postrank-labs/goliath/wiki/Plugins Goliath plugins}.
|
145
|
+
def plugin(name, *args)
|
146
|
+
plugins << [name, args]
|
147
|
+
end
|
148
|
+
|
149
|
+
# Public: The Rack application that redirects to {#nginx_internal_url}.
|
150
|
+
#
|
151
|
+
# If you set {#nginx_internal_url}, you need to call this Rack application
|
152
|
+
# to redirect to the Redisse server.
|
153
|
+
#
|
154
|
+
# Also note that when using the redirect endpoint, two channel names are
|
155
|
+
# reserved, and cannot be used: +polling+ and +lastEventId+.
|
156
|
+
#
|
157
|
+
# Examples
|
158
|
+
#
|
159
|
+
# map "/events" { run Redisse.redirect_endpoint }
|
160
|
+
def redirect_endpoint
|
161
|
+
@redirect_endpoint ||= RedirectEndpoint.new self
|
162
|
+
end
|
163
|
+
|
164
|
+
autoload :RedirectEndpoint, __dir__ + '/redisse/redirect_endpoint'
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
def plugins
|
169
|
+
@plugins ||= []
|
170
|
+
end
|
171
|
+
|
172
|
+
def publisher
|
173
|
+
@publisher ||= RedisPublisher.new(redis)
|
174
|
+
end
|
175
|
+
|
176
|
+
def redis
|
177
|
+
@redis ||= Redis.new(url: redis_server)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
require 'redisse/configuration'
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Redisse
|
2
|
+
extend self
|
3
|
+
|
4
|
+
# Public: Define the list of channels to subscribe to.
|
5
|
+
#
|
6
|
+
# Calls the given block with a Rack environment, the block is expected to
|
7
|
+
# return a list of channels the current user has access to. The list is then
|
8
|
+
# coerced using +Kernel#Array+.
|
9
|
+
#
|
10
|
+
# Once the block is defined, other calls will be handled by the block
|
11
|
+
# directly, as if the method had been redefined directly. It simply gives a
|
12
|
+
# nicer API:
|
13
|
+
#
|
14
|
+
# Redisse.channels do |env|
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# vs
|
18
|
+
#
|
19
|
+
# def Redisse.channels(env)
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# block - The block that lists the channels for the given Rack environment.
|
23
|
+
#
|
24
|
+
# Examples
|
25
|
+
#
|
26
|
+
# Redisse.channels do |env|
|
27
|
+
# %w( comment post )
|
28
|
+
# end
|
29
|
+
# # will result in subscriptions to 'comment' and 'post' channels.
|
30
|
+
#
|
31
|
+
# Redisse.channels({})
|
32
|
+
# # => ["comment", "post"]
|
33
|
+
def self.channels(*, &block)
|
34
|
+
if block
|
35
|
+
# overwrite method with block
|
36
|
+
define_singleton_method :channels, &block
|
37
|
+
else
|
38
|
+
super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
self.redis_server = ENV['REDISSE_REDIS'] ||
|
43
|
+
'redis://localhost:6379/'
|
44
|
+
self.default_port = ENV['REDISSE_PORT'] ||
|
45
|
+
8080
|
46
|
+
self.nginx_internal_url = '/redisse'
|
47
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'redisse/server_sent_events'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Redisse
|
5
|
+
# Internal: Publisher that pushes to Redis with history.
|
6
|
+
class RedisPublisher
|
7
|
+
include ServerSentEvents
|
8
|
+
|
9
|
+
REDISSE_LAST_EVENT_ID = 'redisse:lastEventId'.freeze
|
10
|
+
HISTORY_SIZE = 100
|
11
|
+
|
12
|
+
def initialize(redis)
|
13
|
+
@redis = redis or raise 'RedisPublisher needs a Redis client'
|
14
|
+
end
|
15
|
+
|
16
|
+
def publish(channel, data, type)
|
17
|
+
event_id = @redis.incr(REDISSE_LAST_EVENT_ID)
|
18
|
+
event = server_sent_event(data, type: type, id: event_id)
|
19
|
+
@redis.publish(channel, event)
|
20
|
+
@redis.zadd(channel, event_id, event)
|
21
|
+
@redis.zremrangebyrank(channel, 0, -1-HISTORY_SIZE)
|
22
|
+
event_id
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Internal: Publisher that stores events in memory for easy testing.
|
27
|
+
#
|
28
|
+
# See {Redisse#test_mode! Redisse#test_mode!}.
|
29
|
+
class TestPublisher
|
30
|
+
def initialize
|
31
|
+
@published = []
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :published
|
35
|
+
|
36
|
+
attr_accessor :filter
|
37
|
+
|
38
|
+
def publish(channel, data, type)
|
39
|
+
return if filter && !(filter === type)
|
40
|
+
@published << TestEvent.new(channel, data, type)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Define then reopen instead of using the block of Struct.new for YARD.
|
45
|
+
TestEvent = Struct.new :channel, :data, :type
|
46
|
+
|
47
|
+
# Public: An event in test mode.
|
48
|
+
#
|
49
|
+
# You can re-open or add modules to this class if you want to add behavior
|
50
|
+
# to events found in {Redisse#published Redisse#published} for easier
|
51
|
+
# testing.
|
52
|
+
#
|
53
|
+
# Examples
|
54
|
+
#
|
55
|
+
# class Redisse::TestEvent
|
56
|
+
# def yml
|
57
|
+
# YAML.load data
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# def private?
|
61
|
+
# channel.start_with? 'private'
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
class TestEvent
|
65
|
+
# Public: Helper method to parse the Event data as JSON.
|
66
|
+
def json
|
67
|
+
JSON.parse(data)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Redisse
|
4
|
+
|
5
|
+
# Public: Rack app that redirects to the Redisse server via X-Accel-Redirect.
|
6
|
+
class RedirectEndpoint
|
7
|
+
|
8
|
+
def initialize(redisse)
|
9
|
+
@redisse = redisse
|
10
|
+
self.base_url = redisse.nginx_internal_url
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
response = Rack::Response.new
|
15
|
+
response['X-Accel-Redirect'] = redirect_url(env)
|
16
|
+
response
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def redirect_url(env)
|
22
|
+
channels = @redisse.channels(env)
|
23
|
+
fail 'Wrong channel "polling"' if channels.include? 'polling'
|
24
|
+
fail 'Reserved channel "lastEventId"' if channels.include? 'lastEventId'
|
25
|
+
@base_url + '?' + URI.encode_www_form(redirect_options(env) + channels)
|
26
|
+
end
|
27
|
+
|
28
|
+
def redirect_options(env)
|
29
|
+
params = URI.decode_www_form(env['QUERY_STRING'])
|
30
|
+
[].tap do |options|
|
31
|
+
options << 'polling'.freeze if params.assoc('polling'.freeze)
|
32
|
+
last_event_id_param = params.assoc('lastEventId')
|
33
|
+
options << last_event_id_param if last_event_id_param
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def base_url=(url)
|
38
|
+
url = String(url)
|
39
|
+
url += "/" unless url.end_with? '/'
|
40
|
+
@base_url = url
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
require 'redisse'
|
2
|
+
require 'goliath/api'
|
3
|
+
require 'rack/accept_media_types'
|
4
|
+
require 'goliath/runner'
|
5
|
+
require 'em-hiredis'
|
6
|
+
|
7
|
+
module Redisse
|
8
|
+
|
9
|
+
# Public: Run the server.
|
10
|
+
#
|
11
|
+
# If you use the provided binary you don't need to call this method.
|
12
|
+
#
|
13
|
+
# By default, the {#channels} method is called directly.
|
14
|
+
#
|
15
|
+
# If {#nginx_internal_url} is set, the channels will actually come from the
|
16
|
+
# internal redirect URL generated in the Rack app by {#redirect_endpoint}.
|
17
|
+
def run
|
18
|
+
run_as_standalone if nginx_internal_url
|
19
|
+
server = Server.new(self)
|
20
|
+
runner = Goliath::Runner.new(ARGV, server)
|
21
|
+
runner.app = Goliath::Rack::Builder.build(self, server)
|
22
|
+
runner.load_plugins([Server::Stats] + plugins)
|
23
|
+
runner.run
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# Internal: Redefine {#channels} to find channels in the redirect URL.
|
29
|
+
def run_as_standalone
|
30
|
+
channels do |env|
|
31
|
+
query_string = env['QUERY_STRING'] || ''
|
32
|
+
channels = query_string.split('&').map { |channel|
|
33
|
+
URI.decode_www_form_component(channel)
|
34
|
+
}
|
35
|
+
channels.delete('polling')
|
36
|
+
channels.delete_if {|channel| channel.start_with?('lastEventId=') }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Internal: Goliath::API class that defines the server.
|
41
|
+
#
|
42
|
+
# See {Redisse#run}.
|
43
|
+
class Server < Goliath::API
|
44
|
+
require 'redisse/server/stats'
|
45
|
+
require 'redisse/server/responses'
|
46
|
+
include Responses
|
47
|
+
require 'redisse/server/redis'
|
48
|
+
include Redis
|
49
|
+
|
50
|
+
# Public: Delay between receiving a message and closing the connection.
|
51
|
+
#
|
52
|
+
# Closing the connection is necessary when using long polling, because the
|
53
|
+
# client is not able to read the data before the connection is closed. But
|
54
|
+
# instead of closing immediately, we delay a bit closing the connection to
|
55
|
+
# give a chance for several messages to be sent in a row.
|
56
|
+
LONG_POLLING_DELAY = 1
|
57
|
+
|
58
|
+
# Public: The period between heartbeats in seconds.
|
59
|
+
HEARTBEAT_PERIOD = 15
|
60
|
+
|
61
|
+
def initialize(redisse)
|
62
|
+
@redisse = redisse
|
63
|
+
super()
|
64
|
+
end
|
65
|
+
|
66
|
+
def response(env)
|
67
|
+
return not_acceptable unless acceptable?(env)
|
68
|
+
channels = Array(redisse.channels(env))
|
69
|
+
return not_found if channels.empty?
|
70
|
+
subscribe(env, channels) or return service_unavailable
|
71
|
+
send_history_events(env, channels)
|
72
|
+
heartbeat(env)
|
73
|
+
streaming_response(200, {
|
74
|
+
'Content-Type' => 'text/event-stream',
|
75
|
+
'Cache-Control' => 'no-cache',
|
76
|
+
'X-Accel-Buffering' => 'no',
|
77
|
+
})
|
78
|
+
end
|
79
|
+
|
80
|
+
def on_close(env)
|
81
|
+
env.status[:stats][:connected] -= 1
|
82
|
+
env.status[:stats][:served] += 1
|
83
|
+
unsubscribe(env)
|
84
|
+
stop_heartbeat(env)
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
attr_reader :redisse
|
90
|
+
|
91
|
+
def subscribe(env, channels)
|
92
|
+
return unless pubsub { env.stream_close }
|
93
|
+
env.status[:stats][:connected] += 1
|
94
|
+
env.logger.debug { "Subscribing to #{channels}" }
|
95
|
+
env_sender = -> event { send_event(env, event) }
|
96
|
+
pubsub_subcribe(channels, env_sender)
|
97
|
+
env['redisse.unsubscribe'.freeze] = -> do
|
98
|
+
pubsub_unsubscribe_proc(channels, env_sender)
|
99
|
+
end
|
100
|
+
true
|
101
|
+
end
|
102
|
+
|
103
|
+
def heartbeat(env)
|
104
|
+
env['redisse.heartbeat_timer'.freeze] = EM.add_periodic_timer(HEARTBEAT_PERIOD) do
|
105
|
+
env.logger.debug "Sending heartbeat".freeze
|
106
|
+
env.stream_send(": hb\n".freeze)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def stop_heartbeat(env)
|
111
|
+
return unless timer = env['redisse.heartbeat_timer'.freeze]
|
112
|
+
env.logger.debug "Stopping heartbeat".freeze
|
113
|
+
timer.cancel
|
114
|
+
end
|
115
|
+
|
116
|
+
def unsubscribe(env)
|
117
|
+
return unless unsubscribe = env['redisse.unsubscribe'.freeze]
|
118
|
+
env['redisse.unsubscribe'.freeze] = nil
|
119
|
+
env.logger.debug "Unsubscribing".freeze
|
120
|
+
unsubscribe.call
|
121
|
+
end
|
122
|
+
|
123
|
+
def send_event(env, event)
|
124
|
+
env.status[:stats][:events] += 1
|
125
|
+
env.logger.debug { "Sending:\n#{event.chomp.chomp}" }
|
126
|
+
env.stream_send(event)
|
127
|
+
return unless long_polling?(env)
|
128
|
+
env["redisse.long_polling_timer".freeze] ||= EM.add_timer(LONG_POLLING_DELAY) do
|
129
|
+
env.stream_close
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def long_polling?(env)
|
134
|
+
key = "redisse.long_polling".freeze
|
135
|
+
env.fetch(key) do
|
136
|
+
env[key] = Rack::Request.new(env).GET.keys.include?('polling')
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def send_history_events(env, channels)
|
141
|
+
last_event_id = last_event_id(env)
|
142
|
+
return unless last_event_id
|
143
|
+
EM::Synchrony.next_tick do
|
144
|
+
events = events_for_channels(channels, last_event_id)
|
145
|
+
env.logger.debug { "Sending #{events.size} history events" }
|
146
|
+
if (first = events.first) && first.start_with?('type: missedevents')
|
147
|
+
env.status[:stats][:missing] += 1
|
148
|
+
end
|
149
|
+
events.each { |event| send_event(env, event) }
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def last_event_id(env)
|
154
|
+
last_event_id = env['HTTP_LAST_EVENT_ID'] ||
|
155
|
+
Rack::Request.new(env).GET['lastEventId']
|
156
|
+
last_event_id = last_event_id.to_i
|
157
|
+
last_event_id.nonzero? && last_event_id
|
158
|
+
end
|
159
|
+
|
160
|
+
def events_for_channels(channels, last_event_id)
|
161
|
+
events_with_ids = channels.each_with_object([]) { |channel, events|
|
162
|
+
channel_events = events_for_channel(channel, last_event_id)
|
163
|
+
events.concat(channel_events)
|
164
|
+
}.sort_by!(&:last)
|
165
|
+
handle_missing_events(events_with_ids, last_event_id)
|
166
|
+
events_with_ids.map(&:first)
|
167
|
+
end
|
168
|
+
|
169
|
+
def handle_missing_events(events_with_ids, last_event_id)
|
170
|
+
first_event, first_event_id = events_with_ids.first
|
171
|
+
return unless first_event
|
172
|
+
if first_event_id == last_event_id
|
173
|
+
events_with_ids.shift
|
174
|
+
else
|
175
|
+
event = ServerSentEvents.server_sent_event(nil, type: :missedevents)
|
176
|
+
events_with_ids.unshift([event])
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def events_for_channel(channel, last_event_id)
|
181
|
+
df = redis.zrangebyscore(channel, last_event_id, '+inf', 'withscores')
|
182
|
+
events_scores = EM::Synchrony.sync(df)
|
183
|
+
events_scores.each_slice(2).map do |event, score|
|
184
|
+
[event, score.to_i]
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def acceptable?(env)
|
189
|
+
accept_media_types = Rack::AcceptMediaTypes.new(env['HTTP_ACCEPT'])
|
190
|
+
accept_media_types.include?('text/event-stream')
|
191
|
+
end
|
192
|
+
|
193
|
+
public
|
194
|
+
|
195
|
+
def options_parser(opts, options)
|
196
|
+
opts.on '--redis REDIS_URL', 'URL of the Redis connection' do |url|
|
197
|
+
redisse.redis_server = url
|
198
|
+
end
|
199
|
+
default_port = redisse.default_port
|
200
|
+
return unless default_port
|
201
|
+
options[:port] = default_port
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
205
|
+
end
|