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