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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5799fb06db34c512846d7e6d11fd403c7bc2b8a7
4
+ data.tar.gz: 798680edb40005c798ab67fc4d13dc1d690209ab
5
+ SHA512:
6
+ metadata.gz: 61edfb6b8f496dfe5497d565dc0e6a7de62d9d1a104e827f589c364188c70126f0a521d597667802a183c3be4e33df1c8387a73ef6dcd9935ed31b7c7e619f87
7
+ data.tar.gz: 2014e85400758f2a0a1b0eb149668ad36b5b20b95dc18a32822c30f307aede8a879075f4de88ff626fa1efc76255c0056a130d413f09710e2e7d68be63036757
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ /example/nginx.log
19
+ /example/nginx.pid
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format documentation
3
+ --order rand
@@ -0,0 +1 @@
1
+ 2.1.2
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in redisse.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Tigerlily
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,152 @@
1
+ # Redisse
2
+
3
+ Redisse is a Redis-backed Ruby library for creating [Server-Sent
4
+ Events](http://www.w3.org/TR/eventsource/), publishing them from your
5
+ application, and serving them to your clients.
6
+
7
+ * **Homepage:**
8
+ [github.com/tigerlily/redisse](https://github.com/tigerlily/redisse)
9
+ * **Documentation:**
10
+ [tigerlily.github.io/redisse](https://tigerlily.github.io/redisse/)
11
+
12
+ ## Features
13
+
14
+ * Pub/Sub split into **channels** for privacy & access rights handling.
15
+
16
+ * **SSE history** via the `Last-Event-Id` header and the `lastEventId` query
17
+ parameter, with a limit of 100 events per channel.
18
+
19
+ * **Long-polling** via the `polling` query parameter. Allows to send several
20
+ events at once for long-polling clients by waiting one second before closing
21
+ the connection.
22
+
23
+ * **Lightweight**: only one Redis connection for history and one for all
24
+ subscriptions, no matter the number of connected clients.
25
+
26
+ * **`missedevents` event fired** when the full requested history could not be
27
+ found, to allow the client to handle the case where events were missed.
28
+
29
+ * **Event types** from SSE are left untouched for your application code, but
30
+ keep in mind that a client will receive events of all types from their
31
+ channels. To handle access rights, use channels instead.
32
+
33
+ ## Rationale
34
+
35
+ Redisse’s design comes from these requirements:
36
+
37
+ * The client wants to listen to several channels but use only one connection.
38
+ (e.g. a single `EventSource` object is created in the browser but you want
39
+ events coming from different Redis channels.)
40
+
41
+ * A server handles the concurrent connections so that the application servers
42
+ don't need to (e.g. Unicorn workers).
43
+
44
+ * The application is written in Ruby, so there needs to be a Ruby API to
45
+ publish events.
46
+
47
+ * The application is written on top of Rack, so the code that lists the Redis
48
+ Pub/Sub channels to subscribe to needs to be able to use Rack middlewares and
49
+ should receive a Rack environment. (e.g. you can use
50
+ [Warden](https://github.com/hassox/warden) as a middleware and simply use
51
+ `env['warden'].user` to decide which channels the user can access.)
52
+
53
+ ### Redirect endpoint
54
+
55
+ The simplest way that last point can be fulfilled is by actually loading and
56
+ running your code in the Redisse server. Unfortunately since it’s
57
+ EventMachine-based, if your method takes a while to returns the channels, all
58
+ the other connected clients will be blocked too. You'll also have some
59
+ duplication between your [Rack config](https://github.com/tigerlily/redisse/blob/9052630e57081714365188a8f55f0549aee03d56/example/config.ru#L30)
60
+ and [Redisse server config](https://github.com/tigerlily/redisse/blob/9052630e57081714365188a8f55f0549aee03d56/example/lib/sse_server.rb#L15).
61
+
62
+ Another way if you use nginx is instead to use a endpoint in your main
63
+ application that will use the header X-Accel-Redirect to redirect to the
64
+ Redisse server, which is now free from your blocking code. The channels will be
65
+ sent instead via the redirect URL. See the [section on nginx](#behind-nginx)
66
+ for more info.
67
+
68
+ ## Installation
69
+
70
+ Add this line to your application's Gemfile:
71
+
72
+ gem 'redisse', '~> 0.4.0'
73
+
74
+ ## Usage
75
+
76
+ Configure Redisse (e.g. in `config/initializers/redisse.rb`):
77
+
78
+ require 'redisse'
79
+
80
+ Redisse.channels do |env|
81
+ %w[ global ]
82
+ end
83
+
84
+ Use the endpoint in your main application (in config.ru or your router):
85
+
86
+ # config.ru Rack
87
+ map "/events" do
88
+ run Redisse.redirect_endpoint
89
+ end
90
+
91
+ # config/routes.rb Rails
92
+ get "/events" => Redisse.redirect_endpoint
93
+
94
+ Run the server:
95
+
96
+ $ redisse --stdout --verbose
97
+
98
+ Get ready to receive events:
99
+
100
+ $ curl localhost:8080 -H 'Accept: text/event-stream'
101
+
102
+ Send a Server-Sent Event:
103
+
104
+ Redisse.publish('global', success: "It's working!")
105
+
106
+ ### Testing
107
+
108
+ In the traditional Rack app specs or tests, use `Redisse.test_mode!`:
109
+
110
+ describe "SSE" do
111
+ before do
112
+ Redisse.test_mode!
113
+ end
114
+
115
+ it "should send a Server-Sent Event" do
116
+ post '/publish', channel: 'global', message: 'Hello'
117
+ expect(Redisse.published.size).to be == 1
118
+ end
119
+ end
120
+
121
+ See [the example app
122
+ specs](https://github.com/tigerlily/redisse/blob/master/example/spec/app_spec.rb).
123
+
124
+ ### Behind nginx
125
+
126
+ When running behind nginx as a reverse proxy, you should disable buffering
127
+ (`proxy_buffering off`) and close the connection to the server when the client
128
+ disconnects (`proxy_ignore_client_abort on`) to preserve resources (otherwise
129
+ connections to Redis will be kept alive longer than necessary).
130
+
131
+ You should take advantage of the [redirect endpoint](#redirect-endpoint)
132
+ instead of directing the SSE requests to the SSE server. Let your Rack
133
+ application determine the channels, but have the request served by the SSE
134
+ server with a redirect (X-Accel-Redirect) to an internal location.
135
+
136
+ In this case, and if you have a large number of long-named channels, the
137
+ internal redirect URL will be long and you might need to increase
138
+ `proxy_buffer_size` from its default in your Rack application location
139
+ configuration. For example, 8k will allow you about 200 channels with UUIDs as
140
+ names, which is quite a lot.
141
+
142
+ You can check the [nginx conf of the
143
+ example](https://github.com/tigerlily/redisse/blob/master/example/nginx.conf)
144
+ for all the details.
145
+
146
+ ## Contributing
147
+
148
+ 1. Fork it
149
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
150
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
151
+ 4. Push to the branch (`git push origin my-new-feature`)
152
+ 5. Create new Pull Request
@@ -0,0 +1,28 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ begin
4
+ require 'yard'
5
+ YARD::Config.load_plugin 'tomdoc'
6
+ YARD::Rake::YardocTask.new do |t|
7
+ t.name = :doc
8
+ end
9
+
10
+ task :gh_pages => :doc do
11
+ sh 'git checkout gh-pages'
12
+ sh 'rsync -a doc/* .'
13
+ sh 'git add .'
14
+ version = "v#{Bundler::GemHelper.gemspec.version}"
15
+ sh "git commit -m 'Documentation for #{version}'"
16
+ sh 'git checkout -'
17
+ end
18
+ rescue LoadError
19
+ end
20
+
21
+ begin
22
+ require 'rspec/core/rake_task'
23
+ RSpec::Core::RakeTask.new(:spec) do |task|
24
+ task.pattern = "{example/,}" + task.pattern
25
+ end
26
+ task :default => :spec
27
+ rescue LoadError
28
+ end
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'redisse/server'
4
+ Redisse.run
@@ -0,0 +1,3 @@
1
+ REDIS_PORT=6380
2
+ REDISSE_REDIS=redis://localhost:$REDIS_PORT/
3
+ REDISSE_PORT=8082
@@ -0,0 +1,37 @@
1
+ # Redisse example: Rack app
2
+
3
+ Get the dependencies:
4
+
5
+ $ bundle
6
+
7
+ Note that the example uses [dotenv](https://github.com/bkeepers/dotenv):
8
+
9
+ $ cat .env
10
+
11
+ Change .env to point to a running Redis server, or simply run the dedicated
12
+ Redis server:
13
+
14
+ $ bin/redis
15
+
16
+ Run the Rack application server:
17
+
18
+ $ bundle exec dotenv rackup --port 8081
19
+
20
+ Run the SSE server:
21
+
22
+ $ bundle exec dotenv redisse --stdout --verbose
23
+
24
+ Finally run nginx to glue them together:
25
+
26
+ $ nginx -p $PWD -c nginx.conf
27
+
28
+ Open [http://localhost:8080/](http://localhost:8080/) in multiple browsers and
29
+ tabs and then send messages to see them replicated.
30
+
31
+ A Rack session cookie is used to randomly select one of four channels
32
+ (`channel_1` to `channel_4`) and simulate different access rights for
33
+ different users of your application.
34
+
35
+ You can also send events from the command line:
36
+
37
+ $ bin/publish global message 'Hello CLI'
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ require 'bundler/setup'
3
+ require 'dotenv'
4
+ Dotenv.load
5
+ require 'redisse'
6
+
7
+ if count = ARGV.detect {|arg| arg.start_with?('N=') }
8
+ ARGV.delete(count)
9
+ count = count[/(\d+)/, 1].to_i
10
+ else
11
+ count = 1
12
+ end
13
+ count.times do
14
+ puts "Event ##{ Redisse.publish ARGV[0], ARGV[1] => ARGV[2] } published"
15
+ end
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ require 'bundler/setup'
3
+ require 'dotenv'
4
+ Dotenv.load
5
+
6
+ puts "Starting Redis server on 127.0.0.1:#{ENV['REDIS_PORT']}"
7
+ Process.exec 'redis-server ' \
8
+ '--daemonize yes ' \
9
+ '--pidfile redis.pid ' \
10
+ '--port $REDIS_PORT ' \
11
+ '--bind 127.0.0.1 ' \
12
+ '--logfile /dev/null ' \
13
+ '--databases 1'
@@ -0,0 +1,40 @@
1
+ require 'bundler/setup'
2
+ require 'dotenv'
3
+ Dotenv.load
4
+
5
+ require 'redisse'
6
+
7
+ Redisse.channels do |env|
8
+ env['rack.session']['channels'] ||=
9
+ %w[ global ] << "channel_#{rand(4)+1}"
10
+ end
11
+
12
+ class Application
13
+ def call(env)
14
+ request = Rack::Request.new env
15
+
16
+ if publish?(request)
17
+ Redisse.publish(request['channel'], request['message'])
18
+ return Rack::Response.new "No Content", 204
19
+ elsif subscriptions?(request)
20
+ return Rack::Response.new Redisse.channels(env).join(", "), 200
21
+ end
22
+
23
+ Rack::Response.new "Not Found", 404
24
+ end
25
+
26
+ def publish?(request)
27
+ request.post? && request.path_info == '/publish'
28
+ end
29
+
30
+ def subscriptions?(request)
31
+ request.get? && request.path_info == '/subscriptions'
32
+ end
33
+ end
34
+
35
+ use Rack::Session::Cookie, secret: 'not a secret'
36
+ use Rack::Static, urls: {"/" => 'index.html'}, root: 'public', index: 'index.html'
37
+ map "/events" do
38
+ run Redisse.redirect_endpoint
39
+ end
40
+ run Application.new
@@ -0,0 +1,29 @@
1
+ pid nginx.pid;
2
+ error_log nginx.log error;
3
+ daemon off;
4
+
5
+ events {
6
+ worker_connections 1024;
7
+ }
8
+
9
+ http {
10
+ server {
11
+ listen 8080;
12
+
13
+ location / {
14
+ proxy_pass http://localhost:8081;
15
+
16
+ # necessary if lots of long-named channels
17
+ #proxy_buffer_size 8k;
18
+ }
19
+
20
+ location /redisse/ {
21
+ internal;
22
+ proxy_pass http://localhost:8082;
23
+ proxy_buffering off;
24
+ proxy_ignore_client_abort on;
25
+ proxy_http_version 1.1;
26
+ }
27
+ }
28
+ access_log off;
29
+ }
@@ -0,0 +1,46 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head><title>Redisse example: Rack app</title></head>
4
+ <body>
5
+ <h1>Redisse example: Rack app</h1>
6
+
7
+ <p>Subscribed to: <span id=subscriptions></span></p>
8
+
9
+ <form id=publish method=POST action=publish>
10
+ <label>Channel
11
+ <input name=channel value=global>
12
+ </label>
13
+ <label>Message
14
+ <input name=message value=Hello>
15
+ </label>
16
+ <input type=submit>
17
+ </form>
18
+
19
+ <pre id=log></pre>
20
+
21
+ <script>
22
+ var subscriptions = document.getElementById('subscriptions')
23
+ var xhr = new XMLHttpRequest()
24
+ xhr.open('GET', '/subscriptions', false)
25
+ xhr.send(null)
26
+ subscriptions.innerHTML = xhr.responseText
27
+
28
+ var form = document.getElementById('publish')
29
+ form.addEventListener('submit', function(e) {
30
+ e.preventDefault()
31
+ var data = new FormData(form)
32
+ var xhr = new XMLHttpRequest()
33
+ xhr.open(form.method, form.action, false)
34
+ xhr.send(data)
35
+ return false
36
+ }, false)
37
+
38
+ var log = document.getElementById('log')
39
+ var source = new EventSource('/events')
40
+ source.addEventListener('message', function(e) {
41
+ log.innerHTML += e.data + "\n"
42
+ }, false)
43
+ </script>
44
+
45
+ </body>
46
+ </html>