jugglite 0.0.1.alpha
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.
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +40 -0
- data/Rakefile +5 -0
- data/bin/jugglite +15 -0
- data/jugglite.gemspec +24 -0
- data/lib/jugglite/app.rb +119 -0
- data/lib/jugglite/deferrable_body.rb +22 -0
- data/lib/jugglite/sse_connection.rb +42 -0
- data/lib/jugglite/version.rb +3 -0
- data/lib/jugglite.rb +5 -0
- data/spec/acceptance/stream_and_publish_spec.rb +58 -0
- data/spec/benchmark/max_connections.rb +57 -0
- data/spec/spec_helper.rb +19 -0
- metadata +131 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 andruby
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# Jugglite
|
2
|
+
|
3
|
+
Jugglite is a replacement for the incredible [Juggernaut](https://github.com/maccman/juggernaut) by Maccman. It uses [Server Sent Events](http://www.html5rocks.com/en/tutorials/eventsource/basics/) to push events from your application to the client's browser.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'jugglite'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install jugglite
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
Jugglite comes with a binary. This binary runs a thin server that listens on redis for application messages and passes it along to all connected clients.
|
22
|
+
|
23
|
+
You can run the binary from any terminal
|
24
|
+
`jugglite`
|
25
|
+
|
26
|
+
TODO: Foreman & multiple processes
|
27
|
+
|
28
|
+
TODO: Behind nginx so the client connects on one port
|
29
|
+
|
30
|
+
## Contributing
|
31
|
+
|
32
|
+
1. Fork it
|
33
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
34
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
35
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
36
|
+
5. Create new Pull Request
|
37
|
+
|
38
|
+
## License
|
39
|
+
|
40
|
+
Jugglite is licensed under the [MIT license](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/jugglite
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
host = ENV['HOST'] || '127.0.0.1'
|
4
|
+
port = (ENV['PORT'] || 3000).to_i
|
5
|
+
file_descriptors = (ENV['FD_SIZE'] || 202400).to_i
|
6
|
+
|
7
|
+
require 'jugglite'
|
8
|
+
|
9
|
+
new_size = EM.set_descriptor_table_size( file_descriptors )
|
10
|
+
STDERR.puts "New descriptor-table size is #{new_size}"
|
11
|
+
EM.epoll
|
12
|
+
EM.kqueue
|
13
|
+
|
14
|
+
STDERR.puts "Starting server at #{host}:#{port}"
|
15
|
+
Thin::Server.start(host, port, Jugglite::App.new)
|
data/jugglite.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'jugglite/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "jugglite"
|
8
|
+
gem.version = Jugglite::VERSION
|
9
|
+
gem.authors = ["andruby"]
|
10
|
+
gem.email = ["andrew@bedesign.be"]
|
11
|
+
gem.description = %q{Lightweight SSE server}
|
12
|
+
gem.summary = %q{Server Sent Events server written in rack on top of thin inspired by Juggernaut for real time push}
|
13
|
+
gem.homepage = "http://github.com/andruby/jugglite"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency('thin')
|
21
|
+
gem.add_dependency('em-hiredis')
|
22
|
+
gem.add_development_dependency('redis')
|
23
|
+
gem.add_development_dependency('rspec')
|
24
|
+
end
|
data/lib/jugglite/app.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'em-hiredis'
|
3
|
+
|
4
|
+
module Jugglite
|
5
|
+
# Let's go for plain Rack y'all
|
6
|
+
class App
|
7
|
+
AsyncResponse = [-1, {}, []].freeze
|
8
|
+
Headers = {'Content-Type' => 'text/event-stream;charset=utf-8'}
|
9
|
+
|
10
|
+
# Options include:
|
11
|
+
# +path+ : the URI path to listen to ('/stream')
|
12
|
+
# +keepalive_timeout+ : the timeout in seconds between keepalive comments
|
13
|
+
def initialize(app = nil, options = {})
|
14
|
+
@app = app
|
15
|
+
@options = {
|
16
|
+
path: '/stream',
|
17
|
+
keepalive_timeout: 20
|
18
|
+
}.merge(options)
|
19
|
+
STDERR.puts "Registered Jugglite to listen to #{@options[:path]}"
|
20
|
+
@subscription_map = {}
|
21
|
+
EventMachine::next_tick { setup_redis }
|
22
|
+
EventMachine::next_tick { setup_keepalive }
|
23
|
+
end
|
24
|
+
|
25
|
+
def call(env)
|
26
|
+
if @app.nil? || (env["PATH_INFO"] == @options[:path])
|
27
|
+
handle_stream(env)
|
28
|
+
else
|
29
|
+
@app.call(env)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def handle_stream(env)
|
35
|
+
request = Rack::Request.new(env)
|
36
|
+
connection = SseConnection.new(request)
|
37
|
+
|
38
|
+
# Get the headers out there, let the client know we're alive...
|
39
|
+
EventMachine::next_tick do
|
40
|
+
register_connection(connection)
|
41
|
+
# Calling thin's Connection.post_process([status, headers, body])
|
42
|
+
# This is how you start a response to the client asynchronously
|
43
|
+
env['async.callback'].call [200, Headers, connection.body]
|
44
|
+
connection.comment("registered to channels: #{channels_for_request(request).to_a.join(', ')}")
|
45
|
+
end
|
46
|
+
|
47
|
+
connection.callback { unregister_connection(connection) }
|
48
|
+
connection.errback { unregister_connection(connection) }
|
49
|
+
|
50
|
+
# Needed for crappy Rack::Lint
|
51
|
+
throw :async
|
52
|
+
|
53
|
+
# Returning a status of -1 keeps the connection open
|
54
|
+
# You need to use env['async.callback'].call to send the status, headers & body later
|
55
|
+
AsyncResponse
|
56
|
+
end
|
57
|
+
|
58
|
+
def setup_redis
|
59
|
+
@redis_channels = Set.new
|
60
|
+
@async_redis = EM::Hiredis.connect
|
61
|
+
@async_redis.on(:message) do |channel, message|
|
62
|
+
expedite_incoming_message(channel, message)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def setup_keepalive
|
67
|
+
EventMachine::add_periodic_timer(@options[:keepalive_timeout]) do
|
68
|
+
count = @subscription_map.count
|
69
|
+
@subscription_map.each_key do |connection|
|
70
|
+
connection.keepalive(count)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def channels_for_request(request)
|
76
|
+
# TODO: Sanitize? Check signature?
|
77
|
+
channel = Array(request.params["channel"])
|
78
|
+
Set.new(channel)
|
79
|
+
end
|
80
|
+
|
81
|
+
def register_connection(connection)
|
82
|
+
requested_channels = channels_for_request(connection.request)
|
83
|
+
subscribe_to_new_channels(requested_channels - @redis_channels)
|
84
|
+
@subscription_map[connection] = requested_channels
|
85
|
+
end
|
86
|
+
|
87
|
+
def unregister_connection(connection)
|
88
|
+
@subscription_map.delete(connection)
|
89
|
+
end
|
90
|
+
|
91
|
+
def subscribe_to_new_channels(channels)
|
92
|
+
channels.each do |channel|
|
93
|
+
puts "Listening to channel: #{channel}"
|
94
|
+
@async_redis.subscribe(channel)
|
95
|
+
@redis_channels << channel
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def expedite_incoming_message(channel, message)
|
100
|
+
no_connection_listening = true
|
101
|
+
# Select upfront and use EM::Iterator
|
102
|
+
@subscription_map.each do |connection, channels|
|
103
|
+
if channels.include?(channel)
|
104
|
+
connection.write message
|
105
|
+
no_connection_listening = false
|
106
|
+
end
|
107
|
+
end
|
108
|
+
# We stop listening to a channel whenever a message comes in from a channel
|
109
|
+
# which has no associated connections
|
110
|
+
# Why? Because clients will disconnect and connect all the time when they load new pages
|
111
|
+
# But we don't want to subscribe and unsubscribe from redis all the time.
|
112
|
+
if no_connection_listening
|
113
|
+
puts "Stop listening to channel: #{channel}"
|
114
|
+
@async_redis.unsubscribe(channel)
|
115
|
+
@redis_channels.delete(channel)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Jugglite
|
2
|
+
class DeferrableBody
|
3
|
+
include EventMachine::Deferrable
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@queue = EM::Queue.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def write(body)
|
10
|
+
@queue.push(body)
|
11
|
+
end
|
12
|
+
|
13
|
+
def each &blk
|
14
|
+
@body_callback = blk
|
15
|
+
processor = proc { |item|
|
16
|
+
@body_callback.call(item)
|
17
|
+
@queue.pop(&processor)
|
18
|
+
}
|
19
|
+
@queue.pop(&processor)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Jugglite
|
2
|
+
class SseConnection
|
3
|
+
attr_reader :body, :request
|
4
|
+
|
5
|
+
def initialize(request)
|
6
|
+
@request = request
|
7
|
+
@body = DeferrableBody.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def write(message, options = {})
|
11
|
+
buffer = ""
|
12
|
+
options.each do |k, v|
|
13
|
+
buffer << "#{k}: #{v}\n"
|
14
|
+
end
|
15
|
+
message.each_line do |line|
|
16
|
+
buffer << "data: #{line.strip}\n"
|
17
|
+
end
|
18
|
+
@body.write(buffer << "\n")
|
19
|
+
end
|
20
|
+
|
21
|
+
def keepalive(extra=nil)
|
22
|
+
# From http://dev.w3.org/html5/eventsource/#notes
|
23
|
+
comment("keepalive #{extra}")
|
24
|
+
end
|
25
|
+
|
26
|
+
def comment(comment)
|
27
|
+
@body.write(": #{comment}\n")
|
28
|
+
end
|
29
|
+
|
30
|
+
def close
|
31
|
+
@body.succeed
|
32
|
+
end
|
33
|
+
|
34
|
+
def callback(&block)
|
35
|
+
@body.callback(&block)
|
36
|
+
end
|
37
|
+
|
38
|
+
def errback(&block)
|
39
|
+
@body.errback(&block)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/jugglite.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'net/http'
|
3
|
+
|
4
|
+
# Execute a GET request for +channel+ that reads the body in chunks
|
5
|
+
# until the +regexp+ is received or +timeout+ seconds have passed
|
6
|
+
def listen_on_channel_until(channel, regexp, timeout = 5)
|
7
|
+
Net::HTTP.start(@host, @port) do |http|
|
8
|
+
request = Net::HTTP::Get.new("/?channel=#{@channel}")
|
9
|
+
|
10
|
+
body = ""
|
11
|
+
http.request(request) do |response|
|
12
|
+
start_time = Time.now
|
13
|
+
response.read_body do |chunk|
|
14
|
+
body << chunk
|
15
|
+
body.should include(": registered to channels: #{@channel}")
|
16
|
+
http.finish
|
17
|
+
break
|
18
|
+
end
|
19
|
+
break
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "Streaming and publishing" do
|
25
|
+
before(:all) do
|
26
|
+
@host = '127.0.0.1'
|
27
|
+
@port = rand(10000)+10000
|
28
|
+
@channel = "randomized:test:#{rand(2**32)}"
|
29
|
+
@app = nil
|
30
|
+
@thread = Thread.new do
|
31
|
+
@app = Thin::Server.new(@host, @port, Jugglite::App.new)
|
32
|
+
@app.start
|
33
|
+
end
|
34
|
+
sleep(0.01) until @app && @app.running?
|
35
|
+
end
|
36
|
+
|
37
|
+
after(:all) do
|
38
|
+
@app.stop!
|
39
|
+
@thread.join
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should keep a GET open" do
|
43
|
+
Net::HTTP.start(@host, @port) do |http|
|
44
|
+
request = Net::HTTP::Get.new("/?channel=#{@channel}")
|
45
|
+
|
46
|
+
body = ""
|
47
|
+
http.request(request) do |response|
|
48
|
+
response.read_body do |chunk|
|
49
|
+
body << chunk
|
50
|
+
body.should include(": registered to channels: #{@channel}")
|
51
|
+
http.finish
|
52
|
+
break
|
53
|
+
end
|
54
|
+
break
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# Will try to open as many connections as possible
|
2
|
+
# at a rate of about 500 new connections per second.
|
3
|
+
# Every 2 seconds the script will print out the number of:
|
4
|
+
# connections triggered, connections that received headers,
|
5
|
+
# connections failed and connections finished.
|
6
|
+
|
7
|
+
# Optional env variables and their defaults:
|
8
|
+
# HOST: the hostname to connect to (127.0.0.1)
|
9
|
+
# PORT: the port to connect to (3000)
|
10
|
+
# RATE: the number of connections per second to open (1000)
|
11
|
+
# FD_SIZE: try and increase the number of open file descriptors (202400)
|
12
|
+
|
13
|
+
# Note: You'll probably need to increase the open file descriptor limit
|
14
|
+
# for your platform to reach the limits.
|
15
|
+
# On *nix you can use "ulimit -n XXXXX" to set and "ulimit -n" to read the limit
|
16
|
+
|
17
|
+
host = ENV['HOST'] || '127.0.0.1'
|
18
|
+
port = (ENV['PORT'] || 3000).to_i
|
19
|
+
$between_time = 1.0 / (ENV['RATE'] || 2000).to_i
|
20
|
+
file_descriptors = (ENV['FD_SIZE'] || 202400).to_i
|
21
|
+
$url = "http://#{host}:#{port}/stream"
|
22
|
+
|
23
|
+
require 'eventmachine'
|
24
|
+
require 'redis'
|
25
|
+
require 'em-http-request'
|
26
|
+
|
27
|
+
new_size = EM.set_descriptor_table_size(file_descriptors)
|
28
|
+
STDERR.puts "New EventMachine descriptor-table size is #{new_size}"
|
29
|
+
EM.epoll
|
30
|
+
EM.kqueue
|
31
|
+
|
32
|
+
$trigger_count = 0
|
33
|
+
$headers_count = 0
|
34
|
+
$error_count = 0
|
35
|
+
$finish_count = 0
|
36
|
+
|
37
|
+
def connect_to_stream(counter)
|
38
|
+
http = EventMachine::HttpRequest.new($url, :inactivity_timeout => 30).get
|
39
|
+
http.headers { |hash| $headers_count+= 1 }
|
40
|
+
http.callback { |result| $finish_count+= 1 }
|
41
|
+
http.errback { |obj| $error_count+=1 }
|
42
|
+
end
|
43
|
+
|
44
|
+
EM.run do
|
45
|
+
launch_timer = EventMachine::add_periodic_timer($between_time) do
|
46
|
+
connect_to_stream($trigger_count+=1)
|
47
|
+
end
|
48
|
+
|
49
|
+
EM::next_tick { $start_time = Time.now } # Set the start time
|
50
|
+
|
51
|
+
monitor_timer = EventMachine::add_periodic_timer(2) do
|
52
|
+
secs_elapsed = (Time.now - $start_time).round
|
53
|
+
print "#{'%4i' % secs_elapsed} triggered: #{'%6i' % $trigger_count}, "
|
54
|
+
print "headered: #{'%6i' % $headers_count}, errors: #{'%6i' % $error_count}, "
|
55
|
+
puts "finish_count: #{'%6i' % $finish_count}"
|
56
|
+
end
|
57
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative '../lib/jugglite'
|
2
|
+
|
3
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
4
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
5
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
6
|
+
# loaded once.
|
7
|
+
#
|
8
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
9
|
+
RSpec.configure do |config|
|
10
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
11
|
+
config.run_all_when_everything_filtered = true
|
12
|
+
config.filter_run :focus
|
13
|
+
|
14
|
+
# Run specs in random order to surface order dependencies. If you find an
|
15
|
+
# order dependency and want to debug it, you can fix the order by providing
|
16
|
+
# the seed, which is printed after each run.
|
17
|
+
# --seed 1234
|
18
|
+
config.order = 'random'
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jugglite
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1.alpha
|
5
|
+
prerelease: 6
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- andruby
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-11-15 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: thin
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: em-hiredis
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: redis
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rspec
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
description: Lightweight SSE server
|
79
|
+
email:
|
80
|
+
- andrew@bedesign.be
|
81
|
+
executables:
|
82
|
+
- jugglite
|
83
|
+
extensions: []
|
84
|
+
extra_rdoc_files: []
|
85
|
+
files:
|
86
|
+
- .gitignore
|
87
|
+
- .rspec
|
88
|
+
- Gemfile
|
89
|
+
- LICENSE.txt
|
90
|
+
- README.md
|
91
|
+
- Rakefile
|
92
|
+
- bin/jugglite
|
93
|
+
- jugglite.gemspec
|
94
|
+
- lib/jugglite.rb
|
95
|
+
- lib/jugglite/app.rb
|
96
|
+
- lib/jugglite/deferrable_body.rb
|
97
|
+
- lib/jugglite/sse_connection.rb
|
98
|
+
- lib/jugglite/version.rb
|
99
|
+
- spec/acceptance/stream_and_publish_spec.rb
|
100
|
+
- spec/benchmark/max_connections.rb
|
101
|
+
- spec/spec_helper.rb
|
102
|
+
homepage: http://github.com/andruby/jugglite
|
103
|
+
licenses: []
|
104
|
+
post_install_message:
|
105
|
+
rdoc_options: []
|
106
|
+
require_paths:
|
107
|
+
- lib
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
109
|
+
none: false
|
110
|
+
requirements:
|
111
|
+
- - ! '>='
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
|
+
none: false
|
116
|
+
requirements:
|
117
|
+
- - ! '>'
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: 1.3.1
|
120
|
+
requirements: []
|
121
|
+
rubyforge_project:
|
122
|
+
rubygems_version: 1.8.23
|
123
|
+
signing_key:
|
124
|
+
specification_version: 3
|
125
|
+
summary: Server Sent Events server written in rack on top of thin inspired by Juggernaut
|
126
|
+
for real time push
|
127
|
+
test_files:
|
128
|
+
- spec/acceptance/stream_and_publish_spec.rb
|
129
|
+
- spec/benchmark/max_connections.rb
|
130
|
+
- spec/spec_helper.rb
|
131
|
+
has_rdoc:
|